diff --git a/.changeset/dirty-parrots-smell.md b/.changeset/dirty-parrots-smell.md new file mode 100644 index 000000000000..50864d20a4a5 --- /dev/null +++ b/.changeset/dirty-parrots-smell.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure non-matching elements are scoped for `:not(...)` selector diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index f71a86095840..3630573f8d2d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -515,12 +515,27 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, for (const complex_selector of selector.args.children) { const relative = truncate(complex_selector); - if ( - relative.length === 0 /* is :global(...) */ || - apply_selector(relative, rule, element, stylesheet) - ) { + const is_global = relative.length === 0; + + if (is_global || apply_selector(relative, rule, element, stylesheet)) { complex_selector.metadata.used = true; matched = true; + } else if (name === 'not') { + // For `:not(...)` we gotta do the inverse: If it did not match, mark the element and possibly + // everything above (if the selector is written is a such) as scoped (because they matched by not matching). + // The above if condition will ensure the selector itself will be marked as used if it doesn't match at least once, + // and therefore having actual usefulness in the CSS output. + element.metadata.scoped = true; + + // bar:not(foo bar) means that foo is an ancestor of bar + if (complex_selector.children.length > 1) { + /** @type {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | null} */ + let el = get_element_parent(element); + while (el) { + el.metadata.scoped = true; + el = get_element_parent(el); + } + } } else if (complex_selector.children.length > 1 && (name == 'is' || name == 'where')) { // foo :is(bar baz) can also mean that bar is an ancestor of foo, and baz a descendant. // We can't fully check if that actually matches with our current algorithm, so we just assume it does. diff --git a/packages/svelte/tests/css/samples/not-selector-global/_config.js b/packages/svelte/tests/css/samples/not-selector-global/_config.js new file mode 100644 index 000000000000..cbe78d6031a4 --- /dev/null +++ b/packages/svelte/tests/css/samples/not-selector-global/_config.js @@ -0,0 +1,20 @@ +import { test } from '../../test'; + +export default test({ + warnings: [ + { + code: 'css_unused_selector', + message: 'Unused CSS selector ":global(.x) :not(.unused)"', + start: { + line: 17, + column: 1, + character: 289 + }, + end: { + line: 17, + column: 26, + character: 314 + } + } + ] +}); diff --git a/packages/svelte/tests/css/samples/not-selector-global/expected.css b/packages/svelte/tests/css/samples/not-selector-global/expected.css new file mode 100644 index 000000000000..3884cf2d2d2c --- /dev/null +++ b/packages/svelte/tests/css/samples/not-selector-global/expected.css @@ -0,0 +1,16 @@ + + .svelte-xyz:not(.foo) { + color: green; + } + .svelte-xyz:not(.foo:where(.svelte-xyz)):not(.unused) { + color: green; + } + .x:not(.foo.svelte-xyz) { + color: green; + } + .x:not(.unused.svelte-xyz) { + color: red; /* TODO would be nice to prune this one day */ + } + /* (unused) :global(.x) :not(.unused) { + color: red; + }*/ diff --git a/packages/svelte/tests/css/samples/not-selector-global/expected.html b/packages/svelte/tests/css/samples/not-selector-global/expected.html new file mode 100644 index 000000000000..b4834bb8189e --- /dev/null +++ b/packages/svelte/tests/css/samples/not-selector-global/expected.html @@ -0,0 +1 @@ +

foo

bar

\ No newline at end of file diff --git a/packages/svelte/tests/css/samples/not-selector-global/input.svelte b/packages/svelte/tests/css/samples/not-selector-global/input.svelte new file mode 100644 index 000000000000..6a4c7c43b811 --- /dev/null +++ b/packages/svelte/tests/css/samples/not-selector-global/input.svelte @@ -0,0 +1,20 @@ +

foo

+

bar

+ + \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/not-selector/_config.js b/packages/svelte/tests/css/samples/not-selector/_config.js index 6ab49a8809fb..02c903de7d04 100644 --- a/packages/svelte/tests/css/samples/not-selector/_config.js +++ b/packages/svelte/tests/css/samples/not-selector/_config.js @@ -20,42 +20,28 @@ export default test({ code: 'css_unused_selector', message: 'Unused CSS selector ":not(.foo):not(.unused)"', start: { - line: 18, + line: 12, column: 1, - character: 221 + character: 124 }, end: { - line: 18, + line: 12, column: 24, - character: 244 + character: 147 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "p :not(.foo)"', start: { - line: 25, + line: 19, column: 1, - character: 300 + character: 203 }, end: { - line: 25, + line: 19, column: 13, - character: 312 - } - }, - { - code: 'css_unused_selector', - message: 'Unused CSS selector ":global(.x) :not(.unused)"', - start: { - line: 34, - column: 1, - character: 469 - }, - end: { - line: 34, - column: 26, - character: 494 + character: 215 } } ] diff --git a/packages/svelte/tests/css/samples/not-selector/expected.css b/packages/svelte/tests/css/samples/not-selector/expected.css index 45e9ad69c051..2d9d59b2c4e0 100644 --- a/packages/svelte/tests/css/samples/not-selector/expected.css +++ b/packages/svelte/tests/css/samples/not-selector/expected.css @@ -5,13 +5,7 @@ /* (unused) :not(.unused) { color: red; }*/ - .svelte-xyz:not(.foo) { - color: green; - } - .svelte-xyz:not(.foo:where(.svelte-xyz)):not(.unused) { - color: green; - } /* (unused) :not(.foo):not(.unused) { color: red; }*/ @@ -22,12 +16,3 @@ /* (unused) p :not(.foo) { color: red; }*/ - .x:not(.foo.svelte-xyz) { - color: green; - } - .x:not(.unused.svelte-xyz) { - color: red; /* TODO would be nice to prune this one day */ - } - /* (unused) :global(.x) :not(.unused) { - color: red; - }*/ diff --git a/packages/svelte/tests/css/samples/not-selector/expected.html b/packages/svelte/tests/css/samples/not-selector/expected.html new file mode 100644 index 000000000000..b4834bb8189e --- /dev/null +++ b/packages/svelte/tests/css/samples/not-selector/expected.html @@ -0,0 +1 @@ +

foo

bar

\ No newline at end of file diff --git a/packages/svelte/tests/css/samples/not-selector/input.svelte b/packages/svelte/tests/css/samples/not-selector/input.svelte index 57d062cf4993..5d4c5dd9c4f3 100644 --- a/packages/svelte/tests/css/samples/not-selector/input.svelte +++ b/packages/svelte/tests/css/samples/not-selector/input.svelte @@ -8,13 +8,7 @@ :not(.unused) { color: red; } - :not(:global(.foo)) { - color: green; - } - :not(.foo):not(:global(.unused)) { - color: green; - } :not(.foo):not(.unused) { color: red; } @@ -25,13 +19,4 @@ p :not(.foo) { color: red; } - :global(.x):not(.foo) { - color: green; - } - :global(.x):not(.unused) { - color: red; /* TODO would be nice to prune this one day */ - } - :global(.x) :not(.unused) { - color: red; - } \ No newline at end of file