Skip to content

fix(compiler-sfc): resolve :deep inside :is/:where at start position#14736

Closed
ZackaryShen wants to merge 1 commit intovuejs:mainfrom
ZackaryShen:fix/deep-selector-in-is-14724
Closed

fix(compiler-sfc): resolve :deep inside :is/:where at start position#14736
ZackaryShen wants to merge 1 commit intovuejs:mainfrom
ZackaryShen:fix/deep-selector-in-is-14724

Conversation

@ZackaryShen
Copy link
Copy Markdown

@ZackaryShen ZackaryShen commented Apr 19, 2026

Description

Fix issue #14724 where :deep at the start of :is/:where selector was not being properly resolved.

Before

:is(:deep(.foo)) .bar { color: red; }

Was compiled to:

:is(:deep(.foo)) .bar { color: red; } /* :deep not resolved! */

After

:is(:deep(.foo)) .bar { color: red; }

Is compiled to:

:is([data-v-xxx] .foo) .bar { color: red; }

Test cases added

  • :is(:deep(.foo))
  • :where(:deep(.foo))
  • :is(:deep(.foo)) .bar

Fixes #14724

Summary by CodeRabbit

  • Bug Fixes

    • Fixed scoped CSS attribute injection when :deep() is used within :is() and :where() pseudo-classes, ensuring proper selector rewriting.
  • Tests

    • Added test coverage for scoped CSS compilation of :deep() in :is() and :where() pseudo-selectors.

Fix issue vuejs#14724 where :deep at the start of :is/:where selector
was not being properly resolved to [data-v-xxx].

Before: :is(:deep(.foo)) -> :is(:deep(.foo))
After:  :is(:deep(.foo)) -> :is([data-v-xxx] .foo)

The fix checks if __deep was set during recursion and injects
the scoped attribute at the start of the inner selector.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 19, 2026

📝 Walkthrough

Walkthrough

This PR fixes scoped CSS compilation for :deep() pseudo-selectors when nested at the start of :is() and :where() pseudo-classes. The scoped attribute is now correctly injected before the deep selector rather than after the entire pseudo-class block.

Changes

Cohort / File(s) Summary
Test Coverage
packages/compiler-sfc/__tests__/compileStyle.spec.ts
Added 16 lines of test cases validating correct rewriting of :is(:deep(.foo)) and :where(:deep(.foo)) to place scoped attributes before the deep selector ([data-v-test] .foo within the pseudo-class), including cases with additional selectors following.
Selector Rewriting Logic
packages/compiler-sfc/src/style/pluginScoped.ts
Updated rewriteSelector function to conditionally insert scoped attribute ([id] or [id-s]) before the first node inside :is()/:where() with a leading space combinator when rule.__deep is set, ensuring correct attribute placement for deep selectors within these pseudo-classes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • #12918 — Modifies the same rewriteSelector logic in pluginScoped.ts for handling trailing universal selectors in scoped attribute injection.

Suggested labels

scope: sfc, :hammer: p3-minor-bug

Suggested reviewers

  • edison1105
  • Doctor-wu

Poem

🐰 A rabbit hops through CSS rules so deep,
Where :is and :where selectors sleep,
Now :deep finds its rightful place,
With scoped attributes keeping pace,
The styles compile, no more bemused!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: fixing how :deep() inside :is()/:where() is resolved when appearing at the start position.
Linked Issues check ✅ Passed The code changes directly address issue #14724 by implementing the fix to resolve :deep() at the start of :is()/:where(), with matching test coverage for the expected transformations.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing the :deep() resolution issue: test file additions and pluginScoped.ts modifications directly implement the required fix with no unrelated changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/compiler-sfc/src/style/pluginScoped.ts`:
- Around line 249-268: The new injection block incorrectly calls
selector.insertBefore with an inner-selector node (causing wrong placement and
duplicate scoped attributes) and it’s also skipped for cases like
:is(:deep(.foo)) .bar because rewriteSelector’s node-tracking lets later
siblings overwrite the recorded pseudo; fix by removing this explicit insertion
(rely on the recursive call in rewriteSelector to scope the inner :deep/:deep()
content) or, if you prefer explicit handling, perform insertions on the inner
Selector container (use (node as selectorParser.Pseudo).nodes[0] as the
container) rather than the outer selector and only when shouldInject is false to
avoid double-injection; additionally update rewriteSelector’s node-tracking so
the recorded node (the :is/:where pseudo) is not clobbered by subsequent sibling
nodes so the :is branch still runs for selectors like :is(:deep(.foo)) .bar.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a686998f-d288-429f-abe6-2ca1631da6f0

📥 Commits

Reviewing files that changed from the base of the PR and between 7df0edd and 8d68719.

📒 Files selected for processing (2)
  • packages/compiler-sfc/__tests__/compileStyle.spec.ts
  • packages/compiler-sfc/src/style/pluginScoped.ts

