Skip to content

Commit 829be3d

Browse files
authored
fix: take snippets into account when scoping CSS (#13589)
fixes #10143
1 parent 6f03561 commit 829be3d

File tree

9 files changed

+135
-5
lines changed

9 files changed

+135
-5
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: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js';
99
* @typedef {{
1010
* stylesheet: Compiler.Css.StyleSheet;
1111
* element: Compiler.AST.RegularElement | Compiler.AST.SvelteElement;
12+
* from_render_tag: boolean;
1213
* }} State
1314
*/
1415
/** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */
@@ -53,10 +54,17 @@ const nesting_selector = {
5354
/**
5455
*
5556
* @param {Compiler.Css.StyleSheet} stylesheet
56-
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
57+
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag} element
5758
*/
5859
export function prune(stylesheet, element) {
59-
walk(stylesheet, { stylesheet, element }, visitors);
60+
if (element.type === 'RenderTag') {
61+
const parent = get_element_parent(element);
62+
if (!parent) return;
63+
64+
walk(stylesheet, { stylesheet, element: parent, from_render_tag: true }, visitors);
65+
} else {
66+
walk(stylesheet, { stylesheet, element, from_render_tag: false }, visitors);
67+
}
6068
}
6169

6270
/** @type {Visitors<Compiler.Css.Node, State>} */
@@ -101,7 +109,37 @@ const visitors = {
101109
}
102110
}
103111

104-
if (
112+
if (context.state.from_render_tag) {
113+
// We're searching for a match that crosses a render tag boundary. That means we have to both traverse up
114+
// the element tree (to see if we find an entry point) but also remove selectors from the end (assuming
115+
// they are part of the render tag we don't see). We do all possible combinations of both until we find a match.
116+
/** @type {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | null} */
117+
let element = context.state.element;
118+
119+
while (element) {
120+
const selectors_to_check = selectors.slice();
121+
122+
while (selectors_to_check.length > 0) {
123+
selectors_to_check.pop();
124+
125+
if (
126+
apply_selector(
127+
selectors_to_check,
128+
/** @type {Compiler.Css.Rule} */ (node.metadata.rule),
129+
element,
130+
context.state.stylesheet,
131+
true
132+
)
133+
) {
134+
mark(inner, element);
135+
node.metadata.used = true;
136+
return;
137+
}
138+
}
139+
140+
element = get_element_parent(element);
141+
}
142+
} else if (
105143
apply_selector(
106144
selectors,
107145
/** @type {Compiler.Css.Rule} */ (node.metadata.rule),
@@ -230,6 +268,13 @@ function apply_combinator(
230268
crossed_component_boundary = true;
231269
}
232270

271+
if (parent.type === 'SnippetBlock') {
272+
// We assume the snippet might be rendered in a place where the parent selectors match.
273+
// (We could do more static analysis and check the render tag reference to see if this snippet block continues
274+
// with elements that actually match the selector, but that would be a lot of work for little gain)
275+
return true;
276+
}
277+
233278
if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') {
234279
if (apply_selector(parent_selectors, rule, parent, stylesheet, check_has)) {
235280
// TODO the `name === ' '` causes false positives, but removing it causes false negatives...
@@ -724,7 +769,7 @@ function unquote(str) {
724769
}
725770

726771
/**
727-
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} node
772+
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag} node
728773
* @returns {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | null}
729774
*/
730775
function get_element_parent(node) {

packages/svelte/src/compiler/phases/2-analyze/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,8 @@ export function analyze_component(root, source, options) {
691691
}
692692

693693
outer: for (const element of analysis.elements) {
694+
if (element.type === 'RenderTag') continue;
695+
694696
if (element.metadata.scoped) {
695697
// Dynamic elements in dom mode always use spread for attributes and therefore shouldn't have a class attribute added to them
696698
// TODO this happens during the analysis phase, which shouldn't know anything about client vs server

packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { mark_subtree_dynamic } from './shared/fragment.js';
1212
export function RenderTag(node, context) {
1313
validate_opening_tag(node, context.state, '@');
1414

15+
context.state.analysis.elements.push(node);
16+
1517
const callee = unwrap_optional(node.expression).callee;
1618

1719
node.metadata.dynamic =

packages/svelte/src/compiler/phases/types.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ export interface ComponentAnalysis extends Analysis {
3636
root: ScopeRoot;
3737
instance: Js;
3838
template: Template;
39-
elements: Array<AST.RegularElement | AST.SvelteElement>;
39+
/** Used for CSS pruning and scoping */
40+
elements: Array<AST.RegularElement | AST.SvelteElement | AST.RenderTag>;
4041
runes: boolean;
4142
exports: Array<{ name: string; alias: string | null }>;
4243
/** Whether the component uses `$$props` */
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.svelte-xyz > span:where(.svelte-xyz) {
3+
color: green;
4+
}
5+
div.svelte-xyz span:where(.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><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)