Skip to content

Commit 1b0d5b6

Browse files
author
Alan Agius
committed
fix(@angular-devkit/build-optimizer): wrap ClassDeclarations in an IIFE for better treeshaking
With this change we wrap ClassDeclarations inside an IIFE, also we move some code from the class fold into the wrap-enums. This changes the below code: ```js export class Foo { method() { } } Foo.bar = 'barValue'; __decorate([ methodDecorator ], Foo.prototype, "method", null); ``` to ```js export const Foo = /*@__PURE__*/ (() => { class Foo { method() { } } Foo.bar = 'barValue'; __decorate([ methodDecorator ], Foo.prototype, "method", null); return Foo; })(); ``` Fixes #14610
1 parent 0873e75 commit 1b0d5b6

File tree

5 files changed

+282
-76
lines changed

5 files changed

+282
-76
lines changed

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

-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
TransformJavascriptOutput,
1212
transformJavascript,
1313
} from '../helpers/transform-javascript';
14-
import { getFoldFileTransformer } from '../transforms/class-fold';
1514
import { getImportTslibTransformer, testImportTslib } from '../transforms/import-tslib';
1615
import { getPrefixClassesTransformer, testPrefixClasses } from '../transforms/prefix-classes';
1716
import { getPrefixFunctionsTransformer } from '../transforms/prefix-functions';
@@ -124,14 +123,12 @@ export function buildOptimizer(options: BuildOptimizerOptions): TransformJavascr
124123
// getPrefixFunctionsTransformer needs to be before getFoldFileTransformer.
125124
getPrefixFunctionsTransformer,
126125
selectedGetScrubFileTransformer,
127-
getFoldFileTransformer,
128126
);
129127
typeCheck = true;
130128
} else if (testScrubFile(content)) {
131129
// Always test as these require the type checker
132130
getTransforms.push(
133131
selectedGetScrubFileTransformer,
134-
getFoldFileTransformer,
135132
);
136133
typeCheck = true;
137134
}

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

