Repository navigation
Conversation
…d color-scheme The ltr/rtl/dark declarations staged on PropertyHandlerContext were a single list per generated rule and were always emitted as normal declarations, so an !important logical property (or color-scheme) lost its importance once it was compiled into :dir()/@media rules. Stage them split by importance, like the @supports entries already were, and build each generated rule's DeclarationBlock from both lists. Handlers stage most of these rules while flushing in finalize(), so DeclarationBlock::minify now sets is_important explicitly before finalizing each handler instead of leaving it at whatever the last handled list set.
|
Warning Review limit reached
Next review available in: 15 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: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
Comment |
|
Status: diff is ready; CI build #96261 is green on every lane that ran.
|
There was a problem hiding this comment.
LGTM — focused fix that routes staged logical/color-scheme fallback declarations into the right importance list, with the finalize-time is_important flag now set per handler.
What was reviewed:
- Confirmed
is_importantis only read insidecontext.rs(staging into ltr/rtl/dark/supports), so setting it before eachfinalize()cannot affect anything else. - The one external consumer of the retyped fields — the pending-merge check at
src/css/rules/mod.rs:767-769— now dispatches toStagedDeclarations::is_empty(), which checks both lists. reset()clears both lists;SupportsEntryrefactor is behavior-preserving (it already split by importance).- Tests cover every
add_logical_rule/add_dark_rulecall site (margin/padding/inset, border, border-radius, transition, color-scheme) plus the mixed and stale-flag cases.
Extended reasoning...
Overview
The PR fixes a CSS minifier bug where !important was dropped from the extra rules generated when compiling away logical inline properties (margin-inline-start, border-inline-*, inset-inline-*, border-*-*-radius, transition-property referencing them) and color-scheme. Two changes:
src/css/context.rs: introduceStagedDeclarations { declarations, important_declarations }and use it forltr,rtl,dark, andSupportsEntry.add_logical_rule/add_dark_rulenow route onself.is_important, and a sharedclone_blockbuilds each generated rule'sDeclarationBlockfrom both lists (previouslyimportant_declarationswas left empty).src/css/declaration.rs: setcontext.is_importantexplicitly before eachfinalize()call, so declarations staged during flush inherit the importance of the handler that owns them rather than whatever the last-processed list left behind.
Tests add a describe("!important in generated fallback rules") block with 10 prefix_test cases, one per staging site plus the mixed, stale-flag, @supports-fallback, and unchanged block-axis cases.
Security risks
None. Pure CSS-output correctness in the minifier; no untrusted-input parsing changes, no allocation-size logic, no FFI.
Level of scrutiny
Medium. It changes shared minifier plumbing (PropertyHandlerContext), so I traced every reader of the retyped fields and every reader of is_important:
is_importantis set only indeclaration.rsand read only incontext.rs(for staging), so the extra assignments aroundfinalize()cannot leak into any other behavior.ltr/rtl/darkare read only insidecontext.rsand atrules/mod.rs:767-769via.is_empty();StagedDeclarations::is_empty()ispub(crate)and covers both lists, so the pending-merge check is unchanged for non-important input and correctly non-empty for important-only input.SupportsEntryalready tracked both lists; the refactor just unifies the shape andclone_blockreproduces the exact prior mapping.- Hoisting
ctx.is_important = importantout of the per-declaration loop is a no-op because no property handler mutates the flag (grep-verified), and the value is overwritten before bothfinalize()calls anyway.
Other factors
- The fix is at the right layer — the shared staging context — rather than in each of the six handlers, matching the "fix the whole class" guidance.
- Test coverage maps directly onto every
add_logical_rule/add_dark_rulecall site found by grep (margin_padding, border, border_radius, transition, ui), plus the interaction withadd_conditional_propertyvia thevar()+lab()case. - Non-important declarations are unaffected: they land in
.declarationsas before, andreset()/is_empty()see both lists. - The PR description confirms 9/10 new tests fail on main and all pass with the fix, and that upstream lightningcss shares the bug (so the expected output is derived from cascade semantics, not upstream behavior).
|
This also reproduces in a plain printf '.a { border-start-start-radius: 3px !important }' > a.css
bun build --minify a.cssBun 1.4.3 prints six rules. None of them has .a:not(:-webkit-any(:lang(ae),...,:lang(yi))){border-top-left-radius:3px}
.a:not(:-moz-any(...)){border-top-left-radius:3px}
.a:not(:is(...)){border-top-left-radius:3px}
.a:-webkit-any(...){border-top-right-radius:3px}
.a:-moz-any(...){border-top-right-radius:3px}
.a:is(...){border-top-right-radius:3px}The default floor is chrome 80, edge 80, firefox 78, safari 14 (
#40369 raises the default floor. At its floor the minifier compiles no logical property away, so a default build no longer reaches this code. Explicit lower targets (#40368) still reach it. |
Problem
!importantand the targets require compiling logical properties away, the generated direction rules drop the!important..foo { margin-inline-start: 2px !important }withsafari: 12comes out as.foo:not(:lang(...)) { margin-left: 2px }and.foo:lang(...) { margin-right: 2px }.!important, and the rtl rule also loses to a later plain rule of higher specificity, while the source declaration would have won. Block-axis properties are unaffected because they are compiled into the original rule, which keeps the importance.color-scheme: light dark !importantemits the light variables with!importantin the rule itself, but the@media (prefers-color-scheme: dark)override without it, so the dark override can never apply.@supportsfallbacks split out of the generated rules inherit the same loss, since they are derived from declarations that were already staged as normal ones.PropertyHandlerContext(src/css/context.rs) stages theltr/rtl/darkdeclarations as a singleVec<Property>each and builds the generated rules with an emptyimportant_declarationslist. The@supportsentries next to them already keep two lists and honoris_important.finalize(), andDeclarationBlock::minify(src/css/declaration.rs) leftcontext.is_importantat whatever the last handled list set, so the important handler was finalized with the flag describing the normal declarations whenever the rule had any.Fix
StagedDeclarationsholds a normal and an important list;ltr,rtl,darkand the@supportsentries all use it,add_logical_rule/add_dark_rule/add_conditional_propertyroute onis_important, and oneclone_blockhelper builds every generated rule'sDeclarationBlockfrom both lists.DeclarationBlock::minifysetsis_importantexplicitly before finalizing each handler, so declarations staged duringfinalize()are attributed to the handler that staged them.DeclarationBlock::minifyalready keeps the two lists apart, so!importantflows through to their@supportsfallbacks as well.declarations, andreset()and the pending-merge check inrules/mod.rs(which callsis_empty()on the staged lists) see both lists.test/js/bun/css/css.test.ts,describe("!important in generated fallback rules"), one case per staging site (margin/padding/inset handler, border, border-radius, transition, color-scheme), plus a mixed important/normal rule, an important declaration followed by a normal one (the stale flag case), an unparsedvar()+lab()value whose@supportsfallback must stay important, and a block-axis case as the unchanged contrast. 9 of the 10 fail on the current build, all pass with the fix.bun bd test test/js/bun/css/passes apart from six pre-existing timeouts (css-fuzz and two subprocess tests) that fail identically on a build without this diff in the same container;test/bundler/css/and the css regression tests pass;cargo clippy -p bun_cssis clean.Background
margin-inline-start, the minifier rewrites the declaration into physical properties. Block-axis properties map to a fixed physical side and stay in the rule. Inline-axis properties depend on the text direction, so the minifier removes them from the rule and emits two extra rules, one with:dir(ltr)and one with:dir(rtl)appended to the selectors (later compiled to:lang()lists), each holding the physical declaration for that direction.color-schemeworks the same way, emitting a@media (prefers-color-scheme: dark)rule that overrides the variables set in the main rule.PropertyHandlerContextis where the property handlers stage these extra declarations while a rule's declarations are minified;minify_style_arminrules/mod.rsturns them into rules after the rule is done and then callsreset().DeclarationBlockstores normal and!importantdeclarations as two separate lists instead of a flag per declaration, andminifyruns two independent sets of handlers over them (handlerandimportant_handler).context.is_importantis the only thing telling the context which set is currently running.finalize()is the end-of-block flush of a handler: handlers such as margin buffer the sides they have seen and only decide what to emit (and what to stage) once the whole block has been handled.Output before and after for the repro