diff --git a/packages/ngtools/webpack/src/angular_compiler_plugin.ts b/packages/ngtools/webpack/src/angular_compiler_plugin.ts index 826189b2373b..dd6ac7971ad2 100644 --- a/packages/ngtools/webpack/src/angular_compiler_plugin.ts +++ b/packages/ngtools/webpack/src/angular_compiler_plugin.ts @@ -29,6 +29,7 @@ import { formatDiagnostics, readConfiguration, } from '@angular/compiler-cli'; +import { constructorParametersDownlevelTransform } from '@angular/compiler-cli/src/tooling'; import { ChildProcess, ForkOptions, fork } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; @@ -59,7 +60,6 @@ import { replaceServerBootstrap, } from './transformers'; import { collectDeepNodes } from './transformers/ast_helpers'; -import { downlevelConstructorParameters } from './transformers/ctor-parameters'; import { removeIvyJitSupportCalls } from './transformers/remove-ivy-jit-support-calls'; import { AUTO_START_ARG, @@ -245,6 +245,11 @@ export class AngularCompilerPlugin { options.missingTranslation as 'error' | 'warning' | 'ignore'; } + // For performance, disable AOT decorator downleveling transformer for applications in the CLI. + // The transformer is not needed for VE or Ivy in this plugin since Angular decorators are removed. + // While the transformer would make no changes, it would still need to walk each source file AST. + this._compilerOptions.annotationsAs = 'decorators' as 'decorators'; + // Process forked type checker options. if (options.forkTypeChecker !== undefined) { this._forkTypeChecker = options.forkTypeChecker; @@ -1021,7 +1026,13 @@ export class AngularCompilerPlugin { replaceResources(isAppPath, getTypeChecker, this._options.directTemplateLoading)); // Downlevel constructor parameters for DI support // This is required to support forwardRef in ES2015 due to TDZ issues - this._transformers.push(downlevelConstructorParameters(getTypeChecker)); + // This wrapper is needed here due to the program not being available until after the transformers are created. + const downlevelFactory: ts.TransformerFactory = (context) => { + const factory = constructorParametersDownlevelTransform(this._getTsProgram() as ts.Program); + + return factory(context); + }; + this._transformers.push(downlevelFactory); } else { if (!this._compilerOptions.enableIvy) { // Remove unneeded angular decorators in VE. diff --git a/packages/ngtools/webpack/src/transformers/ctor-parameters.ts b/packages/ngtools/webpack/src/transformers/ctor-parameters.ts deleted file mode 100644 index e0df7501f51f..000000000000 --- a/packages/ngtools/webpack/src/transformers/ctor-parameters.ts +++ /dev/null @@ -1,335 +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 * as ts from 'typescript'; - -export function downlevelConstructorParameters( - getTypeChecker: () => ts.TypeChecker, -): ts.TransformerFactory { - return (context: ts.TransformationContext) => { - const transformer = decoratorDownlevelTransformer(getTypeChecker(), []); - - return transformer(context); - }; -} - -// The following is sourced from tsickle with local modifications -// Only the creation of `ctorParameters` is retained -// https://github.com/angular/tsickle/blob/0ceb7d6bc47f6945a6c4c09689f1388eb48f5c07/src/decorator_downlevel_transformer.ts -// - -/** - * Extracts the type of the decorator (the function or expression invoked), as well as all the - * arguments passed to the decorator. Returns an AST with the form: - * - * // For @decorator(arg1, arg2) - * { type: decorator, args: [arg1, arg2] } - */ -function extractMetadataFromSingleDecorator( - decorator: ts.Decorator, - diagnostics: ts.Diagnostic[], -): ts.ObjectLiteralExpression { - const metadataProperties: ts.ObjectLiteralElementLike[] = []; - const expr = decorator.expression; - switch (expr.kind) { - case ts.SyntaxKind.Identifier: - // The decorator was a plain @Foo. - metadataProperties.push(ts.createPropertyAssignment('type', expr)); - break; - case ts.SyntaxKind.CallExpression: - // The decorator was a call, like @Foo(bar). - const call = expr as ts.CallExpression; - metadataProperties.push(ts.createPropertyAssignment('type', call.expression)); - if (call.arguments.length) { - const args: ts.Expression[] = []; - for (const arg of call.arguments) { - args.push(arg); - } - const argsArrayLiteral = ts.createArrayLiteral(args); - argsArrayLiteral.elements.hasTrailingComma = true; - metadataProperties.push(ts.createPropertyAssignment('args', argsArrayLiteral)); - } - break; - default: - diagnostics.push({ - file: decorator.getSourceFile(), - start: decorator.getStart(), - length: decorator.getEnd() - decorator.getStart(), - messageText: `${ - ts.SyntaxKind[decorator.kind] - } not implemented in gathering decorator metadata`, - category: ts.DiagnosticCategory.Error, - code: 0, - }); - break; - } - - return ts.createObjectLiteral(metadataProperties); -} - -/** - * createCtorParametersClassProperty creates a static 'ctorParameters' property containing - * downleveled decorator information. - * - * The property contains an arrow function that returns an array of object literals of the shape: - * static ctorParameters = () => [{ - * type: SomeClass|undefined, // the type of the param that's decorated, if it's a value. - * decorators: [{ - * type: DecoratorFn, // the type of the decorator that's invoked. - * args: [ARGS], // the arguments passed to the decorator. - * }] - * }]; - */ -function createCtorParametersClassProperty( - diagnostics: ts.Diagnostic[], - entityNameToExpression: (n: ts.EntityName) => ts.Expression | undefined, - ctorParameters: ParameterDecorationInfo[], - typeChecker: ts.TypeChecker, -): ts.PropertyDeclaration { - const params: ts.Expression[] = []; - - for (const ctorParam of ctorParameters) { - if (!ctorParam.type && ctorParam.decorators.length === 0) { - params.push(ts.createNull()); - continue; - } - - const paramType = ctorParam.type - ? typeReferenceToExpression(entityNameToExpression, ctorParam.type, typeChecker) - : undefined; - const members = [ - ts.createPropertyAssignment('type', paramType || ts.createIdentifier('undefined')), - ]; - - const decorators: ts.ObjectLiteralExpression[] = []; - for (const deco of ctorParam.decorators) { - decorators.push(extractMetadataFromSingleDecorator(deco, diagnostics)); - } - if (decorators.length) { - members.push(ts.createPropertyAssignment('decorators', ts.createArrayLiteral(decorators))); - } - params.push(ts.createObjectLiteral(members)); - } - - const initializer = ts.createArrowFunction( - undefined, - undefined, - [], - undefined, - ts.createToken(ts.SyntaxKind.EqualsGreaterThanToken), - ts.createArrayLiteral(params, true), - ); - - const ctorProp = ts.createProperty( - undefined, - [ts.createToken(ts.SyntaxKind.StaticKeyword)], - 'ctorParameters', - undefined, - undefined, - initializer, - ); - - return ctorProp; -} - -/** - * Returns an expression representing the (potentially) value part for the given node. - * - * This is a partial re-implementation of TypeScript's serializeTypeReferenceNode. This is a - * workaround for https://github.com/Microsoft/TypeScript/issues/17516 (serializeTypeReferenceNode - * not being exposed). In practice this implementation is sufficient for Angular's use of type - * metadata. - */ -function typeReferenceToExpression( - entityNameToExpression: (n: ts.EntityName) => ts.Expression | undefined, - node: ts.TypeNode, - typeChecker: ts.TypeChecker, -): ts.Expression | undefined { - let kind = node.kind; - if (ts.isLiteralTypeNode(node)) { - // Treat literal types like their base type (boolean, string, number). - kind = node.literal.kind; - } - switch (kind) { - case ts.SyntaxKind.FunctionType: - case ts.SyntaxKind.ConstructorType: - return ts.createIdentifier('Function'); - case ts.SyntaxKind.ArrayType: - case ts.SyntaxKind.TupleType: - return ts.createIdentifier('Array'); - case ts.SyntaxKind.TypePredicate: - case ts.SyntaxKind.TrueKeyword: - case ts.SyntaxKind.FalseKeyword: - case ts.SyntaxKind.BooleanKeyword: - return ts.createIdentifier('Boolean'); - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.StringKeyword: - return ts.createIdentifier('String'); - case ts.SyntaxKind.ObjectKeyword: - return ts.createIdentifier('Object'); - case ts.SyntaxKind.NumberKeyword: - case ts.SyntaxKind.NumericLiteral: - return ts.createIdentifier('Number'); - case ts.SyntaxKind.UnionType: - const childTypeNodes = (node as ts.UnionTypeNode).types.filter(t => t.kind !== ts.SyntaxKind.NullKeyword); - - return childTypeNodes.length === 1 - ? typeReferenceToExpression(entityNameToExpression, childTypeNodes[0], typeChecker) - : undefined; - - case ts.SyntaxKind.TypeReference: - const typeRef = node as ts.TypeReferenceNode; - let typeSymbol = typeChecker.getSymbolAtLocation(typeRef.typeName); - if (typeSymbol && typeSymbol.flags & ts.SymbolFlags.Alias) { - typeSymbol = typeChecker.getAliasedSymbol(typeSymbol); - } - - if (!typeSymbol || !(typeSymbol.flags & ts.SymbolFlags.Value)) { - return undefined; - } - - const type = typeChecker.getTypeOfSymbolAtLocation(typeSymbol, typeRef); - if (!type || typeChecker.getSignaturesOfType(type, ts.SignatureKind.Construct).length === 0) { - return undefined; - } - - // Ignore any generic types, just return the base type. - return entityNameToExpression(typeRef.typeName); - default: - return undefined; - } -} - -/** ParameterDecorationInfo describes the information for a single constructor parameter. */ -interface ParameterDecorationInfo { - /** - * The type declaration for the parameter. Only set if the type is a value (e.g. a class, not an - * interface). - */ - type: ts.TypeNode | null; - /** The list of decorators found on the parameter, null if none. */ - decorators: ts.Decorator[]; -} - -/** - * Transformer factory for the decorator downlevel transformer. See fileoverview for details. - */ -export function decoratorDownlevelTransformer( - typeChecker: ts.TypeChecker, - diagnostics: ts.Diagnostic[], -): (context: ts.TransformationContext) => ts.Transformer { - return (context: ts.TransformationContext) => { - const parameterTypeSymbols = new Set(); - - /** - * Converts an EntityName (from a type annotation) to an expression (accessing a value). - * - * For a given ts.EntityName, this walks depth first to find the leftmost ts.Identifier, then - * converts the path into property accesses. - * - */ - function entityNameToExpression(name: ts.EntityName): ts.Expression | undefined { - if (ts.isIdentifier(name)) { - const typeSymbol = typeChecker.getSymbolAtLocation(name); - if (typeSymbol) { - parameterTypeSymbols.add(typeSymbol); - } - - // Based on TS's strategy to allow the checker to reach this identifier - // tslint:disable-next-line:max-line-length - // https://github.com/microsoft/TypeScript/blob/7f47a08a5e9874f0f97a667bd81eebddec61247c/src/compiler/transformers/ts.ts#L2093 - const exp = ts.getMutableClone(name); - exp.flags &= ~ts.NodeFlags.Synthesized; - ((exp as unknown) as { original: undefined }).original = undefined; - exp.parent = ts.getParseTreeNode(name.getSourceFile()); - - return exp; - } - const ref = entityNameToExpression(name.left); - if (!ref) { - return undefined; - } - - return ts.createPropertyAccess(ref, name.right); - } - - function classMemberVisitor(node: ts.Node): ts.VisitResult { - if (!ts.isConstructorDeclaration(node) || !node.body) { - return visitor(node); - } - - const parametersInfo: ParameterDecorationInfo[] = []; - for (const param of node.parameters) { - const paramInfo: ParameterDecorationInfo = { decorators: [], type: null }; - - for (const decorator of param.decorators || []) { - paramInfo.decorators.push(decorator); - } - if (param.type) { - // param has a type provided, e.g. "foo: Bar". - // The type will be emitted as a value expression in entityNameToExpression, which takes - // care not to emit anything for types that cannot be expressed as a value (e.g. - // interfaces). - paramInfo.type = param.type; - } - parametersInfo.push(paramInfo); - } - - if (parametersInfo.length > 0) { - const ctorProperty = createCtorParametersClassProperty( - diagnostics, - entityNameToExpression, - parametersInfo, - typeChecker, - ); - - return [node, ctorProperty]; - } else { - return node; - } - } - - function visitor(node: T): ts.Node { - if (ts.isClassDeclaration(node) && node.decorators && node.decorators.length > 0) { - return ts.updateClassDeclaration( - node, - node.decorators, - node.modifiers, - node.name, - node.typeParameters, - node.heritageClauses, - ts.visitNodes(node.members, classMemberVisitor), - ); - } else { - return ts.visitEachChild(node, visitor, context); - } - } - - return (sf: ts.SourceFile) => { - parameterTypeSymbols.clear(); - - return ts.visitEachChild( - visitor(sf) as ts.SourceFile, - function visitImports(node: ts.Node): ts.Node { - if ( - (ts.isImportSpecifier(node) || ts.isNamespaceImport(node) || ts.isImportClause(node)) && - node.name - ) { - const importSymbol = typeChecker.getSymbolAtLocation(node.name); - if (importSymbol && parameterTypeSymbols.has(importSymbol)) { - // Using a clone prevents TS from removing the import specifier - return ts.getMutableClone(node); - } - } - - return ts.visitEachChild(node, visitImports, context); - }, - context, - ); - }; - }; -} diff --git a/packages/ngtools/webpack/src/transformers/ctor-parameters_spec.ts b/packages/ngtools/webpack/src/transformers/ctor-parameters_spec.ts deleted file mode 100644 index bf1b1a879ddb..000000000000 --- a/packages/ngtools/webpack/src/transformers/ctor-parameters_spec.ts +++ /dev/null @@ -1,251 +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 { tags } from '@angular-devkit/core'; // tslint:disable-line:no-implicit-dependencies -import { downlevelConstructorParameters } from './ctor-parameters'; -import { createTypescriptContext, transformTypescript } from './spec_helpers'; - -function transform(input: string, additionalFiles?: Record) { - const { program, compilerHost } = createTypescriptContext(input, additionalFiles); - const transformer = downlevelConstructorParameters(() => program.getTypeChecker()); - const result = transformTypescript(undefined, [transformer], program, compilerHost); - - return result; -} - -// tslint:disable-next-line: no-big-function -describe('Constructor Parameter Transformer', () => { - it('records class name in same module', () => { - const input = ` - export class ClassInject {}; - - @Injectable() - export class MyService { - constructor(v: ClassInject) {} - } - `; - - const output = ` - import { __decorate } from "tslib"; - export class ClassInject { } ; - let MyService = class MyService { constructor(v) { } }; - MyService.ctorParameters = () => [ { type: ClassInject } ]; - MyService = __decorate([ Injectable() ], MyService); - export { MyService }; - `; - - const result = transform(input); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - - it('does not record when class does not have a decorator', () => { - const input = ` - export class ClassInject {}; - - export class MyService { - constructor(v: ClassInject) {} - } - `; - - const output = ` - export class ClassInject { } ; - export class MyService { constructor(v) { } } - `; - - const result = transform(input); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - - it('records class name of root-provided injectable in same module', () => { - const input = ` - @Injectable({ - providedIn: 'root' - }) - export class RootProvidedService { - - constructor() { } - } - - @Injectable() - export class MyService { - constructor(v: RootProvidedService) {} - } - `; - - const output = ` - import { __decorate } from "tslib"; - let RootProvidedService = class RootProvidedService { constructor() { } }; - RootProvidedService = __decorate([ Injectable({ providedIn: 'root' }) ], RootProvidedService); - export { RootProvidedService }; - let MyService = class MyService { constructor(v) { } }; - MyService.ctorParameters = () => [ { type: RootProvidedService } ]; - MyService = __decorate([ Injectable() ], MyService); - export { MyService }; - `; - - const result = transform(input); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - - it('records class name of root-provided injectable in imported module', () => { - const rootProvided = { - 'root-provided-service.ts': ` - @Injectable({ - providedIn: 'root' - }) - export class RootProvidedService { - constructor() { } - } - `, - }; - - const input = ` - import { RootProvidedService } from './root-provided-service'; - - @Injectable() - export class MyService { - constructor(v: RootProvidedService) {} - } - `; - - const output = ` - import { __decorate } from "tslib"; - import { RootProvidedService } from './root-provided-service'; - - let MyService = class MyService { - constructor(v) { } - }; - MyService.ctorParameters = () => [ { type: RootProvidedService } ]; - MyService = __decorate([ Injectable() ], MyService); - export { MyService }; - `; - - const result = transform(input, rootProvided); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - - it('does not record exported interface name in same module with Inject decorators', () => { - const input = ` - export interface InterInject {} - export const INTERFACE_INJECT = new InjectionToken('interface-inject'); - - @Injectable() - export class MyService { - constructor(@Inject(INTERFACE_INJECT) v: InterInject) {} - } - `; - - const output = ` - import { __decorate, __param } from "tslib"; - export const INTERFACE_INJECT = new InjectionToken('interface-inject'); - let MyService = class MyService { constructor(v) { } }; - MyService.ctorParameters = () => [ { type: undefined, decorators: [{ type: Inject, args: [INTERFACE_INJECT,] }] } ]; - MyService = __decorate([ Injectable(), __param(0, Inject(INTERFACE_INJECT)) ], MyService); - export { MyService }; - `; - - const result = transform(input); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - - it('does not record interface name in same module with Inject decorators', () => { - const input = ` - interface InterInject {} - export const INTERFACE_INJECT = new InjectionToken('interface-inject'); - - @Injectable() - export class MyService { - constructor(@Inject(INTERFACE_INJECT) v: InterInject) {} - } - `; - - const output = ` - import { __decorate, __param } from "tslib"; - export const INTERFACE_INJECT = new InjectionToken('interface-inject'); - let MyService = class MyService { constructor(v) { } }; - MyService.ctorParameters = () => [ { type: undefined, decorators: [{ type: Inject, args: [INTERFACE_INJECT,] }] } ]; - MyService = __decorate([ Injectable(), __param(0, Inject(INTERFACE_INJECT)) ], MyService); - export { MyService }; - `; - - const result = transform(input); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - - it('does not record interface name in imported module with Inject decorators', () => { - const injectedModule = { - 'module-inject': ` - export interface InterInject {}; - export const INTERFACE_INJECT = new InjectionToken('interface-inject'); - `, - }; - - const input = ` - import { INTERFACE_INJECT, InterInject } from './module-inject'; - - @Injectable() - export class MyService { - constructor(@Inject(INTERFACE_INJECT) v: InterInject) {} - } - `; - - const output = ` - import { __decorate, __param } from "tslib"; - import { INTERFACE_INJECT } from './module-inject'; - let MyService = class MyService { constructor(v) { } }; - MyService.ctorParameters = () => [ { type: undefined, decorators: [{ type: Inject, args: [INTERFACE_INJECT,] }] } ]; - MyService = __decorate([ Injectable(), __param(0, Inject(INTERFACE_INJECT)) ], MyService); - export { MyService }; - `; - - const result = transform(input, injectedModule); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - - it('should work with union type and nullable argument', () => { - const input = ` - @Injectable() - export class ProvidedService { - constructor() { } - } - - @Injectable() - export class LibService { - constructor( - @Optional() private service: ProvidedService | null, - ) { - } - } - `; - - const output = ` - import { __decorate, __param } from "tslib"; - - let ProvidedService = class ProvidedService { constructor() { } }; - ProvidedService = __decorate([ Injectable() ], ProvidedService); - export { ProvidedService }; - - let LibService = class LibService { - constructor(service) { this.service = service; } - }; - LibService.ctorParameters = () => [ { type: ProvidedService, decorators: [{ type: Optional }] } ]; - LibService = __decorate([ Injectable(), __param(0, Optional()) ], LibService); - export { LibService }; - `; - - const result = transform(input); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); -});