Skip to content

Commit 8356240

Browse files
clydinangular-robot[bot]
authored andcommitted
fix(@angular-devkit/build-angular): use babel default export helper in build optimizer
Within the build optimizer's static member optimization pass, a class that is directly default exported must be split into two statements: the class declaration and the default export. This is because the pass can wrap classes in a pure annotated IIFE which results in a variable declaration replacement and variable declarations can not be directly default exported. Previously, the pass did this splitting manually but this was causing later babel plugins to fail. In addition to updating the AST in this case, scoping information also needed to be updated. To support this, a babel helper package is now used that handles the details of the statement split operation.
1 parent c65b026 commit 8356240

File tree

7 files changed

+64
-29
lines changed

7 files changed

+64
-29
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
"@babel/core": "7.20.12",
8585
"@babel/generator": "7.20.14",
8686
"@babel/helper-annotate-as-pure": "7.18.6",
87+
"@babel/helper-split-export-declaration": "7.18.6",
8788
"@babel/plugin-proposal-async-generator-functions": "7.20.7",
8889
"@babel/plugin-transform-async-to-generator": "7.20.7",
8990
"@babel/plugin-transform-runtime": "7.19.6",

packages/angular_devkit/build_angular/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ ts_library(
111111
"@npm//@babel/core",
112112
"@npm//@babel/generator",
113113
"@npm//@babel/helper-annotate-as-pure",
114+
"@npm//@babel/helper-split-export-declaration",
114115
"@npm//@babel/plugin-proposal-async-generator-functions",
115116
"@npm//@babel/plugin-transform-async-to-generator",
116117
"@npm//@babel/plugin-transform-runtime",

packages/angular_devkit/build_angular/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"@babel/core": "7.20.12",
1414
"@babel/generator": "7.20.14",
1515
"@babel/helper-annotate-as-pure": "7.18.6",
16+
"@babel/helper-split-export-declaration": "7.18.6",
1617
"@babel/plugin-proposal-async-generator-functions": "7.20.7",
1718
"@babel/plugin-transform-async-to-generator": "7.20.7",
1819
"@babel/plugin-transform-runtime": "7.19.6",

packages/angular_devkit/build_angular/src/babel/plugins/adjust-static-class-members.ts

+32-21
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import { NodePath, PluginObj, PluginPass, types } from '@babel/core';
1010
import annotateAsPure from '@babel/helper-annotate-as-pure';
11+
import splitExportDeclaration from '@babel/helper-split-export-declaration';
1112

1213
/**
1314
* The name of the Typescript decorator helper function created by the TypeScript compiler.
@@ -183,12 +184,18 @@ function analyzeClassSiblings(
183184
}
184185

185186
/**
186-
* The set of classed already visited and analyzed during the plugin's execution.
187+
* The set of classes already visited and analyzed during the plugin's execution.
187188
* This is used to prevent adjusted classes from being repeatedly analyzed which can lead
188189
* to an infinite loop.
189190
*/
190191
const visitedClasses = new WeakSet<types.Class>();
191192

193+
/**
194+
* A map of classes that have already been analyzed during the default export splitting step.
195+
* This is used to avoid analyzing a class declaration twice if it is a direct default export.
196+
*/
197+
const exportDefaultAnalysis = new WeakMap<types.Class, ReturnType<typeof analyzeClassSiblings>>();
198+
192199
/**
193200
* A babel plugin factory function for adjusting classes; primarily with Angular metadata.
194201
* The adjustments include wrapping classes with known safe or no side effects with pure
@@ -201,6 +208,25 @@ const visitedClasses = new WeakSet<types.Class>();
201208
export default function (): PluginObj {
202209
return {
203210
visitor: {
211+
// When a class is converted to a variable declaration, the default export must be moved
212+
// to a subsequent statement to prevent a JavaScript syntax error.
213+
ExportDefaultDeclaration(path: NodePath<types.ExportDefaultDeclaration>, state: PluginPass) {
214+
const declaration = path.get('declaration');
215+
if (!declaration.isClassDeclaration()) {
216+
return;
217+
}
218+
219+
const { wrapDecorators } = state.opts as { wrapDecorators: boolean };
220+
const analysis = analyzeClassSiblings(path, declaration.node.id, wrapDecorators);
221+
exportDefaultAnalysis.set(declaration.node, analysis);
222+
223+
// Splitting the export declaration is not needed if the class will not be wrapped
224+
if (analysis.hasPotentialSideEffects) {
225+
return;
226+
}
227+
228+
splitExportDeclaration(path);
229+
},
204230
ClassDeclaration(path: NodePath<types.ClassDeclaration>, state: PluginPass) {
205231
const { node: classNode, parentPath } = path;
206232
const { wrapDecorators } = state.opts as { wrapDecorators: boolean };
@@ -210,14 +236,10 @@ export default function (): PluginObj {
210236
}
211237

212238
// Analyze sibling statements for elements of the class that were downleveled
213-
const hasExport =
214-
parentPath.isExportNamedDeclaration() || parentPath.isExportDefaultDeclaration();
215-
const origin = hasExport ? parentPath : path;
216-
const { wrapStatementPaths, hasPotentialSideEffects } = analyzeClassSiblings(
217-
origin,
218-
classNode.id,
219-
wrapDecorators,
220-
);
239+
const origin = parentPath.isExportNamedDeclaration() ? parentPath : path;
240+
const { wrapStatementPaths, hasPotentialSideEffects } =
241+
exportDefaultAnalysis.get(classNode) ??
242+
analyzeClassSiblings(origin, classNode.id, wrapDecorators);
221243

222244
visitedClasses.add(classNode);
223245

@@ -288,18 +310,7 @@ export default function (): PluginObj {
288310
const declaration = types.variableDeclaration('let', [
289311
types.variableDeclarator(types.cloneNode(classNode.id), replacementInitializer),
290312
]);
291-
if (parentPath.isExportDefaultDeclaration()) {
292-
// When converted to a variable declaration, the default export must be moved
293-
// to a subsequent statement to prevent a JavaScript syntax error.
294-
parentPath.replaceWithMultiple([
295-
declaration,
296-
types.exportNamedDeclaration(undefined, [
297-
types.exportSpecifier(types.cloneNode(classNode.id), types.identifier('default')),
298-
]),
299-
]);
300-
} else {
301-
path.replaceWith(declaration);
302-
}
313+
path.replaceWith(declaration);
303314
},
304315
ClassExpression(path: NodePath<types.ClassExpression>, state: PluginPass) {
305316
const { node: classNode, parentPath } = path;

packages/angular_devkit/build_angular/src/babel/plugins/adjust-static-class-members_spec.ts

+20-7
Original file line numberDiff line numberDiff line change
@@ -169,14 +169,27 @@ describe('adjust-static-class-members Babel plugin', () => {
169169
});
170170

171171
it('does not wrap default exported class with no connected siblings', () => {
172-
testCaseNoChange(`
173-
export default class CustomComponentEffects {
174-
constructor(_actions) {
175-
this._actions = _actions;
176-
this.doThis = this._actions;
172+
// NOTE: This could technically have no changes but the default export splitting detection
173+
// does not perform class property analysis currently.
174+
testCase({
175+
input: `
176+
export default class CustomComponentEffects {
177+
constructor(_actions) {
178+
this._actions = _actions;
179+
this.doThis = this._actions;
180+
}
177181
}
178-
}
179-
`);
182+
`,
183+
expected: `
184+
class CustomComponentEffects {
185+
constructor(_actions) {
186+
this._actions = _actions;
187+
this.doThis = this._actions;
188+
}
189+
}
190+
export { CustomComponentEffects as default };
191+
`,
192+
});
180193
});
181194

182195
it('does wrap not default exported class with only side effect fields', () => {

packages/angular_devkit/build_angular/src/typings.d.ts

+8
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,11 @@ declare module '@babel/helper-annotate-as-pure' {
1111
pathOrNode: import('@babel/types').Node | { node: import('@babel/types').Node },
1212
): void;
1313
}
14+
15+
declare module '@babel/helper-split-export-declaration' {
16+
export default function splitExportDeclaration(
17+
exportDeclaration: import('@babel/traverse').NodePath<
18+
import('@babel/types').ExportDefaultDeclaration
19+
>,
20+
): void;
21+
}

yarn.lock

+1-1
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,7 @@
581581
dependencies:
582582
"@babel/types" "^7.20.0"
583583

584-
"@babel/helper-split-export-declaration@^7.18.6":
584+
"@babel/helper-split-export-declaration@7.18.6", "@babel/helper-split-export-declaration@^7.18.6":
585585
version "7.18.6"
586586
resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075"
587587
integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==

0 commit comments

Comments
 (0)