+2-5
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,10 @@ import { buildOptimizer } from './build-optimizer';
1616
describe('build-optimizer', () => {
1717
const imports = 'import { Injectable, Input, Component } from \'@angular/core\';';
1818
const clazz = 'var Clazz = (function () { function Clazz() { } return Clazz; }());';
19-
const staticProperty = 'Clazz.prop = 1;';
2019
const decorators = 'Clazz.decorators = [ { type: Injectable } ];';
2120

2221
describe('basic functionality', () => {
23-
it('applies class-fold, scrub-file and prefix-functions to side-effect free modules', () => {
22+
it('applies scrub-file and prefix-functions to side-effect free modules', () => {
2423
const input = tags.stripIndent`
2524
${imports}
2625
var __extends = (this && this.__extends) || function (d, b) {
@@ -34,7 +33,6 @@ describe('build-optimizer', () => {
3433
ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default";
3534
})(ChangeDetectionStrategy || (ChangeDetectionStrategy = {}));
3635
${clazz}
37-
${staticProperty}
3836
${decorators}
3937
Clazz.propDecorators = { 'ngIf': [{ type: Input }] };
4038
Clazz.ctorParameters = function () { return [{type: Injectable}]; };
@@ -65,7 +63,7 @@ describe('build-optimizer', () => {
6563
ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default";
6664
return ChangeDetectionStrategy;
6765
})({});
68-
var Clazz = /*@__PURE__*/ (function () { function Clazz() { } ${staticProperty} return Clazz; }());
66+
var Clazz = /*@__PURE__*/ (function () { function Clazz() { } return Clazz; }());
6967
var ComponentClazz = /*@__PURE__*/ (function () {
7068
function ComponentClazz() { }
7169
return ComponentClazz;
@@ -202,7 +200,6 @@ describe('build-optimizer', () => {
202200
xit('doesn\'t produce sourcemaps when emitting was skipped', () => {
203201
const ignoredInput = tags.oneLine`
204202
var Clazz = (function () { function Clazz() { } return Clazz; }());
205-
${staticProperty}
206203
`;
207204
const invalidInput = tags.oneLine`
208205
))))invalid syntax

packages/angular_devkit/build_optimizer/src/transforms/class-fold.ts

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ interface StatementData {
2020
hostClass: ClassData;
2121
}
2222

23+
/** @deprecated Since version 8 */
2324
export function getFoldFileTransformer(program: ts.Program): ts.TransformerFactory<ts.SourceFile> {
2425
const checker = program.getTypeChecker();
2526

packages/angular_devkit/build_optimizer/src/transforms/wrap-enums.ts

+121-64
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ function visitBlockStatements(
6363
// 'oIndex' is the original statement index; 'uIndex' is the updated statement index
6464
for (let oIndex = 0, uIndex = 0; oIndex < statements.length - 1; oIndex++, uIndex++) {
6565
const currentStatement = statements[oIndex];
66+
let newStatement: ts.Statement | undefined;
67+
let oldStatementsLength = 0;
6668

6769
// these can't contain an enum declaration
6870
if (currentStatement.kind === ts.SyntaxKind.ImportDeclaration) {
@@ -79,53 +81,47 @@ function visitBlockStatements(
7981
// * not be last statement
8082
// * be a variable statement
8183
// * have only one declaration
82-
// * have an ClassExpression as a initializer
84+
// * have an ClassExpression or BinaryExpression and a right
85+
// of kind ClassExpression as a initializer
8386
if (ts.isVariableStatement(currentStatement)
8487
&& currentStatement.declarationList.declarations.length === 1) {
8588

8689
const variableDeclaration = currentStatement.declarationList.declarations[0];
90+
const initializer = variableDeclaration.initializer;
8791
if (ts.isIdentifier(variableDeclaration.name)) {
8892
const name = variableDeclaration.name.text;
8993

90-
if (!variableDeclaration.initializer) {
94+
if (!initializer) {
9195
const iife = findTs2_3EnumIife(name, statements[oIndex + 1]);
9296
if (iife) {
93-
// found an enum
94-
if (!updatedStatements) {
95-
updatedStatements = statements.slice();
96-
}
9797
// update IIFE and replace variable statement and old IIFE
98-
updatedStatements.splice(uIndex, 2, updateEnumIife(
98+
oldStatementsLength = 2;
99+
newStatement = updateEnumIife(
99100
currentStatement,
100101
iife[0],
101102
iife[1],
102-
));
103+
);
103104
// skip IIFE statement
104105
oIndex++;
105-
continue;
106106
}
107-
} else if (ts.isObjectLiteralExpression(variableDeclaration.initializer)
108-
&& variableDeclaration.initializer.properties.length === 0) {
107+
} else if (ts.isObjectLiteralExpression(initializer)
108+
&& initializer.properties.length === 0) {
109109
const enumStatements = findTs2_2EnumStatements(name, statements, oIndex + 1);
110110
if (enumStatements.length > 0) {
111-
// found an enum
112-
if (!updatedStatements) {
113-
updatedStatements = statements.slice();
114-
}
115111
// create wrapper and replace variable statement and enum member statements
116-
updatedStatements.splice(uIndex, enumStatements.length + 1, createWrappedEnum(
112+
oldStatementsLength = enumStatements.length + 1;
113+
newStatement = createWrappedEnum(
117114
name,
118115
currentStatement,
119116
enumStatements,
120-
variableDeclaration.initializer,
121-
));
117+
initializer,
118+
);
122119
// skip enum member declarations
123120
oIndex += enumStatements.length;
124-
continue;
125121
}
126-
} else if (ts.isObjectLiteralExpression(variableDeclaration.initializer)
127-
&& variableDeclaration.initializer.properties.length !== 0) {
128-
const literalPropertyCount = variableDeclaration.initializer.properties.length;
122+
} else if (ts.isObjectLiteralExpression(initializer)
123+
&& initializer.properties.length !== 0) {
124+
const literalPropertyCount = initializer.properties.length;
129125

130126
// tsickle es2015 enums first statement is an export declaration
131127
const isPotentialEnumExport = ts.isExportDeclaration(statements[oIndex + 1]);
@@ -136,42 +132,61 @@ function visitBlockStatements(
136132

137133
const enumStatements = findEnumNameStatements(name, statements, oIndex + 1);
138134
if (enumStatements.length === literalPropertyCount) {
139-
// found an enum
140-
if (!updatedStatements) {
141-
updatedStatements = statements.slice();
142-
}
143135
// create wrapper and replace variable statement and enum member statements
144-
const deleteCount = enumStatements.length + (isPotentialEnumExport ? 2 : 1);
145-
updatedStatements.splice(uIndex, deleteCount, createWrappedEnum(
136+
oldStatementsLength = enumStatements.length + (isPotentialEnumExport ? 2 : 1);
137+
newStatement = createWrappedEnum(
146138
name,
147139
currentStatement,
148140
enumStatements,
149-
variableDeclaration.initializer,
141+
initializer,
150142
isPotentialEnumExport,
151-
));
143+
);
152144
// skip enum member declarations
153145
oIndex += enumStatements.length;
154-
continue;
155146
}
156-
} else if (ts.isClassExpression(variableDeclaration.initializer)) {
157-
const classStatements = findClassExpressionStatements(name, statements, oIndex);
147+
} else if (
148+
ts.isClassExpression(initializer)
149+
|| (
150+
ts.isBinaryExpression(initializer)
151+
&& ts.isClassExpression(initializer.right)
152+
)
153+
) {
154+
const classStatements = findClassStatements(name, statements, oIndex);
158155
if (!classStatements) {
159156
continue;
160157
}
161158

162-
if (!updatedStatements) {
163-
updatedStatements = [...statements];
164-
}
165-
166-
updatedStatements.splice(uIndex, classStatements.length, createWrappedClass(
167-
name,
159+
oldStatementsLength = classStatements.length;
160+
newStatement = createWrappedClass(
161+
variableDeclaration,
168162
classStatements,
169-
));
163+
);
170164

171165
oIndex += classStatements.length - 1;
172-
continue;
173166
}
174167
}
168+
} else if (ts.isClassDeclaration(currentStatement)) {
169+
const name = (currentStatement.name as ts.Identifier).text;
170+
const classStatements = findClassStatements(name, statements, oIndex);
171+
if (!classStatements) {
172+
continue;
173+
}
174+
175+
oldStatementsLength = classStatements.length;
176+
newStatement = createWrappedClass(
177+
currentStatement,
178+
classStatements,
179+
);
180+
181+
oIndex += classStatements.length - 1;
182+
}
183+
184+
if (newStatement) {
185+
if (!updatedStatements) {
186+
updatedStatements = [...statements];
187+
}
188+
189+
updatedStatements.splice(uIndex, oldStatementsLength, newStatement);
175190
}
176191

177192
const result = ts.visitNode(currentStatement, visitor);
@@ -433,7 +448,7 @@ function updateHostNode(
433448
}
434449

435450
/**
436-
* Find class expression statements.
451+
* Find class expression or declaration statements.
437452
*
438453
* The classExpressions block to wrap in an iife must
439454
* - end with an ExpressionStatement
@@ -445,36 +460,63 @@ function updateHostNode(
445460
Foo = __decorate([]);
446461
```
447462
*/
448-
function findClassExpressionStatements(
463+
function findClassStatements(
449464
name: string,
450465
statements: ts.NodeArray<ts.Statement>,
451466
statementIndex: number,
452467
): ts.Statement[] | undefined {
453-
let index = statementIndex + 1;
454-
let statement = statements[index];
468+
let count = 1;
469+
470+
for (let index = statementIndex + 1; index < statements.length; ++index) {
471+
const statement = statements[index];
472+
if (!ts.isExpressionStatement(statement)) {
473+
break;
474+
}
455475

456-
while (ts.isExpressionStatement(statement)) {
457476
const expression = statement.expression;
477+
458478
if (ts.isCallExpression(expression)) {
459-
// Ex:
460-
// __decorate([propDecorator()], FooClass, "propertyName", void 0);
461-
// __decorate$1([propDecorator()], FooClass, "propertyName", void 0);
462-
const callExpression = expression.expression;
463-
if (!ts.isIdentifier(callExpression) || !/^__decorate(\$\d+)?$/.test(callExpression.text)) {
464-
break;
479+
// Ex:
480+
// setClassMetadata(FooClass, [{}], void 0);
481+
// __decorate([propDecorator()], FooClass.prototype, "propertyName", void 0);
482+
// __decorate([propDecorator()], FooClass, "propertyName", void 0);
483+
// __decorate$1([propDecorator()], FooClass, "propertyName", void 0);
484+
const args = expression.arguments;
485+
486+
if (args.length > 2) {
487+
const isReferenced = args.some(arg => {
488+
const potentialIdentifier = ts.isPropertyAccessExpression(arg) ? arg.expression : arg;
489+
490+
return ts.isIdentifier(potentialIdentifier) && potentialIdentifier.text === name;
491+
});
492+
493+
if (isReferenced) {
494+
count++;
495+
continue;
496+
}
497+
}
498+
} else if (ts.isBinaryExpression(expression)) {
499+
const node = ts.isBinaryExpression(expression.left)
500+
? expression.left.left
501+
: expression.left;
502+
503+
const leftExpression = ts.isPropertyAccessExpression(node)
504+
// Static Properties // Ex: Foo.bar = 'value';
505+
? node.expression
506+
// Ex: FooClass = __decorate([Component()], FooClass);
507+
: node;
508+
509+
if (ts.isIdentifier(leftExpression) && leftExpression.text === name) {
510+
count++;
511+
continue;
465512
}
466513
}
467514

468-
if (
469-
ts.isBinaryExpression(expression)
470-
&& ts.isIdentifier(expression.left)
471-
&& expression.left.getText() === name
472-
) {
473-
// Ex: FooClass = __decorate([Component()], FooClass);
474-
return statements.slice(statementIndex, index + 1);
475-
}
515+
break;
516+
}
476517

477-
statement = statements[++index];
518+
if (count > 1) {
519+
return statements.slice(statementIndex, statementIndex + count);
478520
}
479521

480522
return undefined;
@@ -573,18 +615,33 @@ function createWrappedEnum(
573615
}
574616

575617
function createWrappedClass(
576-
name: string,
618+
hostNode: ts.ClassDeclaration | ts.VariableDeclaration,
577619
statements: ts.Statement[],
578620
): ts.Statement {
621+
const name = (hostNode.name as ts.Identifier).text;
622+
623+
const updatedStatements = [...statements];
624+
625+
if (ts.isClassDeclaration(hostNode)) {
626+
updatedStatements[0] = ts.createClassDeclaration(
627+
hostNode.decorators,
628+
undefined,
629+
hostNode.name,
630+
hostNode.typeParameters,
631+
hostNode.heritageClauses,
632+
hostNode.members,
633+
);
634+
}
635+
579636
const pureIife = addPureComment(
580637
ts.createImmediatelyInvokedArrowFunction([
581-
...statements,
638+
...updatedStatements,
582639
ts.createReturn(ts.createIdentifier(name)),
583640
]),
584641
);
585642

586643
return ts.createVariableStatement(
587-
undefined,
644+
hostNode.modifiers,
588645
ts.createVariableDeclarationList([
589646
ts.createVariableDeclaration(name, undefined, pureIife),
590647
],

0 commit comments

Comments
 (0)