diff --git a/packages/@ngtools/webpack/src/angular_compiler_plugin.ts b/packages/@ngtools/webpack/src/angular_compiler_plugin.ts index 78fde902c826..e01638d1fd59 100644 --- a/packages/@ngtools/webpack/src/angular_compiler_plugin.ts +++ b/packages/@ngtools/webpack/src/angular_compiler_plugin.ts @@ -18,9 +18,11 @@ import { } from './virtual_file_system_decorator'; import { resolveEntryModuleFromMain } from './entry_resolver'; import { + createTransformerFactory, replaceBootstrap, exportNgFactory, exportLazyModuleMap, + AngularDecoratorRemover, registerLocaleData, findResources, replaceResources, @@ -655,6 +657,15 @@ export class AngularCompilerPlugin implements Tapable { if (!this._JitMode) { // Replace bootstrap in browser AOT. this._transformers.push(replaceBootstrap(isAppPath, getEntryModule)); + + // Remove unneeded angular decorators + this._transformers.push(createTransformerFactory( + new AngularDecoratorRemover(), + { + getTypeChecker: () => this._getTsProgram().getTypeChecker(), + exclude: node => !isAppPath(node.fileName), + }, + )); } } else if (this._platform === PLATFORM.Server) { this._transformers.push(exportLazyModuleMap(isMainPath, getLazyRoutes)); diff --git a/packages/@ngtools/webpack/src/transformers/decorator_remover.ts b/packages/@ngtools/webpack/src/transformers/decorator_remover.ts new file mode 100644 index 000000000000..cc2923dac414 --- /dev/null +++ b/packages/@ngtools/webpack/src/transformers/decorator_remover.ts @@ -0,0 +1,44 @@ +import * as ts from 'typescript'; +import { AbstractTransformer } from './transformer'; + + +export class AngularDecoratorRemover extends AbstractTransformer { + + transform(sourceFile: ts.SourceFile): ts.SourceFile { + let decoratorsRemoved = false; + + const visitor: ts.Visitor = node => { + if (ts.isDecorator(node) && this.shouldRemove(node)) { + decoratorsRemoved = true; + return undefined; + } + + return this.visitEachChild(node, visitor); + }; + + const result = this.visitEachChild(sourceFile, visitor); + + // cleanup decorators if modifications were made + if (decoratorsRemoved) { + this.cleanupDecorators(result); + } + + return result; + } + + private shouldRemove(decorator: ts.Decorator): boolean { + const origin = this.getDecoratorOrigin(decorator); + + return origin && origin.module === '@angular/core'; + } + + // Workaround TS bug with empty decorator arrays + private cleanupDecorators(node: ts.Node) { + if (node.decorators && node.decorators.length == 0) { + node.decorators = undefined; + } + + ts.forEachChild(node, node => this.cleanupDecorators(node)); + } + +} diff --git a/packages/@ngtools/webpack/src/transformers/index.ts b/packages/@ngtools/webpack/src/transformers/index.ts index 029a645c539b..30a458c0ba6c 100644 --- a/packages/@ngtools/webpack/src/transformers/index.ts +++ b/packages/@ngtools/webpack/src/transformers/index.ts @@ -1,3 +1,4 @@ +export { createTransformerFactory } from './transformer'; export * from './interfaces'; export * from './ast_helpers'; export * from './make_transform'; @@ -8,3 +9,4 @@ export * from './export_ngfactory'; export * from './export_lazy_module_map'; export * from './register_locale_data'; export * from './replace_resources'; +export * from './decorator_remover'; diff --git a/packages/@ngtools/webpack/src/transformers/transformer.ts b/packages/@ngtools/webpack/src/transformers/transformer.ts new file mode 100644 index 000000000000..8df9453f1c51 --- /dev/null +++ b/packages/@ngtools/webpack/src/transformers/transformer.ts @@ -0,0 +1,150 @@ +import * as ts from 'typescript'; +import { satisfies } from 'semver'; + +const needsNodeSymbolFix = satisfies(ts.version, '< 2.5.2'); + +export interface Transformer { + initialize( + transformationContext: ts.TransformationContext, + programContext: ProgramContext, + ): void; + reset?(): void; + transform?(node: T): T; +} + +export abstract class AbstractTransformer implements Transformer { + private _transformationContext: ts.TransformationContext | null = null; + private _programContext: ProgramContext | null = null; + + protected get transformationContext(): ts.TransformationContext { + if (!this._transformationContext) { + throw new Error('transformer is not initialized'); + } + return this._transformationContext; + } + + protected get programContext(): ProgramContext { + if (!this._programContext) { + throw new Error('transformer is not initialized'); + } + return this._programContext; + } + + initialize(transformationContext: ts.TransformationContext, programContext: ProgramContext) { + this._transformationContext = transformationContext; + this._programContext = programContext; + } + + protected visitEachChild(node: T, visitor: ts.Visitor): T { + return ts.visitEachChild(node, visitor, this.transformationContext); + } + + protected getDecoratorOrigin(decorator: ts.Decorator): DecoratorOrigin { + return getDecoratorOrigin(decorator, this.programContext.getTypeChecker()); + } +} + +export interface ProgramContext { + getTypeChecker(): ts.TypeChecker; +} + +export interface TransformerFactoryOptions { + getTypeChecker?: () => ts.TypeChecker; + exclude?: (node: TNode) => boolean; +} + +export function createTransformerFactory( + transformer: Transformer, + options: TransformerFactoryOptions = {}, +): ts.TransformerFactory { + const programContext: ProgramContext = { + getTypeChecker: () => { + if (!options.getTypeChecker) { + throw new Error('type checker is not available'); + } + return options.getTypeChecker(); + }, + }; + + const factory: ts.TransformerFactory = transformationContext => { + transformer.initialize(transformationContext, programContext); + + if (transformer.reset) { + transformer.reset(); + } + + if (!transformer.transform) { + return node => node; + } + + return node => { + if (options.exclude && options.exclude(node)) { + return node; + } + + const result = transformer.transform(node); + + if (transformer.reset) { + transformer.reset(); + } + + if (result && needsNodeSymbolFix) { + const original = ts.getParseTreeNode(result); + // tslint:disable-next-line:no-any - 'symbol' is internal + (result as any).symbol = (result as any).symbol || (original as any).symbol; + } + + return result; + }; + }; + + return factory; +} + +export interface DecoratorOrigin { + name: string; + module: string; +} + +function getDecoratorOrigin( + decorator: ts.Decorator, + typeChecker: ts.TypeChecker +): DecoratorOrigin | null { + if (!ts.isCallExpression(decorator.expression)) { + return null; + } + + let identifier: ts.Node; + let name: string; + if (ts.isPropertyAccessExpression(decorator.expression.expression)) { + identifier = decorator.expression.expression.expression; + name = decorator.expression.expression.name.text; + } else if (ts.isIdentifier(decorator.expression.expression)) { + identifier = decorator.expression.expression; + } else { + return null; + } + + // NOTE: resolver.getReferencedImportDeclaration would work as well but is internal + const symbol = typeChecker.getSymbolAtLocation(identifier); + if (symbol && symbol.declarations && symbol.declarations.length > 0) { + const declaration = symbol.declarations[0]; + let module: string; + if (ts.isImportSpecifier(declaration)) { + name = (declaration.propertyName || declaration.name).text; + module = (declaration.parent.parent.parent.moduleSpecifier as ts.StringLiteral).text; + } else if (ts.isNamespaceImport(declaration)) { + // Use the name from the decorator namespace property access + module = (declaration.parent.parent.moduleSpecifier as ts.StringLiteral).text; + } else if (ts.isImportClause(declaration)) { + name = declaration.name.text; + module = (declaration.parent.moduleSpecifier as ts.StringLiteral).text; + } else { + return null; + } + + return { name, module }; + } + + return null; +}