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

Commit 1cbb633

Browse files
committed
feat(@angular-devkit/build-optimizer): strip Angular decorators in __decorate call
1 parent 38e8b7c commit 1cbb633

File tree

4 files changed

+183
-6
lines changed

4 files changed

+183
-6
lines changed

packages/angular_devkit/build_optimizer/README.md

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,37 @@ Angular decorators, property decorators and constructor parameters are removed,
3636

3737
```typescript
3838
// input
39-
import { Injectable, Input } from '@angular/core';
40-
import { NotInjectable } from 'another-lib';
39+
import { Injectable, Input, Component } from '@angular/core';
40+
import { NotInjectable, NotComponent } from 'another-lib';
4141
var Clazz = (function () { function Clazz() { } return Clazz; }());
4242
Clazz.decorators = [{ type: Injectable }, { type: NotInjectable }];
4343
Clazz.propDecorators = { 'ngIf': [{ type: Input }] };
4444
Clazz.ctorParameters = function () { return [{type: Injector}]; };
45+
var ComponentClazz = (function () {
46+
function ComponentClazz() { }
47+
ComponentClazz = __decorate([
48+
NotComponent(),
49+
Component({
50+
selector: 'app-root',
51+
templateUrl: './app.component.html',
52+
styleUrls: ['./app.component.css']
53+
})
54+
], ComponentClazz);
55+
return ComponentClazz;
56+
}());
4557

4658
// output
47-
import { Injectable, Input } from '@angular/core';
48-
import { NotInjectable } from 'another-lib';
59+
import { Injectable, Input, Component } from '@angular/core';
60+
import { NotInjectable, NotComponent } from 'another-lib';
4961
var Clazz = (function () { function Clazz() { } return Clazz; }());
5062
Clazz.decorators = [{ type: NotInjectable }];
63+
var ComponentClazz = (function () {
64+
function ComponentClazz() { }
65+
ComponentClazz = __decorate([
66+
NotComponent()
67+
], ComponentClazz);
68+
return ComponentClazz;
69+
}());
5170
```
5271

5372

packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer_spec.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { buildOptimizer } from './build-optimizer';
1111

1212

