From 85a37de0928b491c4155f8278f2a96740a4263e9 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 8 Nov 2024 21:23:17 +0100 Subject: [PATCH 1/8] fix: account for `:has(...)` as part of `:root` We previously marked all `:root` selectors as global-like, which excempted them from further analysis. This causes problems: - things like `:not(...)` are never visited and therefore never marked as used -> we gotta do that directly when coming across this - `:has(...)` was never visited, too. Just marking it as used is not enough though, because we might need to scope its contents Therefore the logic is enhanced to account for these special cases. Fixes #14118 --- .changeset/beige-files-pull.md | 5 +++ .../phases/2-analyze/css/css-analyze.js | 21 +++++++-- .../phases/2-analyze/css/css-prune.js | 43 ++++++++++++++++--- .../svelte/tests/css/samples/root/_config.js | 33 +++++++++++++- .../tests/css/samples/root/expected.css | 26 ++++++++++- .../tests/css/samples/root/expected.html | 2 +- .../tests/css/samples/root/input.svelte | 25 ++++++++++- 7 files changed, 139 insertions(+), 16 deletions(-) create mode 100644 .changeset/beige-files-pull.md diff --git a/.changeset/beige-files-pull.md b/.changeset/beige-files-pull.md new file mode 100644 index 000000000000..2cd98a28198a --- /dev/null +++ b/.changeset/beige-files-pull.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: account for `:has(...)` as part of `:root` diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js index ec36e1ce64f0..083b7d8cbde5 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js @@ -156,9 +156,24 @@ const css_visitors = { ].includes(first.name)); } - node.metadata.is_global_like ||= !!node.selectors.find( - (child) => child.type === 'PseudoClassSelector' && child.name === 'root' - ); + const is_root_without_scoped_children = + node.selectors.some( + (child) => child.type === 'PseudoClassSelector' && child.name === 'root' + ) && + // :root.y:has(.x) is not a global selector because while .y is unscoped, .x inside `:has(...)` should be scoped + !node.selectors.some((child) => child.type === 'PseudoClassSelector' && child.name === 'has'); + + if (is_root_without_scoped_children) { + node.metadata.is_global_like ||= true; + // So that nested selectors like `:root:not(.x)` are not marked as unused + for (const child of node.selectors) { + walk(/** @type {Css.Node} */ (child), null, { + ComplexSelector(node) { + node.metadata.used = true; + } + }); + } + } context.next(); }, 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 a9ea9fe91376..5a982b86c229 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 @@ -196,7 +196,19 @@ function truncate(node) { ); }); - return node.children.slice(0, i + 1); + return node.children.slice(0, i + 1).map((child) => { + // In case of `:root.y:has(...)`, `y` is unscoped, but everything in `:has(...)` should be scoped (if not global). + // To properly accomplish that, we gotta filter out all selector types except `:has` and `:root`. + const root = child.selectors.find((s) => s.type === 'PseudoClassSelector' && s.name === 'root'); + if (!root || child.metadata.is_global_like) return child; + + return { + ...child, + selectors: child.selectors.filter( + (s) => s.type === 'PseudoClassSelector' && (s.name === 'has' || s.name === 'root') + ) + }; + }); } /** @@ -415,6 +427,16 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, /** @type {Array} */ let sibling_elements; // do them lazy because it's rarely used and expensive to calculate + // If this is a :has on a :root, we gotta include the element itself, too, because everything's a descendant of :root + const is_root_selector = other_selectors.some( + (s) => s.type === 'PseudoClassSelector' && s.name === 'root' + ); + + if (is_root_selector) { + child_elements.push(element); + descendant_elements.push(element); + } + walk( /** @type {Compiler.SvelteNode} */ (element.fragment), { is_child: true }, @@ -460,7 +482,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, const descendants = left_most_combinator.name === '+' || left_most_combinator.name === '~' - ? (sibling_elements ??= get_following_sibling_elements(element)) + ? (sibling_elements ??= get_following_sibling_elements(element, is_root_selector)) : left_most_combinator.name === '>' ? child_elements : descendant_elements; @@ -507,9 +529,9 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, switch (selector.type) { case 'PseudoClassSelector': { - if (name === 'host' || name === 'root') { - return false; - } + if (name === 'host') return false; + + if (name === 'root') break; if ( name === 'global' && @@ -681,8 +703,11 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, return true; } -/** @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element */ -function get_following_sibling_elements(element) { +/** + * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element + * @param {boolean} include_self + */ +function get_following_sibling_elements(element, include_self) { /** @type {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.Root | null} */ let parent = get_element_parent(element); @@ -723,6 +748,10 @@ function get_following_sibling_elements(element) { } } + if (include_self) { + sibling_elements.push(element); + } + return sibling_elements; } diff --git a/packages/svelte/tests/css/samples/root/_config.js b/packages/svelte/tests/css/samples/root/_config.js index f47bee71df87..5a1c7f393169 100644 --- a/packages/svelte/tests/css/samples/root/_config.js +++ b/packages/svelte/tests/css/samples/root/_config.js @@ -1,3 +1,34 @@ import { test } from '../../test'; -export default test({}); +export default test({ + warnings: [ + { + code: 'css_unused_selector', + message: 'Unused CSS selector ":root .unused"', + start: { + line: 18, + column: 2, + character: 190 + }, + end: { + line: 18, + column: 15, + character: 203 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector ":root:has(.unused)"', + start: { + line: 25, + column: 2, + character: 269 + }, + end: { + line: 25, + column: 20, + character: 287 + } + } + ] +}); diff --git a/packages/svelte/tests/css/samples/root/expected.css b/packages/svelte/tests/css/samples/root/expected.css index a10769625024..f79d80b41c1a 100644 --- a/packages/svelte/tests/css/samples/root/expected.css +++ b/packages/svelte/tests/css/samples/root/expected.css @@ -1,9 +1,31 @@ + :root { - color: red; + color: green; } .foo:root { - color: blue; + color: green; } :root.foo { color: green; } + :root.unknown { + color: green; + } + + :root h1.svelte-xyz { + color: green; + } + /* (unused) :root .unused { + color: red; + }*/ + + :root:has(h1:where(.svelte-xyz)) { + color: green; + } + /* (unused) :root:has(.unused) { + color: red; + }*/ + + :root:not(.x) { + color: green; + } diff --git a/packages/svelte/tests/css/samples/root/expected.html b/packages/svelte/tests/css/samples/root/expected.html index 1d90ab5df79e..5c30de1c29e8 100644 --- a/packages/svelte/tests/css/samples/root/expected.html +++ b/packages/svelte/tests/css/samples/root/expected.html @@ -1 +1 @@ -

