Skip to content

Commit 188a00f

Browse files
crisbetodgp1130
authored andcommitted
fix(@angular-devkit/build-angular): elide setClassMetadataAsync calls
Updates the logic that elides `setClassMetadata` calls to also elide `setClassMetadataAsync`. The latter will be emitted when the component uses the new `defer` block syntax.
1 parent 4fe0326 commit 188a00f

File tree

2 files changed

+137
-20
lines changed

2 files changed

+137
-20
lines changed

packages/angular_devkit/build_angular/src/tools/babel/plugins/elide-angular-metadata.ts

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,19 @@ import { NodePath, PluginObj, types } from '@babel/core';
1313
*/
1414
const SET_CLASS_METADATA_NAME = 'ɵsetClassMetadata';
1515

16+
/**
17+
* Name of the asynchronous Angular class metadata function created by the Angular compiler.
18+
*/
19+
const SET_CLASS_METADATA_ASYNC_NAME = 'ɵsetClassMetadataAsync';
20+
1621
/**
1722
* Provides one or more keywords that if found within the content of a source file indicate
1823
* that this plugin should be used with a source file.
1924
*
2025
* @returns An a string iterable containing one or more keywords.
2126
*/
2227
export function getKeywords(): Iterable<string> {
23-
return [SET_CLASS_METADATA_NAME];
28+
return [SET_CLASS_METADATA_NAME, SET_CLASS_METADATA_ASYNC_NAME];
2429
}
2530

2631
/**
@@ -33,6 +38,7 @@ export default function (): PluginObj {
3338
visitor: {
3439
CallExpression(path: NodePath<types.CallExpression>) {
3540
const callee = path.node.callee;
41+
const callArguments = path.node.arguments;
3642

3743
// The function being called must be the metadata function name
3844
let calleeName;
@@ -41,31 +47,58 @@ export default function (): PluginObj {
4147
} else if (types.isIdentifier(callee)) {
4248
calleeName = callee.name;
4349
}
44-
if (calleeName !== SET_CLASS_METADATA_NAME) {
45-
return;
46-
}
4750

48-
// There must be four arguments that meet the following criteria:
49-
// * First must be an identifier
50-
// * Second must be an array literal
51-
const callArguments = path.node.arguments;
5251
if (
53-
callArguments.length !== 4 ||
54-
!types.isIdentifier(callArguments[0]) ||
55-
!types.isArrayExpression(callArguments[1])
52+
calleeName !== undefined &&
53+
(isRemoveClassMetadataCall(calleeName, callArguments) ||
54+
isRemoveClassmetadataAsyncCall(calleeName, callArguments))
5655
) {
57-
return;
58-
}
56+
// The metadata function is always emitted inside a function expression
57+
const parent = path.getFunctionParent();
5958

60-
// The metadata function is always emitted inside a function expression
61-
const parent = path.getFunctionParent();
62-
63-
if (parent && (parent.isFunctionExpression() || parent.isArrowFunctionExpression())) {
64-
// Replace the metadata function with `void 0` which is the equivalent return value
65-
// of the metadata function.
66-
path.replaceWith(path.scope.buildUndefinedNode());
59+
if (parent && (parent.isFunctionExpression() || parent.isArrowFunctionExpression())) {
60+
// Replace the metadata function with `void 0` which is the equivalent return value
61+
// of the metadata function.
62+
path.replaceWith(path.scope.buildUndefinedNode());
63+
}
6764
}
6865
},
6966
},
7067
};
7168
}
69+
70+
/** Determines if a function call is a call to `setClassMetadata`. */
71+
function isRemoveClassMetadataCall(name: string, args: types.CallExpression['arguments']): boolean {
72+
// `setClassMetadata` calls have to meet the following criteria:
73+
// * First must be an identifier
74+
// * Second must be an array literal
75+
return (
76+
name === SET_CLASS_METADATA_NAME &&
77+
args.length === 4 &&
78+
types.isIdentifier(args[0]) &&
79+
types.isArrayExpression(args[1])
80+
);
81+
}
82+
83+
/** Determines if a function call is a call to `setClassMetadataAsync`. */
84+
function isRemoveClassmetadataAsyncCall(
85+
name: string,
86+
args: types.CallExpression['arguments'],
87+
): boolean {
88+
// `setClassMetadataAsync` calls have to meet the following criteria:
89+
// * First argument must be an identifier.
90+
// * Second argument must be an inline function.
91+
// * Third argument must be an inline function.
92+
return (
93+
name === SET_CLASS_METADATA_ASYNC_NAME &&
94+
args.length === 3 &&
95+
types.isIdentifier(args[0]) &&
96+
isInlineFunction(args[1]) &&
97+
isInlineFunction(args[2])
98+
);
99+
}
100+
101+
/** Determines if a node is an inline function expression. */
102+
function isInlineFunction(node: types.Node): boolean {
103+
return types.isFunctionExpression(node) || types.isArrowFunctionExpression(node);
104+
}