1313
describe('build-optimizer', () => {
14-
const imports = 'import { Injectable, Input } from \'@angular/core\';';
14+
const imports = 'import { Injectable, Input, Component } from \'@angular/core\';';
1515
const clazz = 'var Clazz = (function () { function Clazz() { } return Clazz; }());';
1616
const staticProperty = 'Clazz.prop = 1;';
1717
const decorators = 'Clazz.decorators = [ { type: Injectable } ];';
@@ -35,6 +35,17 @@ describe('build-optimizer', () => {
3535
${decorators}
3636
Clazz.propDecorators = { 'ngIf': [{ type: Input }] };
3737
Clazz.ctorParameters = function () { return [{type: Injector}]; };
38+
var ComponentClazz = (function () {
39+
function ComponentClazz() { }
40+
ComponentClazz = __decorate([
41+
Component({
42+
selector: 'app-root',
43+
templateUrl: './app.component.html',
44+
styleUrls: ['./app.component.css']
45+
})
46+
], ComponentClazz);
47+
return ComponentClazz;
48+
}());
3849
`;
3950
// tslint:disable:max-line-length
4051
const output = tags.oneLine`
@@ -48,6 +59,10 @@ describe('build-optimizer', () => {
4859
return ChangeDetectionStrategy;
4960
})();
5061
var Clazz = /*@__PURE__*/ (function () { function Clazz() { } ${staticProperty} return Clazz; }());
62+
var ComponentClazz = /*@__PURE__*/ (function () {
63+
function ComponentClazz() { }
64+
return ComponentClazz;
65+
}());
5166
`;
5267

5368
// Check Angular 4/5 and unix/windows paths.

packages/angular_devkit/build_optimizer/src/transforms/scrub-file.ts

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { collectDeepNodes } from '../helpers/ast-utils';
1212
export function testScrubFile(content: string) {
1313
const markers = [
1414
'decorators',
15+
'__decorate',
1516
'propDecorators',
1617
'ctorParameters',
1718
];
@@ -68,6 +69,9 @@ export function getScrubFileTransformer(program: ts.Program): ts.TransformerFact
6869
if (isDecoratorAssignmentExpression(exprStmt)) {
6970
nodes.push(...pickDecorationNodesToRemove(exprStmt, ngMetadata, checker));
7071
}
72+
if (isDecorateAssignmentExpression(exprStmt)) {
73+
nodes.push(...pickDecorateNodesToRemove(exprStmt, ngMetadata, checker));
74+
}
7175
if (isPropDecoratorAssignmentExpression(exprStmt)) {
7276
nodes.push(...pickPropDecorationNodesToRemove(exprStmt, ngMetadata, checker));
7377
}
@@ -99,7 +103,7 @@ export function getScrubFileTransformer(program: ts.Program): ts.TransformerFact
99103

100104
export function expect<T extends ts.Node>(node: ts.Node, kind: ts.SyntaxKind): T {
101105
if (node.kind !== kind) {
102-
throw new Error('Invalid!');
106+
throw new Error('Invalid node type.');
103107
}
104108

105109
return node as T;
@@ -158,6 +162,7 @@ function isAngularCoreSpecifier(node: ts.ImportSpecifier): boolean {
158162
return angularSpecifiers.indexOf(nameOfSpecifier(node)) !== -1;
159163
}
160164

165+
// Check if assignment is `Clazz.decorators = [...];`.
161166
function isDecoratorAssignmentExpression(exprStmt: ts.ExpressionStatement): boolean {
162167
if (exprStmt.expression.kind !== ts.SyntaxKind.BinaryExpression) {
163168
return false;
@@ -183,6 +188,46 @@ function isDecoratorAssignmentExpression(exprStmt: ts.ExpressionStatement): bool
183188
return true;
184189
}
185190

191+
// Check if assignment is `Clazz = __decorate([...], Clazz)`.
192+
function isDecorateAssignmentExpression(exprStmt: ts.ExpressionStatement): boolean {
193+
if (exprStmt.expression.kind !== ts.SyntaxKind.BinaryExpression) {
194+
return false;
195+
}
196+
const expr = exprStmt.expression as ts.BinaryExpression;
197+
if (expr.left.kind !== ts.SyntaxKind.Identifier) {
198+
return false;
199+
}
200+
if (expr.right.kind !== ts.SyntaxKind.CallExpression) {
201+
return false;
202+
}
203+
const classIdent = expr.left as ts.Identifier;
204+
const callExpr = expr.right as ts.CallExpression;
205+
if (callExpr.expression.kind !== ts.SyntaxKind.Identifier) {
206+
return false;
207+
}
208+
const callExprIdent = callExpr.expression as ts.Identifier;
209+
// node.text on a name that starts with two underscores will return three instead.
210+
if (callExprIdent.text !== '___decorate') {
211+
return false;
212+
}
213+
if (callExpr.arguments.length !== 2) {
214+
return false;
215+
}
216+
if (callExpr.arguments[1].kind !== ts.SyntaxKind.Identifier) {
217+
return false;
218+
}
219+
const classArg = callExpr.arguments[1] as ts.Identifier;
220+
if (classIdent.text !== classArg.text) {
221+
return false;
222+
}
223+
if (callExpr.arguments[0].kind !== ts.SyntaxKind.ArrayLiteralExpression) {
224+
return false;
225+
}
226+
227+
return true;
228+
}
229+
230+
// Check if assignment is `Clazz.propDecorators = [...];`.
186231
function isPropDecoratorAssignmentExpression(exprStmt: ts.ExpressionStatement): boolean {
187232
if (exprStmt.expression.kind !== ts.SyntaxKind.BinaryExpression) {
188233
return false;
@@ -208,6 +253,7 @@ function isPropDecoratorAssignmentExpression(exprStmt: ts.ExpressionStatement):
208253
return true;
209254
}
210255

256+
// Check if assignment is `Clazz.ctorParameters = [...];`.
211257
function isCtorParamsAssignmentExpression(exprStmt: ts.ExpressionStatement): boolean {
212258
if (exprStmt.expression.kind !== ts.SyntaxKind.BinaryExpression) {
213259
return false;
@@ -243,6 +289,8 @@ function isCtorParamsWhitelistedService(exprStmt: ts.ExpressionStatement): boole
243289
return platformWhitelist.indexOf(serviceId.text) !== -1;
244290
}
245291

292+
// Remove Angular decorators from`Clazz.decorators = [...];`, or expression itself if all are
293+
// removed.
246294
function pickDecorationNodesToRemove(
247295
exprStmt: ts.ExpressionStatement,
248296
ngMetadata: ts.Node[],
@@ -261,6 +309,38 @@ function pickDecorationNodesToRemove(
261309
return (elements.length > ngDecorators.length) ? ngDecorators : [exprStmt];
262310
}
263311

312+
// Remove Angular decorators from `Clazz = __decorate([...], Clazz)`, or expression itself if all
313+
// are removed.
314+
function pickDecorateNodesToRemove(
315+
exprStmt: ts.ExpressionStatement,
316+
ngMetadata: ts.Node[],
317+
checker: ts.TypeChecker,
318+
): ts.Node[] {
319+
320+
const expr = expect<ts.BinaryExpression>(exprStmt.expression, ts.SyntaxKind.BinaryExpression);
321+
const callExpr = expect<ts.CallExpression>(expr.right, ts.SyntaxKind.CallExpression);
322+
const arrLiteral = expect<ts.ArrayLiteralExpression>(callExpr.arguments[0],
323+
ts.SyntaxKind.ArrayLiteralExpression);
324+
if (!arrLiteral.elements.every((elem) => elem.kind === ts.SyntaxKind.CallExpression)) {
325+
return [];
326+
}
327+
const elements = arrLiteral.elements as ts.NodeArray<ts.CallExpression>;
328+
const ngDecoratorCalls = elements.filter((el) => {
329+
if (el.expression.kind !== ts.SyntaxKind.Identifier) {
330+
return false;
331+
}
332+
const id = el.expression as ts.Identifier;
333+
334+
return identifierIsMetadata(id, ngMetadata, checker);
335+
});
336+
337+
// If all decorators are metadata decorators then return the whole `Class = __decorate([...])'`
338+
// statement so that it is removed in entirety
339+
return (elements.length === ngDecoratorCalls.length) ? [exprStmt] : ngDecoratorCalls;
340+
}
341+
342+
// Remove Angular decorators from`Clazz.propDecorators = [...];`, or expression itself if all
343+
// are removed.
264344
function pickPropDecorationNodesToRemove(
265345
exprStmt: ts.ExpressionStatement,
266346
ngMetadata: ts.Node[],

packages/angular_devkit/build_optimizer/src/transforms/scrub-file_spec.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,69 @@ describe('scrub-file', () => {
7979
});
8080
});
8181

82+
describe('__decorate', () => {
83+
it('removes Angular decorators calls in __decorate', () => {
84+
const output = tags.stripIndent`
85+
import { Component, Injectable } from '@angular/core';
86+
var Clazz = (function () {
87+
function Clazz() { }
88+
return Clazz;
89+
}());
90+
`;
91+
const input = tags.stripIndent`
92+
import { Component, Injectable } from '@angular/core';
93+
var Clazz = (function () {
94+
function Clazz() { }
95+
Clazz = __decorate([
96+
Injectable(),
97+
Component({
98+
selector: 'app-root',
99+
templateUrl: './app.component.html',
100+
styleUrls: ['./app.component.css']
101+
})
102+
], Clazz);
103+
return Clazz;
104+
}());
105+
`;
106+
107+
expect(testScrubFile(input)).toBeTruthy();
108+
expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`);
109+
});
110+
111+
it('removes only Angular decorators calls in __decorate', () => {
112+
const output = tags.stripIndent`
113+
import { Component } from '@angular/core';
114+
import { NotComponent } from 'another-lib';
115+
var Clazz = (function () {
116+
function Clazz() { }
117+
Clazz = __decorate([
118+
NotComponent()
119+
], Clazz);
120+
return Clazz;
121+
}());
122+
`;
123+
const input = tags.stripIndent`
124+
import { Component } from '@angular/core';
125+
import { NotComponent } from 'another-lib';
126+
var Clazz = (function () {
127+
function Clazz() { }
128+
Clazz = __decorate([
129+
NotComponent(),
130+
Component({
131+
selector: 'app-root',
132+
templateUrl: './app.component.html',
133+
styleUrls: ['./app.component.css']
134+
})
135+
], Clazz);
136+
return Clazz;
137+
}());
138+
`;
139+
140+
expect(testScrubFile(input)).toBeTruthy();
141+
expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`);
142+
});
143+
});
144+
82145
describe('propDecorators', () => {
83146
it('removes top-level Angular propDecorators', () => {
84147
const output = tags.stripIndent`

0 commit comments

Comments
 (0)