Skip to content

Commit 3d49277

Browse files
committed
fix: take snippets into account when scoping CSS
fixes #10143
1 parent 6534f50 commit 3d49277

File tree

6 files changed

+106
-2
lines changed

6 files changed

+106
-2
lines changed

.changeset/green-cameras-bake.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

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

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,10 @@ function truncate(node) {
136136
* @param {Compiler.Css.Rule} rule
137137
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
138138
* @param {Compiler.Css.StyleSheet} stylesheet
139+
* @param {boolean} [contains_render_tag]
139140
* @returns {boolean}
140141
*/
141-
function apply_selector(relative_selectors, rule, element, stylesheet) {
142+
function apply_selector(relative_selectors, rule, element, stylesheet, contains_render_tag) {
142143
const parent_selectors = relative_selectors.slice();
143144
const relative_selector = parent_selectors.pop();
144145

@@ -152,7 +153,19 @@ function apply_selector(relative_selectors, rule, element, stylesheet) {
152153
);
153154

154155
if (!possible_match) {
155-
return false;
156+
contains_render_tag ??= element.fragment.nodes.some((node) => node.type === 'RenderTag');
157+
if (contains_render_tag) {
158+
// If the element contains a render tag then we assume the selector might match something inside the rendered snippet
159+
// and traverse the blocks upwards to see if the present blocks match our node further upwards.
160+
// (We could do more static analysis and check the render tag reference to see if this snippet block continues
161+
// with elements that actually match the selector, but that would be a lot of work for little gain)
162+
const possible = apply_selector(parent_selectors, rule, element, stylesheet, true);
163+
if (possible) return true; // e.g `div span` matched for element `<div>{@render tag()}</div>`
164+
// Continue checking if a parent element might match the selector.
165+
// Example: Selector is `p span`, which matches `<p><strong>{@render tag()}</strong></p>` and we're currently at `strong`
166+
} else {
167+
return false;
168+
}
156169
}
157170

158171
if (relative_selector.combinator) {
@@ -171,6 +184,13 @@ function apply_selector(relative_selectors, rule, element, stylesheet) {
171184
crossed_component_boundary = true;
172185
}
173186

187+
if (parent.type === 'SnippetBlock') {
188+
// We assume the snippet might be rendered in a place where the parent selectors match.
189+
// (We could do more static analysis and check the render tag reference to see if this snippet block continues
190+
// with elements that actually match the selector, but that would be a lot of work for little gain)
191+
return true;
192+
}
193+
174194
if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') {
175195
if (apply_selector(parent_selectors, rule, parent, stylesheet)) {
176196
// TODO the `name === ' '` causes false positives, but removing it causes false negatives...
@@ -222,6 +242,10 @@ function apply_selector(relative_selectors, rule, element, stylesheet) {
222242
}
223243
}
224244

245+
// We got to the end of this under the assumption higher up might start matching,
246+
// but turns out it didn't - therefore the selector doesn't apply after all.
247+
if (contains_render_tag) return false;
248+
225249
// if this is the left-most non-global selector, mark it — we want
226250
// `x y z {...}` to become `x.blah y z.blah {...}`
227251
const parent = parent_selectors[parent_selectors.length - 1];
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
warnings: [
5+
{
6+
code: 'css_unused_selector',
7+
message: 'Unused CSS selector "span div"',
8+
start: {
9+
line: 31,
10+
column: 1,
11+
character: 461
12+
},
13+
end: {
14+
line: 31,
15+
column: 9,
16+
character: 469
17+
}
18+
}
19+
]
20+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
2+
div > span.svelte-xyz {
3+
color: green;
4+
}
5+
div span.svelte-xyz {
6+
color: green;
7+
}
8+
div.svelte-xyz span {
9+
color: green;
10+
}
11+
p.svelte-xyz span:where(.svelte-xyz) {
12+
color: green;
13+
}
14+
p.svelte-xyz .foo:where(.svelte-xyz) {
15+
color: purple; /* doesn't match, but our static analysis doesn't handle this currently */
16+
}
17+
/* (unused) span div {
18+
color: red;
19+
}*/
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 class="svelte-xyz"><span class="svelte-xyz">Hello world</span></strong></p>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
color: green;
18+
}
19+
div span {
20+
color: green;
21+
}
22+
div :global(span) {
23+
color: green;
24+
}
25+
p span {
26+
color: green;
27+
}
28+
p .foo {
29+
color: purple; /* doesn't match, but our static analysis doesn't handle this currently */
30+
}
31+
span div {
32+
color: red;
33+
}
34+
</style>

0 commit comments

Comments
 (0)