Comment on lines +249 to +268
// If __deep was set inside :is/:where, we need to inject [id] at the start
// of the inner selector (before the first node), not after.
// :is(:deep(.foo)) should become :is([data-v-xxx] .foo)
if ((rule as any).__deep && (node as selectorParser.Pseudo).nodes.length) {
selector.insertBefore(
(node as selectorParser.Pseudo).nodes[0],
selectorParser.attribute({
attribute: slotted ? id + '-s' : id,
value: slotted ? id + '-s' : id,
raws: {},
quoteMark: `"`,
}),
)
selector.insertBefore(
(node as selectorParser.Pseudo).nodes[0],
selectorParser.combinator({
value: ' ',
}),
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Injection targets the wrong container and double‑injects the scoped attribute.

Three concerns with this block:

  1. Wrong insertion target. selector here is the outer selector that contains the :is(...) pseudo (node). (node as Pseudo).nodes[0] is the first inner Selector of :is, which is a child of node, not of selector. Container.insertBefore(oldNode, newNode) in postcss-selector-parser resolves the position via this.index(oldNode); when oldNode is not a direct child, index returns -1 and Array.prototype.splice(-1, 0, newNode) inserts near the end of the outer selector — not inside the inner selector as intended.

  2. Double injection. The preceding forEach already recurses into each inner selector with the caller's deep (false at the top level), so inside the recursion shouldInject = !deep is true. When the recursion processes :deep(.foo) it rewrites it to [' ', '.foo'], then the existing if (shouldInject) block (lines 282–295) prepends [data-v-xxx], producing [[data-v-xxx], ' ', .foo]. The inner selector is already correctly scoped — adding another insertion here duplicates it.

  3. Won’t fire for :is(:deep(.foo)) .bar. The node‑tracking loop (lines 221–228) only assigns :is/:where to node when !node. With a trailing class like .bar, that class overrides node (first clause of the ||). So at line 243 node is .bar, not :is, and neither the recursion at 246–248 nor this new block is reached — meaning the third new test case cannot produce the asserted ":is([data-v-test] .foo) .bar" snapshot.

Sketch of a fix that targets the correct container (relying on the recursive call to inject for the :deep part is the simplest option; if you want explicit handling, operate on the inner Selector):

🔧 Proposed direction
-      ;(node as selectorParser.Pseudo).nodes.forEach(value =>
-        rewriteSelector(id, rule, value, selectorRoot, deep, slotted),
-      )
-      // If __deep was set inside :is/:where, we need to inject [id] at the start
-      // of the inner selector (before the first node), not after.
-      // :is(:deep(.foo)) should become :is([data-v-xxx] .foo)
-      if ((rule as any).__deep && (node as selectorParser.Pseudo).nodes.length) {
-        selector.insertBefore(
-          (node as selectorParser.Pseudo).nodes[0],
-          selectorParser.attribute({
-            attribute: slotted ? id + '-s' : id,
-            value: slotted ? id + '-s' : id,
-            raws: {},
-            quoteMark: `"`,
-          }),
-        )
-        selector.insertBefore(
-          (node as selectorParser.Pseudo).nodes[0],
-          selectorParser.combinator({
-            value: ' ',
-          }),
-        )
-      }
-      shouldInject = false
+      ;(node as selectorParser.Pseudo).nodes.forEach(inner =>
+        rewriteSelector(id, rule, inner, selectorRoot, deep, slotted),
+      )
+      // The recursive call above already injects [id] at the start of the
+      // inner selector when :deep is encountered there, so nothing extra
+      // needs to be done on the outer selector here.
+      shouldInject = false

If a trailing combinator (e.g. :is(:deep(.foo)) .bar) also needs to be handled, the :is/:where tracking at lines 221–228 has to be extended so that node still points at the pseudo even when later siblings exist — otherwise this whole branch is skipped.

Please run the three new snapshot cases and confirm the actual output; I suspect at least the third (:is(:deep(.foo)) .bar) will not match the recorded snapshot and the first two may emit a stray scoped attribute outside :is(...).

#!/bin/bash
# Verify that the new test cases in compileStyle.spec.ts actually pass.
fd -t f 'compileStyle.spec.ts' packages/compiler-sfc
rg -n -C2 ':is\(:deep|:where\(:deep' packages/compiler-sfc/__tests__/compileStyle.spec.ts
# And inspect rewriteSelector’s node-tracking logic to confirm the :is path is reached for the `:is(:deep(.foo)) .bar` case.
ast-grep --pattern $'function rewriteSelector($$$) { $$$ }'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/compiler-sfc/src/style/pluginScoped.ts` around lines 249 - 268, The
new injection block incorrectly calls selector.insertBefore with an
inner-selector node (causing wrong placement and duplicate scoped attributes)
and it’s also skipped for cases like :is(:deep(.foo)) .bar because
rewriteSelector’s node-tracking lets later siblings overwrite the recorded
pseudo; fix by removing this explicit insertion (rely on the recursive call in
rewriteSelector to scope the inner :deep/:deep() content) or, if you prefer
explicit handling, perform insertions on the inner Selector container (use (node
as selectorParser.Pseudo).nodes[0] as the container) rather than the outer
selector and only when shouldInject is false to avoid double-injection;
additionally update rewriteSelector’s node-tracking so the recorded node (the
:is/:where pseudo) is not clobbered by subsequent sibling nodes so the :is
branch still runs for selectors like :is(:deep(.foo)) .bar.

@edison1105
Copy link
Copy Markdown
Member

duplicate of #14725

@edison1105 edison1105 closed this Apr 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

:deep within :is is not resolved when there is another selector

2 participants