diff --git a/packages/angular_devkit/build_optimizer/README.md b/packages/angular_devkit/build_optimizer/README.md index b9d1354f8f..b316fc7034 100644 --- a/packages/angular_devkit/build_optimizer/README.md +++ b/packages/angular_devkit/build_optimizer/README.md @@ -36,18 +36,37 @@ Angular decorators, property decorators and constructor parameters are removed, ```typescript // input -import { Injectable, Input } from '@angular/core'; -import { NotInjectable } from 'another-lib'; +import { Injectable, Input, Component } from '@angular/core'; +import { NotInjectable, NotComponent } from 'another-lib'; var Clazz = (function () { function Clazz() { } return Clazz; }()); Clazz.decorators = [{ type: Injectable }, { type: NotInjectable }]; Clazz.propDecorators = { 'ngIf': [{ type: Input }] }; Clazz.ctorParameters = function () { return [{type: Injector}]; }; +var ComponentClazz = (function () { + function ComponentClazz() { } + ComponentClazz = __decorate([ + NotComponent(), + Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] + }) + ], ComponentClazz); + return ComponentClazz; +}()); // output -import { Injectable, Input } from '@angular/core'; -import { NotInjectable } from 'another-lib'; +import { Injectable, Input, Component } from '@angular/core'; +import { NotInjectable, NotComponent } from 'another-lib'; var Clazz = (function () { function Clazz() { } return Clazz; }()); Clazz.decorators = [{ type: NotInjectable }]; +var ComponentClazz = (function () { + function ComponentClazz() { } + ComponentClazz = __decorate([ + NotComponent() + ], ComponentClazz); + return ComponentClazz; +}()); ``` diff --git a/packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer_spec.ts b/packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer_spec.ts index 43626bbfd3..b1d0f9fffb 100644 --- a/packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer_spec.ts +++ b/packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer_spec.ts @@ -11,7 +11,7 @@ import { buildOptimizer } from './build-optimizer'; describe('build-optimizer', () => { - const imports = 'import { Injectable, Input } from \'@angular/core\';'; + const imports = 'import { Injectable, Input, Component } from \'@angular/core\';'; const clazz = 'var Clazz = (function () { function Clazz() { } return Clazz; }());'; const staticProperty = 'Clazz.prop = 1;'; const decorators = 'Clazz.decorators = [ { type: Injectable } ];'; @@ -35,6 +35,17 @@ describe('build-optimizer', () => { ${decorators} Clazz.propDecorators = { 'ngIf': [{ type: Input }] }; Clazz.ctorParameters = function () { return [{type: Injector}]; }; + var ComponentClazz = (function () { + function ComponentClazz() { } + ComponentClazz = __decorate([ + Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] + }) + ], ComponentClazz); + return ComponentClazz; + }()); `; // tslint:disable:max-line-length const output = tags.oneLine` @@ -48,6 +59,10 @@ describe('build-optimizer', () => { return ChangeDetectionStrategy; })(); var Clazz = /*@__PURE__*/ (function () { function Clazz() { } ${staticProperty} return Clazz; }()); + var ComponentClazz = /*@__PURE__*/ (function () { + function ComponentClazz() { } + return ComponentClazz; + }()); `; // Check Angular 4/5 and unix/windows paths. diff --git a/packages/angular_devkit/build_optimizer/src/transforms/scrub-file.ts b/packages/angular_devkit/build_optimizer/src/transforms/scrub-file.ts index 96f91b9c05..6972efa9e6 100644 --- a/packages/angular_devkit/build_optimizer/src/transforms/scrub-file.ts +++ b/packages/angular_devkit/build_optimizer/src/transforms/scrub-file.ts @@ -12,6 +12,7 @@ import { collectDeepNodes } from '../helpers/ast-utils'; export function testScrubFile(content: string) { const markers = [ 'decorators', + '__decorate', 'propDecorators', 'ctorParameters', ]; @@ -68,6 +69,9 @@ export function getScrubFileTransformer(program: ts.Program): ts.TransformerFact if (isDecoratorAssignmentExpression(exprStmt)) { nodes.push(...pickDecorationNodesToRemove(exprStmt, ngMetadata, checker)); } + if (isDecorateAssignmentExpression(exprStmt)) { + nodes.push(...pickDecorateNodesToRemove(exprStmt, ngMetadata, checker)); + } if (isPropDecoratorAssignmentExpression(exprStmt)) { nodes.push(...pickPropDecorationNodesToRemove(exprStmt, ngMetadata, checker)); } @@ -99,7 +103,7 @@ export function getScrubFileTransformer(program: ts.Program): ts.TransformerFact export function expect(node: ts.Node, kind: ts.SyntaxKind): T { if (node.kind !== kind) { - throw new Error('Invalid!'); + throw new Error('Invalid node type.'); } return node as T; @@ -158,6 +162,7 @@ function isAngularCoreSpecifier(node: ts.ImportSpecifier): boolean { return angularSpecifiers.indexOf(nameOfSpecifier(node)) !== -1; } +// Check if assignment is `Clazz.decorators = [...];`. function isDecoratorAssignmentExpression(exprStmt: ts.ExpressionStatement): boolean { if (exprStmt.expression.kind !== ts.SyntaxKind.BinaryExpression) { return false; @@ -183,6 +188,46 @@ function isDecoratorAssignmentExpression(exprStmt: ts.ExpressionStatement): bool return true; } +// Check if assignment is `Clazz = __decorate([...], Clazz)`. +function isDecorateAssignmentExpression(exprStmt: ts.ExpressionStatement): boolean { + if (exprStmt.expression.kind !== ts.SyntaxKind.BinaryExpression) { + return false; + } + const expr = exprStmt.expression as ts.BinaryExpression; + if (expr.left.kind !== ts.SyntaxKind.Identifier) { + return false; + } + if (expr.right.kind !== ts.SyntaxKind.CallExpression) { + return false; + } + const classIdent = expr.left as ts.Identifier; + const callExpr = expr.right as ts.CallExpression; + if (callExpr.expression.kind !== ts.SyntaxKind.Identifier) { + return false; + } + const callExprIdent = callExpr.expression as ts.Identifier; + // node.text on a name that starts with two underscores will return three instead. + if (callExprIdent.text !== '___decorate') { + return false; + } + if (callExpr.arguments.length !== 2) { + return false; + } + if (callExpr.arguments[1].kind !== ts.SyntaxKind.Identifier) { + return false; + } + const classArg = callExpr.arguments[1] as ts.Identifier; + if (classIdent.text !== classArg.text) { + return false; + } + if (callExpr.arguments[0].kind !== ts.SyntaxKind.ArrayLiteralExpression) { + return false; + } + + return true; +} + +// Check if assignment is `Clazz.propDecorators = [...];`. function isPropDecoratorAssignmentExpression(exprStmt: ts.ExpressionStatement): boolean { if (exprStmt.expression.kind !== ts.SyntaxKind.BinaryExpression) { return false; @@ -208,6 +253,7 @@ function isPropDecoratorAssignmentExpression(exprStmt: ts.ExpressionStatement): return true; } +// Check if assignment is `Clazz.ctorParameters = [...];`. function isCtorParamsAssignmentExpression(exprStmt: ts.ExpressionStatement): boolean { if (exprStmt.expression.kind !== ts.SyntaxKind.BinaryExpression) { return false; @@ -243,6 +289,8 @@ function isCtorParamsWhitelistedService(exprStmt: ts.ExpressionStatement): boole return platformWhitelist.indexOf(serviceId.text) !== -1; } +// Remove Angular decorators from`Clazz.decorators = [...];`, or expression itself if all are +// removed. function pickDecorationNodesToRemove( exprStmt: ts.ExpressionStatement, ngMetadata: ts.Node[], @@ -261,6 +309,38 @@ function pickDecorationNodesToRemove( return (elements.length > ngDecorators.length) ? ngDecorators : [exprStmt]; } +// Remove Angular decorators from `Clazz = __decorate([...], Clazz)`, or expression itself if all +// are removed. +function pickDecorateNodesToRemove( + exprStmt: ts.ExpressionStatement, + ngMetadata: ts.Node[], + checker: ts.TypeChecker, +): ts.Node[] { + + const expr = expect(exprStmt.expression, ts.SyntaxKind.BinaryExpression); + const callExpr = expect(expr.right, ts.SyntaxKind.CallExpression); + const arrLiteral = expect(callExpr.arguments[0], + ts.SyntaxKind.ArrayLiteralExpression); + if (!arrLiteral.elements.every((elem) => elem.kind === ts.SyntaxKind.CallExpression)) { + return []; + } + const elements = arrLiteral.elements as ts.NodeArray; + const ngDecoratorCalls = elements.filter((el) => { + if (el.expression.kind !== ts.SyntaxKind.Identifier) { + return false; + } + const id = el.expression as ts.Identifier; + + return identifierIsMetadata(id, ngMetadata, checker); + }); + + // If all decorators are metadata decorators then return the whole `Class = __decorate([...])'` + // statement so that it is removed in entirety + return (elements.length === ngDecoratorCalls.length) ? [exprStmt] : ngDecoratorCalls; +} + +// Remove Angular decorators from`Clazz.propDecorators = [...];`, or expression itself if all +// are removed. function pickPropDecorationNodesToRemove( exprStmt: ts.ExpressionStatement, ngMetadata: ts.Node[], diff --git a/packages/angular_devkit/build_optimizer/src/transforms/scrub-file_spec.ts b/packages/angular_devkit/build_optimizer/src/transforms/scrub-file_spec.ts index 41a2c1009b..6ea973460e 100644 --- a/packages/angular_devkit/build_optimizer/src/transforms/scrub-file_spec.ts +++ b/packages/angular_devkit/build_optimizer/src/transforms/scrub-file_spec.ts @@ -79,6 +79,69 @@ describe('scrub-file', () => { }); }); + describe('__decorate', () => { + it('removes Angular decorators calls in __decorate', () => { + const output = tags.stripIndent` + import { Component, Injectable } from '@angular/core'; + var Clazz = (function () { + function Clazz() { } + return Clazz; + }()); + `; + const input = tags.stripIndent` + import { Component, Injectable } from '@angular/core'; + var Clazz = (function () { + function Clazz() { } + Clazz = __decorate([ + Injectable(), + Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] + }) + ], Clazz); + return Clazz; + }()); + `; + + expect(testScrubFile(input)).toBeTruthy(); + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); + }); + + it('removes only Angular decorators calls in __decorate', () => { + const output = tags.stripIndent` + import { Component } from '@angular/core'; + import { NotComponent } from 'another-lib'; + var Clazz = (function () { + function Clazz() { } + Clazz = __decorate([ + NotComponent() + ], Clazz); + return Clazz; + }()); + `; + const input = tags.stripIndent` + import { Component } from '@angular/core'; + import { NotComponent } from 'another-lib'; + var Clazz = (function () { + function Clazz() { } + Clazz = __decorate([ + NotComponent(), + Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] + }) + ], Clazz); + return Clazz; + }()); + `; + + expect(testScrubFile(input)).toBeTruthy(); + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); + }); + }); + describe('propDecorators', () => { it('removes top-level Angular propDecorators', () => { const output = tags.stripIndent`