Skip to content

Commit 8e31c4e

Browse files
committed
fix: better sibling selector handling
Keep sibling selectors when dealing with slots/render tags/`svelte:element` tags fixes #9274
1 parent 8578857 commit 8e31c4e

File tree

11 files changed

+199
-27
lines changed

11 files changed

+199
-27
lines changed

.changeset/giant-plants-grin.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
fix: keep sibling selectors when dealing with slots/render tags/`svelte:element` tags

packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js

+43-27
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,13 @@ function apply_selector(relative_selectors, rule, element, stylesheet) {
175175
let sibling_matched = false;
176176

177177
for (const possible_sibling of siblings.keys()) {
178-
if (apply_selector(parent_selectors, rule, possible_sibling, stylesheet)) {
178+
if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') {
179+
// `{@render foo()}<p>foo</p>` with `:global(.x) + p` is a match
180+
if (parent_selectors.length === 1 && parent_selectors[0].metadata.is_global) {
181+
mark(relative_selector, element);
182+
sibling_matched = true;
183+
}
184+
} else if (apply_selector(parent_selectors, rule, possible_sibling, stylesheet)) {
179185
mark(relative_selector, element);
180186
sibling_matched = true;
181187
}
@@ -564,38 +570,39 @@ function get_element_parent(node) {
564570
function find_previous_sibling(node) {
565571
/** @type {import('#compiler').SvelteNode} */
566572
let current_node = node;
567-
do {
568-
if (current_node.type === 'SlotElement') {
569-
const slot_children = current_node.fragment.nodes;
570-
if (slot_children.length > 0) {
571-
current_node = slot_children.slice(-1)[0]; // go to its last child first
572-
continue;
573-
}
574-
}
575-
while (
576-
// @ts-expect-error TODO
577-
!current_node.prev &&
578-
// @ts-expect-error TODO
579-
current_node.parent &&
580-
// @ts-expect-error TODO
581-
current_node.parent.type === 'SlotElement'
582-
) {
583-
// @ts-expect-error TODO
584-
current_node = current_node.parent;
573+
574+
while (
575+
// @ts-expect-error TODO
576+
!current_node.prev &&
577+
// @ts-expect-error TODO
578+
current_node.parent?.type === 'SlotElement'
579+
) {
580+
// @ts-expect-error TODO
581+
current_node = current_node.parent;
582+
}
583+
584+
// @ts-expect-error
585+
current_node = current_node.prev;
586+
587+
while (current_node?.type === 'SlotElement') {
588+
const slot_children = current_node.fragment.nodes;
589+
if (slot_children.length > 0) {
590+
current_node = slot_children.slice(-1)[0];
591+
} else {
592+
break;
585593
}
586-
// @ts-expect-error
587-
current_node = current_node.prev;
588-
} while (current_node && current_node.type === 'SlotElement');
594+
}
595+
589596
return current_node;
590597
}
591598

592599
/**
593600
* @param {import('#compiler').SvelteNode} node
594601
* @param {boolean} adjacent_only
595-
* @returns {Map<import('#compiler').RegularElement, NodeExistsValue>}
602+
* @returns {Map<import('#compiler').RegularElement | import('#compiler').SvelteElement | import('#compiler').SlotElement | import('#compiler').RenderTag, NodeExistsValue>}
596603
*/
597604
function get_possible_element_siblings(node, adjacent_only) {
598-
/** @type {Map<import('#compiler').RegularElement, NodeExistsValue>} */
605+
/** @type {Map<import('#compiler').RegularElement | import('#compiler').SvelteElement | import('#compiler').SlotElement | import('#compiler').RenderTag, NodeExistsValue>} */
599606
const result = new Map();
600607

601608
/** @type {import('#compiler').SvelteNode} */
@@ -618,6 +625,14 @@ function get_possible_element_siblings(node, adjacent_only) {
618625
if (adjacent_only && has_definite_elements(possible_last_child)) {
619626
return result;
620627
}
628+
} else if (
629+
prev.type === 'SlotElement' ||
630+
prev.type === 'RenderTag' ||
631+
prev.type === 'SvelteElement'
632+
) {
633+
result.set(prev, NODE_PROBABLY_EXISTS);
634+
// Special case: slots, render tags and svelte:element tags could resolve to no siblings,
635+
// so we want to continue until we find a definite sibling even with the adjacent-only combinator
621636
}
622637
}
623638

@@ -720,7 +735,7 @@ function get_possible_last_child(relative_selector, adjacent_only) {
720735
}
721736

722737
/**
723-
* @param {Map<import('#compiler').RegularElement, NodeExistsValue>} result
738+
* @param {Map<unknown, NodeExistsValue>} result
724739
* @returns {boolean}
725740
*/
726741
function has_definite_elements(result) {
@@ -734,8 +749,9 @@ function has_definite_elements(result) {
734749
}
735750

736751
/**
737-
* @param {Map<import('#compiler').RegularElement, NodeExistsValue>} from
738-
* @param {Map<import('#compiler').RegularElement, NodeExistsValue>} to
752+
* @template T
753+
* @param {Map<T, NodeExistsValue>} from
754+
* @param {Map<T, NodeExistsValue>} to
739755
* @returns {void}
740756
*/
741757
function add_to_map(from, to) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
warnings: [
5+
// TODO
6+
// {
7+
// code: 'css-unused-selector',
8+
// message: 'Unused CSS selector ".a ~ .b"',
9+
// start: { character: 111, column: 1, line: 10 },
10+
// end: { character: 118, column: 8, line: 10 }
11+
// },
12+
]
13+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
.before.svelte-xyz + .foo:where(.svelte-xyz) { color: green; }
3+
.before.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; }
4+
.before.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; }
5+
6+
.x + .foo.svelte-xyz { color: green; }
7+
.x + .foo.svelte-xyz span:where(.svelte-xyz) { color: green; }
8+
.x ~ .foo.svelte-xyz { color: green; }
9+
.x ~ .foo.svelte-xyz span:where(.svelte-xyz) { color: green; }
10+
.x ~ .bar.svelte-xyz { color: green; }
11+
12+
/* no match */
13+
/* (unused) :global(.x) + .bar { color: green; }*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<div>
2+
<p class="before">before</p>
3+
{@render children()}
4+
<p class="foo">
5+
<span>foo</span>
6+
</p>
7+
<p class="bar">bar</p>
8+
</div>
9+
10+
<style>
11+
.before + .foo { color: green; }
12+
.before ~ .foo { color: green; }
13+
.before ~ .bar { color: green; }
14+
15+
:global(.x) + .foo { color: green; }
16+
:global(.x) + .foo span { color: green; }
17+
:global(.x) ~ .foo { color: green; }
18+
:global(.x) ~ .foo span { color: green; }
19+
:global(.x) ~ .bar { color: green; }
20+
21+
/* no match */
22+
:global(.x) + .bar { color: green; }
23+
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
warnings: [
5+
// TODO
6+
// {
7+
// code: 'css-unused-selector',
8+
// message: 'Unused CSS selector ".a ~ .b"',
9+
// start: { character: 111, column: 1, line: 10 },
10+
// end: { character: 118, column: 8, line: 10 }
11+
// },
12+
]
13+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
.before.svelte-xyz + .foo:where(.svelte-xyz) { color: green; }
3+
.before.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; }
4+
.before.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; }
5+
6+
.x + .foo.svelte-xyz { color: green; }
7+
.x + .foo.svelte-xyz span:where(.svelte-xyz) { color: green; }
8+
.x ~ .foo.svelte-xyz { color: green; }
9+
.x ~ .foo.svelte-xyz span:where(.svelte-xyz) { color: green; }
10+
.x ~ .bar.svelte-xyz { color: green; }
11+
12+
/* no match */
13+
/* (unused) :global(.x) + .bar { color: green; }*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<div>
2+
<p class="before">before</p>
3+
<slot></slot>
4+
<p class="foo">
5+
<span>foo</span>
6+
</p>
7+
<p class="bar">bar</p>
8+
</div>
9+
10+
<style>
11+
.before + .foo { color: green; }
12+
.before ~ .foo { color: green; }
13+
.before ~ .bar { color: green; }
14+
15+
:global(.x) + .foo { color: green; }
16+
:global(.x) + .foo span { color: green; }
17+
:global(.x) ~ .foo { color: green; }
18+
:global(.x) ~ .foo span { color: green; }
19+
:global(.x) ~ .bar { color: green; }
20+
21+
/* no match */
22+
:global(.x) + .bar { color: green; }
23+
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
warnings: [
5+
// TODO
6+
// {
7+
// code: 'css-unused-selector',
8+
// message: 'Unused CSS selector ".a ~ .b"',
9+
// start: { character: 111, column: 1, line: 10 },
10+
// end: { character: 118, column: 8, line: 10 }
11+
// },
12+
]
13+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
.before.svelte-xyz + .foo:where(.svelte-xyz) { color: green; }
3+
.before.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; }
4+
.before.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; }
5+
6+
.x.svelte-xyz + .foo:where(.svelte-xyz) { color: green; }
7+
.x.svelte-xyz + .foo:where(.svelte-xyz) span:where(.svelte-xyz) { color: green; }
8+
.x.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; }
9+
.x.svelte-xyz ~ .foo:where(.svelte-xyz) span:where(.svelte-xyz) { color: green; }
10+
.x.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; }
11+
12+
/* no match */
13+
/* (unused) .x + .bar { color: green; }*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<script>
2+
let tag = 'div'
3+
</script>
4+
5+
<div>
6+
<p class="before">before</p>
7+
<svelte:element class="x" this={tag}></svelte:element>
8+
<p class="foo">
9+
<span>foo</span>
10+
</p>
11+
<p class="bar">bar</p>
12+
</div>
13+
14+
<style>
15+
.before + .foo { color: green; }
16+
.before ~ .foo { color: green; }
17+
.before ~ .bar { color: green; }
18+
19+
.x + .foo { color: green; }
20+
.x + .foo span { color: green; }
21+
.x ~ .foo { color: green; }
22+
.x ~ .foo span { color: green; }
23+
.x ~ .bar { color: green; }
24+
25+
/* no match */
26+
.x + .bar { color: green; }
27+
</style>

0 commit comments

Comments
 (0)