feat(css): make the printer's mode reachable and support beautifying - #21660
Conversation
`css-parser-tailwind-unit` stops at the parse, so nothing measured serialization or the source map it resolves — the half this branch changes. This covers both paths a nested block can take (streamed as its children finish, or buffered and printed whole), the shapes they cost differently on, and the two pieces only a streamed block reaches: a declaration a later one overrides, and an opener held back so an empty block can still be dropped. Against this branch's merge base: tailwind 345.6 -> 257.0 ms streamed block 113.1 -> 82.1 ms buffered blocks 94.6 -> 83.4 ms flat rules 109.0 -> 104.0 ms deep nesting 10.5 -> 11.1 ms retracted declarations 99.1 -> 77.4 ms dropped empty blocks 34.1 -> 21.8 ms Deep nesting is the run-to-run spread, not a cost: alternating the two arms interleaves them (before 10.80/11.17/11.11, after 10.96/10.85/11.06).
A build that is not minimizing still parses and walks, and that pass is the floor the printer sits on. Running every fixture both ways makes the pair the measurement: the difference is what printing and the source map cost on that shape, which neither number shows alone. It is a far bigger share here than in HTML — the CSS walk skips most of what the printer then has to serialize. fixture walk minify tailwind 36.1 -> 266.7 ms streamed block 9.7 -> 87.7 ms buffered blocks 8.5 -> 84.4 ms flat rules 11.1 -> 112.6 ms deep nesting 2.3 -> 12.2 ms retracted declarations 10.1 -> 90.3 ms dropped empty blocks 6.7 -> 23.4 ms The fixtures move to a table so the two modes cannot drift apart; the `minify (...)` names are unchanged, so their history carries over. Their absolute values shift against the previous commit because the walk arms now share the process — compare within a run, not across.
`PrintOptions.mode` has always been typed `"minify" | "beautify"` and both
printers branch on it, but `process()` hardcoded `"minify"`, so half of it
was unreachable. `process(src, { mode })` now selects it; `minimize: true`
keeps working as the shorthand for `mode: "minify"` it reads as.
Beautifying re-serializes without the transforms — the authored `#ff0000`,
`1px 2px 1px 2px` and comment placement stay as written, one declaration per
line. It is deliberately still ugly in places (no indentation, top-level
items run together); what it must not be is lossy, and two things were:
- kept comments (`/*!`, `@license`, `@preserve`) were only collected when
minifying, so beautifying dropped every license banner;
- a custom property's value prints straight from source, comments included,
but only the minifying path claimed them from the writer, so beautifying
emitted each one twice.
Minify output is unchanged, byte-for-byte, over the 840-file css/html
fixture corpus and 24,000 random inputs. On the same corpus
`minify(beautify(x)) === minify(x)` holds everywhere minify is itself
idempotent.
🦋 Changeset detectedLatest commit: f030f3f The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Warning Review limit reached
Next review available in: 21 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (5)
Note
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #21660 +/- ##
=======================================
Coverage 94.50% 94.51%
=======================================
Files 630 630
Lines 81568 81570 +2
Branches 24113 24112 -1
=======================================
+ Hits 77083 77093 +10
+ Misses 4485 4477 -8
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
This PR is packaged and the instant preview is available (78a4227). Install it locally:
npm i -D webpack@https://pkg.pr.new/webpack@78a4227
yarn add -D webpack@https://pkg.pr.new/webpack@78a4227
pnpm add -D webpack@https://pkg.pr.new/webpack@78a4227 |
Generated code sizeComparing
No asset changed size. No runtime gained or lost a runtime module. Built |
Merging this PR will degrade performance by 3.33%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | Memory | benchmark "asset-modules-source", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
649.5 KB | 1,370.6 KB | -52.61% |
| ❌ | Simulation | benchmark "wasm-modules-sync", scenario '{"name":"mode-development","mode":"development"}' |
1.1 s | 1.4 s | -24.18% |
| ⚡ | Memory | benchmark "devtool-eval", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
957.6 KB | 482.1 KB | +98.63% |
| ⚡ | Memory | benchmark "css-modules", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
921.5 KB | 753 KB | +22.39% |
| 🆕 | Simulation | unit benchmark "css-printer-tailwind-unit", beautify (buffered blocks) |
N/A | 688.5 ms | N/A |
| 🆕 | Simulation | unit benchmark "css-printer-tailwind-unit", beautify (deep nesting) |
N/A | 273.9 ms | N/A |
| 🆕 | Simulation | unit benchmark "css-printer-tailwind-unit", beautify (dropped empty blocks) |
N/A | 736.5 ms | N/A |
| 🆕 | Simulation | unit benchmark "css-printer-tailwind-unit", beautify (flat rules) |
N/A | 990 ms | N/A |
| 🆕 | Simulation | unit benchmark "css-printer-tailwind-unit", beautify (retracted declarations) |
N/A | 832.1 ms | N/A |
| 🆕 | Simulation | unit benchmark "css-printer-tailwind-unit", beautify (streamed block) |
N/A | 727.8 ms | N/A |
| 🆕 | Simulation | unit benchmark "css-printer-tailwind-unit", beautify (tailwind) |
N/A | 2.5 s | N/A |
| 🆕 | Simulation | unit benchmark "css-printer-tailwind-unit", minify (buffered blocks) |
N/A | 1.9 s | N/A |
| 🆕 | Simulation | unit benchmark "css-printer-tailwind-unit", minify (deep nesting) |
N/A | 399.4 ms | N/A |
| 🆕 | Simulation | unit benchmark "css-printer-tailwind-unit", minify (dropped empty blocks) |
N/A | 810.8 ms | N/A |
| 🆕 | Simulation | unit benchmark "css-printer-tailwind-unit", minify (flat rules) |
N/A | 3 s | N/A |
| 🆕 | Simulation | unit benchmark "css-printer-tailwind-unit", minify (retracted declarations) |
N/A | 2 s | N/A |
| 🆕 | Simulation | unit benchmark "css-printer-tailwind-unit", minify (streamed block) |
N/A | 2 s | N/A |
| 🆕 | Simulation | unit benchmark "css-printer-tailwind-unit", minify (tailwind) |
N/A | 5.3 s | N/A |
| 🆕 | Simulation | unit benchmark "css-printer-tailwind-unit", walk (buffered blocks) |
N/A | 265.3 ms | N/A |
| 🆕 | Simulation | unit benchmark "css-printer-tailwind-unit", walk (deep nesting) |
N/A | 81.6 ms | N/A |
| ... | ... | ... | ... | ... | ... |
ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing feat/source-processor-print-modes (f030f3f) with main (e3f177c)
Summary
PrintOptions.modehas always been typed"minify" | "beautify"and both the CSS and HTML printers branch on it throughout, butSourceProcessor.process()hardcoded"minify", so half of that code was unreachable.process(src, { mode })now selects it;minimize: truekeeps working as the shorthand formode: "minify"it reads as, so no caller changes.Beautifying re-serializes rather than rewrites — the authored
#ff0000,1px 2px 1px 2px, attribute quoting and comments stay as written. It is deliberately still ugly (no indentation, top-level items run together); what it must not be is lossy, and two things were: kept license comments (/*!,@license,@preserve) were only collected when minifying, and a custom property's value claimed them from the writer only on the minifying path, so each was emitted twice. Both are fixed here.Also adds
css-printer-tailwind-unit, which benchmarks serialization — the parser case stops at the parse, so nothing measured it. Each fixture runs in all three modes, sominify - beautifyisolates what the value and shorthand transforms cost on top of serializing: onflat rules, 40 ms to serialize and 63 ms more to transform.What kind of change does this PR introduce?
feat
Did you add tests for your changes?
Yes —
CssSyntax — print modesintest/CssSyntax.unittest.js(6 cases, covering both spellings of the mode, the comment parity and the custom-property duplication). Verified beyond that with two properties over the 844-file css/html fixture corpus plus 24,000 random inputs: minify output is byte-identical tomain, andminify(beautify(x)) === minify(x)holds everywhere minify is itself idempotent (the handful of files where it is not diverge at the same byte offset under plainminify(minify(x)), so they are pre-existing and not beautify's).Does this PR introduce a breaking change?
No.
minimize: truebehaves exactly as before and minified output is unchanged byte-for-byte;modeis additive.If relevant, what needs to be documented once your changes are merged or what have you already documented?
n/a —
webpack.css.syntax.SourceProcessoris marked experimental/unstable, and the option is documented in its JSDoc.Use of AI
Claude Code was used to implement this change, to write the differential and round-trip harnesses, and to run the measurements quoted above. Every number here is from runs on this branch against
main; the design decisions and the correctness checks were reviewed by a human before each commit.Generated by Claude Code