diff --git a/packages/compiler-sfc/__tests__/compileStyle.spec.ts b/packages/compiler-sfc/__tests__/compileStyle.spec.ts index 71c0689a397..72727c43196 100644 --- a/packages/compiler-sfc/__tests__/compileStyle.spec.ts +++ b/packages/compiler-sfc/__tests__/compileStyle.spec.ts @@ -41,6 +41,12 @@ describe('SFC scoped CSS', () => { ) }) + test('nesting selector', () => { + expect(compileScoped(`h1 { color: red; .foo { color: red; } }`)).toMatch( + `h1 {\n&[data-v-test] { color: red;\n}\n.foo[data-v-test] { color: red;`, + ) + }) + test('multiple selectors', () => { expect(compileScoped(`h1 .foo, .bar, .baz { color: red; }`)).toMatch( `h1 .foo[data-v-test], .bar[data-v-test], .baz[data-v-test] { color: red;`, @@ -95,6 +101,13 @@ describe('SFC scoped CSS', () => { ":where(.foo[data-v-test] .bar) { color: red; }" `) + expect(compileScoped(`:deep(.foo) { color: red; .bar { color: red; } }`)) + .toMatchInlineSnapshot(` + "[data-v-test] .foo { color: red; + .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 3812e67092a..6a588b56726 100644 --- a/packages/compiler-sfc/src/style/pluginScoped.ts +++ b/packages/compiler-sfc/src/style/pluginScoped.ts @@ -1,4 +1,10 @@ -import type { AtRule, PluginCreator, Rule } from 'postcss' +import { + type AtRule, + type Container, + type Document, + type PluginCreator, + Rule, +} from 'postcss' import selectorParser from 'postcss-selector-parser' import { warn } from '../warn' @@ -71,21 +77,32 @@ function processRule(id: string, rule: Rule) { return } processedRules.add(rule) + let deep = false + let parent: Document | Container | undefined = rule.parent + while (parent && parent.type !== 'root') { + if ((parent as any).__deep) { + deep = true + break + } + parent = parent.parent + } rule.selector = selectorParser(selectorRoot => { selectorRoot.each(selector => { - rewriteSelector(id, selector, selectorRoot) + rewriteSelector(id, rule, selector, selectorRoot, deep) }) }).processSync(rule.selector) } function rewriteSelector( id: string, + rule: Rule, selector: selectorParser.Selector, selectorRoot: selectorParser.Root, + deep: boolean, slotted = false, ) { let node: selectorParser.Node | null = null - let shouldInject = true + let shouldInject = !deep // find the last child node to insert attribute selector selector.each(n => { // DEPRECATED ">>>" and "/deep/" combinator @@ -107,6 +124,7 @@ function rewriteSelector( // deep: inject [id] attribute at the node before the ::v-deep // combinator. if (value === ':deep' || value === '::v-deep') { + ;(rule as any).__deep = true if (n.nodes.length) { // .foo ::v-deep(.bar) -> .foo[xxxxxxx] .bar // replace the current node with ::v-deep's inner selector @@ -147,7 +165,14 @@ function rewriteSelector( // instead. // ::v-slotted(.foo) -> .foo[xxxxxxx-s] if (value === ':slotted' || value === '::v-slotted') { - rewriteSelector(id, n.nodes[0], selectorRoot, true /* slotted */) + rewriteSelector( + id, + rule, + n.nodes[0], + selectorRoot, + deep, + true /* slotted */, + ) let last: selectorParser.Selector['nodes'][0] = n n.nodes[0].each(ss => { selector.insertAfter(last, ss) @@ -206,11 +231,27 @@ function rewriteSelector( } }) + if (rule.nodes.some(node => node.type === 'rule')) { + const deep = (rule as any).__deep + const decls = rule.nodes.filter(node => node.type === 'decl') + if (!deep && decls.length) { + for (const decl of decls) { + rule.removeChild(decl) + } + const hostRule = new Rule({ + nodes: decls, + selector: '&', + }) + rule.prepend(hostRule) + } + shouldInject = deep + } + if (node) { const { type, value } = node as selectorParser.Node if (type === 'pseudo' && (value === ':is' || value === ':where')) { ;(node as selectorParser.Pseudo).nodes.forEach(value => - rewriteSelector(id, value, selectorRoot, slotted), + rewriteSelector(id, rule, value, selectorRoot, deep, slotted), ) shouldInject = false }