-
Notifications
You must be signed in to change notification settings - Fork 12k
/
Copy pathjavascript-optimizer-worker.ts
219 lines (200 loc) · 6.53 KB
/
javascript-optimizer-worker.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import remapping from '@ampproject/remapping';
import type { TransformResult } from 'esbuild';
import { minify } from 'terser';
import { EsbuildExecutor } from './esbuild-executor';
/**
* The options to use when optimizing.
*/
export interface OptimizeRequestOptions {
/**
* Controls advanced optimizations.
* Currently these are only terser related:
* * terser compress passes are set to 2
* * terser pure_getters option is enabled
*/
advanced?: boolean;
/**
* Specifies the string tokens that should be replaced with a defined value.
*/
define?: Record<string, string>;
/**
* Controls whether class, function, and variable names should be left intact
* throughout the output code.
*/
keepIdentifierNames: boolean;
/**
* Controls whether to retain the original name of classes and functions.
*/
keepNames: boolean;
/**
* Controls whether license text is removed from the output code.
* Within the CLI, this option is linked to the license extraction functionality.
*/
removeLicenses?: boolean;
/**
* Controls whether source maps should be generated.
*/
sourcemap?: boolean;
/**
* Specifies the list of supported esbuild targets.
* @see: https://esbuild.github.io/api/#target
*/
target?: string[];
/**
* Controls whether esbuild should only use the WASM-variant instead of trying to
* use the native variant. Some platforms may not support the native-variant and
* this option allows one support test to be conducted prior to all the workers starting.
*/
alwaysUseWasm: boolean;
}
/**
* A request to optimize JavaScript using the supplied options.
*/
interface OptimizeRequest {
/**
* The options to use when optimizing.
*/
options: OptimizeRequestOptions;
/**
* The JavaScript asset to optimize.
*/
asset: {
/**
* The name of the JavaScript asset (typically the filename).
*/
name: string;
/**
* The source content of the JavaScript asset.
*/
code: string;
/**
* The source map of the JavaScript asset, if available.
* This map is merged with all intermediate source maps during optimization.
*/
map: object;
};
}
/**
* The cached esbuild executor.
* This will automatically use the native or WASM version based on platform and availability
* with the native version given priority due to its superior performance.
*/
let esbuild: EsbuildExecutor | undefined;
/**
* Handles optimization requests sent from the main thread via the `JavaScriptOptimizerPlugin`.
*/
export default async function ({ asset, options }: OptimizeRequest) {
// esbuild is used as a first pass
const esbuildResult = await optimizeWithEsbuild(asset.code, asset.name, options);
// terser is used as a second pass
const terserResult = await optimizeWithTerser(
asset.name,
esbuildResult.code,
options.sourcemap,
options.advanced,
);
// Merge intermediate sourcemaps with input sourcemap if enabled
let fullSourcemap;
if (options.sourcemap) {
const partialSourcemaps = [];
if (esbuildResult.map) {
partialSourcemaps.unshift(JSON.parse(esbuildResult.map));
}
if (terserResult.map) {
partialSourcemaps.unshift(terserResult.map);
}
if (asset.map) {
partialSourcemaps.push(asset.map);
}
fullSourcemap = remapping(partialSourcemaps, () => null);
}
return { name: asset.name, code: terserResult.code, map: fullSourcemap };
}
/**
* Optimizes a JavaScript asset using esbuild.
*
* @param content The JavaScript asset source content to optimize.
* @param name The name of the JavaScript asset. Used to generate source maps.
* @param options The optimization request options to apply to the content.
* @returns A promise that resolves with the optimized code, source map, and any warnings.
*/
async function optimizeWithEsbuild(
content: string,
name: string,
options: OptimizeRequest['options'],
): Promise<TransformResult> {
if (!esbuild) {
esbuild = new EsbuildExecutor(options.alwaysUseWasm);
}
return esbuild.transform(content, {
minifyIdentifiers: !options.keepIdentifierNames,
minifySyntax: true,
// NOTE: Disabling whitespace ensures unused pure annotations are kept
minifyWhitespace: false,
pure: ['forwardRef'],
legalComments: options.removeLicenses ? 'none' : 'inline',
sourcefile: name,
sourcemap: options.sourcemap && 'external',
define: options.define,
// This option should always be disabled for browser builds as we don't rely on `.name`
// and causes deadcode to be retained which makes `NG_BUILD_MANGLE` unusable to investigate tree-shaking issues.
// We enable `keepNames` only for server builds as Domino relies on `.name`.
// Once we no longer rely on Domino for SSR we should be able to remove this.
keepNames: options.keepNames,
target: options.target,
});
}
/**
* Optimizes a JavaScript asset using terser.
*
* @param name The name of the JavaScript asset. Used to generate source maps.
* @param code The JavaScript asset source content to optimize.
* @param sourcemaps If true, generate an output source map for the optimized code.
* @param advanced Controls advanced optimizations.
* @returns A promise that resolves with the optimized code and source map.
*/
async function optimizeWithTerser(
name: string,
code: string,
sourcemaps: boolean | undefined,
advanced: boolean | undefined,
): Promise<{ code: string; map?: object }> {
const result = await minify(
{ [name]: code },
{
compress: {
passes: advanced ? 2 : 1,
pure_getters: advanced,
},
// terser only supports up to ES2020
ecma: 2020,
// esbuild in the first pass is used to minify identifiers instead of mangle here
mangle: false,
// esbuild in the first pass is used to minify function names
keep_fnames: true,
format: {
// ASCII output is enabled here as well to prevent terser from converting back to UTF-8
ascii_only: true,
wrap_func_args: false,
},
sourceMap:
sourcemaps &&
({
asObject: true,
// typings don't include asObject option
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any),
},
);
if (!result.code) {
throw new Error('Terser failed for unknown reason.');
}
return { code: result.code, map: result.map as object };
}