index.js 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014
  1. "use strict";
  2. const os = require("os");
  3. const path = require("path");
  4. const {
  5. minify
  6. } = require("./minify");
  7. const {
  8. cleanCssMinify,
  9. cssnanoMinify,
  10. cssoMinify,
  11. esbuildMinify,
  12. esbuildMinifyCss,
  13. getEcmaVersion,
  14. getMinimizerOptionsAt,
  15. htmlMinifierTerser,
  16. jsonMinify,
  17. lightningCssMinify,
  18. memoize,
  19. minifyHtmlNode,
  20. swcMinify,
  21. swcMinifyCss,
  22. swcMinifyHtml,
  23. swcMinifyHtmlFragment,
  24. terserMinify,
  25. throttleAll,
  26. uglifyJsMinify
  27. } = require("./utils");
  28. /** @typedef {import("schema-utils/declarations/validate").Schema} Schema */
  29. /** @typedef {import("webpack").Compiler} Compiler */
  30. /** @typedef {import("webpack").Compilation} Compilation */
  31. /** @typedef {import("webpack").Asset} Asset */
  32. /** @typedef {import("webpack").AssetInfo} AssetInfo */
  33. /** @typedef {import("webpack").TemplatePath} TemplatePath */
  34. /** @typedef {import("jest-worker").Worker} JestWorker */
  35. /** @typedef {import("@jridgewell/trace-mapping").EncodedSourceMap & { sources: string[], sourcesContent?: string[], file: string }} RawSourceMap */
  36. /** @typedef {import("@jridgewell/trace-mapping").TraceMap} TraceMap */
  37. /** @typedef {RegExp | string} Rule */
  38. /** @typedef {Rule[] | Rule} Rules */
  39. // eslint-disable-next-line jsdoc/reject-any-type
  40. /** @typedef {any} EXPECTED_ANY */
  41. // eslint-disable-next-line jsdoc/require-property
  42. /** @typedef {object} EXPECTED_OBJECT */
  43. /**
  44. * @callback ExtractCommentsFunction
  45. * @param {EXPECTED_ANY} astNode ast Node
  46. * @param {{ value: string, type: "comment1" | "comment2" | "comment3" | "comment4", pos: number, line: number, col: number }} comment comment node
  47. * @returns {boolean} true when need to extract comment, otherwise false
  48. */
  49. /**
  50. * @typedef {boolean | "all" | "some" | RegExp | ExtractCommentsFunction} ExtractCommentsCondition
  51. */
  52. /**
  53. * @typedef {TemplatePath} ExtractCommentsFilename
  54. */
  55. /**
  56. * @typedef {boolean | string | ((commentsFile: string) => string)} ExtractCommentsBanner
  57. */
  58. /**
  59. * @typedef {object} ExtractCommentsObject
  60. * @property {ExtractCommentsCondition=} condition condition which comments need to be expected
  61. * @property {ExtractCommentsFilename=} filename filename for extracted comments
  62. * @property {ExtractCommentsBanner=} banner banner in filename for extracted comments
  63. */
  64. /**
  65. * @typedef {ExtractCommentsCondition | ExtractCommentsObject} ExtractCommentsOptions
  66. */
  67. /**
  68. * @typedef {object} ErrorObject
  69. * @property {string} message message
  70. * @property {number=} line line number
  71. * @property {number=} column column number
  72. * @property {string=} stack error stack trace
  73. */
  74. /**
  75. * What one embedded source is and where it is going, as
  76. * `renderEmbeddedSource` describes it.
  77. * @typedef {object} EmbeddedSourceInfo
  78. * @property {string} type the embedded source's language, e.g. `"css"`
  79. * @property {string} hostType the language of the output it is embedded in
  80. * @property {import("webpack").Module} module the module being generated
  81. */
  82. /**
  83. * The two hooks webpack >= 5.110 adds. Declared here rather than read off
  84. * `Compilation`: the plugin supports webpack `^5.1.0`, whose types have
  85. * neither, and it does nothing at all where they are absent.
  86. * @typedef {object} EmbeddedSourceHooks
  87. * @property {{ tapPromise: (name: string, fn: (source: import("webpack").sources.Source, info: EmbeddedSourceInfo) => Promise<import("webpack").sources.Source>) => void }=} renderEmbeddedSource offers each embedded source before it is embedded
  88. * @property {{ tap: (name: string, fn: (module: import("webpack").Module, hash: { update: (data: string) => void }) => void) => void }=} embeddedSourceHash hashes what a `renderEmbeddedSource` tap varies on
  89. */
  90. /**
  91. * @typedef {object} MinimizedResult
  92. * @property {string=} code code
  93. * @property {RawSourceMap=} map source map
  94. * @property {(Error | string)[]=} errors errors
  95. * @property {(Error | string)[]=} warnings warnings
  96. * @property {string[]=} extractedComments extracted comments
  97. */
  98. /**
  99. * @typedef {{ [file: string]: string }} Input
  100. */
  101. /**
  102. * @typedef {{ [key: string]: EXPECTED_ANY }} CustomOptions
  103. */
  104. /**
  105. * @template T
  106. * @typedef {T extends infer U ? U : CustomOptions} InferDefaultType
  107. */
  108. /**
  109. * @template T
  110. * @typedef {T extends EXPECTED_ANY[] ? { [P in keyof T]?: T[P] & InferDefaultType<T[P]> } : T & InferDefaultType<T>} MinimizerOptions
  111. */
  112. /**
  113. * @template T
  114. * @callback BasicMinimizerImplementation
  115. * @param {Input} input
  116. * @param {RawSourceMap | undefined} sourceMap
  117. * @param {MinimizerOptions<T>} minifyOptions
  118. * @param {ExtractCommentsOptions | undefined} extractComments
  119. * @returns {Promise<MinimizedResult> | MinimizedResult}
  120. */
  121. /**
  122. * @typedef {object} MinimizeFunctionHelpers
  123. * @property {() => string | undefined=} getMinimizerVersion function that returns version of minimizer
  124. * @property {() => boolean | undefined=} supportsWorkerThreads true when minimizer support worker threads, otherwise false
  125. * @property {() => boolean | undefined=} supportsWorker true when minimizer support worker, otherwise false
  126. * @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.
  127. * @property {() => string[] | undefined=} getTypes the languages this minimizer minifies, e.g. `["css"]`. Source that carries no filename — what a module embeds in another language's output — is dispatched by this rather than by `test` / `filter`, and a minimizer that declares nothing is never handed any
  128. * @property {(minimizerOptions?: EXPECTED_OBJECT) => string[] | undefined=} getEmbeddedTypes the languages this minimizer can hand out from inside what it minifies, through the `renderEmbeddedSource` option. Empty (or absent) means it nests nothing a caller can reach, and the option is not passed
  129. */
  130. /**
  131. * @template T
  132. * @typedef {T extends EXPECTED_ANY[] ? { [P in keyof T]: BasicMinimizerImplementation<T[P]> & MinimizeFunctionHelpers } : BasicMinimizerImplementation<T> & MinimizeFunctionHelpers} MinimizerImplementation
  133. */
  134. /**
  135. * @template T
  136. * @typedef {object} InternalOptions
  137. * @property {string} name name
  138. * @property {string} input input
  139. * @property {RawSourceMap | undefined} inputSourceMap input source map
  140. * @property {ExtractCommentsOptions | undefined} extractComments extract comments option
  141. * @property {{ implementation: MinimizerImplementation<T>, options: MinimizerOptions<T> }} minimizer minimizer
  142. * @property {{ implementation: MinimizerImplementation<T>, options: MinimizerOptions<T>, claims: string[][], offers: string[][], at: number[] }=} embedded every configured minimizer, for source one language embeds in another: it carries no filename, so `minimizer` — which holds only what this asset's name matched — is not the set to dispatch it across. `claims` is the languages each minifies and `offers` the languages each can hand out, both as data and both parallel to `implementation`, since a minify function reaches a worker as source and carries none of its properties; `at` says which of them `minimizer` holds. Absent when no nested language is reachable at all
  143. * @property {boolean=} module true when code is a EC module, otherwise false
  144. * @property {number | string=} ecma ecma version
  145. */
  146. /**
  147. * @template T
  148. * @typedef {JestWorker & { transform: (options: string) => Promise<MinimizedResult>, minify: (options: InternalOptions<T>) => Promise<MinimizedResult> }} MinimizerWorker
  149. */
  150. /**
  151. * @typedef {undefined | boolean | number} Parallel
  152. */
  153. /**
  154. * @typedef {object} BasePluginOptions
  155. * @property {Rules=} test test rule
  156. * @property {Rules=} include include rile
  157. * @property {Rules=} exclude exclude rule
  158. * @property {ExtractCommentsOptions=} extractComments extract comments options
  159. * @property {Parallel=} parallel parallel option
  160. */
  161. /**
  162. * @template T
  163. * @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
  164. */
  165. /**
  166. * @template T
  167. * @typedef {BasePluginOptions & { minimizer: { implementation: MinimizerImplementation<T>, options: MinimizerOptions<T> } }} InternalPluginOptions
  168. */
  169. const VALIDATION_CONFIGURATION = {
  170. name: "Terser Plugin",
  171. baseDataPath: "options"
  172. };
  173. const getTraceMapping = memoize(() => require("@jridgewell/trace-mapping"));
  174. const getSerializeJavascript = memoize(() => require("./serialize-javascript"));
  175. /**
  176. * @template [T=import("terser").MinifyOptions]
  177. */
  178. class TerserPlugin {
  179. /**
  180. * @param {BasePluginOptions & DefinedDefaultMinimizerAndOptions<T>=} options options
  181. */
  182. constructor(options) {
  183. // Kept for `apply()`, which is where validation runs now: webpack owns when
  184. // it happens, and skips it entirely for `validate: false`.
  185. /**
  186. * @private
  187. * @type {BasePluginOptions & DefinedDefaultMinimizerAndOptions<T>}
  188. */
  189. this.rawOptions = /** @type {BasePluginOptions & DefinedDefaultMinimizerAndOptions<T>} */
  190. options || {};
  191. // TODO handle json and etc in the next major release
  192. // TODO make `minimizer` option instead `minify` and `terserOptions` in the next major release, also rename `terserMinify` to `terserMinimize`
  193. const {
  194. minify = (/** @type {MinimizerImplementation<T>} */
  195. /** @type {unknown} */terserMinify),
  196. minimizerOptions,
  197. terserOptions,
  198. test = /\.[cm]?js(\?.*)?$/i,
  199. extractComments = true,
  200. parallel = true,
  201. include,
  202. exclude
  203. } = this.rawOptions;
  204. // `terserOptions` is a deprecated alias of `minimizerOptions`; prefer the
  205. // new name when both are provided.
  206. const resolvedMinimizerOptions = /** @type {MinimizerOptions<T>} */
  207. typeof minimizerOptions !== "undefined" ? minimizerOptions : terserOptions || {};
  208. /**
  209. * @private
  210. * @type {InternalPluginOptions<T>}
  211. */
  212. this.options = {
  213. test,
  214. extractComments,
  215. parallel,
  216. include,
  217. exclude,
  218. minimizer: {
  219. implementation: minify,
  220. options: resolvedMinimizerOptions
  221. }
  222. };
  223. }
  224. /**
  225. * @private
  226. * @param {unknown} input Input to check
  227. * @returns {boolean} Whether input is a source map
  228. */
  229. static isSourceMap(input) {
  230. // All required options for `new TraceMap(...options)`
  231. // https://github.com/jridgewell/trace-mapping#usage
  232. 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");
  233. }
  234. /**
  235. * @private
  236. * @param {unknown} warning warning
  237. * @param {string} file file
  238. * @returns {Error} built warning
  239. */
  240. static buildWarning(warning, file) {
  241. /**
  242. * @type {Error & { hideStack: true, file: string }}
  243. */
  244. // @ts-expect-error
  245. const builtWarning = new Error(warning.toString());
  246. builtWarning.name = "Warning";
  247. builtWarning.hideStack = true;
  248. builtWarning.file = file;
  249. return builtWarning;
  250. }
  251. /**
  252. * @private
  253. * @param {Error | ErrorObject | string} error error
  254. * @param {string} file file
  255. * @param {TraceMap=} sourceMap source map
  256. * @param {Compilation["requestShortener"]=} requestShortener request shortener
  257. * @returns {Error} built error
  258. */
  259. static buildError(error, file, sourceMap, requestShortener) {
  260. /**
  261. * @type {Error & { file?: string }}
  262. */
  263. let builtError;
  264. if (typeof error === "string") {
  265. builtError = new Error(`${file} from Terser plugin\n${error}`);
  266. builtError.file = file;
  267. return builtError;
  268. }
  269. if (/** @type {ErrorObject} */error.line) {
  270. const {
  271. line,
  272. column
  273. } = /** @type {ErrorObject & { line: number, column: number }} */error;
  274. const original = sourceMap && getTraceMapping().originalPositionFor(sourceMap, {
  275. line,
  276. column
  277. });
  278. if (original && original.source && requestShortener) {
  279. 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")}` : ""}`);
  280. builtError.file = file;
  281. return builtError;
  282. }
  283. builtError = new Error(`${file} from Terser plugin\n${error.message} [${file}:${line},${column}]${error.stack ? `\n${error.stack.split("\n").slice(1).join("\n")}` : ""}`);
  284. builtError.file = file;
  285. return builtError;
  286. }
  287. if (error.stack) {
  288. builtError = new Error(`${file} from Terser plugin\n${typeof error.message !== "undefined" ? error.message : ""}\n${error.stack}`);
  289. builtError.file = file;
  290. return builtError;
  291. }
  292. builtError = new Error(`${file} from Terser plugin\n${error.message}`);
  293. builtError.file = file;
  294. return builtError;
  295. }
  296. /**
  297. * @private
  298. * @param {Parallel} parallel value of the `parallel` option
  299. * @returns {number} number of cores for parallelism
  300. */
  301. static getAvailableNumberOfCores(parallel) {
  302. // In some cases cpus() returns undefined
  303. // https://github.com/nodejs/node/issues/19022
  304. const cpus =
  305. // eslint-disable-next-line n/no-unsupported-features/node-builtins
  306. typeof os.availableParallelism === "function" ?
  307. // eslint-disable-next-line n/no-unsupported-features/node-builtins
  308. {
  309. length: os.availableParallelism()
  310. } : os.cpus() || {
  311. length: 1
  312. };
  313. return parallel === true || typeof parallel === "undefined" ? cpus.length - 1 : Math.min(parallel || 0, cpus.length - 1);
  314. }
  315. /**
  316. * @private
  317. * @param {Compiler} compiler compiler
  318. * @param {Compilation} compilation compilation
  319. * @param {Record<string, import("webpack").sources.Source>} assets assets
  320. * @param {{ availableNumberOfCores: number }} optimizeOptions optimize options
  321. * @returns {Promise<void>}
  322. */
  323. async optimize(compiler, compilation, assets, optimizeOptions) {
  324. const cache = compilation.getCache("TerserWebpackPlugin");
  325. let numberOfAssets = 0;
  326. // Normalize the implementation list to an array so dispatch and the
  327. // worker-pool capability checks below can iterate uniformly. The
  328. // original shape on `this.options.minimizer.implementation` is preserved
  329. // for chunk hashing.
  330. const implementations = Array.isArray(this.options.minimizer.implementation) ? this.options.minimizer.implementation : [this.options.minimizer.implementation];
  331. /**
  332. * Collect the indices of minimizers whose `filter` accepts `name`.
  333. * Filters returning `undefined` are treated as accept (matches the
  334. * convention used by `supportsWorkerThreads`).
  335. * @param {string} name asset name
  336. * @param {AssetInfo} info asset info
  337. * @returns {number[]} indices into `implementations` that accept the asset
  338. */
  339. const matchingMinimizers = (name, info) => {
  340. const matched = [];
  341. for (let i = 0; i < implementations.length; i++) {
  342. const impl = implementations[i];
  343. if (typeof impl.filter !== "function" ||
  344. // eslint-disable-next-line unicorn/no-array-method-this-argument
  345. impl.filter(name, info) !== false) {
  346. matched.push(i);
  347. }
  348. }
  349. return matched;
  350. };
  351. /** @type {Map<string, number[]>} */
  352. const matchedByName = new Map();
  353. const assetsForMinify = await Promise.all(Object.keys(assets).filter(name => {
  354. const {
  355. info
  356. } = /** @type {Asset} */compilation.getAsset(name);
  357. if (
  358. // Skip double minimize assets from child compilation
  359. info.minimized ||
  360. // Skip minimizing for extracted comments assets
  361. info.extractedComments) {
  362. return false;
  363. }
  364. if (!compiler.webpack.ModuleFilenameHelpers.matchObject.bind(undefined, this.options)(name)) {
  365. return false;
  366. }
  367. // Compute the matching minimizers once and carry the result to the
  368. // per-asset task via `matchedByName` so the regexes don't run again.
  369. const matched = matchingMinimizers(name, info);
  370. if (matched.length === 0) {
  371. return false;
  372. }
  373. matchedByName.set(name, matched);
  374. return true;
  375. }).map(async name => {
  376. const {
  377. info,
  378. source
  379. } = /** @type {Asset} */
  380. compilation.getAsset(name);
  381. const eTag = cache.getLazyHashedEtag(source);
  382. const cacheItem = cache.getItemCache(name, eTag);
  383. const output = await cacheItem.getPromise();
  384. if (!output) {
  385. numberOfAssets += 1;
  386. }
  387. return {
  388. name,
  389. info,
  390. inputSource: source,
  391. output,
  392. cacheItem,
  393. matched: (/** @type {number[]} */matchedByName.get(name))
  394. };
  395. }));
  396. if (assetsForMinify.length === 0) {
  397. return;
  398. }
  399. /** @type {undefined | (() => MinimizerWorker<T>)} */
  400. let getWorker;
  401. /** @type {undefined | MinimizerWorker<T>} */
  402. let initializedWorker;
  403. /** @type {undefined | number} */
  404. let numberOfWorkers;
  405. const needCreateWorker = optimizeOptions.availableNumberOfCores > 0 && implementations.every(impl => typeof impl.supportsWorker === "undefined" || typeof impl.supportsWorker === "function" && impl.supportsWorker());
  406. if (needCreateWorker) {
  407. // Do not create unnecessary workers when the number of files is less than the available cores, it saves memory
  408. numberOfWorkers = Math.min(numberOfAssets, optimizeOptions.availableNumberOfCores);
  409. getWorker = () => {
  410. if (initializedWorker) {
  411. return initializedWorker;
  412. }
  413. const {
  414. Worker
  415. } = require("jest-worker");
  416. initializedWorker = /** @type {MinimizerWorker<T>} */
  417. new Worker(require.resolve("./minify"), {
  418. numWorkers: numberOfWorkers,
  419. enableWorkerThreads: implementations.every(impl => typeof impl.supportsWorkerThreads === "undefined" || impl.supportsWorkerThreads() !== false)
  420. });
  421. // https://github.com/facebook/jest/issues/8872#issuecomment-524822081
  422. const workerStdout = initializedWorker.getStdout();
  423. if (workerStdout) {
  424. workerStdout.on("data", chunk => process.stdout.write(chunk));
  425. }
  426. const workerStderr = initializedWorker.getStderr();
  427. if (workerStderr) {
  428. workerStderr.on("data", chunk => process.stderr.write(chunk));
  429. }
  430. return initializedWorker;
  431. };
  432. }
  433. const {
  434. SourceMapSource,
  435. ConcatSource,
  436. RawSource
  437. } = compiler.webpack.sources;
  438. /**
  439. * @param {InternalOptions<T>} options what to minify
  440. * @returns {Promise<MinimizedResult>} the result
  441. */
  442. const run = options => getWorker ? getWorker().transform(getSerializeJavascript()(options)) : minify(options);
  443. /** @typedef {{ extractedCommentsSource: import("webpack").sources.RawSource, commentsFilename: string }} ExtractedCommentsInfo */
  444. /** @type {Map<string, ExtractedCommentsInfo>} */
  445. const allExtractedComments = new Map();
  446. const scheduledTasks = [];
  447. for (const asset of assetsForMinify) {
  448. scheduledTasks.push(async () => {
  449. const {
  450. name,
  451. inputSource,
  452. info,
  453. cacheItem,
  454. matched
  455. } = asset;
  456. let {
  457. output
  458. } = asset;
  459. if (!output) {
  460. let input;
  461. /** @type {RawSourceMap | undefined} */
  462. let inputSourceMap;
  463. const {
  464. source: sourceFromInputSource,
  465. map
  466. } = inputSource.sourceAndMap();
  467. input = sourceFromInputSource;
  468. if (map) {
  469. if (!TerserPlugin.isSourceMap(map)) {
  470. compilation.warnings.push(new Error(`${name} contains invalid source map`));
  471. } else {
  472. inputSourceMap = /** @type {RawSourceMap} */map;
  473. }
  474. }
  475. if (Buffer.isBuffer(input)) {
  476. input = input.toString();
  477. }
  478. // Dispatch to only the minimizers whose `filter` accepted this
  479. // asset (computed once when collecting `assetsForMinify`).
  480. // `minify.js` already normalizes a single implementation into a
  481. // one-element array, so we always hand it the matching subset.
  482. // Options are sliced as references — `minify.js` overlays
  483. // `module`/`ecma` without mutating the caller's object.
  484. const assetImplementation = /** @type {MinimizerImplementation<T>} */
  485. matched.map(i => implementations[i]);
  486. const sourceOptions = this.options.minimizer.options;
  487. const assetMinimizerOptions = /** @type {MinimizerOptions<T>} */
  488. Array.isArray(sourceOptions) ? matched.map(i => sourceOptions[i] || {}) : sourceOptions;
  489. /**
  490. * @type {InternalOptions<T>}
  491. */
  492. const options = {
  493. name,
  494. input,
  495. inputSourceMap,
  496. minimizer: {
  497. implementation: assetImplementation,
  498. options: assetMinimizerOptions
  499. },
  500. extractComments: this.options.extractComments,
  501. embedded: this.embeddedMinimizer(matched)
  502. };
  503. if (typeof info.javascriptModule !== "undefined") {
  504. options.module = info.javascriptModule;
  505. } else if (/\.mjs(\?.*)?$/i.test(name)) {
  506. options.module = true;
  507. } else if (/\.cjs(\?.*)?$/i.test(name)) {
  508. options.module = false;
  509. }
  510. options.ecma = getEcmaVersion(compiler.options.output.environment);
  511. try {
  512. output = await run(options);
  513. } catch (error) {
  514. const hasSourceMap = inputSourceMap && TerserPlugin.isSourceMap(inputSourceMap);
  515. compilation.errors.push(TerserPlugin.buildError(/** @type {Error | ErrorObject | string} */
  516. error, name, hasSourceMap ? new (getTraceMapping().TraceMap)(/** @type {RawSourceMap} */
  517. inputSourceMap) : undefined, hasSourceMap ? compilation.requestShortener : undefined));
  518. return;
  519. }
  520. if (typeof output.code === "undefined") {
  521. compilation.errors.push(new Error(`${name} from Terser plugin\nMinimizer doesn't return result`));
  522. }
  523. if (output.warnings && output.warnings.length > 0) {
  524. output.warnings = output.warnings.map(
  525. /**
  526. * @param {Error | string} item a warning
  527. * @returns {Error} built warning with extra info
  528. */
  529. item => TerserPlugin.buildWarning(item, name));
  530. }
  531. if (output.errors && output.errors.length > 0) {
  532. const hasSourceMap = inputSourceMap && TerserPlugin.isSourceMap(inputSourceMap);
  533. output.errors = output.errors.map(
  534. /**
  535. * @param {Error | string} item an error
  536. * @returns {Error} built error with extra info
  537. */
  538. item => TerserPlugin.buildError(item, name, hasSourceMap ? new (getTraceMapping().TraceMap)(/** @type {RawSourceMap} */
  539. inputSourceMap) : undefined, hasSourceMap ? compilation.requestShortener : undefined));
  540. }
  541. let shebang;
  542. // Custom functions can return `undefined` or `null` when the
  543. // minimizer only produced warnings, errors or extracted comments
  544. if (typeof output.code !== "undefined" && output.code !== null) {
  545. if (/** @type {ExtractCommentsObject} */
  546. this.options.extractComments.banner !== false && output.extractedComments && output.extractedComments.length > 0 && output.code.startsWith("#!")) {
  547. const firstNewlinePosition = output.code.indexOf("\n");
  548. shebang = output.code.slice(0, Math.max(0, firstNewlinePosition));
  549. output.code = output.code.slice(Math.max(0, firstNewlinePosition + 1));
  550. }
  551. if (output.map) {
  552. output.source = new SourceMapSource(output.code, name, output.map, input, /** @type {RawSourceMap} */
  553. inputSourceMap, true);
  554. } else {
  555. output.source = new RawSource(output.code);
  556. }
  557. }
  558. if (output.extractedComments && output.extractedComments.length > 0) {
  559. const commentsFilename = /** @type {ExtractCommentsObject} */
  560. this.options.extractComments.filename || "[file].LICENSE.txt[query]";
  561. let query = "";
  562. let filename = name;
  563. const querySplit = filename.indexOf("?");
  564. if (querySplit >= 0) {
  565. query = filename.slice(querySplit);
  566. filename = filename.slice(0, querySplit);
  567. }
  568. const lastSlashIndex = filename.lastIndexOf("/");
  569. const basename = lastSlashIndex === -1 ? filename : filename.slice(lastSlashIndex + 1);
  570. const data = {
  571. filename,
  572. basename,
  573. query
  574. };
  575. output.commentsFilename = compilation.getPath(commentsFilename, data);
  576. // Banner only applies when we have a new source to prepend to
  577. if (output.source && /** @type {ExtractCommentsObject} */
  578. this.options.extractComments.banner !== false) {
  579. let banner = /** @type {ExtractCommentsObject} */
  580. this.options.extractComments.banner || `For license information please see ${path.relative(path.dirname(name), output.commentsFilename).replace(/\\/g, "/")}`;
  581. if (typeof banner === "function") {
  582. banner = banner(output.commentsFilename);
  583. }
  584. if (banner) {
  585. output.source = new ConcatSource(shebang ? `${shebang}\n` : "", `/*! ${banner} */\n`, output.source);
  586. }
  587. }
  588. const extractedCommentsString = output.extractedComments.sort().join("\n\n");
  589. output.extractedCommentsSource = new RawSource(`${extractedCommentsString}\n`);
  590. }
  591. await cacheItem.storePromise({
  592. source: output.source,
  593. errors: output.errors,
  594. warnings: output.warnings,
  595. commentsFilename: output.commentsFilename,
  596. extractedCommentsSource: output.extractedCommentsSource
  597. });
  598. }
  599. if (output.warnings && output.warnings.length > 0) {
  600. for (const warning of output.warnings) {
  601. compilation.warnings.push(warning);
  602. }
  603. }
  604. if (output.errors && output.errors.length > 0) {
  605. for (const error of output.errors) {
  606. compilation.errors.push(error);
  607. }
  608. }
  609. // Emit extracted comments file even if the main asset was not
  610. // rewritten (some minimizers only produce comments / warnings / errors)
  611. if (output.extractedCommentsSource) {
  612. allExtractedComments.set(name, {
  613. extractedCommentsSource: output.extractedCommentsSource,
  614. commentsFilename: (/** @type {string} */output.commentsFilename)
  615. });
  616. }
  617. if (!output.source) {
  618. return;
  619. }
  620. /** @type {AssetInfo} */
  621. const newInfo = {
  622. minimized: true
  623. };
  624. if (output.extractedCommentsSource) {
  625. newInfo.related = {
  626. license: (/** @type {string} */output.commentsFilename)
  627. };
  628. }
  629. compilation.updateAsset(name, output.source, newInfo);
  630. });
  631. }
  632. const limit = getWorker && numberOfAssets > 0 ? (/** @type {number} */numberOfWorkers) : scheduledTasks.length;
  633. await throttleAll(limit, scheduledTasks);
  634. if (initializedWorker) {
  635. await initializedWorker.end();
  636. }
  637. /** @typedef {{ source: import("webpack").sources.Source, commentsFilename: string, from: string }} ExtractedCommentsInfoWithFrom */
  638. await [...allExtractedComments].sort().reduce(
  639. /**
  640. * @param {Promise<unknown>} previousPromise previous result
  641. * @param {[string, ExtractedCommentsInfo]} extractedComments extracted comments
  642. * @returns {Promise<ExtractedCommentsInfoWithFrom>} extract comments with info
  643. */
  644. async (previousPromise, [from, value]) => {
  645. const previous = /** @type {ExtractedCommentsInfoWithFrom | undefined} * */
  646. await previousPromise;
  647. const {
  648. commentsFilename,
  649. extractedCommentsSource
  650. } = value;
  651. if (previous && previous.commentsFilename === commentsFilename) {
  652. const {
  653. from: previousFrom,
  654. source: prevSource
  655. } = previous;
  656. const mergedName = `${previousFrom}|${from}`;
  657. const name = `${commentsFilename}|${mergedName}`;
  658. const eTag = [prevSource, extractedCommentsSource].map(item => cache.getLazyHashedEtag(item)).reduce((previousValue, currentValue) => cache.mergeEtags(previousValue, currentValue));
  659. let source = await cache.getPromise(name, eTag);
  660. if (!source) {
  661. source = new ConcatSource([...new Set([... /** @type {string} */prevSource.source().split("\n\n"), ... /** @type {string} */extractedCommentsSource.source().split("\n\n")])].join("\n\n"));
  662. await cache.storePromise(name, eTag, source);
  663. }
  664. compilation.updateAsset(commentsFilename, source);
  665. return {
  666. source,
  667. commentsFilename,
  668. from: mergedName
  669. };
  670. }
  671. const existingAsset = compilation.getAsset(commentsFilename);
  672. if (existingAsset) {
  673. return {
  674. source: existingAsset.source,
  675. commentsFilename,
  676. from: commentsFilename
  677. };
  678. }
  679. compilation.emitAsset(commentsFilename, extractedCommentsSource, {
  680. extractedComments: true
  681. });
  682. return {
  683. source: extractedCommentsSource,
  684. commentsFilename,
  685. from
  686. };
  687. }, /** @type {Promise<unknown>} */Promise.resolve());
  688. }
  689. /**
  690. * Every configured minimizer, in order. The `minify` option takes one or an
  691. * array; embedded source is dispatched across all of them either way.
  692. * @private
  693. * @returns {(BasicMinimizerImplementation<EXPECTED_ANY> & MinimizeFunctionHelpers)[]} the minimizers
  694. */
  695. minimizers() {
  696. const {
  697. implementation
  698. } = this.options.minimizer;
  699. return /** @type {(BasicMinimizerImplementation<EXPECTED_ANY> & MinimizeFunctionHelpers)[]} */ /** @type {unknown} */Array.isArray(implementation) ? implementation : [implementation];
  700. }
  701. /**
  702. * Every configured minimizer and its options, for dispatching source one
  703. * language embeds in another. The asset's own entry holds only what its
  704. * filename matched, and a language's minimizer need not be among them — a
  705. * `.css` asset embedding an `<svg>` reaches an SVG minifier that claims no
  706. * asset at all.
  707. * @private
  708. * @param {number[]} matched indices of the minimizers this input's own entry holds
  709. * @returns {{ implementation: MinimizerImplementation<T>, options: MinimizerOptions<T>, claims: string[][], offers: string[][], at: number[] } | undefined} every configured minimizer, or undefined when nothing nested could be reached
  710. */
  711. embeddedMinimizer(matched) {
  712. const minimizers = this.minimizers();
  713. // What each declares travels as data, not on the function: a minify function
  714. // reaches a worker as its source, which carries none of its properties.
  715. const claims = minimizers.map(minimizer => typeof minimizer.getTypes === "function" ? minimizer.getTypes() || [] : []);
  716. const offers = minimizers.map((minimizer, i) => {
  717. const {
  718. getEmbeddedTypes
  719. } = minimizer;
  720. return typeof getEmbeddedTypes === "function" ? getEmbeddedTypes(getMinimizerOptionsAt(this.options.minimizer.options, i)) || [] : [];
  721. });
  722. // Both declarations have to meet, or offering a body would only ever reach
  723. // one with nowhere to go.
  724. if (!matched.some(i => offers[i].some(type => claims.some(claimed => claimed.includes(type))))) {
  725. return undefined;
  726. }
  727. return {
  728. implementation: (/** @type {MinimizerImplementation<T>} */
  729. /** @type {unknown} */minimizers),
  730. options: (/** @type {MinimizerOptions<T>} */
  731. /** @type {unknown} */
  732. minimizers.map((_, i) => getMinimizerOptionsAt(this.options.minimizer.options, i))),
  733. claims,
  734. offers,
  735. // Which of them this input's own entry holds, so the languages it can
  736. // offer are read off the same arrays after a round trip through a worker.
  737. at: matched
  738. };
  739. }
  740. /**
  741. * Minify one source a module embeds in another language's output — CSS or
  742. * HTML reaching the bundle inside a JavaScript string literal, an
  743. * `asset/source` file's text, an `asset/inline` payload. No asset carries
  744. * this text, so there is no filename to dispatch by: it goes to whichever
  745. * minimizer declares `info.type` among the languages it minifies.
  746. * @private
  747. * @param {Compiler} compiler compiler
  748. * @param {Compilation} compilation compilation
  749. * @param {import("webpack").sources.Source} variesOn everything the minified answer varies on beyond the source itself
  750. * @param {import("webpack").sources.Source} source the embedded source
  751. * @param {EmbeddedSourceInfo} info what it is and where it is going
  752. * @returns {Promise<import("webpack").sources.Source>} the minified source, or the original
  753. */
  754. async renderEmbeddedSource(compiler, compilation, variesOn, source, info) {
  755. const {
  756. type,
  757. hostType,
  758. module
  759. } = info;
  760. const minimizers = this.minimizers();
  761. const matched = [];
  762. // A minimizer that declares nothing takes no embedded source: such source
  763. // carries no filename to guess from, and guessing is what `getTypes`
  764. // replaces.
  765. for (let i = 0; i < minimizers.length; i++) {
  766. const {
  767. getTypes
  768. } = minimizers[i];
  769. if (typeof getTypes === "function" && (getTypes() || []).includes(type)) {
  770. matched.push(i);
  771. }
  772. }
  773. if (matched.length === 0) {
  774. return source;
  775. }
  776. const name = module.nameForCondition() || module.identifier();
  777. const cache = compilation.getCache("TerserWebpackPlugin|embeddedSource");
  778. // The minimizers and their options are in the etag, not just the source:
  779. // this cache outlives the build, so an entry stored under one set of
  780. // options must not answer for another.
  781. const cacheItem = cache.getItemCache(`${name}|${type}|${hostType}`, cache.mergeEtags(cache.getLazyHashedEtag(source), cache.getLazyHashedEtag(variesOn)));
  782. let output = /** @type {{ source: import("webpack").sources.Source, errors?: (Error | string)[], warnings?: (Error | string)[] } | undefined} */
  783. await cacheItem.getPromise();
  784. if (!output) {
  785. const {
  786. source: sourceFromInputSource,
  787. map
  788. } = source.sourceAndMap();
  789. const input = Buffer.isBuffer(sourceFromInputSource) ? sourceFromInputSource.toString() : sourceFromInputSource;
  790. const inputSourceMap = map && TerserPlugin.isSourceMap(map) ? (/** @type {RawSourceMap} */map) : undefined;
  791. /** @type {MinimizedResult} */
  792. let result;
  793. try {
  794. // Code generation runs before the worker pool is up, so this one is in
  795. // process. What it embeds in turn is reached by `minify` itself.
  796. result = await minify({
  797. name,
  798. input,
  799. inputSourceMap,
  800. // There is no asset to hang a banner on, and no filename to point
  801. // it at, so comments stay where the minimizer's own defaults keep
  802. // them.
  803. extractComments: false,
  804. minimizer: {
  805. implementation: (/** @type {MinimizerImplementation<T>} */
  806. /** @type {unknown} */matched.map(i => minimizers[i])),
  807. options: (/** @type {MinimizerOptions<T>} */
  808. /** @type {unknown} */
  809. matched.map(i => getMinimizerOptionsAt(this.options.minimizer.options, i)))
  810. },
  811. embedded: this.embeddedMinimizer(matched),
  812. ecma: getEcmaVersion(/** @type {NonNullable<NonNullable<import("webpack").Configuration["output"]>["environment"]>} */
  813. compiler.options.output.environment)
  814. });
  815. } catch (error) {
  816. compilation.errors.push(TerserPlugin.buildError(/** @type {Error | ErrorObject | string} */error, name));
  817. return source;
  818. }
  819. const {
  820. RawSource,
  821. SourceMapSource
  822. } = compiler.webpack.sources;
  823. // A map is asked for only when the input carried one: the generator
  824. // embedding this inlines a new one as a data URI, which costs more than
  825. // minifying saves.
  826. const minified = typeof result.code === "string" ? inputSourceMap && result.map ? new SourceMapSource(result.code, name, /** @type {RawSourceMap} */result.map, /** @type {string} */input, inputSourceMap, true) : new RawSource(result.code) : source;
  827. output = {
  828. source: minified,
  829. errors: (result.errors || []).map(item => TerserPlugin.buildError(/** @type {Error | ErrorObject | string} */item, name)),
  830. warnings: (result.warnings || []).map(item => TerserPlugin.buildWarning(item, name))
  831. };
  832. await cacheItem.storePromise(output);
  833. }
  834. for (const error of (/** @type {Error[]} */output.errors || [])) {
  835. compilation.errors.push(error);
  836. }
  837. for (const warning of (/** @type {Error[]} */output.warnings || [])) {
  838. compilation.warnings.push(warning);
  839. }
  840. return output.source;
  841. }
  842. /**
  843. * Validates the options the plugin was constructed with.
  844. * @private
  845. * @param {Compiler} compiler compiler
  846. * @returns {void}
  847. */
  848. validateOptions(compiler) {
  849. if (typeof compiler.validate === "function") {
  850. compiler.validate(() => (/** @type {Schema} */require("./options.json")), this.rawOptions, VALIDATION_CONFIGURATION);
  851. return;
  852. }
  853. // TODO remove in the next major release, when webpack >= 5.106 is the
  854. // minimum and `compiler.validate` is always there.
  855. const {
  856. validate
  857. } = require("schema-utils");
  858. validate(/** @type {Schema} */require("./options.json"), this.rawOptions, VALIDATION_CONFIGURATION);
  859. }
  860. /**
  861. * @param {Compiler} compiler compiler
  862. * @returns {void}
  863. */
  864. apply(compiler) {
  865. const pluginName = this.constructor.name;
  866. let validated = false;
  867. const validateOptions = () => {
  868. if (validated) {
  869. return;
  870. }
  871. validated = true;
  872. this.validateOptions(compiler);
  873. };
  874. if (compiler.hooks.validate) {
  875. compiler.hooks.validate.tap(pluginName, validateOptions);
  876. }
  877. // TODO remove in the next major release, once every supported webpack calls
  878. // `validate` after it applies `optimization.minimizer`. Until then that hook
  879. // reaches this plugin only where it sits in `plugins`, and webpack < 5.106
  880. // has no such hook at all; `initialize` runs after either placement.
  881. compiler.hooks.initialize.tap(pluginName, validateOptions);
  882. const availableNumberOfCores = TerserPlugin.getAvailableNumberOfCores(this.options.parallel);
  883. compiler.hooks.compilation.tap(pluginName, compilation => {
  884. const hooks = compiler.webpack.javascript.JavascriptModulesPlugin.getCompilationHooks(compilation);
  885. /**
  886. * @param {BasicMinimizerImplementation<EXPECTED_ANY> & MinimizeFunctionHelpers} impl implementation
  887. * @returns {string} minimizer version or "0.0.0"
  888. */
  889. const getVersion = impl => typeof impl.getMinimizerVersion !== "undefined" ? impl.getMinimizerVersion() || "0.0.0" : "0.0.0";
  890. const data = getSerializeJavascript()({
  891. minimizer: Array.isArray(this.options.minimizer.implementation) ? this.options.minimizer.implementation.map(getVersion) : getVersion(/** @type {BasicMinimizerImplementation<EXPECTED_ANY> & MinimizeFunctionHelpers} */
  892. this.options.minimizer.implementation),
  893. options: this.options.minimizer.options
  894. });
  895. hooks.chunkHash.tap(pluginName, (chunk, hash) => {
  896. hash.update("TerserPlugin");
  897. hash.update(data);
  898. });
  899. // Added in webpack 5.110: source one language embeds in another, which no
  900. // asset carries and `processAssets` therefore never sees.
  901. const embeddedHooks = /** @type {EmbeddedSourceHooks} */
  902. /** @type {unknown} */compilation.hooks;
  903. if (embeddedHooks.renderEmbeddedSource && embeddedHooks.embeddedSourceHash) {
  904. // Wrapped once so the etag it yields is computed once per build.
  905. const variesOn = new compiler.webpack.sources.RawSource(data);
  906. embeddedHooks.renderEmbeddedSource.tapPromise(pluginName, (source, info) => this.renderEmbeddedSource(compiler, compilation, variesOn, source, info));
  907. // Module hashes are taken before code generation, so what this tap
  908. // varies on cannot reach the code generation cache key on its own.
  909. embeddedHooks.embeddedSourceHash.tap(pluginName, (module, hash) => {
  910. hash.update("TerserPlugin");
  911. hash.update(data);
  912. });
  913. }
  914. compilation.hooks.processAssets.tapPromise({
  915. name: pluginName,
  916. stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE,
  917. additionalAssets: true
  918. }, assets => this.optimize(compiler, compilation, assets, {
  919. availableNumberOfCores
  920. }));
  921. compilation.hooks.statsPrinter.tap(pluginName, stats => {
  922. stats.hooks.print.for("asset.info.minimized").tap("minimizer-webpack-plugin", (minimized, {
  923. green,
  924. formatFlag
  925. }) => minimized ? /** @type {(text: string) => string} */green(/** @type {(flag: string) => string} */formatFlag("minimized")) : "");
  926. });
  927. });
  928. }
  929. }
  930. TerserPlugin.terserMinify = terserMinify;
  931. TerserPlugin.uglifyJsMinify = uglifyJsMinify;
  932. TerserPlugin.swcMinify = swcMinify;
  933. TerserPlugin.esbuildMinify = esbuildMinify;
  934. TerserPlugin.jsonMinify = jsonMinify;
  935. TerserPlugin.htmlMinifierTerser = htmlMinifierTerser;
  936. TerserPlugin.swcMinifyHtml = swcMinifyHtml;
  937. TerserPlugin.swcMinifyHtmlFragment = swcMinifyHtmlFragment;
  938. TerserPlugin.minifyHtmlNode = minifyHtmlNode;
  939. TerserPlugin.cssnanoMinify = cssnanoMinify;
  940. TerserPlugin.cssoMinify = cssoMinify;
  941. TerserPlugin.cleanCssMinify = cleanCssMinify;
  942. TerserPlugin.esbuildMinifyCss = esbuildMinifyCss;
  943. TerserPlugin.lightningCssMinify = lightningCssMinify;
  944. TerserPlugin.swcMinifyCss = swcMinifyCss;
  945. module.exports = TerserPlugin;