UNPKG

@angular-devkit/build-angular

Version:
168 lines (167 loc) 8.27 kB
"use strict"; /** * @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.dev/license */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.JavaScriptOptimizerPlugin = void 0; const private_1 = require("@angular/build/private"); const piscina_1 = __importDefault(require("piscina")); const environment_options_1 = require("../../../utils/environment-options"); const webpack_diagnostics_1 = require("../../../utils/webpack-diagnostics"); const esbuild_executor_1 = require("./esbuild-executor"); /** * The maximum number of Workers that will be created to execute optimize tasks. */ const MAX_OPTIMIZE_WORKERS = environment_options_1.maxWorkers; /** * The name of the plugin provided to Webpack when tapping Webpack compiler hooks. */ const PLUGIN_NAME = 'angular-javascript-optimizer'; /** * A Webpack plugin that provides JavaScript optimization capabilities. * * The plugin uses both `esbuild` and `terser` to provide both fast and highly-optimized * code output. `esbuild` is used as an initial pass to remove the majority of unused code * as well as shorten identifiers. `terser` is then used as a secondary pass to apply * optimizations not yet implemented by `esbuild`. */ class JavaScriptOptimizerPlugin { options; targets; constructor(options) { this.options = options; if (options.supportedBrowsers) { this.targets = (0, private_1.transformSupportedBrowsersToTargets)(options.supportedBrowsers); } } apply(compiler) { const { OriginalSource, SourceMapSource } = compiler.webpack.sources; compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { const logger = compilation.getLogger('build-angular.JavaScriptOptimizerPlugin'); compilation.hooks.processAssets.tapPromise({ name: PLUGIN_NAME, stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE, }, async (compilationAssets) => { logger.time('optimize js assets'); const scriptsToOptimize = []; const cache = compilation.options.cache && compilation.getCache('JavaScriptOptimizerPlugin'); // Analyze the compilation assets for scripts that require optimization for (const assetName of Object.keys(compilationAssets)) { if (!assetName.endsWith('.js')) { continue; } const scriptAsset = compilation.getAsset(assetName); // Skip assets that have already been optimized or are verbatim copies (project assets) if (!scriptAsset || scriptAsset.info.minimized || scriptAsset.info.copied) { continue; } const { source: scriptAssetSource, name } = scriptAsset; let cacheItem; if (cache) { const eTag = cache.getLazyHashedEtag(scriptAssetSource); cacheItem = cache.getItemCache(name, eTag); const cachedOutput = await cacheItem.getPromise(); if (cachedOutput) { logger.debug(`${name} restored from cache`); compilation.updateAsset(name, cachedOutput.source, (assetInfo) => ({ ...assetInfo, minimized: true, })); continue; } } const { source, map } = scriptAssetSource.sourceAndMap(); scriptsToOptimize.push({ name: scriptAsset.name, code: typeof source === 'string' ? source : source.toString(), map, cacheItem, }); } if (scriptsToOptimize.length === 0) { return; } // Ensure all replacement values are strings which is the expected type for esbuild let define; if (this.options.define) { define = {}; for (const [key, value] of Object.entries(this.options.define)) { define[key] = String(value); } } // Setup the options used by all worker tasks const optimizeOptions = { sourcemap: this.options.sourcemap, define, keepIdentifierNames: this.options.keepIdentifierNames, target: this.targets, removeLicenses: this.options.removeLicenses, advanced: this.options.advanced, // Perform a single native esbuild support check. // This removes the need for each worker to perform the check which would // otherwise require spawning a separate process per worker. alwaysUseWasm: !(await esbuild_executor_1.EsbuildExecutor.hasNativeSupport()), }; // Sort scripts so larger scripts start first - worker pool uses a FIFO queue scriptsToOptimize.sort((a, b) => a.code.length - b.code.length); // Initialize the task worker pool const workerPath = require.resolve('./javascript-optimizer-worker'); const workerPool = new piscina_1.default({ filename: workerPath, maxThreads: MAX_OPTIMIZE_WORKERS, recordTiming: false, }); // Enqueue script optimization tasks and update compilation assets as the tasks complete try { const tasks = []; for (const { name, code, map, cacheItem } of scriptsToOptimize) { logger.time(`optimize asset: ${name}`); tasks.push(workerPool .run({ asset: { name, code, map, }, options: optimizeOptions, }) .then(async ({ code, name, map, errors }) => { if (errors?.length) { for (const error of errors) { (0, webpack_diagnostics_1.addError)(compilation, `Optimization error [${name}]: ${error}`); } return; } const optimizedAsset = map ? new SourceMapSource(code, name, map) : new OriginalSource(code, name); compilation.updateAsset(name, optimizedAsset, (assetInfo) => ({ ...assetInfo, minimized: true, })); logger.timeEnd(`optimize asset: ${name}`); return cacheItem?.storePromise({ source: optimizedAsset, }); }, (error) => { (0, webpack_diagnostics_1.addError)(compilation, `Optimization error [${name}]: ${error.stack || error.message}`); })); } await Promise.all(tasks); } finally { void workerPool.destroy(); } logger.timeEnd('optimize js assets'); }); }); } } exports.JavaScriptOptimizerPlugin = JavaScriptOptimizerPlugin;