utils.js 20 KB

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