utils.js 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722
  1. "use strict";
  2. /** @typedef {import("./index.js").ExtractCommentsOptions} ExtractCommentsOptions */
  3. /** @typedef {import("./index.js").ExtractCommentsFunction} ExtractCommentsFunction */
  4. /** @typedef {import("./index.js").ExtractCommentsCondition} ExtractCommentsCondition */
  5. /** @typedef {import("./index.js").Input} Input */
  6. /** @typedef {import("./index.js").MinimizedResult} MinimizedResult */
  7. /** @typedef {import("./index.js").CustomOptions} CustomOptions */
  8. /** @typedef {import("./index.js").RawSourceMap} RawSourceMap */
  9. /** @typedef {import("./index.js").EXPECTED_OBJECT} EXPECTED_OBJECT */
  10. /** @typedef {import("./index.js").EXPECTED_ANY} EXPECTED_ANY */
  11. /**
  12. * @typedef {string[]} ExtractedComments
  13. */
  14. /**
  15. * The options entry belonging to one minimizer: an array is parallel to the
  16. * implementations, a single object is shared by all of them.
  17. * @param {EXPECTED_ANY} minimizerOptions the options as configured
  18. * @param {number} index index into the implementations
  19. * @returns {EXPECTED_OBJECT} its options
  20. */
  21. function getMinimizerOptionsAt(minimizerOptions, index) {
  22. return Array.isArray(minimizerOptions) ? minimizerOptions[index] || {} : minimizerOptions;
  23. }
  24. const JS_FILE_RE = /\.[cm]?js(\?.*)?$/i;
  25. const JSON_FILE_RE = /\.json(\?.*)?$/i;
  26. const HTML_FILE_RE = /\.html?(\?.*)?$/i;
  27. const CSS_FILE_RE = /\.css(\?.*)?$/i;
  28. /**
  29. * Map a webpack `output.environment` configuration to the highest
  30. * ECMAScript version that the target is known to support. Returns `5`
  31. * when no ES2015+ features are flagged.
  32. * @param {NonNullable<NonNullable<import("webpack").Configuration["output"]>["environment"]>} environment environment
  33. * @returns {number} ecma version (5, 2015, 2017 or 2020)
  34. */
  35. function getEcmaVersion(environment) {
  36. // ES2020 (11th edition)
  37. if (environment.bigIntLiteral || environment.dynamicImport || environment.dynamicImportInWorker || environment.globalThis || environment.optionalChaining) {
  38. return 2020;
  39. }
  40. // ES2017 (8th edition)
  41. if (environment.asyncFunction) {
  42. return 2017;
  43. }
  44. // ES2015 (6th edition)
  45. if (environment.arrowFunction || environment.const || environment.destructuring || environment.forOf || environment.methodShorthand || environment.module || environment.templateLiteral) {
  46. return 2015;
  47. }
  48. return 5;
  49. }
  50. const notSettled = Symbol("not-settled");
  51. /**
  52. * @template T
  53. * @typedef {() => Promise<T>} Task
  54. */
  55. /**
  56. * Run tasks with limited concurrency.
  57. * @template T
  58. * @param {number} limit Limit of tasks that run at once.
  59. * @param {Task<T>[]} tasks List of tasks to run.
  60. * @returns {Promise<T[]>} A promise that fulfills to an array of the results
  61. */
  62. function throttleAll(limit, tasks) {
  63. return new Promise((resolve, reject) => {
  64. const result = Array.from({
  65. length: tasks.length
  66. }).fill(notSettled);
  67. const entries = tasks.entries();
  68. const next = () => {
  69. const {
  70. done,
  71. value
  72. } = entries.next();
  73. if (done) {
  74. const isLast = !result.includes(notSettled);
  75. if (isLast) resolve(result);
  76. return;
  77. }
  78. const [index, task] = value;
  79. /**
  80. * @param {T} resultValue Result value
  81. */
  82. const onFulfilled = resultValue => {
  83. result[index] = resultValue;
  84. next();
  85. };
  86. task().then(onFulfilled, reject);
  87. };
  88. for (let i = 0; i < limit; i++) {
  89. next();
  90. }
  91. });
  92. }
  93. /* istanbul ignore next */
  94. /**
  95. * @param {Input} input input
  96. * @param {RawSourceMap=} sourceMap source map
  97. * @param {CustomOptions=} minimizerOptions options
  98. * @param {ExtractCommentsOptions=} extractComments extract comments option
  99. * @returns {Promise<MinimizedResult>} minimized result
  100. */
  101. async function terserMinify(input, sourceMap, minimizerOptions, extractComments) {
  102. /**
  103. * @param {unknown} value value
  104. * @returns {value is EXPECTED_OBJECT} true when value is object or function
  105. */
  106. const isObject = value => {
  107. const type = typeof value;
  108. // eslint-disable-next-line no-eq-null, eqeqeq
  109. return value != null && (type === "object" || type === "function");
  110. };
  111. /**
  112. * @param {import("terser").MinifyOptions & { sourceMap: import("terser").SourceMapOptions | undefined } & ({ output: import("terser").FormatOptions & { beautify: boolean } } | { format: import("terser").FormatOptions & { beautify: boolean } })} terserOptions terser options
  113. * @param {ExtractedComments} extractedComments extracted comments
  114. * @returns {ExtractCommentsFunction} function to extract comments
  115. */
  116. const buildComments = (terserOptions, extractedComments) => {
  117. /** @type {{ [index: string]: ExtractCommentsCondition }} */
  118. const condition = {};
  119. let comments;
  120. if (terserOptions.format) {
  121. ({
  122. comments
  123. } = terserOptions.format);
  124. } else if (terserOptions.output) {
  125. ({
  126. comments
  127. } = terserOptions.output);
  128. }
  129. condition.preserve = typeof comments !== "undefined" ? comments : false;
  130. if (typeof extractComments === "boolean" && extractComments) {
  131. condition.extract = "some";
  132. } else if (typeof extractComments === "string" || extractComments instanceof RegExp) {
  133. condition.extract = extractComments;
  134. } else if (typeof extractComments === "function") {
  135. condition.extract = extractComments;
  136. } else if (extractComments && isObject(extractComments)) {
  137. condition.extract = typeof extractComments.condition === "boolean" && extractComments.condition ? "some" : typeof extractComments.condition !== "undefined" ? extractComments.condition : "some";
  138. } else {
  139. // No extract
  140. // Preserve using "commentsOpts" or "some"
  141. condition.preserve = typeof comments !== "undefined" ? comments : "some";
  142. condition.extract = false;
  143. }
  144. // Ensure that both conditions are functions
  145. for (const key of ["preserve", "extract"]) {
  146. /** @type {undefined | string} */
  147. let regexStr;
  148. /** @type {undefined | RegExp} */
  149. let regex;
  150. switch (typeof condition[key]) {
  151. case "boolean":
  152. condition[key] = condition[key] ? () => true : () => false;
  153. break;
  154. case "function":
  155. break;
  156. case "string":
  157. if (condition[key] === "all") {
  158. condition[key] = () => true;
  159. break;
  160. }
  161. if (condition[key] === "some") {
  162. condition[key] = /** @type {ExtractCommentsFunction} */
  163. (astNode, comment) => (comment.type === "comment2" || comment.type === "comment1") && /@preserve|@lic|@cc_on|^\**!/i.test(comment.value);
  164. break;
  165. }
  166. regexStr = /** @type {string} */condition[key];
  167. condition[key] = /** @type {ExtractCommentsFunction} */
  168. (astNode, comment) => new RegExp(/** @type {string} */regexStr).test(comment.value);
  169. break;
  170. default:
  171. regex = /** @type {RegExp} */condition[key];
  172. condition[key] = /** @type {ExtractCommentsFunction} */
  173. (astNode, comment) => /** @type {RegExp} */regex.test(comment.value);
  174. }
  175. }
  176. // Redefine the comments function to extract and preserve
  177. // comments according to the two conditions
  178. const seenComments = new Set(extractedComments);
  179. return (astNode, comment) => {
  180. if (/** @type {{ extract: ExtractCommentsFunction }} */
  181. condition.extract(astNode, comment)) {
  182. const commentText = comment.type === "comment2" ? `/*${comment.value}*/` : `//${comment.value}`;
  183. // Don't include duplicate comments
  184. if (!seenComments.has(commentText)) {
  185. seenComments.add(commentText);
  186. extractedComments.push(commentText);
  187. }
  188. }
  189. return /** @type {{ preserve: ExtractCommentsFunction }} */condition.preserve(astNode, comment);
  190. };
  191. };
  192. /**
  193. * @param {import("terser").MinifyOptions=} terserOptions terser options
  194. * @returns {import("terser").MinifyOptions & { sourceMap: import("terser").SourceMapOptions | undefined } & { compress: import("terser").CompressOptions } & ({ output: import("terser").FormatOptions & { beautify: boolean } } | { format: import("terser").FormatOptions & { beautify: boolean } })} built terser options
  195. */
  196. const buildTerserOptions = (terserOptions = {}) => (
  197. // Need deep copy objects to avoid https://github.com/terser/terser/issues/366
  198. {
  199. ...terserOptions,
  200. compress: typeof terserOptions.compress === "boolean" ? terserOptions.compress ? {} : false : {
  201. ...terserOptions.compress
  202. },
  203. // ecma: terserOptions.ecma,
  204. // ie8: terserOptions.ie8,
  205. // keep_classnames: terserOptions.keep_classnames,
  206. // keep_fnames: terserOptions.keep_fnames,
  207. mangle:
  208. // eslint-disable-next-line no-eq-null, eqeqeq
  209. terserOptions.mangle == null ? true : typeof terserOptions.mangle === "boolean" ? terserOptions.mangle : {
  210. ...terserOptions.mangle
  211. },
  212. // module: terserOptions.module,
  213. // nameCache: { ...terserOptions.toplevel },
  214. // the `output` option is deprecated
  215. ...(terserOptions.format ? {
  216. format: {
  217. beautify: false,
  218. ...terserOptions.format
  219. }
  220. } : {
  221. output: {
  222. beautify: false,
  223. ...terserOptions.output
  224. }
  225. }),
  226. parse: {
  227. ...terserOptions.parse
  228. },
  229. // safari10: terserOptions.safari10,
  230. // Ignoring sourceMap from options
  231. sourceMap: undefined
  232. // toplevel: terserOptions.toplevel
  233. });
  234. let minify;
  235. try {
  236. ({
  237. minify
  238. } = require("terser"));
  239. } catch (err) {
  240. return {
  241. errors: [(/** @type {Error} */err)]
  242. };
  243. }
  244. // Copy `terser` options
  245. const terserOptions = buildTerserOptions(minimizerOptions);
  246. // Let terser generate a SourceMap. The dispatcher in `minify.js`
  247. // chains the previous step's map onto this one.
  248. if (sourceMap) {
  249. terserOptions.sourceMap = {
  250. asObject: true
  251. };
  252. }
  253. /** @type {ExtractedComments} */
  254. const extractedComments = [];
  255. if (terserOptions.output) {
  256. terserOptions.output.comments = buildComments(terserOptions, extractedComments);
  257. } else if (terserOptions.format) {
  258. terserOptions.format.comments = buildComments(terserOptions, extractedComments);
  259. }
  260. if (terserOptions.compress) {
  261. // More optimizations
  262. if (typeof terserOptions.compress.ecma === "undefined") {
  263. terserOptions.compress.ecma = terserOptions.ecma;
  264. }
  265. // https://github.com/webpack/webpack/issues/16135
  266. if (terserOptions.ecma === 5 && typeof terserOptions.compress.arrows === "undefined") {
  267. terserOptions.compress.arrows = false;
  268. }
  269. }
  270. const [[filename, code]] = Object.entries(input);
  271. const result = await minify({
  272. [filename]: code
  273. }, terserOptions);
  274. return {
  275. code: (/** @type {string} * */result.code),
  276. map: result.map ? (/** @type {RawSourceMap} * */result.map) : undefined,
  277. extractedComments
  278. };
  279. }
  280. /**
  281. * @returns {string | undefined} the minimizer version
  282. */
  283. terserMinify.getMinimizerVersion = () => {
  284. let packageJson;
  285. try {
  286. packageJson = require("terser/package.json");
  287. } catch (_err) {
  288. // Ignore
  289. }
  290. return packageJson && packageJson.version;
  291. };
  292. /**
  293. * @returns {boolean | undefined} true if worker thread is supported, false otherwise
  294. */
  295. terserMinify.supportsWorkerThreads = () => true;
  296. /**
  297. * The language this minifies, for a caller dispatching source that carries
  298. * no filename of its own.
  299. * @returns {string[]} the languages
  300. */
  301. terserMinify.getTypes = () => ["javascript"];
  302. /**
  303. * @param {string} name asset name
  304. * @returns {boolean} true if `name` looks like a JavaScript file
  305. */
  306. terserMinify.filter = name => JS_FILE_RE.test(name);
  307. /* istanbul ignore next */
  308. /**
  309. * @param {Input} input input
  310. * @param {RawSourceMap=} sourceMap source map
  311. * @param {CustomOptions=} minimizerOptions options
  312. * @param {ExtractCommentsOptions=} extractComments extract comments option
  313. * @returns {Promise<MinimizedResult>} minimized result
  314. */
  315. async function uglifyJsMinify(input, sourceMap, minimizerOptions, extractComments) {
  316. /**
  317. * @param {unknown} value value
  318. * @returns {boolean} true when value is object or function
  319. */
  320. const isObject = value => {
  321. const type = typeof value;
  322. // eslint-disable-next-line no-eq-null, eqeqeq
  323. return value != null && (type === "object" || type === "function");
  324. };
  325. /**
  326. * @param {import("uglify-js").MinifyOptions & { sourceMap: boolean | import("uglify-js").SourceMapOptions | undefined } & { output: import("uglify-js").OutputOptions & { beautify: boolean } }} uglifyJsOptions uglify-js options
  327. * @param {ExtractedComments} extractedComments extracted comments
  328. * @returns {ExtractCommentsFunction} extract comments function
  329. */
  330. const buildComments = (uglifyJsOptions, extractedComments) => {
  331. /** @type {{ [index: string]: ExtractCommentsCondition }} */
  332. const condition = {};
  333. const {
  334. comments
  335. } = uglifyJsOptions.output;
  336. condition.preserve = typeof comments !== "undefined" ? comments : false;
  337. if (typeof extractComments === "boolean" && extractComments) {
  338. condition.extract = "some";
  339. } else if (typeof extractComments === "string" || extractComments instanceof RegExp) {
  340. condition.extract = extractComments;
  341. } else if (typeof extractComments === "function") {
  342. condition.extract = extractComments;
  343. } else if (extractComments && isObject(extractComments)) {
  344. condition.extract = typeof extractComments.condition === "boolean" && extractComments.condition ? "some" : typeof extractComments.condition !== "undefined" ? extractComments.condition : "some";
  345. } else {
  346. // No extract
  347. // Preserve using "commentsOpts" or "some"
  348. condition.preserve = typeof comments !== "undefined" ? comments : "some";
  349. condition.extract = false;
  350. }
  351. // Ensure that both conditions are functions
  352. for (const key of ["preserve", "extract"]) {
  353. /** @type {undefined | string} */
  354. let regexStr;
  355. /** @type {undefined | RegExp} */
  356. let regex;
  357. switch (typeof condition[key]) {
  358. case "boolean":
  359. condition[key] = condition[key] ? () => true : () => false;
  360. break;
  361. case "function":
  362. break;
  363. case "string":
  364. if (condition[key] === "all") {
  365. condition[key] = () => true;
  366. break;
  367. }
  368. if (condition[key] === "some") {
  369. condition[key] = /** @type {ExtractCommentsFunction} */
  370. (astNode, comment) => (comment.type === "comment2" || comment.type === "comment1") && /@preserve|@lic|@cc_on|^\**!/i.test(comment.value);
  371. break;
  372. }
  373. regexStr = /** @type {string} */condition[key];
  374. condition[key] = /** @type {ExtractCommentsFunction} */
  375. (astNode, comment) => new RegExp(/** @type {string} */regexStr).test(comment.value);
  376. break;
  377. default:
  378. regex = /** @type {RegExp} */condition[key];
  379. condition[key] = /** @type {ExtractCommentsFunction} */
  380. (astNode, comment) => /** @type {RegExp} */regex.test(comment.value);
  381. }
  382. }
  383. // Redefine the comments function to extract and preserve
  384. // comments according to the two conditions
  385. const seenComments = new Set(extractedComments);
  386. return (astNode, comment) => {
  387. if (/** @type {{ extract: ExtractCommentsFunction }} */
  388. condition.extract(astNode, comment)) {
  389. const commentText = comment.type === "comment2" ? `/*${comment.value}*/` : `//${comment.value}`;
  390. // Don't include duplicate comments
  391. if (!seenComments.has(commentText)) {
  392. seenComments.add(commentText);
  393. extractedComments.push(commentText);
  394. }
  395. }
  396. return /** @type {{ preserve: ExtractCommentsFunction }} */condition.preserve(astNode, comment);
  397. };
  398. };
  399. /**
  400. * @param {import("uglify-js").MinifyOptions & { ecma?: number | string }=} uglifyJsOptions uglify-js options
  401. * @returns {import("uglify-js").MinifyOptions & { sourceMap: boolean | import("uglify-js").SourceMapOptions | undefined } & { output: import("uglify-js").OutputOptions & { beautify: boolean } }} uglify-js options
  402. */
  403. const buildUglifyJsOptions = (uglifyJsOptions = {}) => {
  404. if (typeof uglifyJsOptions.ecma !== "undefined") {
  405. delete uglifyJsOptions.ecma;
  406. }
  407. if (typeof uglifyJsOptions.module !== "undefined") {
  408. delete uglifyJsOptions.module;
  409. }
  410. // Need deep copy objects to avoid https://github.com/terser/terser/issues/366
  411. return {
  412. ...uglifyJsOptions,
  413. // warnings: uglifyJsOptions.warnings,
  414. parse: {
  415. ...uglifyJsOptions.parse
  416. },
  417. compress: typeof uglifyJsOptions.compress === "boolean" ? uglifyJsOptions.compress : {
  418. ...uglifyJsOptions.compress
  419. },
  420. mangle:
  421. // eslint-disable-next-line no-eq-null, eqeqeq
  422. uglifyJsOptions.mangle == null ? true : typeof uglifyJsOptions.mangle === "boolean" ? uglifyJsOptions.mangle : {
  423. ...uglifyJsOptions.mangle
  424. },
  425. output: {
  426. beautify: false,
  427. ...uglifyJsOptions.output
  428. },
  429. // Ignoring sourceMap from options
  430. sourceMap: undefined
  431. // toplevel: uglifyJsOptions.toplevel
  432. // nameCache: { ...uglifyJsOptions.toplevel },
  433. // ie8: uglifyJsOptions.ie8,
  434. // keep_fnames: uglifyJsOptions.keep_fnames,
  435. };
  436. };
  437. let minify;
  438. try {
  439. ({
  440. minify
  441. } = require("uglify-js"));
  442. } catch (err) {
  443. return {
  444. errors: [(/** @type {Error} */err)]
  445. };
  446. }
  447. // Copy `uglify-js` options
  448. const uglifyJsOptions = buildUglifyJsOptions(minimizerOptions);
  449. // Let `uglify-js` generate a SourceMap. The dispatcher in `minify.js`
  450. // chains the previous step's map onto this one.
  451. if (sourceMap) {
  452. uglifyJsOptions.sourceMap = true;
  453. }
  454. /** @type {ExtractedComments} */
  455. const extractedComments = [];
  456. // @ts-expect-error wrong types in uglify-js
  457. uglifyJsOptions.output.comments = buildComments(uglifyJsOptions, extractedComments);
  458. const [[filename, code]] = Object.entries(input);
  459. const result = await minify({
  460. [filename]: code
  461. }, uglifyJsOptions);
  462. return {
  463. code: result.code,
  464. map: result.map ? JSON.parse(result.map) : undefined,
  465. errors: result.error ? [result.error] : [],
  466. warnings: result.warnings || [],
  467. extractedComments
  468. };
  469. }
  470. /**
  471. * @returns {string | undefined} the minimizer version
  472. */
  473. uglifyJsMinify.getMinimizerVersion = () => {
  474. let packageJson;
  475. try {
  476. packageJson = require("uglify-js/package.json");
  477. } catch (_err) {
  478. // Ignore
  479. }
  480. return packageJson && packageJson.version;
  481. };
  482. /**
  483. * @returns {boolean | undefined} true if worker thread is supported, false otherwise
  484. */
  485. uglifyJsMinify.supportsWorkerThreads = () => true;
  486. /**
  487. * The language this minifies, for a caller dispatching source that carries
  488. * no filename of its own.
  489. * @returns {string[]} the languages
  490. */
  491. uglifyJsMinify.getTypes = () => ["javascript"];
  492. /**
  493. * @param {string} name asset name
  494. * @returns {boolean} true if `name` looks like a JavaScript file
  495. */
  496. uglifyJsMinify.filter = name => JS_FILE_RE.test(name);
  497. /* istanbul ignore next */
  498. /**
  499. * @param {Input} input input
  500. * @param {RawSourceMap=} sourceMap source map
  501. * @param {CustomOptions=} minimizerOptions options
  502. * @param {ExtractCommentsOptions=} extractComments extract comments option
  503. * @returns {Promise<MinimizedResult>} minimized result
  504. */
  505. async function swcMinify(input, sourceMap, minimizerOptions, extractComments) {
  506. /**
  507. * @param {unknown} value value
  508. * @returns {boolean} true when value is object or function
  509. */
  510. const isObject = value => {
  511. const type = typeof value;
  512. // eslint-disable-next-line no-eq-null, eqeqeq
  513. return value != null && (type === "object" || type === "function");
  514. };
  515. /**
  516. * @param {unknown} extractCommentsOptions extract comments option
  517. * @returns {Error} error for unsupported extract comments option
  518. */
  519. const createExtractCommentsError = extractCommentsOptions => new Error(`The 'extractComments' option for 'swcMinify' only supports booleans, "some", "all", string patterns, RegExp values without flags, or object conditions that resolve to those forms. Received: ${extractCommentsOptions instanceof RegExp ? extractCommentsOptions.toString() : typeof extractCommentsOptions}.`);
  520. /**
  521. * @param {unknown} extractCommentsOptions extract comments option
  522. * @returns {{ extractComments: false | true | "some" | "all" | { regex: string }, useDefaultPreserveComments: boolean }} normalized swc extract comments options
  523. */
  524. const normalizeExtractComments = extractCommentsOptions => {
  525. if (typeof extractCommentsOptions === "boolean") {
  526. return {
  527. extractComments: extractCommentsOptions,
  528. useDefaultPreserveComments: !extractCommentsOptions
  529. };
  530. }
  531. if (typeof extractCommentsOptions === "string") {
  532. return {
  533. extractComments: extractCommentsOptions === "some" || extractCommentsOptions === "all" ? extractCommentsOptions : {
  534. regex: extractCommentsOptions
  535. },
  536. useDefaultPreserveComments: false
  537. };
  538. }
  539. if (extractCommentsOptions instanceof RegExp) {
  540. if (extractCommentsOptions.flags) {
  541. throw createExtractCommentsError(extractCommentsOptions);
  542. }
  543. return {
  544. extractComments: {
  545. regex: extractCommentsOptions.source
  546. },
  547. useDefaultPreserveComments: false
  548. };
  549. }
  550. if (typeof extractCommentsOptions === "function") {
  551. throw createExtractCommentsError(extractCommentsOptions);
  552. }
  553. if (extractCommentsOptions && isObject(extractCommentsOptions)) {
  554. const {
  555. condition = "some"
  556. } = /** @type {{ condition?: unknown }} */
  557. extractCommentsOptions;
  558. if (typeof condition === "boolean") {
  559. return {
  560. extractComments: condition ? "some" : false,
  561. useDefaultPreserveComments: false
  562. };
  563. }
  564. if (typeof condition === "string") {
  565. return {
  566. extractComments: condition === "some" || condition === "all" ? condition : {
  567. regex: condition
  568. },
  569. useDefaultPreserveComments: false
  570. };
  571. }
  572. if (condition instanceof RegExp) {
  573. if (condition.flags) {
  574. throw createExtractCommentsError(condition);
  575. }
  576. return {
  577. extractComments: {
  578. regex: condition.source
  579. },
  580. useDefaultPreserveComments: false
  581. };
  582. }
  583. throw createExtractCommentsError(condition);
  584. }
  585. return {
  586. extractComments: false,
  587. useDefaultPreserveComments: false
  588. };
  589. };
  590. /**
  591. * @param {import("@swc/core").JsMinifyOptions=} swcOptions swc options
  592. * @returns {import("@swc/core").JsMinifyOptions & { extractComments?: false | true | "some" | "all" | { regex: string } } & { sourceMap: undefined | boolean } & { compress: import("@swc/core").TerserCompressOptions }} built swc options
  593. */
  594. const buildSwcOptions = (swcOptions = {}) => (
  595. // Need deep copy objects to avoid https://github.com/terser/terser/issues/366
  596. {
  597. ...swcOptions,
  598. compress: typeof swcOptions.compress === "boolean" ? swcOptions.compress ? {} : false : {
  599. ...swcOptions.compress
  600. },
  601. mangle:
  602. // eslint-disable-next-line no-eq-null, eqeqeq
  603. swcOptions.mangle == null ? true : typeof swcOptions.mangle === "boolean" ? swcOptions.mangle : {
  604. ...swcOptions.mangle
  605. },
  606. format: {
  607. ...swcOptions.format
  608. },
  609. // ecma: swcOptions.ecma,
  610. // keep_classnames: swcOptions.keep_classnames,
  611. // keep_fnames: swcOptions.keep_fnames,
  612. // module: swcOptions.module,
  613. // safari10: swcOptions.safari10,
  614. // toplevel: swcOptions.toplevel
  615. sourceMap: undefined
  616. });
  617. let swc;
  618. try {
  619. swc = require("@swc/core");
  620. } catch (err) {
  621. return {
  622. errors: [(/** @type {Error} */err)]
  623. };
  624. }
  625. // Copy `swc` options
  626. const swcOptions = buildSwcOptions(minimizerOptions);
  627. const normalizedExtractComments = normalizeExtractComments(extractComments);
  628. if (!swcOptions.format) {
  629. swcOptions.format = {};
  630. }
  631. // Let `swc` generate a SourceMap.
  632. if (sourceMap) {
  633. swcOptions.sourceMap = true;
  634. }
  635. if (normalizedExtractComments.useDefaultPreserveComments && typeof swcOptions.format.comments === "undefined") {
  636. swcOptions.format.comments = "some";
  637. }
  638. if (normalizedExtractComments.extractComments !== false) {
  639. /** @type {import("@swc/core").JsMinifyOptions & { extractComments?: false | true | "some" | "all" | { regex: string } }} */
  640. swcOptions.extractComments = normalizedExtractComments.extractComments;
  641. }
  642. if (swcOptions.compress) {
  643. // More optimizations
  644. if (typeof swcOptions.compress.ecma === "undefined") {
  645. swcOptions.compress.ecma = swcOptions.ecma;
  646. }
  647. // https://github.com/webpack/webpack/issues/16135
  648. if (swcOptions.ecma === 5 && typeof swcOptions.compress.arrows === "undefined") {
  649. swcOptions.compress.arrows = false;
  650. }
  651. }
  652. const [[filename, code]] = Object.entries(input);
  653. const result = /** @type {import("@swc/core").Output & { extractedComments?: string[] }} */
  654. await swc.minify(code, swcOptions);
  655. let map;
  656. if (result.map) {
  657. map = JSON.parse(result.map);
  658. // TODO workaround for swc because `filename` is not preset as in `swc` signature as for `terser`
  659. map.sources = [filename];
  660. delete map.sourcesContent;
  661. }
  662. return {
  663. code: result.code,
  664. map,
  665. extractedComments: result.extractedComments || []
  666. };
  667. }
  668. /**
  669. * @returns {string | undefined} the minimizer version
  670. */
  671. swcMinify.getMinimizerVersion = () => {
  672. let packageJson;
  673. try {
  674. packageJson = require("@swc/core/package.json");
  675. } catch (_err) {
  676. // Ignore
  677. }
  678. return packageJson && packageJson.version;
  679. };
  680. /**
  681. * @returns {boolean | undefined} true if worker thread is supported, false otherwise
  682. */
  683. swcMinify.supportsWorkerThreads = () => false;
  684. /**
  685. * The language this minifies, for a caller dispatching source that carries
  686. * no filename of its own.
  687. * @returns {string[]} the languages
  688. */
  689. swcMinify.getTypes = () => ["javascript"];
  690. /**
  691. * @param {string} name asset name
  692. * @returns {boolean} true if `name` looks like a JavaScript file
  693. */
  694. swcMinify.filter = name => JS_FILE_RE.test(name);
  695. /* istanbul ignore next */
  696. /**
  697. * @param {Input} input input
  698. * @param {RawSourceMap=} sourceMap source map
  699. * @param {CustomOptions=} minimizerOptions options
  700. * @returns {Promise<MinimizedResult>} minimized result
  701. */
  702. async function esbuildMinify(input, sourceMap, minimizerOptions) {
  703. /**
  704. * @param {import("esbuild").TransformOptions & { ecma?: string | number, module?: boolean }=} esbuildOptions esbuild options
  705. * @returns {import("esbuild").TransformOptions} built esbuild options
  706. */
  707. const buildEsbuildOptions = (esbuildOptions = {}) => {
  708. delete esbuildOptions.ecma;
  709. if (esbuildOptions.module) {
  710. esbuildOptions.format = "esm";
  711. }
  712. delete esbuildOptions.module;
  713. // Need deep copy objects to avoid https://github.com/terser/terser/issues/366
  714. return {
  715. minify: true,
  716. legalComments: "inline",
  717. ...esbuildOptions,
  718. sourcemap: false
  719. };
  720. };
  721. let esbuild;
  722. try {
  723. esbuild = require("esbuild");
  724. } catch (err) {
  725. return {
  726. errors: [(/** @type {Error} */err)]
  727. };
  728. }
  729. // Copy `esbuild` options
  730. const esbuildOptions = buildEsbuildOptions(minimizerOptions);
  731. // Let `esbuild` generate a SourceMap
  732. if (sourceMap) {
  733. esbuildOptions.sourcemap = true;
  734. esbuildOptions.sourcesContent = false;
  735. }
  736. const [[filename, code]] = Object.entries(input);
  737. esbuildOptions.sourcefile = filename;
  738. const result = await esbuild.transform(code, esbuildOptions);
  739. return {
  740. code: result.code,
  741. map: result.map ? JSON.parse(result.map) : undefined,
  742. warnings: result.warnings.length > 0 ? result.warnings.map(item => {
  743. const plugin = item.pluginName ? `\nPlugin Name: ${item.pluginName}` : "";
  744. const location = item.location ? `\n\n${item.location.file}:${item.location.line}:${item.location.column}:\n ${item.location.line} | ${item.location.lineText}\n\nSuggestion: ${item.location.suggestion}` : "";
  745. const notes = 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")}` : "";
  746. return `${item.text} [${item.id}]${plugin}${location}${item.detail ? `\nDetails:\n${item.detail}` : ""}${notes}`;
  747. }) : []
  748. };
  749. }
  750. /**
  751. * @returns {string | undefined} the minimizer version
  752. */
  753. esbuildMinify.getMinimizerVersion = () => {
  754. let packageJson;
  755. try {
  756. packageJson = require("esbuild/package.json");
  757. } catch (_err) {
  758. // Ignore
  759. }
  760. return packageJson && packageJson.version;
  761. };
  762. /**
  763. * @returns {boolean | undefined} true if worker thread is supported, false otherwise
  764. */
  765. esbuildMinify.supportsWorkerThreads = () => false;
  766. /**
  767. * The language this minifies, for a caller dispatching source that carries
  768. * no filename of its own.
  769. * @returns {string[]} the languages
  770. */
  771. esbuildMinify.getTypes = () => ["javascript"];
  772. /**
  773. * @param {string} name asset name
  774. * @returns {boolean} true if `name` looks like a JavaScript file
  775. */
  776. esbuildMinify.filter = name => JS_FILE_RE.test(name);
  777. /* istanbul ignore next */
  778. /**
  779. * @param {Input} input input
  780. * @param {RawSourceMap=} sourceMap source map
  781. * @param {CustomOptions=} minimizerOptions options
  782. * @returns {Promise<MinimizedResult>} minimized result
  783. */
  784. async function jsonMinify(input, sourceMap, minimizerOptions) {
  785. const options = /** @type {{ replacer?: Parameters<typeof JSON.stringify>[1], space?: Parameters<typeof JSON.stringify>[2] }} */
  786. minimizerOptions;
  787. const [[, code]] = Object.entries(input);
  788. const result = JSON.stringify(JSON.parse(code), options.replacer, options.space);
  789. return {
  790. code: result
  791. };
  792. }
  793. jsonMinify.getMinimizerVersion = () => "1.0.0";
  794. jsonMinify.supportsWorker = () => false;
  795. jsonMinify.supportsWorkerThreads = () => false;
  796. /**
  797. * The language this minifies, for a caller dispatching source that carries
  798. * no filename of its own.
  799. * @returns {string[]} the languages
  800. */
  801. jsonMinify.getTypes = () => ["json"];
  802. /**
  803. * @param {string} name asset name
  804. * @returns {boolean} true if `name` looks like a JSON file
  805. */
  806. jsonMinify.filter = name => JSON_FILE_RE.test(name);
  807. /* istanbul ignore next */
  808. /**
  809. * Minify HTML using `html-minifier-terser`.
  810. * @param {Input} input input
  811. * @param {RawSourceMap=} sourceMap source map (ignored for HTML)
  812. * @param {CustomOptions=} minimizerOptions options
  813. * @returns {Promise<MinimizedResult>} minimized result
  814. */
  815. async function htmlMinifierTerser(input, sourceMap, minimizerOptions) {
  816. let htmlMinifier;
  817. try {
  818. htmlMinifier = require("html-minifier-terser");
  819. } catch (err) {
  820. return {
  821. errors: [(/** @type {Error} */err)]
  822. };
  823. }
  824. const [[, code]] = Object.entries(input);
  825. /** @type {import("html-minifier-terser").Options} */
  826. const defaultMinimizerOptions = {
  827. caseSensitive: true,
  828. // `collapseBooleanAttributes` is not always safe, since this can break CSS attribute selectors and not safe for XHTML
  829. collapseWhitespace: true,
  830. conservativeCollapse: true,
  831. keepClosingSlash: true,
  832. // We need ability to use cssnano, or setup own function without extra dependencies
  833. minifyCSS: true,
  834. minifyJS: true,
  835. // `minifyURLs` is unsafe, because we can't guarantee what the base URL is
  836. // `removeAttributeQuotes` is not safe in some rare cases, also HTML spec recommends against doing this
  837. removeComments: true,
  838. // `removeEmptyAttributes` is not safe, can affect certain style or script behavior, look at https://github.com/webpack-contrib/html-loader/issues/323
  839. // `removeRedundantAttributes` is not safe, can affect certain style or script behavior, look at https://github.com/webpack-contrib/html-loader/issues/323
  840. removeScriptTypeAttributes: true,
  841. removeStyleLinkTypeAttributes: true
  842. // `useShortDoctype` is not safe for XHTML
  843. };
  844. const result = await htmlMinifier.minify(code, {
  845. ...defaultMinimizerOptions,
  846. ...(/** @type {import("html-minifier-terser").Options} */minimizerOptions)
  847. });
  848. return {
  849. code: result
  850. };
  851. }
  852. /**
  853. * @returns {string | undefined} the minimizer version
  854. */
  855. htmlMinifierTerser.getMinimizerVersion = () => {
  856. let packageJson;
  857. try {
  858. packageJson = require("html-minifier-terser/package.json");
  859. } catch (_err) {
  860. // Ignore
  861. }
  862. return packageJson && packageJson.version;
  863. };
  864. /**
  865. * @returns {boolean | undefined} true if worker threads are supported
  866. */
  867. htmlMinifierTerser.supportsWorkerThreads = () => true;
  868. /**
  869. * The language this minifies, for a caller dispatching source that carries
  870. * no filename of its own.
  871. * @returns {string[]} the languages
  872. */
  873. htmlMinifierTerser.getTypes = () => ["html"];
  874. /**
  875. * @param {string} name asset name
  876. * @returns {boolean} true if `name` looks like an HTML file
  877. */
  878. htmlMinifierTerser.filter = name => HTML_FILE_RE.test(name);
  879. /* istanbul ignore next */
  880. /**
  881. * Minify HTML using `@minify-html/node`.
  882. * @param {Input} input input
  883. * @param {RawSourceMap=} sourceMap source map (ignored for HTML)
  884. * @param {CustomOptions=} minimizerOptions options
  885. * @returns {Promise<MinimizedResult>} minimized result
  886. */
  887. async function minifyHtmlNode(input, sourceMap, minimizerOptions) {
  888. let minifyHtmlPkg;
  889. try {
  890. minifyHtmlPkg = require("@minify-html/node");
  891. } catch (err) {
  892. return {
  893. errors: [(/** @type {Error} */err)]
  894. };
  895. }
  896. const [[, code]] = Object.entries(input);
  897. const options = /** @type {Parameters<import("@minify-html/node").minify>[1]} */{
  898. ...minimizerOptions
  899. };
  900. const result = await minifyHtmlPkg.minify(Buffer.from(code), options);
  901. return {
  902. code: result.toString()
  903. };
  904. }
  905. /**
  906. * @returns {string | undefined} the minimizer version
  907. */
  908. minifyHtmlNode.getMinimizerVersion = () => {
  909. let packageJson;
  910. try {
  911. packageJson = require("@minify-html/node/package.json");
  912. } catch (_err) {
  913. // Ignore
  914. }
  915. return packageJson && packageJson.version;
  916. };
  917. /**
  918. * @returns {boolean | undefined} false because `@minify-html/node` is a native binding
  919. */
  920. minifyHtmlNode.supportsWorkerThreads = () => false;
  921. /**
  922. * The language this minifies, for a caller dispatching source that carries
  923. * no filename of its own.
  924. * @returns {string[]} the languages
  925. */
  926. minifyHtmlNode.getTypes = () => ["html"];
  927. /**
  928. * @param {string} name asset name
  929. * @returns {boolean} true if `name` looks like an HTML file
  930. */
  931. minifyHtmlNode.filter = name => HTML_FILE_RE.test(name);
  932. /* istanbul ignore next */
  933. /**
  934. * Map an `@swc/html` diagnostic to a regular `Error`.
  935. * @param {EXPECTED_OBJECT} diagnostic diagnostic from `@swc/html`
  936. * @returns {Error} error preserving `span` and `level` from the diagnostic
  937. */
  938. function swcHtmlDiagnosticToError(diagnostic) {
  939. const typed = /** @type {{ message: string, span?: unknown, level?: unknown }} */
  940. diagnostic;
  941. /** @type {Error & { span?: unknown, level?: unknown }} */
  942. const error = new Error(typed.message);
  943. error.span = typed.span;
  944. error.level = typed.level;
  945. return error;
  946. }
  947. /* istanbul ignore next */
  948. /**
  949. * Minify a complete HTML document using `@swc/html`.
  950. * @param {Input} input input
  951. * @param {RawSourceMap=} sourceMap source map (ignored for HTML)
  952. * @param {CustomOptions=} minimizerOptions options
  953. * @returns {Promise<MinimizedResult>} minimized result
  954. */
  955. async function swcMinifyHtml(input, sourceMap, minimizerOptions) {
  956. let swcMinifier;
  957. try {
  958. swcMinifier = require("@swc/html");
  959. } catch (err) {
  960. return {
  961. errors: [(/** @type {Error} */err)]
  962. };
  963. }
  964. const [[, code]] = Object.entries(input);
  965. const options = /** @type {import("@swc/html").Options} */{
  966. ...minimizerOptions
  967. };
  968. const result = await swcMinifier.minify(Buffer.from(code), options);
  969. return {
  970. code: result.code,
  971. errors: result.errors ? result.errors.map(swcHtmlDiagnosticToError) : undefined
  972. };
  973. }
  974. /**
  975. * @returns {string | undefined} the minimizer version
  976. */
  977. swcMinifyHtml.getMinimizerVersion = () => {
  978. let packageJson;
  979. try {
  980. packageJson = require("@swc/html/package.json");
  981. } catch (_err) {
  982. // Ignore
  983. }
  984. return packageJson && packageJson.version;
  985. };
  986. /**
  987. * @returns {boolean | undefined} false because `@swc/html` is a native binding
  988. */
  989. swcMinifyHtml.supportsWorkerThreads = () => false;
  990. /**
  991. * The language this minifies, for a caller dispatching source that carries
  992. * no filename of its own.
  993. * @returns {string[]} the languages
  994. */
  995. swcMinifyHtml.getTypes = () => ["html"];
  996. /**
  997. * @param {string} name asset name
  998. * @returns {boolean} true if `name` looks like an HTML file
  999. */
  1000. swcMinifyHtml.filter = name => HTML_FILE_RE.test(name);
  1001. /* istanbul ignore next */
  1002. /**
  1003. * Minify an HTML fragment using `@swc/html`.
  1004. *
  1005. * Use this for partial HTML (e.g. inside `<template></template>` tags or
  1006. * HTML strings that are inserted into another document).
  1007. * @param {Input} input input
  1008. * @param {RawSourceMap=} sourceMap source map (ignored for HTML)
  1009. * @param {CustomOptions=} minimizerOptions options
  1010. * @returns {Promise<MinimizedResult>} minimized result
  1011. */
  1012. async function swcMinifyHtmlFragment(input, sourceMap, minimizerOptions) {
  1013. let swcMinifier;
  1014. try {
  1015. swcMinifier = require("@swc/html");
  1016. } catch (err) {
  1017. return {
  1018. errors: [(/** @type {Error} */err)]
  1019. };
  1020. }
  1021. const [[, code]] = Object.entries(input);
  1022. const options = /** @type {import("@swc/html").FragmentOptions} */{
  1023. ...minimizerOptions
  1024. };
  1025. const result = await swcMinifier.minifyFragment(Buffer.from(code), options);
  1026. return {
  1027. code: result.code,
  1028. errors: result.errors ? result.errors.map(swcHtmlDiagnosticToError) : undefined
  1029. };
  1030. }
  1031. /**
  1032. * @returns {string | undefined} the minimizer version
  1033. */
  1034. swcMinifyHtmlFragment.getMinimizerVersion = () => {
  1035. let packageJson;
  1036. try {
  1037. packageJson = require("@swc/html/package.json");
  1038. } catch (_err) {
  1039. // Ignore
  1040. }
  1041. return packageJson && packageJson.version;
  1042. };
  1043. /**
  1044. * @returns {boolean | undefined} false because `@swc/html` is a native binding
  1045. */
  1046. swcMinifyHtmlFragment.supportsWorkerThreads = () => false;
  1047. /**
  1048. * The language this minifies, for a caller dispatching source that carries
  1049. * no filename of its own.
  1050. * @returns {string[]} the languages
  1051. */
  1052. swcMinifyHtmlFragment.getTypes = () => ["html"];
  1053. /**
  1054. * @param {string} name asset name
  1055. * @returns {boolean} true if `name` looks like an HTML file
  1056. */
  1057. swcMinifyHtmlFragment.filter = name => HTML_FILE_RE.test(name);
  1058. /* istanbul ignore next */
  1059. /**
  1060. * Minify CSS using `cssnano` (via `postcss`).
  1061. * @param {Input} input input
  1062. * @param {RawSourceMap=} sourceMap source map
  1063. * @param {CustomOptions=} minimizerOptions options
  1064. * @returns {Promise<MinimizedResult>} minimized result
  1065. */
  1066. async function cssnanoMinify(input, sourceMap, minimizerOptions = {
  1067. preset: "default"
  1068. }) {
  1069. /**
  1070. * @template T
  1071. * @param {string} mod module to load
  1072. * @returns {Promise<T>} loaded module
  1073. */
  1074. const load = async mod => {
  1075. let exports;
  1076. try {
  1077. exports = require(mod);
  1078. return exports;
  1079. } catch (err) {
  1080. let importESM;
  1081. try {
  1082. // eslint-disable-next-line no-new-func
  1083. importESM = new Function("id", "return import(id);");
  1084. } catch (_err) {
  1085. importESM = null;
  1086. }
  1087. if (/** @type {Error & { code: string }} */
  1088. err.code === "ERR_REQUIRE_ESM" && importESM) {
  1089. exports = await importESM(mod);
  1090. return exports.default;
  1091. }
  1092. throw err;
  1093. }
  1094. };
  1095. let postcss;
  1096. let cssnano;
  1097. try {
  1098. postcss = require("postcss");
  1099. // cssnano exposes itself only through the `exports` field, which neither
  1100. // `moduleResolution: "node"` nor the `import` plugin's node resolver reads
  1101. // @ts-expect-error
  1102. // eslint-disable-next-line import/no-unresolved
  1103. cssnano = require("cssnano");
  1104. } catch (err) {
  1105. return {
  1106. errors: [(/** @type {Error} */err)]
  1107. };
  1108. }
  1109. const [[name, code]] = Object.entries(input);
  1110. /** @type {import("postcss").ProcessOptions} */
  1111. const postcssOptions = {
  1112. from: name,
  1113. ... /** @type {{ processorOptions?: import("postcss").ProcessOptions }} */minimizerOptions.processorOptions
  1114. };
  1115. if (typeof postcssOptions.parser === "string") {
  1116. try {
  1117. postcssOptions.parser = await load(postcssOptions.parser);
  1118. } catch (error) {
  1119. throw new Error(`Loading PostCSS "${postcssOptions.parser}" parser failed: ${ /** @type {Error} */error.message}\n\n(@${name})`, {
  1120. cause: error
  1121. });
  1122. }
  1123. }
  1124. if (typeof postcssOptions.stringifier === "string") {
  1125. try {
  1126. postcssOptions.stringifier = await load(postcssOptions.stringifier);
  1127. } catch (error) {
  1128. throw new Error(`Loading PostCSS "${postcssOptions.stringifier}" stringifier failed: ${ /** @type {Error} */error.message}\n\n(@${name})`, {
  1129. cause: error
  1130. });
  1131. }
  1132. }
  1133. if (typeof postcssOptions.syntax === "string") {
  1134. try {
  1135. postcssOptions.syntax = await load(postcssOptions.syntax);
  1136. } catch (error) {
  1137. throw new Error(`Loading PostCSS "${postcssOptions.syntax}" syntax failed: ${ /** @type {Error} */error.message}\n\n(@${name})`, {
  1138. cause: error
  1139. });
  1140. }
  1141. }
  1142. if (sourceMap) {
  1143. postcssOptions.map = {
  1144. annotation: false
  1145. };
  1146. }
  1147. const result = await postcss.default([cssnano(minimizerOptions)]).process(code, postcssOptions);
  1148. return {
  1149. code: result.css,
  1150. map: result.map ? (/** @type {RawSourceMap} */
  1151. /** @type {unknown} */result.map.toJSON()) : undefined,
  1152. warnings: result.warnings().map(String)
  1153. };
  1154. }
  1155. /**
  1156. * @returns {string | undefined} the minimizer version
  1157. */
  1158. cssnanoMinify.getMinimizerVersion = () => {
  1159. let packageJson;
  1160. try {
  1161. packageJson = require("cssnano/package.json");
  1162. } catch (_err) {
  1163. // Ignore
  1164. }
  1165. return packageJson && packageJson.version;
  1166. };
  1167. /**
  1168. * @returns {boolean | undefined} true if worker threads are supported
  1169. */
  1170. cssnanoMinify.supportsWorkerThreads = () => true;
  1171. /**
  1172. * The language this minifies, for a caller dispatching source that carries
  1173. * no filename of its own.
  1174. * @returns {string[]} the languages
  1175. */
  1176. cssnanoMinify.getTypes = () => ["css"];
  1177. /**
  1178. * @param {string} name asset name
  1179. * @returns {boolean} true if `name` looks like a CSS file
  1180. */
  1181. cssnanoMinify.filter = name => CSS_FILE_RE.test(name);
  1182. /* istanbul ignore next */
  1183. /**
  1184. * Minify CSS using `csso`.
  1185. * @param {Input} input input
  1186. * @param {RawSourceMap=} sourceMap source map
  1187. * @param {CustomOptions=} minimizerOptions options
  1188. * @returns {Promise<MinimizedResult>} minimized result
  1189. */
  1190. async function cssoMinify(input, sourceMap, minimizerOptions) {
  1191. let csso;
  1192. try {
  1193. csso = require("csso");
  1194. } catch (err) {
  1195. return {
  1196. errors: [(/** @type {Error} */err)]
  1197. };
  1198. }
  1199. const [[filename, code]] = Object.entries(input);
  1200. const result = csso.minify(code, {
  1201. filename,
  1202. sourceMap: Boolean(sourceMap),
  1203. ...minimizerOptions
  1204. });
  1205. return {
  1206. code: result.css,
  1207. map: result.map ? (/** @type {RawSourceMap} */
  1208. /** @type {{ toJSON(): RawSourceMap }} */result.map.toJSON()) : undefined
  1209. };
  1210. }
  1211. /**
  1212. * @returns {string | undefined} the minimizer version
  1213. */
  1214. cssoMinify.getMinimizerVersion = () => {
  1215. let packageJson;
  1216. try {
  1217. packageJson = require("csso/package.json");
  1218. } catch (_err) {
  1219. // Ignore
  1220. }
  1221. return packageJson && packageJson.version;
  1222. };
  1223. /**
  1224. * @returns {boolean | undefined} true if worker threads are supported
  1225. */
  1226. cssoMinify.supportsWorkerThreads = () => true;
  1227. /**
  1228. * The language this minifies, for a caller dispatching source that carries
  1229. * no filename of its own.
  1230. * @returns {string[]} the languages
  1231. */
  1232. cssoMinify.getTypes = () => ["css"];
  1233. /**
  1234. * @param {string} name asset name
  1235. * @returns {boolean} true if `name` looks like a CSS file
  1236. */
  1237. cssoMinify.filter = name => CSS_FILE_RE.test(name);
  1238. /* istanbul ignore next */
  1239. /**
  1240. * Minify CSS using `clean-css`.
  1241. * @param {Input} input input
  1242. * @param {RawSourceMap=} sourceMap source map
  1243. * @param {CustomOptions=} minimizerOptions options
  1244. * @returns {Promise<MinimizedResult>} minimized result
  1245. */
  1246. async function cleanCssMinify(input, sourceMap, minimizerOptions) {
  1247. let CleanCSS;
  1248. try {
  1249. CleanCSS = require("clean-css");
  1250. } catch (err) {
  1251. return {
  1252. errors: [(/** @type {Error} */err)]
  1253. };
  1254. }
  1255. const [[name, code]] = Object.entries(input);
  1256. const result = await new CleanCSS({
  1257. sourceMap: Boolean(sourceMap),
  1258. ...minimizerOptions,
  1259. returnPromise: true
  1260. }).minify({
  1261. [name]: {
  1262. styles: code
  1263. }
  1264. });
  1265. const generatedSourceMap = result.sourceMap ? (/** @type {RawSourceMap} */
  1266. /** @type {{ toJSON(): RawSourceMap }} */(/** @type {unknown} */result.sourceMap).toJSON()) : undefined;
  1267. // workaround for source maps on windows
  1268. if (generatedSourceMap) {
  1269. const isWindowsPathSep = require("path").sep === "\\";
  1270. generatedSourceMap.sources = generatedSourceMap.sources.map(
  1271. /**
  1272. * @param {string | null} item path item
  1273. * @returns {string} normalized path
  1274. */
  1275. item => isWindowsPathSep ? (item || "").replace(/\\/g, "/") : item || "");
  1276. }
  1277. return {
  1278. code: result.styles,
  1279. map: generatedSourceMap,
  1280. warnings: result.warnings
  1281. };
  1282. }
  1283. /**
  1284. * @returns {string | undefined} the minimizer version
  1285. */
  1286. cleanCssMinify.getMinimizerVersion = () => {
  1287. let packageJson;
  1288. try {
  1289. packageJson = require("clean-css/package.json");
  1290. } catch (_err) {
  1291. // Ignore
  1292. }
  1293. return packageJson && packageJson.version;
  1294. };
  1295. /**
  1296. * @returns {boolean | undefined} true if worker threads are supported
  1297. */
  1298. cleanCssMinify.supportsWorkerThreads = () => true;
  1299. /**
  1300. * The language this minifies, for a caller dispatching source that carries
  1301. * no filename of its own.
  1302. * @returns {string[]} the languages
  1303. */
  1304. cleanCssMinify.getTypes = () => ["css"];
  1305. /**
  1306. * @param {string} name asset name
  1307. * @returns {boolean} true if `name` looks like a CSS file
  1308. */
  1309. cleanCssMinify.filter = name => CSS_FILE_RE.test(name);
  1310. /* istanbul ignore next */
  1311. /**
  1312. * Minify CSS using `esbuild` (with the CSS loader).
  1313. * @param {Input} input input
  1314. * @param {RawSourceMap=} sourceMap source map
  1315. * @param {CustomOptions=} minimizerOptions options
  1316. * @returns {Promise<MinimizedResult>} minimized result
  1317. */
  1318. async function esbuildMinifyCss(input, sourceMap, minimizerOptions) {
  1319. /**
  1320. * @param {import("esbuild").TransformOptions & { ecma?: string | number, module?: boolean }=} esbuildOptions esbuild options
  1321. * @returns {import("esbuild").TransformOptions} built esbuild options
  1322. */
  1323. const buildEsbuildOptions = (esbuildOptions = {}) => {
  1324. // `module` and `ecma` are JavaScript-only concepts; the dispatcher
  1325. // injects them for every minimizer, but esbuild's CSS transform
  1326. // rejects unknown options.
  1327. delete esbuildOptions.ecma;
  1328. delete esbuildOptions.module;
  1329. // Need deep copy objects to avoid https://github.com/terser/terser/issues/366
  1330. return {
  1331. loader: "css",
  1332. minify: true,
  1333. legalComments: "inline",
  1334. ...esbuildOptions,
  1335. sourcemap: false
  1336. };
  1337. };
  1338. let esbuild;
  1339. try {
  1340. esbuild = require("esbuild");
  1341. } catch (err) {
  1342. return {
  1343. errors: [(/** @type {Error} */err)]
  1344. };
  1345. }
  1346. // Copy `esbuild` options
  1347. const esbuildOptions = buildEsbuildOptions(minimizerOptions);
  1348. // Let `esbuild` generate a SourceMap
  1349. if (sourceMap) {
  1350. esbuildOptions.sourcemap = true;
  1351. esbuildOptions.sourcesContent = false;
  1352. }
  1353. const [[filename, code]] = Object.entries(input);
  1354. esbuildOptions.sourcefile = filename;
  1355. const result = await esbuild.transform(code, esbuildOptions);
  1356. return {
  1357. code: result.code,
  1358. map: result.map ? JSON.parse(result.map) : undefined,
  1359. warnings: result.warnings.length > 0 ? result.warnings.map(item => {
  1360. const plugin = item.pluginName ? `\nPlugin Name: ${item.pluginName}` : "";
  1361. const location = item.location ? `\n\n${item.location.file}:${item.location.line}:${item.location.column}:\n ${item.location.line} | ${item.location.lineText}\n\nSuggestion: ${item.location.suggestion}` : "";
  1362. const notes = 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")}` : "";
  1363. return `${item.text} [${item.id}]${plugin}${location}${item.detail ? `\nDetails:\n${item.detail}` : ""}${notes}`;
  1364. }) : []
  1365. };
  1366. }
  1367. /**
  1368. * @returns {string | undefined} the minimizer version
  1369. */
  1370. esbuildMinifyCss.getMinimizerVersion = () => {
  1371. let packageJson;
  1372. try {
  1373. packageJson = require("esbuild/package.json");
  1374. } catch (_err) {
  1375. // Ignore
  1376. }
  1377. return packageJson && packageJson.version;
  1378. };
  1379. /**
  1380. * @returns {boolean | undefined} false because `esbuild` is a native binding
  1381. */
  1382. esbuildMinifyCss.supportsWorkerThreads = () => false;
  1383. /**
  1384. * The language this minifies, for a caller dispatching source that carries
  1385. * no filename of its own.
  1386. * @returns {string[]} the languages
  1387. */
  1388. esbuildMinifyCss.getTypes = () => ["css"];
  1389. /**
  1390. * @param {string} name asset name
  1391. * @returns {boolean} true if `name` looks like a CSS file
  1392. */
  1393. esbuildMinifyCss.filter = name => CSS_FILE_RE.test(name);
  1394. /* istanbul ignore next */
  1395. /**
  1396. * Minify CSS using `lightningcss`.
  1397. * @param {Input} input input
  1398. * @param {RawSourceMap=} sourceMap source map
  1399. * @param {CustomOptions=} minimizerOptions options
  1400. * @returns {Promise<MinimizedResult>} minimized result
  1401. */
  1402. async function lightningCssMinify(input, sourceMap, minimizerOptions) {
  1403. let lightningCss;
  1404. try {
  1405. lightningCss = require("lightningcss");
  1406. } catch (err) {
  1407. return {
  1408. errors: [(/** @type {Error} */err)]
  1409. };
  1410. }
  1411. const [[filename, code]] = Object.entries(input);
  1412. /**
  1413. * @param {Partial<import("lightningcss").TransformOptions<import("lightningcss").CustomAtRules>>=} lightningCssOptions lightning css options
  1414. * @returns {import("lightningcss").TransformOptions<import("lightningcss").CustomAtRules>} built lightning css options
  1415. */
  1416. const buildLightningCssOptions = (lightningCssOptions = {}) => (
  1417. // Need deep copy objects to avoid https://github.com/terser/terser/issues/366
  1418. {
  1419. minify: true,
  1420. ...lightningCssOptions,
  1421. sourceMap: false,
  1422. filename,
  1423. code: new Uint8Array(Buffer.from(code))
  1424. });
  1425. // Copy `lightningCss` options
  1426. const lightningCssOptions = buildLightningCssOptions(minimizerOptions);
  1427. // Let `lightningcss` generate a SourceMap. The dispatcher in
  1428. // `minify.js` chains the previous step's map onto this one.
  1429. if (sourceMap) {
  1430. lightningCssOptions.sourceMap = true;
  1431. }
  1432. const result = lightningCss.transform(lightningCssOptions);
  1433. return {
  1434. code: result.code.toString(),
  1435. map: result.map ? JSON.parse(result.map.toString()) : undefined
  1436. };
  1437. }
  1438. /**
  1439. * @returns {string | undefined} the minimizer version
  1440. */
  1441. lightningCssMinify.getMinimizerVersion = () => {
  1442. let packageJson;
  1443. try {
  1444. packageJson = require("lightningcss/package.json");
  1445. } catch (_err) {
  1446. // Ignore
  1447. }
  1448. return packageJson && packageJson.version;
  1449. };
  1450. /**
  1451. * @returns {boolean | undefined} false because `lightningcss` is a native binding
  1452. */
  1453. lightningCssMinify.supportsWorkerThreads = () => false;
  1454. /**
  1455. * The language this minifies, for a caller dispatching source that carries
  1456. * no filename of its own.
  1457. * @returns {string[]} the languages
  1458. */
  1459. lightningCssMinify.getTypes = () => ["css"];
  1460. /**
  1461. * @param {string} name asset name
  1462. * @returns {boolean} true if `name` looks like a CSS file
  1463. */
  1464. lightningCssMinify.filter = name => CSS_FILE_RE.test(name);
  1465. /* istanbul ignore next */
  1466. /**
  1467. * Map a `@swc/css` diagnostic to a regular `Error`.
  1468. * @param {EXPECTED_OBJECT} diagnostic diagnostic from `@swc/css`
  1469. * @returns {Error} error preserving `span` and `level` from the diagnostic
  1470. */
  1471. function swcCssDiagnosticToError(diagnostic) {
  1472. const typed = /** @type {{ message: string, span?: unknown, level?: unknown }} */
  1473. diagnostic;
  1474. /** @type {Error & { span?: unknown, level?: unknown }} */
  1475. const error = new Error(typed.message);
  1476. error.span = typed.span;
  1477. error.level = typed.level;
  1478. return error;
  1479. }
  1480. /* istanbul ignore next */
  1481. /**
  1482. * Minify CSS using `@swc/css`.
  1483. * @param {Input} input input
  1484. * @param {RawSourceMap=} sourceMap source map
  1485. * @param {CustomOptions=} minimizerOptions options
  1486. * @returns {Promise<MinimizedResult>} minimized result
  1487. */
  1488. async function swcMinifyCss(input, sourceMap, minimizerOptions) {
  1489. let swc;
  1490. try {
  1491. swc = require("@swc/css");
  1492. } catch (err) {
  1493. return {
  1494. errors: [(/** @type {Error} */err)]
  1495. };
  1496. }
  1497. const [[filename, code]] = Object.entries(input);
  1498. /**
  1499. * @param {Partial<import("@swc/css").MinifyOptions>=} swcOptions swc options
  1500. * @returns {import("@swc/css").MinifyOptions} built swc options
  1501. */
  1502. const buildSwcOptions = (swcOptions = {}) => (
  1503. // Need deep copy objects to avoid https://github.com/terser/terser/issues/366
  1504. {
  1505. ...swcOptions,
  1506. filename
  1507. });
  1508. // Copy `swc` options
  1509. const swcOptions = buildSwcOptions(minimizerOptions);
  1510. // Let `swc` generate a SourceMap
  1511. if (sourceMap) {
  1512. swcOptions.sourceMap = true;
  1513. }
  1514. const result = await swc.minify(Buffer.from(code), swcOptions);
  1515. return {
  1516. code: result.code.toString(),
  1517. map: result.map ? JSON.parse(result.map.toString()) : undefined,
  1518. errors: result.errors ? result.errors.map(swcCssDiagnosticToError) : undefined
  1519. };
  1520. }
  1521. /**
  1522. * @returns {string | undefined} the minimizer version
  1523. */
  1524. swcMinifyCss.getMinimizerVersion = () => {
  1525. let packageJson;
  1526. try {
  1527. packageJson = require("@swc/css/package.json");
  1528. } catch (_err) {
  1529. // Ignore
  1530. }
  1531. return packageJson && packageJson.version;
  1532. };
  1533. /**
  1534. * @returns {boolean | undefined} false because `@swc/css` is a native binding
  1535. */
  1536. swcMinifyCss.supportsWorkerThreads = () => false;
  1537. /**
  1538. * The language this minifies, for a caller dispatching source that carries
  1539. * no filename of its own.
  1540. * @returns {string[]} the languages
  1541. */
  1542. swcMinifyCss.getTypes = () => ["css"];
  1543. /**
  1544. * @param {string} name asset name
  1545. * @returns {boolean} true if `name` looks like a CSS file
  1546. */
  1547. swcMinifyCss.filter = name => CSS_FILE_RE.test(name);
  1548. /**
  1549. * @template T
  1550. * @typedef {() => T} FunctionReturning
  1551. */
  1552. /**
  1553. * @template T
  1554. * @param {FunctionReturning<T>} fn memorized function
  1555. * @returns {FunctionReturning<T>} new function
  1556. */
  1557. function memoize(fn) {
  1558. let cache = false;
  1559. /** @type {T} */
  1560. let result;
  1561. return () => {
  1562. if (cache) {
  1563. return result;
  1564. }
  1565. result = fn();
  1566. cache = true;
  1567. // Allow to clean up memory for fn
  1568. // and all dependent resources
  1569. /** @type {FunctionReturning<T> | undefined} */
  1570. fn = undefined;
  1571. return /** @type {T} */result;
  1572. };
  1573. }
  1574. module.exports = {
  1575. cleanCssMinify,
  1576. cssnanoMinify,
  1577. cssoMinify,
  1578. esbuildMinify,
  1579. esbuildMinifyCss,
  1580. getEcmaVersion,
  1581. getMinimizerOptionsAt,
  1582. htmlMinifierTerser,
  1583. jsonMinify,
  1584. lightningCssMinify,
  1585. memoize,
  1586. minifyHtmlNode,
  1587. swcMinify,
  1588. swcMinifyCss,
  1589. swcMinifyHtml,
  1590. swcMinifyHtmlFragment,
  1591. terserMinify,
  1592. throttleAll,
  1593. uglifyJsMinify
  1594. };