Skip to content

Commit 5c8561a

Browse files
committed
handle :root with nesting
1 parent 85a37de commit 5c8561a

File tree

4 files changed

+101
-17
lines changed

4 files changed

+101
-17
lines changed

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

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js';
1010
* stylesheet: Compiler.Css.StyleSheet;
1111
* element: Compiler.AST.RegularElement | Compiler.AST.SvelteElement;
1212
* from_render_tag: boolean;
13+
* in_root_selector: boolean;
1314
* }} State
1415
*/
1516
/** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */
@@ -61,9 +62,17 @@ export function prune(stylesheet, element) {
6162
const parent = get_element_parent(element);
6263
if (!parent) return;
6364

64-
walk(stylesheet, { stylesheet, element: parent, from_render_tag: true }, visitors);
65+
walk(
66+
stylesheet,
67+
{ stylesheet, element: parent, from_render_tag: true, in_root_selector: false },
68+
visitors
69+
);
6570
} else {
66-
walk(stylesheet, { stylesheet, element, from_render_tag: false }, visitors);
71+
walk(
72+
stylesheet,
73+
{ stylesheet, element, from_render_tag: false, in_root_selector: false },
74+
visitors
75+
);
6776
}
6877
}
6978

@@ -79,6 +88,16 @@ const visitors = {
7988
ComplexSelector(node, context) {
8089
const selectors = get_relative_selectors(node);
8190
const inner = selectors[selectors.length - 1];
91+
/** @type {State} */
92+
const state = {
93+
...context.state,
94+
in_root_selector: [
95+
node,
96+
...get_parent_rules(node.metadata.rule).flatMap((r) => r.prelude.children)
97+
].some((c) =>
98+
c.children[0].selectors.some((s) => s.type === 'PseudoClassSelector' && s.name === 'root')
99+
)
100+
};
82101

83102
if (context.state.from_render_tag) {
84103
// We're searching for a match that crosses a render tag boundary. That means we have to both traverse up
@@ -98,7 +117,7 @@ const visitors = {
98117
selectors_to_check,
99118
/** @type {Compiler.Css.Rule} */ (node.metadata.rule),
100119
element,
101-
context.state
120+
state
102121
)
103122
) {
104123
mark(inner, element);
@@ -114,7 +133,7 @@ const visitors = {
114133
selectors,
115134
/** @type {Compiler.Css.Rule} */ (node.metadata.rule),
116135
context.state.element,
117-
context.state
136+
state
118137
)
119138
) {
120139
mark(inner, context.state.element);
@@ -127,6 +146,21 @@ const visitors = {
127146
}
128147
};
129148

149+
/**
150+
* @param {Compiler.Css.Rule | null} rule
151+
*/
152+
function get_parent_rules(rule) {
153+
const parents = [];
154+
155+
let parent = rule?.metadata.parent_rule;
156+
while (parent) {
157+
parents.push(parent);
158+
parent = parent.metadata.parent_rule;
159+
}
160+
161+
return parents;
162+
}
163+
130164
/**
131165
* Retrieves the relative selectors (minus the trailing globals) from a complex selector.
132166
* Also searches them for any existing `&` selectors and adds one if none are found.
@@ -198,15 +232,13 @@ function truncate(node) {
198232

199233
return node.children.slice(0, i + 1).map((child) => {
200234
// In case of `:root.y:has(...)`, `y` is unscoped, but everything in `:has(...)` should be scoped (if not global).
201-
// To properly accomplish that, we gotta filter out all selector types except `:has` and `:root`.
235+
// To properly accomplish that, we gotta filter out all selector types `:has`.
202236
const root = child.selectors.find((s) => s.type === 'PseudoClassSelector' && s.name === 'root');
203237
if (!root || child.metadata.is_global_like) return child;
204238

205239
return {
206240
...child,
207-
selectors: child.selectors.filter(
208-
(s) => s.type === 'PseudoClassSelector' && (s.name === 'has' || s.name === 'root')
209-
)
241+
selectors: child.selectors.filter((s) => s.type === 'PseudoClassSelector' && s.name === 'has')
210242
};
211243
});
212244
}
@@ -428,11 +460,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
428460
let sibling_elements; // do them lazy because it's rarely used and expensive to calculate
429461

430462
// If this is a :has on a :root, we gotta include the element itself, too, because everything's a descendant of :root
431-
const is_root_selector = other_selectors.some(
432-
(s) => s.type === 'PseudoClassSelector' && s.name === 'root'
433-
);
434-
435-
if (is_root_selector) {
463+
if (state.in_root_selector) {
436464
child_elements.push(element);
437465
descendant_elements.push(element);
438466
}
@@ -482,7 +510,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
482510

483511
const descendants =
484512
left_most_combinator.name === '+' || left_most_combinator.name === '~'
485-
? (sibling_elements ??= get_following_sibling_elements(element, is_root_selector))
513+
? (sibling_elements ??= get_following_sibling_elements(element, state.in_root_selector))
486514
: left_most_combinator.name === '>'
487515
? child_elements
488516
: descendant_elements;
@@ -529,9 +557,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
529557

530558
switch (selector.type) {
531559
case 'PseudoClassSelector': {
532-
if (name === 'host') return false;
533-
534-
if (name === 'root') break;
560+
if (name === 'host' || name === 'root') return false;
535561

536562
if (
537563
name === 'global' &&

packages/svelte/tests/css/samples/root/_config.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,34 @@ export default test({
2929
column: 20,
3030
character: 287
3131
}
32+
},
33+
{
34+
code: 'css_unused_selector',
35+
message: 'Unused CSS selector ".unused"',
36+
start: {
37+
line: 37,
38+
column: 4,
39+
character: 401
40+
},
41+
end: {
42+
line: 37,
43+
column: 11,
44+
character: 408
45+
}
46+
},
47+
{
48+
code: 'css_unused_selector',
49+
message: 'Unused CSS selector ":has(.unused)"',
50+
start: {
51+
line: 43,
52+
column: 4,
53+
character: 480
54+
},
55+
end: {
56+
line: 43,
57+
column: 17,
58+
character: 493
59+
}
3260
}
3361
]
3462
});

packages/svelte/tests/css/samples/root/expected.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,18 @@
2929
:root:not(.x) {
3030
color: green;
3131
}
32+
33+
:root {
34+
h1.svelte-xyz {
35+
color: green;
36+
}
37+
/* (unused) .unused {
38+
color: red;
39+
}*/
40+
.svelte-xyz:has(h1:where(.svelte-xyz)) {
41+
color: green;
42+
}
43+
/* (unused) :has(.unused) {
44+
color: red;
45+
}*/
46+
}

packages/svelte/tests/css/samples/root/input.svelte

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,21 @@
2929
:root:not(.x) {
3030
color: green;
3131
}
32+
33+
:root {
34+
h1 {
35+
color: green;
36+
}
37+
.unused {
38+
color: red;
39+
}
40+
:has(h1) {
41+
color: green;
42+
}
43+
:has(.unused) {
44+
color: red;
45+
}
46+
}
3247
</style>
3348

3449
<h1>Hello!</h1>

0 commit comments

Comments
 (0)