utils.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. "use strict";
  2. /** @typedef {import("./index.js").Input} Input */
  3. /** @typedef {import("source-map").RawSourceMap} RawSourceMap */
  4. /** @typedef {import("source-map").SourceMapGenerator} SourceMapGenerator */
  5. /** @typedef {import("./index.js").MinimizedResult} MinimizedResult */
  6. /** @typedef {import("./index.js").CustomOptions} CustomOptions */
  7. /** @typedef {import("postcss").ProcessOptions} ProcessOptions */
  8. /** @typedef {import("postcss").Postcss} Postcss */
  9. const notSettled = Symbol(`not-settled`);
  10. /**
  11. * @template T
  12. * @typedef {() => Promise<T>} Task
  13. */
  14. /**
  15. * Run tasks with limited concurency.
  16. * @template T
  17. * @param {number} limit - Limit of tasks that run at once.
  18. * @param {Task<T>[]} tasks - List of tasks to run.
  19. * @returns {Promise<T[]>} A promise that fulfills to an array of the results
  20. */
  21. function throttleAll(limit, tasks) {
  22. if (!Number.isInteger(limit) || limit < 1) {
  23. throw new TypeError(`Expected \`limit\` to be a finite number > 0, got \`${limit}\` (${typeof limit})`);
  24. }
  25. if (!Array.isArray(tasks) || !tasks.every(task => typeof task === `function`)) {
  26. throw new TypeError(`Expected \`tasks\` to be a list of functions returning a promise`);
  27. }
  28. return new Promise((resolve, reject) => {
  29. const result = Array(tasks.length).fill(notSettled);
  30. const entries = tasks.entries();
  31. const next = () => {
  32. const {
  33. done,
  34. value
  35. } = entries.next();
  36. if (done) {
  37. const isLast = !result.includes(notSettled);
  38. if (isLast) resolve(result);
  39. return;
  40. }
  41. const [index, task] = value;
  42. /**
  43. * @param {T} x
  44. */
  45. const onFulfilled = x => {
  46. result[index] = x;
  47. next();
  48. };
  49. task().then(onFulfilled, reject);
  50. };
  51. Array(limit).fill(0).forEach(next);
  52. });
  53. }
  54. /* istanbul ignore next */
  55. /**
  56. * @param {Input} input
  57. * @param {RawSourceMap | undefined} sourceMap
  58. * @param {CustomOptions} minimizerOptions
  59. * @return {Promise<MinimizedResult>}
  60. */
  61. async function cssnanoMinify(input, sourceMap, minimizerOptions = {
  62. preset: "default"
  63. }) {
  64. /**
  65. * @template T
  66. * @param {string} module
  67. * @returns {Promise<T>}
  68. */
  69. const load = async module => {
  70. let exports;
  71. try {
  72. // eslint-disable-next-line import/no-dynamic-require, global-require
  73. exports = require(module);
  74. return exports;
  75. } catch (requireError) {
  76. let importESM;
  77. try {
  78. // eslint-disable-next-line no-new-func
  79. importESM = new Function("id", "return import(id);");
  80. } catch (e) {
  81. importESM = null;
  82. }
  83. if (
  84. /** @type {Error & {code: string}} */
  85. requireError.code === "ERR_REQUIRE_ESM" && importESM) {
  86. exports = await importESM(module);
  87. return exports.default;
  88. }
  89. throw requireError;
  90. }
  91. };
  92. const [[name, code]] = Object.entries(input);
  93. /** @type {ProcessOptions} */
  94. const postcssOptions = {
  95. from: name,
  96. ...minimizerOptions.processorOptions
  97. };
  98. if (typeof postcssOptions.parser === "string") {
  99. try {
  100. postcssOptions.parser = await load(postcssOptions.parser);
  101. } catch (error) {
  102. throw new Error(`Loading PostCSS "${postcssOptions.parser}" parser failed: ${
  103. /** @type {Error} */
  104. error.message}\n\n(@${name})`);
  105. }
  106. }
  107. if (typeof postcssOptions.stringifier === "string") {
  108. try {
  109. postcssOptions.stringifier = await load(postcssOptions.stringifier);
  110. } catch (error) {
  111. throw new Error(`Loading PostCSS "${postcssOptions.stringifier}" stringifier failed: ${
  112. /** @type {Error} */
  113. error.message}\n\n(@${name})`);
  114. }
  115. }
  116. if (typeof postcssOptions.syntax === "string") {
  117. try {
  118. postcssOptions.syntax = await load(postcssOptions.syntax);
  119. } catch (error) {
  120. throw new Error(`Loading PostCSS "${postcssOptions.syntax}" syntax failed: ${
  121. /** @type {Error} */
  122. error.message}\n\n(@${name})`);
  123. }
  124. }
  125. if (sourceMap) {
  126. postcssOptions.map = {
  127. annotation: false
  128. };
  129. }
  130. /** @type {Postcss} */
  131. // eslint-disable-next-line global-require
  132. const postcss = require("postcss").default; // @ts-ignore
  133. // eslint-disable-next-line global-require
  134. const cssnano = require("cssnano"); // @ts-ignore
  135. // Types are broken
  136. const result = await postcss([cssnano(minimizerOptions)]).process(code, postcssOptions);
  137. return {
  138. code: result.css,
  139. map: result.map ? result.map.toJSON() : // eslint-disable-next-line no-undefined
  140. undefined,
  141. warnings: result.warnings().map(String)
  142. };
  143. }
  144. /* istanbul ignore next */
  145. /**
  146. * @param {Input} input
  147. * @param {RawSourceMap | undefined} sourceMap
  148. * @param {CustomOptions} minimizerOptions
  149. * @return {Promise<MinimizedResult>}
  150. */
  151. async function cssoMinify(input, sourceMap, minimizerOptions) {
  152. // eslint-disable-next-line global-require,import/no-extraneous-dependencies
  153. const csso = require("csso");
  154. const [[filename, code]] = Object.entries(input);
  155. const result = csso.minify(code, {
  156. filename,
  157. sourceMap: Boolean(sourceMap),
  158. ...minimizerOptions
  159. });
  160. return {
  161. code: result.css,
  162. map: result.map ?
  163. /** @type {SourceMapGenerator & { toJSON(): RawSourceMap }} */
  164. result.map.toJSON() : // eslint-disable-next-line no-undefined
  165. undefined
  166. };
  167. }
  168. /* istanbul ignore next */
  169. /**
  170. * @param {Input} input
  171. * @param {RawSourceMap | undefined} sourceMap
  172. * @param {CustomOptions} minimizerOptions
  173. * @return {Promise<MinimizedResult>}
  174. */
  175. async function cleanCssMinify(input, sourceMap, minimizerOptions) {
  176. // eslint-disable-next-line global-require,import/no-extraneous-dependencies
  177. const CleanCSS = require("clean-css");
  178. const [[name, code]] = Object.entries(input);
  179. const result = await new CleanCSS({
  180. sourceMap: Boolean(sourceMap),
  181. ...minimizerOptions,
  182. returnPromise: true
  183. }).minify({
  184. [name]: {
  185. styles: code
  186. }
  187. });
  188. const generatedSourceMap = result.sourceMap &&
  189. /** @type {SourceMapGenerator & { toJSON(): RawSourceMap }} */
  190. result.sourceMap.toJSON(); // workaround for source maps on windows
  191. if (generatedSourceMap) {
  192. // eslint-disable-next-line global-require
  193. const isWindowsPathSep = require("path").sep === "\\";
  194. generatedSourceMap.sources = generatedSourceMap.sources.map(
  195. /**
  196. * @param {string} item
  197. * @returns {string}
  198. */
  199. item => isWindowsPathSep ? item.replace(/\\/g, "/") : item);
  200. }
  201. return {
  202. code: result.styles,
  203. map: generatedSourceMap,
  204. warnings: result.warnings
  205. };
  206. }
  207. /* istanbul ignore next */
  208. /**
  209. * @param {Input} input
  210. * @param {RawSourceMap | undefined} sourceMap
  211. * @param {CustomOptions} minimizerOptions
  212. * @return {Promise<MinimizedResult>}
  213. */
  214. async function esbuildMinify(input, sourceMap, minimizerOptions) {
  215. /**
  216. * @param {import("esbuild").TransformOptions} [esbuildOptions={}]
  217. * @returns {import("esbuild").TransformOptions}
  218. */
  219. const buildEsbuildOptions = (esbuildOptions = {}) => {
  220. // Need deep copy objects to avoid https://github.com/terser/terser/issues/366
  221. return {
  222. loader: "css",
  223. minify: true,
  224. legalComments: "inline",
  225. ...esbuildOptions,
  226. sourcemap: false
  227. };
  228. }; // eslint-disable-next-line import/no-extraneous-dependencies, global-require
  229. const esbuild = require("esbuild"); // Copy `esbuild` options
  230. const esbuildOptions = buildEsbuildOptions(minimizerOptions); // Let `esbuild` generate a SourceMap
  231. if (sourceMap) {
  232. esbuildOptions.sourcemap = true;
  233. esbuildOptions.sourcesContent = false;
  234. }
  235. const [[filename, code]] = Object.entries(input);
  236. esbuildOptions.sourcefile = filename;
  237. const result = await esbuild.transform(code, esbuildOptions);
  238. return {
  239. code: result.code,
  240. // eslint-disable-next-line no-undefined
  241. map: result.map ? JSON.parse(result.map) : undefined,
  242. warnings: result.warnings.length > 0 ? result.warnings.map(item => {
  243. return {
  244. source: item.location && item.location.file,
  245. // eslint-disable-next-line no-undefined
  246. line: item.location && item.location.line ? item.location.line : undefined,
  247. // eslint-disable-next-line no-undefined
  248. column: item.location && item.location.column ? item.location.column : undefined,
  249. plugin: item.pluginName,
  250. 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")}` : ""}`
  251. };
  252. }) : []
  253. };
  254. }
  255. /* istanbul ignore next */
  256. /**
  257. * @param {Input} input
  258. * @param {RawSourceMap | undefined} sourceMap
  259. * @param {CustomOptions} minimizerOptions
  260. * @return {Promise<MinimizedResult>}
  261. */
  262. async function parcelCssMinify(input, sourceMap, minimizerOptions) {
  263. const [[filename, code]] = Object.entries(input);
  264. /**
  265. * @param {Partial<import("@parcel/css").TransformOptions>} [parcelCssOptions={}]
  266. * @returns {import("@parcel/css").TransformOptions}
  267. */
  268. const buildParcelCssOptions = (parcelCssOptions = {}) => {
  269. // Need deep copy objects to avoid https://github.com/terser/terser/issues/366
  270. return {
  271. minify: true,
  272. ...parcelCssOptions,
  273. sourceMap: false,
  274. filename,
  275. code: Buffer.from(code)
  276. };
  277. }; // eslint-disable-next-line import/no-extraneous-dependencies, global-require
  278. const parcelCss = require("@parcel/css"); // Copy `esbuild` options
  279. const parcelCssOptions = buildParcelCssOptions(minimizerOptions); // Let `esbuild` generate a SourceMap
  280. if (sourceMap) {
  281. parcelCssOptions.sourceMap = true;
  282. }
  283. const result = await parcelCss.transform(parcelCssOptions);
  284. return {
  285. code: result.code.toString(),
  286. // eslint-disable-next-line no-undefined
  287. map: result.map ? JSON.parse(result.map.toString()) : undefined
  288. };
  289. }
  290. module.exports = {
  291. throttleAll,
  292. cssnanoMinify,
  293. cssoMinify,
  294. cleanCssMinify,
  295. esbuildMinify,
  296. parcelCssMinify
  297. };