feat(ivy): ngcc - recognize static properties on the outer symbol in ES5 (#30795)
Packages that have been compiled using an older version of TypeScript can have their decorators at the top-level of the ES5 bundles, instead of inside the IIFE that is emitted for the class. Before this change, ngcc only took static property assignments inside the IIFE into account, therefore missing the decorators that were assigned at the top-level. This commit extends the ES5 host to look for static properties in two places. Testcases for all bundle formats that contain ES5 have been added to ensure that this works in the various flavours. A patch is included to support UMD bundles. The UMD factory affects how TypeScripts binds the static properties to symbols, see the docblock of the patch function for more details. PR Close #30795
This commit is contained in:
parent
16aa6ceff8
commit
6fbfb5a159
|
@ -451,6 +451,41 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
|
||||||
const classDeclarationParent = classSymbol.valueDeclaration.parent;
|
const classDeclarationParent = classSymbol.valueDeclaration.parent;
|
||||||
return ts.isBlock(classDeclarationParent) ? Array.from(classDeclarationParent.statements) : [];
|
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 /////////////
|
///////////// Internal Helpers /////////////
|
||||||
|
|
|
@ -9,6 +9,7 @@ import * as ts from 'typescript';
|
||||||
|
|
||||||
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
|
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
|
||||||
import {FileSystem} from '../file_system/file_system';
|
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`,
|
* An entry point bundle contains one or two programs, e.g. `src` and `dts`,
|
||||||
|
@ -37,7 +38,11 @@ export function makeBundleProgram(
|
||||||
const r3SymbolsPath =
|
const r3SymbolsPath =
|
||||||
isCore ? findR3SymbolsPath(fs, AbsoluteFsPath.dirname(path), r3FileName) : null;
|
isCore ? findR3SymbolsPath(fs, AbsoluteFsPath.dirname(path), r3FileName) : null;
|
||||||
const rootPaths = r3SymbolsPath ? [path, r3SymbolsPath] : [path];
|
const rootPaths = r3SymbolsPath ? [path, r3SymbolsPath] : [path];
|
||||||
|
|
||||||
|
const originalGetExpandoInitializer = patchTsGetExpandoInitializer();
|
||||||
const program = ts.createProgram(rootPaths, options, host);
|
const program = ts.createProgram(rootPaths, options, host);
|
||||||
|
restoreGetExpandoInitializer(originalGetExpandoInitializer);
|
||||||
|
|
||||||
const file = program.getSourceFile(path) !;
|
const file = program.getSourceFile(path) !;
|
||||||
const r3SymbolsFile = r3SymbolsPath && program.getSourceFile(r3SymbolsPath) || null;
|
const r3SymbolsFile = r3SymbolsPath && program.getSourceFile(r3SymbolsPath) || null;
|
||||||
|
|
||||||
|
|
|
@ -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.');
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import {makeProgram} from '../../../src/ngtsc/testing/in_memory_typescript';
|
||||||
import {BundleProgram} from '../../src/packages/bundle_program';
|
import {BundleProgram} from '../../src/packages/bundle_program';
|
||||||
import {EntryPointFormat, EntryPointJsonProperty} from '../../src/packages/entry_point';
|
import {EntryPointFormat, EntryPointJsonProperty} from '../../src/packages/entry_point';
|
||||||
import {EntryPointBundle} from '../../src/packages/entry_point_bundle';
|
import {EntryPointBundle} from '../../src/packages/entry_point_bundle';
|
||||||
|
import {patchTsGetExpandoInitializer, restoreGetExpandoInitializer} from '../../src/packages/patch_ts_expando_initializer';
|
||||||
import {Folder} from './mock_file_system';
|
import {Folder} from './mock_file_system';
|
||||||
|
|
||||||
export {getDeclaration} from '../../../src/ngtsc/testing/in_memory_typescript';
|
export {getDeclaration} from '../../../src/ngtsc/testing/in_memory_typescript';
|
||||||
|
@ -53,7 +54,11 @@ function makeTestProgramInternal(
|
||||||
host: ts.CompilerHost,
|
host: ts.CompilerHost,
|
||||||
options: ts.CompilerOptions,
|
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(
|
export function makeTestProgram(
|
||||||
|
|
|
@ -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 = {
|
const SIMPLE_ES2015_CLASS_FILE = {
|
||||||
name: '/simple_es2015_class.d.ts',
|
name: '/simple_es2015_class.d.ts',
|
||||||
contents: `
|
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', () => {
|
it('should return null if the symbol is not a class', () => {
|
||||||
const {program, host: compilerHost} = makeTestBundleProgram([FOO_FUNCTION_FILE]);
|
const {program, host: compilerHost} = makeTestBundleProgram([FOO_FUNCTION_FILE]);
|
||||||
const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost);
|
const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost);
|
||||||
|
@ -895,6 +942,24 @@ describe('CommonJsReflectionHost', () => {
|
||||||
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']);
|
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', () => {
|
it('should find non decorated properties on a class', () => {
|
||||||
const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]);
|
const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]);
|
||||||
const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost);
|
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', () => {
|
it('should throw if the symbol is not a class', () => {
|
||||||
const {program, host: compilerHost} = makeTestBundleProgram([FOO_FUNCTION_FILE]);
|
const {program, host: compilerHost} = makeTestBundleProgram([FOO_FUNCTION_FILE]);
|
||||||
const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost);
|
const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost);
|
||||||
|
|
|
@ -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 = {
|
const ACCESSORS_FILE = {
|
||||||
name: '/accessors.js',
|
name: '/accessors.js',
|
||||||
contents: `
|
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', () => {
|
it('should return null if the symbol is not a class', () => {
|
||||||
const program = makeTestProgram(FOO_FUNCTION_FILE);
|
const program = makeTestProgram(FOO_FUNCTION_FILE);
|
||||||
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
|
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
|
||||||
|
@ -897,6 +944,24 @@ describe('Esm5ReflectionHost', () => {
|
||||||
expect(input2.decorators !.map(d => d.name)).toEqual(['Input']);
|
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', () => {
|
it('should find Object.defineProperty members on a class', () => {
|
||||||
const program = makeTestProgram(ACCESSORS_FILE);
|
const program = makeTestProgram(ACCESSORS_FILE);
|
||||||
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
|
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', () => {
|
it('should throw if the symbol is not a class', () => {
|
||||||
const program = makeTestProgram(FOO_FUNCTION_FILE);
|
const program = makeTestProgram(FOO_FUNCTION_FILE);
|
||||||
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
|
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
|
||||||
|
|
|
@ -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 = {
|
const SIMPLE_ES2015_CLASS_FILE = {
|
||||||
name: '/simple_es2015_class.d.ts',
|
name: '/simple_es2015_class.d.ts',
|
||||||
contents: `
|
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', () => {
|
it('should return null if the symbol is not a class', () => {
|
||||||
const {program, host: compilerHost} = makeTestBundleProgram([FOO_FUNCTION_FILE]);
|
const {program, host: compilerHost} = makeTestBundleProgram([FOO_FUNCTION_FILE]);
|
||||||
const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost);
|
const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost);
|
||||||
|
@ -1002,6 +1053,24 @@ describe('UmdReflectionHost', () => {
|
||||||
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']);
|
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', () => {
|
it('should find non decorated properties on a class', () => {
|
||||||
const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]);
|
const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]);
|
||||||
const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost);
|
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', () => {
|
it('should throw if the symbol is not a class', () => {
|
||||||
const {program, host: compilerHost} = makeTestBundleProgram([FOO_FUNCTION_FILE]);
|
const {program, host: compilerHost} = makeTestBundleProgram([FOO_FUNCTION_FILE]);
|
||||||
const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost);
|
const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost);
|
||||||
|
|
Loading…
Reference in New Issue