Skip to content

Commit f277d95

Browse files
clydinalan-agius4
authored andcommitted
refactor(@angular-devkit/build-angular): convert build optimizer passes to babel plugins
The build optimizer passes that are still relevant for Ivy are now implemented as babel plugins. Using babel has several advantages. The `babel-loader` is already present within the build pipeline and many packages will already need to be processed by it. The `babel-loader` also provides the necessary infrastructure to setup babel and link it to the Webpack build system. This removes the need for the infrastructure code within the build optimizer loader and reduces the build optimizer to only a set of transformation plugins for babel to consume. The babel plugin architecture also allows for less code to represent similar transformations and provides a variety of helper utilities which further reduces the amount code needed. The passes are now implemented to be safer when transforming code. Enum values which contain potential side effects are also no longer altered. Enums are wrapped in less destructive manner which reduces the likelihood of incorrectly emitting the transformed enum. Class static fields which contain potential side effects are no longer wrapped in pure annotated IIFEs. Known safe Angular static fields are also wrapped and Angular static fields which are not required in production code are also dropped. The conversion of the passes not only reduces the amount of code to maintain but also provides a noticeable performance improvement. Initial tests of a production build of AIO resulted in a ~10% total build time reduction. Closes angular#12160
1 parent bd82967 commit f277d95

16 files changed

+1575
-18
lines changed

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"@angular/service-worker": "12.0.3",
8484
"@babel/core": "7.14.3",
8585
"@babel/generator": "7.14.3",
86+
"@babel/helper-annotate-as-pure": "7.12.13",
8687
"@babel/plugin-transform-runtime": "7.14.3",
8788
"@babel/preset-env": "7.14.4",
8889
"@babel/runtime": "7.14.0",
@@ -226,7 +227,7 @@
226227
"tree-kill": "1.2.2",
227228
"ts-api-guardian": "0.6.0",
228229
"ts-node": "^10.0.0",
229-
"tslib": "^2.0.0",
230+
"tslib": "2.2.0",
230231
"tslint": "^6.1.3",
231232
"typescript": "4.2.4",
232233
"verdaccio": "5.1.0",

packages/angular_devkit/build_angular/BUILD.bazel

