Skip to content

Commit 39c95da

Browse files
committed
refactor(@angular-devkit/build-angular): ability to perform DCE but retain symbol names
Previously, we enabled the `keepNames` esbuild when mangling was disabled, this caused dead code to be retained because of the transformations that esbuild did to the input. Input ```js class foo {} ``` Output ```js var l = Object.defineProperty, a = (s, c) => l(s, "name", { value: c, configurable: !0 }); class foo {} a(foo, "foo"); ``` Previously we enabled the `keepNames` esbuild option when mangling was disabled, which is actually not needed to retain the name of symbols but is needed for SSR because Domino relies on the `name` property on functions and classes. Closes #22354 (cherry picked from commit 2c9a33d)
1 parent 4c9d72c commit 39c95da

File tree

3 files changed

+79
-49
lines changed

3 files changed

+79
-49
lines changed

packages/angular_devkit/build_angular/src/webpack/configs/common.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,8 @@ export async function getCommonConfig(wco: WebpackConfigOptions): Promise<Config
277277
define: buildOptions.aot ? GLOBAL_DEFS_FOR_TERSER_WITH_AOT : GLOBAL_DEFS_FOR_TERSER,
278278
sourcemap: scriptsSourceMap,
279279
target: scriptTarget,
280-
keepNames: !allowMangle || isPlatformServer,
280+
keepIdentifierNames: !allowMangle || isPlatformServer,
281+
keepNames: isPlatformServer,
281282
removeLicenses: buildOptions.extractLicenses,
282283
advanced: buildOptions.buildOptimizer,
283284
}),

packages/angular_devkit/build_angular/src/webpack/plugins/javascript-optimizer-plugin.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ScriptTarget } from 'typescript';
1111
import type { Compiler, sources } from 'webpack';
1212
import { maxWorkers } from '../../utils/environment-options';
1313
import { EsbuildExecutor } from './esbuild-executor';
14+
import type { OptimizeRequestOptions } from './javascript-optimizer-worker';
1415

1516
/**
1617
* The maximum number of Workers that will be created to execute optimize tasks.
@@ -28,10 +29,10 @@ const PLUGIN_NAME = 'angular-javascript-optimizer';
2829
export interface JavaScriptOptimizerOptions {
2930
/**
3031
* Enables advanced optimizations in the underlying JavaScript optimizers.
31-
* This currently increases the `terser` passes to 3 and enables the `pure_getters`
32+
* This currently increases the `terser` passes to 2 and enables the `pure_getters`
3233
* option for `terser`.
3334
*/
34-
advanced: boolean;
35+
advanced?: boolean;
3536

3637
/**
3738
* An object record of string keys that will be replaced with their respective values when found
@@ -44,7 +45,7 @@ export interface JavaScriptOptimizerOptions {
4445
* The output sourcemap will be a full sourcemap containing the merge of the input sourcemap and
4546
* all intermediate sourcemaps.
4647
*/
47-
sourcemap: boolean;
48+
sourcemap?: boolean;
4849

4950
/**
5051
* The ECMAScript version that should be used when generating output code.
@@ -56,13 +57,22 @@ export interface JavaScriptOptimizerOptions {
5657
/**
5758
* Enables the retention of identifier names and ensures that function and class names are
5859
* present in the output code.
60+
*
61+
* **Note**: in some cases symbols are still renamed to avoid collisions.
62+
*/
63+
keepIdentifierNames: boolean;
64+
65+
/**
66+
* Enables the retention of original name of classes and functions.
67+
*
68+
* **Note**: this causes increase of bundle size as it causes dead-code elimination to not work fully.
5969
*/
6070
keepNames: boolean;
6171

6272
/**
6373
* Enables the removal of all license comments from the output code.
6474
*/
65-
removeLicenses: boolean;
75+
removeLicenses?: boolean;
6676
}
6777

