diff --git a/packages/compiler-cli/ngcc/src/host/esm5_host.ts b/packages/compiler-cli/ngcc/src/host/esm5_host.ts index 0a893c4623..ca6b5ebaee 100644 --- a/packages/compiler-cli/ngcc/src/host/esm5_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm5_host.ts @@ -451,6 +451,41 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { const classDeclarationParent = classSymbol.valueDeclaration.parent; return ts.isBlock(classDeclarationParent) ? Array.from(classDeclarationParent.statements) : []; } + + /** + * Try to retrieve the symbol of a static property on a class. + * + * In ES5, a static property can either be set on the inner function declaration inside the class' + * IIFE, or it can be set on the outer variable declaration. Therefore, the ES5 host checks both + * places, first looking up the property on the inner symbol, and if the property is not found it + * will fall back to looking up the property on the outer symbol. + * + * @param symbol the class whose property we are interested in. + * @param propertyName the name of static property. + * @returns the symbol if it is found or `undefined` if not. + */ + protected getStaticProperty(symbol: ClassSymbol, propertyName: ts.__String): ts.Symbol|undefined { + // The symbol corresponds with the inner function declaration. First lets see if the static + // property is set there. + const prop = super.getStaticProperty(symbol, propertyName); + if (prop !== undefined) { + return prop; + } + + // Otherwise, obtain the outer variable declaration and resolve its symbol, in order to lookup + // static properties there. + const outerClass = getClassDeclarationFromInnerFunctionDeclaration(symbol.valueDeclaration); + if (outerClass === undefined) { + return undefined; + } + + const outerSymbol = this.checker.getSymbolAtLocation(outerClass.name); + if (outerSymbol === undefined || outerSymbol.valueDeclaration === undefined) { + return undefined; + } + + return super.getStaticProperty(outerSymbol as ClassSymbol, propertyName); + } } ///////////// Internal Helpers ///////////// diff --git a/packages/compiler-cli/ngcc/src/packages/bundle_program.ts b/packages/compiler-cli/ngcc/src/packages/bundle_program.ts index 97ba48e265..a1e34dde9c 100644 --- a/packages/compiler-cli/ngcc/src/packages/bundle_program.ts +++ b/packages/compiler-cli/ngcc/src/packages/bundle_program.ts @@ -9,6 +9,7 @@ import * as ts from 'typescript'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {FileSystem} from '../file_system/file_system'; +import {patchTsGetExpandoInitializer, restoreGetExpandoInitializer} from './patch_ts_expando_initializer'; /** * An entry point bundle contains one or two programs, e.g. `src` and `dts`, @@ -37,7 +38,11 @@ export function makeBundleProgram( const r3SymbolsPath = isCore ? findR3SymbolsPath(fs, AbsoluteFsPath.dirname(path), r3FileName) : null; const rootPaths = r3SymbolsPath ? [path, r3SymbolsPath] : [path]; + + const originalGetExpandoInitializer = patchTsGetExpandoInitializer(); const program = ts.createProgram(rootPaths, options, host); + restoreGetExpandoInitializer(originalGetExpandoInitializer); + const file = program.getSourceFile(path) !; const r3SymbolsFile = r3SymbolsPath && program.getSourceFile(r3SymbolsPath) || null; diff --git a/packages/compiler-cli/ngcc/src/packages/patch_ts_expando_initializer.ts b/packages/compiler-cli/ngcc/src/packages/patch_ts_expando_initializer.ts new file mode 100644 index 0000000000..0d6a20b039 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/packages/patch_ts_expando_initializer.ts @@ -0,0 +1,163 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as ts from 'typescript'; +import {hasNameIdentifier} from '../utils'; + +/** + * Consider the following ES5 code that may have been generated for a class: + * + * ``` + * var A = (function(){ + * function A() {} + * return A; + * }()); + * A.staticProp = true; + * ``` + * + * Here, TypeScript marks the symbol for "A" as a so-called "expando symbol", which causes + * "staticProp" to be added as an export of the "A" symbol. + * + * In the example above, symbol "A" has been assigned some flags to indicate that it represents a + * class. Due to this flag, the symbol is considered an expando symbol and as such, "staticProp" is + * stored in `ts.Symbol.exports`. + * + * A problem arises when "A" is not at the top-level, i.e. in UMD bundles. In that case, the symbol + * does not have the flag that marks the symbol as a class. Therefore, TypeScript inspects "A"'s + * initializer expression, which is an IIFE in the above example. Unfortunately however, only IIFEs + * of the form `(function(){})()` qualify as initializer for an "expando symbol"; the slightly + * different form seen in the example above, `(function(){}())`, does not. This prevents the "A" + * symbol from being considered an expando symbol, in turn preventing "staticProp" from being stored + * in `ts.Symbol.exports`. + * + * The logic for identifying symbols as "expando symbols" can be found here: + * https://github.com/microsoft/TypeScript/blob/v3.4.5/src/compiler/binder.ts#L2656-L2685 + * + * Notice how the `getExpandoInitializer` function is available on the "ts" namespace in the + * compiled bundle, so we are able to override this function to accommodate for the alternative + * IIFE notation. The original implementation can be found at: + * https://github.com/Microsoft/TypeScript/blob/v3.4.5/src/compiler/utilities.ts#L1864-L1887 + * + * Issue tracked in https://github.com/microsoft/TypeScript/issues/31778 + * + * @returns the function to pass to `restoreGetExpandoInitializer` to undo the patch, or null if + * the issue is known to have been fixed. + */ +export function patchTsGetExpandoInitializer(): unknown { + if (isTs31778GetExpandoInitializerFixed()) { + return null; + } + + const originalGetExpandoInitializer = (ts as any).getExpandoInitializer; + if (originalGetExpandoInitializer === undefined) { + throw makeUnsupportedTypeScriptError(); + } + + // Override the function to add support for recognizing the IIFE structure used in ES5 bundles. + (ts as any).getExpandoInitializer = + (initializer: ts.Node, isPrototypeAssignment: boolean): ts.Expression | undefined => { + // If the initializer is a call expression within parenthesis, unwrap the parenthesis + // upfront such that unsupported IIFE syntax `(function(){}())` becomes `function(){}()`, + // which is supported. + if (ts.isParenthesizedExpression(initializer) && + ts.isCallExpression(initializer.expression)) { + initializer = initializer.expression; + } + return originalGetExpandoInitializer(initializer, isPrototypeAssignment); + }; + return originalGetExpandoInitializer; +} + +export function restoreGetExpandoInitializer(originalGetExpandoInitializer: unknown): void { + if (originalGetExpandoInitializer !== null) { + (ts as any).getExpandoInitializer = originalGetExpandoInitializer; + } +} + +let ts31778FixedResult: boolean|null = null; + +function isTs31778GetExpandoInitializerFixed(): boolean { + // If the result has already been computed, return early. + if (ts31778FixedResult !== null) { + return ts31778FixedResult; + } + + // Determine if the issue has been fixed by checking if an expando property is present in a + // minimum reproduction using unpatched TypeScript. + ts31778FixedResult = checkIfExpandoPropertyIsPresent(); + + // If the issue does not appear to have been fixed, verify that applying the patch has the desired + // effect. + if (!ts31778FixedResult) { + const originalGetExpandoInitializer = patchTsGetExpandoInitializer(); + try { + const patchIsSuccessful = checkIfExpandoPropertyIsPresent(); + if (!patchIsSuccessful) { + throw makeUnsupportedTypeScriptError(); + } + } finally { + restoreGetExpandoInitializer(originalGetExpandoInitializer); + } + } + + return ts31778FixedResult; +} + +/** + * Verifies whether TS issue 31778 has been fixed by inspecting a symbol from a minimum + * reproduction. If the symbol does in fact have the "expando" as export, the issue has been fixed. + * + * See https://github.com/microsoft/TypeScript/issues/31778 for details. + */ +function checkIfExpandoPropertyIsPresent(): boolean { + const sourceText = ` + (function() { + var A = (function() { + function A() {} + return A; + }()); + A.expando = true; + }());`; + const sourceFile = + ts.createSourceFile('test.js', sourceText, ts.ScriptTarget.ES5, true, ts.ScriptKind.JS); + const host: ts.CompilerHost = { + getSourceFile(): ts.SourceFile | undefined{return sourceFile;}, + fileExists(): boolean{return true;}, + readFile(): string | undefined{return '';}, + writeFile() {}, + getDefaultLibFileName(): string{return '';}, + getCurrentDirectory(): string{return '';}, + getDirectories(): string[]{return [];}, + getCanonicalFileName(fileName: string): string{return fileName;}, + useCaseSensitiveFileNames(): boolean{return true;}, + getNewLine(): string{return '\n';}, + }; + const options = {noResolve: true, noLib: true, noEmit: true, allowJs: true}; + const program = ts.createProgram(['test.js'], options, host); + + function visitor(node: ts.Node): ts.VariableDeclaration|undefined { + if (ts.isVariableDeclaration(node) && hasNameIdentifier(node) && node.name.text === 'A') { + return node; + } + return ts.forEachChild(node, visitor); + } + + const declaration = ts.forEachChild(sourceFile, visitor); + if (declaration === undefined) { + throw new Error('Unable to find declaration of outer A'); + } + + const symbol = program.getTypeChecker().getSymbolAtLocation(declaration.name); + if (symbol === undefined) { + throw new Error('Unable to resolve symbol of outer A'); + } + return symbol.exports !== undefined && symbol.exports.has('expando' as ts.__String); +} + +function makeUnsupportedTypeScriptError(): Error { + return new Error('The TypeScript version used is not supported by ngcc.'); +} diff --git a/packages/compiler-cli/ngcc/test/helpers/utils.ts b/packages/compiler-cli/ngcc/test/helpers/utils.ts index 5463251b46..35bdeba4fc 100644 --- a/packages/compiler-cli/ngcc/test/helpers/utils.ts +++ b/packages/compiler-cli/ngcc/test/helpers/utils.ts @@ -12,6 +12,7 @@ import {makeProgram} from '../../../src/ngtsc/testing/in_memory_typescript'; import {BundleProgram} from '../../src/packages/bundle_program'; import {EntryPointFormat, EntryPointJsonProperty} from '../../src/packages/entry_point'; import {EntryPointBundle} from '../../src/packages/entry_point_bundle'; +import {patchTsGetExpandoInitializer, restoreGetExpandoInitializer} from '../../src/packages/patch_ts_expando_initializer'; import {Folder} from './mock_file_system'; export {getDeclaration} from '../../../src/ngtsc/testing/in_memory_typescript'; @@ -53,7 +54,11 @@ function makeTestProgramInternal( host: ts.CompilerHost, options: ts.CompilerOptions, } { - return makeProgram([getFakeCore(), getFakeTslib(), ...files], {allowJs: true, checkJs: false}); + const originalTsGetExpandoInitializer = patchTsGetExpandoInitializer(); + const program = + makeProgram([getFakeCore(), getFakeTslib(), ...files], {allowJs: true, checkJs: false}); + restoreGetExpandoInitializer(originalTsGetExpandoInitializer); + return program; } export function makeTestProgram( diff --git a/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts b/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts index 051d4f273d..ca9b492323 100644 --- a/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts @@ -53,6 +53,35 @@ exports.SomeDirective = SomeDirective; ` }; +const TOPLEVEL_DECORATORS_FILE = { + name: '/toplevel_decorators.cjs.js', + contents: ` +var core = require('@angular/core'); + +var INJECTED_TOKEN = new InjectionToken('injected'); +var ViewContainerRef = {}; +var TemplateRef = {}; + +var SomeDirective = (function() { + function SomeDirective(_viewContainer, _template, injected) {} + return SomeDirective; +}()); +SomeDirective.decorators = [ + { type: core.Directive, args: [{ selector: '[someDirective]' },] } +]; +SomeDirective.ctorParameters = function() { return [ + { type: ViewContainerRef, }, + { type: TemplateRef, }, + { type: undefined, decorators: [{ type: core.Inject, args: [INJECTED_TOKEN,] },] }, +]; }; +SomeDirective.propDecorators = { + "input1": [{ type: core.Input },], + "input2": [{ type: core.Input },], +}; +exports.SomeDirective = SomeDirective; +` +}; + const SIMPLE_ES2015_CLASS_FILE = { name: '/simple_es2015_class.d.ts', contents: ` @@ -757,6 +786,24 @@ describe('CommonJsReflectionHost', () => { ]); }); + it('should find the decorators on a class at the top level', () => { + const {program, host: compilerHost} = makeTestBundleProgram([TOPLEVEL_DECORATORS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, TOPLEVEL_DECORATORS_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators).toBeDefined(); + expect(decorators.length).toEqual(1); + + const decorator = decorators[0]; + expect(decorator.name).toEqual('Directive'); + expect(decorator.import).toEqual({name: 'Directive', from: '@angular/core'}); + expect(decorator.args !.map(arg => arg.getText())).toEqual([ + '{ selector: \'[someDirective]\' }', + ]); + }); + it('should return null if the symbol is not a class', () => { const {program, host: compilerHost} = makeTestBundleProgram([FOO_FUNCTION_FILE]); const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); @@ -895,6 +942,24 @@ describe('CommonJsReflectionHost', () => { expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); }); + it('should find decorated members on a class at the top level', () => { + const {program, host: compilerHost} = makeTestBundleProgram([TOPLEVEL_DECORATORS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, TOPLEVEL_DECORATORS_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const input1 = members.find(member => member.name === 'input1') !; + expect(input1.kind).toEqual(ClassMemberKind.Property); + expect(input1.isStatic).toEqual(false); + expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); + + const input2 = members.find(member => member.name === 'input2') !; + expect(input2.kind).toEqual(ClassMemberKind.Property); + expect(input2.isStatic).toEqual(false); + expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); + }); + it('should find non decorated properties on a class', () => { const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); @@ -1096,6 +1161,24 @@ describe('CommonJsReflectionHost', () => { ]); }); + it('should find the decorated constructor parameters at the top level', () => { + const {program, host: compilerHost} = makeTestBundleProgram([TOPLEVEL_DECORATORS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, TOPLEVEL_DECORATORS_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters).toBeDefined(); + expect(parameters !.map(parameter => parameter.name)).toEqual([ + '_viewContainer', '_template', 'injected' + ]); + expectTypeValueReferencesForParameters(parameters !, [ + 'ViewContainerRef', + 'TemplateRef', + null, + ]); + }); + it('should throw if the symbol is not a class', () => { const {program, host: compilerHost} = makeTestBundleProgram([FOO_FUNCTION_FILE]); const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); diff --git a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts index 32abcc6c7f..05a1adb824 100644 --- a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts @@ -50,6 +50,35 @@ const SOME_DIRECTIVE_FILE = { }()); `, }; + +const TOPLEVEL_DECORATORS_FILE = { + name: '/toplevel_decorators.js', + contents: ` + import { Directive, Inject, InjectionToken, Input } from '@angular/core'; + + var INJECTED_TOKEN = new InjectionToken('injected'); + var ViewContainerRef = {}; + var TemplateRef = {}; + + var SomeDirective = (function() { + function SomeDirective(_viewContainer, _template, injected) {} + return SomeDirective; + }()); + SomeDirective.decorators = [ + { type: Directive, args: [{ selector: '[someDirective]' },] } + ]; + SomeDirective.ctorParameters = function() { return [ + { type: ViewContainerRef, }, + { type: TemplateRef, }, + { type: undefined, decorators: [{ type: Inject, args: [INJECTED_TOKEN,] },] }, + ]; }; + SomeDirective.propDecorators = { + "input1": [{ type: Input },], + "input2": [{ type: Input },], + }; + `, +}; + const ACCESSORS_FILE = { name: '/accessors.js', contents: ` @@ -758,6 +787,24 @@ describe('Esm5ReflectionHost', () => { ]); }); + it('should find the decorators on a class at the top level', () => { + const program = makeTestProgram(TOPLEVEL_DECORATORS_FILE); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, TOPLEVEL_DECORATORS_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators).toBeDefined(); + expect(decorators.length).toEqual(1); + + const decorator = decorators[0]; + expect(decorator.name).toEqual('Directive'); + expect(decorator.import).toEqual({name: 'Directive', from: '@angular/core'}); + expect(decorator.args !.map(arg => arg.getText())).toEqual([ + '{ selector: \'[someDirective]\' }', + ]); + }); + it('should return null if the symbol is not a class', () => { const program = makeTestProgram(FOO_FUNCTION_FILE); const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); @@ -897,6 +944,24 @@ describe('Esm5ReflectionHost', () => { expect(input2.decorators !.map(d => d.name)).toEqual(['Input']); }); + it('should find decorated members on a class at the top level', () => { + const program = makeTestProgram(TOPLEVEL_DECORATORS_FILE); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, TOPLEVEL_DECORATORS_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const input1 = members.find(member => member.name === 'input1') !; + expect(input1.kind).toEqual(ClassMemberKind.Property); + expect(input1.isStatic).toEqual(false); + expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); + + const input2 = members.find(member => member.name === 'input2') !; + expect(input2.kind).toEqual(ClassMemberKind.Property); + expect(input2.isStatic).toEqual(false); + expect(input2.decorators !.map(d => d.name)).toEqual(['Input']); + }); + it('should find Object.defineProperty members on a class', () => { const program = makeTestProgram(ACCESSORS_FILE); const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); @@ -1156,6 +1221,24 @@ describe('Esm5ReflectionHost', () => { ]); }); + it('should find the decorated constructor parameters at the top level', () => { + const program = makeTestProgram(TOPLEVEL_DECORATORS_FILE); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, TOPLEVEL_DECORATORS_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters).toBeDefined(); + expect(parameters !.map(parameter => parameter.name)).toEqual([ + '_viewContainer', '_template', 'injected' + ]); + expectTypeValueReferencesForParameters(parameters !, [ + 'ViewContainerRef', + 'TemplateRef', + null, + ]); + }); + it('should throw if the symbol is not a class', () => { const program = makeTestProgram(FOO_FUNCTION_FILE); const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); diff --git a/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts b/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts index 85c4962902..deb5057a0e 100644 --- a/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts @@ -57,6 +57,39 @@ const SOME_DIRECTIVE_FILE = { })));`, }; +const TOPLEVEL_DECORATORS_FILE = { + name: '/toplevel_decorators.umd.js', + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : + typeof define === 'function' && define.amd ? define('some_directive', ['exports', '@angular/core'], factory) : + (factory(global.some_directive,global.ng.core)); +}(this, (function (exports,core) { 'use strict'; + + var INJECTED_TOKEN = new InjectionToken('injected'); + var ViewContainerRef = {}; + var TemplateRef = {}; + + var SomeDirective = (function() { + function SomeDirective(_viewContainer, _template, injected) {} + return SomeDirective; + }()); + SomeDirective.decorators = [ + { type: core.Directive, args: [{ selector: '[someDirective]' },] } + ]; + SomeDirective.ctorParameters = function() { return [ + { type: ViewContainerRef, }, + { type: TemplateRef, }, + { type: undefined, decorators: [{ type: core.Inject, args: [INJECTED_TOKEN,] },] }, + ]; }; + SomeDirective.propDecorators = { + "input1": [{ type: core.Input },], + "input2": [{ type: core.Input },], + }; + exports.SomeDirective = SomeDirective; +})));`, +}; + const SIMPLE_ES2015_CLASS_FILE = { name: '/simple_es2015_class.d.ts', contents: ` @@ -864,6 +897,24 @@ describe('UmdReflectionHost', () => { ]); }); + it('should find the decorators on a class at the top level', () => { + const {program, host: compilerHost} = makeTestBundleProgram([TOPLEVEL_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, TOPLEVEL_DECORATORS_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators).toBeDefined(); + expect(decorators.length).toEqual(1); + + const decorator = decorators[0]; + expect(decorator.name).toEqual('Directive'); + expect(decorator.import).toEqual({name: 'Directive', from: '@angular/core'}); + expect(decorator.args !.map(arg => arg.getText())).toEqual([ + '{ selector: \'[someDirective]\' }', + ]); + }); + it('should return null if the symbol is not a class', () => { const {program, host: compilerHost} = makeTestBundleProgram([FOO_FUNCTION_FILE]); const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); @@ -1002,6 +1053,24 @@ describe('UmdReflectionHost', () => { expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); }); + it('should find decorated members on a class at the top level', () => { + const {program, host: compilerHost} = makeTestBundleProgram([TOPLEVEL_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, TOPLEVEL_DECORATORS_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const input1 = members.find(member => member.name === 'input1') !; + expect(input1.kind).toEqual(ClassMemberKind.Property); + expect(input1.isStatic).toEqual(false); + expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); + + const input2 = members.find(member => member.name === 'input2') !; + expect(input2.kind).toEqual(ClassMemberKind.Property); + expect(input2.isStatic).toEqual(false); + expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); + }); + it('should find non decorated properties on a class', () => { const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); @@ -1203,6 +1272,24 @@ describe('UmdReflectionHost', () => { ]); }); + it('should find the decorated constructor parameters at the top level', () => { + const {program, host: compilerHost} = makeTestBundleProgram([TOPLEVEL_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, TOPLEVEL_DECORATORS_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters).toBeDefined(); + expect(parameters !.map(parameter => parameter.name)).toEqual([ + '_viewContainer', '_template', 'injected' + ]); + expectTypeValueReferencesForParameters(parameters !, [ + 'ViewContainerRef', + 'TemplateRef', + null, + ]); + }); + it('should throw if the symbol is not a class', () => { const {program, host: compilerHost} = makeTestBundleProgram([FOO_FUNCTION_FILE]); const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost);