Skip to content

css: keep declarations written after a nested rule in their cascade position - #38539

Closed
robobun wants to merge 3 commits into
mainfrom
farm/79698fa2/css-nested-declarations
Closed

robobun wants to merge 3 commits into
mainfrom
farm/79698fa2/css-nested-declarations

Conversation

@robobun

@robobun robobun commented Aug 14, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • Declarations written after a nested rule are hoisted above it, changing the cascade. bun build in.css --minify on

    a  { color:red; &{color:blue} color:green }
    .c { @media (min-width:600px){ color:pink } color:black }

    prints a{color:green}a{color:#00f}.c{color:#000}@media (min-width:600px){.c{color:pink}}, so a renders blue and .c pink on wide viewports; the source means green and always black. --target=bun (nesting preserved) has the same problem: a{color:green;&{color:#00f}}.

  • Cause: NestedRuleParser::parse_value (src/css/css_parser.rs) appends every declaration to the block's leading declaration list regardless of what was parsed before it, and StyleRule prints that list before its nested rules. The port predates lightningcss's nested declarations rule. parse_style_block separately wraps declarations found directly inside a nested @media/@supports/... in a & { } rule, which for a selector list compiles to :is(a, b) and so also changes their specificity.

Fix

  • Adds CssRule::NestedDeclarations(NestedDeclarationsRule) (src/css/rules/nesting.rs). parse_value parses a declaration that follows a nested rule into the nested declarations rule at the end of the block's rule list (creating one unless the previous declaration already did, and not leaving an empty one behind when the declaration fails to parse and RuleBodyParser retries it as a nested rule such as div:hover {). parse_style_block uses the same rule for declarations directly inside a nested conditional at-rule instead of the & { } wrapper.
  • Printing: when nesting is compiled away, the rule prints as its own block under the enclosing rule's selectors, repeated verbatim (same output lightningcss produces, and no :is() so each selector keeps its specificity). When nesting is preserved, it prints its declarations in place among the nested rules. CssRuleList::to_css now records in Printer::more_rules_follow whether anything follows the rule being printed, so the last declaration keeps its ; exactly when needed, including when an always-true @media all is unwrapped into the enclosing block (lightningcss 1.30 emits color:redcolor:#00f there; see the "trailing semicolon" tests). The declaration printing loop of StyleRule::to_css_base is factored into write_declarations and shared, so composes is consumed in CSS modules the same way in both places; CssModule::handle_composes loses three parameters it never read.
  • Minifying (minify_nested_declarations_arm in src/css/rules/mod.rs) runs the declarations through the property handlers and inserts the fallback rules they stage (logical properties, @supports color fallbacks, prefers-color-scheme for color-scheme) right after the rule, built under a & selector the same way minify_style_arm builds them under a style rule's own selectors, so e.g. a trailing inset-inline-end keeps working with the default browser targets. Otherwise the rule gets the same bookkeeping as the other non-style rules (ends the merge run, dedup barrier, dropped when the enclosing rule is unused), and is charged one selector against the expansion budget, which moved from StyleRule to MinifyContext::charge_selector_expansion to be shared.
  • parse_block still counts the trailing declarations towards the rule's property usage for the CSS modules composes conflict check. (That check is currently defeated for any rule containing a nested rule by a pre-existing bug in how nested rules clear composes_refs; handed off separately, with css modules: keep composes working after a nested rule in the same block #38520 covering the related composes_state clobbering.)
  • Verified with test/js/bun/css/nested-declarations.test.ts (30 cases across preserved/compiled nesting, pretty printing, fallbacks, vendor prefix passes, bun build for both targets: 27 fail on the released build, all pass with this change) and the DeclarationsAfterNestedRule case in test/bundler/css/css-modules.test.ts (fails before, passes after).
  • Also ran: test/js/bun/css/ (the doesnt_crash suite re-bundles the real-world sheets' --target=bun output, exercising re-parsing of the in-place form), test/bundler/esbuild/css.test.ts, test/bundler/css/, test/regression/issue/25794.test.ts, cargo clippy -p bun_css; and a differential run of 40 nesting inputs against lightningcss 1.30.2 in both modes: identical except for pre-existing unrelated printing differences and the two @media all cases above where Bun's output is the valid one.

Background

  • CSS nesting lets a style rule's body mix declarations and rules. Per css-nesting-1, declarations that come after a nested rule form a "nested declarations rule" that matches the same elements as the enclosing rule but cascades from its own position; declarations directly inside a nested @media do the same. Browsers ship this (Chrome 130, Firefox 132, Safari 18.2), so source order is meaningful and a build tool must preserve it.
  • Bun's CSS bundler always runs the lightningcss-derived minify pass. With --target=browser the default targets predate nesting, so nesting is compiled away: nested rules are printed after their parent with & replaced by the parent selectors, which the printer tracks through Printer::ctx / StyleContext (set only in that mode, which is how NestedDeclarationsRule::to_css and the existing @nest printer tell the modes apart). With --target=bun/node nesting is printed as written.
  • Property handlers (DeclarationBlock::minify) may consume a declaration and stage replacement rules in PropertyHandlerContext (e.g. inset-inline-end becomes :dir(ltr)/:dir(rtl) rules); the caller that minified the block has to turn those into rules and place them after it, which previously only minify_style_arm did.
Output before / after for the example above
# --target=browser --minify
before: a{color:green}a{color:#00f}.c{color:#000}@media (min-width:600px){.c{color:pink}}
after:  a{color:red}a{color:#00f}a{color:green}@media (min-width:600px){.c{color:pink}}.c{color:#000}

# --target=bun --minify
before: a{color:green;&{color:#00f}}.c{color:#000;@media (width>=600px){&{color:pink}}}
after:  a{color:red;&{color:#00f}color:green}.c{@media (width>=600px){color:pink}color:#000}

lightningcss 1.30.2 produces the "after" output in both modes.


[review] gate passed · iteration 0 · 10 files touched

fails on main (without fix)
ASAN without fix: 29 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/css/css-modules.test.ts test/js/bun/css/nested-declarations.test.ts
bun test v1.4.0 (b7bae5287)

test/bundler/css/css-modules.test.ts:
(pass) css > css-module/GlobalPseudoFunction [563.15ms]
(pass) css > css-module/BundleTwoFilesWithoutCodeSplitting [128.62ms]
(pass) css > css-module/BundleTwoFilesWithCodeSplitting [133.63ms]
(pass) css > css-module/AnimationNameScopedToKeyframes [139.15ms]
(pass) css > css-module/RepeatedClassAndIdReferences [207.86ms]
(pass) css > css-module/ExportsMapMultipleClassesAndComposes [159.28ms]
243 | 
244 |         // entry.js
245 |         console.log(styles_module_default);
246 |         "
247 |       `);
248 |       api.expectFile("/out/entry.css").toMatchInlineSnapshot(`
                                             ^
error: expect(received).toMatchInlineSnapshot(expected)

  
  "/* styles.module.css */
  .base_-MSaAA {
    color: red;
  }
  
  .button_-MSaAA {
+   color: green;
  }
  
  .button_-MSaAA:hover {
    color: #00f;
- }
- 
- .button_-MSaAA {
-   color: green;
  }
  "
  

- Expect
... (truncated)

release without fix: 29 FAILED
bun test v1.4.0-canary.1 (b7a043103)

test/bundler/css/css-modules.test.ts:
(pass) css > css-module/GlobalPseudoFunction [15.35ms]
(pass) css > css-module/BundleTwoFilesWithoutCodeSplitting [6.84ms]
(pass) css > css-module/BundleTwoFilesWithCodeSplitting [9.77ms]
(pass) css > css-module/AnimationNameScopedToKeyframes [8.84ms]
(pass) css > css-module/RepeatedClassAndIdReferences [6.84ms]
(pass) css > css-module/ExportsMapMultipleClassesAndComposes [11.45ms]
243 | 
244 |         // entry.js
245 |         console.log(styles_module_default);
246 |         "
247 |       `);
248 |       api.expectFile("/out/entry.css").toMatchInlineSnapshot(`
                                             ^
error: expect(received).toMatchInlineSnapshot(expected)

  
  "/* styles.module.css */
  .base_-MSaAA {
    color: red;
  }
  
  .button_-MSaAA {
+   color: green;
  }
  
  .button_-MSaAA:hover {
    color: #00f;
- }
- 
- .button_-MSaAA {
-   color: green;
  }
  "
  

- Expected  - 4
+ Received  + 1

      at onAfterBundle (/workspace/bun/test/bundler/css/css-modules.test.ts:248:40)
      at <anonymous> (/workspace/bun/test/bundler/expectBundled.ts:1591:7)
(fail) css > css-module/Declara
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/css/css-modules.test.ts test/js/bun/css/nested-declarations.test.ts
bun test v1.4.0 (b7bae5287)

test/bundler/css/css-modules.test.ts:
(pass) css > css-module/GlobalPseudoFunction [608.59ms]
(pass) css > css-module/BundleTwoFilesWithoutCodeSplitting [167.01ms]
(pass) css > css-module/BundleTwoFilesWithCodeSplitting [115.77ms]
(pass) css > css-module/AnimationNameScopedToKeyframes [124.85ms]
(pass) css > css-module/RepeatedClassAndIdReferences [261.74ms]
(pass) css > css-module/ExportsMapMultipleClassesAndComposes [150.96ms]
(pass) css > css-module/DeclarationsAfterNestedRule [146.71ms]

test/js/bun/css/nested-declarations.test.ts:
(pass) declarations after a nested rule keep their position > a { color: red; & { color: blue } color: green } [8.14ms]
(pass) declarations after a nested rule keep their position > .a { .b { color: blue } color: green } [2.67ms]
(pass) declarations after a nested rule keep their position > .a { &:hover { color: blue } color: green; &:focus { color: red } } [3.07ms]
(pass) declarations after a ne
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     b7bae52873
  features     baseline

22 deps, 123 codegen, 1176 objects in 889ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1238] gen ErrorCode+*.h
[2/1238] gen bindgenv2
[3/1238] install /workspace/bun
bun install v1.4.0-canary.1 (b7a043103)

Checked 107 installs across 153 packages (no changes) [52.00ms]
[4/1238] gen .bind.ts → GeneratedBindings.cpp
[5/1238] fetch zlib
[zlib] up to date
[6/1238] fetch tinycc
[tinycc] up to date
[7/1237] install /workspace/bun/packages/bun-error
bun install v1.4.0-canary.1 (b7a043103)

Checked 1 install across 2 packages (no changes) [5.00ms]
[8/1237] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[9/1237] install /workspace/bun/src/node-fallbacks
bun install v1.4.0-canary.1 (b7a043103)

Checked 129 installs across 147 packages (no changes) [27.00ms]
[10/1237] subst deps/zlib/zlib.h
[11/1237] gen ProcessBindingConstants.lut.h
Generating /workspace/bun/build/release/codegen/ProcessBindingConstants.lut.h from /workspace/bun/src/jsc/bin
... (truncated)
diff hotspot
src/css/context.rs                          |   9 +
 src/css/css_modules.rs                      |   6 +-
 src/css/css_parser.rs                       |  94 +++++++---
 src/css/printer.rs                          |   6 +
 src/css/rules/media.rs                      |   3 +-
 src/css/rules/mod.rs                        | 120 +++++++++++-
 src/css/rules/nesting.rs                    | 103 ++++++++++-
 src/css/rules/style.rs                      | 160 ++++++++--------
 test/bundler/css/css-modules.test.ts        |  53 ++++++
 test/js/bun/css/nested-declarations.test.ts | 278 ++++++++++++++++++++++++++++
 10 files changed, 702 insertions(+), 130 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                         reads  edits  tests
src/css/context.rs                               1      1      0
src/css/css_modules.rs                           1      1      0
src/css/css_parser.rs                            9      6      0
src/css/printer.rs                               2      2      0
src/css/rules/media.rs                           1      1      0
src/css/rules/mod.rs                             9     12      0
src/css/rules/nesting.rs                         1      2      0
src/css/rules/style.rs                           4      5      0
test/bundler/css/css-modules.test.ts             2      1      0
test/js/bun/css/nested-declarations.test.ts      1      2      0

…osition

Declarations that followed a nested rule were parsed into the enclosing
style rule's own declaration block, which hoisted them above the nested
rules they were written after. `a { color: red; & { color: blue } color:
green }` therefore printed the `green` block before the `blue` one and
rendered blue. Declarations directly inside a nested conditional at-rule
were wrapped in a `& { }` rule, which also changed their specificity for
selector lists.

Parse both into a new CssRule::NestedDeclarations rule that keeps its
position among the nested rules (https://drafts.csswg.org/css-nesting/#nested-declarations-rule).
When nesting is compiled away it prints as a rule under the enclosing
rule's selectors (repeated as written, not wrapped in :is()); when nesting
is preserved it prints its declarations in place, keeping the trailing `;`
whenever more output follows it in the block, including through an
unwrapped `@media all`. Minifying it runs the declarations through the
property handlers and inserts the logical property, @supports and dark
color scheme fallback rules they generate right after it, nested under
`&`, so those fallbacks keep their position too. The declarations still
count towards the CSS modules `composes` property usage of the rule.
@coderabbitai

coderabbitai Bot commented Aug 14, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 10 seconds

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8fd2059d-1c11-4f09-9566-cd4675c6dd7e

📥 Commits

Reviewing files that changed from the base of the PR and between eabb96d and e120c58.

📒 Files selected for processing (10)
  • src/css/context.rs
  • src/css/css_modules.rs
  • src/css/css_parser.rs
  • src/css/printer.rs
  • src/css/rules/media.rs
  • src/css/rules/mod.rs
  • src/css/rules/nesting.rs
  • src/css/rules/style.rs
  • test/bundler/css/css-modules.test.ts
  • test/js/bun/css/nested-declarations.test.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Aug 14, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review; the diff is green on every lane that ran. The red build status is the macOS 14 arm64 test lane never getting an agent (details below), not a test failure.

Reproduced on the released build with bun build in.css --minify on the two-rule example in the description, for both --target=browser (nesting compiled away) and --target=bun (nesting preserved); with this branch both modes print the declarations in source order and match lightningcss 1.30.2 byte for byte.

Tests: test/js/bun/css/nested-declarations.test.ts (27 of its 30 cases fail on the released build, all pass here) and the new DeclarationsAfterNestedRule case in test/bundler/css/css-modules.test.ts.

CI on the current head (e120c58, build 96184): 177 of 179 jobs passed, with only retried flakes in unrelated tests (child_process IPC, crypto/vm/inspect leak tests, watch mode, install registry, terminal, napi hello world). The build is marked failed because its two darwin 14 aarch64 - test-bun shards expired four times in a row (every attempt from 15:23 to 20:05 UTC) without ever being picked up, which used up their retries: that lane has had roughly 270 to 330 shards from about 140 to 160 builds queued on 6 agents all afternoon, and queued jobs expire after an hour and go to the back of the line. The macOS build step passed, the change has no platform-specific code, and the same tests passed on every Linux (glibc, musl, ASAN, x64 and arm64) and Windows (x64 and arm64) lane. Retrying the two shards (or pushing a new build) only makes sense once that queue has drained; until then this is mergeable on the strength of the other lanes, or the two jobs can be retried from Buildkite when the lane has capacity.

Review follow-ups: the undrained stdout pipe in the bun build test was fixed in 2c66fff; e120c58 trims every added comment to one line and names write_declarations' two layout flags (newline_before_first, trailing_semicolon), resolving the comment lint threads.

Two adjacent pre-existing bugs found on the way are tracked separately and are not changed here: #38520 (composes written after a nested rule is rejected) and #38531 (merging adjacent same-query @media re-minifies color-scheme), plus the composes_refs clearing noted in the description.

Comment thread src/css/context.rs Resolved
Comment thread src/css/css_parser.rs Resolved
Comment thread src/css/css_parser.rs Resolved
Comment thread src/css/css_parser.rs Resolved
Comment thread src/css/css_parser.rs Resolved
Comment thread src/css/css_parser.rs Resolved
Comment thread src/css/printer.rs Resolved
Comment thread src/css/rules/mod.rs Resolved
Comment thread src/css/rules/mod.rs Resolved
Comment thread src/css/rules/mod.rs Resolved
Comment thread src/css/rules/mod.rs Resolved
Comment thread src/css/rules/nesting.rs Resolved
Comment thread src/css/rules/nesting.rs Resolved
Comment thread src/css/rules/nesting.rs Resolved
Comment thread src/css/rules/nesting.rs Resolved
Comment thread src/css/rules/nesting.rs Resolved
Comment thread src/css/rules/nesting.rs Resolved
Comment thread src/css/rules/style.rs Resolved
Comment thread src/css/rules/style.rs Resolved
Comment thread src/css/rules/style.rs Resolved
Comment thread src/css/rules/style.rs Resolved

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — the follow-up commits address both the earlier stdout-drain nit (2c66fff now asserts the bundled stylesheet on stdout) and the comment-cop lints (e120c58 trims the added comments to one line and names write_declarations' bool flags). I re-reviewed and didn't find any bugs. Because this threads a new CssRule variant through the parser, minifier, and printer, a human look would still be worthwhile.

What was reviewed:

  • parse_value's pop/push of the trailing NestedDeclarations rule — checked that a failed declaration retried as a nested rule leaves no empty rule behind, and that consecutive declarations coalesce into one rule.
  • write_declarations refactor vs. the old inline loop — the composes handling, trailing-; condition, and i counter (still skipped for consumed composes) match the previous behaviour; handle_composes losing its unused params is safe (it never read them).
  • more_rules_follow / to_css_unwrapped — verified the @media all unwrap is the only caller and the trailing-; tests cover the case lightningcss gets wrong.
  • charge_selector_expansion move to MinifyContext — body is byte-identical to the old StyleRule method.
Extended reasoning...

Overview

This PR fixes a cascade-order bug in Bun's CSS bundler: declarations written after a nested rule were being hoisted into the enclosing rule's leading declaration block, so they printed before the nested rule instead of after it. The fix adds a new CssRule::NestedDeclarations variant (mirroring lightningcss's nested-declarations rule) and threads it through parsing (NestedRuleParser::parse_value, parse_style_block), minifying (minify_nested_declarations_arm, NestedDeclarationsRule::minify), and printing (NestedDeclarationsRule::to_css, a new Printer::more_rules_follow flag, and a shared write_declarations helper factored out of StyleRule::to_css_base). charge_selector_expansion moves from StyleRule to MinifyContext so both callers share it, and CssModule::handle_composes drops three parameters it never read. 30 new test cases plus one itBundled case cover both nesting-preserved and nesting-compiled modes, pretty printing, fallback-rule placement, vendor-prefix passes, and bun build end to end.

Security risks

None. This is CSS AST transformation and serialization; no untrusted-length arithmetic, no new syscalls, no auth/crypto/permissions surface. The one unsafe addition (detach_lifetime_ref on the parser arena in parse_value) is the same pattern already used two lines above in the pre-existing code and is covered by the existing SAFETY comment.

Level of scrutiny

Medium-high. The CSS bundler runs on every bun build of a stylesheet, and this touches the parser, the minify dispatch, the printer, and the CssRule variant table simultaneously — a mistake in any one of those paths would corrupt output for any stylesheet using CSS nesting. The change is well-scoped (each piece is small and follows the existing sibling-variant patterns closely), thoroughly tested (27 of 30 new cases fail on the released build and pass here; a 40-input differential run against lightningcss 1.30.2 matched), and the PR description explains the mechanism precisely. But a new enum variant plus printer state (more_rules_follow) is the kind of cross-cutting change a maintainer familiar with the lightningcss port should sanity-check.

Other factors

  • My prior review's only finding (undrained stdout pipe in the bun build test) was addressed in 2c66fff, which now also asserts the bundled stylesheet on stdout — a stronger test than before.
  • The comment-cop bot flagged multi-line comments across seven files; e120c58 trimmed every added comment to one line and replaced the two positional bools on write_declarations with named args (newline_before_first, trailing_semicolon), so those lints no longer apply to the current diff even though the inline threads aren't marked resolved.
  • I spot-checked the write_declarations extraction against the old inline loop: the i counter is still only incremented for printed declarations (so a consumed composes doesn't throw off the last-declaration ; check), and the handle_composes simplification is safe because the removed params were genuinely unused.
  • The charge_selector_expansion move is a pure hoist — the body is identical, and both call sites pass what the old inline code read.
  • Two adjacent pre-existing bugs (#38520, #38531, and the composes_refs clearing) are explicitly called out as tracked separately and left unchanged here.

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Both review items are in: 2c66fff fixes the undrained stdout pipe in the bun build test, and e120c58 trims the added comments to one line each and names the two write_declarations flags, which is what the comment lint threads asked for (resolved). CI on e120c58 is green on every lane that has finished; the remaining annotations are retried flakes in unrelated tests.

@robobun

robobun commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

This PR also fixes a second report: declarations directly inside a nested at-rule, under a selector list that has a pseudo-element.

*, ::before, ::after { @media (prefers-reduced-motion: reduce) { animation: none } }
  • main prints @media (…) { :is(*, :before, :after) { animation: none } }. :is() cannot match a pseudo-element, so a browser applies the rule to * only. With --target=bun, main prints & { animation: none }, which has the same loss.
  • main with the src/css of this PR prints *, :before, :after { animation: none } in the @media, and bare declarations with --target=bun. I checked both on a debug build.

I did not open a second PR for that report. One gap of the same kind is left here. NestedDeclarationsRule::minify builds the fallback rules under &, so under a selector list they still print :is():

.a::before, .b { @media screen { color: var(--x, lab(50% 20 30)) } }

With Chrome 95 targets this prints @media screen{.a:before,.b{color:var(--x,#a16945)}@supports (color:lab(0% 0 0)){:is(.a:before,.b){color:var(--x,lab(50% 20 30))}}}. The @supports rule does not reach .a::before, and :is() gives .b the highest specificity of the list. The prefers-color-scheme: dark rule for color-scheme has the same shape.

Branch robobun/ba9fb4e8/css-nested-declarations-parent-list has a fix for that shape. It can fold into this PR:

  • A selector flag, SelectorFlags::NESTED_DECLARATIONS, marks the & of such a rule. Selector compare, hash and clone already cover the flags.
  • serialize_style_rule_selectors prints the parent rule's selector list for a flagged &, as the parent rule prints it.
  • The branch also has 33 tests for the selector list cases (test/js/bun/css/css.test.ts, test/bundler/esbuild/css.test.ts). On main with this PR, 31 pass. One of the two that fail expects the fallback output above. The other pins a selector merge (.a,.b,:is(.a,.b) .c{color:red}) that this PR prints as two rules.

@robobun

robobun commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator Author

Closing: superseded by #45462. It has the same nested declarations rule on current main. It builds the fallback rules on the selectors of the enclosing rule, and it lists what it gives up against this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants