Skip to content

Commit e510ff8

Browse files
committed
fix(@angular/build): mark InjectionToken as pure for improved tree-shaking
`new InjectionToken(...)` is not considered pure by default by build tools, which prevents it from being tree-shaken when unused. This can lead to unused services being included in production bundles if they are provided as a default value for a token. This change marks `new InjectionToken(...)` calls as pure. The `InjectionToken` constructor is side-effect free; its only purpose is to create a token for the dependency injection system. Marking it as pure is safe and allows build tools like esbuild to tree-shake unused tokens and their dependencies. Closes #31270 (cherry picked from commit 6556211)
1 parent 2b45442 commit e510ff8

File tree

3 files changed

+74
-19
lines changed

3 files changed

+74
-19
lines changed

packages/angular/build/src/tools/babel/plugins/pure-toplevel-functions.ts

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,17 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import type { PluginObj } from '@babel/core';
9+
import type { NodePath, PluginObj, PluginPass, types } from '@babel/core';
1010
import annotateAsPure from '@babel/helper-annotate-as-pure';
1111
import * as tslib from 'tslib';
1212

1313
/**
14-
* A cached set of TypeScript helper function names used by the helper name matcher utility function.
14+
* A set of constructor names that are considered to be side-effect free.
15+
*/
16+
const sideEffectFreeConstructors = new Set<string>(['InjectionToken']);
17+
18+
/**
19+
* A set of TypeScript helper function names used by the helper name matcher utility function.
1520
*/
1621
const tslibHelpers = new Set<string>(Object.keys(tslib).filter((h) => h.startsWith('__')));
1722

@@ -44,15 +49,23 @@ function isBabelHelperName(name: string): boolean {
4449
return babelHelpers.has(name);
4550
}
4651

52+
interface ExtendedPluginPass extends PluginPass {
53+
opts: { topLevelSafeMode?: boolean };
54+
}
55+
4756
/**
4857
* A babel plugin factory function for adding the PURE annotation to top-level new and call expressions.
49-
*
5058
* @returns A babel plugin object instance.
5159
*/
5260
export default function (): PluginObj {
5361
return {
5462
visitor: {
55-
CallExpression(path) {
63+
CallExpression(path: NodePath<types.CallExpression>, state: ExtendedPluginPass) {
64+
const { topLevelSafeMode = false } = state.opts;
65+
if (topLevelSafeMode) {
66+
return;
67+
}
68+
5669
// If the expression has a function parent, it is not top-level
5770
if (path.getFunctionParent()) {
5871
return;
@@ -65,6 +78,7 @@ export default function (): PluginObj {
6578
) {
6679
return;
6780
}
81+
6882
// Do not annotate TypeScript helpers emitted by the TypeScript compiler or Babel helpers.
6983
// They are intended to cause side effects.
7084
if (
@@ -76,9 +90,22 @@ export default function (): PluginObj {
7690

7791
annotateAsPure(path);
7892
},
79-
NewExpression(path) {
93+
NewExpression(path: NodePath<types.NewExpression>, state: ExtendedPluginPass) {
8094
// If the expression has a function parent, it is not top-level
81-
if (!path.getFunctionParent()) {
95+
if (path.getFunctionParent()) {
96+
return;
97+
}
98+
99+
const { topLevelSafeMode = false } = state.opts;
100+
101+
if (!topLevelSafeMode) {
102+
annotateAsPure(path);
103+
104+
return;
105+
}
106+
107+
const callee = path.get('callee');
108+
if (callee.isIdentifier() && sideEffectFreeConstructors.has(callee.node.name)) {
82109
annotateAsPure(path);
83110
}
84111
},

packages/angular/build/src/tools/babel/plugins/pure-toplevel-functions_spec.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,24 @@
77
*/
88

99
import { transformSync } from '@babel/core';
10-
// eslint-disable-next-line import/no-extraneous-dependencies
1110
import { format } from 'prettier';
1211
import pureTopLevelPlugin from './pure-toplevel-functions';
1312

1413
function testCase({
1514
input,
1615
expected,
16+
options,
1717
}: {
1818
input: string;
1919
expected: string;
20+
options?: { topLevelSafeMode: boolean };
2021
}): jasmine.ImplementationCallback {
2122
return async () => {
2223
const result = transformSync(input, {
2324
configFile: false,
2425
babelrc: false,
2526
compact: true,
26-
plugins: [pureTopLevelPlugin],
27+
plugins: [[pureTopLevelPlugin, options]],
2728
});
2829
if (!result?.code) {
2930
fail('Expected babel to return a transform result.');
@@ -152,4 +153,33 @@ describe('pure-toplevel-functions Babel plugin', () => {
152153
};
153154
`),
154155
);
156+
157+
describe('topLevelSafeMode: true', () => {
158+
it(
159+
'annotates top-level `new InjectionToken` expressions',
160+
testCase({
161+
input: `const result = new InjectionToken('abc');`,
162+
expected: `const result = /*#__PURE__*/ new InjectionToken('abc');`,
163+
options: { topLevelSafeMode: true },
164+
}),
165+
);
166+
167+
it(
168+
'does not annotate other top-level `new` expressions',
169+
testCase({
170+
input: 'const result = new SomeClass();',
171+
expected: 'const result = new SomeClass();',
172+
options: { topLevelSafeMode: true },
173+
}),
174+
);
175+
176+
it(
177+
'does not annotate top-level function calls',
178+
testCase({
179+
input: 'const result = someCall();',
180+
expected: 'const result = someCall();',
181+
options: { topLevelSafeMode: true },
182+
}),
183+
);
184+
});
155185
});

packages/angular/build/src/tools/esbuild/javascript-transformer-worker.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -78,21 +78,19 @@ async function transformWithBabel(
7878
}
7979

8080
if (options.advancedOptimizations) {
81-
const sideEffectFree = options.sideEffects === false;
82-
const safeAngularPackage =
83-
sideEffectFree && /[\\/]node_modules[\\/]@angular[\\/]/.test(filename);
84-
8581
const { adjustStaticMembers, adjustTypeScriptEnums, elideAngularMetadata, markTopLevelPure } =
8682
await import('../babel/plugins');
8783

88-
if (safeAngularPackage) {
89-
plugins.push(markTopLevelPure);
90-
}
84+
const sideEffectFree = options.sideEffects === false;
85+
const safeAngularPackage =
86+
sideEffectFree && /[\\/]node_modules[\\/]@angular[\\/]/.test(filename);
9187

92-
plugins.push(elideAngularMetadata, adjustTypeScriptEnums, [
93-
adjustStaticMembers,
94-
{ wrapDecorators: sideEffectFree },
95-
]);
88+
plugins.push(
89+
[markTopLevelPure, { topLevelSafeMode: !safeAngularPackage }],
90+
elideAngularMetadata,
91+
adjustTypeScriptEnums,
92+
[adjustStaticMembers, { wrapDecorators: sideEffectFree }],
93+
);
9694
}
9795

9896
// If no additional transformations are needed, return the data directly

0 commit comments

Comments
 (0)