Skip to content
This repository was archived by the owner on Apr 4, 2025. It is now read-only.

feat(@angular-devkit/build-optimizer): strip Angular decorators in __… #210

Merged
merged 1 commit into from
Oct 17, 2017
Merged
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
27 changes: 23 additions & 4 deletions packages/angular_devkit/build_optimizer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}());
```


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 } ];';
Expand All @@ -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`
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { collectDeepNodes } from '../helpers/ast-utils';
export function testScrubFile(content: string) {
const markers = [
'decorators',
'__decorate',
'propDecorators',
'ctorParameters',
];
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -99,7 +103,7 @@ export function getScrubFileTransformer(program: ts.Program): ts.TransformerFact

export function expect<T extends ts.Node>(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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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[],
Expand All @@ -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<ts.BinaryExpression>(exprStmt.expression, ts.SyntaxKind.BinaryExpression);
const callExpr = expect<ts.CallExpression>(expr.right, ts.SyntaxKind.CallExpression);
const arrLiteral = expect<ts.ArrayLiteralExpression>(callExpr.arguments[0],
ts.SyntaxKind.ArrayLiteralExpression);
if (!arrLiteral.elements.every((elem) => elem.kind === ts.SyntaxKind.CallExpression)) {
return [];
}
const elements = arrLiteral.elements as ts.NodeArray<ts.CallExpression>;
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[],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down