Repository navigation
feat(css): give every target-driven rewrite a switch of its own - #21932
alexander-akait merged 2 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🦋 Changeset detectedLatest commit: 690d225 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 |
f02a4ea to
d6af4cc
Compare
8c87a6b to
626115f
Compare
`optimization.minimize.css` says every transform keeping the stylesheet's meaning may be turned off on its own, so a document one rewrite breaks can be minimized without it. Eleven rewrites did not honour that: everything the browserslist target drives — a hex alpha as `rgba()`, a double-position stop, `inset` / `overflow` / `place-*` as longhands, `text-decoration` as its own, `system-ui` as the platform stack, a `:lang()` or `:not()` list as the `:is()` it means, a media range as the `min-`/`max-` pair, `light-dark()` as the custom-property pair — could only be escaped by dropping the target, which takes vendor prefixing with it. `lowerUnsupported` turns the policy off and leaves the rest of the minifier running; it does not make the printer write a spelling the target cannot read. Three more rewrites answered to nothing, and now answer to the switch that owns them: `@charset` and an empty rule on the collected path to `removeDeadRules` (the streaming path already asked it, so the two paths disagreed), and the `all` before an `and` and a repeated condition operand to `shortenMediaQueries` — whose description now says it shortens a condition prelude, `@supports` and `@container` included. A unit test asks each of the eleven to stand still with the switch off, and `minimize-lower-unsupported-off` is the same sheet and target as `minimize-lower-unsupported` with the one option set.
Asked whether a lowered spelling is itself minified, two answers came back no: - A media feature's own number was never shortened, lowered or not: `@media (0480.0px <= width <= 768.50px)` kept every digit it was written with, and the `min-`/`max-` pair lowered out of it kept them too. A feature is a `<length>`, a `<ratio>` or an `<integer>` and nothing else, so the number in one shortens like any other — where a prelude number elsewhere is An+B and stripping a `+` there breaks the selector, which is what the guard was for. A `style()` query still compares its tokens as written, and `@supports` reads a declaration rather than a feature. - A `text-decoration` whose color is a hex or a call was not lowered at all. The print leaves out the separator between an ident and a hash — `solid #fff` is `solid#fff`, two tokens whatever stands between them — and the lowering split the printed value on its spaces, so the slots came back unreadable and the rewrite declined. For Safari 15 that means the whole declaration is dropped and the underline is lost, which is the failure the lowering exists to prevent. It now splits at the junctions the print could have left bare, by the same test the print makes; and a call that is a color rather than one that takes one fills a color slot, out of `<color>`'s own grammar. Everything else a lowering writes was already minified: the `inset` longhands drop a zero's unit, the `text-decoration` ones shorten the color, the `light-dark()` halves shorten theirs, the hex-alpha rewrite writes the shortest alpha that quantizes back, and the `system-ui` stack drops its quotes.
d6af4cc to
af2282e
Compare
626115f to
690d225
Compare
0c5a7ad
into
perf/css-drop-what-is-already-said
* feat(css): give every target-driven rewrite a switch of its own `optimization.minimize.css` says every transform keeping the stylesheet's meaning may be turned off on its own, so a document one rewrite breaks can be minimized without it. Eleven rewrites did not honour that: everything the browserslist target drives — a hex alpha as `rgba()`, a double-position stop, `inset` / `overflow` / `place-*` as longhands, `text-decoration` as its own, `system-ui` as the platform stack, a `:lang()` or `:not()` list as the `:is()` it means, a media range as the `min-`/`max-` pair, `light-dark()` as the custom-property pair — could only be escaped by dropping the target, which takes vendor prefixing with it. `lowerUnsupported` turns the policy off and leaves the rest of the minifier running; it does not make the printer write a spelling the target cannot read. Three more rewrites answered to nothing, and now answer to the switch that owns them: `@charset` and an empty rule on the collected path to `removeDeadRules` (the streaming path already asked it, so the two paths disagreed), and the `all` before an `and` and a repeated condition operand to `shortenMediaQueries` — whose description now says it shortens a condition prelude, `@supports` and `@container` included. A unit test asks each of the eleven to stand still with the switch off, and `minimize-lower-unsupported-off` is the same sheet and target as `minimize-lower-unsupported` with the one option set. * fix(css): minify what a lowering reads and what it writes Asked whether a lowered spelling is itself minified, two answers came back no: - A media feature's own number was never shortened, lowered or not: `@media (0480.0px <= width <= 768.50px)` kept every digit it was written with, and the `min-`/`max-` pair lowered out of it kept them too. A feature is a `<length>`, a `<ratio>` or an `<integer>` and nothing else, so the number in one shortens like any other — where a prelude number elsewhere is An+B and stripping a `+` there breaks the selector, which is what the guard was for. A `style()` query still compares its tokens as written, and `@supports` reads a declaration rather than a feature. - A `text-decoration` whose color is a hex or a call was not lowered at all. The print leaves out the separator between an ident and a hash — `solid #fff` is `solid#fff`, two tokens whatever stands between them — and the lowering split the printed value on its spaces, so the slots came back unreadable and the rewrite declined. For Safari 15 that means the whole declaration is dropped and the underline is lost, which is the failure the lowering exists to prevent. It now splits at the junctions the print could have left bare, by the same test the print makes; and a call that is a color rather than one that takes one fills a color slot, out of `<color>`'s own grammar. Everything else a lowering writes was already minified: the `inset` longhands drop a zero's unit, the `text-decoration` ones shorten the color, the `light-dark()` halves shorten theirs, the hex-alpha rewrite writes the shortest alpha that quantizes back, and the `system-ui` stack drops its quotes.
…21931) * perf(css): drop what a declaration, a slot or a query already says Five things the minifier wrote out that say nothing: - A declaration a later one overrides, where the two are spelled from one vocabulary — a pair of numbers carrying one unit, a pair of hexes on one side of the alpha four digits state, a pair of color names as old as naming a color, or `inherit`. That is what parts an override from a fallback without asking the target: `color:#eee` before `color:lab(…)` is the older color standing where the newer one is not read, while `color:#eee;color:#333` is one an engine reading either reads both of. Two adjacent rules with one selector join on the same terms, where they used to be kept apart by any property they shared. - A shorthand slot holding its own initial, where the slot takes more than keywords: `border:medium none currentcolor` is `border:none`. The initial table now carries what each slot takes beyond its spellings, so a sibling filling it is read by the classes its own value is in — a length, a color — rather than by a list of names, and a value that classes as nothing keeps the slot. - A `<time>` of zero nothing after it depends on, in a transition or an animation, and an iteration count of one. - The `all` a media query states before an `and`, which matches what the condition alone does. - An operand a condition already states, in `@supports`, `@media` and `@container` alike. `rebeccapurple` and `transparent` are the names left out of the first, derived from the compatibility rows they carry of their own: an engine may read neither while reading the rest. Held to Chromium: every rewrite computes as what it replaced, over the equivalence fixtures a new one covers. * test(css): hold every lowering to the whole computed style, not the probes The lowering tier compared the properties each fixture names. It now compares every property the CSSOM reports for those elements, so a rewrite reaching one nobody thought to probe is one the tier sees. Two differences it turned up are real and now stated rather than hidden: - a folded color serializes differently. Chromium keeps the space a color was written in, so `color(srgb .2 .4 .6)` computes as itself where `#369` computes as `rgb(51, 102, 153)` — the same color, a different string for a script to read. Values are compared as the color they paint, which is what the rewrite preserves. - lowering `light-dark()` writes `--webpack-light` and `--webpack-dark`, which a script enumerating computed custom properties would see where the source has none. The fixtures naming that lowering now say so. The corpus tier already reads every `.css` under `configCases` and compares the CSSOM rule by rule — conditions evaluated rather than compared as text, blocks computed by the engine — and every `style=""` as the declarations it parses to. The queries and shorthand slots this session learned to shorten had no corpus entry, so they were held by unit tests alone; they have one now. * feat(css): give every target-driven rewrite a switch of its own (#21932) * feat(css): give every target-driven rewrite a switch of its own `optimization.minimize.css` says every transform keeping the stylesheet's meaning may be turned off on its own, so a document one rewrite breaks can be minimized without it. Eleven rewrites did not honour that: everything the browserslist target drives — a hex alpha as `rgba()`, a double-position stop, `inset` / `overflow` / `place-*` as longhands, `text-decoration` as its own, `system-ui` as the platform stack, a `:lang()` or `:not()` list as the `:is()` it means, a media range as the `min-`/`max-` pair, `light-dark()` as the custom-property pair — could only be escaped by dropping the target, which takes vendor prefixing with it. `lowerUnsupported` turns the policy off and leaves the rest of the minifier running; it does not make the printer write a spelling the target cannot read. Three more rewrites answered to nothing, and now answer to the switch that owns them: `@charset` and an empty rule on the collected path to `removeDeadRules` (the streaming path already asked it, so the two paths disagreed), and the `all` before an `and` and a repeated condition operand to `shortenMediaQueries` — whose description now says it shortens a condition prelude, `@supports` and `@container` included. A unit test asks each of the eleven to stand still with the switch off, and `minimize-lower-unsupported-off` is the same sheet and target as `minimize-lower-unsupported` with the one option set. * fix(css): minify what a lowering reads and what it writes Asked whether a lowered spelling is itself minified, two answers came back no: - A media feature's own number was never shortened, lowered or not: `@media (0480.0px <= width <= 768.50px)` kept every digit it was written with, and the `min-`/`max-` pair lowered out of it kept them too. A feature is a `<length>`, a `<ratio>` or an `<integer>` and nothing else, so the number in one shortens like any other — where a prelude number elsewhere is An+B and stripping a `+` there breaks the selector, which is what the guard was for. A `style()` query still compares its tokens as written, and `@supports` reads a declaration rather than a feature. - A `text-decoration` whose color is a hex or a call was not lowered at all. The print leaves out the separator between an ident and a hash — `solid #fff` is `solid#fff`, two tokens whatever stands between them — and the lowering split the printed value on its spaces, so the slots came back unreadable and the rewrite declined. For Safari 15 that means the whole declaration is dropped and the underline is lost, which is the failure the lowering exists to prevent. It now splits at the junctions the print could have left bare, by the same test the print makes; and a call that is a color rather than one that takes one fills a color slot, out of `<color>`'s own grammar. Everything else a lowering writes was already minified: the `inset` longhands drop a zero's unit, the `text-decoration` ones shorten the color, the `light-dark()` halves shorten theirs, the hex-alpha rewrite writes the shortest alpha that quantizes back, and the `system-ui` stack drops its quotes. * fix(css): drop no declaration a keyframe, a revert or a slot's arity keeps The browser tier over web-platform-tests caught three colors and animations this turn's own redundancy removal changed the meaning of. - CSS Animations 1 §3 ignores an `!important` declaration inside a keyframe, so the plain one before it is what the engine reads. `border-color:green; border-color:red!important` kept only the important one, which the engine then discards — the element lost its border color rather than gaining a red one (`css-cascade/important-prop.html`). - The rule-level override read the later block's *first* declaration of the property rather than its last, so `#t{color:green}` before `#t{color:red;color:revert-rule}` was compared against the red and dropped. `revert-rule` reads back to that very rule, so the color went green to red (`css-cascade/revert-rule-layer.html`). The last one decides now, which is also what leaves the `revert` family declining on its own terms. - One slot takes the iteration count and two take a time, so `animation:1 2`, `animation:1 1`, `animation:0s 0s 0s` and `transition:0s 0s 0s` are layers no engine reads — and dropping the default from one handed back a layer that runs. Both droppers decline where a layer names more than the slots take. The folds that were right stay: `animation:0s 0s` is still `animation:0s`, `animation:x 1 2s` still loses its count, and an `!important` outside a keyframe still stands over what precedes it. The two changesets this pull request carried are one, as the guide asks, and the `};` recovery case in the html suite names two properties now — with one, the override removal takes the pair it was written to show. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_018tNx7sU4SbnKFhFeofDVr5 * fix(css): let a url() answer as a url before it is read as a call `_valueClasses` tested for a function call before it tested for `url()`, so a well-formed `url(...)` reached the call branch first and left the url test unreachable. Reword `lowerUnsupported`'s closing clause, which did not parse. * style(css): hold the new comments to the two-line limit Also drops a leftover `@returns`-only JSDoc block stacked above `collectColorFunctions`'s real one. * test(css): drive the two dataset guards the generator grew `collectColorFunctions` and `collectLaterColorNames` read their dataset off module state, so neither guard could be reached from a test. Take it as an argument the way `collectGradientFunctions` and `collectMergeableAtRules` already do, and cover both throws. * docs: make the comment-length limit countable, and check it The rule said "as short as possible — ideally one line, at most two" in the middle of a paragraph, which reads as a preference rather than a bound, and nothing checked it before review did. State the limit first, give a command that reports any comment block a change grows past two lines, and cut the section from 33 lines to 19. Bring the comments this branch added under it. * test(css): update the CLI snapshot for the reworded description The CLI flags are derived from the schema, so rewording `lowerUnsupported` changed four lines of `Cli.basictest.js.snap`. * build: check comment length over both comment forms The awk one-liner the guide carried matched `//` runs only, so a three-line `/* … */` block passed it, and an untracked file was in no diff at all — a new file stayed unchecked until the commit that would have caught it. Move the check into `tooling/check-comment-length.js` behind `yarn lint:comments`, reading both forms and every new file, and exempting JSDoc and the license header. Covered by a unit test. * test: cover the comment-length checker's command path Codecov read 12 lines of `tooling/check-comment-length.js` uncovered — the whole `require.main` body, which no test reached. Split it into `report` and `main` and drive both over a throwaway git repository, tracked file and untracked alike, plus the command itself end to end. * fix: read the comment-length repository without changing directory `runtimes (bun)` failed all seven repository tests with `process.chdir() is not supported in workers` — jest runs them in a worker there, and the newest Bun refuses the call. Take the repository as an argument through `report` and `main` instead, which also stops the checker depending on whatever directory it is called from. --------- Co-authored-by: Claude Opus 5 <[email protected]>
Summary
Adds
optimization.minimize.css.lowerUnsupported, so the target-driven lowering can be turned off on its own when a rewrite is wrong for a project, and audits the rest: every rewrite the minifier makes now answers to a switch. That audit found a real bug — the streaming and collected printer paths disagreed aboutremoveDeadRules. Also minifies what a lowering reads and what it writes, so a lowered value is not left in a longer spelling than the one it replaced.Top of the stack, on #21931 — review the top two commits only.
What kind of change does this PR introduce?
feat
Did you add tests for your changes?
Yes —
test/CssSyntax.unittest.js(a describe asserting every rewrite has a switch),test/configCases/css/minimize-lower-unsupported-off/, andtest/configCases/css/minimize-transforms-off/.Does this PR introduce a breaking change?
No — the new option defaults to the current behaviour.
If relevant, what needs to be documented once your changes are merged or what have you already documented?
optimization.minimize.css.lowerUnsupported.Use of AI
Yes. Written with Claude Code: it audited the rewrites against the switches, added the option, and wrote the tests; every change was reviewed before pushing.
🤖 Generated with Claude Code
https://claude.ai/code/session_018tNx7sU4SbnKFhFeofDVr5
Generated by Claude Code