Skip to content

feat(@ngtools/webpack): add lightweight decorator remover #8456

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/@ngtools/webpack/src/angular_compiler_plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import {
} from './virtual_file_system_decorator';
import { resolveEntryModuleFromMain } from './entry_resolver';
import {
createTransformerFactory,
replaceBootstrap,
exportNgFactory,
exportLazyModuleMap,
AngularDecoratorRemover,
registerLocaleData,
findResources,
replaceResources,
Expand Down Expand Up @@ -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));
Expand Down
44 changes: 44 additions & 0 deletions packages/@ngtools/webpack/src/transformers/decorator_remover.ts
Original file line number Diff line number Diff line change
@@ -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));
}

}
2 changes: 2 additions & 0 deletions packages/@ngtools/webpack/src/transformers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { createTransformerFactory } from './transformer';
export * from './interfaces';
export * from './ast_helpers';
export * from './make_transform';
Expand All @@ -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';
150 changes: 150 additions & 0 deletions packages/@ngtools/webpack/src/transformers/transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import * as ts from 'typescript';
import { satisfies } from 'semver';

const needsNodeSymbolFix = satisfies(ts.version, '< 2.5.2');

export interface Transformer<T extends ts.Node = ts.SourceFile> {
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<T extends ts.Node>(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<TNode> {
getTypeChecker?: () => ts.TypeChecker;
exclude?: (node: TNode) => boolean;
}

export function createTransformerFactory<TNode extends ts.Node>(
transformer: Transformer<TNode>,
options: TransformerFactoryOptions<TNode> = {},
): ts.TransformerFactory<TNode> {
const programContext: ProgramContext = {
getTypeChecker: () => {
if (!options.getTypeChecker) {
throw new Error('type checker is not available');
}
return options.getTypeChecker();
},
};

const factory: ts.TransformerFactory<TNode> = 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;
}