-
Notifications
You must be signed in to change notification settings - Fork 12k
/
Copy pathjavascript-optimizer-worker.ts
146 lines (131 loc) · 3.92 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
/**
* @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 { TransformFailure, transform } from 'esbuild';
import { minify } from 'terser';
/**
* A request to optimize JavaScript using the supplied options.
*/
interface OptimizeRequest {
/**
* The options to use when optimizing.
*/
options: {
advanced: boolean;
define?: Record<string, string>;
keepNames: boolean;
removeLicenses: boolean;
sourcemap: boolean;
target: 5 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020;
};
/**
* The JavaScript asset to optimize.
*/
asset: {
name: string;
code: string;
map: object;
};
}
export default async function ({ asset, options }: OptimizeRequest) {
// esbuild is used as a first pass
let esbuildResult;
try {
esbuildResult = await transform(asset.code, {
minifyIdentifiers: !options.keepNames,
minifySyntax: true,
// NOTE: Disabling whitespace ensures unused pure annotations are kept
minifyWhitespace: false,
pure: ['forwardRef'],
legalComments: options.removeLicenses ? 'none' : 'inline',
sourcefile: asset.name,
sourcemap: options.sourcemap && 'external',
define: options.define,
keepNames: options.keepNames,
target: `es${options.target}`,
});
} catch (error) {
const failure = error as TransformFailure;
// If esbuild fails with only ES5 support errors, fallback to just terser.
// This will only happen if ES5 is the output target and a global script contains ES2015+ syntax.
// In that case, the global script is technically already invalid for the target environment but
// this is and has been considered a configuration issue. Global scripts must be compatible with
// the target environment.
if (
failure.errors?.every((error) =>
error.text.includes('to the configured target environment ("es5") is not supported yet'),
)
) {
esbuildResult = {
code: asset.code,
};
} else {
throw error;
}
}
// terser is used as a second pass
const terserResult = await optimizeWithTerser(
asset.name,
esbuildResult.code,
options.sourcemap,
options.target,
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 };
}
async function optimizeWithTerser(
name: string,
code: string,
sourcemaps: boolean,
target: OptimizeRequest['options']['target'],
advanced: boolean,
): Promise<{ code: string; map?: object }> {
const result = await minify(
{ [name]: code },
{
compress: {
passes: advanced ? 2 : 1,
pure_getters: advanced,
},
ecma: target,
// esbuild in the first pass is used to minify identifiers instead of mangle here
mangle: false,
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 };
}