+4
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ ts_library(
109109
"@npm//@angular/localize",
110110
"@npm//@angular/service-worker",
111111
"@npm//@babel/core",
112+
"@npm//@babel/helper-annotate-as-pure",
112113
"@npm//@babel/plugin-transform-runtime",
113114
"@npm//@babel/preset-env",
114115
"@npm//@babel/runtime",
@@ -184,6 +185,7 @@ ts_library(
184185
"@npm//terser-webpack-plugin",
185186
"@npm//text-table",
186187
"@npm//tree-kill",
188+
"@npm//tslib",
187189
"@npm//tslint",
188190
"@npm//typescript",
189191
"@npm//webpack",
@@ -201,6 +203,7 @@ ts_library(
201203
include = [
202204
"plugins/**/*_spec.ts",
203205
"src/utils/**/*_spec.ts",
206+
"src/babel/**/*_spec.ts",
204207
"src/angular-cli-files/**/*_spec.ts",
205208
],
206209
),
@@ -210,6 +213,7 @@ ts_library(
210213
":build_angular",
211214
"//packages/angular_devkit/architect/testing",
212215
"//packages/angular_devkit/core",
216+
"@npm//prettier",
213217
"@npm//typescript",
214218
"@npm//webpack",
215219
],

packages/angular_devkit/build_angular/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"@angular-devkit/core": "0.0.0",
1313
"@babel/core": "7.14.3",
1414
"@babel/generator": "7.14.3",
15+
"@babel/helper-annotate-as-pure": "7.12.13",
1516
"@babel/plugin-transform-async-to-generator": "7.13.0",
1617
"@babel/plugin-transform-runtime": "7.14.3",
1718
"@babel/preset-env": "7.14.4",
@@ -67,6 +68,7 @@
6768
"terser-webpack-plugin": "5.1.3",
6869
"text-table": "0.2.0",
6970
"tree-kill": "1.2.2",
71+
"tslib": "2.2.0",
7072
"webpack": "5.38.1",
7173
"webpack-dev-middleware": "5.0.0",
7274
"webpack-dev-server": "3.11.2",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { NodePath, PluginObj, PluginPass, types } from '@babel/core';
10+
import annotateAsPure from '@babel/helper-annotate-as-pure';
11+
12+
/**
13+
* The name of the Typescript decorator helper function created by the TypeScript compiler.
14+
*/
15+
const TSLIB_DECORATE_HELPER_NAME = '__decorate';
16+
17+
/**
18+
* The set of Angular static fields that should always be wrapped.
19+
* These fields may appear to have side effects but are safe to remove if the associated class
20+
* is otherwise unused within the output.
21+
*/
22+
const angularStaticsToWrap = new Set([
23+
'ɵcmp',
24+
'ɵdir',
25+
'ɵfac',
26+
'ɵinj',
27+
'ɵmod',
28+
'ɵpipe',
29+
'ɵprov',
30+
'INJECTOR_KEY',
31+
]);
32+
33+
/**
34+
* An object map of static fields and related value checks for discovery of Angular generated
35+
* JIT related static fields.
36+
*/
37+
const angularStaticsToElide: Record<string, (path: NodePath<types.Expression>) => boolean> = {
38+
'ctorParameters'(path) {
39+
return path.isFunctionExpression() || path.isArrowFunctionExpression();
40+
},
41+
'decorators'(path) {
42+
return path.isArrayExpression();
43+
},
44+
'propDecorators'(path) {
45+
return path.isObjectExpression();
46+
},
47+
};
48+
49+
/**
50+
* Provides one or more keywords that if found within the content of a source file indicate
51+
* that this plugin should be used with a source file.
52+
*
53+
* @returns An a string iterable containing one or more keywords.
54+
*/
55+
export function getKeywords(): Iterable<string> {
56+
return ['class'];
57+
}
58+
59+
/**
60+
* Analyze the sibling nodes of a class to determine if any downlevel elements should be
61+
* wrapped in a pure annotated IIFE. Also determines if any elements have potential side
62+
* effects.
63+
*
64+
* @param origin The starting NodePath location for analyzing siblings.
65+
* @param classIdentifier The identifier node that represents the name of the class.
66+
* @param allowWrappingDecorators Whether to allow decorators to be wrapped.
67+
* @returns An object containing the results of the analysis.
68+
*/
69+
function analyzeClassSiblings(
70+
origin: NodePath,
71+
classIdentifier: types.Identifier,
72+
allowWrappingDecorators: boolean,
73+
): { hasPotentialSideEffects: boolean; wrapStatementPaths: NodePath<types.Statement>[] } {
74+
const wrapStatementPaths: NodePath<types.Statement>[] = [];
75+
let hasPotentialSideEffects = false;
76+
for (let i = 1; ; ++i) {
77+
const nextStatement = origin.getSibling(+origin.key + i);
78+
if (!nextStatement.isExpressionStatement()) {
79+
break;
80+
}
81+
82+
// Valid sibling statements for class declarations are only assignment expressions
83+
// and TypeScript decorator helper call expressions
84+
const nextExpression = nextStatement.get('expression');
85+
if (nextExpression.isCallExpression()) {
86+
if (
87+
!types.isIdentifier(nextExpression.node.callee) ||
88+
nextExpression.node.callee.name !== TSLIB_DECORATE_HELPER_NAME
89+
) {
90+
break;
91+
}
92+
93+
if (allowWrappingDecorators) {
94+
wrapStatementPaths.push(nextStatement);
95+
} else {
96+
// Statement cannot be safely wrapped which makes wrapping the class unneeded.
97+
// The statement will prevent even a wrapped class from being optimized away.
98+
hasPotentialSideEffects = true;
99+
}
100+
101+
continue;
102+
} else if (!nextExpression.isAssignmentExpression()) {
103+
break;
104+
}
105+
106+
// Valid assignment expressions should be member access expressions using the class
107+
// name as the object and an identifier as the property for static fields or only
108+
// the class name for decorators.
109+
const left = nextExpression.get('left');
110+
if (left.isIdentifier()) {
111+
if (
112+
!left.scope.bindingIdentifierEquals(left.node.name, classIdentifier) ||
113+
!types.isCallExpression(nextExpression.node.right) ||
114+
!types.isIdentifier(nextExpression.node.right.callee) ||
115+
nextExpression.node.right.callee.name !== TSLIB_DECORATE_HELPER_NAME
116+
) {
117+
break;
118+
}
119+
120+
if (allowWrappingDecorators) {
121+
wrapStatementPaths.push(nextStatement);
122+
} else {
123+
// Statement cannot be safely wrapped which makes wrapping the class unneeded.
124+
// The statement will prevent even a wrapped class from being optimized away.
125+
hasPotentialSideEffects = true;
126+
}
127+
128+
continue;
129+
} else if (
130+
!left.isMemberExpression() ||
131+
!types.isIdentifier(left.node.object) ||
132+
!left.scope.bindingIdentifierEquals(left.node.object.name, classIdentifier) ||
133+
!types.isIdentifier(left.node.property)
134+
) {
135+
break;
136+
}
137+
138+
const propertyName = left.node.property.name;
139+
const assignmentValue = nextExpression.get('right');
140+
if (angularStaticsToElide[propertyName]?.(assignmentValue)) {
141+
nextStatement.remove();
142+
--i;
143+
} else if (angularStaticsToWrap.has(propertyName) || assignmentValue.isPure()) {
144+
wrapStatementPaths.push(nextStatement);
145+
} else {
146+
// Statement cannot be safely wrapped which makes wrapping the class unneeded.
147+
// The statement will prevent even a wrapped class from being optimized away.
148+
hasPotentialSideEffects = true;
149+
}
150+
}
151+
152+
return { hasPotentialSideEffects, wrapStatementPaths };
153+
}
154+
155+
/**
156+
* The set of classed already visited and analyzed during the plugin's execution.
157+
* This is used to prevent adjusted classes from being repeatedly analyzed which can lead
158+
* to an infinite loop.
159+
*/
160+
const visitedClasses = new WeakSet<types.Class>();
161+
162+
/**
163+
* A babel plugin factory function for adjusting classes; primarily with Angular metadata.
164+
* The adjustments include wrapping classes with known safe or no side effects with pure
165+
* annotations to support dead code removal of unused classes. Angular compiler generated
166+
* metadata static fields not required in AOT mode are also elided to better support bundler-
167+
* level treeshaking.
168+
*
169+
* @returns A babel plugin object instance.
170+
*/
171+
export default function (): PluginObj {
172+
return {
173+
visitor: {
174+
ClassDeclaration(path: NodePath<types.ClassDeclaration>, state: PluginPass) {
175+
const { node: classNode, parentPath } = path;
176+
const { wrapDecorators } = state.opts as { wrapDecorators: boolean };
177+
178+
if (visitedClasses.has(classNode)) {
179+
return;
180+
}
181+
182+
// Analyze sibling statements for elements of the class that were downleveled
183+
const hasExport =
184+
parentPath.isExportNamedDeclaration() || parentPath.isExportDefaultDeclaration();
185+
const origin = hasExport ? parentPath : path;
186+
const { wrapStatementPaths, hasPotentialSideEffects } = analyzeClassSiblings(
187+
origin,
188+
classNode.id,
189+
wrapDecorators,
190+
);
191+
192+
visitedClasses.add(classNode);
193+
194+
if (hasPotentialSideEffects || wrapStatementPaths.length === 0) {
195+
return;
196+
}
197+
198+
const wrapStatementNodes: types.Statement[] = [];
199+
for (const statementPath of wrapStatementPaths) {
200+
wrapStatementNodes.push(statementPath.node);
201+
statementPath.remove();
202+
}
203+
204+
// Wrap class and safe static assignments in a pure annotated IIFE
205+
const container = types.arrowFunctionExpression(
206+
[],
207+
types.blockStatement([
208+
classNode,
209+
...wrapStatementNodes,
210+
types.returnStatement(types.cloneNode(classNode.id)),
211+
]),
212+
);
213+
const replacementInitializer = types.callExpression(
214+
types.parenthesizedExpression(container),
215+
[],
216+
);
217+
annotateAsPure(replacementInitializer);
218+
219+
// Replace class with IIFE wrapped class
220+
const declaration = types.variableDeclaration('let', [
221+
types.variableDeclarator(types.cloneNode(classNode.id), replacementInitializer),
222+
]);
223+
if (parentPath.isExportDefaultDeclaration()) {
224+
// When converted to a variable declaration, the default export must be moved
225+
// to a subsequent statement to prevent a JavaScript syntax error.
226+
parentPath.replaceWithMultiple([
227+
declaration,
228+
types.exportNamedDeclaration(undefined, [
229+
types.exportSpecifier(types.cloneNode(classNode.id), types.identifier('default')),
230+
]),
231+
]);
232+
} else {
233+
path.replaceWith(declaration);
234+
}
235+
},
236+
ClassExpression(path: NodePath<types.ClassExpression>, state: PluginPass) {
237+
const { node: classNode, parentPath } = path;
238+
const { wrapDecorators } = state.opts as { wrapDecorators: boolean };
239+
240+
// Class expressions are used by TypeScript to represent downlevel class/constructor decorators.
241+
// If not wrapping decorators, they do not need to be processed.
242+
if (!wrapDecorators || visitedClasses.has(classNode)) {
243+
return;
244+
}
245+
246+
if (
247+
!classNode.id ||
248+
!parentPath.isVariableDeclarator() ||
249+
!types.isIdentifier(parentPath.node.id) ||
250+
parentPath.node.id.name !== classNode.id.name
251+
) {
252+
return;
253+
}
254+
255+
const origin = parentPath.parentPath;
256+
if (!origin.isVariableDeclaration() || origin.node.declarations.length !== 1) {
257+
return;
258+
}
259+
260+
const { wrapStatementPaths, hasPotentialSideEffects } = analyzeClassSiblings(
261+
origin,
262+
parentPath.node.id,
263+
wrapDecorators,
264+
);
265+
266+
visitedClasses.add(classNode);
267+
268+
if (hasPotentialSideEffects || wrapStatementPaths.length === 0) {
269+
return;
270+
}
271+
272+
const wrapStatementNodes: types.Statement[] = [];
273+
for (const statementPath of wrapStatementPaths) {
274+
wrapStatementNodes.push(statementPath.node);
275+
statementPath.remove();
276+
}
277+
278+
// Wrap class and safe static assignments in a pure annotated IIFE
279+
const container = types.arrowFunctionExpression(
280+
[],
281+
types.blockStatement([
282+
types.variableDeclaration('let', [
283+
types.variableDeclarator(types.cloneNode(classNode.id), classNode),
284+
]),
285+
...wrapStatementNodes,
286+
types.returnStatement(types.cloneNode(classNode.id)),
287+
]),
288+
);
289+
const replacementInitializer = types.callExpression(
290+
types.parenthesizedExpression(container),
291+
[],
292+
);
293+
annotateAsPure(replacementInitializer);
294+
295+
// Add the wrapped class directly to the variable declaration
296+
parentPath.get('init').replaceWith(replacementInitializer);
297+
},
298+
},
299+
};
300+
}

0 commit comments

Comments
 (0)