packages/angular_devkit/build_angular/src/tools/babel/plugins/elide-angular-metadata_spec.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,88 @@ describe('elide-angular-metadata Babel plugin', () => {
102102
`,
103103
}),
104104
);
105+
106+
it(
107+
'elides pure annotated ɵsetClassMetadataAsync',
108+
testCase({
109+
input: `
110+
import { Component } from '@angular/core';
111+
export class SomeClass {}
112+
/*@__PURE__*/ (function () {
113+
i0.ɵsetClassMetadataAsync(SomeClass,
114+
function () { return [import("./cmp-a").then(function (m) { return m.CmpA; })]; },
115+
function (CmpA) { i0.ɵsetClassMetadata(SomeClass, [{
116+
type: Component,
117+
args: [{
118+
selector: 'test-cmp',
119+
standalone: true,
120+
imports: [CmpA, LocalDep],
121+
template: '{#defer}<cmp-a/>{/defer}',
122+
}]
123+
}], null, null); });
124+
})();
125+
`,
126+
expected: `
127+
import { Component } from '@angular/core';
128+
export class SomeClass {}
129+
/*@__PURE__*/ (function () { void 0 })();
130+
`,
131+
}),
132+
);
133+
134+
it(
135+
'elides JIT mode protected ɵsetClassMetadataAsync',
136+
testCase({
137+
input: `
138+
import { Component } from '@angular/core';
139+
export class SomeClass {}
140+
(function () {
141+
(typeof ngJitMode === "undefined" || ngJitMode) && i0.ɵsetClassMetadataAsync(SomeClass,
142+
function () { return [import("./cmp-a").then(function (m) { return m.CmpA; })]; },
143+
function (CmpA) { i0.ɵsetClassMetadata(SomeClass, [{
144+
type: Component,
145+
args: [{
146+
selector: 'test-cmp',
147+
standalone: true,
148+
imports: [CmpA, LocalDep],
149+
template: '{#defer}<cmp-a/>{/defer}',
150+
}]
151+
}], null, null); });
152+
})();
153+
`,
154+
expected: `
155+
import { Component } from '@angular/core';
156+
export class SomeClass {}
157+
(function () { (typeof ngJitMode === "undefined" || ngJitMode) && void 0 })();
158+
`,
159+
}),
160+
);
161+
162+
it(
163+
'elides arrow-function-based ɵsetClassMetadataAsync',
164+
testCase({
165+
input: `
166+
import { Component } from '@angular/core';
167+
export class SomeClass {}
168+
/*@__PURE__*/ (() => {
169+
i0.ɵsetClassMetadataAsync(SomeClass,
170+
() => [import("./cmp-a").then(m => m.CmpA)],
171+
(CmpA) => { i0.ɵsetClassMetadata(SomeClass, [{
172+
type: Component,
173+
args: [{
174+
selector: 'test-cmp',
175+
standalone: true,
176+
imports: [CmpA, LocalDep],
177+
template: '{#defer}<cmp-a/>{/defer}',
178+
}]
179+
}], null, null); });
180+
})();
181+
`,
182+
expected: `
183+
import { Component } from '@angular/core';
184+
export class SomeClass {}
185+
/*@__PURE__*/ (() => { void 0 })();
186+
`,
187+
}),
188+
);
105189
});

0 commit comments

Comments
 (0)