| 1 | "use strict";
|
|---|
| 2 |
|
|---|
| 3 | const os = require("os");
|
|---|
| 4 | const path = require("path");
|
|---|
| 5 | const {
|
|---|
| 6 | validate
|
|---|
| 7 | } = require("schema-utils");
|
|---|
| 8 | const {
|
|---|
| 9 | minify
|
|---|
| 10 | } = require("./minify");
|
|---|
| 11 | const schema = require("./options.json");
|
|---|
| 12 | const {
|
|---|
| 13 | cleanCssMinify,
|
|---|
| 14 | cssnanoMinify,
|
|---|
| 15 | cssoMinify,
|
|---|
| 16 | esbuildMinify,
|
|---|
| 17 | esbuildMinifyCss,
|
|---|
| 18 | getEcmaVersion,
|
|---|
| 19 | htmlMinifierTerser,
|
|---|
| 20 | jsonMinify,
|
|---|
| 21 | lightningCssMinify,
|
|---|
| 22 | memoize,
|
|---|
| 23 | minifyHtmlNode,
|
|---|
| 24 | swcMinify,
|
|---|
| 25 | swcMinifyCss,
|
|---|
| 26 | swcMinifyHtml,
|
|---|
| 27 | swcMinifyHtmlFragment,
|
|---|
| 28 | terserMinify,
|
|---|
| 29 | throttleAll,
|
|---|
| 30 | uglifyJsMinify
|
|---|
| 31 | } = require("./utils");
|
|---|
| 32 |
|
|---|
| 33 | /** @typedef {import("schema-utils/declarations/validate").Schema} Schema */
|
|---|
| 34 | /** @typedef {import("webpack").Compiler} Compiler */
|
|---|
| 35 | /** @typedef {import("webpack").Compilation} Compilation */
|
|---|
| 36 | /** @typedef {import("webpack").Asset} Asset */
|
|---|
| 37 | /** @typedef {import("webpack").AssetInfo} AssetInfo */
|
|---|
| 38 | /** @typedef {import("webpack").TemplatePath} TemplatePath */
|
|---|
| 39 | /** @typedef {import("jest-worker").Worker} JestWorker */
|
|---|
| 40 | /** @typedef {import("@jridgewell/trace-mapping").EncodedSourceMap & { sources: string[], sourcesContent?: string[], file: string }} RawSourceMap */
|
|---|
| 41 | /** @typedef {import("@jridgewell/trace-mapping").TraceMap} TraceMap */
|
|---|
| 42 |
|
|---|
| 43 | /** @typedef {RegExp | string} Rule */
|
|---|
| 44 | /** @typedef {Rule[] | Rule} Rules */
|
|---|
| 45 |
|
|---|
| 46 | // eslint-disable-next-line jsdoc/reject-any-type
|
|---|
| 47 | /** @typedef {any} EXPECTED_ANY */
|
|---|
| 48 | // eslint-disable-next-line jsdoc/require-property
|
|---|
| 49 | /** @typedef {object} EXPECTED_OBJECT */
|
|---|
| 50 |
|
|---|
| 51 | /**
|
|---|
| 52 | * @callback ExtractCommentsFunction
|
|---|
| 53 | * @param {EXPECTED_ANY} astNode ast Node
|
|---|
| 54 | * @param {{ value: string, type: "comment1" | "comment2" | "comment3" | "comment4", pos: number, line: number, col: number }} comment comment node
|
|---|
| 55 | * @returns {boolean} true when need to extract comment, otherwise false
|
|---|
| 56 | */
|
|---|
| 57 |
|
|---|
| 58 | /**
|
|---|
| 59 | * @typedef {boolean | "all" | "some" | RegExp | ExtractCommentsFunction} ExtractCommentsCondition
|
|---|
| 60 | */
|
|---|
| 61 |
|
|---|
| 62 | /**
|
|---|
| 63 | * @typedef {TemplatePath} ExtractCommentsFilename
|
|---|
| 64 | */
|
|---|
| 65 |
|
|---|
| 66 | /**
|
|---|
| 67 | * @typedef {boolean | string | ((commentsFile: string) => string)} ExtractCommentsBanner
|
|---|
| 68 | */
|
|---|
| 69 |
|
|---|
| 70 | /**
|
|---|
| 71 | * @typedef {object} ExtractCommentsObject
|
|---|
| 72 | * @property {ExtractCommentsCondition=} condition condition which comments need to be expected
|
|---|
| 73 | * @property {ExtractCommentsFilename=} filename filename for extracted comments
|
|---|
| 74 | * @property {ExtractCommentsBanner=} banner banner in filename for extracted comments
|
|---|
| 75 | */
|
|---|
| 76 |
|
|---|
| 77 | /**
|
|---|
| 78 | * @typedef {ExtractCommentsCondition | ExtractCommentsObject} ExtractCommentsOptions
|
|---|
| 79 | */
|
|---|
| 80 |
|
|---|
| 81 | /**
|
|---|
| 82 | * @typedef {object} ErrorObject
|
|---|
| 83 | * @property {string} message message
|
|---|
| 84 | * @property {number=} line line number
|
|---|
| 85 | * @property {number=} column column number
|
|---|
| 86 | * @property {string=} stack error stack trace
|
|---|
| 87 | */
|
|---|
| 88 |
|
|---|
| 89 | /**
|
|---|
| 90 | * @typedef {object} MinimizedResult
|
|---|
| 91 | * @property {string=} code code
|
|---|
| 92 | * @property {RawSourceMap=} map source map
|
|---|
| 93 | * @property {(Error | string)[]=} errors errors
|
|---|
| 94 | * @property {(Error | string)[]=} warnings warnings
|
|---|
| 95 | * @property {string[]=} extractedComments extracted comments
|
|---|
| 96 | */
|
|---|
| 97 |
|
|---|
| 98 | /**
|
|---|
| 99 | * @typedef {{ [file: string]: string }} Input
|
|---|
| 100 | */
|
|---|
| 101 |
|
|---|
| 102 | /**
|
|---|
| 103 | * @typedef {{ [key: string]: EXPECTED_ANY }} CustomOptions
|
|---|
| 104 | */
|
|---|
| 105 |
|
|---|
| 106 | /**
|
|---|
| 107 | * @template T
|
|---|
| 108 | * @typedef {T extends infer U ? U : CustomOptions} InferDefaultType
|
|---|
| 109 | */
|
|---|
| 110 |
|
|---|
| 111 | /**
|
|---|
| 112 | * @template T
|
|---|
| 113 | * @typedef {T extends EXPECTED_ANY[] ? { [P in keyof T]?: T[P] & InferDefaultType<T[P]> } : T & InferDefaultType<T>} MinimizerOptions
|
|---|
| 114 | */
|
|---|
| 115 |
|
|---|
| 116 | /**
|
|---|
| 117 | * @template T
|
|---|
| 118 | * @callback BasicMinimizerImplementation
|
|---|
| 119 | * @param {Input} input
|
|---|
| 120 | * @param {RawSourceMap | undefined} sourceMap
|
|---|
| 121 | * @param {MinimizerOptions<T>} minifyOptions
|
|---|
| 122 | * @param {ExtractCommentsOptions | undefined} extractComments
|
|---|
| 123 | * @returns {Promise<MinimizedResult> | MinimizedResult}
|
|---|
| 124 | */
|
|---|
| 125 |
|
|---|
| 126 | /**
|
|---|
| 127 | * @typedef {object} MinimizeFunctionHelpers
|
|---|
| 128 | * @property {() => string | undefined=} getMinimizerVersion function that returns version of minimizer
|
|---|
| 129 | * @property {() => boolean | undefined=} supportsWorkerThreads true when minimizer support worker threads, otherwise false
|
|---|
| 130 | * @property {() => boolean | undefined=} supportsWorker true when minimizer support worker, otherwise false
|
|---|
| 131 | * @property {(name: string, info?: AssetInfo) => boolean | undefined=} filter return true when the minimizer supports the asset, otherwise false. When an array of minimizers is configured, each asset is dispatched only to the minimizers whose `filter` accepts it. Assets rejected by every minimizer in the array are skipped entirely.
|
|---|
| 132 | */
|
|---|
| 133 |
|
|---|
| 134 | /**
|
|---|
| 135 | * @template T
|
|---|
| 136 | * @typedef {T extends EXPECTED_ANY[] ? { [P in keyof T]: BasicMinimizerImplementation<T[P]> & MinimizeFunctionHelpers } : BasicMinimizerImplementation<T> & MinimizeFunctionHelpers} MinimizerImplementation
|
|---|
| 137 | */
|
|---|
| 138 |
|
|---|
| 139 | /**
|
|---|
| 140 | * @template T
|
|---|
| 141 | * @typedef {object} InternalOptions
|
|---|
| 142 | * @property {string} name name
|
|---|
| 143 | * @property {string} input input
|
|---|
| 144 | * @property {RawSourceMap | undefined} inputSourceMap input source map
|
|---|
| 145 | * @property {ExtractCommentsOptions | undefined} extractComments extract comments option
|
|---|
| 146 | * @property {{ implementation: MinimizerImplementation<T>, options: MinimizerOptions<T> }} minimizer minimizer
|
|---|
| 147 | * @property {boolean=} module true when code is a EC module, otherwise false
|
|---|
| 148 | * @property {number | string=} ecma ecma version
|
|---|
| 149 | */
|
|---|
| 150 |
|
|---|
| 151 | /**
|
|---|
| 152 | * @template T
|
|---|
| 153 | * @typedef {JestWorker & { transform: (options: string) => Promise<MinimizedResult>, minify: (options: InternalOptions<T>) => Promise<MinimizedResult> }} MinimizerWorker
|
|---|
| 154 | */
|
|---|
| 155 |
|
|---|
| 156 | /**
|
|---|
| 157 | * @typedef {undefined | boolean | number} Parallel
|
|---|
| 158 | */
|
|---|
| 159 |
|
|---|
| 160 | /**
|
|---|
| 161 | * @typedef {object} BasePluginOptions
|
|---|
| 162 | * @property {Rules=} test test rule
|
|---|
| 163 | * @property {Rules=} include include rile
|
|---|
| 164 | * @property {Rules=} exclude exclude rule
|
|---|
| 165 | * @property {ExtractCommentsOptions=} extractComments extract comments options
|
|---|
| 166 | * @property {Parallel=} parallel parallel option
|
|---|
| 167 | */
|
|---|
| 168 |
|
|---|
| 169 | /**
|
|---|
| 170 | * @template T
|
|---|
| 171 | * @typedef {T extends import("terser").MinifyOptions ? { minify?: MinimizerImplementation<T> | undefined, minimizerOptions?: MinimizerOptions<T> | undefined, terserOptions?: MinimizerOptions<T> | undefined } : { minify: MinimizerImplementation<T>, minimizerOptions?: MinimizerOptions<T> | undefined, terserOptions?: MinimizerOptions<T> | undefined }} DefinedDefaultMinimizerAndOptions
|
|---|
| 172 | */
|
|---|
| 173 |
|
|---|
| 174 | /**
|
|---|
| 175 | * @template T
|
|---|
| 176 | * @typedef {BasePluginOptions & { minimizer: { implementation: MinimizerImplementation<T>, options: MinimizerOptions<T> } }} InternalPluginOptions
|
|---|
| 177 | */
|
|---|
| 178 |
|
|---|
| 179 | const getTraceMapping = memoize(() => require("@jridgewell/trace-mapping"));
|
|---|
| 180 | const getSerializeJavascript = memoize(() => require("./serialize-javascript"));
|
|---|
| 181 |
|
|---|
| 182 | /**
|
|---|
| 183 | * @template [T=import("terser").MinifyOptions]
|
|---|
| 184 | */
|
|---|
| 185 | class TerserPlugin {
|
|---|
| 186 | /**
|
|---|
| 187 | * @param {BasePluginOptions & DefinedDefaultMinimizerAndOptions<T>=} options options
|
|---|
| 188 | */
|
|---|
| 189 | constructor(options) {
|
|---|
| 190 | validate(/** @type {Schema} */schema, options || {}, {
|
|---|
| 191 | name: "Terser Plugin",
|
|---|
| 192 | baseDataPath: "options"
|
|---|
| 193 | });
|
|---|
| 194 |
|
|---|
| 195 | // TODO handle json and etc in the next major release
|
|---|
| 196 | // TODO make `minimizer` option instead `minify` and `terserOptions` in the next major release, also rename `terserMinify` to `terserMinimize`
|
|---|
| 197 | const {
|
|---|
| 198 | minify = (/** @type {MinimizerImplementation<T>} */
|
|---|
| 199 | /** @type {unknown} */terserMinify),
|
|---|
| 200 | minimizerOptions,
|
|---|
| 201 | terserOptions,
|
|---|
| 202 | test = /\.[cm]?js(\?.*)?$/i,
|
|---|
| 203 | extractComments = true,
|
|---|
| 204 | parallel = true,
|
|---|
| 205 | include,
|
|---|
| 206 | exclude
|
|---|
| 207 | } = options || {};
|
|---|
| 208 |
|
|---|
| 209 | // `terserOptions` is a deprecated alias of `minimizerOptions`; prefer the
|
|---|
| 210 | // new name when both are provided.
|
|---|
| 211 | const resolvedMinimizerOptions = /** @type {MinimizerOptions<T>} */
|
|---|
| 212 |
|
|---|
| 213 | typeof minimizerOptions !== "undefined" ? minimizerOptions : terserOptions || {};
|
|---|
| 214 |
|
|---|
| 215 | /**
|
|---|
| 216 | * @private
|
|---|
| 217 | * @type {InternalPluginOptions<T>}
|
|---|
| 218 | */
|
|---|
| 219 | this.options = {
|
|---|
| 220 | test,
|
|---|
| 221 | extractComments,
|
|---|
| 222 | parallel,
|
|---|
| 223 | include,
|
|---|
| 224 | exclude,
|
|---|
| 225 | minimizer: {
|
|---|
| 226 | implementation: minify,
|
|---|
| 227 | options: resolvedMinimizerOptions
|
|---|
| 228 | }
|
|---|
| 229 | };
|
|---|
| 230 | }
|
|---|
| 231 |
|
|---|
| 232 | /**
|
|---|
| 233 | * @private
|
|---|
| 234 | * @param {unknown} input Input to check
|
|---|
| 235 | * @returns {boolean} Whether input is a source map
|
|---|
| 236 | */
|
|---|
| 237 | static isSourceMap(input) {
|
|---|
| 238 | // All required options for `new TraceMap(...options)`
|
|---|
| 239 | // https://github.com/jridgewell/trace-mapping#usage
|
|---|
| 240 | return Boolean(input && typeof input === "object" && input !== null && "version" in input && "sources" in input && Array.isArray(input.sources) && "mappings" in input && typeof input.mappings === "string");
|
|---|
| 241 | }
|
|---|
| 242 |
|
|---|
| 243 | /**
|
|---|
| 244 | * @private
|
|---|
| 245 | * @param {unknown} warning warning
|
|---|
| 246 | * @param {string} file file
|
|---|
| 247 | * @returns {Error} built warning
|
|---|
| 248 | */
|
|---|
| 249 | static buildWarning(warning, file) {
|
|---|
| 250 | /**
|
|---|
| 251 | * @type {Error & { hideStack: true, file: string }}
|
|---|
| 252 | */
|
|---|
| 253 | // @ts-expect-error
|
|---|
| 254 | const builtWarning = new Error(warning.toString());
|
|---|
| 255 | builtWarning.name = "Warning";
|
|---|
| 256 | builtWarning.hideStack = true;
|
|---|
| 257 | builtWarning.file = file;
|
|---|
| 258 | return builtWarning;
|
|---|
| 259 | }
|
|---|
| 260 |
|
|---|
| 261 | /**
|
|---|
| 262 | * @private
|
|---|
| 263 | * @param {Error | ErrorObject | string} error error
|
|---|
| 264 | * @param {string} file file
|
|---|
| 265 | * @param {TraceMap=} sourceMap source map
|
|---|
| 266 | * @param {Compilation["requestShortener"]=} requestShortener request shortener
|
|---|
| 267 | * @returns {Error} built error
|
|---|
| 268 | */
|
|---|
| 269 | static buildError(error, file, sourceMap, requestShortener) {
|
|---|
| 270 | /**
|
|---|
| 271 | * @type {Error & { file?: string }}
|
|---|
| 272 | */
|
|---|
| 273 | let builtError;
|
|---|
| 274 | if (typeof error === "string") {
|
|---|
| 275 | builtError = new Error(`${file} from Terser plugin\n${error}`);
|
|---|
| 276 | builtError.file = file;
|
|---|
| 277 | return builtError;
|
|---|
| 278 | }
|
|---|
| 279 | if (/** @type {ErrorObject} */error.line) {
|
|---|
| 280 | const {
|
|---|
| 281 | line,
|
|---|
| 282 | column
|
|---|
| 283 | } = /** @type {ErrorObject & { line: number, column: number }} */error;
|
|---|
| 284 | const original = sourceMap && getTraceMapping().originalPositionFor(sourceMap, {
|
|---|
| 285 | line,
|
|---|
| 286 | column
|
|---|
| 287 | });
|
|---|
| 288 | if (original && original.source && requestShortener) {
|
|---|
| 289 | builtError = new Error(`${file} from Terser plugin\n${error.message} [${requestShortener.shorten(original.source)}:${original.line},${original.column}][${file}:${line},${column}]${error.stack ? `\n${error.stack.split("\n").slice(1).join("\n")}` : ""}`);
|
|---|
| 290 | builtError.file = file;
|
|---|
| 291 | return builtError;
|
|---|
| 292 | }
|
|---|
| 293 | builtError = new Error(`${file} from Terser plugin\n${error.message} [${file}:${line},${column}]${error.stack ? `\n${error.stack.split("\n").slice(1).join("\n")}` : ""}`);
|
|---|
| 294 | builtError.file = file;
|
|---|
| 295 | return builtError;
|
|---|
| 296 | }
|
|---|
| 297 | if (error.stack) {
|
|---|
| 298 | builtError = new Error(`${file} from Terser plugin\n${typeof error.message !== "undefined" ? error.message : ""}\n${error.stack}`);
|
|---|
| 299 | builtError.file = file;
|
|---|
| 300 | return builtError;
|
|---|
| 301 | }
|
|---|
| 302 | builtError = new Error(`${file} from Terser plugin\n${error.message}`);
|
|---|
| 303 | builtError.file = file;
|
|---|
| 304 | return builtError;
|
|---|
| 305 | }
|
|---|
| 306 |
|
|---|
| 307 | /**
|
|---|
| 308 | * @private
|
|---|
| 309 | * @param {Parallel} parallel value of the `parallel` option
|
|---|
| 310 | * @returns {number} number of cores for parallelism
|
|---|
| 311 | */
|
|---|
| 312 | static getAvailableNumberOfCores(parallel) {
|
|---|
| 313 | // In some cases cpus() returns undefined
|
|---|
| 314 | // https://github.com/nodejs/node/issues/19022
|
|---|
| 315 | const cpus =
|
|---|
| 316 | // eslint-disable-next-line n/no-unsupported-features/node-builtins
|
|---|
| 317 | typeof os.availableParallelism === "function" ?
|
|---|
| 318 | // eslint-disable-next-line n/no-unsupported-features/node-builtins
|
|---|
| 319 | {
|
|---|
| 320 | length: os.availableParallelism()
|
|---|
| 321 | } : os.cpus() || {
|
|---|
| 322 | length: 1
|
|---|
| 323 | };
|
|---|
| 324 | return parallel === true || typeof parallel === "undefined" ? cpus.length - 1 : Math.min(parallel || 0, cpus.length - 1);
|
|---|
| 325 | }
|
|---|
| 326 |
|
|---|
| 327 | /**
|
|---|
| 328 | * @private
|
|---|
| 329 | * @param {Compiler} compiler compiler
|
|---|
| 330 | * @param {Compilation} compilation compilation
|
|---|
| 331 | * @param {Record<string, import("webpack").sources.Source>} assets assets
|
|---|
| 332 | * @param {{ availableNumberOfCores: number }} optimizeOptions optimize options
|
|---|
| 333 | * @returns {Promise<void>}
|
|---|
| 334 | */
|
|---|
| 335 | async optimize(compiler, compilation, assets, optimizeOptions) {
|
|---|
| 336 | const cache = compilation.getCache("TerserWebpackPlugin");
|
|---|
| 337 | let numberOfAssets = 0;
|
|---|
| 338 |
|
|---|
| 339 | // Normalize the implementation list to an array so dispatch and the
|
|---|
| 340 | // worker-pool capability checks below can iterate uniformly. The
|
|---|
| 341 | // original shape on `this.options.minimizer.implementation` is preserved
|
|---|
| 342 | // for chunk hashing.
|
|---|
| 343 | const implementations = Array.isArray(this.options.minimizer.implementation) ? this.options.minimizer.implementation : [this.options.minimizer.implementation];
|
|---|
| 344 |
|
|---|
| 345 | /**
|
|---|
| 346 | * Collect the indices of minimizers whose `filter` accepts `name`.
|
|---|
| 347 | * Filters returning `undefined` are treated as accept (matches the
|
|---|
| 348 | * convention used by `supportsWorkerThreads`).
|
|---|
| 349 | * @param {string} name asset name
|
|---|
| 350 | * @param {AssetInfo} info asset info
|
|---|
| 351 | * @returns {number[]} indices into `implementations` that accept the asset
|
|---|
| 352 | */
|
|---|
| 353 | const matchingMinimizers = (name, info) => {
|
|---|
| 354 | const matched = [];
|
|---|
| 355 | for (let i = 0; i < implementations.length; i++) {
|
|---|
| 356 | const impl = implementations[i];
|
|---|
| 357 | if (typeof impl.filter !== "function" ||
|
|---|
| 358 | // eslint-disable-next-line unicorn/no-array-method-this-argument
|
|---|
| 359 | impl.filter(name, info) !== false) {
|
|---|
| 360 | matched.push(i);
|
|---|
| 361 | }
|
|---|
| 362 | }
|
|---|
| 363 | return matched;
|
|---|
| 364 | };
|
|---|
| 365 | /** @type {Map<string, number[]>} */
|
|---|
| 366 | const matchedByName = new Map();
|
|---|
| 367 | const assetsForMinify = await Promise.all(Object.keys(assets).filter(name => {
|
|---|
| 368 | const {
|
|---|
| 369 | info
|
|---|
| 370 | } = /** @type {Asset} */compilation.getAsset(name);
|
|---|
| 371 | if (
|
|---|
| 372 | // Skip double minimize assets from child compilation
|
|---|
| 373 | info.minimized ||
|
|---|
| 374 | // Skip minimizing for extracted comments assets
|
|---|
| 375 | info.extractedComments) {
|
|---|
| 376 | return false;
|
|---|
| 377 | }
|
|---|
| 378 | if (!compiler.webpack.ModuleFilenameHelpers.matchObject.bind(undefined, this.options)(name)) {
|
|---|
| 379 | return false;
|
|---|
| 380 | }
|
|---|
| 381 |
|
|---|
| 382 | // Compute the matching minimizers once and carry the result to the
|
|---|
| 383 | // per-asset task via `matchedByName` so the regexes don't run again.
|
|---|
| 384 | const matched = matchingMinimizers(name, info);
|
|---|
| 385 | if (matched.length === 0) {
|
|---|
| 386 | return false;
|
|---|
| 387 | }
|
|---|
| 388 | matchedByName.set(name, matched);
|
|---|
| 389 | return true;
|
|---|
| 390 | }).map(async name => {
|
|---|
| 391 | const {
|
|---|
| 392 | info,
|
|---|
| 393 | source
|
|---|
| 394 | } = /** @type {Asset} */
|
|---|
| 395 | compilation.getAsset(name);
|
|---|
| 396 | const eTag = cache.getLazyHashedEtag(source);
|
|---|
| 397 | const cacheItem = cache.getItemCache(name, eTag);
|
|---|
| 398 | const output = await cacheItem.getPromise();
|
|---|
| 399 | if (!output) {
|
|---|
| 400 | numberOfAssets += 1;
|
|---|
| 401 | }
|
|---|
| 402 | return {
|
|---|
| 403 | name,
|
|---|
| 404 | info,
|
|---|
| 405 | inputSource: source,
|
|---|
| 406 | output,
|
|---|
| 407 | cacheItem,
|
|---|
| 408 | matched: (/** @type {number[]} */matchedByName.get(name))
|
|---|
| 409 | };
|
|---|
| 410 | }));
|
|---|
| 411 | if (assetsForMinify.length === 0) {
|
|---|
| 412 | return;
|
|---|
| 413 | }
|
|---|
| 414 |
|
|---|
| 415 | /** @type {undefined | (() => MinimizerWorker<T>)} */
|
|---|
| 416 | let getWorker;
|
|---|
| 417 | /** @type {undefined | MinimizerWorker<T>} */
|
|---|
| 418 | let initializedWorker;
|
|---|
| 419 | /** @type {undefined | number} */
|
|---|
| 420 | let numberOfWorkers;
|
|---|
| 421 | const needCreateWorker = optimizeOptions.availableNumberOfCores > 0 && implementations.every(impl => typeof impl.supportsWorker === "undefined" || typeof impl.supportsWorker === "function" && impl.supportsWorker());
|
|---|
| 422 | if (needCreateWorker) {
|
|---|
| 423 | // Do not create unnecessary workers when the number of files is less than the available cores, it saves memory
|
|---|
| 424 | numberOfWorkers = Math.min(numberOfAssets, optimizeOptions.availableNumberOfCores);
|
|---|
| 425 | getWorker = () => {
|
|---|
| 426 | if (initializedWorker) {
|
|---|
| 427 | return initializedWorker;
|
|---|
| 428 | }
|
|---|
| 429 | const {
|
|---|
| 430 | Worker
|
|---|
| 431 | } = require("jest-worker");
|
|---|
| 432 | initializedWorker = /** @type {MinimizerWorker<T>} */
|
|---|
| 433 |
|
|---|
| 434 | new Worker(require.resolve("./minify"), {
|
|---|
| 435 | numWorkers: numberOfWorkers,
|
|---|
| 436 | enableWorkerThreads: implementations.every(impl => typeof impl.supportsWorkerThreads === "undefined" || impl.supportsWorkerThreads() !== false)
|
|---|
| 437 | });
|
|---|
| 438 |
|
|---|
| 439 | // https://github.com/facebook/jest/issues/8872#issuecomment-524822081
|
|---|
| 440 | const workerStdout = initializedWorker.getStdout();
|
|---|
| 441 | if (workerStdout) {
|
|---|
| 442 | workerStdout.on("data", chunk => process.stdout.write(chunk));
|
|---|
| 443 | }
|
|---|
| 444 | const workerStderr = initializedWorker.getStderr();
|
|---|
| 445 | if (workerStderr) {
|
|---|
| 446 | workerStderr.on("data", chunk => process.stderr.write(chunk));
|
|---|
| 447 | }
|
|---|
| 448 | return initializedWorker;
|
|---|
| 449 | };
|
|---|
| 450 | }
|
|---|
| 451 | const {
|
|---|
| 452 | SourceMapSource,
|
|---|
| 453 | ConcatSource,
|
|---|
| 454 | RawSource
|
|---|
| 455 | } = compiler.webpack.sources;
|
|---|
| 456 |
|
|---|
| 457 | /** @typedef {{ extractedCommentsSource: import("webpack").sources.RawSource, commentsFilename: string }} ExtractedCommentsInfo */
|
|---|
| 458 | /** @type {Map<string, ExtractedCommentsInfo>} */
|
|---|
| 459 | const allExtractedComments = new Map();
|
|---|
| 460 | const scheduledTasks = [];
|
|---|
| 461 | for (const asset of assetsForMinify) {
|
|---|
| 462 | scheduledTasks.push(async () => {
|
|---|
| 463 | const {
|
|---|
| 464 | name,
|
|---|
| 465 | inputSource,
|
|---|
| 466 | info,
|
|---|
| 467 | cacheItem,
|
|---|
| 468 | matched
|
|---|
| 469 | } = asset;
|
|---|
| 470 | let {
|
|---|
| 471 | output
|
|---|
| 472 | } = asset;
|
|---|
| 473 | if (!output) {
|
|---|
| 474 | let input;
|
|---|
| 475 | /** @type {RawSourceMap | undefined} */
|
|---|
| 476 | let inputSourceMap;
|
|---|
| 477 | const {
|
|---|
| 478 | source: sourceFromInputSource,
|
|---|
| 479 | map
|
|---|
| 480 | } = inputSource.sourceAndMap();
|
|---|
| 481 | input = sourceFromInputSource;
|
|---|
| 482 | if (map) {
|
|---|
| 483 | if (!TerserPlugin.isSourceMap(map)) {
|
|---|
| 484 | compilation.warnings.push(new Error(`${name} contains invalid source map`));
|
|---|
| 485 | } else {
|
|---|
| 486 | inputSourceMap = /** @type {RawSourceMap} */map;
|
|---|
| 487 | }
|
|---|
| 488 | }
|
|---|
| 489 | if (Buffer.isBuffer(input)) {
|
|---|
| 490 | input = input.toString();
|
|---|
| 491 | }
|
|---|
| 492 |
|
|---|
| 493 | // Dispatch to only the minimizers whose `filter` accepted this
|
|---|
| 494 | // asset (computed once when collecting `assetsForMinify`).
|
|---|
| 495 | // `minify.js` already normalizes a single implementation into a
|
|---|
| 496 | // one-element array, so we always hand it the matching subset.
|
|---|
| 497 | // Options are sliced as references — `minify.js` overlays
|
|---|
| 498 | // `module`/`ecma` without mutating the caller's object.
|
|---|
| 499 | const assetImplementation = /** @type {MinimizerImplementation<T>} */
|
|---|
| 500 | matched.map(i => implementations[i]);
|
|---|
| 501 | const sourceOptions = this.options.minimizer.options;
|
|---|
| 502 | const assetMinimizerOptions = /** @type {MinimizerOptions<T>} */
|
|---|
| 503 |
|
|---|
| 504 | Array.isArray(sourceOptions) ? matched.map(i => sourceOptions[i] || {}) : sourceOptions;
|
|---|
| 505 |
|
|---|
| 506 | /**
|
|---|
| 507 | * @type {InternalOptions<T>}
|
|---|
| 508 | */
|
|---|
| 509 | const options = {
|
|---|
| 510 | name,
|
|---|
| 511 | input,
|
|---|
| 512 | inputSourceMap,
|
|---|
| 513 | minimizer: {
|
|---|
| 514 | implementation: assetImplementation,
|
|---|
| 515 | options: assetMinimizerOptions
|
|---|
| 516 | },
|
|---|
| 517 | extractComments: this.options.extractComments
|
|---|
| 518 | };
|
|---|
| 519 | if (typeof info.javascriptModule !== "undefined") {
|
|---|
| 520 | options.module = info.javascriptModule;
|
|---|
| 521 | } else if (/\.mjs(\?.*)?$/i.test(name)) {
|
|---|
| 522 | options.module = true;
|
|---|
| 523 | } else if (/\.cjs(\?.*)?$/i.test(name)) {
|
|---|
| 524 | options.module = false;
|
|---|
| 525 | }
|
|---|
| 526 | options.ecma = getEcmaVersion(compiler.options.output.environment);
|
|---|
| 527 | try {
|
|---|
| 528 | output = await (getWorker ? getWorker().transform(getSerializeJavascript()(options)) : minify(options));
|
|---|
| 529 | } catch (error) {
|
|---|
| 530 | const hasSourceMap = inputSourceMap && TerserPlugin.isSourceMap(inputSourceMap);
|
|---|
| 531 | compilation.errors.push(TerserPlugin.buildError(/** @type {Error | ErrorObject | string} */
|
|---|
| 532 | error, name, hasSourceMap ? new (getTraceMapping().TraceMap)(/** @type {RawSourceMap} */
|
|---|
| 533 | inputSourceMap) : undefined, hasSourceMap ? compilation.requestShortener : undefined));
|
|---|
| 534 | return;
|
|---|
| 535 | }
|
|---|
| 536 | if (typeof output.code === "undefined") {
|
|---|
| 537 | compilation.errors.push(new Error(`${name} from Terser plugin\nMinimizer doesn't return result`));
|
|---|
| 538 | }
|
|---|
| 539 | if (output.warnings && output.warnings.length > 0) {
|
|---|
| 540 | output.warnings = output.warnings.map(
|
|---|
| 541 | /**
|
|---|
| 542 | * @param {Error | string} item a warning
|
|---|
| 543 | * @returns {Error} built warning with extra info
|
|---|
| 544 | */
|
|---|
| 545 | item => TerserPlugin.buildWarning(item, name));
|
|---|
| 546 | }
|
|---|
| 547 | if (output.errors && output.errors.length > 0) {
|
|---|
| 548 | const hasSourceMap = inputSourceMap && TerserPlugin.isSourceMap(inputSourceMap);
|
|---|
| 549 | output.errors = output.errors.map(
|
|---|
| 550 | /**
|
|---|
| 551 | * @param {Error | string} item an error
|
|---|
| 552 | * @returns {Error} built error with extra info
|
|---|
| 553 | */
|
|---|
| 554 | item => TerserPlugin.buildError(item, name, hasSourceMap ? new (getTraceMapping().TraceMap)(/** @type {RawSourceMap} */
|
|---|
| 555 | inputSourceMap) : undefined, hasSourceMap ? compilation.requestShortener : undefined));
|
|---|
| 556 | }
|
|---|
| 557 | let shebang;
|
|---|
| 558 |
|
|---|
| 559 | // Custom functions can return `undefined` or `null` when the
|
|---|
| 560 | // minimizer only produced warnings, errors or extracted comments
|
|---|
| 561 | if (typeof output.code !== "undefined" && output.code !== null) {
|
|---|
| 562 | if (/** @type {ExtractCommentsObject} */
|
|---|
| 563 | this.options.extractComments.banner !== false && output.extractedComments && output.extractedComments.length > 0 && output.code.startsWith("#!")) {
|
|---|
| 564 | const firstNewlinePosition = output.code.indexOf("\n");
|
|---|
| 565 | shebang = output.code.slice(0, Math.max(0, firstNewlinePosition));
|
|---|
| 566 | output.code = output.code.slice(Math.max(0, firstNewlinePosition + 1));
|
|---|
| 567 | }
|
|---|
| 568 | if (output.map) {
|
|---|
| 569 | output.source = new SourceMapSource(output.code, name, output.map, input, /** @type {RawSourceMap} */
|
|---|
| 570 | inputSourceMap, true);
|
|---|
| 571 | } else {
|
|---|
| 572 | output.source = new RawSource(output.code);
|
|---|
| 573 | }
|
|---|
| 574 | }
|
|---|
| 575 | if (output.extractedComments && output.extractedComments.length > 0) {
|
|---|
| 576 | const commentsFilename = /** @type {ExtractCommentsObject} */
|
|---|
| 577 | this.options.extractComments.filename || "[file].LICENSE.txt[query]";
|
|---|
| 578 | let query = "";
|
|---|
| 579 | let filename = name;
|
|---|
| 580 | const querySplit = filename.indexOf("?");
|
|---|
| 581 | if (querySplit >= 0) {
|
|---|
| 582 | query = filename.slice(querySplit);
|
|---|
| 583 | filename = filename.slice(0, querySplit);
|
|---|
| 584 | }
|
|---|
| 585 | const lastSlashIndex = filename.lastIndexOf("/");
|
|---|
| 586 | const basename = lastSlashIndex === -1 ? filename : filename.slice(lastSlashIndex + 1);
|
|---|
| 587 | const data = {
|
|---|
| 588 | filename,
|
|---|
| 589 | basename,
|
|---|
| 590 | query
|
|---|
| 591 | };
|
|---|
| 592 | output.commentsFilename = compilation.getPath(commentsFilename, data);
|
|---|
| 593 |
|
|---|
| 594 | // Banner only applies when we have a new source to prepend to
|
|---|
| 595 | if (output.source && /** @type {ExtractCommentsObject} */
|
|---|
| 596 | this.options.extractComments.banner !== false) {
|
|---|
| 597 | let banner = /** @type {ExtractCommentsObject} */
|
|---|
| 598 | this.options.extractComments.banner || `For license information please see ${path.relative(path.dirname(name), output.commentsFilename).replace(/\\/g, "/")}`;
|
|---|
| 599 | if (typeof banner === "function") {
|
|---|
| 600 | banner = banner(output.commentsFilename);
|
|---|
| 601 | }
|
|---|
| 602 | if (banner) {
|
|---|
| 603 | output.source = new ConcatSource(shebang ? `${shebang}\n` : "", `/*! ${banner} */\n`, output.source);
|
|---|
| 604 | }
|
|---|
| 605 | }
|
|---|
| 606 | const extractedCommentsString = output.extractedComments.sort().join("\n\n");
|
|---|
| 607 | output.extractedCommentsSource = new RawSource(`${extractedCommentsString}\n`);
|
|---|
| 608 | }
|
|---|
| 609 | await cacheItem.storePromise({
|
|---|
| 610 | source: output.source,
|
|---|
| 611 | errors: output.errors,
|
|---|
| 612 | warnings: output.warnings,
|
|---|
| 613 | commentsFilename: output.commentsFilename,
|
|---|
| 614 | extractedCommentsSource: output.extractedCommentsSource
|
|---|
| 615 | });
|
|---|
| 616 | }
|
|---|
| 617 | if (output.warnings && output.warnings.length > 0) {
|
|---|
| 618 | for (const warning of output.warnings) {
|
|---|
| 619 | compilation.warnings.push(warning);
|
|---|
| 620 | }
|
|---|
| 621 | }
|
|---|
| 622 | if (output.errors && output.errors.length > 0) {
|
|---|
| 623 | for (const error of output.errors) {
|
|---|
| 624 | compilation.errors.push(error);
|
|---|
| 625 | }
|
|---|
| 626 | }
|
|---|
| 627 |
|
|---|
| 628 | // Emit extracted comments file even if the main asset was not
|
|---|
| 629 | // rewritten (some minimizers only produce comments / warnings / errors)
|
|---|
| 630 | if (output.extractedCommentsSource) {
|
|---|
| 631 | allExtractedComments.set(name, {
|
|---|
| 632 | extractedCommentsSource: output.extractedCommentsSource,
|
|---|
| 633 | commentsFilename: (/** @type {string} */output.commentsFilename)
|
|---|
| 634 | });
|
|---|
| 635 | }
|
|---|
| 636 | if (!output.source) {
|
|---|
| 637 | return;
|
|---|
| 638 | }
|
|---|
| 639 |
|
|---|
| 640 | /** @type {AssetInfo} */
|
|---|
| 641 | const newInfo = {
|
|---|
| 642 | minimized: true
|
|---|
| 643 | };
|
|---|
| 644 | if (output.extractedCommentsSource) {
|
|---|
| 645 | newInfo.related = {
|
|---|
| 646 | license: (/** @type {string} */output.commentsFilename)
|
|---|
| 647 | };
|
|---|
| 648 | }
|
|---|
| 649 | compilation.updateAsset(name, output.source, newInfo);
|
|---|
| 650 | });
|
|---|
| 651 | }
|
|---|
| 652 | const limit = getWorker && numberOfAssets > 0 ? (/** @type {number} */numberOfWorkers) : scheduledTasks.length;
|
|---|
| 653 | await throttleAll(limit, scheduledTasks);
|
|---|
| 654 | if (initializedWorker) {
|
|---|
| 655 | await initializedWorker.end();
|
|---|
| 656 | }
|
|---|
| 657 |
|
|---|
| 658 | /** @typedef {{ source: import("webpack").sources.Source, commentsFilename: string, from: string }} ExtractedCommentsInfoWithFrom */
|
|---|
| 659 | await [...allExtractedComments].sort().reduce(
|
|---|
| 660 | /**
|
|---|
| 661 | * @param {Promise<unknown>} previousPromise previous result
|
|---|
| 662 | * @param {[string, ExtractedCommentsInfo]} extractedComments extracted comments
|
|---|
| 663 | * @returns {Promise<ExtractedCommentsInfoWithFrom>} extract comments with info
|
|---|
| 664 | */
|
|---|
| 665 | async (previousPromise, [from, value]) => {
|
|---|
| 666 | const previous = /** @type {ExtractedCommentsInfoWithFrom | undefined} * */
|
|---|
| 667 | await previousPromise;
|
|---|
| 668 | const {
|
|---|
| 669 | commentsFilename,
|
|---|
| 670 | extractedCommentsSource
|
|---|
| 671 | } = value;
|
|---|
| 672 | if (previous && previous.commentsFilename === commentsFilename) {
|
|---|
| 673 | const {
|
|---|
| 674 | from: previousFrom,
|
|---|
| 675 | source: prevSource
|
|---|
| 676 | } = previous;
|
|---|
| 677 | const mergedName = `${previousFrom}|${from}`;
|
|---|
| 678 | const name = `${commentsFilename}|${mergedName}`;
|
|---|
| 679 | const eTag = [prevSource, extractedCommentsSource].map(item => cache.getLazyHashedEtag(item)).reduce((previousValue, currentValue) => cache.mergeEtags(previousValue, currentValue));
|
|---|
| 680 | let source = await cache.getPromise(name, eTag);
|
|---|
| 681 | if (!source) {
|
|---|
| 682 | source = new ConcatSource([...new Set([... /** @type {string} */prevSource.source().split("\n\n"), ... /** @type {string} */extractedCommentsSource.source().split("\n\n")])].join("\n\n"));
|
|---|
| 683 | await cache.storePromise(name, eTag, source);
|
|---|
| 684 | }
|
|---|
| 685 | compilation.updateAsset(commentsFilename, source);
|
|---|
| 686 | return {
|
|---|
| 687 | source,
|
|---|
| 688 | commentsFilename,
|
|---|
| 689 | from: mergedName
|
|---|
| 690 | };
|
|---|
| 691 | }
|
|---|
| 692 | const existingAsset = compilation.getAsset(commentsFilename);
|
|---|
| 693 | if (existingAsset) {
|
|---|
| 694 | return {
|
|---|
| 695 | source: existingAsset.source,
|
|---|
| 696 | commentsFilename,
|
|---|
| 697 | from: commentsFilename
|
|---|
| 698 | };
|
|---|
| 699 | }
|
|---|
| 700 | compilation.emitAsset(commentsFilename, extractedCommentsSource, {
|
|---|
| 701 | extractedComments: true
|
|---|
| 702 | });
|
|---|
| 703 | return {
|
|---|
| 704 | source: extractedCommentsSource,
|
|---|
| 705 | commentsFilename,
|
|---|
| 706 | from
|
|---|
| 707 | };
|
|---|
| 708 | }, /** @type {Promise<unknown>} */Promise.resolve());
|
|---|
| 709 | }
|
|---|
| 710 |
|
|---|
| 711 | /**
|
|---|
| 712 | * @param {Compiler} compiler compiler
|
|---|
| 713 | * @returns {void}
|
|---|
| 714 | */
|
|---|
| 715 | apply(compiler) {
|
|---|
| 716 | const pluginName = this.constructor.name;
|
|---|
| 717 | const availableNumberOfCores = TerserPlugin.getAvailableNumberOfCores(this.options.parallel);
|
|---|
| 718 | compiler.hooks.compilation.tap(pluginName, compilation => {
|
|---|
| 719 | const hooks = compiler.webpack.javascript.JavascriptModulesPlugin.getCompilationHooks(compilation);
|
|---|
| 720 | /**
|
|---|
| 721 | * @param {BasicMinimizerImplementation<EXPECTED_ANY> & MinimizeFunctionHelpers} impl implementation
|
|---|
| 722 | * @returns {string} minimizer version or "0.0.0"
|
|---|
| 723 | */
|
|---|
| 724 | const getVersion = impl => typeof impl.getMinimizerVersion !== "undefined" ? impl.getMinimizerVersion() || "0.0.0" : "0.0.0";
|
|---|
| 725 | const data = getSerializeJavascript()({
|
|---|
| 726 | minimizer: Array.isArray(this.options.minimizer.implementation) ? this.options.minimizer.implementation.map(getVersion) : getVersion(/** @type {BasicMinimizerImplementation<EXPECTED_ANY> & MinimizeFunctionHelpers} */
|
|---|
| 727 | this.options.minimizer.implementation),
|
|---|
| 728 | options: this.options.minimizer.options
|
|---|
| 729 | });
|
|---|
| 730 | hooks.chunkHash.tap(pluginName, (chunk, hash) => {
|
|---|
| 731 | hash.update("TerserPlugin");
|
|---|
| 732 | hash.update(data);
|
|---|
| 733 | });
|
|---|
| 734 | compilation.hooks.processAssets.tapPromise({
|
|---|
| 735 | name: pluginName,
|
|---|
| 736 | stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE,
|
|---|
| 737 | additionalAssets: true
|
|---|
| 738 | }, assets => this.optimize(compiler, compilation, assets, {
|
|---|
| 739 | availableNumberOfCores
|
|---|
| 740 | }));
|
|---|
| 741 | compilation.hooks.statsPrinter.tap(pluginName, stats => {
|
|---|
| 742 | stats.hooks.print.for("asset.info.minimized").tap("minimizer-webpack-plugin", (minimized, {
|
|---|
| 743 | green,
|
|---|
| 744 | formatFlag
|
|---|
| 745 | }) => minimized ? /** @type {(text: string) => string} */green(/** @type {(flag: string) => string} */formatFlag("minimized")) : "");
|
|---|
| 746 | });
|
|---|
| 747 | });
|
|---|
| 748 | }
|
|---|
| 749 | }
|
|---|
| 750 | TerserPlugin.terserMinify = terserMinify;
|
|---|
| 751 | TerserPlugin.uglifyJsMinify = uglifyJsMinify;
|
|---|
| 752 | TerserPlugin.swcMinify = swcMinify;
|
|---|
| 753 | TerserPlugin.esbuildMinify = esbuildMinify;
|
|---|
| 754 | TerserPlugin.jsonMinify = jsonMinify;
|
|---|
| 755 | TerserPlugin.htmlMinifierTerser = htmlMinifierTerser;
|
|---|
| 756 | TerserPlugin.swcMinifyHtml = swcMinifyHtml;
|
|---|
| 757 | TerserPlugin.swcMinifyHtmlFragment = swcMinifyHtmlFragment;
|
|---|
| 758 | TerserPlugin.minifyHtmlNode = minifyHtmlNode;
|
|---|
| 759 | TerserPlugin.cssnanoMinify = cssnanoMinify;
|
|---|
| 760 | TerserPlugin.cssoMinify = cssoMinify;
|
|---|
| 761 | TerserPlugin.cleanCssMinify = cleanCssMinify;
|
|---|
| 762 | TerserPlugin.esbuildMinifyCss = esbuildMinifyCss;
|
|---|
| 763 | TerserPlugin.lightningCssMinify = lightningCssMinify;
|
|---|
| 764 | TerserPlugin.swcMinifyCss = swcMinifyCss;
|
|---|
| 765 | module.exports = TerserPlugin; |
|---|