diff --git a/integration/ngcc/package.json b/integration/ngcc/package.json new file mode 100644 index 000000000000..156054a557ee --- /dev/null +++ b/integration/ngcc/package.json @@ -0,0 +1,31 @@ +{ + "name": "angular-integration", + "version": "0.0.0", + "license": "MIT", + "dependencies": { + "@angular/animations": "file:../../dist/packages-dist/animations", + "@angular/common": "file:../../dist/packages-dist/common", + "@angular/compiler": "file:../../dist/packages-dist/compiler", + "@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli", + "@angular/core": "file:../../dist/packages-dist/core", + "@angular/forms": "file:../../dist/packages-dist/forms", + "@angular/http": "file:../../dist/packages-dist/http", + "@angular/platform-browser": "file:../../dist/packages-dist/platform-browser", + "@angular/platform-browser-dynamic": "file:../../dist/packages-dist/platform-browser-dynamic", + "@angular/platform-server": "file:../../dist/packages-dist/platform-server", + "@angular/router": "file:../../dist/packages-dist/router", + "@types/node": "^9.4.0", + "rxjs": "file:../../node_modules/rxjs", + "typescript": "file:../../node_modules/typescript", + "zone.js": "file:../../node_modules/zone.js" + }, + "devDependencies": { + "@types/jasmine": "2.5.41", + "concurrently": "3.4.0", + "lite-server": "2.2.2", + "protractor": "file:../../node_modules/protractor" + }, + "scripts": { + "test": "./test.sh" + } +} diff --git a/integration/ngcc/src/main.ts b/integration/ngcc/src/main.ts new file mode 100644 index 000000000000..9ec01929571e --- /dev/null +++ b/integration/ngcc/src/main.ts @@ -0,0 +1,20 @@ +import {Component, NgModule, ɵrenderComponent as renderComponent} from '@angular/core'; +import {CommonModule} from '@angular/common'; +@Component({ + selector: 'hello-world', + template: ` + +

Hello World

+ `, +}) +class HelloWorld { + visible = false; +} + +@NgModule({ + declarations: [HelloWorld], + imports: [CommonModule], +}) +class Module {} + +renderComponent(HelloWorld); diff --git a/integration/ngcc/test.sh b/integration/ngcc/test.sh new file mode 100755 index 000000000000..30ddbef9f7f6 --- /dev/null +++ b/integration/ngcc/test.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e -x + +PATH=$PATH:$(npm bin) + +ivy-ngcc fesm2015,esm2015 +ngc -p tsconfig-app.json + +# Look for correct output +grep "directives: \[\S*\.NgIf\]" dist/src/main.js > /dev/null diff --git a/integration/ngcc/tsconfig-app.json b/integration/ngcc/tsconfig-app.json new file mode 100644 index 000000000000..ab1b05c33ff5 --- /dev/null +++ b/integration/ngcc/tsconfig-app.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "moduleResolution": "node", + "lib": ["es2015", "dom"], + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "outDir": "dist", + "types": ["node"], + "rootDir": "." + }, + "files": [ + "src/main.ts" + ], + "angularCompilerOptions": { + "enableIvy": "ngtsc" + } +} \ No newline at end of file diff --git a/packages/compiler-cli/src/metadata/bundler.ts b/packages/compiler-cli/src/metadata/bundler.ts index d071bc7768b0..7eb0c56d1652 100644 --- a/packages/compiler-cli/src/metadata/bundler.ts +++ b/packages/compiler-cli/src/metadata/bundler.ts @@ -8,10 +8,11 @@ import * as path from 'path'; import * as ts from 'typescript'; -import {MetadataCollector} from '../metadata/collector'; -import {ClassMetadata, ConstructorMetadata, FunctionMetadata, METADATA_VERSION, MemberMetadata, MetadataEntry, MetadataError, MetadataImportedSymbolReferenceExpression, MetadataMap, MetadataObject, MetadataSymbolicExpression, MetadataSymbolicReferenceExpression, MetadataValue, MethodMetadata, ModuleExportMetadata, ModuleMetadata, isClassMetadata, isConstructorMetadata, isFunctionMetadata, isInterfaceMetadata, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataImportedSymbolReferenceExpression, isMetadataModuleReferenceExpression, isMetadataSymbolicExpression, isMethodMetadata} from '../metadata/schema'; import {MetadataCache} from '../transformers/metadata_cache'; +import {MetadataCollector} from './collector'; +import {ClassMetadata, ConstructorMetadata, FunctionMetadata, METADATA_VERSION, MemberMetadata, MetadataEntry, MetadataError, MetadataImportedSymbolReferenceExpression, MetadataMap, MetadataObject, MetadataSymbolicExpression, MetadataSymbolicReferenceExpression, MetadataValue, MethodMetadata, ModuleExportMetadata, ModuleMetadata, isClassMetadata, isConstructorMetadata, isFunctionMetadata, isInterfaceMetadata, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataImportedSymbolReferenceExpression, isMetadataModuleReferenceExpression, isMetadataSymbolicExpression, isMethodMetadata} from './schema'; + // The character set used to produce private names. diff --git a/packages/compiler-cli/src/ngcc/src/analyzer.ts b/packages/compiler-cli/src/ngcc/src/analyzer.ts index f7be4aa09246..f55a6e6948b6 100644 --- a/packages/compiler-cli/src/ngcc/src/analyzer.ts +++ b/packages/compiler-cli/src/ngcc/src/analyzer.ts @@ -66,7 +66,7 @@ export class Analyzer { analyzeFile(file: ParsedFile): AnalyzedFile { const constantPool = new ConstantPool(); const analyzedClasses = - file.decoratedClasses.map(clazz => this.analyzeClass(file.sourceFile, constantPool, clazz)) + file.decoratedClasses.map(clazz => this.analyzeClass(constantPool, clazz)) .filter(isDefined); return { @@ -75,8 +75,7 @@ export class Analyzer { }; } - protected analyzeClass(file: ts.SourceFile, pool: ConstantPool, clazz: ParsedClass): AnalyzedClass - |undefined { + protected analyzeClass(pool: ConstantPool, clazz: ParsedClass): AnalyzedClass|undefined { const matchingHandlers = this.handlers .map(handler => ({ handler, diff --git a/packages/compiler-cli/src/ngcc/src/constants.ts b/packages/compiler-cli/src/ngcc/src/constants.ts new file mode 100644 index 000000000000..e73bb7df0f93 --- /dev/null +++ b/packages/compiler-cli/src/ngcc/src/constants.ts @@ -0,0 +1,9 @@ +/** + * @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 + */ + +export const IMPORT_PREFIX = 'ɵngcc'; diff --git a/packages/compiler-cli/src/ngcc/src/host/dts_mapper.ts b/packages/compiler-cli/src/ngcc/src/host/dts_mapper.ts new file mode 100644 index 000000000000..17a755653f7d --- /dev/null +++ b/packages/compiler-cli/src/ngcc/src/host/dts_mapper.ts @@ -0,0 +1,32 @@ +/** + * @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 {relative, resolve} from 'path'; + +/** + * Map source files to their associated typings definitions files. + */ +export class DtsMapper { + constructor(private sourceRoot: string, private dtsRoot: string) {} + + /** + * Given the absolute path to a source file, return the absolute path to the corresponding `.d.ts` + * file. Assume that source files and `.d.ts` files have the same directory layout and the names + * of the `.d.ts` files can be derived by replacing the `.js` extension of the source file with + * `.d.ts`. + * + * @param sourceFileName The absolute path to the source file whose corresponding `.d.ts` file + * should be returned. + * + * @returns The absolute path to the `.d.ts` file that corresponds to the specified source file. + */ + getDtsFileNameFor(sourceFileName: string): string { + const relativeSourcePath = relative(this.sourceRoot, sourceFileName); + return resolve(this.dtsRoot, relativeSourcePath).replace(/\.js$/, '.d.ts'); + } +} diff --git a/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts b/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts index a2dff5d7df03..7c68d0cccdd6 100644 --- a/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts +++ b/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts @@ -6,420 +6,39 @@ * found in the LICENSE file at https://angular.io/license */ +import {readFileSync} from 'fs'; import * as ts from 'typescript'; -import {ClassMember, ClassMemberKind, Decorator, Parameter} from '../../../ngtsc/host'; -import {TypeScriptReflectionHost, reflectObjectLiteral} from '../../../ngtsc/metadata'; -import {getNameText} from '../utils'; -import {NgccReflectionHost} from './ngcc_host'; +import {DtsMapper} from './dts_mapper'; +import {Fesm2015ReflectionHost} from './fesm2015_host'; -export const DECORATORS = 'decorators' as ts.__String; -export const PROP_DECORATORS = 'propDecorators' as ts.__String; -export const CONSTRUCTOR = '__constructor' as ts.__String; -export const CONSTRUCTOR_PARAMS = 'ctorParameters' as ts.__String; - -/** - * Esm2015 packages contain ECMAScript 2015 classes, etc. - * Decorators are defined via static properties on the class. For example: - * - * ``` - * class SomeDirective { - * } - * SomeDirective.decorators = [ - * { type: Directive, args: [{ selector: '[someDirective]' },] } - * ]; - * SomeDirective.ctorParameters = () => [ - * { type: ViewContainerRef, }, - * { type: TemplateRef, }, - * { type: undefined, decorators: [{ type: Inject, args: [INJECTED_TOKEN,] },] }, - * ]; - * SomeDirective.propDecorators = { - * "input1": [{ type: Input },], - * "input2": [{ type: Input },], - * }; - * ``` - * - * * Classes are decorated if they have a static property called `decorators`. - * * Members are decorated if there is a matching key on a static property - * called `propDecorators`. - * * Constructor parameters decorators are found on an object returned from - * a static method called `ctorParameters`. - */ -export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements NgccReflectionHost { - constructor(checker: ts.TypeChecker) { super(checker); } +export class Esm2015ReflectionHost extends Fesm2015ReflectionHost { + constructor(checker: ts.TypeChecker, protected dtsMapper: DtsMapper) { super(checker); } /** - * Examine a declaration (for example, of a class or function) and return metadata about any - * decorators present on the declaration. - * - * @param declaration a TypeScript `ts.Declaration` node representing the class or function over - * which to reflect. For example, if the intent is to reflect the decorators of a class and the - * source is in ES6 format, this will be a `ts.ClassDeclaration` node. If the source is in ES5 - * format, this might be a `ts.VariableDeclaration` as classes in ES5 are represented as the - * result of an IIFE execution. + * Get the number of generic type parameters of a given class. * - * @returns an array of `Decorator` metadata if decorators are present on the declaration, or - * `null` if either no decorators were present or if the declaration is not of a decoratable type. + * @returns the number of type parameters of the class, if known, or `null` if the declaration + * is not a class or has an unknown number of type parameters. */ - getDecoratorsOfDeclaration(declaration: ts.Declaration): Decorator[]|null { - const symbol = this.getClassSymbol(declaration); - if (symbol) { - if (symbol.exports && symbol.exports.has(DECORATORS)) { - // Symbol of the identifier for `SomeDirective.decorators`. - const decoratorsSymbol = symbol.exports.get(DECORATORS) !; - const decoratorsIdentifier = decoratorsSymbol.valueDeclaration; + getGenericArityOfClass(clazz: ts.Declaration): number|null { + if (ts.isClassDeclaration(clazz) && clazz.name) { + const sourcePath = clazz.getSourceFile(); + const dtsPath = this.dtsMapper.getDtsFileNameFor(sourcePath.fileName); + const dtsContents = readFileSync(dtsPath, 'utf8'); + // TODO: investigate caching parsed .d.ts files as they're needed for several different + // purposes in ngcc. + const dtsFile = ts.createSourceFile( + dtsPath, dtsContents, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS); - if (decoratorsIdentifier && decoratorsIdentifier.parent) { - if (ts.isBinaryExpression(decoratorsIdentifier.parent)) { - // AST of the array of decorator values - const decoratorsArray = decoratorsIdentifier.parent.right; - return this.reflectDecorators(decoratorsArray); - } + for (let i = dtsFile.statements.length - 1; i >= 0; i--) { + const stmt = dtsFile.statements[i]; + if (ts.isClassDeclaration(stmt) && stmt.name !== undefined && + stmt.name.text === clazz.name.text) { + return stmt.typeParameters ? stmt.typeParameters.length : 0; } } } return null; } - - /** - * Examine a declaration which should be of a class, and return metadata about the members of the - * class. - * - * @param declaration a TypeScript `ts.Declaration` node representing the class over which to - * reflect. If the source is in ES6 format, this will be a `ts.ClassDeclaration` node. If the - * source is in ES5 format, this might be a `ts.VariableDeclaration` as classes in ES5 are - * represented as the result of an IIFE execution. - * - * @returns an array of `ClassMember` metadata representing the members of the class. - * - * @throws if `declaration` does not resolve to a class declaration. - */ - getMembersOfClass(clazz: ts.Declaration): ClassMember[] { - const members: ClassMember[] = []; - const symbol = this.getClassSymbol(clazz); - if (!symbol) { - throw new Error(`Attempted to get members of a non-class: "${clazz.getText()}"`); - } - - // The decorators map contains all the properties that are decorated - const decoratorsMap = this.getMemberDecorators(symbol); - - // The member map contains all the method (instance and static); and any instance properties - // that are initialized in the class. - if (symbol.members) { - symbol.members.forEach((value, key) => { - const decorators = removeFromMap(decoratorsMap, key); - const member = this.reflectMember(value, decorators); - if (member) { - members.push(member); - } - }); - } - - // The static property map contains all the static properties - if (symbol.exports) { - symbol.exports.forEach((value, key) => { - const decorators = removeFromMap(decoratorsMap, key); - const member = this.reflectMember(value, decorators, true); - if (member) { - members.push(member); - } - }); - } - - // Deal with any decorated properties that were not initialized in the class - decoratorsMap.forEach((value, key) => { - members.push({ - implementation: null, - decorators: value, - isStatic: false, - kind: ClassMemberKind.Property, - name: key, - nameNode: null, - node: null, - type: null, - value: null - }); - }); - - return members; - } - - /** - * Reflect over the constructor of a class and return metadata about its parameters. - * - * This method only looks at the constructor of a class directly and not at any inherited - * constructors. - * - * @param declaration a TypeScript `ts.Declaration` node representing the class over which to - * reflect. If the source is in ES6 format, this will be a `ts.ClassDeclaration` node. If the - * source is in ES5 format, this might be a `ts.VariableDeclaration` as classes in ES5 are - * represented as the result of an IIFE execution. - * - * @returns an array of `Parameter` metadata representing the parameters of the constructor, if - * a constructor exists. If the constructor exists and has 0 parameters, this array will be empty. - * If the class has no constructor, this method returns `null`. - * - * @throws if `declaration` does not resolve to a class declaration. - */ - getConstructorParameters(clazz: ts.Declaration): Parameter[]|null { - const classSymbol = this.getClassSymbol(clazz); - if (!classSymbol) { - throw new Error( - `Attempted to get constructor parameters of a non-class: "${clazz.getText()}"`); - } - const parameterNodes = this.getConstructorParameterDeclarations(classSymbol); - if (parameterNodes) { - const parameters: Parameter[] = []; - const decoratorInfo = this.getConstructorDecorators(classSymbol); - parameterNodes.forEach((node, index) => { - const info = decoratorInfo[index]; - const decorators = - info && info.has('decorators') && this.reflectDecorators(info.get('decorators') !) || - null; - const type = info && info.get('type') || null; - const nameNode = node.name; - parameters.push({name: getNameText(nameNode), nameNode, type, decorators}); - }); - return parameters; - } - return null; - } - - /** - * Find a symbol for a declaration that we think is a class. - * @param declaration The declaration whose symbol we are finding - * @returns the symbol for the declaration or `undefined` if it is not - * a "class" or has no symbol. - */ - getClassSymbol(declaration: ts.Declaration): ts.Symbol|undefined { - return ts.isClassDeclaration(declaration) ? - declaration.name && this.checker.getSymbolAtLocation(declaration.name) : - undefined; - } - - /** - * Member decorators are declared as static properties of the class in ES2015: - * - * ``` - * SomeDirective.propDecorators = { - * "ngForOf": [{ type: Input },], - * "ngForTrackBy": [{ type: Input },], - * "ngForTemplate": [{ type: Input },], - * }; - * ``` - */ - protected getMemberDecorators(classSymbol: ts.Symbol): Map { - const memberDecorators = new Map(); - if (classSymbol.exports && classSymbol.exports.has(PROP_DECORATORS)) { - // Symbol of the identifier for `SomeDirective.propDecorators`. - const propDecoratorsMap = - getPropertyValueFromSymbol(classSymbol.exports.get(PROP_DECORATORS) !); - if (propDecoratorsMap && ts.isObjectLiteralExpression(propDecoratorsMap)) { - const propertiesMap = reflectObjectLiteral(propDecoratorsMap); - propertiesMap.forEach( - (value, name) => { memberDecorators.set(name, this.reflectDecorators(value)); }); - } - } - return memberDecorators; - } - - /** - * Reflect over the given expression and extract decorator information. - * @param decoratorsArray An expression that contains decorator information. - */ - protected reflectDecorators(decoratorsArray: ts.Expression): Decorator[] { - const decorators: Decorator[] = []; - - if (ts.isArrayLiteralExpression(decoratorsArray)) { - // Add each decorator that is imported from `@angular/core` into the `decorators` array - decoratorsArray.elements.forEach(node => { - - // If the decorator is not an object literal expression then we are not interested - if (ts.isObjectLiteralExpression(node)) { - // We are only interested in objects of the form: `{ type: DecoratorType, args: [...] }` - const decorator = reflectObjectLiteral(node); - - // Is the value of the `type` property an identifier? - const typeIdentifier = decorator.get('type'); - if (typeIdentifier && ts.isIdentifier(typeIdentifier)) { - decorators.push({ - name: typeIdentifier.text, - import: this.getImportOfIdentifier(typeIdentifier), node, - args: getDecoratorArgs(node), - }); - } - } - }); - } - return decorators; - } - - protected reflectMember(symbol: ts.Symbol, decorators?: Decorator[], isStatic?: boolean): - ClassMember|null { - let kind: ClassMemberKind|null = null; - let value: ts.Expression|null = null; - let name: string|null = null; - let nameNode: ts.Identifier|null = null; - let type = null; - - - const node = symbol.valueDeclaration || symbol.declarations && symbol.declarations[0]; - if (!node || !isClassMemberType(node)) { - return null; - } - - if (symbol.flags & ts.SymbolFlags.Method) { - kind = ClassMemberKind.Method; - } else if (symbol.flags & ts.SymbolFlags.Property) { - kind = ClassMemberKind.Property; - } else if (symbol.flags & ts.SymbolFlags.GetAccessor) { - kind = ClassMemberKind.Getter; - } else if (symbol.flags & ts.SymbolFlags.SetAccessor) { - kind = ClassMemberKind.Setter; - } - - if (isStatic && isPropertyAccess(node)) { - name = node.name.text; - value = symbol.flags & ts.SymbolFlags.Property ? node.parent.right : null; - } else if (isThisAssignment(node)) { - kind = ClassMemberKind.Property; - name = node.left.name.text; - value = node.right; - isStatic = false; - } else if (ts.isConstructorDeclaration(node)) { - kind = ClassMemberKind.Constructor; - name = 'constructor'; - isStatic = false; - } - - if (kind === null) { - console.warn(`Unknown member type: "${node.getText()}`); - return null; - } - - if (!name) { - if (isNamedDeclaration(node) && node.name && ts.isIdentifier(node.name)) { - name = node.name.text; - nameNode = node.name; - } else { - return null; - } - } - - // If we have still not determined if this is a static or instance member then - // look for the `static` keyword on the declaration - if (isStatic === undefined) { - isStatic = node.modifiers !== undefined && - node.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword); - } - - return { - node, - implementation: node, kind, type, name, nameNode, value, isStatic, - decorators: decorators || [] - }; - } - - /** - * Find the declarations of the constructor parameters of a class identified by its symbol. - * @param classSymbol the class whose parameters we want to find. - * @returns an array of `ts.ParameterDeclaration` objects representing each of the parameters in - * the - * class's constructor or null if there is no constructor. - */ - protected getConstructorParameterDeclarations(classSymbol: ts.Symbol): - ts.ParameterDeclaration[]|null { - const constructorSymbol = classSymbol.members && classSymbol.members.get(CONSTRUCTOR); - if (constructorSymbol) { - // For some reason the constructor does not have a `valueDeclaration` ?!? - const constructor = constructorSymbol.declarations && - constructorSymbol.declarations[0] as ts.ConstructorDeclaration; - if (constructor && constructor.parameters) { - return Array.from(constructor.parameters); - } - return []; - } - return null; - } - - /** - * Constructors parameter decorators are declared in the body of static method of the class in - * ES2015: - * - * ``` - * SomeDirective.ctorParameters = () => [ - * { type: ViewContainerRef, }, - * { type: TemplateRef, }, - * { type: IterableDiffers, }, - * { type: undefined, decorators: [{ type: Inject, args: [INJECTED_TOKEN,] },] }, - * ]; - * ``` - */ - protected getConstructorDecorators(classSymbol: ts.Symbol): (Map|null)[] { - if (classSymbol.exports && classSymbol.exports.has(CONSTRUCTOR_PARAMS)) { - const paramDecoratorsProperty = - getPropertyValueFromSymbol(classSymbol.exports.get(CONSTRUCTOR_PARAMS) !); - if (paramDecoratorsProperty && ts.isArrowFunction(paramDecoratorsProperty)) { - if (ts.isArrayLiteralExpression(paramDecoratorsProperty.body)) { - return paramDecoratorsProperty.body.elements.map( - element => - ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null); - } - } - } - return []; - } -} - -/** - * The arguments of a decorator are held in the `args` property of its declaration object. - */ -function getDecoratorArgs(node: ts.ObjectLiteralExpression): ts.Expression[] { - const argsProperty = node.properties.filter(ts.isPropertyAssignment) - .find(property => getNameText(property.name) === 'args'); - const argsExpression = argsProperty && argsProperty.initializer; - return argsExpression && ts.isArrayLiteralExpression(argsExpression) ? - Array.from(argsExpression.elements) : - []; -} - -/** - * Helper method to extract the value of a property given the property's "symbol", - * which is actually the symbol of the identifier of the property. - */ -export function getPropertyValueFromSymbol(propSymbol: ts.Symbol): ts.Expression|undefined { - const propIdentifier = propSymbol.valueDeclaration; - const parent = propIdentifier && propIdentifier.parent; - return parent && ts.isBinaryExpression(parent) ? parent.right : undefined; -} - -function removeFromMap(map: Map, key: ts.__String): T|undefined { - const mapKey = key as string; - const value = map.get(mapKey); - if (value !== undefined) { - map.delete(mapKey); - } - return value; -} - -function isPropertyAccess(node: ts.Node): node is ts.PropertyAccessExpression& - {parent: ts.BinaryExpression} { - return !!node.parent && ts.isBinaryExpression(node.parent) && ts.isPropertyAccessExpression(node); } - -function isThisAssignment(node: ts.Declaration): node is ts.BinaryExpression& - {left: ts.PropertyAccessExpression} { - return ts.isBinaryExpression(node) && ts.isPropertyAccessExpression(node.left) && - node.left.expression.kind === ts.SyntaxKind.ThisKeyword; -} - -function isNamedDeclaration(node: ts.Declaration): node is ts.NamedDeclaration { - return !!(node as any).name; -} - - -function isClassMemberType(node: ts.Declaration): node is ts.ClassElement| - ts.PropertyAccessExpression|ts.BinaryExpression { - return ts.isClassElement(node) || isPropertyAccess(node) || ts.isBinaryExpression(node); -} \ No newline at end of file diff --git a/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts b/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts index b79b13d0de20..35e86b7895b5 100644 --- a/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts +++ b/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts @@ -7,9 +7,10 @@ */ import * as ts from 'typescript'; -import {ClassMember, ClassMemberKind, Decorator} from '../../../ngtsc/host'; +import {ClassMember, ClassMemberKind, Decorator, FunctionDefinition, Parameter} from '../../../ngtsc/host'; import {reflectObjectLiteral} from '../../../ngtsc/metadata'; -import {CONSTRUCTOR_PARAMS, Esm2015ReflectionHost, getPropertyValueFromSymbol} from './esm2015_host'; +import {getNameText} from '../utils'; +import {CONSTRUCTOR_PARAMS, Fesm2015ReflectionHost, getPropertyValueFromSymbol} from './fesm2015_host'; /** * ESM5 packages contain ECMAScript IIFE functions that act like classes. For example: @@ -28,32 +29,94 @@ import {CONSTRUCTOR_PARAMS, Esm2015ReflectionHost, getPropertyValueFromSymbol} f * a static method called `ctorParameters`. * */ -export class Esm5ReflectionHost extends Esm2015ReflectionHost { +export class Esm5ReflectionHost extends Fesm2015ReflectionHost { constructor(checker: ts.TypeChecker) { super(checker); } /** - * Check whether the given declaration node actually represents a class. + * Check whether the given node actually represents a class. */ - isClass(node: ts.Declaration): boolean { return !!this.getClassSymbol(node); } + isClass(node: ts.Node): boolean { return super.isClass(node) || !!this.getClassSymbol(node); } /** - * In ESM5 the implementation of a class is a function expression that is hidden inside an IIFE. + * Find a symbol for a node that we think is a class. + * + * In ES5, the implementation of a class is a function expression that is hidden inside an IIFE. * So we need to dig around inside to get hold of the "class" symbol. - * @param declaration the top level declaration that represents an exported class. + * + * `node` might be one of: + * - A class declaration (from a declaration file). + * - The declaration of the outer variable, which is assigned the result of the IIFE. + * - The function declaration inside the IIFE, which is eventually returned and assigned to the + * outer variable. + * + * @param node The top level declaration that represents an exported class or the function + * expression inside the IIFE. + * @returns The symbol for the node or `undefined` if it is not a "class" or has no symbol. */ - getClassSymbol(declaration: ts.Declaration): ts.Symbol|undefined { - if (ts.isVariableDeclaration(declaration)) { - const iifeBody = getIifeBody(declaration); - if (iifeBody) { - const innerClassIdentifier = getReturnIdentifier(iifeBody); - if (innerClassIdentifier) { - return this.checker.getSymbolAtLocation(innerClassIdentifier); - } - } + getClassSymbol(node: ts.Node): ts.Symbol|undefined { + const symbol = super.getClassSymbol(node); + if (symbol) return symbol; + + if (ts.isVariableDeclaration(node)) { + const iifeBody = getIifeBody(node); + if (!iifeBody) return undefined; + + const innerClassIdentifier = getReturnIdentifier(iifeBody); + if (!innerClassIdentifier) return undefined; + + return this.checker.getSymbolAtLocation(innerClassIdentifier); + } else if (ts.isFunctionDeclaration(node)) { + // It might be the function expression inside the IIFE. We need to go 5 levels up... + + // 1. IIFE body. + let outerNode = node.parent; + if (!outerNode || !ts.isBlock(outerNode)) return undefined; + + // 2. IIFE function expression. + outerNode = outerNode.parent; + if (!outerNode || !ts.isFunctionExpression(outerNode)) return undefined; + + // 3. IIFE call expression. + outerNode = outerNode.parent; + if (!outerNode || !ts.isCallExpression(outerNode)) return undefined; + + // 4. Parenthesis around IIFE. + outerNode = outerNode.parent; + if (!outerNode || !ts.isParenthesizedExpression(outerNode)) return undefined; + + // 5. Outer variable declaration. + outerNode = outerNode.parent; + if (!outerNode || !ts.isVariableDeclaration(outerNode)) return undefined; + + return this.getClassSymbol(outerNode); } + return undefined; } + /** + * Parse a function declaration to find the relevant metadata about it. + * In ESM5 we need to do special work with optional arguments to the function, since they get + * their own initializer statement that needs to be parsed and then not included in the "body" + * statements of the function. + * @param node the function declaration to parse. + */ + getDefinitionOfFunction(node: T): FunctionDefinition { + const parameters = + node.parameters.map(p => ({name: getNameText(p.name), node: p, initializer: null})); + let lookingForParamInitializers = true; + + const statements = node.body && node.body.statements.filter(s => { + lookingForParamInitializers = + lookingForParamInitializers && reflectParamInitializer(s, parameters); + // If we are no longer looking for parameter initializers then we include this statement + return !lookingForParamInitializers; + }); + + return {node, body: statements || null, parameters}; + } + /** * Find the declarations of the constructor parameters of a class identified by its symbol. * In ESM5 there is no "class" so the constructor that we want is actually the declaration @@ -134,4 +197,52 @@ function getReturnStatement(declaration: ts.Expression | undefined): ts.ReturnSt function reflectArrayElement(element: ts.Expression) { return ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null; -} \ No newline at end of file +} + +/** + * Parse the statement to extract the ESM5 parameter initializer if there is one. + * If one is found, add it to the appropriate parameter in the `parameters` collection. + * + * The form we are looking for is: + * + * ``` + * if (arg === void 0) { arg = initializer; } + * ``` + * + * @param statement A statement that may be initializing an optional parameter + * @param parameters The collection of parameters that were found in the function definition + * @returns true if the statement was a parameter initializer + */ +function reflectParamInitializer(statement: ts.Statement, parameters: Parameter[]) { + if (ts.isIfStatement(statement) && isUndefinedComparison(statement.expression) && + ts.isBlock(statement.thenStatement) && statement.thenStatement.statements.length === 1) { + const ifStatementComparison = statement.expression; // (arg === void 0) + const thenStatement = statement.thenStatement.statements[0]; // arg = initializer; + if (isAssignment(thenStatement)) { + const comparisonName = ifStatementComparison.left.text; + const assignmentName = thenStatement.expression.left.text; + if (comparisonName === assignmentName) { + const parameter = parameters.find(p => p.name === comparisonName); + if (parameter) { + parameter.initializer = thenStatement.expression.right; + return true; + } + } + } + } + return false; +} + +function isUndefinedComparison(expression: ts.Expression): expression is ts.Expression& + {left: ts.Identifier, right: ts.Expression} { + return ts.isBinaryExpression(expression) && + expression.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken && + ts.isVoidExpression(expression.right) && ts.isIdentifier(expression.left); +} + +function isAssignment(statement: ts.Statement): statement is ts.ExpressionStatement& + {expression: {left: ts.Identifier, right: ts.Expression}} { + return ts.isExpressionStatement(statement) && ts.isBinaryExpression(statement.expression) && + statement.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isIdentifier(statement.expression.left); +} diff --git a/packages/compiler-cli/src/ngcc/src/host/fesm2015_host.ts b/packages/compiler-cli/src/ngcc/src/host/fesm2015_host.ts new file mode 100644 index 000000000000..5a03b789f1c5 --- /dev/null +++ b/packages/compiler-cli/src/ngcc/src/host/fesm2015_host.ts @@ -0,0 +1,425 @@ +/** + * @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 {ClassMember, ClassMemberKind, CtorParameter, Decorator} from '../../../ngtsc/host'; +import {TypeScriptReflectionHost, reflectObjectLiteral} from '../../../ngtsc/metadata'; +import {getNameText} from '../utils'; + +import {NgccReflectionHost} from './ngcc_host'; + +export const DECORATORS = 'decorators' as ts.__String; +export const PROP_DECORATORS = 'propDecorators' as ts.__String; +export const CONSTRUCTOR = '__constructor' as ts.__String; +export const CONSTRUCTOR_PARAMS = 'ctorParameters' as ts.__String; + +/** + * Esm2015 packages contain ECMAScript 2015 classes, etc. + * Decorators are defined via static properties on the class. For example: + * + * ``` + * class SomeDirective { + * } + * SomeDirective.decorators = [ + * { type: Directive, args: [{ selector: '[someDirective]' },] } + * ]; + * SomeDirective.ctorParameters = () => [ + * { type: ViewContainerRef, }, + * { type: TemplateRef, }, + * { type: undefined, decorators: [{ type: Inject, args: [INJECTED_TOKEN,] },] }, + * ]; + * SomeDirective.propDecorators = { + * "input1": [{ type: Input },], + * "input2": [{ type: Input },], + * }; + * ``` + * + * * Classes are decorated if they have a static property called `decorators`. + * * Members are decorated if there is a matching key on a static property + * called `propDecorators`. + * * Constructor parameters decorators are found on an object returned from + * a static method called `ctorParameters`. + */ +export class Fesm2015ReflectionHost extends TypeScriptReflectionHost implements NgccReflectionHost { + constructor(checker: ts.TypeChecker) { super(checker); } + + /** + * Examine a declaration (for example, of a class or function) and return metadata about any + * decorators present on the declaration. + * + * @param declaration a TypeScript `ts.Declaration` node representing the class or function over + * which to reflect. For example, if the intent is to reflect the decorators of a class and the + * source is in ES6 format, this will be a `ts.ClassDeclaration` node. If the source is in ES5 + * format, this might be a `ts.VariableDeclaration` as classes in ES5 are represented as the + * result of an IIFE execution. + * + * @returns an array of `Decorator` metadata if decorators are present on the declaration, or + * `null` if either no decorators were present or if the declaration is not of a decoratable type. + */ + getDecoratorsOfDeclaration(declaration: ts.Declaration): Decorator[]|null { + const symbol = this.getClassSymbol(declaration); + if (symbol) { + if (symbol.exports && symbol.exports.has(DECORATORS)) { + // Symbol of the identifier for `SomeDirective.decorators`. + const decoratorsSymbol = symbol.exports.get(DECORATORS) !; + const decoratorsIdentifier = decoratorsSymbol.valueDeclaration; + + if (decoratorsIdentifier && decoratorsIdentifier.parent) { + if (ts.isBinaryExpression(decoratorsIdentifier.parent)) { + // AST of the array of decorator values + const decoratorsArray = decoratorsIdentifier.parent.right; + return this.reflectDecorators(decoratorsArray); + } + } + } + } + return null; + } + + /** + * Examine a declaration which should be of a class, and return metadata about the members of the + * class. + * + * @param declaration a TypeScript `ts.Declaration` node representing the class over which to + * reflect. If the source is in ES6 format, this will be a `ts.ClassDeclaration` node. If the + * source is in ES5 format, this might be a `ts.VariableDeclaration` as classes in ES5 are + * represented as the result of an IIFE execution. + * + * @returns an array of `ClassMember` metadata representing the members of the class. + * + * @throws if `declaration` does not resolve to a class declaration. + */ + getMembersOfClass(clazz: ts.Declaration): ClassMember[] { + const members: ClassMember[] = []; + const symbol = this.getClassSymbol(clazz); + if (!symbol) { + throw new Error(`Attempted to get members of a non-class: "${clazz.getText()}"`); + } + + // The decorators map contains all the properties that are decorated + const decoratorsMap = this.getMemberDecorators(symbol); + + // The member map contains all the method (instance and static); and any instance properties + // that are initialized in the class. + if (symbol.members) { + symbol.members.forEach((value, key) => { + const decorators = removeFromMap(decoratorsMap, key); + const member = this.reflectMember(value, decorators); + if (member) { + members.push(member); + } + }); + } + + // The static property map contains all the static properties + if (symbol.exports) { + symbol.exports.forEach((value, key) => { + const decorators = removeFromMap(decoratorsMap, key); + const member = this.reflectMember(value, decorators, true); + if (member) { + members.push(member); + } + }); + } + + // Deal with any decorated properties that were not initialized in the class + decoratorsMap.forEach((value, key) => { + members.push({ + implementation: null, + decorators: value, + isStatic: false, + kind: ClassMemberKind.Property, + name: key, + nameNode: null, + node: null, + type: null, + value: null + }); + }); + + return members; + } + + /** + * Reflect over the constructor of a class and return metadata about its parameters. + * + * This method only looks at the constructor of a class directly and not at any inherited + * constructors. + * + * @param declaration a TypeScript `ts.Declaration` node representing the class over which to + * reflect. If the source is in ES6 format, this will be a `ts.ClassDeclaration` node. If the + * source is in ES5 format, this might be a `ts.VariableDeclaration` as classes in ES5 are + * represented as the result of an IIFE execution. + * + * @returns an array of `Parameter` metadata representing the parameters of the constructor, if + * a constructor exists. If the constructor exists and has 0 parameters, this array will be empty. + * If the class has no constructor, this method returns `null`. + * + * @throws if `declaration` does not resolve to a class declaration. + */ + getConstructorParameters(clazz: ts.Declaration): CtorParameter[]|null { + const classSymbol = this.getClassSymbol(clazz); + if (!classSymbol) { + throw new Error( + `Attempted to get constructor parameters of a non-class: "${clazz.getText()}"`); + } + const parameterNodes = this.getConstructorParameterDeclarations(classSymbol); + if (parameterNodes) { + const parameters: CtorParameter[] = []; + const decoratorInfo = this.getConstructorDecorators(classSymbol); + parameterNodes.forEach((node, index) => { + const info = decoratorInfo[index]; + const decorators = + info && info.has('decorators') && this.reflectDecorators(info.get('decorators') !) || + null; + const type = info && info.get('type') || null; + const nameNode = node.name; + parameters.push({name: getNameText(nameNode), nameNode, type, decorators}); + }); + return parameters; + } + return null; + } + + /** + * Find a symbol for a node that we think is a class. + * @param node The node whose symbol we are finding. + * @returns The symbol for the node or `undefined` if it is not a "class" or has no symbol. + */ + getClassSymbol(declaration: ts.Node): ts.Symbol|undefined { + return ts.isClassDeclaration(declaration) ? + declaration.name && this.checker.getSymbolAtLocation(declaration.name) : + undefined; + } + + /** + * Member decorators are declared as static properties of the class in ES2015: + * + * ``` + * SomeDirective.propDecorators = { + * "ngForOf": [{ type: Input },], + * "ngForTrackBy": [{ type: Input },], + * "ngForTemplate": [{ type: Input },], + * }; + * ``` + */ + protected getMemberDecorators(classSymbol: ts.Symbol): Map { + const memberDecorators = new Map(); + if (classSymbol.exports && classSymbol.exports.has(PROP_DECORATORS)) { + // Symbol of the identifier for `SomeDirective.propDecorators`. + const propDecoratorsMap = + getPropertyValueFromSymbol(classSymbol.exports.get(PROP_DECORATORS) !); + if (propDecoratorsMap && ts.isObjectLiteralExpression(propDecoratorsMap)) { + const propertiesMap = reflectObjectLiteral(propDecoratorsMap); + propertiesMap.forEach( + (value, name) => { memberDecorators.set(name, this.reflectDecorators(value)); }); + } + } + return memberDecorators; + } + + /** + * Reflect over the given expression and extract decorator information. + * @param decoratorsArray An expression that contains decorator information. + */ + protected reflectDecorators(decoratorsArray: ts.Expression): Decorator[] { + const decorators: Decorator[] = []; + + if (ts.isArrayLiteralExpression(decoratorsArray)) { + // Add each decorator that is imported from `@angular/core` into the `decorators` array + decoratorsArray.elements.forEach(node => { + + // If the decorator is not an object literal expression then we are not interested + if (ts.isObjectLiteralExpression(node)) { + // We are only interested in objects of the form: `{ type: DecoratorType, args: [...] }` + const decorator = reflectObjectLiteral(node); + + // Is the value of the `type` property an identifier? + const typeIdentifier = decorator.get('type'); + if (typeIdentifier && ts.isIdentifier(typeIdentifier)) { + decorators.push({ + name: typeIdentifier.text, + import: this.getImportOfIdentifier(typeIdentifier), node, + args: getDecoratorArgs(node), + }); + } + } + }); + } + return decorators; + } + + protected reflectMember(symbol: ts.Symbol, decorators?: Decorator[], isStatic?: boolean): + ClassMember|null { + let kind: ClassMemberKind|null = null; + let value: ts.Expression|null = null; + let name: string|null = null; + let nameNode: ts.Identifier|null = null; + let type = null; + + + const node = symbol.valueDeclaration || symbol.declarations && symbol.declarations[0]; + if (!node || !isClassMemberType(node)) { + return null; + } + + if (symbol.flags & ts.SymbolFlags.Method) { + kind = ClassMemberKind.Method; + } else if (symbol.flags & ts.SymbolFlags.Property) { + kind = ClassMemberKind.Property; + } else if (symbol.flags & ts.SymbolFlags.GetAccessor) { + kind = ClassMemberKind.Getter; + } else if (symbol.flags & ts.SymbolFlags.SetAccessor) { + kind = ClassMemberKind.Setter; + } + + if (isStatic && isPropertyAccess(node)) { + name = node.name.text; + value = symbol.flags & ts.SymbolFlags.Property ? node.parent.right : null; + } else if (isThisAssignment(node)) { + kind = ClassMemberKind.Property; + name = node.left.name.text; + value = node.right; + isStatic = false; + } else if (ts.isConstructorDeclaration(node)) { + kind = ClassMemberKind.Constructor; + name = 'constructor'; + isStatic = false; + } + + if (kind === null) { + console.warn(`Unknown member type: "${node.getText()}`); + return null; + } + + if (!name) { + if (isNamedDeclaration(node) && node.name && ts.isIdentifier(node.name)) { + name = node.name.text; + nameNode = node.name; + } else { + return null; + } + } + + // If we have still not determined if this is a static or instance member then + // look for the `static` keyword on the declaration + if (isStatic === undefined) { + isStatic = node.modifiers !== undefined && + node.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword); + } + + return { + node, + implementation: node, kind, type, name, nameNode, value, isStatic, + decorators: decorators || [] + }; + } + + /** + * Find the declarations of the constructor parameters of a class identified by its symbol. + * @param classSymbol the class whose parameters we want to find. + * @returns an array of `ts.ParameterDeclaration` objects representing each of the parameters in + * the + * class's constructor or null if there is no constructor. + */ + protected getConstructorParameterDeclarations(classSymbol: ts.Symbol): + ts.ParameterDeclaration[]|null { + const constructorSymbol = classSymbol.members && classSymbol.members.get(CONSTRUCTOR); + if (constructorSymbol) { + // For some reason the constructor does not have a `valueDeclaration` ?!? + const constructor = constructorSymbol.declarations && + constructorSymbol.declarations[0] as ts.ConstructorDeclaration; + if (constructor && constructor.parameters) { + return Array.from(constructor.parameters); + } + return []; + } + return null; + } + + /** + * Constructors parameter decorators are declared in the body of static method of the class in + * ES2015: + * + * ``` + * SomeDirective.ctorParameters = () => [ + * { type: ViewContainerRef, }, + * { type: TemplateRef, }, + * { type: IterableDiffers, }, + * { type: undefined, decorators: [{ type: Inject, args: [INJECTED_TOKEN,] },] }, + * ]; + * ``` + */ + protected getConstructorDecorators(classSymbol: ts.Symbol): (Map|null)[] { + if (classSymbol.exports && classSymbol.exports.has(CONSTRUCTOR_PARAMS)) { + const paramDecoratorsProperty = + getPropertyValueFromSymbol(classSymbol.exports.get(CONSTRUCTOR_PARAMS) !); + if (paramDecoratorsProperty && ts.isArrowFunction(paramDecoratorsProperty)) { + if (ts.isArrayLiteralExpression(paramDecoratorsProperty.body)) { + return paramDecoratorsProperty.body.elements.map( + element => + ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null); + } + } + } + return []; + } +} + +/** + * The arguments of a decorator are held in the `args` property of its declaration object. + */ +function getDecoratorArgs(node: ts.ObjectLiteralExpression): ts.Expression[] { + const argsProperty = node.properties.filter(ts.isPropertyAssignment) + .find(property => getNameText(property.name) === 'args'); + const argsExpression = argsProperty && argsProperty.initializer; + return argsExpression && ts.isArrayLiteralExpression(argsExpression) ? + Array.from(argsExpression.elements) : + []; +} + +/** + * Helper method to extract the value of a property given the property's "symbol", + * which is actually the symbol of the identifier of the property. + */ +export function getPropertyValueFromSymbol(propSymbol: ts.Symbol): ts.Expression|undefined { + const propIdentifier = propSymbol.valueDeclaration; + const parent = propIdentifier && propIdentifier.parent; + return parent && ts.isBinaryExpression(parent) ? parent.right : undefined; +} + +function removeFromMap(map: Map, key: ts.__String): T|undefined { + const mapKey = key as string; + const value = map.get(mapKey); + if (value !== undefined) { + map.delete(mapKey); + } + return value; +} + +function isPropertyAccess(node: ts.Node): node is ts.PropertyAccessExpression& + {parent: ts.BinaryExpression} { + return !!node.parent && ts.isBinaryExpression(node.parent) && ts.isPropertyAccessExpression(node); +} + +function isThisAssignment(node: ts.Declaration): node is ts.BinaryExpression& + {left: ts.PropertyAccessExpression} { + return ts.isBinaryExpression(node) && ts.isPropertyAccessExpression(node.left) && + node.left.expression.kind === ts.SyntaxKind.ThisKeyword; +} + +function isNamedDeclaration(node: ts.Declaration): node is ts.NamedDeclaration { + return !!(node as any).name; +} + + +function isClassMemberType(node: ts.Declaration): node is ts.ClassElement| + ts.PropertyAccessExpression|ts.BinaryExpression { + return ts.isClassElement(node) || isPropertyAccess(node) || ts.isBinaryExpression(node); +} diff --git a/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts b/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts index ef32c719ed7c..c0746643fccb 100644 --- a/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts +++ b/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts @@ -12,5 +12,5 @@ import {ReflectionHost} from '../../../ngtsc/host'; * A reflection host that has extra methods for looking at non-Typescript package formats */ export interface NgccReflectionHost extends ReflectionHost { - getClassSymbol(declaration: ts.Declaration): ts.Symbol|undefined; + getClassSymbol(node: ts.Node): ts.Symbol|undefined; } diff --git a/packages/compiler-cli/src/ngcc/src/main.ts b/packages/compiler-cli/src/ngcc/src/main.ts index fece2d136977..7918bc9dbb3c 100644 --- a/packages/compiler-cli/src/ngcc/src/main.ts +++ b/packages/compiler-cli/src/ngcc/src/main.ts @@ -5,17 +5,87 @@ * 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 {resolve} from 'path'; +import {existsSync, lstatSync, readFileSync, readdirSync} from 'fs'; +import {posix as path} from 'path'; + import {PackageTransformer} from './transform/package_transformer'; export function mainNgcc(args: string[]): number { - const packagePath = resolve(args[0]); - - // TODO: find all the package tyoes to transform - // TODO: error/warning logging/handling etc + const formats = args[0] ? args[0].split(',') : ['fesm2015', 'esm2015', 'fesm5', 'esm5']; + const packagePaths = args[1] ? [path.resolve(args[1])] : findPackagesToCompile(); + const targetPath = args[2] ? args[2] : 'node_modules'; const transformer = new PackageTransformer(); - transformer.transform(packagePath, 'fesm2015'); + packagePaths.forEach(packagePath => { + formats.forEach(format => { + // TODO: remove before flight + console.warn(`Compiling ${packagePath} : ${format}`); + transformer.transform(packagePath, format, targetPath); + }); + }); return 0; } + +// TODO - consider nested node_modules + +/** + * Check whether the given folder needs to be included in the ngcc compilation. + * We do not care about folders that are: + * + * - symlinks + * - node_modules + * - do not contain a package.json + * - do not have a typings property in package.json + * - do not have an appropriate metadata.json file + * + * @param folderPath The absolute path to the folder. + */ +function hasMetadataFile(folderPath: string): boolean { + const folderName = path.basename(folderPath); + if (folderName === 'node_modules' || lstatSync(folderPath).isSymbolicLink()) { + return false; + } + const packageJsonPath = path.join(folderPath, 'package.json'); + if (!existsSync(packageJsonPath)) { + return false; + } + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); + if (!packageJson.typings) { + return false; + } + // TODO: avoid if packageJson contains built marker + const metadataPath = + path.join(folderPath, packageJson.typings.replace(/\.d\.ts$/, '.metadata.json')); + return existsSync(metadataPath); +} + +/** + * Look for packages that need to be compiled. + * The function will recurse into folders that start with `@...`, e.g. `@angular/...`. + * Without an argument it starts at `node_modules`. + */ +function findPackagesToCompile(folder: string = 'node_modules'): string[] { + const fullPath = path.resolve(folder); + const packagesToCompile: string[] = []; + readdirSync(fullPath) + .filter(p => !p.startsWith('.')) + .filter(p => lstatSync(path.join(fullPath, p)).isDirectory()) + .forEach(p => { + const packagePath = path.join(fullPath, p); + if (p.startsWith('@')) { + packagesToCompile.push(...findPackagesToCompile(packagePath)); + } else { + packagesToCompile.push(packagePath); + } + }); + + return packagesToCompile.filter(path => recursiveDirTest(path, hasMetadataFile)); +} + +function recursiveDirTest(dir: string, test: (dir: string) => boolean): boolean { + return test(dir) || readdirSync(dir).some(segment => { + const fullPath = path.join(dir, segment); + return lstatSync(fullPath).isDirectory() && recursiveDirTest(fullPath, test); + }); +} diff --git a/packages/compiler-cli/src/ngcc/src/parsing/utils.ts b/packages/compiler-cli/src/ngcc/src/parsing/utils.ts deleted file mode 100644 index e9270e0d8533..000000000000 --- a/packages/compiler-cli/src/ngcc/src/parsing/utils.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * @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 {readFileSync} from 'fs'; -import {dirname, resolve} from 'path'; -import {find} from 'shelljs'; - -/** - * Match paths to package.json files. - */ -const PACKAGE_JSON_REGEX = /\/package\.json$/; - -/** - * Match paths that have a `node_modules` segment at the start or in the middle. - */ -const NODE_MODULES_REGEX = /(?:^|\/)node_modules\//; - -/** - * Search the `rootDirectory` and its subdirectories to find package.json files. - * It ignores node dependencies, i.e. those under `node_modules` folders. - * @param rootDirectory the directory in which we should search. - */ -export function findAllPackageJsonFiles(rootDirectory: string): string[] { - // TODO(gkalpak): Investigate whether skipping `node_modules/` directories (instead of traversing - // them and filtering out the results later) makes a noticeable difference. - const paths = Array.from(find(rootDirectory)); - return paths.filter( - path => PACKAGE_JSON_REGEX.test(path) && - !NODE_MODULES_REGEX.test(path.slice(rootDirectory.length))); -} - -/** - * Identify the entry points of a package. - * @param packageDirectory The absolute path to the root directory that contains this package. - * @param format The format of the entry point within the package. - * @returns A collection of paths that point to entry points for this package. - */ -export function getEntryPoints(packageDirectory: string, format: string): string[] { - const packageJsonPaths = findAllPackageJsonFiles(packageDirectory); - return packageJsonPaths - .map(packageJsonPath => { - const entryPointPackageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); - const relativeEntryPointPath = entryPointPackageJson[format]; - return relativeEntryPointPath && resolve(dirname(packageJsonPath), relativeEntryPointPath); - }) - .filter(entryPointPath => entryPointPath); -} diff --git a/packages/compiler-cli/src/ngcc/src/rendering/esm2015_renderer.ts b/packages/compiler-cli/src/ngcc/src/rendering/esm2015_renderer.ts index 10db8b292e94..d3a473a4f5c2 100644 --- a/packages/compiler-cli/src/ngcc/src/rendering/esm2015_renderer.ts +++ b/packages/compiler-cli/src/ngcc/src/rendering/esm2015_renderer.ts @@ -7,13 +7,10 @@ */ import * as ts from 'typescript'; import MagicString from 'magic-string'; -import {NgccReflectionHost} from '../host/ngcc_host'; import {AnalyzedClass} from '../analyzer'; import {Renderer} from './renderer'; export class Esm2015Renderer extends Renderer { - constructor(protected host: NgccReflectionHost) { super(); } - /** * Add the imports at the top of the file */ diff --git a/packages/compiler-cli/src/ngcc/src/rendering/esm5_renderer.ts b/packages/compiler-cli/src/ngcc/src/rendering/esm5_renderer.ts index 5f5d7fdec3e9..aadfe23a7979 100644 --- a/packages/compiler-cli/src/ngcc/src/rendering/esm5_renderer.ts +++ b/packages/compiler-cli/src/ngcc/src/rendering/esm5_renderer.ts @@ -5,12 +5,6 @@ * 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 MagicString from 'magic-string'; -import {NgccReflectionHost} from '../host/ngcc_host'; -import {AnalyzedClass, AnalyzedFile} from '../analyzer'; import {Esm2015Renderer} from './esm2015_renderer'; -export class Esm5Renderer extends Esm2015Renderer { - constructor(host: NgccReflectionHost) { super(host); } -} +export class Esm5Renderer extends Esm2015Renderer {} diff --git a/packages/compiler-cli/src/ngcc/src/rendering/renderer.ts b/packages/compiler-cli/src/ngcc/src/rendering/renderer.ts index 974d428fb81a..a5f421352c7f 100644 --- a/packages/compiler-cli/src/ngcc/src/rendering/renderer.ts +++ b/packages/compiler-cli/src/ngcc/src/rendering/renderer.ts @@ -5,16 +5,19 @@ * 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 {dirname} from 'path'; -import * as ts from 'typescript'; - +import {ConstantPool, Expression, Statement, WrappedNodeExpr, WritePropExpr} from '@angular/compiler'; +import {SourceMapConverter, commentRegex, fromJSON, fromMapFileSource, fromObject, fromSource, generateMapFileComment, mapFileCommentRegex, removeComments, removeMapFileComments} from 'convert-source-map'; +import {readFileSync, statSync} from 'fs'; import MagicString from 'magic-string'; -import {commentRegex, mapFileCommentRegex, fromJSON, fromSource, fromMapFileSource, fromObject, generateMapFileComment, removeComments, removeMapFileComments, SourceMapConverter} from 'convert-source-map'; +import {basename, dirname} from 'path'; import {SourceMapConsumer, SourceMapGenerator, RawSourceMap} from 'source-map'; -import {ConstantPool, Expression, Statement, WrappedNodeExpr, WritePropExpr} from '@angular/compiler'; -import {AnalyzedClass, AnalyzedFile} from '../analyzer'; +import * as ts from 'typescript'; + import {Decorator} from '../../../ngtsc/host'; import {ImportManager, translateStatement} from '../../../ngtsc/transform'; +import {AnalyzedClass, AnalyzedFile} from '../analyzer'; +import {IMPORT_PREFIX} from '../constants'; +import {NgccReflectionHost} from '../host/ngcc_host'; interface SourceMapInfo { source: string; @@ -55,19 +58,21 @@ export interface FileInfo { } /** - * A base-class for rendering an `AnalyzedClass`. - * Package formats have output files that must be rendered differently, - * Concrete sub-classes must implement the `addImports`, `addDefinitions` and - * `removeDecorators` abstract methods. + * A base-class for rendering an `AnalyzedFile`. + * + * Package formats have output files that must be rendered differently. Concrete sub-classes must + * implement the `addImports`, `addDefinitions` and `removeDecorators` abstract methods. */ export abstract class Renderer { + constructor(protected host: NgccReflectionHost) {} + /** * Render the source code and source-map for an Analyzed file. * @param file The analyzed file to render. * @param targetPath The absolute path where the rendered file will be written. */ renderFile(file: AnalyzedFile, targetPath: string): RenderResult { - const importManager = new ImportManager(false, 'ɵngcc'); + const importManager = new ImportManager(false, IMPORT_PREFIX); const input = this.extractSourceMap(file.sourceFile); const outputText = new MagicString(input.source); @@ -133,7 +138,20 @@ export abstract class Renderer { try { externalSourceMap = fromMapFileSource(file.text, dirname(file.fileName)); } catch (e) { - console.warn(e); + if (e.code === 'ENOENT') { + console.warn( + `The external map file specified in the source code comment "${e.path}" was not found on the file system.`); + const mapPath = file.fileName + '.map'; + if (basename(e.path) !== basename(mapPath) && statSync(mapPath).isFile()) { + console.warn( + `Guessing the map file name from the source file name: "${basename(mapPath)}"`); + try { + externalSourceMap = fromObject(JSON.parse(readFileSync(mapPath, 'utf8'))); + } catch (e) { + console.error(e); + } + } + } } return { source: removeMapFileComments(file.text).replace(/\n\n$/, '\n'), diff --git a/packages/compiler-cli/src/ngcc/src/transform/package_transformer.ts b/packages/compiler-cli/src/ngcc/src/transform/package_transformer.ts index ab974015c06a..06d3c6aabe02 100644 --- a/packages/compiler-cli/src/ngcc/src/transform/package_transformer.ts +++ b/packages/compiler-cli/src/ngcc/src/transform/package_transformer.ts @@ -5,77 +5,104 @@ * 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 {writeFileSync} from 'fs'; +import {existsSync, readFileSync, writeFileSync} from 'fs'; import {dirname, relative, resolve} from 'path'; -import {mkdir} from 'shelljs'; +import {mkdir, mv} from 'shelljs'; import * as ts from 'typescript'; -import {Analyzer} from '../analyzer'; +import {DtsFileTransformer} from '../../../ngtsc/transform'; +import {AnalyzedFile, Analyzer} from '../analyzer'; +import {IMPORT_PREFIX} from '../constants'; +import {DtsMapper} from '../host/dts_mapper'; import {Esm2015ReflectionHost} from '../host/esm2015_host'; import {Esm5ReflectionHost} from '../host/esm5_host'; +import {Fesm2015ReflectionHost} from '../host/fesm2015_host'; import {NgccReflectionHost} from '../host/ngcc_host'; import {Esm2015FileParser} from '../parsing/esm2015_parser'; import {Esm5FileParser} from '../parsing/esm5_parser'; import {FileParser} from '../parsing/file_parser'; -import {getEntryPoints} from '../parsing/utils'; import {Esm2015Renderer} from '../rendering/esm2015_renderer'; import {Esm5Renderer} from '../rendering/esm5_renderer'; import {FileInfo, Renderer} from '../rendering/renderer'; +import {getEntryPoints} from './utils'; + + /** * A Package is stored in a directory on disk and that directory can contain one or more package - formats - e.g. fesm2015, UMD, etc. + * formats - e.g. fesm2015, UMD, etc. Additionally, each package provides typings (`.d.ts` files). * * Each of these formats exposes one or more entry points, which are source files that need to be * parsed to identify the decorated exported classes that need to be analyzed and compiled by one or * more `DecoratorHandler` objects. * - * Each entry point to a package is identified by a `SourceFile` that can be parsed and analyzed - * to identify classes that need to be transformed; and then finally rendered and written to disk. - + * Each entry point to a package is identified by a `SourceFile` that can be parsed and analyzed to + * identify classes that need to be transformed; and then finally rendered and written to disk. * The actual file which needs to be transformed depends upon the package format. * + * Along with the source files, the corresponding source maps (either inline or external) and + * `.d.ts` files are transformed accordingly. + * * - Flat file packages have all the classes in a single file. * - Other packages may re-export classes from other non-entry point files. * - Some formats may contain multiple "modules" in a single file. */ export class PackageTransformer { - transform(packagePath: string, format: string): void { + transform(packagePath: string, format: string, targetPath: string = 'node_modules'): void { const sourceNodeModules = this.findNodeModulesPath(packagePath); - const targetNodeModules = sourceNodeModules.replace(/node_modules$/, 'node_modules_ngtsc'); - const entryPointPaths = getEntryPoints(packagePath, format); - entryPointPaths.forEach(entryPointPath => { - const options: ts.CompilerOptions = {allowJs: true, rootDir: entryPointPath}; + const targetNodeModules = resolve(sourceNodeModules, '..', targetPath); + const entryPoints = getEntryPoints(packagePath, format); + + entryPoints.forEach(entryPoint => { + const outputFiles: FileInfo[] = []; + const options: ts.CompilerOptions = { + allowJs: true, + maxNodeModuleJsDepth: Infinity, + rootDir: entryPoint.entryFileName, + }; + + // Create the TS program and necessary helpers. + // TODO : create a custom compiler host that reads from .bak files if available. const host = ts.createCompilerHost(options); - const packageProgram = ts.createProgram([entryPointPath], options, host); - const entryPointFile = packageProgram.getSourceFile(entryPointPath) !; + const packageProgram = ts.createProgram([entryPoint.entryFileName], options, host); const typeChecker = packageProgram.getTypeChecker(); + const dtsMapper = new DtsMapper(entryPoint.entryRoot, entryPoint.dtsEntryRoot); + const reflectionHost = this.getHost(format, packageProgram, dtsMapper); - const reflectionHost = this.getHost(format, packageProgram); const parser = this.getFileParser(format, packageProgram, reflectionHost); const analyzer = new Analyzer(typeChecker, reflectionHost); const renderer = this.getRenderer(format, packageProgram, reflectionHost); + // Parse and analyze the files. + const entryPointFile = packageProgram.getSourceFile(entryPoint.entryFileName) !; const parsedFiles = parser.parseFile(entryPointFile); - parsedFiles.forEach(parsedFile => { - const analyzedFile = analyzer.analyzeFile(parsedFile); - const targetPath = resolve( - targetNodeModules, relative(sourceNodeModules, analyzedFile.sourceFile.fileName)); - const {source, map} = renderer.renderFile(analyzedFile, targetPath); - this.writeFile(source); - if (map) { - this.writeFile(map); - } - }); + const analyzedFiles = parsedFiles.map(parsedFile => analyzer.analyzeFile(parsedFile)); + + // Transform the source files and source maps. + outputFiles.push(...this.transformSourceFiles( + analyzedFiles, sourceNodeModules, targetNodeModules, renderer)); + + // Transform the `.d.ts` files (if necessary). + // TODO(gkalpak): What about `.d.ts` source maps? (See + // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html#new---declarationmap.) + if (format === 'esm2015') { + outputFiles.push(...this.transformDtsFiles( + analyzedFiles, sourceNodeModules, targetNodeModules, dtsMapper)); + } + + // Write out all the transformed files. + outputFiles.forEach(file => this.writeFile(file)); }); } - getHost(format: string, program: ts.Program): NgccReflectionHost { + getHost(format: string, program: ts.Program, dtsMapper: DtsMapper): NgccReflectionHost { switch (format) { case 'esm2015': + return new Esm2015ReflectionHost(program.getTypeChecker(), dtsMapper); case 'fesm2015': - return new Esm2015ReflectionHost(program.getTypeChecker()); + return new Fesm2015ReflectionHost(program.getTypeChecker()); + case 'esm5': case 'fesm5': return new Esm5ReflectionHost(program.getTypeChecker()); default: @@ -88,6 +115,7 @@ export class PackageTransformer { case 'esm2015': case 'fesm2015': return new Esm2015FileParser(program, host); + case 'esm5': case 'fesm5': return new Esm5FileParser(program, host); default: @@ -100,6 +128,7 @@ export class PackageTransformer { case 'esm2015': case 'fesm2015': return new Esm2015Renderer(host); + case 'esm5': case 'fesm5': return new Esm5Renderer(host); default: @@ -114,8 +143,63 @@ export class PackageTransformer { return src; } + transformDtsFiles( + analyzedFiles: AnalyzedFile[], sourceNodeModules: string, targetNodeModules: string, + dtsMapper: DtsMapper): FileInfo[] { + const outputFiles: FileInfo[] = []; + + analyzedFiles.forEach(analyzedFile => { + // Create a `DtsFileTransformer` for the source file and record the generated fields, which + // will allow the corresponding `.d.ts` file to be transformed later. + const dtsTransformer = new DtsFileTransformer(null, IMPORT_PREFIX); + analyzedFile.analyzedClasses.forEach( + analyzedClass => + dtsTransformer.recordStaticField(analyzedClass.name, analyzedClass.compilation)); + + // Find the corresponding `.d.ts` file. + const sourceFileName = analyzedFile.sourceFile.fileName; + const originalDtsFileName = dtsMapper.getDtsFileNameFor(sourceFileName); + const originalDtsContents = readFileSync(originalDtsFileName, 'utf8'); + + // Transform the `.d.ts` file based on the recorded source file changes. + const transformedDtsFileName = + resolve(targetNodeModules, relative(sourceNodeModules, originalDtsFileName)); + const transformedDtsContents = dtsTransformer.transform(originalDtsContents, sourceFileName); + + // Add the transformed `.d.ts` file to the list of output files. + outputFiles.push({path: transformedDtsFileName, contents: transformedDtsContents}); + }); + + return outputFiles; + } + + transformSourceFiles( + analyzedFiles: AnalyzedFile[], sourceNodeModules: string, targetNodeModules: string, + renderer: Renderer): FileInfo[] { + const outputFiles: FileInfo[] = []; + + analyzedFiles.forEach(analyzedFile => { + // Transform the source file based on the recorded changes. + const targetPath = + resolve(targetNodeModules, relative(sourceNodeModules, analyzedFile.sourceFile.fileName)); + const {source, map} = renderer.renderFile(analyzedFile, targetPath); + + // Add the transformed file (and source map, if available) to the list of output files. + outputFiles.push(source); + if (map) { + outputFiles.push(map); + } + }); + + return outputFiles; + } + writeFile(file: FileInfo): void { mkdir('-p', dirname(file.path)); + const backPath = file.path + '.bak'; + if (existsSync(file.path) && !existsSync(backPath)) { + mv(file.path, backPath); + } writeFileSync(file.path, file.contents, 'utf8'); } } diff --git a/packages/compiler-cli/src/ngcc/src/transform/utils.ts b/packages/compiler-cli/src/ngcc/src/transform/utils.ts new file mode 100644 index 000000000000..caa042f36e42 --- /dev/null +++ b/packages/compiler-cli/src/ngcc/src/transform/utils.ts @@ -0,0 +1,88 @@ +/** + * @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 {readFileSync} from 'fs'; +import {dirname, relative, resolve} from 'path'; +import {find} from 'shelljs'; + +import {isDefined} from '../utils'; + +/** + * Represents an entry point to a package or sub-package. + * + * It exposes the absolute path to the entry point file and a method to get the `.d.ts` file that + * corresponds to any source file that belongs to the package (assuming source files and `.d.ts` + * files have the same directory layout). + */ +export class EntryPoint { + entryFileName: string; + entryRoot: string; + dtsEntryRoot: string; + + /** + * @param packageRoot The absolute path to the root directory that contains the package. + * @param relativeEntryPath The relative path to the entry point file. + * @param relativeDtsEntryPath The relative path to the `.d.ts` entry point file. + */ + constructor(packageRoot: string, relativeEntryPath: string, relativeDtsEntryPath: string) { + this.entryFileName = resolve(packageRoot, relativeEntryPath); + this.entryRoot = dirname(this.entryFileName); + const dtsEntryFileName = resolve(packageRoot, relativeDtsEntryPath); + this.dtsEntryRoot = dirname(dtsEntryFileName); + } +} + +/** + * Match paths to `package.json` files. + */ +const PACKAGE_JSON_REGEX = /\/package\.json$/; + +/** + * Match paths that have a `node_modules` segment at the start or in the middle. + */ +const NODE_MODULES_REGEX = /(?:^|\/)node_modules\//; + +/** + * Search the `rootDirectory` and its subdirectories to find `package.json` files. + * It ignores node dependencies, i.e. those under `node_modules` directories. + * + * @param rootDirectory The directory in which we should search. + */ +export function findAllPackageJsonFiles(rootDirectory: string): string[] { + // TODO(gkalpak): Investigate whether skipping `node_modules/` directories (instead of traversing + // them and filtering out the results later) makes a noticeable difference. + const paths = Array.from(find(rootDirectory)); + return paths.filter( + path => PACKAGE_JSON_REGEX.test(path) && + !NODE_MODULES_REGEX.test(path.slice(rootDirectory.length))); +} + +/** + * Identify the entry points of a package. + * + * @param packageDirectory The absolute path to the root directory that contains the package. + * @param format The format of the entry points to look for within the package. + * + * @returns A collection of `EntryPoint`s that correspond to entry points for the package. + */ +export function getEntryPoints(packageDirectory: string, format: string): EntryPoint[] { + const packageJsonPaths = findAllPackageJsonFiles(packageDirectory); + const entryPoints = + packageJsonPaths + .map(packageJsonPath => { + const entryPointPackageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); + const entryPointPath: string|undefined = entryPointPackageJson[format]; + if (!entryPointPath) { + return undefined; + } + const dtsEntryPointPath = entryPointPackageJson.typings || entryPointPath; + return new EntryPoint(dirname(packageJsonPath), entryPointPath, dtsEntryPointPath); + }) + .filter(isDefined); + return entryPoints; +} diff --git a/packages/compiler-cli/src/ngcc/src/utils.ts b/packages/compiler-cli/src/ngcc/src/utils.ts index 425e6c9da718..7fe3aee36f5e 100644 --- a/packages/compiler-cli/src/ngcc/src/utils.ts +++ b/packages/compiler-cli/src/ngcc/src/utils.ts @@ -14,9 +14,9 @@ export function getOriginalSymbol(checker: ts.TypeChecker): (symbol: ts.Symbol) } export function isDefined(value: T | undefined | null): value is T { - return !!value; + return (value !== undefined) && (value !== null); } export function getNameText(name: ts.PropertyName | ts.BindingName): string { return ts.isIdentifier(name) || ts.isLiteralExpression(name) ? name.text : name.getText(); -} \ No newline at end of file +} diff --git a/packages/compiler-cli/src/ngcc/test/analyzer_spec.ts b/packages/compiler-cli/src/ngcc/test/analyzer_spec.ts index 96fc978071b4..d33e8ea7c3e1 100644 --- a/packages/compiler-cli/src/ngcc/test/analyzer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/analyzer_spec.ts @@ -9,7 +9,7 @@ import * as ts from 'typescript'; import {Decorator} from '../../ngtsc/host'; import {DecoratorHandler} from '../../ngtsc/transform'; import {AnalyzedFile, Analyzer} from '../src/analyzer'; -import {Esm2015ReflectionHost} from '../src/host/esm2015_host'; +import {Fesm2015ReflectionHost} from '../src/host/fesm2015_host'; import {ParsedClass} from '../src/parsing/parsed_class'; import {ParsedFile} from '../src/parsing/parsed_file'; import {getDeclaration, makeProgram} from './helpers/utils'; @@ -80,7 +80,7 @@ describe('Analyzer', () => { program = makeProgram(TEST_PROGRAM); const file = createParsedFile(program); const analyzer = new Analyzer( - program.getTypeChecker(), new Esm2015ReflectionHost(program.getTypeChecker())); + program.getTypeChecker(), new Fesm2015ReflectionHost(program.getTypeChecker())); testHandler = createTestHandler(); analyzer.handlers = [testHandler]; result = analyzer.analyzeFile(file); diff --git a/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts b/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts index 03a35831cde1..5296927c8962 100644 --- a/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts @@ -6,1014 +6,50 @@ * found in the LICENSE file at https://angular.io/license */ +import * as fs from 'fs'; import * as ts from 'typescript'; -import {ClassMemberKind, Import} from '../../../ngtsc/host'; + +import {DtsMapper} from '../../src/host/dts_mapper'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {getDeclaration, makeProgram} from '../helpers/utils'; -const SOME_DIRECTIVE_FILE = { - name: '/some_directive.js', - contents: ` - import { Directive, Inject, InjectionToken, Input, HostListener, HostBinding } from '@angular/core'; - - const INJECTED_TOKEN = new InjectionToken('injected'); - const ViewContainerRef = {}; - const TemplateRef = {}; - - class SomeDirective { - constructor(_viewContainer, _template, injected) { - this.instanceProperty = 'instance'; - } - instanceMethod() {} - - onClick() {} - - @HostBinding('class.foo') - get isClassFoo() { return false; } - - static staticMethod() {} - } - SomeDirective.staticProperty = 'static'; - SomeDirective.decorators = [ - { type: Directive, args: [{ selector: '[someDirective]' },] } - ]; - SomeDirective.ctorParameters = () => [ - { type: ViewContainerRef, }, - { type: TemplateRef, }, - { type: undefined, decorators: [{ type: Inject, args: [INJECTED_TOKEN,] },] }, - ]; - SomeDirective.propDecorators = { - "input1": [{ type: Input },], - "input2": [{ type: Input },], - "target": [{ type: HostBinding, args: ['attr.target',] }, { type: Input },], - "onClick": [{ type: HostListener, args: ['click',] },], - }; - `, -}; - -const SIMPLE_CLASS_FILE = { - name: '/simple_class.js', - contents: ` - class EmptyClass {} - class NoDecoratorConstructorClass { - constructor(foo) {} - } - `, -}; - -const FOO_FUNCTION_FILE = { - name: '/foo_function.js', - contents: ` - import { Directive } from '@angular/core'; - - function foo() {} - foo.decorators = [ - { type: Directive, args: [{ selector: '[ignored]' },] } - ]; - `, -}; - -const INVALID_DECORATORS_FILE = { - name: '/invalid_decorators.js', - contents: ` - const NotArrayLiteralDecorator = {}; - class NotArrayLiteral { - } - NotArrayLiteral.decorators = () => [ - { type: NotArrayLiteralDecorator, args: [{ selector: '[ignored]' },] }, - ]; - - const NotObjectLiteralDecorator = {}; - class NotObjectLiteral { - } - NotObjectLiteral.decorators = [ - "This is not an object literal", - { type: NotObjectLiteralDecorator }, - ]; - - const NoTypePropertyDecorator1 = {}; - const NoTypePropertyDecorator2 = {}; - class NoTypeProperty { - } - NoTypeProperty.decorators = [ - { notType: NoTypePropertyDecorator1 }, - { type: NoTypePropertyDecorator2 }, - ]; - - const NotIdentifierDecorator = {}; - class NotIdentifier { - } - NotIdentifier.decorators = [ - { type: 'StringsLiteralsAreNotIdentifiers' }, - { type: NotIdentifierDecorator }, - ]; - `, -}; - -const INVALID_DECORATOR_ARGS_FILE = { - name: '/invalid_decorator_args.js', - contents: ` - const NoArgsPropertyDecorator = {}; - class NoArgsProperty { - } - NoArgsProperty.decorators = [ - { type: NoArgsPropertyDecorator }, - ]; - - const NoPropertyAssignmentDecorator = {}; - const args = [{ selector: '[ignored]' },]; - class NoPropertyAssignment { - } - NoPropertyAssignment.decorators = [ - { type: NoPropertyAssignmentDecorator, args }, - ]; - - const NotArrayLiteralDecorator = {}; - class NotArrayLiteral { - } - NotArrayLiteral.decorators = [ - { type: NotArrayLiteralDecorator, args: () => [{ selector: '[ignored]' },] }, - ]; - `, -}; - -const INVALID_PROP_DECORATORS_FILE = { - name: '/invalid_prop_decorators.js', - contents: ` - const NotObjectLiteralDecorator = {}; - class NotObjectLiteral { - } - NotObjectLiteral.propDecorators = () => ({ - "prop": [{ type: NotObjectLiteralDecorator },] - }); - - const NotObjectLiteralPropDecorator = {}; - class NotObjectLiteralProp { - } - NotObjectLiteralProp.propDecorators = { - "prop": [ - "This is not an object literal", - { type: NotObjectLiteralPropDecorator }, - ] - }; - - const NoTypePropertyDecorator1 = {}; - const NoTypePropertyDecorator2 = {}; - class NoTypeProperty { - } - NoTypeProperty.propDecorators = { - "prop": [ - { notType: NoTypePropertyDecorator1 }, - { type: NoTypePropertyDecorator2 }, - ] - }; - - const NotIdentifierDecorator = {}; - class NotIdentifier { - } - NotIdentifier.propDecorators = { - "prop": [ - { type: 'StringsLiteralsAreNotIdentifiers' }, - { type: NotIdentifierDecorator }, - ] - }; - `, -}; - -const INVALID_PROP_DECORATOR_ARGS_FILE = { - name: '/invalid_prop_decorator_args.js', - contents: ` - const NoArgsPropertyDecorator = {}; - class NoArgsProperty { - } - NoArgsProperty.propDecorators = { - "prop": [{ type: NoArgsPropertyDecorator },] - }; - - const NoPropertyAssignmentDecorator = {}; - const args = [{ selector: '[ignored]' },]; - class NoPropertyAssignment { - } - NoPropertyAssignment.propDecorators = { - "prop": [{ type: NoPropertyAssignmentDecorator, args },] - }; - - const NotArrayLiteralDecorator = {}; - class NotArrayLiteral { - } - NotArrayLiteral.propDecorators = { - "prop": [{ type: NotArrayLiteralDecorator, args: () => [{ selector: '[ignored]' },] },], - }; - `, -}; - -const INVALID_CTOR_DECORATORS_FILE = { - name: '/invalid_ctor_decorators.js', - contents: ` - const NoParametersDecorator = {}; - class NoParameters { - constructor() { - } - } - - const NotArrowFunctionDecorator = {}; - class NotArrowFunction { - constructor(arg1) { - } - } - NotArrowFunction.ctorParameters = function() { - return { type: 'ParamType', decorators: [{ type: NotArrowFunctionDecorator },] }; - }; - - const NotArrayLiteralDecorator = {}; - class NotArrayLiteral { - constructor(arg1) { - } - } - NotArrayLiteral.ctorParameters = () => 'StringsAreNotArrayLiterals'; - - const NotObjectLiteralDecorator = {}; - class NotObjectLiteral { - constructor(arg1, arg2) { - } - } - NotObjectLiteral.ctorParameters = () => [ - "This is not an object literal", - { type: 'ParamType', decorators: [{ type: NotObjectLiteralDecorator },] }, - ]; - - const NoTypePropertyDecorator1 = {}; - const NoTypePropertyDecorator2 = {}; - class NoTypeProperty { - constructor(arg1, arg2) { - } - } - NoTypeProperty.ctorParameters = () => [ - { - type: 'ParamType', - decorators: [ - { notType: NoTypePropertyDecorator1 }, - { type: NoTypePropertyDecorator2 }, - ] - }, - ]; - - const NotIdentifierDecorator = {}; - class NotIdentifier { - constructor(arg1, arg2) { - } - } - NotIdentifier.ctorParameters = () => [ - { - type: 'ParamType', - decorators: [ - { type: 'StringsLiteralsAreNotIdentifiers' }, - { type: NotIdentifierDecorator }, - ] - }, - ]; - `, -}; - -const INVALID_CTOR_DECORATOR_ARGS_FILE = { - name: '/invalid_ctor_decorator_args.js', - contents: ` - const NoArgsPropertyDecorator = {}; - class NoArgsProperty { - constructor(arg1) { - } - } - NoArgsProperty.ctorParameters = () => [ - { type: 'ParamType', decorators: [{ type: NoArgsPropertyDecorator },] }, - ]; - - const NoPropertyAssignmentDecorator = {}; - const args = [{ selector: '[ignored]' },]; - class NoPropertyAssignment { - constructor(arg1) { - } - } - NoPropertyAssignment.ctorParameters = () => [ - { type: 'ParamType', decorators: [{ type: NoPropertyAssignmentDecorator, args },] }, - ]; - - const NotArrayLiteralDecorator = {}; - class NotArrayLiteral { - constructor(arg1) { - } - } - NotArrayLiteral.ctorParameters = () => [ - { type: 'ParamType', decorators: [{ type: NotArrayLiteralDecorator, args: () => [{ selector: '[ignored]' },] },] }, - ]; - `, -}; - -const IMPORTS_FILES = [ +const CLASSES = [ { - name: '/a.js', + name: '/src/class.js', contents: ` - export const a = 'a'; + export class NoTypeParam {} + export class OneTypeParam {} + export class TwoTypeParams {} `, }, { - name: '/b.js', + name: '/typings/class.d.ts', contents: ` - import {a} from './a.js'; - import {a as foo} from './a.js'; - - const b = a; - const c = foo; - const d = b; - `, - }, -]; - -const EXPORTS_FILES = [ - { - name: '/a.js', - contents: ` - export const a = 'a'; - `, - }, - { - name: '/b.js', - contents: ` - import {Directive} from '@angular/core'; - import {a} from './a'; - import {a as foo} from './a'; - export {Directive} from '@angular/core'; - export {a} from './a'; - export const b = a; - export const c = foo; - export const d = b; - export const e = 'e'; - export const DirectiveX = Directive; - export class SomeClass {} + export class NoTypeParam {} + export class OneTypeParam {} + export class TwoTypeParams {} `, }, ]; describe('Esm2015ReflectionHost', () => { - - describe('getDecoratorsOfDeclaration()', () => { - it('should find the decorators on a class', () => { - const program = makeProgram(SOME_DIRECTIVE_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = - getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); - 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 = makeProgram(FOO_FUNCTION_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const functionNode = - getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', ts.isFunctionDeclaration); - const decorators = host.getDecoratorsOfDeclaration(functionNode); - expect(decorators).toBe(null); - }); - - it('should return null if there are no decorators', () => { - const program = makeProgram(SIMPLE_CLASS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = - getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isClassDeclaration); - const decorators = host.getDecoratorsOfDeclaration(classNode); - expect(decorators).toBe(null); - }); - - it('should ignore `decorators` if it is not an array literal', () => { - const program = makeProgram(INVALID_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_DECORATORS_FILE.name, 'NotArrayLiteral', ts.isClassDeclaration); - const decorators = host.getDecoratorsOfDeclaration(classNode); - expect(decorators).toEqual([]); - }); - - it('should ignore decorator elements that are not object literals', () => { - const program = makeProgram(INVALID_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_DECORATORS_FILE.name, 'NotObjectLiteral', ts.isClassDeclaration); - const decorators = host.getDecoratorsOfDeclaration(classNode) !; - - expect(decorators.length).toBe(1); - expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NotObjectLiteralDecorator'})); - }); - - it('should ignore decorator elements that have no `type` property', () => { - const program = makeProgram(INVALID_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_DECORATORS_FILE.name, 'NoTypeProperty', ts.isClassDeclaration); - const decorators = host.getDecoratorsOfDeclaration(classNode) !; - - expect(decorators.length).toBe(1); - expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NoTypePropertyDecorator2'})); - }); - - it('should ignore decorator elements whose `type` value is not an identifier', () => { - const program = makeProgram(INVALID_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_DECORATORS_FILE.name, 'NotIdentifier', ts.isClassDeclaration); - const decorators = host.getDecoratorsOfDeclaration(classNode) !; - - expect(decorators.length).toBe(1); - expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NotIdentifierDecorator'})); - }); - - it('should use `getImportOfIdentifier()` to retrieve import info', () => { - const mockImportInfo = {} as Import; - const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier') - .and.returnValue(mockImportInfo); - - const program = makeProgram(SOME_DIRECTIVE_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = - getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); - const decorators = host.getDecoratorsOfDeclaration(classNode) !; - - expect(decorators.length).toEqual(1); - expect(decorators[0].import).toBe(mockImportInfo); - - const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier; - expect(typeIdentifier.text).toBe('Directive'); - }); - - describe('(returned decorators `args`)', () => { - it('should be an empty array if decorator has no `args` property', () => { - const program = makeProgram(INVALID_DECORATOR_ARGS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', ts.isClassDeclaration); - const decorators = host.getDecoratorsOfDeclaration(classNode) !; - - expect(decorators.length).toBe(1); - expect(decorators[0].name).toBe('NoArgsPropertyDecorator'); - expect(decorators[0].args).toEqual([]); - }); - - it('should be an empty array if decorator\'s `args` has no property assignment', () => { - const program = makeProgram(INVALID_DECORATOR_ARGS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', - ts.isClassDeclaration); - const decorators = host.getDecoratorsOfDeclaration(classNode) !; - - expect(decorators.length).toBe(1); - expect(decorators[0].name).toBe('NoPropertyAssignmentDecorator'); - expect(decorators[0].args).toEqual([]); - }); - - it('should be an empty array if `args` property value is not an array literal', () => { - const program = makeProgram(INVALID_DECORATOR_ARGS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', ts.isClassDeclaration); - const decorators = host.getDecoratorsOfDeclaration(classNode) !; - - expect(decorators.length).toBe(1); - expect(decorators[0].name).toBe('NotArrayLiteralDecorator'); - expect(decorators[0].args).toEqual([]); - }); - }); - }); - - describe('getMembersOfClass()', () => { - it('should find decorated properties on a class', () => { - const program = makeProgram(SOME_DIRECTIVE_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = - getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); - 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 = makeProgram(SOME_DIRECTIVE_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = - getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); - const members = host.getMembersOfClass(classNode); - - const instanceProperty = members.find(member => member.name === 'instanceProperty') !; - expect(instanceProperty.kind).toEqual(ClassMemberKind.Property); - expect(instanceProperty.isStatic).toEqual(false); - expect(ts.isBinaryExpression(instanceProperty.implementation !)).toEqual(true); - expect(instanceProperty.value !.getText()).toEqual(`'instance'`); - }); - - it('should find static methods on a class', () => { - const program = makeProgram(SOME_DIRECTIVE_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = - getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); - const members = host.getMembersOfClass(classNode); - - const staticMethod = members.find(member => member.name === 'staticMethod') !; - expect(staticMethod.kind).toEqual(ClassMemberKind.Method); - expect(staticMethod.isStatic).toEqual(true); - expect(ts.isMethodDeclaration(staticMethod.implementation !)).toEqual(true); - }); - - it('should find static properties on a class', () => { - const program = makeProgram(SOME_DIRECTIVE_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = - getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); - const members = host.getMembersOfClass(classNode); - - const staticProperty = members.find(member => member.name === 'staticProperty') !; - expect(staticProperty.kind).toEqual(ClassMemberKind.Property); - expect(staticProperty.isStatic).toEqual(true); - expect(ts.isPropertyAccessExpression(staticProperty.implementation !)).toEqual(true); - expect(staticProperty.value !.getText()).toEqual(`'static'`); - }); - - it('should throw if the symbol is not a class', () => { - const program = makeProgram(FOO_FUNCTION_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const functionNode = - getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', ts.isFunctionDeclaration); - expect(() => { - host.getMembersOfClass(functionNode); - }).toThrowError(`Attempted to get members of a non-class: "function foo() {}"`); - }); - - it('should return an empty array if there are no prop decorators', () => { - const program = makeProgram(SIMPLE_CLASS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = - getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isClassDeclaration); - const members = host.getMembersOfClass(classNode); - - expect(members).toEqual([]); - }); - - it('should not process decorated properties in `propDecorators` if it is not an object literal', - () => { - const program = makeProgram(INVALID_PROP_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_PROP_DECORATORS_FILE.name, 'NotObjectLiteral', ts.isClassDeclaration); - const members = host.getMembersOfClass(classNode); - - expect(members.map(member => member.name)).not.toContain('prop'); - }); - - it('should ignore prop decorator elements that are not object literals', () => { - const program = makeProgram(INVALID_PROP_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_PROP_DECORATORS_FILE.name, 'NotObjectLiteralProp', - ts.isClassDeclaration); - const members = host.getMembersOfClass(classNode); - const prop = members.find(m => m.name === 'prop') !; - const decorators = prop.decorators !; - - expect(decorators.length).toBe(1); - expect(decorators[0]).toEqual(jasmine.objectContaining({ - name: 'NotObjectLiteralPropDecorator' - })); - }); - - it('should ignore prop decorator elements that have no `type` property', () => { - const program = makeProgram(INVALID_PROP_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_PROP_DECORATORS_FILE.name, 'NoTypeProperty', ts.isClassDeclaration); - const members = host.getMembersOfClass(classNode); - const prop = members.find(m => m.name === 'prop') !; - const decorators = prop.decorators !; - - expect(decorators.length).toBe(1); - expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NoTypePropertyDecorator2'})); - }); - - it('should ignore prop decorator elements whose `type` value is not an identifier', () => { - const program = makeProgram(INVALID_PROP_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_PROP_DECORATORS_FILE.name, 'NotIdentifier', ts.isClassDeclaration); - const members = host.getMembersOfClass(classNode); - const prop = members.find(m => m.name === 'prop') !; - const decorators = prop.decorators !; - - expect(decorators.length).toBe(1); - expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NotIdentifierDecorator'})); - }); - - it('should use `getImportOfIdentifier()` to retrieve import info', () => { - let callCount = 0; - const spy = - spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier').and.callFake(() => { - callCount++; - return {name: `name${callCount}`, from: `from${callCount}`}; - }); - - const program = makeProgram(SOME_DIRECTIVE_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = - getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); - const members = host.getMembersOfClass(classNode); - - expect(spy).toHaveBeenCalled(); - expect(spy.calls.allArgs().map(arg => arg[0].getText())).toEqual([ - 'Input', - 'Input', - 'HostBinding', - 'Input', - 'HostListener', - ]); - - const index = members.findIndex(member => member.name === 'input1'); - expect(members[index].decorators !.length).toBe(1); - expect(members[index].decorators ![0].import).toEqual({name: 'name1', from: 'from1'}); - }); - - describe('(returned prop decorators `args`)', () => { - it('should be an empty array if prop decorator has no `args` property', () => { - const program = makeProgram(INVALID_PROP_DECORATOR_ARGS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', - ts.isClassDeclaration); - const members = host.getMembersOfClass(classNode); - const prop = members.find(m => m.name === 'prop') !; - const decorators = prop.decorators !; - - expect(decorators.length).toBe(1); - expect(decorators[0].name).toBe('NoArgsPropertyDecorator'); - expect(decorators[0].args).toEqual([]); - }); - - it('should be an empty array if prop decorator\'s `args` has no property assignment', () => { - const program = makeProgram(INVALID_PROP_DECORATOR_ARGS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', - ts.isClassDeclaration); - const members = host.getMembersOfClass(classNode); - const prop = members.find(m => m.name === 'prop') !; - const decorators = prop.decorators !; - - expect(decorators.length).toBe(1); - expect(decorators[0].name).toBe('NoPropertyAssignmentDecorator'); - expect(decorators[0].args).toEqual([]); - }); - - it('should be an empty array if `args` property value is not an array literal', () => { - const program = makeProgram(INVALID_PROP_DECORATOR_ARGS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', - ts.isClassDeclaration); - const members = host.getMembersOfClass(classNode); - const prop = members.find(m => m.name === 'prop') !; - const decorators = prop.decorators !; - - expect(decorators.length).toBe(1); - expect(decorators[0].name).toBe('NotArrayLiteralDecorator'); - expect(decorators[0].args).toEqual([]); - }); - }); - }); - - describe('getConstructorParameters', () => { - it('should find the decorated constructor parameters', () => { - const program = makeProgram(SOME_DIRECTIVE_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = - getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); - const parameters = host.getConstructorParameters(classNode); - - expect(parameters).toBeDefined(); - expect(parameters !.map(parameter => parameter.name)).toEqual([ - '_viewContainer', '_template', 'injected' - ]); - expect(parameters !.map(parameter => parameter.type !.getText())).toEqual([ - 'ViewContainerRef', 'TemplateRef', 'undefined' - ]); - }); - - it('should throw if the symbol is not a class', () => { - const program = makeProgram(FOO_FUNCTION_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const functionNode = - getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', ts.isFunctionDeclaration); - expect(() => { host.getConstructorParameters(functionNode); }) - .toThrowError( - 'Attempted to get constructor parameters of a non-class: "function foo() {}"'); - }); - - it('should return `null` if there is no constructor', () => { - const program = makeProgram(SIMPLE_CLASS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = - getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isClassDeclaration); - const parameters = host.getConstructorParameters(classNode); - expect(parameters).toBe(null); - }); - - it('should return an array even if there are no decorators', () => { - const program = makeProgram(SIMPLE_CLASS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = getDeclaration( - program, SIMPLE_CLASS_FILE.name, 'NoDecoratorConstructorClass', ts.isClassDeclaration); - const parameters = host.getConstructorParameters(classNode); - - expect(parameters).toEqual(jasmine.any(Array)); - expect(parameters !.length).toEqual(1); - expect(parameters ![0].name).toEqual('foo'); - expect(parameters ![0].decorators).toBe(null); - }); - - it('should return an empty array if there are no constructor parameters', () => { - const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_CTOR_DECORATORS_FILE.name, 'NoParameters', ts.isClassDeclaration); - const parameters = host.getConstructorParameters(classNode); - - expect(parameters).toEqual([]); - }); - - it('should ignore `ctorParameters` if it is not an arrow function', () => { - const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_CTOR_DECORATORS_FILE.name, 'NotArrowFunction', ts.isClassDeclaration); - const parameters = host.getConstructorParameters(classNode); - - expect(parameters !.length).toBe(1); - expect(parameters ![0]).toEqual(jasmine.objectContaining({ - name: 'arg1', - decorators: null, - })); - }); - - it('should ignore `ctorParameters` if it does not return an array literal', () => { - const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_CTOR_DECORATORS_FILE.name, 'NotArrayLiteral', ts.isClassDeclaration); - const parameters = host.getConstructorParameters(classNode); - - expect(parameters !.length).toBe(1); - expect(parameters ![0]).toEqual(jasmine.objectContaining({ - name: 'arg1', - decorators: null, - })); - }); - - describe('(returned parameters `decorators`)', () => { - it('should ignore param decorator elements that are not object literals', () => { - const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_CTOR_DECORATORS_FILE.name, 'NotObjectLiteral', ts.isClassDeclaration); - const parameters = host.getConstructorParameters(classNode); - - expect(parameters !.length).toBe(2); - expect(parameters ![0]).toEqual(jasmine.objectContaining({ - name: 'arg1', - decorators: null, - })); - expect(parameters ![1]).toEqual(jasmine.objectContaining({ - name: 'arg2', - decorators: jasmine.any(Array) as any - })); - }); - - it('should ignore param decorator elements that have no `type` property', () => { - const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_CTOR_DECORATORS_FILE.name, 'NoTypeProperty', ts.isClassDeclaration); - const parameters = host.getConstructorParameters(classNode); - const decorators = parameters ![0].decorators !; - - expect(decorators.length).toBe(1); - expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NoTypePropertyDecorator2'})); - }); - - it('should ignore param decorator elements whose `type` value is not an identifier', () => { - const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_CTOR_DECORATORS_FILE.name, 'NotIdentifier', ts.isClassDeclaration); - const parameters = host.getConstructorParameters(classNode); - const decorators = parameters ![0].decorators !; - - expect(decorators.length).toBe(1); - expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NotIdentifierDecorator'})); - }); - - it('should use `getImportOfIdentifier()` to retrieve import info', () => { - const mockImportInfo = {} as Import; - const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier') - .and.returnValue(mockImportInfo); - - const program = makeProgram(SOME_DIRECTIVE_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = getDeclaration( - program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); - const parameters = host.getConstructorParameters(classNode); - const decorators = parameters ![2].decorators !; - - expect(decorators.length).toEqual(1); - expect(decorators[0].import).toBe(mockImportInfo); - - const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier; - expect(typeIdentifier.text).toBe('Inject'); - }); - }); - - describe('(returned parameters `decorators.args`)', () => { - it('should be an empty array if param decorator has no `args` property', () => { - const program = makeProgram(INVALID_CTOR_DECORATOR_ARGS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', - ts.isClassDeclaration); - const parameters = host.getConstructorParameters(classNode); - expect(parameters !.length).toBe(1); - const decorators = parameters ![0].decorators !; - - expect(decorators.length).toBe(1); - expect(decorators[0].name).toBe('NoArgsPropertyDecorator'); - expect(decorators[0].args).toEqual([]); - }); - - it('should be an empty array if param decorator\'s `args` has no property assignment', () => { - const program = makeProgram(INVALID_CTOR_DECORATOR_ARGS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', - ts.isClassDeclaration); - const parameters = host.getConstructorParameters(classNode); - const decorators = parameters ![0].decorators !; - - expect(decorators.length).toBe(1); - expect(decorators[0].name).toBe('NoPropertyAssignmentDecorator'); - expect(decorators[0].args).toEqual([]); - }); - - it('should be an empty array if `args` property value is not an array literal', () => { - const program = makeProgram(INVALID_CTOR_DECORATOR_ARGS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', - ts.isClassDeclaration); - const parameters = host.getConstructorParameters(classNode); - const decorators = parameters ![0].decorators !; - - expect(decorators.length).toBe(1); - expect(decorators[0].name).toBe('NotArrayLiteralDecorator'); - expect(decorators[0].args).toEqual([]); - }); - }); - }); - - describe('getImportOfIdentifier', () => { - it('should find the import of an identifier', () => { - const program = makeProgram(...IMPORTS_FILES); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const variableNode = - getDeclaration(program, IMPORTS_FILES[1].name, 'b', ts.isVariableDeclaration); - const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); - - expect(importOfIdent).toEqual({name: 'a', from: './a.js'}); - }); - - it('should find the name by which the identifier was exported, not imported', () => { - const program = makeProgram(...IMPORTS_FILES); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const variableNode = - getDeclaration(program, IMPORTS_FILES[1].name, 'c', ts.isVariableDeclaration); - const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); - - expect(importOfIdent).toEqual({name: 'a', from: './a.js'}); - }); - - it('should return null if the identifier was not imported', () => { - const program = makeProgram(...IMPORTS_FILES); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const variableNode = - getDeclaration(program, IMPORTS_FILES[1].name, 'd', ts.isVariableDeclaration); - const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); - - expect(importOfIdent).toBeNull(); - }); - }); - - describe('getDeclarationOfIdentifier', () => { - it('should return the declaration of a locally defined identifier', () => { - const program = makeProgram(SOME_DIRECTIVE_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = - getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); - const ctrDecorators = host.getConstructorParameters(classNode) !; - const identifierOfViewContainerRef = ctrDecorators[0].type !as ts.Identifier; - - const expectedDeclarationNode = getDeclaration( - program, SOME_DIRECTIVE_FILE.name, 'ViewContainerRef', ts.isVariableDeclaration); - const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfViewContainerRef); - expect(actualDeclaration).not.toBe(null); - expect(actualDeclaration !.node).toBe(expectedDeclarationNode); - expect(actualDeclaration !.viaModule).toBe(null); - }); - - it('should return the declaration of an externally defined identifier', () => { - const program = makeProgram(SOME_DIRECTIVE_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const classNode = - getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); - const classDecorators = host.getDecoratorsOfDeclaration(classNode) !; - const identifierOfDirective = ((classDecorators[0].node as ts.ObjectLiteralExpression) - .properties[0] as ts.PropertyAssignment) - .initializer as ts.Identifier; - - const expectedDeclarationNode = getDeclaration( - program, 'node_modules/@angular/core/index.ts', 'Directive', ts.isVariableDeclaration); - const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective); - expect(actualDeclaration).not.toBe(null); - expect(actualDeclaration !.node).toBe(expectedDeclarationNode); - expect(actualDeclaration !.viaModule).toBe('@angular/core'); - }); - }); - - describe('getExportsOfModule()', () => { - it('should return a map of all the exports from a given module', () => { - const program = makeProgram(...EXPORTS_FILES); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const file = program.getSourceFile(EXPORTS_FILES[1].name) !; - const exportDeclarations = host.getExportsOfModule(file); - expect(exportDeclarations).not.toBe(null); - expect(Array.from(exportDeclarations !.keys())).toEqual([ - 'Directive', - 'a', - 'b', - 'c', - 'd', - 'e', - 'DirectiveX', - 'SomeClass', - ]); - - const values = Array.from(exportDeclarations !.values()) - .map(declaration => [declaration.node.getText(), declaration.viaModule]); - expect(values).toEqual([ - // TODO clarify what is expected here... - // [`Directive = callableClassDecorator()`, '@angular/core'], - [`Directive = callableClassDecorator()`, null], - [`a = 'a'`, null], - [`b = a`, null], - [`c = foo`, null], - [`d = b`, null], - [`e = 'e'`, null], - [`DirectiveX = Directive`, null], - ['export class SomeClass {}', null], - ]); - }); - }); - - describe('isClass()', () => { - it('should return true if a given node is a TS class declaration', () => { - const program = makeProgram(SIMPLE_CLASS_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const node = - getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isClassDeclaration); - expect(host.isClass(node)).toBe(true); - }); - - it('should return false if a given node is a TS function declaration', () => { - const program = makeProgram(FOO_FUNCTION_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); - const node = getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', ts.isFunctionDeclaration); - expect(host.isClass(node)).toBe(false); + describe('getGenericArityOfClass()', () => { + it('should properly count type parameters', () => { + // Mock out reading the `d.ts` file from disk + const readFileSyncSpy = spyOn(fs, 'readFileSync').and.returnValue(CLASSES[1].contents); + const program = makeProgram(CLASSES[0]); + + const dtsMapper = new DtsMapper('/src', '/typings'); + const host = new Esm2015ReflectionHost(program.getTypeChecker(), dtsMapper); + const noTypeParamClass = + getDeclaration(program, '/src/class.js', 'NoTypeParam', ts.isClassDeclaration); + expect(host.getGenericArityOfClass(noTypeParamClass)).toBe(0); + const oneTypeParamClass = + getDeclaration(program, '/src/class.js', 'OneTypeParam', ts.isClassDeclaration); + expect(host.getGenericArityOfClass(oneTypeParamClass)).toBe(1); + const twoTypeParamsClass = + getDeclaration(program, '/src/class.js', 'TwoTypeParams', ts.isClassDeclaration); + expect(host.getGenericArityOfClass(twoTypeParamsClass)).toBe(2); }); }); }); diff --git a/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts b/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts index 172eefb98f8b..01978321f985 100644 --- a/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts @@ -7,8 +7,10 @@ */ import * as ts from 'typescript'; + import {ClassMemberKind, Import} from '../../../ngtsc/host'; import {Esm5ReflectionHost} from '../../src/host/esm5_host'; +import {Fesm2015ReflectionHost} from '../../src/host/fesm2015_host'; import {getDeclaration, makeProgram} from '../helpers/utils'; const SOME_DIRECTIVE_FILE = { @@ -50,7 +52,8 @@ const SIMPLE_CLASS_FILE = { name: '/simple_class.js', contents: ` var EmptyClass = (function() { - function EmptyClass() {} + function EmptyClass() { + } return EmptyClass; }()); var NoDecoratorConstructorClass = (function() { @@ -406,6 +409,43 @@ const EXPORTS_FILES = [ }, ]; +const FUNCTION_BODY_FILE = { + name: '/function_body.js', + contents: ` + function foo(x) { + return x; + } + function bar(x, y) { + if (y === void 0) { y = 42; } + return x + y; + } + function complex() { + var x = 42; + return 42; + } + function baz(x) { + var y; + if (x === void 0) { y = 42; } + return y; + } + var y; + function qux(x) { + if (x === void 0) { y = 42; } + return y; + } + function moo() { + var x; + if (x === void 0) { x = 42; } + return x; + } + var x; + function juu() { + if (x === void 0) { x = 42; } + return x; + } + ` +}; + describe('Esm5ReflectionHost', () => { describe('getDecoratorsOfDeclaration()', () => { @@ -928,6 +968,54 @@ describe('Esm5ReflectionHost', () => { }); }); + describe('getDefinitionOfFunction()', () => { + it('should return an object describing the function declaration passed as an argument', () => { + const program = makeProgram(FUNCTION_BODY_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + + const fooNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'foo', ts.isFunctionDeclaration) !; + const fooDef = host.getDefinitionOfFunction(fooNode); + expect(fooDef.node).toBe(fooNode); + expect(fooDef.body !.length).toEqual(1); + expect(fooDef.body ![0].getText()).toEqual(`return x;`); + expect(fooDef.parameters.length).toEqual(1); + expect(fooDef.parameters[0].name).toEqual('x'); + expect(fooDef.parameters[0].initializer).toBe(null); + + const barNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'bar', ts.isFunctionDeclaration) !; + const barDef = host.getDefinitionOfFunction(barNode); + expect(barDef.node).toBe(barNode); + expect(barDef.body !.length).toEqual(1); + expect(ts.isReturnStatement(barDef.body ![0])).toBeTruthy(); + expect(barDef.body ![0].getText()).toEqual(`return x + y;`); + expect(barDef.parameters.length).toEqual(2); + expect(barDef.parameters[0].name).toEqual('x'); + expect(fooDef.parameters[0].initializer).toBe(null); + expect(barDef.parameters[1].name).toEqual('y'); + expect(barDef.parameters[1].initializer !.getText()).toEqual('42'); + + const bazNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'baz', ts.isFunctionDeclaration) !; + const bazDef = host.getDefinitionOfFunction(bazNode); + expect(bazDef.node).toBe(bazNode); + expect(bazDef.body !.length).toEqual(3); + expect(bazDef.parameters.length).toEqual(1); + expect(bazDef.parameters[0].name).toEqual('x'); + expect(bazDef.parameters[0].initializer).toBe(null); + + const quxNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'qux', ts.isFunctionDeclaration) !; + const quxDef = host.getDefinitionOfFunction(quxNode); + expect(quxDef.node).toBe(quxNode); + expect(quxDef.body !.length).toEqual(2); + expect(quxDef.parameters.length).toEqual(1); + expect(quxDef.parameters[0].name).toEqual('x'); + expect(quxDef.parameters[0].initializer).toBe(null); + }); + }); + describe('getImportOfIdentifier', () => { it('should find the import of an identifier', () => { const program = makeProgram(...IMPORTS_FILES); @@ -1037,20 +1125,104 @@ describe('Esm5ReflectionHost', () => { }); }); - describe('isClass()', () => { - it('should return true if a given node is an ES5 class declaration', () => { + describe('getClassSymbol()', () => { + let superGetClassSymbolSpy: jasmine.Spy; + + beforeEach(() => { + superGetClassSymbolSpy = spyOn(Fesm2015ReflectionHost.prototype, 'getClassSymbol'); + }); + + it('should return the class symbol returned by the superclass (if any)', () => { + const mockNode = {} as ts.Node; + const mockSymbol = {} as ts.Symbol; + superGetClassSymbolSpy.and.returnValue(mockSymbol); + + const host = new Esm5ReflectionHost({} as any); + + expect(host.getClassSymbol(mockNode)).toBe(mockSymbol); + expect(superGetClassSymbolSpy).toHaveBeenCalledWith(mockNode); + }); + + it('should return the class symbol for an ES5 class (outer variable declaration)', () => { const program = makeProgram(SIMPLE_CLASS_FILE); const host = new Esm5ReflectionHost(program.getTypeChecker()); const node = getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration); - expect(host.isClass(node)).toBe(true); + expect(host.getClassSymbol(node)).toBeDefined(); }); - it('should return false if a given node is not an ES5 class declaration', () => { + it('should return the class symbol for an ES5 class (inner function declaration)', () => { + const program = makeProgram(SIMPLE_CLASS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const outerNode = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration); + const innerNode = + (((outerNode.initializer as ts.ParenthesizedExpression).expression as ts.CallExpression) + .expression as ts.FunctionExpression) + .body.statements.find(ts.isFunctionDeclaration) !; + + expect(host.getClassSymbol(innerNode)).toBeDefined(); + }); + + it('should return the same class symbol for outer and inner declarations', () => { + const program = makeProgram(SIMPLE_CLASS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const outerNode = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration); + const innerNode = + (((outerNode.initializer as ts.ParenthesizedExpression).expression as ts.CallExpression) + .expression as ts.FunctionExpression) + .body.statements.find(ts.isFunctionDeclaration) !; + + expect(host.getClassSymbol(innerNode)).toBe(host.getClassSymbol(outerNode)); + }); + + it('should return undefined if node is not an ES5 class', () => { const program = makeProgram(FOO_FUNCTION_FILE); const host = new Esm5ReflectionHost(program.getTypeChecker()); const node = getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', ts.isFunctionDeclaration); - expect(host.isClass(node)).toBe(false); + expect(host.getClassSymbol(node)).toBeUndefined(); + }); + }); + + describe('isClass()', () => { + let host: Esm5ReflectionHost; + let mockNode: ts.Node; + let superIsClassSpy: jasmine.Spy; + let getClassSymbolSpy: jasmine.Spy; + + beforeEach(() => { + host = new Esm5ReflectionHost(null as any); + mockNode = {} as any; + + superIsClassSpy = spyOn(Fesm2015ReflectionHost.prototype, 'isClass'); + getClassSymbolSpy = spyOn(Esm5ReflectionHost.prototype, 'getClassSymbol'); + }); + + it('should return true if superclass returns true', () => { + superIsClassSpy.and.returnValue(true); + + expect(host.isClass(mockNode)).toBe(true); + expect(superIsClassSpy).toHaveBeenCalledWith(mockNode); + expect(getClassSymbolSpy).not.toHaveBeenCalled(); + }); + + it('should return true if it can find a symbol for the class', () => { + superIsClassSpy.and.returnValue(false); + getClassSymbolSpy.and.returnValue(true); + + expect(host.isClass(mockNode)).toBe(true); + expect(superIsClassSpy).toHaveBeenCalledWith(mockNode); + expect(getClassSymbolSpy).toHaveBeenCalledWith(mockNode); + }); + + it('should return false if it cannot find a symbol for the class', () => { + superIsClassSpy.and.returnValue(false); + getClassSymbolSpy.and.returnValue(false); + + expect(host.isClass(mockNode)).toBe(false); + expect(superIsClassSpy).toHaveBeenCalledWith(mockNode); + expect(getClassSymbolSpy).toHaveBeenCalledWith(mockNode); }); }); }); diff --git a/packages/compiler-cli/src/ngcc/test/host/fesm2015_host_spec.ts b/packages/compiler-cli/src/ngcc/test/host/fesm2015_host_spec.ts new file mode 100644 index 000000000000..b2e0b9d6a426 --- /dev/null +++ b/packages/compiler-cli/src/ngcc/test/host/fesm2015_host_spec.ts @@ -0,0 +1,1123 @@ +/** + * @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 {ClassMemberKind, Import} from '../../../ngtsc/host'; +import {Fesm2015ReflectionHost} from '../../src/host/fesm2015_host'; +import {getDeclaration, makeProgram} from '../helpers/utils'; + +const SOME_DIRECTIVE_FILE = { + name: '/some_directive.js', + contents: ` + import { Directive, Inject, InjectionToken, Input, HostListener, HostBinding } from '@angular/core'; + + const INJECTED_TOKEN = new InjectionToken('injected'); + const ViewContainerRef = {}; + const TemplateRef = {}; + + class SomeDirective { + constructor(_viewContainer, _template, injected) { + this.instanceProperty = 'instance'; + } + instanceMethod() {} + + onClick() {} + + @HostBinding('class.foo') + get isClassFoo() { return false; } + + static staticMethod() {} + } + SomeDirective.staticProperty = 'static'; + SomeDirective.decorators = [ + { type: Directive, args: [{ selector: '[someDirective]' },] } + ]; + SomeDirective.ctorParameters = () => [ + { type: ViewContainerRef, }, + { type: TemplateRef, }, + { type: undefined, decorators: [{ type: Inject, args: [INJECTED_TOKEN,] },] }, + ]; + SomeDirective.propDecorators = { + "input1": [{ type: Input },], + "input2": [{ type: Input },], + "target": [{ type: HostBinding, args: ['attr.target',] }, { type: Input },], + "onClick": [{ type: HostListener, args: ['click',] },], + }; + `, +}; + +const SIMPLE_CLASS_FILE = { + name: '/simple_class.js', + contents: ` + class EmptyClass {} + class NoDecoratorConstructorClass { + constructor(foo) {} + } + `, +}; + +const FOO_FUNCTION_FILE = { + name: '/foo_function.js', + contents: ` + import { Directive } from '@angular/core'; + + function foo() {} + foo.decorators = [ + { type: Directive, args: [{ selector: '[ignored]' },] } + ]; + `, +}; + +const INVALID_DECORATORS_FILE = { + name: '/invalid_decorators.js', + contents: ` + const NotArrayLiteralDecorator = {}; + class NotArrayLiteral { + } + NotArrayLiteral.decorators = () => [ + { type: NotArrayLiteralDecorator, args: [{ selector: '[ignored]' },] }, + ]; + + const NotObjectLiteralDecorator = {}; + class NotObjectLiteral { + } + NotObjectLiteral.decorators = [ + "This is not an object literal", + { type: NotObjectLiteralDecorator }, + ]; + + const NoTypePropertyDecorator1 = {}; + const NoTypePropertyDecorator2 = {}; + class NoTypeProperty { + } + NoTypeProperty.decorators = [ + { notType: NoTypePropertyDecorator1 }, + { type: NoTypePropertyDecorator2 }, + ]; + + const NotIdentifierDecorator = {}; + class NotIdentifier { + } + NotIdentifier.decorators = [ + { type: 'StringsLiteralsAreNotIdentifiers' }, + { type: NotIdentifierDecorator }, + ]; + `, +}; + +const INVALID_DECORATOR_ARGS_FILE = { + name: '/invalid_decorator_args.js', + contents: ` + const NoArgsPropertyDecorator = {}; + class NoArgsProperty { + } + NoArgsProperty.decorators = [ + { type: NoArgsPropertyDecorator }, + ]; + + const NoPropertyAssignmentDecorator = {}; + const args = [{ selector: '[ignored]' },]; + class NoPropertyAssignment { + } + NoPropertyAssignment.decorators = [ + { type: NoPropertyAssignmentDecorator, args }, + ]; + + const NotArrayLiteralDecorator = {}; + class NotArrayLiteral { + } + NotArrayLiteral.decorators = [ + { type: NotArrayLiteralDecorator, args: () => [{ selector: '[ignored]' },] }, + ]; + `, +}; + +const INVALID_PROP_DECORATORS_FILE = { + name: '/invalid_prop_decorators.js', + contents: ` + const NotObjectLiteralDecorator = {}; + class NotObjectLiteral { + } + NotObjectLiteral.propDecorators = () => ({ + "prop": [{ type: NotObjectLiteralDecorator },] + }); + + const NotObjectLiteralPropDecorator = {}; + class NotObjectLiteralProp { + } + NotObjectLiteralProp.propDecorators = { + "prop": [ + "This is not an object literal", + { type: NotObjectLiteralPropDecorator }, + ] + }; + + const NoTypePropertyDecorator1 = {}; + const NoTypePropertyDecorator2 = {}; + class NoTypeProperty { + } + NoTypeProperty.propDecorators = { + "prop": [ + { notType: NoTypePropertyDecorator1 }, + { type: NoTypePropertyDecorator2 }, + ] + }; + + const NotIdentifierDecorator = {}; + class NotIdentifier { + } + NotIdentifier.propDecorators = { + "prop": [ + { type: 'StringsLiteralsAreNotIdentifiers' }, + { type: NotIdentifierDecorator }, + ] + }; + `, +}; + +const INVALID_PROP_DECORATOR_ARGS_FILE = { + name: '/invalid_prop_decorator_args.js', + contents: ` + const NoArgsPropertyDecorator = {}; + class NoArgsProperty { + } + NoArgsProperty.propDecorators = { + "prop": [{ type: NoArgsPropertyDecorator },] + }; + + const NoPropertyAssignmentDecorator = {}; + const args = [{ selector: '[ignored]' },]; + class NoPropertyAssignment { + } + NoPropertyAssignment.propDecorators = { + "prop": [{ type: NoPropertyAssignmentDecorator, args },] + }; + + const NotArrayLiteralDecorator = {}; + class NotArrayLiteral { + } + NotArrayLiteral.propDecorators = { + "prop": [{ type: NotArrayLiteralDecorator, args: () => [{ selector: '[ignored]' },] },], + }; + `, +}; + +const INVALID_CTOR_DECORATORS_FILE = { + name: '/invalid_ctor_decorators.js', + contents: ` + const NoParametersDecorator = {}; + class NoParameters { + constructor() { + } + } + + const NotArrowFunctionDecorator = {}; + class NotArrowFunction { + constructor(arg1) { + } + } + NotArrowFunction.ctorParameters = function() { + return { type: 'ParamType', decorators: [{ type: NotArrowFunctionDecorator },] }; + }; + + const NotArrayLiteralDecorator = {}; + class NotArrayLiteral { + constructor(arg1) { + } + } + NotArrayLiteral.ctorParameters = () => 'StringsAreNotArrayLiterals'; + + const NotObjectLiteralDecorator = {}; + class NotObjectLiteral { + constructor(arg1, arg2) { + } + } + NotObjectLiteral.ctorParameters = () => [ + "This is not an object literal", + { type: 'ParamType', decorators: [{ type: NotObjectLiteralDecorator },] }, + ]; + + const NoTypePropertyDecorator1 = {}; + const NoTypePropertyDecorator2 = {}; + class NoTypeProperty { + constructor(arg1, arg2) { + } + } + NoTypeProperty.ctorParameters = () => [ + { + type: 'ParamType', + decorators: [ + { notType: NoTypePropertyDecorator1 }, + { type: NoTypePropertyDecorator2 }, + ] + }, + ]; + + const NotIdentifierDecorator = {}; + class NotIdentifier { + constructor(arg1, arg2) { + } + } + NotIdentifier.ctorParameters = () => [ + { + type: 'ParamType', + decorators: [ + { type: 'StringsLiteralsAreNotIdentifiers' }, + { type: NotIdentifierDecorator }, + ] + }, + ]; + `, +}; + +const INVALID_CTOR_DECORATOR_ARGS_FILE = { + name: '/invalid_ctor_decorator_args.js', + contents: ` + const NoArgsPropertyDecorator = {}; + class NoArgsProperty { + constructor(arg1) { + } + } + NoArgsProperty.ctorParameters = () => [ + { type: 'ParamType', decorators: [{ type: NoArgsPropertyDecorator },] }, + ]; + + const NoPropertyAssignmentDecorator = {}; + const args = [{ selector: '[ignored]' },]; + class NoPropertyAssignment { + constructor(arg1) { + } + } + NoPropertyAssignment.ctorParameters = () => [ + { type: 'ParamType', decorators: [{ type: NoPropertyAssignmentDecorator, args },] }, + ]; + + const NotArrayLiteralDecorator = {}; + class NotArrayLiteral { + constructor(arg1) { + } + } + NotArrayLiteral.ctorParameters = () => [ + { type: 'ParamType', decorators: [{ type: NotArrayLiteralDecorator, args: () => [{ selector: '[ignored]' },] },] }, + ]; + `, +}; + +const IMPORTS_FILES = [ + { + name: '/a.js', + contents: ` + export const a = 'a'; + `, + }, + { + name: '/b.js', + contents: ` + import {a} from './a.js'; + import {a as foo} from './a.js'; + + const b = a; + const c = foo; + const d = b; + `, + }, +]; + +const EXPORTS_FILES = [ + { + name: '/a.js', + contents: ` + export const a = 'a'; + `, + }, + { + name: '/b.js', + contents: ` + import {Directive} from '@angular/core'; + import {a} from './a'; + import {a as foo} from './a'; + export {Directive} from '@angular/core'; + export {a} from './a'; + export const b = a; + export const c = foo; + export const d = b; + export const e = 'e'; + export const DirectiveX = Directive; + export class SomeClass {} + `, + }, +]; + +const FUNCTION_BODY_FILE = { + name: '/function_body.js', + contents: ` + function foo(x) { + return x; + } + function bar(x, y = 42) { + return x + y; + } + function baz(x) { + let y; + if (y === void 0) { y = 42; } + return x; + } + let y; + function qux(x) { + if (x === void 0) { y = 42; } + return y; + } + function moo() { + let x; + if (x === void 0) { x = 42; } + return x; + } + let x; + function juu() { + if (x === void 0) { x = 42; } + return x; + } + ` +}; + +describe('Fesm2015ReflectionHost', () => { + + describe('getDecoratorsOfDeclaration()', () => { + it('should find the decorators on a class', () => { + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); + 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 = makeProgram(FOO_FUNCTION_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const functionNode = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', ts.isFunctionDeclaration); + const decorators = host.getDecoratorsOfDeclaration(functionNode); + expect(decorators).toBe(null); + }); + + it('should return null if there are no decorators', () => { + const program = makeProgram(SIMPLE_CLASS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isClassDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode); + expect(decorators).toBe(null); + }); + + it('should ignore `decorators` if it is not an array literal', () => { + const program = makeProgram(INVALID_DECORATORS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NotArrayLiteral', ts.isClassDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode); + expect(decorators).toEqual([]); + }); + + it('should ignore decorator elements that are not object literals', () => { + const program = makeProgram(INVALID_DECORATORS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NotObjectLiteral', ts.isClassDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NotObjectLiteralDecorator'})); + }); + + it('should ignore decorator elements that have no `type` property', () => { + const program = makeProgram(INVALID_DECORATORS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NoTypeProperty', ts.isClassDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NoTypePropertyDecorator2'})); + }); + + it('should ignore decorator elements whose `type` value is not an identifier', () => { + const program = makeProgram(INVALID_DECORATORS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NotIdentifier', ts.isClassDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NotIdentifierDecorator'})); + }); + + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + const mockImportInfo = {} as Import; + const spy = spyOn(Fesm2015ReflectionHost.prototype, 'getImportOfIdentifier') + .and.returnValue(mockImportInfo); + + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toEqual(1); + expect(decorators[0].import).toBe(mockImportInfo); + + const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier; + expect(typeIdentifier.text).toBe('Directive'); + }); + + describe('(returned decorators `args`)', () => { + it('should be an empty array if decorator has no `args` property', () => { + const program = makeProgram(INVALID_DECORATOR_ARGS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', ts.isClassDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NoArgsPropertyDecorator'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if decorator\'s `args` has no property assignment', () => { + const program = makeProgram(INVALID_DECORATOR_ARGS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', + ts.isClassDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NoPropertyAssignmentDecorator'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if `args` property value is not an array literal', () => { + const program = makeProgram(INVALID_DECORATOR_ARGS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', ts.isClassDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NotArrayLiteralDecorator'); + expect(decorators[0].args).toEqual([]); + }); + }); + }); + + describe('getMembersOfClass()', () => { + it('should find decorated properties on a class', () => { + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); + 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 = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); + const members = host.getMembersOfClass(classNode); + + const instanceProperty = members.find(member => member.name === 'instanceProperty') !; + expect(instanceProperty.kind).toEqual(ClassMemberKind.Property); + expect(instanceProperty.isStatic).toEqual(false); + expect(ts.isBinaryExpression(instanceProperty.implementation !)).toEqual(true); + expect(instanceProperty.value !.getText()).toEqual(`'instance'`); + }); + + it('should find static methods on a class', () => { + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); + const members = host.getMembersOfClass(classNode); + + const staticMethod = members.find(member => member.name === 'staticMethod') !; + expect(staticMethod.kind).toEqual(ClassMemberKind.Method); + expect(staticMethod.isStatic).toEqual(true); + expect(ts.isMethodDeclaration(staticMethod.implementation !)).toEqual(true); + }); + + it('should find static properties on a class', () => { + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); + const members = host.getMembersOfClass(classNode); + + const staticProperty = members.find(member => member.name === 'staticProperty') !; + expect(staticProperty.kind).toEqual(ClassMemberKind.Property); + expect(staticProperty.isStatic).toEqual(true); + expect(ts.isPropertyAccessExpression(staticProperty.implementation !)).toEqual(true); + expect(staticProperty.value !.getText()).toEqual(`'static'`); + }); + + it('should throw if the symbol is not a class', () => { + const program = makeProgram(FOO_FUNCTION_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const functionNode = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', ts.isFunctionDeclaration); + expect(() => { + host.getMembersOfClass(functionNode); + }).toThrowError(`Attempted to get members of a non-class: "function foo() {}"`); + }); + + it('should return an empty array if there are no prop decorators', () => { + const program = makeProgram(SIMPLE_CLASS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isClassDeclaration); + const members = host.getMembersOfClass(classNode); + + expect(members).toEqual([]); + }); + + it('should not process decorated properties in `propDecorators` if it is not an object literal', + () => { + const program = makeProgram(INVALID_PROP_DECORATORS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NotObjectLiteral', ts.isClassDeclaration); + const members = host.getMembersOfClass(classNode); + + expect(members.map(member => member.name)).not.toContain('prop'); + }); + + it('should ignore prop decorator elements that are not object literals', () => { + const program = makeProgram(INVALID_PROP_DECORATORS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NotObjectLiteralProp', + ts.isClassDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({ + name: 'NotObjectLiteralPropDecorator' + })); + }); + + it('should ignore prop decorator elements that have no `type` property', () => { + const program = makeProgram(INVALID_PROP_DECORATORS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NoTypeProperty', ts.isClassDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NoTypePropertyDecorator2'})); + }); + + it('should ignore prop decorator elements whose `type` value is not an identifier', () => { + const program = makeProgram(INVALID_PROP_DECORATORS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NotIdentifier', ts.isClassDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NotIdentifierDecorator'})); + }); + + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + let callCount = 0; + const spy = + spyOn(Fesm2015ReflectionHost.prototype, 'getImportOfIdentifier').and.callFake(() => { + callCount++; + return {name: `name${callCount}`, from: `from${callCount}`}; + }); + + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); + const members = host.getMembersOfClass(classNode); + + expect(spy).toHaveBeenCalled(); + expect(spy.calls.allArgs().map(arg => arg[0].getText())).toEqual([ + 'Input', + 'Input', + 'HostBinding', + 'Input', + 'HostListener', + ]); + + const index = members.findIndex(member => member.name === 'input1'); + expect(members[index].decorators !.length).toBe(1); + expect(members[index].decorators ![0].import).toEqual({name: 'name1', from: 'from1'}); + }); + + describe('(returned prop decorators `args`)', () => { + it('should be an empty array if prop decorator has no `args` property', () => { + const program = makeProgram(INVALID_PROP_DECORATOR_ARGS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', + ts.isClassDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NoArgsPropertyDecorator'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if prop decorator\'s `args` has no property assignment', () => { + const program = makeProgram(INVALID_PROP_DECORATOR_ARGS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', + ts.isClassDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NoPropertyAssignmentDecorator'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if `args` property value is not an array literal', () => { + const program = makeProgram(INVALID_PROP_DECORATOR_ARGS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', + ts.isClassDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NotArrayLiteralDecorator'); + expect(decorators[0].args).toEqual([]); + }); + }); + }); + + describe('getConstructorParameters()', () => { + it('should find the decorated constructor parameters', () => { + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters).toBeDefined(); + expect(parameters !.map(parameter => parameter.name)).toEqual([ + '_viewContainer', '_template', 'injected' + ]); + expect(parameters !.map(parameter => parameter.type !.getText())).toEqual([ + 'ViewContainerRef', 'TemplateRef', 'undefined' + ]); + }); + + it('should throw if the symbol is not a class', () => { + const program = makeProgram(FOO_FUNCTION_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const functionNode = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', ts.isFunctionDeclaration); + expect(() => { host.getConstructorParameters(functionNode); }) + .toThrowError( + 'Attempted to get constructor parameters of a non-class: "function foo() {}"'); + }); + + it('should return `null` if there is no constructor', () => { + const program = makeProgram(SIMPLE_CLASS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + expect(parameters).toBe(null); + }); + + it('should return an array even if there are no decorators', () => { + const program = makeProgram(SIMPLE_CLASS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'NoDecoratorConstructorClass', ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters).toEqual(jasmine.any(Array)); + expect(parameters !.length).toEqual(1); + expect(parameters ![0].name).toEqual('foo'); + expect(parameters ![0].decorators).toBe(null); + }); + + it('should return an empty array if there are no constructor parameters', () => { + const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NoParameters', ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters).toEqual([]); + }); + + it('should ignore `ctorParameters` if it is not an arrow function', () => { + const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotArrowFunction', ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters !.length).toBe(1); + expect(parameters ![0]).toEqual(jasmine.objectContaining({ + name: 'arg1', + decorators: null, + })); + }); + + it('should ignore `ctorParameters` if it does not return an array literal', () => { + const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotArrayLiteral', ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters !.length).toBe(1); + expect(parameters ![0]).toEqual(jasmine.objectContaining({ + name: 'arg1', + decorators: null, + })); + }); + + describe('(returned parameters `decorators`)', () => { + it('should ignore param decorator elements that are not object literals', () => { + const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotObjectLiteral', ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters !.length).toBe(2); + expect(parameters ![0]).toEqual(jasmine.objectContaining({ + name: 'arg1', + decorators: null, + })); + expect(parameters ![1]).toEqual(jasmine.objectContaining({ + name: 'arg2', + decorators: jasmine.any(Array) as any + })); + }); + + it('should ignore param decorator elements that have no `type` property', () => { + const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NoTypeProperty', ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NoTypePropertyDecorator2'})); + }); + + it('should ignore param decorator elements whose `type` value is not an identifier', () => { + const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotIdentifier', ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NotIdentifierDecorator'})); + }); + + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + const mockImportInfo = {} as Import; + const spy = spyOn(Fesm2015ReflectionHost.prototype, 'getImportOfIdentifier') + .and.returnValue(mockImportInfo); + + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![2].decorators !; + + expect(decorators.length).toEqual(1); + expect(decorators[0].import).toBe(mockImportInfo); + + const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier; + expect(typeIdentifier.text).toBe('Inject'); + }); + }); + + describe('(returned parameters `decorators.args`)', () => { + it('should be an empty array if param decorator has no `args` property', () => { + const program = makeProgram(INVALID_CTOR_DECORATOR_ARGS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', + ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + expect(parameters !.length).toBe(1); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NoArgsPropertyDecorator'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if param decorator\'s `args` has no property assignment', () => { + const program = makeProgram(INVALID_CTOR_DECORATOR_ARGS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', + ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NoPropertyAssignmentDecorator'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if `args` property value is not an array literal', () => { + const program = makeProgram(INVALID_CTOR_DECORATOR_ARGS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', + ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NotArrayLiteralDecorator'); + expect(decorators[0].args).toEqual([]); + }); + }); + }); + + describe('getDefinitionOfFunction()', () => { + it('should return an object describing the function declaration passed as an argument', () => { + const program = makeProgram(FUNCTION_BODY_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + + const fooNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'foo', ts.isFunctionDeclaration) !; + const fooDef = host.getDefinitionOfFunction(fooNode); + expect(fooDef.node).toBe(fooNode); + expect(fooDef.body !.length).toEqual(1); + expect(fooDef.body ![0].getText()).toEqual(`return x;`); + expect(fooDef.parameters.length).toEqual(1); + expect(fooDef.parameters[0].name).toEqual('x'); + expect(fooDef.parameters[0].initializer).toBe(null); + + const barNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'bar', ts.isFunctionDeclaration) !; + const barDef = host.getDefinitionOfFunction(barNode); + expect(barDef.node).toBe(barNode); + expect(barDef.body !.length).toEqual(1); + expect(ts.isReturnStatement(barDef.body ![0])).toBeTruthy(); + expect(barDef.body ![0].getText()).toEqual(`return x + y;`); + expect(barDef.parameters.length).toEqual(2); + expect(barDef.parameters[0].name).toEqual('x'); + expect(fooDef.parameters[0].initializer).toBe(null); + expect(barDef.parameters[1].name).toEqual('y'); + expect(barDef.parameters[1].initializer !.getText()).toEqual('42'); + + const bazNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'baz', ts.isFunctionDeclaration) !; + const bazDef = host.getDefinitionOfFunction(bazNode); + expect(bazDef.node).toBe(bazNode); + expect(bazDef.body !.length).toEqual(3); + expect(bazDef.parameters.length).toEqual(1); + expect(bazDef.parameters[0].name).toEqual('x'); + expect(bazDef.parameters[0].initializer).toBe(null); + + const quxNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'qux', ts.isFunctionDeclaration) !; + const quxDef = host.getDefinitionOfFunction(quxNode); + expect(quxDef.node).toBe(quxNode); + expect(quxDef.body !.length).toEqual(2); + expect(quxDef.parameters.length).toEqual(1); + expect(quxDef.parameters[0].name).toEqual('x'); + expect(quxDef.parameters[0].initializer).toBe(null); + + const mooNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'moo', ts.isFunctionDeclaration) !; + const mooDef = host.getDefinitionOfFunction(mooNode); + expect(mooDef.node).toBe(mooNode); + expect(mooDef.body !.length).toEqual(3); + expect(mooDef.parameters).toEqual([]); + + const juuNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'juu', ts.isFunctionDeclaration) !; + const juuDef = host.getDefinitionOfFunction(juuNode); + expect(juuDef.node).toBe(juuNode); + expect(juuDef.body !.length).toEqual(2); + expect(juuDef.parameters).toEqual([]); + }); + }); + + describe('getImportOfIdentifier()', () => { + it('should find the import of an identifier', () => { + const program = makeProgram(...IMPORTS_FILES); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const variableNode = + getDeclaration(program, IMPORTS_FILES[1].name, 'b', ts.isVariableDeclaration); + const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); + + expect(importOfIdent).toEqual({name: 'a', from: './a.js'}); + }); + + it('should find the name by which the identifier was exported, not imported', () => { + const program = makeProgram(...IMPORTS_FILES); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const variableNode = + getDeclaration(program, IMPORTS_FILES[1].name, 'c', ts.isVariableDeclaration); + const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); + + expect(importOfIdent).toEqual({name: 'a', from: './a.js'}); + }); + + it('should return null if the identifier was not imported', () => { + const program = makeProgram(...IMPORTS_FILES); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const variableNode = + getDeclaration(program, IMPORTS_FILES[1].name, 'd', ts.isVariableDeclaration); + const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); + + expect(importOfIdent).toBeNull(); + }); + }); + + describe('getDeclarationOfIdentifier()', () => { + it('should return the declaration of a locally defined identifier', () => { + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); + const ctrDecorators = host.getConstructorParameters(classNode) !; + const identifierOfViewContainerRef = ctrDecorators[0].type !as ts.Identifier; + + const expectedDeclarationNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'ViewContainerRef', ts.isVariableDeclaration); + const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfViewContainerRef); + expect(actualDeclaration).not.toBe(null); + expect(actualDeclaration !.node).toBe(expectedDeclarationNode); + expect(actualDeclaration !.viaModule).toBe(null); + }); + + it('should return the declaration of an externally defined identifier', () => { + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); + const classDecorators = host.getDecoratorsOfDeclaration(classNode) !; + const identifierOfDirective = ((classDecorators[0].node as ts.ObjectLiteralExpression) + .properties[0] as ts.PropertyAssignment) + .initializer as ts.Identifier; + + const expectedDeclarationNode = getDeclaration( + program, 'node_modules/@angular/core/index.ts', 'Directive', ts.isVariableDeclaration); + const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective); + expect(actualDeclaration).not.toBe(null); + expect(actualDeclaration !.node).toBe(expectedDeclarationNode); + expect(actualDeclaration !.viaModule).toBe('@angular/core'); + }); + }); + + describe('getExportsOfModule()', () => { + it('should return a map of all the exports from a given module', () => { + const program = makeProgram(...EXPORTS_FILES); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const file = program.getSourceFile(EXPORTS_FILES[1].name) !; + const exportDeclarations = host.getExportsOfModule(file); + expect(exportDeclarations).not.toBe(null); + expect(Array.from(exportDeclarations !.keys())).toEqual([ + 'Directive', + 'a', + 'b', + 'c', + 'd', + 'e', + 'DirectiveX', + 'SomeClass', + ]); + + const values = Array.from(exportDeclarations !.values()) + .map(declaration => [declaration.node.getText(), declaration.viaModule]); + expect(values).toEqual([ + // TODO clarify what is expected here... + // [`Directive = callableClassDecorator()`, '@angular/core'], + [`Directive = callableClassDecorator()`, null], + [`a = 'a'`, null], + [`b = a`, null], + [`c = foo`, null], + [`d = b`, null], + [`e = 'e'`, null], + [`DirectiveX = Directive`, null], + ['export class SomeClass {}', null], + ]); + }); + }); + + describe('isClass()', () => { + it('should return true if a given node is a TS class declaration', () => { + const program = makeProgram(SIMPLE_CLASS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const node = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isClassDeclaration); + expect(host.isClass(node)).toBe(true); + }); + + it('should return false if a given node is a TS function declaration', () => { + const program = makeProgram(FOO_FUNCTION_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const node = getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', ts.isFunctionDeclaration); + expect(host.isClass(node)).toBe(false); + }); + }); + + describe('getGenericArityOfClass()', () => { + it('should return 0 for a basic class', () => { + const program = makeProgram(SIMPLE_CLASS_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const node = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isClassDeclaration); + expect(host.getGenericArityOfClass(node)).toBe(0); + }); + }); +}); diff --git a/packages/compiler-cli/src/ngcc/test/parser/esm2015_parser_spec.ts b/packages/compiler-cli/src/ngcc/test/parsing/esm2015_parser_spec.ts similarity index 91% rename from packages/compiler-cli/src/ngcc/test/parser/esm2015_parser_spec.ts rename to packages/compiler-cli/src/ngcc/test/parsing/esm2015_parser_spec.ts index 4607d4a9cf0f..9c8385f89815 100644 --- a/packages/compiler-cli/src/ngcc/test/parser/esm2015_parser_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/parsing/esm2015_parser_spec.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; -import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; +import {Fesm2015ReflectionHost} from '../../src/host/fesm2015_host'; import {Esm2015FileParser} from '../../src/parsing/esm2015_parser'; import {makeProgram} from '../helpers/utils'; @@ -39,7 +39,7 @@ describe('Esm2015PackageParser', () => { describe('getDecoratedClasses()', () => { it('should return an array of object for each class that is exported and decorated', () => { const program = makeProgram(BASIC_FILE); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); const parser = new Esm2015FileParser(program, host); const parsedFiles = parser.parseFile(program.getSourceFile(BASIC_FILE.name) !); diff --git a/packages/compiler-cli/src/ngcc/test/parser/esm5_parser_spec.ts b/packages/compiler-cli/src/ngcc/test/parsing/esm5_parser_spec.ts similarity index 100% rename from packages/compiler-cli/src/ngcc/test/parser/esm5_parser_spec.ts rename to packages/compiler-cli/src/ngcc/test/parsing/esm5_parser_spec.ts diff --git a/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts b/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts index 9bcd127c7f81..2b540491b2c9 100644 --- a/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts @@ -9,13 +9,13 @@ import * as ts from 'typescript'; import MagicString from 'magic-string'; import {makeProgram} from '../helpers/utils'; import {Analyzer} from '../../src/analyzer'; -import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; +import {Fesm2015ReflectionHost} from '../../src/host/fesm2015_host'; import {Esm2015FileParser} from '../../src/parsing/esm2015_parser'; import {Esm2015Renderer} from '../../src/rendering/esm2015_renderer'; function setup(file: {name: string, contents: string}) { const program = makeProgram(file); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); const parser = new Esm2015FileParser(program, host); const analyzer = new Analyzer(program.getTypeChecker(), host); const renderer = new Esm2015Renderer(host); diff --git a/packages/compiler-cli/src/ngcc/test/rendering/renderer_spec.ts b/packages/compiler-cli/src/ngcc/test/rendering/renderer_spec.ts index 8b907f9278cc..a298e7a25cc7 100644 --- a/packages/compiler-cli/src/ngcc/test/rendering/renderer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/rendering/renderer_spec.ts @@ -12,7 +12,7 @@ import MagicString from 'magic-string'; import {fromObject, generateMapFileComment} from 'convert-source-map'; import {makeProgram} from '../helpers/utils'; import {AnalyzedClass, Analyzer} from '../../src/analyzer'; -import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; +import {Fesm2015ReflectionHost} from '../../src/host/fesm2015_host'; import {Esm2015FileParser} from '../../src/parsing/esm2015_parser'; import {Renderer} from '../../src/rendering/renderer'; @@ -32,7 +32,7 @@ class TestRenderer extends Renderer { } function createTestRenderer() { - const renderer = new TestRenderer(); + const renderer = new TestRenderer({} as Fesm2015ReflectionHost); spyOn(renderer, 'addImports').and.callThrough(); spyOn(renderer, 'addDefinitions').and.callThrough(); spyOn(renderer, 'removeDecorators').and.callThrough(); @@ -41,7 +41,7 @@ function createTestRenderer() { function analyze(file: {name: string, contents: string}) { const program = makeProgram(file); - const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); const parser = new Esm2015FileParser(program, host); const analyzer = new Analyzer(program.getTypeChecker(), host); @@ -183,4 +183,4 @@ describe('Renderer', () => { expect(result.map !.contents).toEqual(MERGED_OUTPUT_PROGRAM_MAP.toJSON()); }); }); -}); \ No newline at end of file +}); diff --git a/packages/compiler-cli/src/ngcc/test/parser/parser_spec.ts b/packages/compiler-cli/src/ngcc/test/transform/utils_spec.ts similarity index 55% rename from packages/compiler-cli/src/ngcc/test/parser/parser_spec.ts rename to packages/compiler-cli/src/ngcc/test/transform/utils_spec.ts index 559fcea3f594..5b14e4d39f2a 100644 --- a/packages/compiler-cli/src/ngcc/test/parser/parser_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/transform/utils_spec.ts @@ -7,12 +7,16 @@ */ import * as mockFs from 'mock-fs'; -import {findAllPackageJsonFiles, getEntryPoints} from '../../src/parsing/utils'; +import {EntryPoint, findAllPackageJsonFiles, getEntryPoints} from '../../src/transform/utils'; function createMockFileSystem() { mockFs({ '/node_modules/@angular/common': { - 'package.json': '{ "fesm2015": "./fesm2015/common.js", "fesm5": "./fesm5/common.js" }', + 'package.json': `{ + "fesm2015": "./fesm2015/common.js", + "fesm5": "./fesm5/common.js", + "typings": "./common.d.ts" + }`, 'fesm2015': { 'common.js': 'DUMMY CONTENT', 'http.js': 'DUMMY CONTENT', @@ -20,14 +24,28 @@ function createMockFileSystem() { 'testing.js': 'DUMMY CONTENT', }, 'http': { - 'package.json': '{ "fesm2015": "../fesm2015/http.js", "fesm5": "../fesm5/http.js" }', + 'package.json': `{ + "fesm2015": "../fesm2015/http.js", + "fesm5": "../fesm5/http.js", + "typings": "./http.d.ts" + }`, 'testing': { - 'package.json': - '{ "fesm2015": "../../fesm2015/http/testing.js", "fesm5": "../../fesm5/http/testing.js" }', + 'package.json': `{ + "fesm2015": "../../fesm2015/http/testing.js", + "fesm5": "../../fesm5/http/testing.js", + "typings": "../http/testing.d.ts" + }`, }, }, + 'other': { + 'package.json': '{ }', + }, 'testing': { - 'package.json': '{ "fesm2015": "../fesm2015/testing.js", "fesm5": "../fesm5/testing.js" }', + 'package.json': `{ + "fesm2015": "../fesm2015/testing.js", + "fesm5": "../fesm5/testing.js", + "typings": "../testing.d.ts" + }`, }, 'node_modules': { 'tslib': { @@ -40,6 +58,15 @@ function createMockFileSystem() { }, }, }, + '/node_modules/@angular/no-typings': { + 'package.json': `{ + "fesm2015": "./fesm2015/index.js" + }`, + 'fesm2015': { + 'index.js': 'DUMMY CONTENT', + 'index.d.ts': 'DUMMY CONTENT', + }, + }, '/node_modules/@angular/other': { 'not-package.json': '{ "fesm2015": "./fesm2015/other.js" }', 'package.jsonot': '{ "fesm5": "./fesm5/other.js" }', @@ -63,6 +90,13 @@ function restoreRealFileSystem() { mockFs.restore(); } +describe('EntryPoint', () => { + it('should expose the absolute path to the entry point file', () => { + const entryPoint = new EntryPoint('/foo/bar', '../baz/qux/../quux.js', '/typings/foo/bar.d.ts'); + expect(entryPoint.entryFileName).toBe('/foo/baz/quux.js'); + }); +}); + describe('findAllPackageJsonFiles()', () => { beforeEach(createMockFileSystem); afterEach(restoreRealFileSystem); @@ -72,6 +106,7 @@ describe('findAllPackageJsonFiles()', () => { expect(paths.sort()).toEqual([ '/node_modules/@angular/common/http/package.json', '/node_modules/@angular/common/http/testing/package.json', + '/node_modules/@angular/common/other/package.json', '/node_modules/@angular/common/package.json', '/node_modules/@angular/common/testing/package.json', ]); @@ -102,9 +137,12 @@ describe('getEntryPoints()', () => { beforeEach(createMockFileSystem); afterEach(restoreRealFileSystem); - it('should return the paths for the specified format from each package.json', () => { - const paths = getEntryPoints('/node_modules/@angular/common', 'fesm2015'); - expect(paths.sort()).toEqual([ + it('should return the entry points for the specified format from each `package.json`', () => { + const entryPoints = getEntryPoints('/node_modules/@angular/common', 'fesm2015'); + entryPoints.forEach(ep => expect(ep).toEqual(jasmine.any(EntryPoint))); + + const sortedPaths = entryPoints.map(x => x.entryFileName).sort(); + expect(sortedPaths).toEqual([ '/node_modules/@angular/common/fesm2015/common.js', '/node_modules/@angular/common/fesm2015/http.js', '/node_modules/@angular/common/fesm2015/http/testing.js', @@ -112,13 +150,22 @@ describe('getEntryPoints()', () => { ]); }); - it('should return an empty array if there are no matching package.json files', () => { - const paths = getEntryPoints('/node_modules/@angular/other', 'fesm2015'); - expect(paths).toEqual([]); + it('should return an empty array if there are no matching `package.json` files', () => { + const entryPoints = getEntryPoints('/node_modules/@angular/other', 'fesm2015'); + expect(entryPoints).toEqual([]); }); it('should return an empty array if there are no matching formats', () => { - const paths = getEntryPoints('/node_modules/@angular/other', 'main'); - expect(paths).toEqual([]); + const entryPoints = getEntryPoints('/node_modules/@angular/common', 'fesm3000'); + expect(entryPoints).toEqual([]); + }); + + it('should return an entry point even if the typings are not specified', () => { + const entryPoints = getEntryPoints('/node_modules/@angular/no-typings', 'fesm2015'); + expect(entryPoints.length).toEqual(1); + expect(entryPoints[0].entryFileName) + .toEqual('/node_modules/@angular/no-typings/fesm2015/index.js'); + expect(entryPoints[0].entryRoot).toEqual('/node_modules/@angular/no-typings/fesm2015'); + expect(entryPoints[0].dtsEntryRoot).toEqual(entryPoints[0].entryRoot); }); }); diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index ce32ab2feb30..e67d5bce2b16 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -156,7 +156,7 @@ export function extractDirectiveMetadata( inputs: {...inputsFromMeta, ...inputsFromFields}, outputs: {...outputsFromMeta, ...outputsFromFields}, queries, selector, type: new WrappedNodeExpr(clazz.name !), - typeArgumentCount: (clazz.typeParameters || []).length, + typeArgumentCount: reflector.getGenericArityOfClass(clazz) || 0, typeSourceSpan: null !, usesInheritance, exportAs, }; return {decoratedElements, decorator: directive, metadata}; diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts index de80ac54b42d..4d3a0e92e2bc 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts @@ -64,21 +64,21 @@ export class NgModuleDecoratorHandler implements DecoratorHandler this._extractModuleFromModuleWithProvidersFn(ref.node)); - imports = resolveTypeList(importsMeta, 'imports'); + imports = this.resolveTypeList(importsMeta, 'imports'); } let exports: Reference[] = []; if (ngModule.has('exports')) { const exportsMeta = staticallyResolve( ngModule.get('exports') !, this.reflector, this.checker, ref => this._extractModuleFromModuleWithProvidersFn(ref.node)); - exports = resolveTypeList(exportsMeta, 'exports'); + exports = this.resolveTypeList(exportsMeta, 'exports'); } // Register this module's information with the SelectorScopeRegistry. This ensures that during @@ -181,39 +181,39 @@ export class NgModuleDecoratorHandler implements DecoratorHandler { - // Unwrap ModuleWithProviders for modules that are locally declared (and thus static resolution - // was able to descend into the function and return an object literal, a Map). - if (entry instanceof Map && entry.has('ngModule')) { - entry = entry.get('ngModule') !; + /** + * Compute a list of `Reference`s from a resolved metadata value. + */ + private resolveTypeList(resolvedList: ResolvedValue, name: string): Reference[] { + const refList: Reference[] = []; + if (!Array.isArray(resolvedList)) { + throw new Error(`Expected array when reading property ${name}`); } - if (Array.isArray(entry)) { - // Recurse into nested arrays. - refList.push(...resolveTypeList(entry, name)); - } else if (entry instanceof Reference) { - if (!entry.expressable) { - throw new Error(`Value at position ${idx} in ${name} array is not expressable`); - } else if (!ts.isClassDeclaration(entry.node)) { - throw new Error(`Value at position ${idx} in ${name} array is not a class declaration`); + resolvedList.forEach((entry, idx) => { + // Unwrap ModuleWithProviders for modules that are locally declared (and thus static + // resolution was able to descend into the function and return an object literal, a Map). + if (entry instanceof Map && entry.has('ngModule')) { + entry = entry.get('ngModule') !; } - refList.push(entry); - } else { - // TODO(alxhub): expand ModuleWithProviders. - throw new Error(`Value at position ${idx} in ${name} array is not a reference: ${entry}`); - } - }); - return refList; + if (Array.isArray(entry)) { + // Recurse into nested arrays. + refList.push(...this.resolveTypeList(entry, name)); + } else if (entry instanceof Reference) { + if (!entry.expressable) { + throw new Error(`Value at position ${idx} in ${name} array is not expressable`); + } else if (!this.reflector.isClass(entry.node)) { + throw new Error(`Value at position ${idx} in ${name} array is not a class declaration`); + } + refList.push(entry); + } else { + // TODO(alxhub): expand ModuleWithProviders. + throw new Error(`Value at position ${idx} in ${name} array is not a reference: ${entry}`); + } + }); + + return refList; + } } diff --git a/packages/compiler-cli/src/ngtsc/host/src/reflection.ts b/packages/compiler-cli/src/ngtsc/host/src/reflection.ts index 16b3867fb00c..bb205a125d5c 100644 --- a/packages/compiler-cli/src/ngtsc/host/src/reflection.ts +++ b/packages/compiler-cli/src/ngtsc/host/src/reflection.ts @@ -147,9 +147,9 @@ export interface ClassMember { } /** - * A parameter to a function or constructor. + * A parameter to a constructor. */ -export interface Parameter { +export interface CtorParameter { /** * Name of the parameter, if available. * @@ -180,6 +180,54 @@ export interface Parameter { decorators: Decorator[]|null; } +/** + * Definition of a function or method, including its body if present and any parameters. + * + * In TypeScript code this metadata will be a simple reflection of the declarations in the node + * itself. In ES5 code this can be more complicated, as the default values for parameters may + * be extracted from certain body statements. + */ +export interface FunctionDefinition { + /** + * A reference to the node which declares the function. + */ + node: T; + + /** + * Statements of the function body, if a body is present, or null if no body is present. + * + * This list may have been filtered to exclude statements which perform parameter default value + * initialization. + */ + body: ts.Statement[]|null; + + /** + * Metadata regarding the function's parameters, including possible default value expressions. + */ + parameters: Parameter[]; +} + +/** + * A parameter to a function or method. + */ +export interface Parameter { + /** + * Name of the parameter, if available. + */ + name: string|null; + + /** + * Declaration which created this parameter. + */ + node: ts.ParameterDeclaration; + + /** + * Expression which represents the default value of the parameter, if any. + */ + initializer: ts.Expression|null; +} + /** * The source of an imported symbol, including the original symbol name and the module from which it * was imported. @@ -273,7 +321,30 @@ export interface ReflectionHost { * a constructor exists. If the constructor exists and has 0 parameters, this array will be empty. * If the class has no constructor, this method returns `null`. */ - getConstructorParameters(declaration: ts.Declaration): Parameter[]|null; + getConstructorParameters(declaration: ts.Declaration): CtorParameter[]|null; + + /** + * Reflect over a function and return metadata about its parameters and body. + * + * Functions in TypeScript and ES5 code have different AST representations, in particular around + * default values for parameters. A TypeScript function has its default value as the initializer + * on the parameter declaration, whereas an ES5 function has its default value set in a statement + * of the form: + * + * if (param === void 0) { param = 3; } + * + * This method abstracts over these details, and interprets the function declaration and body to + * extract parameter default values and the "real" body. + * + * A current limitation is that this metadata has no representation for shorthand assignment of + * parameter objects in the function signature. + * + * @param fn a TypeScript `ts.Declaration` node representing the function over which to reflect. + * + * @returns a `FunctionDefinition` giving metadata about the function definition. + */ + getDefinitionOfFunction(fn: T): FunctionDefinition; /** * Determine if an identifier was imported from another module and return `Import` metadata @@ -335,9 +406,17 @@ export interface ReflectionHost { getExportsOfModule(module: ts.Node): Map|null; /** - * Check whether the given declaration node actually represents a class. + * Check whether the given node actually represents a class. */ - isClass(node: ts.Declaration): boolean; + isClass(node: ts.Node): boolean; hasBaseClass(node: ts.Declaration): boolean; + + /** + * Get the number of generic type parameters of a given class. + * + * @returns the number of type parameters of the class, if known, or `null` if the declaration + * is not a class or has an unknown number of type parameters. + */ + getGenericArityOfClass(clazz: ts.Declaration): number|null; } diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/reflector.ts b/packages/compiler-cli/src/ngtsc/metadata/src/reflector.ts index 2332fe94c1e7..d03bdbd26edd 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/reflector.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/reflector.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; -import {ClassMember, ClassMemberKind, Declaration, Decorator, Import, Parameter, ReflectionHost} from '../../host'; +import {ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, FunctionDefinition, Import, ReflectionHost} from '../../host'; /** * reflector.ts implements static reflection of declarations using the TypeScript `ts.TypeChecker`. @@ -31,7 +31,7 @@ export class TypeScriptReflectionHost implements ReflectionHost { .filter((member): member is ClassMember => member !== null); } - getConstructorParameters(declaration: ts.Declaration): Parameter[]|null { + getConstructorParameters(declaration: ts.Declaration): CtorParameter[]|null { const clazz = castDeclarationToClassOrDie(declaration); // First, find the constructor. @@ -127,7 +127,7 @@ export class TypeScriptReflectionHost implements ReflectionHost { return map; } - isClass(node: ts.Declaration): boolean { + isClass(node: ts.Node): boolean { // In TypeScript code, classes are ts.ClassDeclarations. return ts.isClassDeclaration(node); } @@ -146,6 +146,26 @@ export class TypeScriptReflectionHost implements ReflectionHost { return this.getDeclarationOfSymbol(symbol); } + getDefinitionOfFunction(node: T): FunctionDefinition { + return { + node, + body: node.body !== undefined ? Array.from(node.body.statements) : null, + parameters: node.parameters.map(param => { + const name = parameterName(param.name); + const initializer = param.initializer || null; + return {name, node: param, initializer}; + }), + }; + } + + getGenericArityOfClass(clazz: ts.Declaration): number|null { + if (!ts.isClassDeclaration(clazz)) { + return null; + } + return clazz.typeParameters !== undefined ? clazz.typeParameters.length : 0; + } + /** * Resolve a `ts.Symbol` to its declaration, keeping track of the `viaModule` along the way. * diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/resolver.ts b/packages/compiler-cli/src/ngtsc/metadata/src/resolver.ts index acc8ff3f17ff..a0988a5e3708 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/resolver.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/resolver.ts @@ -17,7 +17,7 @@ import * as ts from 'typescript'; import {ClassMemberKind, ReflectionHost} from '../../host'; -const TS_DTS_EXTENSION = /(\.d)?\.ts$/; +const TS_DTS_JS_EXTENSION = /(\.d)?\.ts|\.js$/; /** * Represents a value which cannot be determined statically. @@ -147,7 +147,7 @@ export class ResolvedReference extends Reference // TODO(alxhub): investigate the need to map such paths via the Host for proper g3 support. let relative = path.posix.relative(path.dirname(context.fileName), this.node.getSourceFile().fileName) - .replace(TS_DTS_EXTENSION, ''); + .replace(TS_DTS_JS_EXTENSION, ''); // path.relative() does not include the leading './'. if (!relative.startsWith('.')) { @@ -202,7 +202,7 @@ function pickIdentifier( context: ts.SourceFile, primary: ts.Identifier, secondaries: ts.Identifier[], mode: ImportMode): ts.Identifier|null { context = ts.getOriginalNode(context) as ts.SourceFile; - let localIdentifier: ts.Identifier|null = null; + if (ts.getOriginalNode(primary).getSourceFile() === context) { return primary; } else if (mode === ImportMode.UseExistingImport) { @@ -547,11 +547,11 @@ class StaticInterpreter { `calling something that is not a function declaration? ${ts.SyntaxKind[lhs.node.kind]} (${node.getText()})`); } - const fn = lhs.node; + const fn = this.host.getDefinitionOfFunction(lhs.node); // If the function is foreign (declared through a d.ts file), attempt to resolve it with the // foreignFunctionResolver, if one is specified. - if (fn.body === undefined) { + if (fn.body === null) { let expr: ts.Expression|null = null; if (context.foreignFunctionResolver) { expr = context.foreignFunctionResolver(lhs, node.arguments); @@ -572,10 +572,10 @@ class StaticInterpreter { } const body = fn.body; - if (body.statements.length !== 1 || !ts.isReturnStatement(body.statements[0])) { + if (body.length !== 1 || !ts.isReturnStatement(body[0])) { throw new Error('Function body must have a single return statement only.'); } - const ret = body.statements[0] as ts.ReturnStatement; + const ret = body[0] as ts.ReturnStatement; const newScope: Scope = new Map(); fn.parameters.forEach((param, index) => { @@ -584,10 +584,10 @@ class StaticInterpreter { const arg = node.arguments[index]; value = this.visitExpression(arg, context); } - if (value === undefined && param.initializer !== undefined) { + if (value === undefined && param.initializer !== null) { value = this.visitExpression(param.initializer, context); } - newScope.set(param, value); + newScope.set(param.node, value); }); return ret.expression !== undefined ? @@ -671,7 +671,8 @@ class StaticInterpreter { function isFunctionOrMethodReference(ref: Reference): ref is Reference { - return ts.isFunctionDeclaration(ref.node) || ts.isMethodDeclaration(ref.node); + return ts.isFunctionDeclaration(ref.node) || ts.isMethodDeclaration(ref.node) || + ts.isFunctionExpression(ref.node); } function literal(value: ResolvedValue): any { diff --git a/packages/compiler-cli/src/ngtsc/metadata/test/reflector_spec.ts b/packages/compiler-cli/src/ngtsc/metadata/test/reflector_spec.ts index 47e05f0642e9..71ff87090a58 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/test/reflector_spec.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/test/reflector_spec.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; -import {Parameter} from '../../host'; +import {CtorParameter} from '../../host'; import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; import {TypeScriptReflectionHost} from '../src/reflector'; @@ -165,7 +165,7 @@ describe('reflector', () => { }); function expectParameter( - param: Parameter, name: string, type?: string, decorator?: string, + param: CtorParameter, name: string, type?: string, decorator?: string, decoratorFrom?: string): void { expect(param.name !).toEqual(name); if (type === undefined) { diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 79901cf2be30..84d6919c4ad2 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -140,8 +140,7 @@ export class NgtscProgram implements api.Program { sourceFiles: ReadonlyArray) => { if (fileName.endsWith('.d.ts')) { data = sourceFiles.reduce( - (data, sf) => this.compilation !.transformedDtsFor(sf.fileName, data, fileName), - data); + (data, sf) => this.compilation !.transformedDtsFor(sf.fileName, data), data); } this.host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles); }; diff --git a/packages/compiler-cli/src/ngtsc/transform/index.ts b/packages/compiler-cli/src/ngtsc/transform/index.ts index 00eea48b1350..30cde5930b22 100644 --- a/packages/compiler-cli/src/ngtsc/transform/index.ts +++ b/packages/compiler-cli/src/ngtsc/transform/index.ts @@ -8,5 +8,6 @@ export * from './src/api'; export {IvyCompilation} from './src/compilation'; +export {DtsFileTransformer} from './src/declaration'; export {ivyTransformFactory} from './src/transform'; -export {ImportManager, translateStatement} from './src/translator'; \ No newline at end of file +export {ImportManager, translateStatement} from './src/translator'; diff --git a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts index 6789d0e807a9..318c2822a4e4 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts @@ -192,7 +192,7 @@ export class IvyCompilation { * Process a .d.ts source string and return a transformed version that incorporates the changes * made to the source file. */ - transformedDtsFor(tsFileName: string, dtsOriginalSource: string, dtsPath: string): string { + transformedDtsFor(tsFileName: string, dtsOriginalSource: string): string { // No need to transform if no changes have been requested to the input file. if (!this.dtsMap.has(tsFileName)) { return dtsOriginalSource; diff --git a/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts b/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts index fbb4cee703fc..dec8eb96f25e 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts @@ -8,8 +8,6 @@ import * as ts from 'typescript'; -import {relativePathBetween} from '../../util/src/path'; - import {CompileResult} from './api'; import {ImportManager, translateType} from './translator'; @@ -22,8 +20,8 @@ export class DtsFileTransformer { private ivyFields = new Map(); private imports: ImportManager; - constructor(private coreImportsFrom: ts.SourceFile|null) { - this.imports = new ImportManager(coreImportsFrom !== null); + constructor(private coreImportsFrom: ts.SourceFile|null, importPrefix?: string) { + this.imports = new ImportManager(coreImportsFrom !== null, importPrefix); } /** @@ -64,4 +62,4 @@ export class DtsFileTransformer { return dts; } -} \ No newline at end of file +} diff --git a/packages/compiler-cli/src/ngtsc/transform/src/translator.ts b/packages/compiler-cli/src/ngtsc/transform/src/translator.ts index dc4b69c1f268..c8067167ad50 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/translator.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/translator.ts @@ -310,7 +310,7 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor { const exprStr = type.value.visitExpression(this, context); if (type.typeParams !== null) { const typeSegments = type.typeParams.map(param => param.visitType(this, context)); - return `${exprStr}<${typeSegments.join(',')}>`; + return `${exprStr}<${typeSegments.join(', ')}>`; } else { return exprStr; } @@ -412,7 +412,7 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor { visitLiteralArrayExpr(ast: LiteralArrayExpr, context: Context): string { const values = ast.entries.map(expr => expr.visitExpression(this, context)); - return `[${values.join(',')}]`; + return `[${values.join(', ')}]`; } visitLiteralMapExpr(ast: LiteralMapExpr, context: Context) { @@ -434,4 +434,4 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor { visitTypeofExpr(ast: TypeofExpr, context: Context): string { return `typeof ${ast.expr.visitExpression(this, context)}`; } -} \ No newline at end of file +} diff --git a/packages/compiler-cli/test/ngcc/ngcc_spec.ts b/packages/compiler-cli/test/ngcc/ngcc_spec.ts index aab460be005c..1247930b872c 100644 --- a/packages/compiler-cli/test/ngcc/ngcc_spec.ts +++ b/packages/compiler-cli/test/ngcc/ngcc_spec.ts @@ -6,25 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import * as fs from 'fs'; -import * as path from 'path'; -import {cat, find} from 'shelljs'; +import {join} from 'path'; import {mainNgcc} from '../../src/ngcc/src/main'; import {TestSupport, isInBazel, setup} from '../test_support'; -function setupNodeModules(support: TestSupport): void { - const corePath = path.join(process.env.TEST_SRCDIR, 'angular/packages/core/npm_package'); - const commonPath = path.join(process.env.TEST_SRCDIR, 'angular/packages/common/npm_package'); - - const nodeModulesPath = path.join(support.basePath, 'node_modules'); - const angularCoreDirectory = path.join(nodeModulesPath, '@angular/core'); - const angularCommonDirectory = path.join(nodeModulesPath, '@angular/common'); - - // fs.symlinkSync(corePath, angularCoreDirectory); - // fs.symlinkSync(commonPath, angularCommonDirectory); -} +const OUTPUT_PATH = 'node_modules_ngtsc'; describe('ngcc behavioral tests', () => { if (!isInBazel()) { @@ -32,57 +20,61 @@ describe('ngcc behavioral tests', () => { return; } - let basePath: string; - let outDir: string; - let write: (fileName: string, content: string) => void; - let errorSpy: jasmine.Spy&((s: string) => void); + // Temporary local debugging aid. Set to `true` to turn on. + const preserveOutput = false; + const onSpecCompleted = (format: string) => { + if (preserveOutput) { + const {tmpdir} = require('os'); + const {cp, mkdir, rm, set} = require('shelljs'); - function shouldExist(fileName: string) { - if (!fs.existsSync(path.resolve(outDir, fileName))) { - throw new Error(`Expected ${fileName} to be emitted (outDir: ${outDir})`); - } - } + const tempRootDir = join(tmpdir(), 'ngcc-spec', format); + const outputDir = OUTPUT_PATH; - function shouldNotExist(fileName: string) { - if (fs.existsSync(path.resolve(outDir, fileName))) { - throw new Error(`Did not expect ${fileName} to be emitted (outDir: ${outDir})`); + set('-e'); + rm('-rf', tempRootDir); + mkdir('-p', tempRootDir); + cp('-R', join(support.basePath, outputDir), tempRootDir); + + global.console.log(`Copied '${outputDir}' to '${tempRootDir}'.`); } - } + }; - function getContents(fileName: string): string { - shouldExist(fileName); - const modulePath = path.resolve(outDir, fileName); - return fs.readFileSync(modulePath, 'utf8'); - } + let support: TestSupport; + beforeEach(() => support = setup()); - function writeConfig( - tsconfig: string = - '{"extends": "./tsconfig-base.json", "angularCompilerOptions": {"enableIvy": "ngtsc"}}') { - write('tsconfig.json', tsconfig); - } + it('should run ngcc without errors for fesm2015', () => { + const commonPath = join(support.basePath, 'node_modules/@angular/common'); + const format = 'fesm2015'; - beforeEach(() => { - errorSpy = jasmine.createSpy('consoleError').and.callFake(console.error); - const support = setup(); - basePath = support.basePath; - outDir = path.join(basePath, 'built'); - process.chdir(basePath); - write = (fileName: string, content: string) => { support.write(fileName, content); }; + expect(mainNgcc([format, commonPath, OUTPUT_PATH])).toBe(0); - setupNodeModules(support); + onSpecCompleted(format); }); - it('should run ngcc without errors', () => { - const nodeModulesPath = path.join(basePath, 'node_modules'); - console.error(nodeModulesPath); - const commonPath = path.join(nodeModulesPath, '@angular/common'); - const exitCode = mainNgcc([commonPath]); + it('should run ngcc without errors for fesm5', () => { + const commonPath = join(support.basePath, 'node_modules/@angular/common'); + const format = 'fesm5'; + + expect(mainNgcc([format, commonPath, OUTPUT_PATH])).toBe(0); + + onSpecCompleted(format); + }); + + it('should run ngcc without errors for esm2015', () => { + const commonPath = join(support.basePath, 'node_modules/@angular/common'); + const format = 'esm2015'; + + expect(mainNgcc([format, commonPath, OUTPUT_PATH])).toBe(0); + + onSpecCompleted(format); + }); - console.warn(find('node_modules_ngtsc').filter(p => p.endsWith('.js') || p.endsWith('map'))); + it('should run ngcc without errors for esm5', () => { + const commonPath = join(support.basePath, 'node_modules/@angular/common'); + const format = 'esm5'; - console.warn(cat('node_modules_ngtsc/@angular/common/fesm2015/common.js').stdout); - console.warn(cat('node_modules_ngtsc/@angular/common/fesm2015/common.js.map').stdout); + expect(mainNgcc([format, commonPath, OUTPUT_PATH])).toBe(0); - expect(exitCode).toBe(0); + onSpecCompleted(format); }); }); diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index c27411238bba..9bb8ef85e36c 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -405,7 +405,7 @@ describe('ngtsc behavioral tests', () => { const dtsContents = getContents('test.d.ts'); expect(dtsContents) - .toContain('i0.ɵNgModuleDef'); + .toContain('i0.ɵNgModuleDef'); }); it('should unwrap a ModuleWithProviders function if a generic type is provided for it', () => { diff --git a/packages/compiler/src/render3/r3_factory.ts b/packages/compiler/src/render3/r3_factory.ts index b07f91a9257f..00e91bba2a78 100644 --- a/packages/compiler/src/render3/r3_factory.ts +++ b/packages/compiler/src/render3/r3_factory.ts @@ -181,8 +181,10 @@ export function compileFactoryFunction(meta: R3FactoryMetadata): } else { const baseFactory = o.variable(`ɵ${meta.name}_BaseFactory`); const getInheritedFactory = o.importExpr(R3.getInheritedFactory); - const baseFactoryStmt = baseFactory.set(getInheritedFactory.callFn([meta.type])) - .toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]); + const baseFactoryStmt = + baseFactory.set(getInheritedFactory.callFn([meta.type])).toDeclStmt(o.INFERRED_TYPE, [ + o.StmtModifier.Exported, o.StmtModifier.Final + ]); statements.push(baseFactoryStmt); // There is no constructor, use the base class' factory to construct typeForCtor. @@ -206,8 +208,10 @@ export function compileFactoryFunction(meta: R3FactoryMetadata): if (meta.delegate.isEquivalent(meta.type)) { throw new Error(`Illegal state: compiling factory that delegates to itself`); } - const delegateFactoryStmt = delegateFactory.set(getFactoryOf.callFn([meta.delegate])) - .toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]); + const delegateFactoryStmt = + delegateFactory.set(getFactoryOf.callFn([meta.delegate])).toDeclStmt(o.INFERRED_TYPE, [ + o.StmtModifier.Exported, o.StmtModifier.Final + ]); statements.push(delegateFactoryStmt); const r = makeConditionalFactory(delegateFactory.callFn([]));