Skip to content

Commit 96f432d

Browse files
committed
fix: take snippets into account when scoping css selectors
fixes #10143
1 parent c7a7725 commit 96f432d

File tree

5 files changed

+98
-10
lines changed

5 files changed

+98
-10
lines changed

.changeset/four-actors-grow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
fix: take snippets into account when scoping css selectors

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

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export default class Selector {
5858
apply(node) {
5959
/** @type {Array<{ node: import('#compiler').RegularElement | import('#compiler').SvelteElement; block: Block }>} */
6060
const to_encapsulate = [];
61-
apply_selector(this.local_blocks.slice(), node, to_encapsulate);
61+
apply_selector(this.local_blocks, node, to_encapsulate);
6262
if (to_encapsulate.length > 0) {
6363
to_encapsulate.forEach(({ node, block }) => {
6464
this.stylesheet.nodes_with_css_class.add(node);
@@ -203,20 +203,36 @@ export default class Selector {
203203
* @param {Block[]} blocks
204204
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} node
205205
* @param {Array<{ node: import('#compiler').RegularElement | import('#compiler').SvelteElement; block: Block }>} to_encapsulate
206+
* @param {boolean} [has_render_tag]
206207
* @returns {boolean}
207208
*/
208-
function apply_selector(blocks, node, to_encapsulate) {
209+
function apply_selector(
210+
blocks,
211+
node,
212+
to_encapsulate,
213+
has_render_tag = node?.fragment.nodes.some((n) => n.type === 'RenderTag')
214+
) {
215+
blocks = blocks.slice();
209216
const block = blocks.pop();
210217
if (!block) return false;
211218
if (!node) {
212219
return (
213220
(block.global && blocks.every((block) => block.global)) || (block.host && blocks.length === 0)
214221
);
215222
}
216-
const applies = block_might_apply_to_node(block, node);
223+
224+
let applies = block_might_apply_to_node(block, node);
217225

218226
if (applies === NO_MATCH) {
219-
return false;
227+
if (has_render_tag) {
228+
// If the element contains a render tag then we assume the selector might match something inside the rendered snippet
229+
// and traverse the blocks upwards to see if the present blocks match our node further upwards.
230+
// We could do more static analysis and check the render tag reference to see if this snippet block continues
231+
// with elements that actually match the selector, but that would be a lot of work for little gain
232+
return apply_selector(blocks, node, to_encapsulate, true);
233+
} else {
234+
return false;
235+
}
220236
}
221237

222238
if (applies === UNKNOWN_SELECTOR) {
@@ -225,7 +241,7 @@ function apply_selector(blocks, node, to_encapsulate) {
225241
}
226242

227243
if (block.combinator) {
228-
if (block.combinator.type === 'Combinator' && block.combinator.name === ' ') {
244+
if (block.combinator.name === ' ') {
229245
for (const ancestor_block of blocks) {
230246
if (ancestor_block.global) {
231247
continue;
@@ -234,7 +250,7 @@ function apply_selector(blocks, node, to_encapsulate) {
234250
to_encapsulate.push({ node, block });
235251
return true;
236252
}
237-
/** @type {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} */
253+
/** @type {ReturnType<typeof get_element_parent>} */
238254
let parent = node;
239255
while ((parent = get_element_parent(parent))) {
240256
if (block_might_apply_to_node(ancestor_block, parent) !== NO_MATCH) {
@@ -250,10 +266,27 @@ function apply_selector(blocks, node, to_encapsulate) {
250266
to_encapsulate.push({ node, block });
251267
return true;
252268
}
269+
// The inverse of the render tag logic above: mark the node as encapsulated if it's inside a snippet block.
270+
// May result in false positives just like the render tag logic for the same reasons.
271+
// TODO try to get rid of .parent in favor of path in the long run
272+
if (node.parent?.type === 'SnippetBlock') {
273+
to_encapsulate.push({ node, block });
274+
return true;
275+
}
253276
return false;
254277
} else if (block.combinator.name === '>') {
255278
const has_global_parent = blocks.every((block) => block.global);
256-
if (has_global_parent || apply_selector(blocks, get_element_parent(node), to_encapsulate)) {
279+
if (
280+
has_global_parent ||
281+
apply_selector(blocks, get_element_parent(node), to_encapsulate, has_render_tag)
282+
) {
283+
to_encapsulate.push({ node, block });
284+
return true;
285+
}
286+
// The inverse of the render tag logic above: mark the node as encapsulated if it's inside a snippet block.
287+
// May result in false positives just like the render tag logic for the same reasons.
288+
// TODO try to get rid of .parent in favor of path in the long run
289+
if (node.parent?.type === 'SnippetBlock') {
257290
to_encapsulate.push({ node, block });
258291
return true;
259292
}
@@ -273,7 +306,7 @@ function apply_selector(blocks, node, to_encapsulate) {
273306
return true;
274307
}
275308
for (const possible_sibling of siblings.keys()) {
276-
if (apply_selector(blocks.slice(), possible_sibling, to_encapsulate)) {
309+
if (apply_selector(blocks, possible_sibling, to_encapsulate, has_render_tag)) {
277310
to_encapsulate.push({ node, block });
278311
has_match = true;
279312
}
@@ -514,9 +547,10 @@ function get_element_parent(node) {
514547
// @ts-expect-error TODO figure out a more elegant solution
515548
(parent = parent.parent) &&
516549
parent.type !== 'RegularElement' &&
517-
parent.type !== 'SvelteElement'
550+
parent.type !== 'SvelteElement' &&
551+
parent.type !== 'SnippetBlock'
518552
);
519-
return parent ?? null;
553+
return parent?.type !== 'SnippetBlock' ? parent ?? null : null;
520554
}
521555

522556
/**
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
div.svelte-xyz > span.svelte-xyz {
3+
background-color: red;
4+
}
5+
6+
div.svelte-xyz span.svelte-xyz {
7+
letter-spacing: 10px;
8+
}
9+
10+
div.svelte-xyz span {
11+
text-decoration: underline;
12+
}
13+
14+
p.svelte-xyz span.svelte-xyz.svelte-xyz {
15+
background: black;
16+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<div class="svelte-xyz"><span class="svelte-xyz">Hello world</span></div>
2+
<p class="svelte-xyz"><strong><span class="svelte-xyz">Hello world</span></strong></p>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{#snippet my_snippet()}
2+
<span>Hello world</span>
3+
{/snippet}
4+
5+
<div>{@render my_snippet()}</div>
6+
7+
<p>
8+
{#snippet my_snippet()}
9+
<span>Hello world</span>
10+
{/snippet}
11+
12+
<strong>{@render my_snippet()}</strong>
13+
</p>
14+
15+
<style>
16+
div > span {
17+
background-color: red;
18+
}
19+
20+
div span {
21+
letter-spacing: 10px;
22+
}
23+
24+
div :global(span) {
25+
text-decoration: underline;
26+
}
27+
28+
p span {
29+
background: black;
30+
}
31+
</style>

0 commit comments

Comments
 (0)