diff --git a/.changeset/nice-numbers-remember.md b/.changeset/nice-numbers-remember.md new file mode 100644 index 000000000000..810343aedf1c --- /dev/null +++ b/.changeset/nice-numbers-remember.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure explicit nesting selector is always applied diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index 4306d49d818f..4207a1650269 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -77,38 +77,9 @@ const visitors = { } }, ComplexSelector(node, context) { - const selectors = truncate(node); + const selectors = get_relative_selectors(node); const inner = selectors[selectors.length - 1]; - if (node.metadata.rule?.metadata.parent_rule && selectors.length > 0) { - let has_explicit_nesting_selector = false; - - // nesting could be inside pseudo classes like :is, :has or :where - for (let selector of selectors) { - walk( - selector, - {}, - { - // @ts-ignore - NestingSelector() { - has_explicit_nesting_selector = true; - } - } - ); - // if we found one we can break from the others - if (has_explicit_nesting_selector) break; - } - - if (!has_explicit_nesting_selector) { - selectors[0] = { - ...selectors[0], - combinator: descendant_combinator - }; - - selectors.unshift(nesting_selector); - } - } - if (context.state.from_render_tag) { // We're searching for a match that crosses a render tag boundary. That means we have to both traverse up // the element tree (to see if we find an entry point) but also remove selectors from the end (assuming @@ -156,6 +127,50 @@ const visitors = { } }; +/** + * Retrieves the relative selectors (minus the trailing globals) from a complex selector. + * Also searches them for any existing `&` selectors and adds one if none are found. + * This ensures we traverse up to the parent rule when the inner selectors match and we're + * trying to see if the parent rule also matches. + * @param {Compiler.Css.ComplexSelector} node + */ +function get_relative_selectors(node) { + const selectors = truncate(node); + + if (node.metadata.rule?.metadata.parent_rule && selectors.length > 0) { + let has_explicit_nesting_selector = false; + + // nesting could be inside pseudo classes like :is, :has or :where + for (let selector of selectors) { + walk( + selector, + {}, + { + // @ts-ignore + NestingSelector() { + has_explicit_nesting_selector = true; + } + } + ); + // if we found one we can break from the others + if (has_explicit_nesting_selector) break; + } + + if (!has_explicit_nesting_selector) { + if (selectors[0].combinator === null) { + selectors[0] = { + ...selectors[0], + combinator: descendant_combinator + }; + } + + selectors.unshift(nesting_selector); + } + } + + return selectors; +} + /** * Discard trailing `:global(...)` selectors without a `:has/is/where/not(...)` modifier, these are unused for scoping purposes * @param {Compiler.Css.ComplexSelector} node @@ -641,7 +656,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, const parent = /** @type {Compiler.Css.Rule} */ (rule.metadata.parent_rule); for (const complex_selector of parent.prelude.children) { - if (apply_selector(truncate(complex_selector), parent, element, state)) { + if (apply_selector(get_relative_selectors(complex_selector), parent, element, state)) { complex_selector.metadata.used = true; matched = true; } diff --git a/packages/svelte/tests/css/samples/nested-in-pseudo/_config.js b/packages/svelte/tests/css/samples/nested-in-pseudo/_config.js deleted file mode 100644 index 292c6c49ac9d..000000000000 --- a/packages/svelte/tests/css/samples/nested-in-pseudo/_config.js +++ /dev/null @@ -1,5 +0,0 @@ -import { test } from '../../test'; - -export default test({ - warnings: [] -}); diff --git a/packages/svelte/tests/css/samples/nested-in-pseudo/expected.css b/packages/svelte/tests/css/samples/nested-in-pseudo/expected.css deleted file mode 100644 index 3f365a898884..000000000000 --- a/packages/svelte/tests/css/samples/nested-in-pseudo/expected.css +++ /dev/null @@ -1,6 +0,0 @@ - -nav.svelte-xyz{ - header:where(.svelte-xyz):has(&){ - color: red; - } -} diff --git a/packages/svelte/tests/css/samples/nested-in-pseudo/expected.html b/packages/svelte/tests/css/samples/nested-in-pseudo/expected.html deleted file mode 100644 index 44a5d4a52dd6..000000000000 --- a/packages/svelte/tests/css/samples/nested-in-pseudo/expected.html +++ /dev/null @@ -1 +0,0 @@ -
\ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-in-pseudo/input.svelte b/packages/svelte/tests/css/samples/nested-in-pseudo/input.svelte deleted file mode 100644 index 8a6758f60f3f..000000000000 --- a/packages/svelte/tests/css/samples/nested-in-pseudo/input.svelte +++ /dev/null @@ -1,11 +0,0 @@ -
- -
- - \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nesting-selectors/_config.js b/packages/svelte/tests/css/samples/nesting-selectors/_config.js new file mode 100644 index 000000000000..8960eff1b684 --- /dev/null +++ b/packages/svelte/tests/css/samples/nesting-selectors/_config.js @@ -0,0 +1,48 @@ +import { test } from '../../test'; + +export default test({ + warnings: [ + { + code: 'css_unused_selector', + message: 'Unused CSS selector ".unused:has(&)"', + start: { + line: 10, + column: 2, + character: 105 + }, + end: { + line: 10, + column: 16, + character: 119 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector "&.unused"', + start: { + line: 23, + column: 3, + character: 223 + }, + end: { + line: 23, + column: 11, + character: 231 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector "&.unused"', + start: { + line: 37, + column: 3, + character: 344 + }, + end: { + line: 37, + column: 11, + character: 352 + } + } + ] +}); diff --git a/packages/svelte/tests/css/samples/nesting-selectors/expected.css b/packages/svelte/tests/css/samples/nesting-selectors/expected.css new file mode 100644 index 000000000000..1b606b10cae1 --- /dev/null +++ b/packages/svelte/tests/css/samples/nesting-selectors/expected.css @@ -0,0 +1,37 @@ + + nav.svelte-xyz { + header:where(.svelte-xyz):has(&){ + color: green; + } + /* (unused) .unused:has(&){ + color: red; + }*/ + } + + header.svelte-xyz { + > nav:where(.svelte-xyz) { + color: green; + + &.active { + color: green; + } + + /* (unused) &.unused { + color: red; + }*/ + } + } + + header.svelte-xyz { + & > nav:where(.svelte-xyz) { + color: green; + + &.active { + color: green; + } + + /* (unused) &.unused { + color: red; + }*/ + } + } diff --git a/packages/svelte/tests/css/samples/nesting-selectors/expected.html b/packages/svelte/tests/css/samples/nesting-selectors/expected.html new file mode 100644 index 000000000000..59e17c2d028a --- /dev/null +++ b/packages/svelte/tests/css/samples/nesting-selectors/expected.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nesting-selectors/input.svelte b/packages/svelte/tests/css/samples/nesting-selectors/input.svelte new file mode 100644 index 000000000000..98881237c019 --- /dev/null +++ b/packages/svelte/tests/css/samples/nesting-selectors/input.svelte @@ -0,0 +1,42 @@ +
+ +
+ + \ No newline at end of file