Hello!

\ No newline at end of file +

Hello!

\ No newline at end of file diff --git a/packages/svelte/tests/css/samples/root/input.svelte b/packages/svelte/tests/css/samples/root/input.svelte index 979c9d4f0a88..dd2b2004eee4 100644 --- a/packages/svelte/tests/css/samples/root/input.svelte +++ b/packages/svelte/tests/css/samples/root/input.svelte @@ -1,13 +1,34 @@

Hello!

From 5c8561a586d1d659c28d97aa0efd61ed4965bec4 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 8 Nov 2024 23:14:37 +0100 Subject: [PATCH 2/8] handle :root with nesting --- .../phases/2-analyze/css/css-prune.js | 60 +++++++++++++------ .../svelte/tests/css/samples/root/_config.js | 28 +++++++++ .../tests/css/samples/root/expected.css | 15 +++++ .../tests/css/samples/root/input.svelte | 15 +++++ 4 files changed, 101 insertions(+), 17 deletions(-) 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 5a982b86c229..723d0b0e3376 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 @@ -10,6 +10,7 @@ import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js'; * stylesheet: Compiler.Css.StyleSheet; * element: Compiler.AST.RegularElement | Compiler.AST.SvelteElement; * from_render_tag: boolean; + * in_root_selector: boolean; * }} State */ /** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */ @@ -61,9 +62,17 @@ export function prune(stylesheet, element) { const parent = get_element_parent(element); if (!parent) return; - walk(stylesheet, { stylesheet, element: parent, from_render_tag: true }, visitors); + walk( + stylesheet, + { stylesheet, element: parent, from_render_tag: true, in_root_selector: false }, + visitors + ); } else { - walk(stylesheet, { stylesheet, element, from_render_tag: false }, visitors); + walk( + stylesheet, + { stylesheet, element, from_render_tag: false, in_root_selector: false }, + visitors + ); } } @@ -79,6 +88,16 @@ const visitors = { ComplexSelector(node, context) { const selectors = get_relative_selectors(node); const inner = selectors[selectors.length - 1]; + /** @type {State} */ + const state = { + ...context.state, + in_root_selector: [ + node, + ...get_parent_rules(node.metadata.rule).flatMap((r) => r.prelude.children) + ].some((c) => + c.children[0].selectors.some((s) => s.type === 'PseudoClassSelector' && s.name === 'root') + ) + }; if (context.state.from_render_tag) { // 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 = { selectors_to_check, /** @type {Compiler.Css.Rule} */ (node.metadata.rule), element, - context.state + state ) ) { mark(inner, element); @@ -114,7 +133,7 @@ const visitors = { selectors, /** @type {Compiler.Css.Rule} */ (node.metadata.rule), context.state.element, - context.state + state ) ) { mark(inner, context.state.element); @@ -127,6 +146,21 @@ const visitors = { } }; +/** + * @param {Compiler.Css.Rule | null} rule + */ +function get_parent_rules(rule) { + const parents = []; + + let parent = rule?.metadata.parent_rule; + while (parent) { + parents.push(parent); + parent = parent.metadata.parent_rule; + } + + return parents; +} + /** * Retrieves the relative selectors (minus the trailing globals) from a complex selector. * Also searches them for any existing `&` selectors and adds one if none are found. @@ -198,15 +232,13 @@ function truncate(node) { return node.children.slice(0, i + 1).map((child) => { // In case of `:root.y:has(...)`, `y` is unscoped, but everything in `:has(...)` should be scoped (if not global). - // To properly accomplish that, we gotta filter out all selector types except `:has` and `:root`. + // To properly accomplish that, we gotta filter out all selector types `:has`. const root = child.selectors.find((s) => s.type === 'PseudoClassSelector' && s.name === 'root'); if (!root || child.metadata.is_global_like) return child; return { ...child, - selectors: child.selectors.filter( - (s) => s.type === 'PseudoClassSelector' && (s.name === 'has' || s.name === 'root') - ) + selectors: child.selectors.filter((s) => s.type === 'PseudoClassSelector' && s.name === 'has') }; }); } @@ -428,11 +460,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, let sibling_elements; // do them lazy because it's rarely used and expensive to calculate // If this is a :has on a :root, we gotta include the element itself, too, because everything's a descendant of :root - const is_root_selector = other_selectors.some( - (s) => s.type === 'PseudoClassSelector' && s.name === 'root' - ); - - if (is_root_selector) { + if (state.in_root_selector) { child_elements.push(element); descendant_elements.push(element); } @@ -482,7 +510,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, const descendants = left_most_combinator.name === '+' || left_most_combinator.name === '~' - ? (sibling_elements ??= get_following_sibling_elements(element, is_root_selector)) + ? (sibling_elements ??= get_following_sibling_elements(element, state.in_root_selector)) : left_most_combinator.name === '>' ? child_elements : descendant_elements; @@ -529,9 +557,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, switch (selector.type) { case 'PseudoClassSelector': { - if (name === 'host') return false; - - if (name === 'root') break; + if (name === 'host' || name === 'root') return false; if ( name === 'global' && diff --git a/packages/svelte/tests/css/samples/root/_config.js b/packages/svelte/tests/css/samples/root/_config.js index 5a1c7f393169..7707b9ac2243 100644 --- a/packages/svelte/tests/css/samples/root/_config.js +++ b/packages/svelte/tests/css/samples/root/_config.js @@ -29,6 +29,34 @@ export default test({ column: 20, character: 287 } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector ".unused"', + start: { + line: 37, + column: 4, + character: 401 + }, + end: { + line: 37, + column: 11, + character: 408 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector ":has(.unused)"', + start: { + line: 43, + column: 4, + character: 480 + }, + end: { + line: 43, + column: 17, + character: 493 + } } ] }); diff --git a/packages/svelte/tests/css/samples/root/expected.css b/packages/svelte/tests/css/samples/root/expected.css index f79d80b41c1a..745f4a789d28 100644 --- a/packages/svelte/tests/css/samples/root/expected.css +++ b/packages/svelte/tests/css/samples/root/expected.css @@ -29,3 +29,18 @@ :root:not(.x) { color: green; } + + :root { + h1.svelte-xyz { + color: green; + } + /* (unused) .unused { + color: red; + }*/ + .svelte-xyz:has(h1:where(.svelte-xyz)) { + color: green; + } + /* (unused) :has(.unused) { + color: red; + }*/ + } diff --git a/packages/svelte/tests/css/samples/root/input.svelte b/packages/svelte/tests/css/samples/root/input.svelte index dd2b2004eee4..c2d0dd59b319 100644 --- a/packages/svelte/tests/css/samples/root/input.svelte +++ b/packages/svelte/tests/css/samples/root/input.svelte @@ -29,6 +29,21 @@ :root:not(.x) { color: green; } + + :root { + h1 { + color: green; + } + .unused { + color: red; + } + :has(h1) { + color: green; + } + :has(.unused) { + color: red; + } + }

