Skip to content

Commit 35b0594

Browse files
filipesilvamgechev
authored andcommitted
feat(@angular-devkit/build-optimizer): also fold ES2015 classes
Although ES5 classes had their static properties folded in, ES2015 ones did not. This PR adds that new functionality. It should also make this particular transform a bit faster since it will stop early. Fix #13487
1 parent b956db6 commit 35b0594

File tree

2 files changed

+138
-35
lines changed

2 files changed

+138
-35
lines changed

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

+79-23
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import * as ts from 'typescript';
99

1010
interface ClassData {
1111
name: string;
12-
class: ts.VariableDeclaration;
13-
classFunction: ts.FunctionExpression;
12+
declaration: ts.VariableDeclaration | ts.ClassDeclaration;
13+
function?: ts.FunctionExpression;
1414
statements: StatementData[];
1515
}
1616

@@ -26,28 +26,34 @@ export function getFoldFileTransformer(program: ts.Program): ts.TransformerFacto
2626

2727
const transformer: ts.Transformer<ts.SourceFile> = (sf: ts.SourceFile) => {
2828

29-
const classes = findClassDeclarations(sf);
30-
const statements = findClassStaticPropertyAssignments(sf, checker, classes);
29+
const statementsToRemove: ts.ExpressionStatement[] = [];
30+
const classesWithoutStatements = findClassDeclarations(sf);
31+
let classes = findClassesWithStaticPropertyAssignments(sf, checker, classesWithoutStatements);
3132

3233
const visitor: ts.Visitor = (node: ts.Node): ts.VisitResult<ts.Node> => {
34+
if (classes.length === 0 && statementsToRemove.length === 0) {
35+
// There are no more statements to fold.
36+
return ts.visitEachChild(node, visitor, context);
37+
}
38+
3339
// Check if node is a statement to be dropped.
34-
if (statements.find((st) => st.expressionStatement === node)) {
40+
const stmtIdx = statementsToRemove.indexOf(node as ts.ExpressionStatement);
41+
if (stmtIdx != -1) {
42+
statementsToRemove.splice(stmtIdx, 1);
43+
3544
return undefined;
3645
}
3746

38-
// Check if node is a class to add statements to.
39-
const clazz = classes.find((cl) => cl.classFunction === node);
47+
// Check if node is a ES5 class to add statements to.
48+
let clazz = classes.find((cl) => cl.function === node);
4049
if (clazz) {
41-
const functionExpression: ts.FunctionExpression = node as ts.FunctionExpression;
42-
43-
const newExpressions = clazz.statements.map((st) =>
44-
ts.createStatement(st.expressionStatement.expression));
50+
const functionExpression = node as ts.FunctionExpression;
4551

4652
// Create a new body with all the original statements, plus new ones,
4753
// plus return statement.
4854
const newBody = ts.createBlock([
4955
...functionExpression.body.statements.slice(0, -1),
50-
...newExpressions,
56+
...clazz.statements.map(st => st.expressionStatement),
5157
...functionExpression.body.statements.slice(-1),
5258
]);
5359

@@ -61,8 +67,47 @@ export function getFoldFileTransformer(program: ts.Program): ts.TransformerFacto
6167
newBody,
6268
);
6369

70+
// Update remaining classes and statements.
71+
statementsToRemove.push(...clazz.statements.map(st => st.expressionStatement));
72+
classes = classes.filter(cl => cl != clazz);
73+
6474
// Replace node with modified one.
65-
return ts.visitEachChild(newNode, visitor, context);
75+
return newNode;
76+
}
77+
78+
// Check if node is a ES2015 class to replace with a pure IIFE.
79+
clazz = classes.find((cl) => !cl.function && cl.declaration === node);
80+
if (clazz) {
81+
const classStatement = clazz.declaration as ts.ClassDeclaration;
82+
const innerReturn = ts.createReturn(ts.createIdentifier(clazz.name));
83+
84+
const iife = ts.createImmediatelyInvokedFunctionExpression([
85+
classStatement,
86+
...clazz.statements.map(st => st.expressionStatement),
87+
innerReturn,
88+
]);
89+
90+
const pureIife = ts.addSyntheticLeadingComment(
91+
iife,
92+
ts.SyntaxKind.MultiLineCommentTrivia,
93+
'@__PURE__',
94+
false,
95+
);
96+
97+
// Move the original class modifiers to the var statement.
98+
const newNode = ts.createVariableStatement(
99+
clazz.declaration.modifiers,
100+
ts.createVariableDeclarationList([
101+
ts.createVariableDeclaration(clazz.name, undefined, pureIife),
102+
], ts.NodeFlags.Const),
103+
);
104+
clazz.declaration.modifiers = undefined;
105+
106+
// Update remaining classes and statements.
107+
statementsToRemove.push(...clazz.statements.map(st => st.expressionStatement));
108+
classes = classes.filter(cl => cl != clazz);
109+
110+
return newNode;
66111
}
67112

68113
// Otherwise return node as is.
@@ -80,6 +125,19 @@ function findClassDeclarations(node: ts.Node): ClassData[] {
80125
const classes: ClassData[] = [];
81126
// Find all class declarations, build a ClassData for each.
82127
ts.forEachChild(node, (child) => {
128+
// Check if it is a named class declaration first.
129+
// Technically it doesn't need a name in TS if it's the default export, but when downleveled
130+
// it will be have a name (e.g. `default_1`).
131+
if (ts.isClassDeclaration(child) && child.name) {
132+
classes.push({
133+
name: child.name.text,
134+
declaration: child,
135+
statements: [],
136+
});
137+
138+
return;
139+
}
140+
83141
if (child.kind !== ts.SyntaxKind.VariableStatement) {
84142
return;
85143
}
@@ -122,22 +180,20 @@ function findClassDeclarations(node: ts.Node): ClassData[] {
122180
}
123181
classes.push({
124182
name,
125-
class: varDecl,
126-
classFunction: fn,
183+
declaration: varDecl,
184+
function: fn,
127185
statements: [],
128186
});
129187
});
130188

131189
return classes;
132190
}
133191

134-
function findClassStaticPropertyAssignments(
192+
function findClassesWithStaticPropertyAssignments(
135193
node: ts.Node,
136194
checker: ts.TypeChecker,
137-
classes: ClassData[]): StatementData[] {
138-
139-
const statements: StatementData[] = [];
140-
195+
classes: ClassData[],
196+
) {
141197
// Find each assignment outside of the declaration.
142198
ts.forEachChild(node, (child) => {
143199
if (child.kind !== ts.SyntaxKind.ExpressionStatement) {
@@ -166,15 +222,15 @@ function findClassStaticPropertyAssignments(
166222
return;
167223
}
168224

169-
const hostClass = classes.find((clazz => decls.includes(clazz.class)));
225+
const hostClass = classes.find((clazz => decls.includes(clazz.declaration)));
170226
if (!hostClass) {
171227
return;
172228
}
173229
const statement: StatementData = { expressionStatement, hostClass };
174230

175231
hostClass.statements.push(statement);
176-
statements.push(statement);
177232
});
178233

179-
return statements;
234+
// Only return classes that have static property assignments.
235+
return classes.filter(cl => cl.statements.length != 0);
180236
}

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

+59-12
Original file line numberDiff line numberDiff line change
@@ -15,33 +15,80 @@ const transform = (content: string) => transformJavascript(
1515
{ content, getTransforms: [getFoldFileTransformer], typeCheck: true }).content;
1616

1717
describe('class-fold', () => {
18-
it('folds static properties into class', () => {
19-
const staticProperty = 'Clazz.prop = 1;';
20-
const input = tags.stripIndent`
18+
describe('es5', () => {
19+
it('folds static properties into class', () => {
20+
const staticProperty = 'Clazz.prop = 1;';
21+
const input = tags.stripIndent`
2122
var Clazz = (function () { function Clazz() { } return Clazz; }());
2223
${staticProperty}
2324
`;
24-
const output = tags.stripIndent`
25+
const output = tags.stripIndent`
2526
var Clazz = (function () { function Clazz() { }
2627
${staticProperty} return Clazz; }());
2728
`;
2829

29-
expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`);
30-
});
30+
expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`);
31+
});
3132

32-
it('folds multiple static properties into class', () => {
33-
const staticProperty = 'Clazz.prop = 1;';
34-
const anotherStaticProperty = 'Clazz.anotherProp = 2;';
35-
const input = tags.stripIndent`
33+
it('folds multiple static properties into class', () => {
34+
const staticProperty = 'Clazz.prop = 1;';
35+
const anotherStaticProperty = 'Clazz.anotherProp = 2;';
36+
const input = tags.stripIndent`
3637
var Clazz = (function () { function Clazz() { } return Clazz; }());
3738
${staticProperty}
3839
${anotherStaticProperty}
3940
`;
40-
const output = tags.stripIndent`
41+
const output = tags.stripIndent`
4142
var Clazz = (function () { function Clazz() { }
4243
${staticProperty} ${anotherStaticProperty} return Clazz; }());
4344
`;
4445

45-
expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`);
46+
expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`);
47+
});
48+
});
49+
50+
describe('es2015', () => {
51+
it('folds static properties in IIFE', () => {
52+
const input = tags.stripIndent`
53+
export class TemplateRef { }
54+
TemplateRef.__NG_ELEMENT_ID__ = () => SWITCH_TEMPLATE_REF_FACTORY(TemplateRef, ElementRef);
55+
`;
56+
const output = tags.stripIndent`
57+
export const TemplateRef = /*@__PURE__*/ function () {
58+
class TemplateRef { }
59+
TemplateRef.__NG_ELEMENT_ID__ = () => SWITCH_TEMPLATE_REF_FACTORY(TemplateRef, ElementRef);
60+
return TemplateRef;
61+
}();
62+
`;
63+
64+
expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`);
65+
});
66+
67+
it('folds multiple static properties into class', () => {
68+
const input = tags.stripIndent`
69+
export class TemplateRef { }
70+
TemplateRef.__NG_ELEMENT_ID__ = () => SWITCH_TEMPLATE_REF_FACTORY(TemplateRef, ElementRef);
71+
TemplateRef.somethingElse = true;
72+
`;
73+
const output = tags.stripIndent`
74+
export const TemplateRef = /*@__PURE__*/ function () {
75+
class TemplateRef {
76+
}
77+
TemplateRef.__NG_ELEMENT_ID__ = () => SWITCH_TEMPLATE_REF_FACTORY(TemplateRef, ElementRef);
78+
TemplateRef.somethingElse = true;
79+
return TemplateRef;
80+
}();
81+
`;
82+
83+
expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`);
84+
});
85+
86+
it(`doesn't wrap classes without static properties in IIFE`, () => {
87+
const input = tags.stripIndent`
88+
export class TemplateRef { }
89+
`;
90+
91+
expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${input}`);
92+
});
4693
});
4794
});

0 commit comments

Comments
 (0)