From 506f21924c925ea81152095d9cbe803992be9a17 Mon Sep 17 00:00:00 2001 From: babu-ch Date: Tue, 10 Mar 2026 18:55:01 +0900 Subject: [PATCH 1/7] fix(compiler-sfc): keep leading universal selector before non-space combinators --- .../__tests__/compileStyle.spec.ts | 21 +++++++++++++++++++ .../compiler-sfc/src/style/pluginScoped.ts | 12 +++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileStyle.spec.ts b/packages/compiler-sfc/__tests__/compileStyle.spec.ts index 9b7e7c53710..bb6f6a7a24d 100644 --- a/packages/compiler-sfc/__tests__/compileStyle.spec.ts +++ b/packages/compiler-sfc/__tests__/compileStyle.spec.ts @@ -397,6 +397,27 @@ color: red ).toHaveBeenWarned() }) }) + + test('should keep leading universal selector before non-space combinator', () => { + // * > .foo: * removed → invalid CSS ` > .foo` + expect(compileScoped(`* > .section { color: red; }`)) + .toMatchInlineSnapshot(` + "* > .section[data-v-test] { color: red; + }" + `) + // * + .foo: * removed → invalid CSS ` + .foo` + expect(compileScoped(`* + .section { color: red; }`)) + .toMatchInlineSnapshot(` + "* + .section[data-v-test] { color: red; + }" + `) + // * ~ .foo: * removed → invalid CSS ` ~ .foo` + expect(compileScoped(`* ~ .section { color: red; }`)) + .toMatchInlineSnapshot(` + "* ~ .section[data-v-test] { color: red; + }" + `) + }) }) describe('SFC CSS modules', () => { diff --git a/packages/compiler-sfc/src/style/pluginScoped.ts b/packages/compiler-sfc/src/style/pluginScoped.ts index 81c735a6c3f..439ad08e3dd 100644 --- a/packages/compiler-sfc/src/style/pluginScoped.ts +++ b/packages/compiler-sfc/src/style/pluginScoped.ts @@ -199,9 +199,17 @@ function rewriteSelector( if (!prev) { // * .foo {} -> .foo[xxxxxxx] {} if (next) { - if (next.type === 'combinator' && next.value === ' ') { - selector.removeChild(next) + if (next.type === 'combinator') { + if (next.value === ' ') { + // * .foo {} -> .foo[xxxxxxx] {} + selector.removeChild(next) + selector.removeChild(n) + return + } + // * > .foo, * + .foo, * ~ .foo: keep * + return } + // *.foo {} -> .foo[xxxxxxx] {} selector.removeChild(n) return } else { From f22dfc6a93aa4d0a1e0b905bfd7c4d12b439c5e8 Mon Sep 17 00:00:00 2001 From: babu-ch Date: Tue, 10 Mar 2026 18:55:52 +0900 Subject: [PATCH 2/7] fix(compiler-sfc): keep leading universal selector in nested rules --- .../__tests__/compileStyle.spec.ts | 40 +++++++++++++++++++ .../compiler-sfc/src/style/pluginScoped.ts | 11 +++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileStyle.spec.ts b/packages/compiler-sfc/__tests__/compileStyle.spec.ts index bb6f6a7a24d..86f8785b101 100644 --- a/packages/compiler-sfc/__tests__/compileStyle.spec.ts +++ b/packages/compiler-sfc/__tests__/compileStyle.spec.ts @@ -418,6 +418,46 @@ color: red }" `) }) + + test('should keep leading universal selector in nested rules', () => { + // non-nested: * should still be removed + expect(compileScoped(`* .section { color: red; }`)).toMatchInlineSnapshot(` + ".section[data-v-test] { color: red; + }" + `) + // .outer { * .section {} } → * must be preserved to avoid matching direct children + expect(compileScoped(`.outer { * .section { color: red; } }`)) + .toMatchInlineSnapshot(` + ".outer { + * .section[data-v-test] { color: red; + } + }" + `) + // nested non-space combinator should also keep * + expect(compileScoped(`.outer { * > .section { color: red; } }`)) + .toMatchInlineSnapshot(` + ".outer { + * > .section[data-v-test] { color: red; + } + }" + `) + // *.foo in nested: * should still be removed (*.foo is equivalent to .foo) + expect(compileScoped(`.outer { *.foo { color: red; } }`)) + .toMatchInlineSnapshot(` + ".outer { + .foo[data-v-test] { color: red; + } + }" + `) + // standalone * in nested: converted to [data-v-id] + expect(compileScoped(`.outer { * { color: red; } }`)) + .toMatchInlineSnapshot(` + ".outer { + [data-v-test] { color: red; + } + }" + `) + }) }) describe('SFC CSS modules', () => { diff --git a/packages/compiler-sfc/src/style/pluginScoped.ts b/packages/compiler-sfc/src/style/pluginScoped.ts index 439ad08e3dd..94f0727a078 100644 --- a/packages/compiler-sfc/src/style/pluginScoped.ts +++ b/packages/compiler-sfc/src/style/pluginScoped.ts @@ -201,9 +201,14 @@ function rewriteSelector( if (next) { if (next.type === 'combinator') { if (next.value === ' ') { - // * .foo {} -> .foo[xxxxxxx] {} - selector.removeChild(next) - selector.removeChild(n) + const isNested = rule.parent?.type === 'rule' + if (!isNested) { + // * .foo {} -> .foo[xxxxxxx] {} + selector.removeChild(next) + selector.removeChild(n) + return + } + // nested: .outer { * .foo {} } -> keep * to preserve semantics return } // * > .foo, * + .foo, * ~ .foo: keep * From fb98d8d52703e82da8310d044718cc2215cb17d5 Mon Sep 17 00:00:00 2001 From: babu-ch Date: Tue, 10 Mar 2026 19:04:55 +0900 Subject: [PATCH 3/7] refactor(compiler-sfc): simplify leading universal selector condition --- packages/compiler-sfc/src/style/pluginScoped.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/compiler-sfc/src/style/pluginScoped.ts b/packages/compiler-sfc/src/style/pluginScoped.ts index 94f0727a078..2d19176ae5a 100644 --- a/packages/compiler-sfc/src/style/pluginScoped.ts +++ b/packages/compiler-sfc/src/style/pluginScoped.ts @@ -200,18 +200,12 @@ function rewriteSelector( // * .foo {} -> .foo[xxxxxxx] {} if (next) { if (next.type === 'combinator') { - if (next.value === ' ') { - const isNested = rule.parent?.type === 'rule' - if (!isNested) { - // * .foo {} -> .foo[xxxxxxx] {} - selector.removeChild(next) - selector.removeChild(n) - return - } - // nested: .outer { * .foo {} } -> keep * to preserve semantics - return + if (next.value === ' ' && rule.parent?.type !== 'rule') { + // * .foo {} -> .foo[xxxxxxx] {} + selector.removeChild(next) + selector.removeChild(n) } - // * > .foo, * + .foo, * ~ .foo: keep * + // keep *: nested .outer { * .foo {} } or non-space combinator (* > .foo etc.) return } // *.foo {} -> .foo[xxxxxxx] {} From 121601d04cdf9352ea031c95ea14943fb292b0f2 Mon Sep 17 00:00:00 2001 From: babu-ch Date: Tue, 10 Mar 2026 20:34:50 +0900 Subject: [PATCH 4/7] fix(compiler-sfc): walk ancestor chain to detect CSS nesting for leading universal selector --- .../__tests__/compileStyle.spec.ts | 19 +++++++++++++++++++ .../compiler-sfc/src/style/pluginScoped.ts | 10 ++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileStyle.spec.ts b/packages/compiler-sfc/__tests__/compileStyle.spec.ts index 86f8785b101..019791e40f0 100644 --- a/packages/compiler-sfc/__tests__/compileStyle.spec.ts +++ b/packages/compiler-sfc/__tests__/compileStyle.spec.ts @@ -457,6 +457,25 @@ color: red } }" `) + // @media at top-level (no rule ancestor): * should still be removed + expect(compileScoped(`@media screen { * .section { color: red; } }`)) + .toMatchInlineSnapshot(` + "@media screen { + .section[data-v-test] { color: red; + } + }" + `) + // * nested inside .outer via @media: * must be preserved + expect( + compileScoped(`.outer { @media screen { * .section { color: red; } } }`), + ).toMatchInlineSnapshot(` + ".outer[data-v-test] { + @media screen { + * .section[data-v-test] { color: red; + } + } + }" + `) }) }) diff --git a/packages/compiler-sfc/src/style/pluginScoped.ts b/packages/compiler-sfc/src/style/pluginScoped.ts index 2d19176ae5a..8414ef69dc8 100644 --- a/packages/compiler-sfc/src/style/pluginScoped.ts +++ b/packages/compiler-sfc/src/style/pluginScoped.ts @@ -76,17 +76,22 @@ function processRule(id: string, rule: Rule) { } processedRules.add(rule) let deep = false + let isNested = false let parent: Document | Container | undefined = rule.parent while (parent && parent.type !== 'root') { if ((parent as any).__deep) { deep = true break } + if (parent.type === 'rule') { + isNested = true + break + } parent = parent.parent } rule.selector = selectorParser(selectorRoot => { selectorRoot.each(selector => { - rewriteSelector(id, rule, selector, selectorRoot, deep) + rewriteSelector(id, rule, selector, selectorRoot, deep, false, isNested) }) }).processSync(rule.selector) } @@ -98,6 +103,7 @@ function rewriteSelector( selectorRoot: selectorParser.Root, deep: boolean, slotted = false, + isNested = false, ) { let node: selectorParser.Node | null = null let shouldInject = !deep @@ -200,7 +206,7 @@ function rewriteSelector( // * .foo {} -> .foo[xxxxxxx] {} if (next) { if (next.type === 'combinator') { - if (next.value === ' ' && rule.parent?.type !== 'rule') { + if (next.value === ' ' && !isNested) { // * .foo {} -> .foo[xxxxxxx] {} selector.removeChild(next) selector.removeChild(n) From 0cf3322611bd2bc09b9091cfc0664f4c9e25b3c6 Mon Sep 17 00:00:00 2001 From: babu-ch Date: Fri, 13 Mar 2026 12:11:32 +0900 Subject: [PATCH 5/7] refactor(compiler-sfc): remove unnecessary comments in compileStyle tests --- packages/compiler-sfc/__tests__/compileStyle.spec.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileStyle.spec.ts b/packages/compiler-sfc/__tests__/compileStyle.spec.ts index 019791e40f0..83e2abf61dd 100644 --- a/packages/compiler-sfc/__tests__/compileStyle.spec.ts +++ b/packages/compiler-sfc/__tests__/compileStyle.spec.ts @@ -399,19 +399,16 @@ color: red }) test('should keep leading universal selector before non-space combinator', () => { - // * > .foo: * removed → invalid CSS ` > .foo` expect(compileScoped(`* > .section { color: red; }`)) .toMatchInlineSnapshot(` "* > .section[data-v-test] { color: red; }" `) - // * + .foo: * removed → invalid CSS ` + .foo` expect(compileScoped(`* + .section { color: red; }`)) .toMatchInlineSnapshot(` "* + .section[data-v-test] { color: red; }" `) - // * ~ .foo: * removed → invalid CSS ` ~ .foo` expect(compileScoped(`* ~ .section { color: red; }`)) .toMatchInlineSnapshot(` "* ~ .section[data-v-test] { color: red; @@ -420,12 +417,10 @@ color: red }) test('should keep leading universal selector in nested rules', () => { - // non-nested: * should still be removed expect(compileScoped(`* .section { color: red; }`)).toMatchInlineSnapshot(` ".section[data-v-test] { color: red; }" `) - // .outer { * .section {} } → * must be preserved to avoid matching direct children expect(compileScoped(`.outer { * .section { color: red; } }`)) .toMatchInlineSnapshot(` ".outer { @@ -433,7 +428,6 @@ color: red } }" `) - // nested non-space combinator should also keep * expect(compileScoped(`.outer { * > .section { color: red; } }`)) .toMatchInlineSnapshot(` ".outer { @@ -441,7 +435,6 @@ color: red } }" `) - // *.foo in nested: * should still be removed (*.foo is equivalent to .foo) expect(compileScoped(`.outer { *.foo { color: red; } }`)) .toMatchInlineSnapshot(` ".outer { @@ -449,7 +442,6 @@ color: red } }" `) - // standalone * in nested: converted to [data-v-id] expect(compileScoped(`.outer { * { color: red; } }`)) .toMatchInlineSnapshot(` ".outer { @@ -457,7 +449,6 @@ color: red } }" `) - // @media at top-level (no rule ancestor): * should still be removed expect(compileScoped(`@media screen { * .section { color: red; } }`)) .toMatchInlineSnapshot(` "@media screen { @@ -465,7 +456,6 @@ color: red } }" `) - // * nested inside .outer via @media: * must be preserved expect( compileScoped(`.outer { @media screen { * .section { color: red; } } }`), ).toMatchInlineSnapshot(` From 78be4a4e221a568a22ee0762b80ac71b994306b3 Mon Sep 17 00:00:00 2001 From: babu-ch Date: Fri, 13 Mar 2026 13:08:06 +0900 Subject: [PATCH 6/7] fix(compiler-sfc): fully walk ancestor chain for deep and nested detection --- .../__tests__/compileStyle.spec.ts | 18 ++++++++++++++++++ .../compiler-sfc/src/style/pluginScoped.ts | 2 -- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileStyle.spec.ts b/packages/compiler-sfc/__tests__/compileStyle.spec.ts index 83e2abf61dd..fc2ea1bbc91 100644 --- a/packages/compiler-sfc/__tests__/compileStyle.spec.ts +++ b/packages/compiler-sfc/__tests__/compileStyle.spec.ts @@ -158,6 +158,24 @@ color: red } }" `) + expect(compileScoped(`:deep(.foo) { .bar { .baz { color: red; } } }`)) + .toMatchInlineSnapshot(` + "[data-v-test] .foo { + .bar { + .baz { color: red; + } + } + }" + `) + expect(compileScoped(`.outer { :deep(.foo) { * .bar { color: red; } } }`)) + .toMatchInlineSnapshot(` + ".outer { + [data-v-test] .foo { + * .bar { color: red; + } + } + }" + `) }) test('::v-slotted', () => { diff --git a/packages/compiler-sfc/src/style/pluginScoped.ts b/packages/compiler-sfc/src/style/pluginScoped.ts index 8414ef69dc8..02a05a41ae3 100644 --- a/packages/compiler-sfc/src/style/pluginScoped.ts +++ b/packages/compiler-sfc/src/style/pluginScoped.ts @@ -81,11 +81,9 @@ function processRule(id: string, rule: Rule) { while (parent && parent.type !== 'root') { if ((parent as any).__deep) { deep = true - break } if (parent.type === 'rule') { isNested = true - break } parent = parent.parent } From ecde825390e244b5d87b879a5df3381236f3ca12 Mon Sep 17 00:00:00 2001 From: babu-ch Date: Fri, 13 Mar 2026 13:34:38 +0900 Subject: [PATCH 7/7] fix(compiler-sfc): propagate isNested to recursive rewriteSelector calls --- .../__tests__/compileStyle.spec.ts | 40 +++++++++++++++++++ .../compiler-sfc/src/style/pluginScoped.ts | 3 +- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/packages/compiler-sfc/__tests__/compileStyle.spec.ts b/packages/compiler-sfc/__tests__/compileStyle.spec.ts index fc2ea1bbc91..f4fecfaef68 100644 --- a/packages/compiler-sfc/__tests__/compileStyle.spec.ts +++ b/packages/compiler-sfc/__tests__/compileStyle.spec.ts @@ -485,6 +485,46 @@ color: red }" `) }) + + test('should keep leading universal selector in :is() in nested rules', () => { + expect(compileScoped(`.outer { :is(* .foo) { color: red; } }`)) + .toMatchInlineSnapshot(` + ".outer { + :is(* .foo[data-v-test]) { color: red; + } + }" + `) + }) + + test('should keep leading universal selector in :where() in nested rules', () => { + expect(compileScoped(`.outer { :where(* .foo) { color: red; } }`)) + .toMatchInlineSnapshot(` + ".outer { + :where(* .foo[data-v-test]) { color: red; + } + }" + `) + }) + + test('should keep leading universal selector before non-space combinator in :is() in nested rules', () => { + expect(compileScoped(`.outer { :is(* > .foo) { color: red; } }`)) + .toMatchInlineSnapshot(` + ".outer { + :is(* > .foo[data-v-test]) { color: red; + } + }" + `) + }) + + test('should keep leading universal selector in ::v-slotted() in nested rules', () => { + expect(compileScoped(`.outer { ::v-slotted(* .foo) { color: red; } }`)) + .toMatchInlineSnapshot(` + ".outer { + * .foo[data-v-test-s] { color: red; + } + }" + `) + }) }) describe('SFC CSS modules', () => { diff --git a/packages/compiler-sfc/src/style/pluginScoped.ts b/packages/compiler-sfc/src/style/pluginScoped.ts index 02a05a41ae3..1506012eca9 100644 --- a/packages/compiler-sfc/src/style/pluginScoped.ts +++ b/packages/compiler-sfc/src/style/pluginScoped.ts @@ -174,6 +174,7 @@ function rewriteSelector( selectorRoot, deep, true /* slotted */, + isNested, ) let last: selectorParser.Selector['nodes'][0] = n n.nodes[0].each(ss => { @@ -255,7 +256,7 @@ function rewriteSelector( const { type, value } = node as selectorParser.Node if (type === 'pseudo' && (value === ':is' || value === ':where')) { ;(node as selectorParser.Pseudo).nodes.forEach(value => - rewriteSelector(id, rule, value, selectorRoot, deep, slotted), + rewriteSelector(id, rule, value, selectorRoot, deep, slotted, isNested), ) shouldInject = false }