1 | "use strict";
|
---|
2 | /**
|
---|
3 | * @license
|
---|
4 | * Copyright Google LLC All Rights Reserved.
|
---|
5 | *
|
---|
6 | * Use of this source code is governed by an MIT-style license that can be
|
---|
7 | * found in the LICENSE file at https://angular.io/license
|
---|
8 | */
|
---|
9 | var __importDefault = (this && this.__importDefault) || function (mod) {
|
---|
10 | return (mod && mod.__esModule) ? mod : { "default": mod };
|
---|
11 | };
|
---|
12 | Object.defineProperty(exports, "__esModule", { value: true });
|
---|
13 | exports.JavaScriptOptimizerPlugin = void 0;
|
---|
14 | const piscina_1 = __importDefault(require("piscina"));
|
---|
15 | const typescript_1 = require("typescript");
|
---|
16 | const environment_options_1 = require("../../utils/environment-options");
|
---|
17 | const esbuild_executor_1 = require("./esbuild-executor");
|
---|
18 | /**
|
---|
19 | * The maximum number of Workers that will be created to execute optimize tasks.
|
---|
20 | */
|
---|
21 | const MAX_OPTIMIZE_WORKERS = environment_options_1.maxWorkers;
|
---|
22 | /**
|
---|
23 | * The name of the plugin provided to Webpack when tapping Webpack compiler hooks.
|
---|
24 | */
|
---|
25 | const PLUGIN_NAME = 'angular-javascript-optimizer';
|
---|
26 | /**
|
---|
27 | * A Webpack plugin that provides JavaScript optimization capabilities.
|
---|
28 | *
|
---|
29 | * The plugin uses both `esbuild` and `terser` to provide both fast and highly-optimized
|
---|
30 | * code output. `esbuild` is used as an initial pass to remove the majority of unused code
|
---|
31 | * as well as shorten identifiers. `terser` is then used as a secondary pass to apply
|
---|
32 | * optimizations not yet implemented by `esbuild`.
|
---|
33 | */
|
---|
34 | class JavaScriptOptimizerPlugin {
|
---|
35 | constructor(options = {}) {
|
---|
36 | this.options = options;
|
---|
37 | }
|
---|
38 | apply(compiler) {
|
---|
39 | const { OriginalSource, SourceMapSource } = compiler.webpack.sources;
|
---|
40 | compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
|
---|
41 | compilation.hooks.processAssets.tapPromise({
|
---|
42 | name: PLUGIN_NAME,
|
---|
43 | stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE,
|
---|
44 | }, async (compilationAssets) => {
|
---|
45 | const scriptsToOptimize = [];
|
---|
46 | const cache = compilation.options.cache && compilation.getCache('JavaScriptOptimizerPlugin');
|
---|
47 | // Analyze the compilation assets for scripts that require optimization
|
---|
48 | for (const assetName of Object.keys(compilationAssets)) {
|
---|
49 | if (!assetName.endsWith('.js')) {
|
---|
50 | continue;
|
---|
51 | }
|
---|
52 | const scriptAsset = compilation.getAsset(assetName);
|
---|
53 | // Skip assets that have already been optimized or are verbatim copies (project assets)
|
---|
54 | if (!scriptAsset || scriptAsset.info.minimized || scriptAsset.info.copied) {
|
---|
55 | continue;
|
---|
56 | }
|
---|
57 | const { source: scriptAssetSource, name } = scriptAsset;
|
---|
58 | let cacheItem;
|
---|
59 | if (cache) {
|
---|
60 | const eTag = cache.getLazyHashedEtag(scriptAssetSource);
|
---|
61 | cacheItem = cache.getItemCache(name, eTag);
|
---|
62 | const cachedOutput = await cacheItem.getPromise();
|
---|
63 | if (cachedOutput) {
|
---|
64 | compilation.updateAsset(name, cachedOutput.source, {
|
---|
65 | minimized: true,
|
---|
66 | });
|
---|
67 | continue;
|
---|
68 | }
|
---|
69 | }
|
---|
70 | const { source, map } = scriptAssetSource.sourceAndMap();
|
---|
71 | scriptsToOptimize.push({
|
---|
72 | name: scriptAsset.name,
|
---|
73 | code: typeof source === 'string' ? source : source.toString(),
|
---|
74 | map,
|
---|
75 | cacheItem,
|
---|
76 | });
|
---|
77 | }
|
---|
78 | if (scriptsToOptimize.length === 0) {
|
---|
79 | return;
|
---|
80 | }
|
---|
81 | // Ensure all replacement values are strings which is the expected type for esbuild
|
---|
82 | let define;
|
---|
83 | if (this.options.define) {
|
---|
84 | define = {};
|
---|
85 | for (const [key, value] of Object.entries(this.options.define)) {
|
---|
86 | define[key] = String(value);
|
---|
87 | }
|
---|
88 | }
|
---|
89 | let target = 2017;
|
---|
90 | if (this.options.target) {
|
---|
91 | if (this.options.target <= typescript_1.ScriptTarget.ES5) {
|
---|
92 | target = 5;
|
---|
93 | }
|
---|
94 | else if (this.options.target < typescript_1.ScriptTarget.ESNext) {
|
---|
95 | target = Number(typescript_1.ScriptTarget[this.options.target].slice(2));
|
---|
96 | }
|
---|
97 | else {
|
---|
98 | target = 2020;
|
---|
99 | }
|
---|
100 | }
|
---|
101 | // Setup the options used by all worker tasks
|
---|
102 | const optimizeOptions = {
|
---|
103 | sourcemap: this.options.sourcemap,
|
---|
104 | define,
|
---|
105 | keepNames: this.options.keepNames,
|
---|
106 | target,
|
---|
107 | removeLicenses: this.options.removeLicenses,
|
---|
108 | advanced: this.options.advanced,
|
---|
109 | // Perform a single native esbuild support check.
|
---|
110 | // This removes the need for each worker to perform the check which would
|
---|
111 | // otherwise require spawning a separate process per worker.
|
---|
112 | alwaysUseWasm: !esbuild_executor_1.EsbuildExecutor.hasNativeSupport(),
|
---|
113 | };
|
---|
114 | // Sort scripts so larger scripts start first - worker pool uses a FIFO queue
|
---|
115 | scriptsToOptimize.sort((a, b) => a.code.length - b.code.length);
|
---|
116 | // Initialize the task worker pool
|
---|
117 | const workerPath = require.resolve('./javascript-optimizer-worker');
|
---|
118 | const workerPool = new piscina_1.default({
|
---|
119 | filename: workerPath,
|
---|
120 | maxThreads: MAX_OPTIMIZE_WORKERS,
|
---|
121 | });
|
---|
122 | // Enqueue script optimization tasks and update compilation assets as the tasks complete
|
---|
123 | try {
|
---|
124 | const tasks = [];
|
---|
125 | for (const { name, code, map, cacheItem } of scriptsToOptimize) {
|
---|
126 | tasks.push(workerPool
|
---|
127 | .run({
|
---|
128 | asset: {
|
---|
129 | name,
|
---|
130 | code,
|
---|
131 | map,
|
---|
132 | },
|
---|
133 | options: optimizeOptions,
|
---|
134 | })
|
---|
135 | .then(({ code, name, map }) => {
|
---|
136 | const optimizedAsset = map
|
---|
137 | ? new SourceMapSource(code, name, map)
|
---|
138 | : new OriginalSource(code, name);
|
---|
139 | compilation.updateAsset(name, optimizedAsset, { minimized: true });
|
---|
140 | return cacheItem === null || cacheItem === void 0 ? void 0 : cacheItem.storePromise({
|
---|
141 | source: optimizedAsset,
|
---|
142 | });
|
---|
143 | }, (error) => {
|
---|
144 | const optimizationError = new compiler.webpack.WebpackError(`Optimization error [${name}]: ${error.stack || error.message}`);
|
---|
145 | compilation.errors.push(optimizationError);
|
---|
146 | }));
|
---|
147 | }
|
---|
148 | await Promise.all(tasks);
|
---|
149 | }
|
---|
150 | finally {
|
---|
151 | void workerPool.destroy();
|
---|
152 | }
|
---|
153 | });
|
---|
154 | });
|
---|
155 | }
|
---|
156 | }
|
---|
157 | exports.JavaScriptOptimizerPlugin = JavaScriptOptimizerPlugin;
|
---|