6878
/**
@@ -74,7 +84,7 @@ export interface JavaScriptOptimizerOptions {
7484
* optimizations not yet implemented by `esbuild`.
7585
*/
7686
export class JavaScriptOptimizerPlugin {
77-
constructor(public options: Partial<JavaScriptOptimizerOptions> = {}) {}
87+
constructor(public options: JavaScriptOptimizerOptions) {}
7888

7989
apply(compiler: Compiler) {
8090
const { OriginalSource, SourceMapSource } = compiler.webpack.sources;
@@ -142,22 +152,25 @@ export class JavaScriptOptimizerPlugin {
142152
}
143153
}
144154

145-
let target = 2017;
155+
let target: OptimizeRequestOptions['target'] = 2017;
146156
if (this.options.target) {
147157
if (this.options.target <= ScriptTarget.ES5) {
148158
target = 5;
149159
} else if (this.options.target < ScriptTarget.ESNext) {
150-
target = Number(ScriptTarget[this.options.target].slice(2));
160+
target = Number(
161+
ScriptTarget[this.options.target].slice(2),
162+
) as OptimizeRequestOptions['target'];
151163
} else {
152164
target = 2020;
153165
}
154166
}
155167

156168
// Setup the options used by all worker tasks
157-
const optimizeOptions = {
169+
const optimizeOptions: OptimizeRequestOptions = {
158170
sourcemap: this.options.sourcemap,
159171
define,
160172
keepNames: this.options.keepNames,
173+
keepIdentifierNames: this.options.keepIdentifierNames,
161174
target,
162175
removeLicenses: this.options.removeLicenses,
163176
advanced: this.options.advanced,

packages/angular_devkit/build_angular/src/webpack/plugins/javascript-optimizer-worker.ts

Lines changed: 56 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,50 +11,60 @@ import type { TransformFailure, TransformResult } from 'esbuild';
1111
import { minify } from 'terser';
1212
import { EsbuildExecutor } from './esbuild-executor';
1313

14+
/**
15+
* The options to use when optimizing.
16+
*/
17+
export interface OptimizeRequestOptions {
18+
/**
19+
* Controls advanced optimizations.
20+
* Currently these are only terser related:
21+
* * terser compress passes are set to 2
22+
* * terser pure_getters option is enabled
23+
*/
24+
advanced?: boolean;
25+
/**
26+
* Specifies the string tokens that should be replaced with a defined value.
27+
*/
28+
define?: Record<string, string>;
29+
/**
30+
* Controls whether class, function, and variable names should be left intact
31+
* throughout the output code.
32+
*/
33+
keepIdentifierNames: boolean;
34+
35+
/**
36+
* Controls whether to retain the original name of classes and functions.
37+
*/
38+
keepNames: boolean;
39+
/**
40+
* Controls whether license text is removed from the output code.
41+
* Within the CLI, this option is linked to the license extraction functionality.
42+
*/
43+
removeLicenses?: boolean;
44+
/**
45+
* Controls whether source maps should be generated.
46+
*/
47+
sourcemap?: boolean;
48+
/**
49+
* Specifies the target ECMAScript version for the output code.
50+
*/
51+
target: 5 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020;
52+
/**
53+
* Controls whether esbuild should only use the WASM-variant instead of trying to
54+
* use the native variant. Some platforms may not support the native-variant and
55+
* this option allows one support test to be conducted prior to all the workers starting.
56+
*/
57+
alwaysUseWasm: boolean;
58+
}
59+
1460
/**
1561
* A request to optimize JavaScript using the supplied options.
1662
*/
1763
interface OptimizeRequest {
1864
/**
1965
* The options to use when optimizing.
2066
*/
21-
options: {
22-
/**
23-
* Controls advanced optimizations.
24-
* Currently these are only terser related:
25-
* * terser compress passes are set to 2
26-
* * terser pure_getters option is enabled
27-
*/
28-
advanced: boolean;
29-
/**
30-
* Specifies the string tokens that should be replaced with a defined value.
31-
*/
32-
define?: Record<string, string>;
33-
/**
34-
* Controls whether class, function, and variable names should be left intact
35-
* throughout the output code.
36-
*/
37-
keepNames: boolean;
38-
/**
39-
* Controls whether license text is removed from the output code.
40-
* Within the CLI, this option is linked to the license extraction functionality.
41-
*/
42-
removeLicenses: boolean;
43-
/**
44-
* Controls whether source maps should be generated.
45-
*/
46-
sourcemap: boolean;
47-
/**
48-
* Specifies the target ECMAScript version for the output code.
49-
*/
50-
target: 5 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020;
51-
/**
52-
* Controls whether esbuild should only use the WASM-variant instead of trying to
53-
* use the native variant. Some platforms may not support the native-variant and
54-
* this option allows one support test to be conducted prior to all the workers starting.
55-
*/
56-
alwaysUseWasm: boolean;
57-
};
67+
options: OptimizeRequestOptions;
5868

5969
/**
6070
* The JavaScript asset to optimize.
@@ -142,7 +152,7 @@ async function optimizeWithEsbuild(
142152
let result: TransformResult;
143153
try {
144154
result = await esbuild.transform(content, {
145-
minifyIdentifiers: !options.keepNames,
155+
minifyIdentifiers: !options.keepIdentifierNames,
146156
minifySyntax: true,
147157
// NOTE: Disabling whitespace ensures unused pure annotations are kept
148158
minifyWhitespace: false,
@@ -151,6 +161,10 @@ async function optimizeWithEsbuild(
151161
sourcefile: name,
152162
sourcemap: options.sourcemap && 'external',
153163
define: options.define,
164+
// This option should always be disabled for browser builds as we don't rely on `.name`
165+
// and causes deadcode to be retained which makes `NG_BUILD_MANGLE` unusable to investigate tree-shaking issues.
166+
// We enable `keepNames` only for server builds as Domino relies on `.name`.
167+
// Once we no longer rely on Domino for SSR we should be able to remove this.
154168
keepNames: options.keepNames,
155169
target: `es${options.target}`,
156170
});
@@ -193,9 +207,9 @@ async function optimizeWithEsbuild(
193207
async function optimizeWithTerser(
194208
name: string,
195209
code: string,
196-
sourcemaps: boolean,
210+
sourcemaps: boolean | undefined,
197211
target: OptimizeRequest['options']['target'],
198-
advanced: boolean,
212+
advanced: boolean | undefined,
199213
): Promise<{ code: string; map?: object }> {
200214
const result = await minify(
201215
{ [name]: code },
@@ -207,6 +221,8 @@ async function optimizeWithTerser(
207221
ecma: target,
208222
// esbuild in the first pass is used to minify identifiers instead of mangle here
209223
mangle: false,
224+
// esbuild in the first pass is used to minify function names
225+
keep_fnames: true,
210226
format: {
211227
// ASCII output is enabled here as well to prevent terser from converting back to UTF-8
212228
ascii_only: true,

0 commit comments

Comments
 (0)