cssMinify.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author sheo13666q @sheo13666q
  4. */
  5. "use strict";
  6. /** @import { CssEnvironment, CssTransformOptions } from "./syntax" */
  7. /**
  8. * What a renderer made of one embedded body: the minified text, and anything it
  9. * has to report about it. A bare string is the text alone.
  10. * @typedef {{ code?: string, warnings?: (Error | string)[], errors?: (Error | string)[] }} EmbeddedSourceResult
  11. */
  12. /**
  13. * Minifies source a stylesheet embeds; may answer asynchronously.
  14. * @typedef {(source: string, info: { type: string, hostType: string }) => Promise<string | EmbeddedSourceResult | undefined> | string | EmbeddedSourceResult | undefined} AsyncEmbeddedSourceRenderer
  15. */
  16. /** @import { SourceMap } from "../util/SourceProcessor" */
  17. /**
  18. * A `minify` function for `minimizer-webpack-plugin` (passed as its `minify`
  19. * option): safely serializes one CSS asset's minimized form (collapse
  20. * whitespace, drop redundant separators and empty rules, shorten colors /
  21. * numbers / easing functions / identifier escapes, normalize string and `url()`
  22. * quoting, keep custom-property values verbatim unless asked otherwise), so CSS minification reuses
  23. * that plugin's pipeline — source maps, caching and worker-thread parallelization.
  24. *
  25. * The parser is read from the public `webpack.css.syntax` API inside the body, not
  26. * imported at module scope: `minimizer-webpack-plugin` ships this function to its
  27. * worker pool as source (a top-level import wouldn't survive), and `require("webpack")`
  28. * re-resolves in the worker — via the installed package, or the dev self-link.
  29. * @param {{ [file: string]: string }} input a single `{ filename: code }` entry
  30. * @param {object=} sourceMap the asset's input source map — the plugin chains it onto the map returned here, so it isn't read directly
  31. * @param {{ as?: "stylesheet" | "block-contents", environment?: CssEnvironment, convertLengthUnits?: boolean, rewriteCustomProperties?: boolean, renderEmbeddedSource?: AsyncEmbeddedSourceRenderer } & CssTransformOptions=} minimizerOptions minimizer options — `environment` carries the target's CSS abilities (see `output.environment`), so a spelling the target cannot read is not reached for; the rest is `optimization.minimize.css` (e.g. `convertLengthUnits`, and the per-transform switches). `renderEmbeddedSource` minifies source this stylesheet embeds, and may be asynchronous: one parse serves both it and the output
  32. * @returns {Promise<{ code: string, map?: SourceMap, warnings?: (Error | string)[], errors?: (Error | string)[] }>} the minified CSS, its input->output source map (one anchor for the whole of a `block-contents` print, which is emitted as one piece), and what a renderer reported over what this embeds
  33. */
  34. const cssMinify = async (input, sourceMap, minimizerOptions = {}) => {
  35. const webpack = /** @type {typeof import("../index")} */ (
  36. // eslint-disable-next-line import/no-extraneous-dependencies -- webpack self-require, re-resolved inside the worker
  37. require(/** @type {string} */ ("webpack"))
  38. );
  39. // TODO expose the remaining declined transforms on `optimization.minimize.css`
  40. // like `convertLengthUnits` — the switches below turn off what webpack does
  41. // make, not what it declines. Each is on in other minifiers; what each costs:
  42. // - dropping a declaration a later one overrides, which loses a fallback pair
  43. // an engine that cannot read the newer spelling depends on;
  44. // - merging rules a third stands between, and merging non-adjacent `@media`,
  45. // both of which reorder the cascade (csso loses ~12,900 Tailwind classes to
  46. // it). Adjacent rules sharing a block do join, nothing being between them to
  47. // step over — only where every selector is a shape each engine parses, since
  48. // one it cannot invalidates the whole list and loses the rest with it;
  49. // - the color conversions webpack declines rather than guesses. Every polar and
  50. // Lab function converts to hex, except two cases the other minifiers convert
  51. // regardless: a channel landing near a `.5` boundary, where implementations
  52. // round opposite ways (it is why esbuild and lightningcss emit different
  53. // bytes for `hwb(194 0% 0%)`), and a Lab-family color outside the sRGB gamut,
  54. // which hex would clip to a different color rather than respell.
  55. // Which longhand families merge into their shorthand is decided in
  56. // `tooling/generate-css-data.js`, and three kinds are excluded there for good
  57. // rather than pending an option:
  58. // - a shorthand gathering a whole family (`border`, `font`, `background`,
  59. // `transition`, `flex`, `columns`) resets longhands `computed` does not
  60. // name — `border` clears `border-image`, `font` clears `font-size-adjust` —
  61. // so the merge would drop a declaration nothing in the family wrote. The
  62. // ones checked against a browser and found to reset nothing else do merge
  63. // (`FAMILY_LONGHANDS`), where each value parses back into its own slot;
  64. // - a shorthand materially newer than its longhands (`place-items`/`-content`
  65. // /`-self`) would lose both declarations, not one, on a target reading only
  66. // the longhands. `output.environment` states this for `inset` alone.
  67. // `overflow` is newer only two values wide, so it merges when it collapses;
  68. // - a pair two shorthands both claim, which cannot be right for both. None
  69. // today: `mdn-data` gave `corner-inline-start-shape` the block-start edge's
  70. // corners, corrected in the generator to the pair Chromium computes.
  71. const {
  72. SourceProcessor,
  73. askEmbeddedRenderer,
  74. collectEmbeddedDiagnostics,
  75. embeddedText,
  76. pickTransforms
  77. } = webpack.css.syntax;
  78. const [[file, code]] = Object.entries(input);
  79. // `process` parses once, and with `mode: "minify"` the same walk also prints
  80. // the safely minified serialization — no second parse. Naming the input with
  81. // `source` / `content` is what asks for the map, which the plugin composes
  82. // back to the original source.
  83. const {
  84. as,
  85. environment,
  86. convertLengthUnits,
  87. rewriteCustomProperties,
  88. renderEmbeddedSource
  89. } = minimizerOptions;
  90. // One set of options, handed to the print. `as` names the production: a
  91. // stylesheet, or the declaration list an HTML `style=""` holds — the printer
  92. // composes that list the same way it composes a rule's block.
  93. const printOptions = {
  94. mode: /** @type {"minify"} */ ("minify"),
  95. as,
  96. source: file,
  97. content: code,
  98. environment,
  99. convertLengthUnits,
  100. rewriteCustomProperties,
  101. // The per-transform switches stand beside those rather than nested in
  102. // them, so a config names one the way it names `convertLengthUnits`;
  103. // `pickTransforms` is what knows which names those are.
  104. transforms: pickTransforms(minimizerOptions)
  105. };
  106. /** @type {EmbeddedSourceResult[]} */
  107. const reported = [];
  108. // Handed each body the print offers, with what it reported kept: `processAsync`
  109. // spells the `url()` around one from the answer, and prints an untapped run's
  110. // spelling for a body declined or thrown on.
  111. const renderer =
  112. renderEmbeddedSource === undefined
  113. ? undefined
  114. : (/** @type {string} */ _source, /** @type {EXPECTED_ANY} */ hole) =>
  115. askEmbeddedRenderer(renderEmbeddedSource, hole, reported).then(
  116. embeddedText
  117. );
  118. const result = await new SourceProcessor().processAsync(code, {
  119. ...printOptions,
  120. renderEmbeddedSource: renderer
  121. });
  122. return reported.length === 0
  123. ? result
  124. : { ...result, ...collectEmbeddedDiagnostics(reported) };
  125. };
  126. // Worker-safe (see the body's in-worker `require`), so it may run in the shared
  127. // worker-thread pool alongside terser.
  128. cssMinify.supportsWorkerThreads = () => true;
  129. /**
  130. * The language this minifies, for a caller dispatching source that carries no
  131. * filename — CSS a module embeds in a JavaScript string literal.
  132. * @returns {string[]} the languages
  133. */
  134. cssMinify.getTypes = () => ["css"];
  135. /**
  136. * The languages this can offer a caller through `renderEmbeddedSource` — what a
  137. * `url()` `data:` payload's media type may name.
  138. * @returns {string[]} the languages
  139. */
  140. cssMinify.getEmbeddedTypes = () => [
  141. // A copy: the list is one module-level array, and what a caller does with what
  142. // it is handed is not this module's to bound. `htmlMinify` copies too.
  143. .../** @type {typeof import("../index")} */ (
  144. // eslint-disable-next-line import/no-extraneous-dependencies -- webpack self-require, as the body does
  145. require(/** @type {string} */ ("webpack"))
  146. ).css.syntax.EMBEDDED_LANGUAGES
  147. ];
  148. // When several minify functions share one `minimizer-webpack-plugin` instance,
  149. // each asset is dispatched only to the ones whose `filter` accepts it — this
  150. // claims CSS, so terser (JS) and this can coexist in a single plugin / worker pool.
  151. /**
  152. * @param {string} name asset filename
  153. * @returns {boolean} true for CSS assets
  154. */
  155. cssMinify.filter = (name) => /\.css(\?.*)?$/i.test(name);
  156. module.exports = cssMinify;