utils.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. "use strict";
  2. /** @typedef {import("source-map").RawSourceMap} RawSourceMap */
  3. /** @typedef {import("terser").FormatOptions} TerserFormatOptions */
  4. /** @typedef {import("terser").MinifyOptions} TerserOptions */
  5. /** @typedef {import("terser").ECMA} TerserECMA */
  6. /** @typedef {import("./index.js").ExtractCommentsOptions} ExtractCommentsOptions */
  7. /** @typedef {import("./index.js").ExtractCommentsFunction} ExtractCommentsFunction */
  8. /** @typedef {import("./index.js").ExtractCommentsCondition} ExtractCommentsCondition */
  9. /** @typedef {import("./index.js").Input} Input */
  10. /** @typedef {import("./index.js").MinimizedResult} MinimizedResult */
  11. /** @typedef {import("./index.js").PredefinedOptions} PredefinedOptions */
  12. /** @typedef {import("./index.js").CustomOptions} CustomOptions */
  13. /**
  14. * @typedef {Array<string>} ExtractedComments
  15. */
  16. const notSettled = Symbol(`not-settled`);
  17. /**
  18. * @template T
  19. * @typedef {() => Promise<T>} Task
  20. */
  21. /**
  22. * Run tasks with limited concurency.
  23. * @template T
  24. * @param {number} limit - Limit of tasks that run at once.
  25. * @param {Task<T>[]} tasks - List of tasks to run.
  26. * @returns {Promise<T[]>} A promise that fulfills to an array of the results
  27. */
  28. function throttleAll(limit, tasks) {
  29. if (!Number.isInteger(limit) || limit < 1) {
  30. throw new TypeError(`Expected \`limit\` to be a finite number > 0, got \`${limit}\` (${typeof limit})`);
  31. }
  32. if (!Array.isArray(tasks) || !tasks.every(task => typeof task === `function`)) {
  33. throw new TypeError(`Expected \`tasks\` to be a list of functions returning a promise`);
  34. }
  35. return new Promise((resolve, reject) => {
  36. const result = Array(tasks.length).fill(notSettled);
  37. const entries = tasks.entries();
  38. const next = () => {
  39. const {
  40. done,
  41. value
  42. } = entries.next();
  43. if (done) {
  44. const isLast = !result.includes(notSettled);
  45. if (isLast) resolve(
  46. /** @type{T[]} **/
  47. result);
  48. return;
  49. }
  50. const [index, task] = value;
  51. /**
  52. * @param {T} x
  53. */
  54. const onFulfilled = x => {
  55. result[index] = x;
  56. next();
  57. };
  58. task().then(onFulfilled, reject);
  59. };
  60. Array(limit).fill(0).forEach(next);
  61. });
  62. }
  63. /* istanbul ignore next */
  64. /**
  65. * @param {Input} input
  66. * @param {RawSourceMap | undefined} sourceMap
  67. * @param {PredefinedOptions & CustomOptions} minimizerOptions
  68. * @param {ExtractCommentsOptions | undefined} extractComments
  69. * @return {Promise<MinimizedResult>}
  70. */
  71. async function terserMinify(input, sourceMap, minimizerOptions, extractComments) {
  72. /**
  73. * @param {any} value
  74. * @returns {boolean}
  75. */
  76. const isObject = value => {
  77. const type = typeof value;
  78. return value != null && (type === "object" || type === "function");
  79. };
  80. /**
  81. * @param {TerserOptions & { sourceMap: undefined } & ({ output: TerserFormatOptions & { beautify: boolean } } | { format: TerserFormatOptions & { beautify: boolean } })} terserOptions
  82. * @param {ExtractedComments} extractedComments
  83. * @returns {ExtractCommentsFunction}
  84. */
  85. const buildComments = (terserOptions, extractedComments) => {
  86. /** @type {{ [index: string]: ExtractCommentsCondition }} */
  87. const condition = {};
  88. let comments;
  89. if (terserOptions.format) {
  90. ({
  91. comments
  92. } = terserOptions.format);
  93. } else if (terserOptions.output) {
  94. ({
  95. comments
  96. } = terserOptions.output);
  97. }
  98. condition.preserve = typeof comments !== "undefined" ? comments : false;
  99. if (typeof extractComments === "boolean" && extractComments) {
  100. condition.extract = "some";
  101. } else if (typeof extractComments === "string" || extractComments instanceof RegExp) {
  102. condition.extract = extractComments;
  103. } else if (typeof extractComments === "function") {
  104. condition.extract = extractComments;
  105. } else if (extractComments && isObject(extractComments)) {
  106. condition.extract = typeof extractComments.condition === "boolean" && extractComments.condition ? "some" : typeof extractComments.condition !== "undefined" ? extractComments.condition : "some";
  107. } else {
  108. // No extract
  109. // Preserve using "commentsOpts" or "some"
  110. condition.preserve = typeof comments !== "undefined" ? comments : "some";
  111. condition.extract = false;
  112. } // Ensure that both conditions are functions
  113. ["preserve", "extract"].forEach(key => {
  114. /** @type {undefined | string} */
  115. let regexStr;
  116. /** @type {undefined | RegExp} */
  117. let regex;
  118. switch (typeof condition[key]) {
  119. case "boolean":
  120. condition[key] = condition[key] ? () => true : () => false;
  121. break;
  122. case "function":
  123. break;
  124. case "string":
  125. if (condition[key] === "all") {
  126. condition[key] = () => true;
  127. break;
  128. }
  129. if (condition[key] === "some") {
  130. condition[key] =
  131. /** @type {ExtractCommentsFunction} */
  132. (astNode, comment) => (comment.type === "comment2" || comment.type === "comment1") && /@preserve|@lic|@cc_on|^\**!/i.test(comment.value);
  133. break;
  134. }
  135. regexStr =
  136. /** @type {string} */
  137. condition[key];
  138. condition[key] =
  139. /** @type {ExtractCommentsFunction} */
  140. (astNode, comment) => new RegExp(
  141. /** @type {string} */
  142. regexStr).test(comment.value);
  143. break;
  144. default:
  145. regex =
  146. /** @type {RegExp} */
  147. condition[key];
  148. condition[key] =
  149. /** @type {ExtractCommentsFunction} */
  150. (astNode, comment) =>
  151. /** @type {RegExp} */
  152. regex.test(comment.value);
  153. }
  154. }); // Redefine the comments function to extract and preserve
  155. // comments according to the two conditions
  156. return (astNode, comment) => {
  157. if (
  158. /** @type {{ extract: ExtractCommentsFunction }} */
  159. condition.extract(astNode, comment)) {
  160. const commentText = comment.type === "comment2" ? `/*${comment.value}*/` : `//${comment.value}`; // Don't include duplicate comments
  161. if (!extractedComments.includes(commentText)) {
  162. extractedComments.push(commentText);
  163. }
  164. }
  165. return (
  166. /** @type {{ preserve: ExtractCommentsFunction }} */
  167. condition.preserve(astNode, comment)
  168. );
  169. };
  170. };
  171. /**
  172. * @param {PredefinedOptions & TerserOptions} [terserOptions={}]
  173. * @returns {TerserOptions & { sourceMap: undefined } & ({ output: TerserFormatOptions & { beautify: boolean } } | { format: TerserFormatOptions & { beautify: boolean } })}
  174. */
  175. const buildTerserOptions = (terserOptions = {}) => {
  176. // Need deep copy objects to avoid https://github.com/terser/terser/issues/366
  177. return { ...terserOptions,
  178. compress: typeof terserOptions.compress === "boolean" ? terserOptions.compress : { ...terserOptions.compress
  179. },
  180. // ecma: terserOptions.ecma,
  181. // ie8: terserOptions.ie8,
  182. // keep_classnames: terserOptions.keep_classnames,
  183. // keep_fnames: terserOptions.keep_fnames,
  184. mangle: terserOptions.mangle == null ? true : typeof terserOptions.mangle === "boolean" ? terserOptions.mangle : { ...terserOptions.mangle
  185. },
  186. // module: terserOptions.module,
  187. // nameCache: { ...terserOptions.toplevel },
  188. // the `output` option is deprecated
  189. ...(terserOptions.format ? {
  190. format: {
  191. beautify: false,
  192. ...terserOptions.format
  193. }
  194. } : {
  195. output: {
  196. beautify: false,
  197. ...terserOptions.output
  198. }
  199. }),
  200. parse: { ...terserOptions.parse
  201. },
  202. // safari10: terserOptions.safari10,
  203. // Ignoring sourceMap from options
  204. // eslint-disable-next-line no-undefined
  205. sourceMap: undefined // toplevel: terserOptions.toplevel
  206. };
  207. }; // eslint-disable-next-line global-require
  208. const {
  209. minify
  210. } = require("terser"); // Copy `terser` options
  211. const terserOptions = buildTerserOptions(minimizerOptions); // Let terser generate a SourceMap
  212. if (sourceMap) {
  213. // @ts-ignore
  214. terserOptions.sourceMap = {
  215. asObject: true
  216. };
  217. }
  218. /** @type {ExtractedComments} */
  219. const extractedComments = [];
  220. if (terserOptions.output) {
  221. terserOptions.output.comments = buildComments(terserOptions, extractedComments);
  222. } else if (terserOptions.format) {
  223. terserOptions.format.comments = buildComments(terserOptions, extractedComments);
  224. }
  225. const [[filename, code]] = Object.entries(input);
  226. const result = await minify({
  227. [filename]: code
  228. }, terserOptions);
  229. return {
  230. code:
  231. /** @type {string} **/
  232. result.code,
  233. // @ts-ignore
  234. // eslint-disable-next-line no-undefined
  235. map: result.map ?
  236. /** @type {RawSourceMap} **/
  237. result.map : undefined,
  238. extractedComments
  239. };
  240. }
  241. /**
  242. * @returns {string | undefined}
  243. */
  244. terserMinify.getMinimizerVersion = () => {
  245. let packageJson;
  246. try {
  247. // eslint-disable-next-line global-require
  248. packageJson = require("terser/package.json");
  249. } catch (error) {// Ignore
  250. }
  251. return packageJson && packageJson.version;
  252. };
  253. /* istanbul ignore next */
  254. /**
  255. * @param {Input} input
  256. * @param {RawSourceMap | undefined} sourceMap
  257. * @param {PredefinedOptions & CustomOptions} minimizerOptions
  258. * @param {ExtractCommentsOptions | undefined} extractComments
  259. * @return {Promise<MinimizedResult>}
  260. */
  261. async function uglifyJsMinify(input, sourceMap, minimizerOptions, extractComments) {
  262. /**
  263. * @param {any} value
  264. * @returns {boolean}
  265. */
  266. const isObject = value => {
  267. const type = typeof value;
  268. return value != null && (type === "object" || type === "function");
  269. };
  270. /**
  271. * @param {import("uglify-js").MinifyOptions & { sourceMap: undefined } & { output: import("uglify-js").OutputOptions & { beautify: boolean }}} uglifyJsOptions
  272. * @param {ExtractedComments} extractedComments
  273. * @returns {ExtractCommentsFunction}
  274. */
  275. const buildComments = (uglifyJsOptions, extractedComments) => {
  276. /** @type {{ [index: string]: ExtractCommentsCondition }} */
  277. const condition = {};
  278. const {
  279. comments
  280. } = uglifyJsOptions.output;
  281. condition.preserve = typeof comments !== "undefined" ? comments : false;
  282. if (typeof extractComments === "boolean" && extractComments) {
  283. condition.extract = "some";
  284. } else if (typeof extractComments === "string" || extractComments instanceof RegExp) {
  285. condition.extract = extractComments;
  286. } else if (typeof extractComments === "function") {
  287. condition.extract = extractComments;
  288. } else if (extractComments && isObject(extractComments)) {
  289. condition.extract = typeof extractComments.condition === "boolean" && extractComments.condition ? "some" : typeof extractComments.condition !== "undefined" ? extractComments.condition : "some";
  290. } else {
  291. // No extract
  292. // Preserve using "commentsOpts" or "some"
  293. condition.preserve = typeof comments !== "undefined" ? comments : "some";
  294. condition.extract = false;
  295. } // Ensure that both conditions are functions
  296. ["preserve", "extract"].forEach(key => {
  297. /** @type {undefined | string} */
  298. let regexStr;
  299. /** @type {undefined | RegExp} */
  300. let regex;
  301. switch (typeof condition[key]) {
  302. case "boolean":
  303. condition[key] = condition[key] ? () => true : () => false;
  304. break;
  305. case "function":
  306. break;
  307. case "string":
  308. if (condition[key] === "all") {
  309. condition[key] = () => true;
  310. break;
  311. }
  312. if (condition[key] === "some") {
  313. condition[key] =
  314. /** @type {ExtractCommentsFunction} */
  315. (astNode, comment) => (comment.type === "comment2" || comment.type === "comment1") && /@preserve|@lic|@cc_on|^\**!/i.test(comment.value);
  316. break;
  317. }
  318. regexStr =
  319. /** @type {string} */
  320. condition[key];
  321. condition[key] =
  322. /** @type {ExtractCommentsFunction} */
  323. (astNode, comment) => new RegExp(
  324. /** @type {string} */
  325. regexStr).test(comment.value);
  326. break;
  327. default:
  328. regex =
  329. /** @type {RegExp} */
  330. condition[key];
  331. condition[key] =
  332. /** @type {ExtractCommentsFunction} */
  333. (astNode, comment) =>
  334. /** @type {RegExp} */
  335. regex.test(comment.value);
  336. }
  337. }); // Redefine the comments function to extract and preserve
  338. // comments according to the two conditions
  339. return (astNode, comment) => {
  340. if (
  341. /** @type {{ extract: ExtractCommentsFunction }} */
  342. condition.extract(astNode, comment)) {
  343. const commentText = comment.type === "comment2" ? `/*${comment.value}*/` : `//${comment.value}`; // Don't include duplicate comments
  344. if (!extractedComments.includes(commentText)) {
  345. extractedComments.push(commentText);
  346. }
  347. }
  348. return (
  349. /** @type {{ preserve: ExtractCommentsFunction }} */
  350. condition.preserve(astNode, comment)
  351. );
  352. };
  353. };
  354. /**
  355. * @param {PredefinedOptions & import("uglify-js").MinifyOptions} [uglifyJsOptions={}]
  356. * @returns {import("uglify-js").MinifyOptions & { sourceMap: undefined } & { output: import("uglify-js").OutputOptions & { beautify: boolean }}}
  357. */
  358. const buildUglifyJsOptions = (uglifyJsOptions = {}) => {
  359. // eslint-disable-next-line no-param-reassign
  360. delete minimizerOptions.ecma; // eslint-disable-next-line no-param-reassign
  361. delete minimizerOptions.module; // Need deep copy objects to avoid https://github.com/terser/terser/issues/366
  362. return { ...uglifyJsOptions,
  363. // warnings: uglifyJsOptions.warnings,
  364. parse: { ...uglifyJsOptions.parse
  365. },
  366. compress: typeof uglifyJsOptions.compress === "boolean" ? uglifyJsOptions.compress : { ...uglifyJsOptions.compress
  367. },
  368. mangle: uglifyJsOptions.mangle == null ? true : typeof uglifyJsOptions.mangle === "boolean" ? uglifyJsOptions.mangle : { ...uglifyJsOptions.mangle
  369. },
  370. output: {
  371. beautify: false,
  372. ...uglifyJsOptions.output
  373. },
  374. // Ignoring sourceMap from options
  375. // eslint-disable-next-line no-undefined
  376. sourceMap: undefined // toplevel: uglifyJsOptions.toplevel
  377. // nameCache: { ...uglifyJsOptions.toplevel },
  378. // ie8: uglifyJsOptions.ie8,
  379. // keep_fnames: uglifyJsOptions.keep_fnames,
  380. };
  381. }; // eslint-disable-next-line global-require, import/no-extraneous-dependencies
  382. const {
  383. minify
  384. } = require("uglify-js"); // Copy `uglify-js` options
  385. const uglifyJsOptions = buildUglifyJsOptions(minimizerOptions); // Let terser generate a SourceMap
  386. if (sourceMap) {
  387. // @ts-ignore
  388. uglifyJsOptions.sourceMap = true;
  389. }
  390. /** @type {ExtractedComments} */
  391. const extractedComments = []; // @ts-ignore
  392. uglifyJsOptions.output.comments = buildComments(uglifyJsOptions, extractedComments);
  393. const [[filename, code]] = Object.entries(input);
  394. const result = await minify({
  395. [filename]: code
  396. }, uglifyJsOptions);
  397. return {
  398. code: result.code,
  399. // eslint-disable-next-line no-undefined
  400. map: result.map ? JSON.parse(result.map) : undefined,
  401. errors: result.error ? [result.error] : [],
  402. warnings: result.warnings || [],
  403. extractedComments
  404. };
  405. }
  406. /**
  407. * @returns {string | undefined}
  408. */
  409. uglifyJsMinify.getMinimizerVersion = () => {
  410. let packageJson;
  411. try {
  412. // eslint-disable-next-line global-require, import/no-extraneous-dependencies
  413. packageJson = require("uglify-js/package.json");
  414. } catch (error) {// Ignore
  415. }
  416. return packageJson && packageJson.version;
  417. };
  418. /* istanbul ignore next */
  419. /**
  420. * @param {Input} input
  421. * @param {RawSourceMap | undefined} sourceMap
  422. * @param {PredefinedOptions & CustomOptions} minimizerOptions
  423. * @return {Promise<MinimizedResult>}
  424. */
  425. async function swcMinify(input, sourceMap, minimizerOptions) {
  426. /**
  427. * @param {PredefinedOptions & import("@swc/core").JsMinifyOptions} [swcOptions={}]
  428. * @returns {import("@swc/core").JsMinifyOptions & { sourceMap: undefined }}
  429. */
  430. const buildSwcOptions = (swcOptions = {}) => {
  431. // Need deep copy objects to avoid https://github.com/terser/terser/issues/366
  432. return { ...swcOptions,
  433. compress: typeof swcOptions.compress === "boolean" ? swcOptions.compress : { ...swcOptions.compress
  434. },
  435. mangle: swcOptions.mangle == null ? true : typeof swcOptions.mangle === "boolean" ? swcOptions.mangle : { ...swcOptions.mangle
  436. },
  437. // ecma: swcOptions.ecma,
  438. // keep_classnames: swcOptions.keep_classnames,
  439. // keep_fnames: swcOptions.keep_fnames,
  440. // module: swcOptions.module,
  441. // safari10: swcOptions.safari10,
  442. // toplevel: swcOptions.toplevel
  443. // eslint-disable-next-line no-undefined
  444. sourceMap: undefined
  445. };
  446. }; // eslint-disable-next-line import/no-extraneous-dependencies, global-require
  447. const swc = require("@swc/core"); // Copy `swc` options
  448. const swcOptions = buildSwcOptions(minimizerOptions); // Let `swc` generate a SourceMap
  449. if (sourceMap) {
  450. // @ts-ignore
  451. swcOptions.sourceMap = true;
  452. }
  453. const [[filename, code]] = Object.entries(input);
  454. const result = await swc.minify(code, swcOptions);
  455. let map;
  456. if (result.map) {
  457. map = JSON.parse(result.map); // TODO workaround for swc because `filename` is not preset as in `swc` signature as for `terser`
  458. map.sources = [filename];
  459. delete map.sourcesContent;
  460. }
  461. return {
  462. code: result.code,
  463. map
  464. };
  465. }
  466. /**
  467. * @returns {string | undefined}
  468. */
  469. swcMinify.getMinimizerVersion = () => {
  470. let packageJson;
  471. try {
  472. // eslint-disable-next-line global-require, import/no-extraneous-dependencies
  473. packageJson = require("@swc/core/package.json");
  474. } catch (error) {// Ignore
  475. }
  476. return packageJson && packageJson.version;
  477. };
  478. /* istanbul ignore next */
  479. /**
  480. * @param {Input} input
  481. * @param {RawSourceMap | undefined} sourceMap
  482. * @param {PredefinedOptions & CustomOptions} minimizerOptions
  483. * @return {Promise<MinimizedResult>}
  484. */
  485. async function esbuildMinify(input, sourceMap, minimizerOptions) {
  486. /**
  487. * @param {PredefinedOptions & import("esbuild").TransformOptions} [esbuildOptions={}]
  488. * @returns {import("esbuild").TransformOptions}
  489. */
  490. const buildEsbuildOptions = (esbuildOptions = {}) => {
  491. // eslint-disable-next-line no-param-reassign
  492. delete esbuildOptions.ecma;
  493. if (esbuildOptions.module) {
  494. // eslint-disable-next-line no-param-reassign
  495. esbuildOptions.format = "esm";
  496. } // eslint-disable-next-line no-param-reassign
  497. delete esbuildOptions.module; // Need deep copy objects to avoid https://github.com/terser/terser/issues/366
  498. return {
  499. minify: true,
  500. legalComments: "inline",
  501. ...esbuildOptions,
  502. sourcemap: false
  503. };
  504. }; // eslint-disable-next-line import/no-extraneous-dependencies, global-require
  505. const esbuild = require("esbuild"); // Copy `esbuild` options
  506. const esbuildOptions = buildEsbuildOptions(minimizerOptions); // Let `esbuild` generate a SourceMap
  507. if (sourceMap) {
  508. esbuildOptions.sourcemap = true;
  509. esbuildOptions.sourcesContent = false;
  510. }
  511. const [[filename, code]] = Object.entries(input);
  512. esbuildOptions.sourcefile = filename;
  513. const result = await esbuild.transform(code, esbuildOptions);
  514. return {
  515. code: result.code,
  516. // eslint-disable-next-line no-undefined
  517. map: result.map ? JSON.parse(result.map) : undefined,
  518. warnings: result.warnings.length > 0 ? result.warnings.map(item => {
  519. return {
  520. name: "Warning",
  521. source: item.location && item.location.file,
  522. line: item.location && item.location.line,
  523. column: item.location && item.location.column,
  524. plugin: item.pluginName,
  525. message: `${item.text}${item.detail ? `\nDetails:\n${item.detail}` : ""}${item.notes.length > 0 ? `\n\nNotes:\n${item.notes.map(note => `${note.location ? `[${note.location.file}:${note.location.line}:${note.location.column}] ` : ""}${note.text}${note.location ? `\nSuggestion: ${note.location.suggestion}` : ""}${note.location ? `\nLine text:\n${note.location.lineText}\n` : ""}`).join("\n")}` : ""}`
  526. };
  527. }) : []
  528. };
  529. }
  530. /**
  531. * @returns {string | undefined}
  532. */
  533. esbuildMinify.getMinimizerVersion = () => {
  534. let packageJson;
  535. try {
  536. // eslint-disable-next-line global-require, import/no-extraneous-dependencies
  537. packageJson = require("esbuild/package.json");
  538. } catch (error) {// Ignore
  539. }
  540. return packageJson && packageJson.version;
  541. };
  542. module.exports = {
  543. throttleAll,
  544. terserMinify,
  545. uglifyJsMinify,
  546. swcMinify,
  547. esbuildMinify
  548. };