index.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685
  1. "use strict";
  2. const os = require("os");
  3. const {
  4. SourceMapConsumer
  5. } = require("source-map");
  6. const {
  7. validate
  8. } = require("schema-utils");
  9. const serialize = require("serialize-javascript");
  10. const {
  11. Worker
  12. } = require("jest-worker");
  13. const {
  14. throttleAll,
  15. cssnanoMinify,
  16. cssoMinify,
  17. cleanCssMinify,
  18. esbuildMinify,
  19. parcelCssMinify
  20. } = require("./utils");
  21. const schema = require("./options.json");
  22. const {
  23. minify
  24. } = require("./minify");
  25. /** @typedef {import("schema-utils/declarations/validate").Schema} Schema */
  26. /** @typedef {import("webpack").Compiler} Compiler */
  27. /** @typedef {import("webpack").Compilation} Compilation */
  28. /** @typedef {import("webpack").WebpackError} WebpackError */
  29. /** @typedef {import("jest-worker").Worker} JestWorker */
  30. /** @typedef {import("source-map").RawSourceMap} RawSourceMap */
  31. /** @typedef {import("webpack").Asset} Asset */
  32. /** @typedef {import("postcss").ProcessOptions} ProcessOptions */
  33. /** @typedef {import("postcss").Syntax} Syntax */
  34. /** @typedef {import("postcss").Parser} Parser */
  35. /** @typedef {import("postcss").Stringifier} Stringifier */
  36. /**
  37. * @typedef {Object} CssNanoOptions
  38. * @property {string} [configFile]
  39. * @property {[string, object] | string | undefined} [preset]
  40. */
  41. /** @typedef {Error & { plugin?: string, text?: string, source?: string } | string} Warning */
  42. /**
  43. * @typedef {Object} WarningObject
  44. * @property {string} message
  45. * @property {string} [plugin]
  46. * @property {string} [text]
  47. * @property {number} [line]
  48. * @property {number} [column]
  49. */
  50. /**
  51. * @typedef {Object} ErrorObject
  52. * @property {string} message
  53. * @property {number} [line]
  54. * @property {number} [column]
  55. * @property {string} [stack]
  56. */
  57. /**
  58. * @typedef {Object} MinimizedResult
  59. * @property {string} code
  60. * @property {RawSourceMap} [map]
  61. * @property {Array<Error | ErrorObject| string>} [errors]
  62. * @property {Array<Warning | WarningObject | string>} [warnings]
  63. */
  64. /**
  65. * @typedef {{ [file: string]: string }} Input
  66. */
  67. /**
  68. * @typedef {{ [key: string]: any }} CustomOptions
  69. */
  70. /**
  71. * @template T
  72. * @typedef {T extends infer U ? U : CustomOptions} InferDefaultType
  73. */
  74. /**
  75. * @template T
  76. * @callback BasicMinimizerImplementation
  77. * @param {Input} input
  78. * @param {RawSourceMap | undefined} sourceMap
  79. * @param {InferDefaultType<T>} minifyOptions
  80. * @returns {Promise<MinimizedResult>}
  81. */
  82. /**
  83. * @template T
  84. * @typedef {T extends any[] ? { [P in keyof T]: BasicMinimizerImplementation<T[P]>; } : BasicMinimizerImplementation<T>} MinimizerImplementation
  85. */
  86. /**
  87. * @template T
  88. * @typedef {T extends any[] ? { [P in keyof T]?: InferDefaultType<T[P]> } : InferDefaultType<T>} MinimizerOptions
  89. */
  90. /**
  91. * @template T
  92. * @typedef {Object} InternalOptions
  93. * @property {string} name
  94. * @property {string} input
  95. * @property {RawSourceMap | undefined} inputSourceMap
  96. * @property {{ implementation: MinimizerImplementation<T>, options: MinimizerOptions<T> }} minimizer
  97. */
  98. /**
  99. * @typedef InternalResult
  100. * @property {Array<{ code: string, map: RawSourceMap | undefined }>} outputs
  101. * @property {Array<Warning | WarningObject | string>} warnings
  102. * @property {Array<Error | ErrorObject | string>} errors
  103. */
  104. /** @typedef {undefined | boolean | number} Parallel */
  105. /** @typedef {RegExp | string} Rule */
  106. /** @typedef {Rule[] | Rule} Rules */
  107. /** @typedef {(warning: Warning | WarningObject | string, file: string, source?: string) => boolean} WarningsFilter */
  108. /**
  109. * @typedef {Object} BasePluginOptions
  110. * @property {Rules} [test]
  111. * @property {Rules} [include]
  112. * @property {Rules} [exclude]
  113. * @property {WarningsFilter} [warningsFilter]
  114. * @property {Parallel} [parallel]
  115. */
  116. /**
  117. * @template T
  118. * @typedef {JestWorker & { transform: (options: string) => InternalResult, minify: (options: InternalOptions<T>) => InternalResult }} MinimizerWorker
  119. */
  120. /**
  121. * @typedef{ProcessOptions | { from?: string, to?: string, parser?: string | Syntax | Parser, stringifier?: string | Syntax | Stringifier, syntax?: string | Syntax } } ProcessOptionsExtender
  122. */
  123. /**
  124. * @typedef {CssNanoOptions & { processorOptions?: ProcessOptionsExtender }} CssNanoOptionsExtended
  125. */
  126. /**
  127. * @template T
  128. * @typedef {T extends CssNanoOptionsExtended ? { minify?: MinimizerImplementation<T> | undefined, minimizerOptions?: MinimizerOptions<T> | undefined } : { minify: MinimizerImplementation<T>, minimizerOptions?: MinimizerOptions<T> | undefined }} DefinedDefaultMinimizerAndOptions
  129. */
  130. /**
  131. * @template T
  132. * @typedef {BasePluginOptions & { minimizer: { implementation: MinimizerImplementation<T>, options: MinimizerOptions<T> } }} InternalPluginOptions
  133. */
  134. const warningRegex = /\s.+:+([0-9]+):+([0-9]+)/;
  135. /**
  136. * @template [T=CssNanoOptionsExtended]
  137. */
  138. class CssMinimizerPlugin {
  139. /**
  140. * @param {BasePluginOptions & DefinedDefaultMinimizerAndOptions<T>} [options]
  141. */
  142. constructor(options) {
  143. validate(
  144. /** @type {Schema} */
  145. schema, options || {}, {
  146. name: "Css Minimizer Plugin",
  147. baseDataPath: "options"
  148. });
  149. const {
  150. minify =
  151. /** @type {BasicMinimizerImplementation<T>} */
  152. cssnanoMinify,
  153. minimizerOptions =
  154. /** @type {MinimizerOptions<T>} */
  155. {},
  156. test = /\.css(\?.*)?$/i,
  157. warningsFilter = () => true,
  158. parallel = true,
  159. include,
  160. exclude
  161. } = options || {};
  162. /**
  163. * @private
  164. * @type {InternalPluginOptions<T>}
  165. */
  166. this.options = {
  167. test,
  168. warningsFilter,
  169. parallel,
  170. include,
  171. exclude,
  172. minimizer: {
  173. implementation:
  174. /** @type {MinimizerImplementation<T>} */
  175. minify,
  176. options: minimizerOptions
  177. }
  178. };
  179. }
  180. /**
  181. * @private
  182. * @param {any} input
  183. * @returns {boolean}
  184. */
  185. static isSourceMap(input) {
  186. // All required options for `new SourceMapConsumer(...options)`
  187. // https://github.com/mozilla/source-map#new-sourcemapconsumerrawsourcemap
  188. return Boolean(input && input.version && input.sources && Array.isArray(input.sources) && typeof input.mappings === "string");
  189. }
  190. /**
  191. * @private
  192. * @param {Warning | WarningObject | string} warning
  193. * @param {string} file
  194. * @param {WarningsFilter} [warningsFilter]
  195. * @param {SourceMapConsumer} [sourceMap]
  196. * @param {Compilation["requestShortener"]} [requestShortener]
  197. * @returns {Error & { hideStack?: boolean, file?: string } | undefined}
  198. */
  199. static buildWarning(warning, file, warningsFilter, sourceMap, requestShortener) {
  200. let warningMessage = typeof warning === "string" ? warning : `${warning.plugin ? `[${warning.plugin}] ` : ""}${warning.text || warning.message}`;
  201. let locationMessage = "";
  202. let source;
  203. if (sourceMap) {
  204. let line;
  205. let column;
  206. if (typeof warning === "string") {
  207. const match = warningRegex.exec(warning);
  208. if (match) {
  209. line = +match[1];
  210. column = +match[2];
  211. }
  212. } else {
  213. ({
  214. line,
  215. column
  216. } =
  217. /** @type {WarningObject} */
  218. warning);
  219. }
  220. if (line && column) {
  221. const original = sourceMap.originalPositionFor({
  222. line,
  223. column
  224. });
  225. if (original && original.source && original.source !== file && requestShortener) {
  226. ({
  227. source
  228. } = original);
  229. warningMessage = `${warningMessage.replace(warningRegex, "")}`;
  230. locationMessage = `${requestShortener.shorten(original.source)}:${original.line}:${original.column}`;
  231. }
  232. }
  233. }
  234. if (warningsFilter && !warningsFilter(warning, file, source)) {
  235. return;
  236. }
  237. /**
  238. * @type {Error & { hideStack?: boolean, file?: string }}
  239. */
  240. const builtWarning = new Error(`${file} from Css Minimizer plugin\n${warningMessage}${locationMessage ? ` ${locationMessage}` : ""}`);
  241. builtWarning.name = "Warning";
  242. builtWarning.hideStack = true;
  243. builtWarning.file = file; // eslint-disable-next-line consistent-return
  244. return builtWarning;
  245. }
  246. /**
  247. * @private
  248. * @param {Error | ErrorObject | string} error
  249. * @param {string} file
  250. * @param {SourceMapConsumer} [sourceMap]
  251. * @param {Compilation["requestShortener"]} [requestShortener]
  252. * @returns {Error}
  253. */
  254. static buildError(error, file, sourceMap, requestShortener) {
  255. /**
  256. * @type {Error & { file?: string }}
  257. */
  258. let builtError;
  259. if (typeof error === "string") {
  260. builtError = new Error(`${file} from Css Minimizer plugin\n${error}`);
  261. builtError.file = file;
  262. return builtError;
  263. }
  264. if (
  265. /** @type {ErrorObject} */
  266. error.line &&
  267. /** @type {ErrorObject} */
  268. error.column) {
  269. const {
  270. line,
  271. column
  272. } =
  273. /** @type {ErrorObject & { line: number, column: number }} */
  274. error;
  275. const original = sourceMap && sourceMap.originalPositionFor({
  276. line,
  277. column
  278. });
  279. if (original && original.source && requestShortener) {
  280. builtError = new Error(`${file} from Css Minimizer plugin\n${error.message} [${requestShortener.shorten(original.source)}:${original.line},${original.column}][${file}:${line},${column}]${error.stack ? `\n${error.stack.split("\n").slice(1).join("\n")}` : ""}`);
  281. builtError.file = file;
  282. return builtError;
  283. }
  284. builtError = new Error(`${file} from Css Minimizer plugin\n${error.message} [${file}:${line},${column}]${error.stack ? `\n${error.stack.split("\n").slice(1).join("\n")}` : ""}`);
  285. builtError.file = file;
  286. return builtError;
  287. }
  288. if (error.stack) {
  289. builtError = new Error(`${file} from Css Minimizer plugin\n${error.stack}`);
  290. builtError.file = file;
  291. return builtError;
  292. }
  293. builtError = new Error(`${file} from Css Minimizer plugin\n${error.message}`);
  294. builtError.file = file;
  295. return builtError;
  296. }
  297. /**
  298. * @private
  299. * @param {Parallel} parallel
  300. * @returns {number}
  301. */
  302. static getAvailableNumberOfCores(parallel) {
  303. // In some cases cpus() returns undefined
  304. // https://github.com/nodejs/node/issues/19022
  305. const cpus = os.cpus() || {
  306. length: 1
  307. };
  308. return parallel === true ? cpus.length - 1 : Math.min(Number(parallel) || 0, cpus.length - 1);
  309. }
  310. /**
  311. * @private
  312. * @param {Compiler} compiler
  313. * @param {Compilation} compilation
  314. * @param {Record<string, import("webpack").sources.Source>} assets
  315. * @param {{availableNumberOfCores: number}} optimizeOptions
  316. * @returns {Promise<void>}
  317. */
  318. async optimize(compiler, compilation, assets, optimizeOptions) {
  319. const cache = compilation.getCache("CssMinimizerWebpackPlugin");
  320. let numberOfAssetsForMinify = 0;
  321. const assetsForMinify = await Promise.all(Object.keys(typeof assets === "undefined" ? compilation.assets : assets).filter(name => {
  322. const {
  323. info
  324. } =
  325. /** @type {Asset} */
  326. compilation.getAsset(name);
  327. if ( // Skip double minimize assets from child compilation
  328. info.minimized) {
  329. return false;
  330. }
  331. if (!compiler.webpack.ModuleFilenameHelpers.matchObject.bind( // eslint-disable-next-line no-undefined
  332. undefined, this.options)(name)) {
  333. return false;
  334. }
  335. return true;
  336. }).map(async name => {
  337. const {
  338. info,
  339. source
  340. } =
  341. /** @type {Asset} */
  342. compilation.getAsset(name);
  343. const eTag = cache.getLazyHashedEtag(source);
  344. const cacheItem = cache.getItemCache(name, eTag);
  345. const output = await cacheItem.getPromise();
  346. if (!output) {
  347. numberOfAssetsForMinify += 1;
  348. }
  349. return {
  350. name,
  351. info,
  352. inputSource: source,
  353. output,
  354. cacheItem
  355. };
  356. }));
  357. if (assetsForMinify.length === 0) {
  358. return;
  359. }
  360. /** @type {undefined | (() => MinimizerWorker<T>)} */
  361. let getWorker;
  362. /** @type {undefined | MinimizerWorker<T>} */
  363. let initializedWorker;
  364. /** @type {undefined | number} */
  365. let numberOfWorkers;
  366. if (optimizeOptions.availableNumberOfCores > 0) {
  367. // Do not create unnecessary workers when the number of files is less than the available cores, it saves memory
  368. numberOfWorkers = Math.min(numberOfAssetsForMinify, optimizeOptions.availableNumberOfCores);
  369. getWorker = () => {
  370. if (initializedWorker) {
  371. return initializedWorker;
  372. }
  373. initializedWorker =
  374. /** @type {MinimizerWorker<T>} */
  375. new Worker(require.resolve("./minify"), {
  376. numWorkers: numberOfWorkers,
  377. enableWorkerThreads: true
  378. }); // https://github.com/facebook/jest/issues/8872#issuecomment-524822081
  379. const workerStdout = initializedWorker.getStdout();
  380. if (workerStdout) {
  381. workerStdout.on("data", chunk => process.stdout.write(chunk));
  382. }
  383. const workerStderr = initializedWorker.getStderr();
  384. if (workerStderr) {
  385. workerStderr.on("data", chunk => process.stderr.write(chunk));
  386. }
  387. return initializedWorker;
  388. };
  389. }
  390. const {
  391. SourceMapSource,
  392. RawSource
  393. } = compiler.webpack.sources;
  394. const scheduledTasks = [];
  395. for (const asset of assetsForMinify) {
  396. scheduledTasks.push(async () => {
  397. const {
  398. name,
  399. inputSource,
  400. cacheItem
  401. } = asset;
  402. let {
  403. output
  404. } = asset;
  405. if (!output) {
  406. let input;
  407. /** @type {RawSourceMap | undefined} */
  408. let inputSourceMap;
  409. const {
  410. source: sourceFromInputSource,
  411. map
  412. } = inputSource.sourceAndMap();
  413. input = sourceFromInputSource;
  414. if (map) {
  415. if (!CssMinimizerPlugin.isSourceMap(map)) {
  416. compilation.warnings.push(
  417. /** @type {WebpackError} */
  418. new Error(`${name} contains invalid source map`));
  419. } else {
  420. inputSourceMap =
  421. /** @type {RawSourceMap} */
  422. map;
  423. }
  424. }
  425. if (Buffer.isBuffer(input)) {
  426. input = input.toString();
  427. }
  428. /**
  429. * @type {InternalOptions<T>}
  430. */
  431. const options = {
  432. name,
  433. input,
  434. inputSourceMap,
  435. minimizer: {
  436. implementation: this.options.minimizer.implementation,
  437. options: this.options.minimizer.options
  438. }
  439. };
  440. let result;
  441. try {
  442. result = await (getWorker ? getWorker().transform(serialize(options)) : minify(options));
  443. } catch (error) {
  444. const hasSourceMap = inputSourceMap && CssMinimizerPlugin.isSourceMap(inputSourceMap);
  445. compilation.errors.push(
  446. /** @type {WebpackError} */
  447. CssMinimizerPlugin.buildError(
  448. /** @type {any} */
  449. error, name, hasSourceMap ? new SourceMapConsumer(
  450. /** @type {RawSourceMap} */
  451. inputSourceMap) : // eslint-disable-next-line no-undefined
  452. undefined, // eslint-disable-next-line no-undefined
  453. hasSourceMap ? compilation.requestShortener : undefined));
  454. return;
  455. }
  456. output = {
  457. warnings: [],
  458. errors: []
  459. };
  460. for (const item of result.outputs) {
  461. if (item.map) {
  462. let originalSource;
  463. let innerSourceMap;
  464. if (output.source) {
  465. ({
  466. source: originalSource,
  467. map: innerSourceMap
  468. } = output.source.sourceAndMap());
  469. } else {
  470. originalSource = input;
  471. innerSourceMap = inputSourceMap;
  472. } // TODO need API for merging source maps in `webpack-source`
  473. output.source = new SourceMapSource(item.code, name, item.map, originalSource, innerSourceMap, true);
  474. } else {
  475. output.source = new RawSource(item.code);
  476. }
  477. }
  478. if (result.errors && result.errors.length > 0) {
  479. const hasSourceMap = inputSourceMap && CssMinimizerPlugin.isSourceMap(inputSourceMap);
  480. for (const error of result.errors) {
  481. output.warnings.push(CssMinimizerPlugin.buildError(error, name, hasSourceMap ? new SourceMapConsumer(
  482. /** @type {RawSourceMap} */
  483. inputSourceMap) : // eslint-disable-next-line no-undefined
  484. undefined, // eslint-disable-next-line no-undefined
  485. hasSourceMap ? compilation.requestShortener : undefined));
  486. }
  487. }
  488. if (result.warnings && result.warnings.length > 0) {
  489. const hasSourceMap = inputSourceMap && CssMinimizerPlugin.isSourceMap(inputSourceMap);
  490. for (const warning of result.warnings) {
  491. const buildWarning = CssMinimizerPlugin.buildWarning(warning, name, this.options.warningsFilter, hasSourceMap ? new SourceMapConsumer(
  492. /** @type {RawSourceMap} */
  493. inputSourceMap) : // eslint-disable-next-line no-undefined
  494. undefined, // eslint-disable-next-line no-undefined
  495. hasSourceMap ? compilation.requestShortener : undefined);
  496. if (buildWarning) {
  497. output.warnings.push(buildWarning);
  498. }
  499. }
  500. }
  501. await cacheItem.storePromise({
  502. source: output.source,
  503. warnings: output.warnings,
  504. errors: output.errors
  505. });
  506. }
  507. if (output.warnings && output.warnings.length > 0) {
  508. for (const warning of output.warnings) {
  509. compilation.warnings.push(warning);
  510. }
  511. }
  512. if (output.errors && output.errors.length > 0) {
  513. for (const error of output.errors) {
  514. compilation.errors.push(error);
  515. }
  516. }
  517. const newInfo = {
  518. minimized: true
  519. };
  520. const {
  521. source
  522. } = output;
  523. compilation.updateAsset(name, source, newInfo);
  524. });
  525. }
  526. const limit = getWorker && numberOfAssetsForMinify > 0 ?
  527. /** @type {number} */
  528. numberOfWorkers : scheduledTasks.length;
  529. await throttleAll(limit, scheduledTasks);
  530. if (initializedWorker) {
  531. await initializedWorker.end();
  532. }
  533. }
  534. /**
  535. * @param {Compiler} compiler
  536. * @returns {void}
  537. */
  538. apply(compiler) {
  539. const pluginName = this.constructor.name;
  540. const availableNumberOfCores = CssMinimizerPlugin.getAvailableNumberOfCores(this.options.parallel);
  541. compiler.hooks.compilation.tap(pluginName, compilation => {
  542. compilation.hooks.processAssets.tapPromise({
  543. name: pluginName,
  544. stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE,
  545. additionalAssets: true
  546. }, assets => this.optimize(compiler, compilation, assets, {
  547. availableNumberOfCores
  548. }));
  549. compilation.hooks.statsPrinter.tap(pluginName, stats => {
  550. stats.hooks.print.for("asset.info.minimized").tap("css-minimizer-webpack-plugin", (minimized, {
  551. green,
  552. formatFlag
  553. }) => // eslint-disable-next-line no-undefined
  554. minimized ?
  555. /** @type {Function} */
  556. green(
  557. /** @type {Function} */
  558. formatFlag("minimized")) : "");
  559. });
  560. });
  561. }
  562. }
  563. CssMinimizerPlugin.cssnanoMinify = cssnanoMinify;
  564. CssMinimizerPlugin.cssoMinify = cssoMinify;
  565. CssMinimizerPlugin.cleanCssMinify = cleanCssMinify;
  566. CssMinimizerPlugin.esbuildMinify = esbuildMinify;
  567. CssMinimizerPlugin.parcelCssMinify = parcelCssMinify;
  568. module.exports = CssMinimizerPlugin;