index.js 29 KB

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