Hello!

From ab3bd3a4b7dca8175c15828603f5cd9f8d544a60 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 11 Nov 2024 10:16:00 +0100 Subject: [PATCH 3/8] nesting selector fix --- .../src/compiler/phases/2-analyze/css/css-prune.js | 5 ++++- packages/svelte/tests/css/samples/root/_config.js | 14 ++++++++++++++ .../svelte/tests/css/samples/root/expected.css | 6 ++++++ .../svelte/tests/css/samples/root/input.svelte | 6 ++++++ 4 files changed, 30 insertions(+), 1 deletion(-) 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 723d0b0e3376..352c11ddcaf0 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 @@ -710,7 +710,10 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, const parent = /** @type {Compiler.Css.Rule} */ (rule.metadata.parent_rule); for (const complex_selector of parent.prelude.children) { - if (apply_selector(get_relative_selectors(complex_selector), parent, element, state)) { + if ( + apply_selector(get_relative_selectors(complex_selector), parent, element, state) || + complex_selector.children.every((s) => is_global(s, parent)) + ) { complex_selector.metadata.used = true; matched = true; } diff --git a/packages/svelte/tests/css/samples/root/_config.js b/packages/svelte/tests/css/samples/root/_config.js index 7707b9ac2243..fad7fc536d64 100644 --- a/packages/svelte/tests/css/samples/root/_config.js +++ b/packages/svelte/tests/css/samples/root/_config.js @@ -57,6 +57,20 @@ export default test({ column: 17, character: 493 } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector "&:has(.unused)"', + start: { + line: 49, + column: 4, + character: 566 + }, + end: { + line: 49, + column: 18, + character: 580 + } } ] }); diff --git a/packages/svelte/tests/css/samples/root/expected.css b/packages/svelte/tests/css/samples/root/expected.css index 745f4a789d28..b4c82c258ebc 100644 --- a/packages/svelte/tests/css/samples/root/expected.css +++ b/packages/svelte/tests/css/samples/root/expected.css @@ -43,4 +43,10 @@ /* (unused) :has(.unused) { color: red; }*/ + &:has(h1.svelte-xyz) { + color: green; + } + /* (unused) &:has(.unused) { + color: red; + }*/ } diff --git a/packages/svelte/tests/css/samples/root/input.svelte b/packages/svelte/tests/css/samples/root/input.svelte index c2d0dd59b319..e06a1a36fa05 100644 --- a/packages/svelte/tests/css/samples/root/input.svelte +++ b/packages/svelte/tests/css/samples/root/input.svelte @@ -43,6 +43,12 @@ :has(.unused) { color: red; } + &:has(h1) { + color: green; + } + &:has(.unused) { + color: red; + } } From 50af4d06bfe09e90fc2d34c5f053a76fc5d69f00 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 11 Nov 2024 10:53:44 +0100 Subject: [PATCH 4/8] tweak --- .../phases/2-analyze/css/css-prune.js | 45 ++++++++----------- 1 file changed, 18 insertions(+), 27 deletions(-) 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 352c11ddcaf0..e6a224eaa2a7 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 @@ -10,7 +10,6 @@ import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js'; * stylesheet: Compiler.Css.StyleSheet; * element: Compiler.AST.RegularElement | Compiler.AST.SvelteElement; * from_render_tag: boolean; - * in_root_selector: boolean; * }} State */ /** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */ @@ -62,17 +61,9 @@ export function prune(stylesheet, element) { const parent = get_element_parent(element); if (!parent) return; - walk( - stylesheet, - { stylesheet, element: parent, from_render_tag: true, in_root_selector: false }, - visitors - ); + walk(stylesheet, { stylesheet, element: parent, from_render_tag: true }, visitors); } else { - walk( - stylesheet, - { stylesheet, element, from_render_tag: false, in_root_selector: false }, - visitors - ); + walk(stylesheet, { stylesheet, element, from_render_tag: false }, visitors); } } @@ -88,16 +79,6 @@ const visitors = { ComplexSelector(node, context) { const selectors = get_relative_selectors(node); const inner = selectors[selectors.length - 1]; - /** @type {State} */ - const state = { - ...context.state, - in_root_selector: [ - node, - ...get_parent_rules(node.metadata.rule).flatMap((r) => r.prelude.children) - ].some((c) => - c.children[0].selectors.some((s) => s.type === 'PseudoClassSelector' && s.name === 'root') - ) - }; if (context.state.from_render_tag) { // We're searching for a match that crosses a render tag boundary. That means we have to both traverse up @@ -117,7 +98,7 @@ const visitors = { selectors_to_check, /** @type {Compiler.Css.Rule} */ (node.metadata.rule), element, - state + context.state ) ) { mark(inner, element); @@ -133,7 +114,7 @@ const visitors = { selectors, /** @type {Compiler.Css.Rule} */ (node.metadata.rule), context.state.element, - state + context.state ) ) { mark(inner, context.state.element); @@ -147,6 +128,7 @@ const visitors = { }; /** + * Returns all parent rules; root is last * @param {Compiler.Css.Rule | null} rule */ function get_parent_rules(rule) { @@ -232,7 +214,7 @@ function truncate(node) { return node.children.slice(0, i + 1).map((child) => { // In case of `:root.y:has(...)`, `y` is unscoped, but everything in `:has(...)` should be scoped (if not global). - // To properly accomplish that, we gotta filter out all selector types `:has`. + // To properly accomplish that, we gotta filter out all selector types except `:has`. const root = child.selectors.find((s) => s.type === 'PseudoClassSelector' && s.name === 'root'); if (!root || child.metadata.is_global_like) return child; @@ -459,8 +441,17 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, /** @type {Array} */ let sibling_elements; // do them lazy because it's rarely used and expensive to calculate - // If this is a :has on a :root, we gotta include the element itself, too, because everything's a descendant of :root - if (state.in_root_selector) { + // If this is a :has inside a global selector, we gotta include the element itself, too, + // because the global selector might be for an element that's outside the component (e.g. :root). + const rules = [rule, ...get_parent_rules(rule)]; + const include_self = + rules.some((r) => r.prelude.children.some((c) => c.children.some((s) => is_global(s, r)))) || + rules[rules.length - 1].prelude.children.some((c) => + c.children.some((r) => + r.selectors.some((s) => s.type === 'PseudoClassSelector' && s.name === 'root') + ) + ); + if (include_self) { child_elements.push(element); descendant_elements.push(element); } @@ -510,7 +501,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, const descendants = left_most_combinator.name === '+' || left_most_combinator.name === '~' - ? (sibling_elements ??= get_following_sibling_elements(element, state.in_root_selector)) + ? (sibling_elements ??= get_following_sibling_elements(element, include_self)) : left_most_combinator.name === '>' ? child_elements : descendant_elements; From bc4ab8b4b1ee648eee0346cadf746d0e47f5c5a6 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 11 Nov 2024 12:55:00 +0100 Subject: [PATCH 5/8] fix more bugs and simplify --- .../phases/2-analyze/css/css-analyze.js | 27 +------ .../phases/2-analyze/css/css-prune.js | 68 ++--------------- .../compiler/phases/2-analyze/css/css-warn.js | 7 +- .../compiler/phases/2-analyze/css/utils.js | 74 ++++++++++++++++++- .../compiler/phases/3-transform/css/index.js | 14 ++-- packages/svelte/src/compiler/types/css.d.ts | 4 +- .../svelte/tests/css/samples/has/_config.js | 28 +++++++ .../svelte/tests/css/samples/has/expected.css | 19 ++++- .../svelte/tests/css/samples/has/input.svelte | 15 ++++ .../svelte/tests/css/samples/is/_config.js | 40 +++++----- .../svelte/tests/css/samples/is/expected.css | 15 ++++ .../svelte/tests/css/samples/is/input.svelte | 15 ++++ 12 files changed, 208 insertions(+), 118 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js index 083b7d8cbde5..7b95e2b55cfe 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js @@ -4,6 +4,7 @@ import { walk } from 'zimmerframe'; import * as e from '../../../errors.js'; import { is_keyframes_node } from '../../css.js'; +import { is_global } from './utils.js'; /** * @typedef {Visitors< @@ -15,27 +16,6 @@ import { is_keyframes_node } from '../../css.js'; * >} CssVisitors */ -/** - * True if is `:global(...)` or `:global` - * @param {Css.RelativeSelector} relative_selector - * @returns {relative_selector is Css.RelativeSelector & { selectors: [Css.PseudoClassSelector, ...Array] }} - */ -function is_global(relative_selector) { - const first = relative_selector.selectors[0]; - - return ( - first.type === 'PseudoClassSelector' && - first.name === 'global' && - (first.args === null || - // Only these two selector types keep the whole selector global, because e.g. - // :global(button).x means that the selector is still scoped because of the .x - relative_selector.selectors.every( - (selector) => - selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector' - )) - ); -} - /** * True if is `:global` * @param {Css.SimpleSelector} simple_selector @@ -156,15 +136,14 @@ const css_visitors = { ].includes(first.name)); } - const is_root_without_scoped_children = + node.metadata.is_global_like ||= node.selectors.some( (child) => child.type === 'PseudoClassSelector' && child.name === 'root' ) && // :root.y:has(.x) is not a global selector because while .y is unscoped, .x inside `:has(...)` should be scoped !node.selectors.some((child) => child.type === 'PseudoClassSelector' && child.name === 'has'); - if (is_root_without_scoped_children) { - node.metadata.is_global_like ||= true; + if (node.metadata.is_global_like || node.metadata.is_global) { // So that nested selectors like `:root:not(.x)` are not marked as unused for (const child of node.selectors) { walk(/** @type {Css.Node} */ (child), null, { 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 e6a224eaa2a7..bf0fd275662c 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 @@ -1,7 +1,7 @@ /** @import { Visitors } from 'zimmerframe' */ /** @import * as Compiler from '#compiler' */ import { walk } from 'zimmerframe'; -import { get_possible_values } from './utils.js'; +import { get_parent_rules, get_possible_values, is_outer_global } from './utils.js'; import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js'; import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js'; @@ -127,22 +127,6 @@ const visitors = { } }; -/** - * Returns all parent rules; root is last - * @param {Compiler.Css.Rule | null} rule - */ -function get_parent_rules(rule) { - const parents = []; - - let parent = rule?.metadata.parent_rule; - while (parent) { - parents.push(parent); - parent = parent.metadata.parent_rule; - } - - return parents; -} - /** * Retrieves the relative selectors (minus the trailing globals) from a complex selector. * Also searches them for any existing `&` selectors and adds one if none are found. @@ -188,7 +172,7 @@ function get_relative_selectors(node) { } /** - * Discard trailing `:global(...)` selectors without a `:has/is/where/not(...)` modifier, these are unused for scoping purposes + * Discard trailing `:global(...)` selectors, these are unused for scoping purposes * @param {Compiler.Css.ComplexSelector} node */ function truncate(node) { @@ -198,17 +182,8 @@ function truncate(node) { // not after a :global selector !metadata.is_global_like && !(first.type === 'PseudoClassSelector' && first.name === 'global' && first.args === null) && - // not a :global(...) without a :has/is/where/not(...) modifier - (!metadata.is_global || - selectors.some( - (selector) => - selector.type === 'PseudoClassSelector' && - selector.args !== null && - (selector.name === 'has' || - selector.name === 'is' || - selector.name === 'where' || - selector.name === 'not') - )) + // not a :global(...) without a :has/is/where(...) modifier that is scoped + !metadata.is_global ); }); @@ -360,7 +335,9 @@ function apply_combinator(combinator, relative_selector, parent_selectors, rule, * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element */ function mark(relative_selector, element) { - relative_selector.metadata.scoped = true; + if (!is_outer_global(relative_selector)) { + relative_selector.metadata.scoped = true; + } element.metadata.scoped = true; } @@ -522,20 +499,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, } if (!matched) { - if (relative_selector.metadata.is_global && !relative_selector.metadata.is_global_like) { - // Edge case: `:global(.x):has(.y)` where `.x` is global but `.y` doesn't match. - // Since `used` is set to `true` for `:global(.x)` in css-analyze beforehand, and - // we have no way of knowing if it's safe to set it back to `false`, we'll mark - // the inner selector as used and scoped to prevent it from being pruned, which could - // result in a invalid CSS output (e.g. `.x:has(/* unused .y */)`). The result - // can't match a real element, so the only drawback is the missing prune. - // TODO clean this up some day - complex_selectors[0].metadata.used = true; - complex_selectors[0].children.forEach((selector) => { - selector.metadata.scoped = true; - }); - } - return false; } } @@ -617,23 +580,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, } if (!matched) { - if ( - relative_selector.metadata.is_global && - !relative_selector.metadata.is_global_like - ) { - // Edge case: `:global(.x):is(.y)` where `.x` is global but `.y` doesn't match. - // Since `used` is set to `true` for `:global(.x)` in css-analyze beforehand, and - // we have no way of knowing if it's safe to set it back to `false`, we'll mark - // the inner selector as used and scoped to prevent it from being pruned, which could - // result in a invalid CSS output (e.g. `.x:is(/* unused .y */)`). The result - // can't match a real element, so the only drawback is the missing prune. - // TODO clean this up some day - selector.args.children[0].metadata.used = true; - selector.args.children[0].children.forEach((selector) => { - selector.metadata.scoped = true; - }); - } - return false; } } diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js index 482278b79554..eab67327e2cc 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js @@ -24,7 +24,12 @@ const visitors = { } }, ComplexSelector(node, context) { - if (!node.metadata.used) { + if ( + !node.metadata.used && + // prevent double-marking of `.unused:is(.unused)` + (context.path.at(-2)?.type !== 'PseudoClassSelector' || + /** @type {Css.ComplexSelector} */ (context.path.at(-4))?.metadata.used) + ) { const content = context.state.stylesheet.content; const text = content.styles.substring(node.start - content.start, node.end - content.start); w.css_unused_selector(node, text); diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/utils.js b/packages/svelte/src/compiler/phases/2-analyze/css/utils.js index 99603fad291d..cb4437e95f2b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/utils.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/utils.js @@ -1,4 +1,4 @@ -/** @import { AST } from '#compiler' */ +/** @import { AST, Css } from '#compiler' */ /** @import { Node } from 'estree' */ const UNKNOWN = {}; @@ -33,3 +33,75 @@ export function get_possible_values(chunk) { if (values.has(UNKNOWN)) return null; return values; } + +/** + * Returns all parent rules; root is last + * @param {Css.Rule | null} rule + */ +export function get_parent_rules(rule) { + const parents = []; + + let parent = rule?.metadata.parent_rule; + while (parent) { + parents.push(parent); + parent = parent.metadata.parent_rule; + } + + return parents; +} + +/** + * True if is `:global(...)` or `:global` and no pseudo class that is scoped. + * @param {Css.RelativeSelector} relative_selector + * @returns {relative_selector is Css.RelativeSelector & { selectors: [Css.PseudoClassSelector, ...Array] }} + */ +export function is_global(relative_selector) { + const first = relative_selector.selectors[0]; + + return ( + first.type === 'PseudoClassSelector' && + first.name === 'global' && + (first.args === null || + // Only these two selector types keep the whole selector global, because e.g. + // :global(button).x means that the selector is still scoped because of the .x + relative_selector.selectors.every( + (selector) => + (selector.type === 'PseudoClassSelector' && // These make the selector scoped + ((selector.name !== 'has' && + selector.name !== 'is' && + selector.name !== 'where' && + // Not is special because we want to scope as specific as possible, but because :not + // inverses the result, we want to leave the unscoped, too. The exception is more than + // one selector in the :not (.e.g :not(.x .y)), then .x and .y should be scoped + (selector.name !== 'not' || + selector.args === null || + selector.args.children.every((c) => c.children.length === 1))) || + // selectors with has/is/where/not can also be global if all their children are global + selector.args === null || + selector.args.children.every((c) => c.children.every((r) => is_global(r))))) || + selector.type === 'PseudoElementSelector' + )) + ); +} + +/** + * True if is `:global(...)` or `:global`, irrespective of whether or not there are any pseudo classes that are scoped. + * Difference to `is_global`: `:global(x):has(y)` is `true` for `is_outer_global` but `false` for `is_global`. + * @param {Css.RelativeSelector} relative_selector + * @returns {relative_selector is Css.RelativeSelector & { selectors: [Css.PseudoClassSelector, ...Array] }} + */ +export function is_outer_global(relative_selector) { + const first = relative_selector.selectors[0]; + + return ( + first.type === 'PseudoClassSelector' && + first.name === 'global' && + (first.args === null || + // Only these two selector types keep the whole selector global, because e.g. + // :global(button).x means that the selector is still scoped because of the .x + relative_selector.selectors.every( + (selector) => + selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector' + )) + ); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/css/index.js b/packages/svelte/src/compiler/phases/3-transform/css/index.js index 350eca35d8a6..d1f3d3baa62d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/css/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/css/index.js @@ -292,6 +292,13 @@ const visitors = { context.state.code.prependRight(global.start, '&'); } continue; + } else { + // for any :global() or :global at the middle of compound selector + for (const selector of relative_selector.selectors) { + if (selector.type === 'PseudoClassSelector' && selector.name === 'global') { + remove_global_pseudo_class(selector, null, context.state); + } + } } if (relative_selector.metadata.scoped) { @@ -306,13 +313,6 @@ const visitors = { } } - // for any :global() or :global at the middle of compound selector - for (const selector of relative_selector.selectors) { - if (selector.type === 'PseudoClassSelector' && selector.name === 'global') { - remove_global_pseudo_class(selector, null, context.state); - } - } - if (relative_selector.selectors.some((s) => s.type === 'NestingSelector')) { continue; } diff --git a/packages/svelte/src/compiler/types/css.d.ts b/packages/svelte/src/compiler/types/css.d.ts index 97ac7fc0d3a7..ba4079ecd065 100644 --- a/packages/svelte/src/compiler/types/css.d.ts +++ b/packages/svelte/src/compiler/types/css.d.ts @@ -87,8 +87,8 @@ export namespace Css { /** * `true` if the whole selector is unscoped, e.g. `:global(...)` or `:global` or `:global.x`. * Selectors like `:global(...).x` are not considered global, because they still need scoping. - * Selectors like `:global(...):is/where/not/has(...)` are considered global even if they aren't - * strictly speaking (we should consolidate the logic around this at some point). + * Selectors like `:global(...):is/where/not/has(...)` are only considered global if all their + * children are global. */ is_global: boolean; /** `:root`, `:host`, `::view-transition`, or selectors after a `:global` */ diff --git a/packages/svelte/tests/css/samples/has/_config.js b/packages/svelte/tests/css/samples/has/_config.js index 8b8e7760f5de..45311e5799ae 100644 --- a/packages/svelte/tests/css/samples/has/_config.js +++ b/packages/svelte/tests/css/samples/has/_config.js @@ -44,6 +44,20 @@ export default test({ character: 401 } }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector ":global(.foo):has(.unused)"', + start: { + line: 40, + column: 1, + character: 422 + }, + end: { + line: 40, + column: 27, + character: 448 + } + }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(y):has(.unused)"', @@ -141,6 +155,20 @@ export default test({ column: 11, character: 1336 } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector ":has(.unused)"', + start: { + line: 129, + column: 2, + character: 1409 + }, + end: { + line: 129, + column: 15, + character: 1422 + } } ] }); diff --git a/packages/svelte/tests/css/samples/has/expected.css b/packages/svelte/tests/css/samples/has/expected.css index 4cb8d76bf687..f29b55686072 100644 --- a/packages/svelte/tests/css/samples/has/expected.css +++ b/packages/svelte/tests/css/samples/has/expected.css @@ -27,9 +27,9 @@ /* (unused) x:has(.unused) { color: red; }*/ - .foo:has(.unused.svelte-xyz) { + /* (unused) :global(.foo):has(.unused) { color: red; - } + }*/ x.svelte-xyz:has(y:where(.svelte-xyz) /* (unused) .unused*/) { color: green; @@ -111,3 +111,18 @@ /* (unused) x:has(~ y) { color: red; }*/ + + .foo { + .svelte-xyz:has(x:where(.svelte-xyz)) { + color: green; + } + /* (unused) :has(.unused) { + color: red; + }*/ + &:has(x.svelte-xyz) { + color: green; + } + &:has(/* (unused) .unused*/) { + color: red; + } + } diff --git a/packages/svelte/tests/css/samples/has/input.svelte b/packages/svelte/tests/css/samples/has/input.svelte index 8eb6296f050c..3487b64e8c57 100644 --- a/packages/svelte/tests/css/samples/has/input.svelte +++ b/packages/svelte/tests/css/samples/has/input.svelte @@ -121,4 +121,19 @@ x:has(~ y) { color: red; } + + :global(.foo) { + :has(x) { + color: green; + } + :has(.unused) { + color: red; + } + &:has(x) { + color: green; + } + &:has(.unused) { + color: red; + } + } diff --git a/packages/svelte/tests/css/samples/is/_config.js b/packages/svelte/tests/css/samples/is/_config.js index e4c24eb75681..eee617e7e4bd 100644 --- a/packages/svelte/tests/css/samples/is/_config.js +++ b/packages/svelte/tests/css/samples/is/_config.js @@ -30,20 +30,6 @@ export default test({ character: 125 } }, - { - code: 'css_unused_selector', - message: 'Unused CSS selector ".unused"', - start: { - line: 14, - column: 7, - character: 117 - }, - end: { - line: 14, - column: 14, - character: 124 - } - }, { code: 'css_unused_selector', message: 'Unused CSS selector ":global(.foo) :is(.unused)"', @@ -60,16 +46,30 @@ export default test({ }, { code: 'css_unused_selector', - message: 'Unused CSS selector ".unused"', + message: 'Unused CSS selector ":global(.foo):is(.unused)"', start: { - line: 28, - column: 19, - character: 292 + line: 34, + column: 1, + character: 363 }, end: { - line: 28, + line: 34, column: 26, - character: 299 + character: 388 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector ":is(.unused)"', + start: { + line: 52, + column: 2, + character: 636 + }, + end: { + line: 52, + column: 14, + character: 648 } } ] diff --git a/packages/svelte/tests/css/samples/is/expected.css b/packages/svelte/tests/css/samples/is/expected.css index be8deeff2892..639f0d53928c 100644 --- a/packages/svelte/tests/css/samples/is/expected.css +++ b/packages/svelte/tests/css/samples/is/expected.css @@ -22,6 +22,12 @@ /* (unused) :global(.foo) :is(.unused) { color: red; }*/ + .foo:is(x.svelte-xyz) { + color: green; + } + /* (unused) :global(.foo):is(.unused) { + color: red; + }*/ x.svelte-xyz :is(html *) { color: green; @@ -32,3 +38,12 @@ y.svelte-xyz :is(x:where(.svelte-xyz) :where(.svelte-xyz)) { color: green; /* matches z */ } + + .foo { + :is(x.svelte-xyz) { + color: green; + } + /* (unused) :is(.unused) { + color: red; + }*/ + } diff --git a/packages/svelte/tests/css/samples/is/input.svelte b/packages/svelte/tests/css/samples/is/input.svelte index 06aa0669b90f..b5ae6630876a 100644 --- a/packages/svelte/tests/css/samples/is/input.svelte +++ b/packages/svelte/tests/css/samples/is/input.svelte @@ -28,6 +28,12 @@ :global(.foo) :is(.unused) { color: red; } + :global(.foo):is(x) { + color: green; + } + :global(.foo):is(.unused) { + color: red; + } x :is(:global(html *)) { color: green; @@ -38,4 +44,13 @@ y :is(x *) { color: green; /* matches z */ } + + :global(.foo) { + :is(x) { + color: green; + } + :is(.unused) { + color: red; + } + } From aa39e1e2112ec65d05fc866dd789f2817da32457 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 11 Nov 2024 13:16:58 +0100 Subject: [PATCH 6/8] another fix --- .../phases/2-analyze/css/css-analyze.js | 8 +++- .../compiler/phases/2-analyze/css/utils.js | 40 ++++++++++++------- .../svelte/tests/css/samples/has/_config.js | 14 +++++++ .../svelte/tests/css/samples/has/expected.css | 4 +- 4 files changed, 47 insertions(+), 19 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js index 7b95e2b55cfe..3add76a6858e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js @@ -4,7 +4,7 @@ import { walk } from 'zimmerframe'; import * as e from '../../../errors.js'; import { is_keyframes_node } from '../../css.js'; -import { is_global } from './utils.js'; +import { is_global, is_unscoped_pseudo_class } from './utils.js'; /** * @typedef {Visitors< @@ -99,11 +99,15 @@ const css_visitors = { node.metadata.rule?.metadata.parent_rule && node.children[0]?.selectors[0]?.type === 'NestingSelector' ) { + const first = node.children[0]?.selectors[1]; + const no_nesting_scope = + first?.type !== 'PseudoClassSelector' || is_unscoped_pseudo_class(first); const parent_is_global = node.metadata.rule.metadata.parent_rule.prelude.children.some( (child) => child.children.length === 1 && child.children[0].metadata.is_global ); // mark `&:hover` in `:global(.foo) { &:hover { color: green }}` as used - if (parent_is_global) { + if (no_nesting_scope && parent_is_global) { + // here? node.metadata.used = true; } } diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/utils.js b/packages/svelte/src/compiler/phases/2-analyze/css/utils.js index cb4437e95f2b..0226a150c97d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/utils.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/utils.js @@ -66,24 +66,34 @@ export function is_global(relative_selector) { // :global(button).x means that the selector is still scoped because of the .x relative_selector.selectors.every( (selector) => - (selector.type === 'PseudoClassSelector' && // These make the selector scoped - ((selector.name !== 'has' && - selector.name !== 'is' && - selector.name !== 'where' && - // Not is special because we want to scope as specific as possible, but because :not - // inverses the result, we want to leave the unscoped, too. The exception is more than - // one selector in the :not (.e.g :not(.x .y)), then .x and .y should be scoped - (selector.name !== 'not' || - selector.args === null || - selector.args.children.every((c) => c.children.length === 1))) || - // selectors with has/is/where/not can also be global if all their children are global - selector.args === null || - selector.args.children.every((c) => c.children.every((r) => is_global(r))))) || - selector.type === 'PseudoElementSelector' + is_unscoped_pseudo_class(selector) || selector.type === 'PseudoElementSelector' )) ); } +/** + * `true` if is a pseudo class that cannot be or is not scoped + * @param {Css.SimpleSelector} selector + */ +export function is_unscoped_pseudo_class(selector) { + return ( + selector.type === 'PseudoClassSelector' && + // These make the selector scoped + ((selector.name !== 'has' && + selector.name !== 'is' && + selector.name !== 'where' && + // Not is special because we want to scope as specific as possible, but because :not + // inverses the result, we want to leave the unscoped, too. The exception is more than + // one selector in the :not (.e.g :not(.x .y)), then .x and .y should be scoped + (selector.name !== 'not' || + selector.args === null || + selector.args.children.every((c) => c.children.length === 1))) || + // selectors with has/is/where/not can also be global if all their children are global + selector.args === null || + selector.args.children.every((c) => c.children.every((r) => is_global(r)))) + ); +} + /** * True if is `:global(...)` or `:global`, irrespective of whether or not there are any pseudo classes that are scoped. * Difference to `is_global`: `:global(x):has(y)` is `true` for `is_outer_global` but `false` for `is_global`. @@ -97,7 +107,7 @@ export function is_outer_global(relative_selector) { first.type === 'PseudoClassSelector' && first.name === 'global' && (first.args === null || - // Only these two selector types keep the whole selector global, because e.g. + // Only these two selector types can keep the whole selector global, because e.g. // :global(button).x means that the selector is still scoped because of the .x relative_selector.selectors.every( (selector) => diff --git a/packages/svelte/tests/css/samples/has/_config.js b/packages/svelte/tests/css/samples/has/_config.js index 45311e5799ae..e5dc5f3459b9 100644 --- a/packages/svelte/tests/css/samples/has/_config.js +++ b/packages/svelte/tests/css/samples/has/_config.js @@ -169,6 +169,20 @@ export default test({ column: 15, character: 1422 } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector "&:has(.unused)"', + start: { + line: 135, + column: 2, + character: 1480 + }, + end: { + line: 135, + column: 16, + character: 1494 + } } ] }); diff --git a/packages/svelte/tests/css/samples/has/expected.css b/packages/svelte/tests/css/samples/has/expected.css index f29b55686072..8eda676f9385 100644 --- a/packages/svelte/tests/css/samples/has/expected.css +++ b/packages/svelte/tests/css/samples/has/expected.css @@ -122,7 +122,7 @@ &:has(x.svelte-xyz) { color: green; } - &:has(/* (unused) .unused*/) { + /* (unused) &:has(.unused) { color: red; - } + }*/ } From cca75841ceb1f782a7775d750ae5a4d69dbacba6 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 11 Nov 2024 13:18:57 +0100 Subject: [PATCH 7/8] oops --- packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js index 3add76a6858e..38551f328f37 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js @@ -107,7 +107,6 @@ const css_visitors = { ); // mark `&:hover` in `:global(.foo) { &:hover { color: green }}` as used if (no_nesting_scope && parent_is_global) { - // here? node.metadata.used = true; } } From 261d6f12a0375416e156e695cb02b25dcd940deb Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 11 Nov 2024 15:05:23 +0100 Subject: [PATCH 8/8] coindicentally this also fixes #14189 --- .changeset/great-bulldogs-wonder.md | 5 +++++ .../tests/css/samples/not-selector-global/expected.css | 9 +++++++++ .../tests/css/samples/not-selector-global/input.svelte | 9 +++++++++ 3 files changed, 23 insertions(+) create mode 100644 .changeset/great-bulldogs-wonder.md diff --git a/.changeset/great-bulldogs-wonder.md b/.changeset/great-bulldogs-wonder.md new file mode 100644 index 000000000000..b6cafa45852f --- /dev/null +++ b/.changeset/great-bulldogs-wonder.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent nested pseudo class from being marked as unused diff --git a/packages/svelte/tests/css/samples/not-selector-global/expected.css b/packages/svelte/tests/css/samples/not-selector-global/expected.css index ea9ff3948653..b815dcf9aa1a 100644 --- a/packages/svelte/tests/css/samples/not-selector-global/expected.css +++ b/packages/svelte/tests/css/samples/not-selector-global/expected.css @@ -27,3 +27,12 @@ span:not(p span) { color: green; } + + .x { + .svelte-xyz:not(.foo) { + color: green; + } + &:not(.foo) { + color: green; + } + } diff --git a/packages/svelte/tests/css/samples/not-selector-global/input.svelte b/packages/svelte/tests/css/samples/not-selector-global/input.svelte index 1f0e6cd6db1d..1d2f7a9bca53 100644 --- a/packages/svelte/tests/css/samples/not-selector-global/input.svelte +++ b/packages/svelte/tests/css/samples/not-selector-global/input.svelte @@ -34,4 +34,13 @@ :global(span:not(p span)) { color: green; } + + :global(.x) { + :not(.foo) { + color: green; + } + &:not(.foo) { + color: green; + } + } \ No newline at end of file