index.js 26 KB

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