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([]));