From 88d34f8998cc40a5ab0cb1708f95edde9d60ea62 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Sat, 10 Oct 2026 07:12:21 +0300 Subject: [PATCH] fix(compiler): keep a nested rule's parent selector in source order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getClassNameSelectors reversed lightningcss's selector array in place to walk it from the subject outwards. A nested rule, such as @media, @container or &:hover inside a rule, reads its parent's selector list again, so it saw that selector reversed: `.group .x { @media (…) { … } }` put the nested declarations on `.group` inside an `.x` container. The walk now reads a reversed copy, so every rule compiles against the selector as written. --- src/__tests__/compiler/compiler.test.tsx | 25 ++++++++++++++++++++++++ src/__tests__/native/grouping.test.tsx | 17 ++++++++++++++++ src/compiler/selector-builder.ts | 3 ++- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/__tests__/compiler/compiler.test.tsx b/src/__tests__/compiler/compiler.test.tsx index b08e24c3..20743e03 100644 --- a/src/__tests__/compiler/compiler.test.tsx +++ b/src/__tests__/compiler/compiler.test.tsx @@ -289,6 +289,31 @@ test("media query nested in rules", () => { }); }); +test.each([ + ["@media", "@media (min-width: 0px) { color: blue; }"], + ["@container", "@container (min-width: 0px) { color: blue; }"], + ["&:hover", "&:hover { color: blue; }"], +])( + "a %s rule nested in a descendant selector applies to that selector's subject", + (_kind, nested) => { + const stylesheet = compile( + `.group .x { color: red; ${nested} }`, + ).stylesheet(); + const rules = (name: string) => + stylesheet.s?.find(([className]) => className === name)?.[1] ?? []; + + expect(rules("x").map((rule) => rule.d)).toStrictEqual([ + [{ color: "#f00" }], + [{ color: "#00f" }], + ]); + for (const rule of rules("x")) { + expect(rule.cq).toContainEqual({ n: "g:group" }); + } + expect(rules("x").flatMap((rule) => rule.c ?? [])).toStrictEqual([]); + expect(rules("group").flatMap((rule) => rule.d ?? [])).toStrictEqual([]); + }, +); + test("container queries", () => { const compiled = compile(` @container (width > 400px) { diff --git a/src/__tests__/native/grouping.test.tsx b/src/__tests__/native/grouping.test.tsx index 1484e4b8..1b51351d 100644 --- a/src/__tests__/native/grouping.test.tsx +++ b/src/__tests__/native/grouping.test.tsx @@ -83,6 +83,23 @@ test("group - active (animated) supplies changed and restored transition targets expectStyle("#000"); }); +test("a rule nested in a descendant selector applies to that selector's subject", () => { + registerCSS( + `.group .x { color: red; @media (min-width: 0px) { color: blue; } }`, + ); + + render( + + + , + ); + + expect(screen.getByTestId(childID).props.style).toStrictEqual({ + color: "#00f", + }); + expect(screen.getByTestId(parentID).props.style).toStrictEqual(undefined); +}); + test("group selector", () => { registerCSS( `.my-a.my-b .my-class { diff --git a/src/compiler/selector-builder.ts b/src/compiler/selector-builder.ts index 6787c0ca..10675d5e 100644 --- a/src/compiler/selector-builder.ts +++ b/src/compiler/selector-builder.ts @@ -63,9 +63,10 @@ export function getClassNameSelectors( } else if (isUniversalSelector(selector)) { return [{ type: "universalVariables" }]; } else { + // A nested rule reads its parent's selector list again, so the list is never reversed in place return ( parseComponents( - selector.reverse(), + selector.toReversed(), options, root, root,