Skip to content

Commit 301ea42

Browse files
dummdidummtrueadm
authored andcommitted
fix: remove scoping for most :not selectors (#14177)
fixes #14168 This reverts the whole "selectors inside `:not` are scoped" logic. Scoping is done so that styles don't bleed. But within `:not`,everything is reversed, which means scoping the selectors now means they are more likely to bleed. That is the opposite of what we want to achieve, therefore we should just leave those selectors alone. The exception are `:not` selectors with descendant selectors, as that means "look up the tree" and we need to scope all ancestor elements in that case.
1 parent 1098bb7 commit 301ea42

File tree

18 files changed

+172
-171
lines changed

18 files changed

+172
-171
lines changed

.changeset/quick-eels-occur.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: remove scoping for `:not` selectors

packages/svelte/src/compiler/migrate/index.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ function migrate_css(state) {
5050
while (code) {
5151
if (
5252
code.startsWith(':has') ||
53-
code.startsWith(':not') ||
5453
code.startsWith(':is') ||
55-
code.startsWith(':where')
54+
code.startsWith(':where') ||
55+
code.startsWith(':not')
5656
) {
5757
let start = code.indexOf('(') + 1;
5858
let is_global = false;
@@ -74,7 +74,7 @@ function migrate_css(state) {
7474
char = code[end];
7575
}
7676
if (start && end) {
77-
if (!is_global) {
77+
if (!is_global && !code.startsWith(':not')) {
7878
str.prependLeft(starting + start, ':global(');
7979
str.appendRight(starting + end - 1, ')');
8080
}

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

Lines changed: 38 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ 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-
* inside_not: boolean;
1413
* }} State
1514
*/
1615
/** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */
@@ -62,13 +61,9 @@ export function prune(stylesheet, element) {
6261
const parent = get_element_parent(element);
6362
if (!parent) return;
6463

65-
walk(
66-
stylesheet,
67-
{ stylesheet, element: parent, from_render_tag: true, inside_not: false },
68-
visitors
69-
);
64+
walk(stylesheet, { stylesheet, element: parent, from_render_tag: true }, visitors);
7065
} else {
71-
walk(stylesheet, { stylesheet, element, from_render_tag: false, inside_not: false }, visitors);
66+
walk(stylesheet, { stylesheet, element, from_render_tag: false }, visitors);
7267
}
7368
}
7469

@@ -179,9 +174,9 @@ function truncate(node) {
179174
selector.type === 'PseudoClassSelector' &&
180175
selector.args !== null &&
181176
(selector.name === 'has' ||
182-
selector.name === 'not' ||
183177
selector.name === 'is' ||
184-
selector.name === 'where')
178+
selector.name === 'where' ||
179+
selector.name === 'not')
185180
))
186181
);
187182
});
@@ -227,7 +222,7 @@ function apply_selector(relative_selectors, rule, element, state) {
227222
// if this is the left-most non-global selector, mark it — we want
228223
// `x y z {...}` to become `x.blah y z.blah {...}`
229224
const parent = parent_selectors[parent_selectors.length - 1];
230-
if (!state.inside_not && (!parent || is_global(parent, rule))) {
225+
if (!parent || is_global(parent, rule)) {
231226
mark(relative_selector, element);
232227
}
233228

@@ -269,7 +264,7 @@ function apply_combinator(combinator, relative_selector, parent_selectors, rule,
269264
if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') {
270265
if (apply_selector(parent_selectors, rule, parent, state)) {
271266
// TODO the `name === ' '` causes false positives, but removing it causes false negatives...
272-
if (!state.inside_not && (name === ' ' || crossed_component_boundary)) {
267+
if (name === ' ' || crossed_component_boundary) {
273268
mark(parent_selectors[parent_selectors.length - 1], parent);
274269
}
275270

@@ -295,15 +290,11 @@ function apply_combinator(combinator, relative_selector, parent_selectors, rule,
295290
if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') {
296291
// `{@render foo()}<p>foo</p>` with `:global(.x) + p` is a match
297292
if (parent_selectors.length === 1 && parent_selectors[0].metadata.is_global) {
298-
if (!state.inside_not) {
299-
mark(relative_selector, element);
300-
}
293+
mark(relative_selector, element);
301294
sibling_matched = true;
302295
}
303296
} else if (apply_selector(parent_selectors, rule, possible_sibling, state)) {
304-
if (!state.inside_not) {
305-
mark(relative_selector, element);
306-
}
297+
mark(relative_selector, element);
307298
sibling_matched = true;
308299
}
309300
}
@@ -512,7 +503,35 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
512503
// We came across a :global, everything beyond it is global and therefore a potential match
513504
if (name === 'global' && selector.args === null) return true;
514505

515-
if ((name === 'is' || name === 'where' || name === 'not') && selector.args) {
506+
// :not(...) contents should stay unscoped. Scoping them would achieve the opposite of what we want,
507+
// because they are then _more_ likely to bleed out of the component. The exception is complex selectors
508+
// with descendants, in which case we scope them all.
509+
if (name === 'not' && selector.args) {
510+
for (const complex_selector of selector.args.children) {
511+
complex_selector.metadata.used = true;
512+
const relative = truncate(complex_selector);
513+
514+
if (complex_selector.children.length > 1) {
515+
// foo:not(bar foo) means that bar is an ancestor of foo (side note: ending with foo is the only way the selector make sense).
516+
// We can't fully check if that actually matches with our current algorithm, so we just assume it does.
517+
// The result may not match a real element, so the only drawback is the missing prune.
518+
for (const selector of relative) {
519+
selector.metadata.scoped = true;
520+
}
521+
522+
/** @type {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | null} */
523+
let el = element;
524+
while (el) {
525+
el.metadata.scoped = true;
526+
el = get_element_parent(el);
527+
}
528+
}
529+
}
530+
531+
break;
532+
}
533+
534+
if ((name === 'is' || name === 'where') && selector.args) {
516535
let matched = false;
517536

518537
for (const complex_selector of selector.args.children) {
@@ -522,32 +541,9 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
522541
if (is_global) {
523542
complex_selector.metadata.used = true;
524543
matched = true;
525-
} else if (name !== 'not' && apply_selector(relative, rule, element, state)) {
544+
} else if (apply_selector(relative, rule, element, state)) {
526545
complex_selector.metadata.used = true;
527546
matched = true;
528-
} else if (
529-
name === 'not' &&
530-
!apply_selector(relative, rule, element, { ...state, inside_not: true })
531-
) {
532-
// For `:not(...)` we gotta do the inverse: If it did not match, mark the element and possibly
533-
// everything above (if the selector is written is a such) as scoped (because they matched by not matching).
534-
element.metadata.scoped = true;
535-
complex_selector.metadata.used = true;
536-
matched = true;
537-
538-
for (const r of relative) {
539-
r.metadata.scoped = true;
540-
}
541-
542-
// bar:not(foo bar) means that foo is an ancestor of bar
543-
if (complex_selector.children.length > 1) {
544-
/** @type {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | null} */
545-
let el = get_element_parent(element);
546-
while (el) {
547-
el.metadata.scoped = true;
548-
el = get_element_parent(el);
549-
}
550-
}
551547
} else if (complex_selector.children.length > 1 && (name == 'is' || name == 'where')) {
552548
// foo :is(bar baz) can also mean that bar is an ancestor of foo, and baz a descendant.
553549
// We can't fully check if that actually matches with our current algorithm, so we just assume it does.

packages/svelte/src/compiler/phases/3-transform/css/index.js

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -278,29 +278,10 @@ const visitors = {
278278
ComplexSelector(node, context) {
279279
const before_bumped = context.state.specificity.bumped;
280280

281-
/**
282-
* @param {Css.PseudoClassSelector} selector
283-
* @param {Css.Combinator | null} combinator
284-
*/
285-
function remove_global_pseudo_class(selector, combinator) {
286-
if (selector.args === null) {
287-
let start = selector.start;
288-
if (combinator?.name === ' ') {
289-
// div :global.x becomes div.x
290-
while (/\s/.test(context.state.code.original[start - 1])) start--;
291-
}
292-
context.state.code.remove(start, selector.start + ':global'.length);
293-
} else {
294-
context.state.code
295-
.remove(selector.start, selector.start + ':global('.length)
296-
.remove(selector.end - 1, selector.end);
297-
}
298-
}
299-
300281
for (const relative_selector of node.children) {
301282
if (relative_selector.metadata.is_global) {
302283
const global = /** @type {Css.PseudoClassSelector} */ (relative_selector.selectors[0]);
303-
remove_global_pseudo_class(global, relative_selector.combinator);
284+
remove_global_pseudo_class(global, relative_selector.combinator, context.state);
304285

305286
if (
306287
node.metadata.rule?.metadata.parent_rule &&
@@ -328,7 +309,7 @@ const visitors = {
328309
// for any :global() or :global at the middle of compound selector
329310
for (const selector of relative_selector.selectors) {
330311
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
331-
remove_global_pseudo_class(selector, null);
312+
remove_global_pseudo_class(selector, null, context.state);
332313
}
333314
}
334315

@@ -380,6 +361,26 @@ const visitors = {
380361
}
381362
};
382363

364+
/**
365+
* @param {Css.PseudoClassSelector} selector
366+
* @param {Css.Combinator | null} combinator
367+
* @param {State} state
368+
*/
369+
function remove_global_pseudo_class(selector, combinator, state) {
370+
if (selector.args === null) {
371+
let start = selector.start;
372+
if (combinator?.name === ' ') {
373+
// div :global.x becomes div.x
374+
while (/\s/.test(state.code.original[start - 1])) start--;
375+
}
376+
state.code.remove(start, selector.start + ':global'.length);
377+
} else {
378+
state.code
379+
.remove(selector.start, selector.start + ':global('.length)
380+
.remove(selector.end - 1, selector.end);
381+
}
382+
}
383+
383384
/**
384385
* Walk backwards until we find a non-whitespace character
385386
* @param {number} end
Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,5 @@
11
import { test } from '../../test';
22

33
export default test({
4-
warnings: [
5-
{
6-
code: 'css_unused_selector',
7-
message: 'Unused CSS selector ":global(.x) :not(p)"',
8-
start: {
9-
line: 14,
10-
column: 1,
11-
character: 197
12-
},
13-
end: {
14-
line: 14,
15-
column: 20,
16-
character: 216
17-
}
18-
}
19-
]
4+
warnings: []
205
});

packages/svelte/tests/css/samples/not-selector-global/expected.css

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,28 @@
22
.svelte-xyz:not(.foo) {
33
color: green;
44
}
5-
.svelte-xyz:not(.foo:where(.svelte-xyz)):not(.unused) {
5+
.svelte-xyz:not(.foo):not(.unused) {
66
color: green;
77
}
8-
.x:not(.foo.svelte-xyz) {
8+
.x:not(.foo) {
99
color: green;
1010
}
11-
/* (unused) :global(.x) :not(p) {
12-
color: red;
13-
}*/
14-
.x:not(p.svelte-xyz) {
15-
color: red; /* TODO would be nice to prune this one day */
11+
.x .svelte-xyz:not(p) {
12+
color: green;
13+
}
14+
.x:not(p) {
15+
color: green;
16+
}
17+
.x .svelte-xyz:not(.unused) {
18+
color: green;
19+
}
20+
21+
span:not(p.svelte-xyz span:where(.svelte-xyz)) {
22+
color: green;
23+
}
24+
span.svelte-xyz:not(p span) {
25+
color: green;
1626
}
17-
.x .svelte-xyz:not(.unused:where(.svelte-xyz)) {
27+
span:not(p span) {
1828
color: green;
1929
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
<p class="foo svelte-xyz">foo</p> <p class="bar svelte-xyz">bar</p>
1+
<p class="foo svelte-xyz">foo</p>
2+
<p class="bar svelte-xyz">bar <span class="svelte-xyz">baz</span></p>
3+
<span class="svelte-xyz">buzz</span>

packages/svelte/tests/css/samples/not-selector-global/input.svelte

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
<p class="foo">foo</p>
2-
<p class="bar">bar</p>
2+
<p class="bar">
3+
bar
4+
<span>baz</span>
5+
</p>
6+
<span>buzz</span>
37

48
<style>
59
:not(:global(.foo)) {
@@ -12,12 +16,22 @@
1216
color: green;
1317
}
1418
:global(.x) :not(p) {
15-
color: red;
19+
color: green;
1620
}
1721
:global(.x):not(p) {
18-
color: red; /* TODO would be nice to prune this one day */
22+
color: green;
1923
}
2024
:global(.x) :not(.unused) {
2125
color: green;
2226
}
27+
28+
:global(span):not(p span) {
29+
color: green;
30+
}
31+
span:not(:global(p span)) {
32+
color: green;
33+
}
34+
:global(span:not(p span)) {
35+
color: green;
36+
}
2337
</style>

packages/svelte/tests/css/samples/not-selector-hash-on-right-elements/_config.js

Lines changed: 0 additions & 5 deletions
This file was deleted.

packages/svelte/tests/css/samples/not-selector-hash-on-right-elements/expected.css

Lines changed: 0 additions & 4 deletions
This file was deleted.

packages/svelte/tests/css/samples/not-selector-hash-on-right-elements/expected.html

Lines changed: 0 additions & 1 deletion
This file was deleted.

packages/svelte/tests/css/samples/not-selector-hash-on-right-elements/input.svelte

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)