Skip to content

Commit b52fb62

Browse files
committed
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 f9657bc commit b52fb62

13 files changed

+1452
-2
lines changed

packages/angular_devkit/build_angular/BUILD.bazel

+3
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ ts_library(
186186
"@npm//terser-webpack-plugin",
187187
"@npm//text-table",
188188
"@npm//tree-kill",
189+
"@npm//tslib",
189190
"@npm//tslint",
190191
"@npm//typescript",
191192
"@npm//webpack",
@@ -203,6 +204,7 @@ ts_library(
203204
include = [
204205
"plugins/**/*_spec.ts",
205206
"src/utils/**/*_spec.ts",
207+
"src/babel/**/*_spec.ts",
206208
"src/angular-cli-files/**/*_spec.ts",
207209
],
208210
),
@@ -212,6 +214,7 @@ ts_library(
212214
":build_angular",
213215
"//packages/angular_devkit/architect/testing",
214216
"//packages/angular_devkit/core",
217+
"@npm//prettier",
215218
"@npm//typescript",
216219
"@npm//webpack",
217220
],

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.2",
@@ -68,6 +69,7 @@
6869
"terser-webpack-plugin": "5.1.2",
6970
"text-table": "0.2.0",
7071
"tree-kill": "1.2.2",
72+
"tslib": "2.2.0",
7173
"webpack": "5.38.1",
7274
"webpack-dev-middleware": "4.2.0",
7375
"webpack-dev-server": "3.11.2",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
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+
* The set of classed already visited and analyzed during the plugin's execution.
61+
* This is used to prevent adjusted classes from being repeatedly analyzed which can lead
62+
* to an infinite loop.
63+
*/
64+
const visitedClasses = new WeakSet<types.Class>();
65+
66+
/**
67+
* A babel plugin factory function for adjusting classes; primarily with Angular metadata.
68+
* The adjustments include wrapping classes with known safe or no side effects with pure
69+
* annotations to support dead code removal of unused classes. Angular compiler generated
70+
* metadata static fields not required in AOT mode are also elided to better support bundler-
71+
* level treeshaking.
72+
*
73+
* @returns A babel plugin object instance.
74+
*/
75+
export default function (): PluginObj {
76+
return {
77+
visitor: {
78+
ClassDeclaration(path: NodePath<types.ClassDeclaration>, state: PluginPass) {
79+
const { node: classNode, parentPath } = path;
80+
const { wrapDecorators } = state.opts as { wrapDecorators: boolean };
81+
82+
if (visitedClasses.has(classNode)) {
83+
return;
84+
}
85+
86+
// Analyze sibling statements for elements of the class that were downleveled
87+
const hasExport =
88+
parentPath.isExportNamedDeclaration() || parentPath.isExportDefaultDeclaration();
89+
const origin = hasExport ? parentPath : path;
90+
const wrapStatementPaths: NodePath<types.Statement>[] = [];
91+
let skipWrapping = false;
92+
for (let i = 1; ; ++i) {
93+
const nextStatement = origin.getSibling(+origin.key + i);
94+
if (!nextStatement.isExpressionStatement()) {
95+
break;
96+
}
97+
98+
// Valid sibling statements for class declarations are only assignment expressions
99+
// and TypeScript decorator helper call expressions
100+
const nextExpression = nextStatement.get('expression');
101+
if (wrapDecorators && nextExpression.isCallExpression()) {
102+
if (
103+
!types.isIdentifier(nextExpression.node.callee) ||
104+
nextExpression.node.callee.name !== TSLIB_DECORATE_HELPER_NAME
105+
) {
106+
break;
107+
}
108+
wrapStatementPaths.push(nextStatement);
109+
110+
continue;
111+
} else if (!nextExpression.isAssignmentExpression()) {
112+
break;
113+
}
114+
115+
// Valid assignment expressions should be member access expressions using the class
116+
// name as the object and an identifier as the property.
117+
const left = nextExpression.get('left');
118+
if (
119+
!left.isMemberExpression() ||
120+
!types.isIdentifier(left.node.object) ||
121+
!left.scope.bindingIdentifierEquals(left.node.object.name, classNode.id) ||
122+
!types.isIdentifier(left.node.property)
123+
) {
124+
break;
125+
}
126+
127+
const propertyName = left.node.property.name;
128+
const assignmentValue = nextExpression.get('right');
129+
if (angularStaticsToElide[propertyName]?.(assignmentValue)) {
130+
nextStatement.remove();
131+
--i;
132+
} else if (angularStaticsToWrap.has(propertyName) || assignmentValue.isPure()) {
133+
wrapStatementPaths.push(nextStatement);
134+
} else {
135+
// Statement cannot be safely wrapped which makes wrapping the class unneeded.
136+
// The statement will prevent even a wrapped class from being optimized away.
137+
skipWrapping = true;
138+
}
139+
}
140+
141+
visitedClasses.add(classNode);
142+
143+
if (skipWrapping || wrapStatementPaths.length === 0) {
144+
return;
145+
}
146+
147+
const wrapStatementNodes: types.Statement[] = [];
148+
for (const statementPath of wrapStatementPaths) {
149+
wrapStatementNodes.push(statementPath.node);
150+
statementPath.remove();
151+
}
152+
153+
// Wrap class and safe static assignments in a pure annotated IIFE
154+
const container = types.arrowFunctionExpression(
155+
[],
156+
types.blockStatement([
157+
classNode,
158+
...wrapStatementNodes,
159+
types.returnStatement(types.cloneNode(classNode.id)),
160+
]),
161+
);
162+
const replacementInitializer = types.callExpression(
163+
types.parenthesizedExpression(container),
164+
[],
165+
);
166+
annotateAsPure(replacementInitializer);
167+
168+
// Replace class with IIFE wrapped class
169+
const declaration = types.variableDeclaration('let', [
170+
types.variableDeclarator(types.cloneNode(classNode.id), replacementInitializer),
171+
]);
172+
if (parentPath.isExportDefaultDeclaration()) {
173+
// When converted to a variable declaration, the default export must be moved
174+
// to a subsequent statement to prevent a JavaScript syntax error.
175+
parentPath.replaceWithMultiple([
176+
declaration,
177+
types.exportNamedDeclaration(undefined, [
178+
types.exportSpecifier(types.cloneNode(classNode.id), types.identifier('default')),
179+
]),
180+
]);
181+
} else {
182+
path.replaceWith(declaration);
183+
}
184+
},
185+
},
186+
};
187+
}

0 commit comments

Comments
 (0)