From 08b3a74e9a2772b09aa856161552ae8b87dd8b3d Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 11 Oct 2024 11:12:37 +0200 Subject: [PATCH 1/4] breaking: scope `:has(...)` selectors The main part of #13395 This implements scoping for selectors inside `:has(...)`. The approach is to first descend into the contents of a `:has(...)` selector, then in case of a match, try to match the rest of the selector ignoring the `:has(...)` part. In other words, `.x:has(y)` is essentially treated as `x y` with `y` being matched first, then walking up the selector chain taking into account combinators. This is a breaking change because people could've used `:has(.unknown)` with `.unknown` not appearing in the HTML, and so they need to do `:has(:global(.unknown))` instead --- .changeset/silly-houses-promise.md | 5 + .../phases/2-analyze/css/css-prune.js | 250 +++++++++++++----- .../compiler/phases/3-transform/css/index.js | 2 +- packages/svelte/src/compiler/types/css.d.ts | 1 + .../svelte/tests/css/samples/has/_config.js | 90 +++++++ .../svelte/tests/css/samples/has/expected.css | 77 ++++++ .../svelte/tests/css/samples/has/input.svelte | 84 ++++++ .../03-appendix/02-breaking-changes.md | 4 +- 8 files changed, 449 insertions(+), 64 deletions(-) create mode 100644 .changeset/silly-houses-promise.md create mode 100644 packages/svelte/tests/css/samples/has/_config.js create mode 100644 packages/svelte/tests/css/samples/has/expected.css create mode 100644 packages/svelte/tests/css/samples/has/input.svelte diff --git a/.changeset/silly-houses-promise.md b/.changeset/silly-houses-promise.md new file mode 100644 index 000000000000..a993df0ff362 --- /dev/null +++ b/.changeset/silly-houses-promise.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +breaking: scope `:has(...)` selectors 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 1896c83d1a25..d42e0b97b598 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 @@ -106,7 +106,8 @@ const visitors = { selectors, /** @type {Compiler.Css.Rule} */ (node.metadata.rule), context.state.element, - context.state.stylesheet + context.state.stylesheet, + true ) ) { mark(inner, context.state.element); @@ -120,12 +121,18 @@ const visitors = { }; /** - * Discard trailing `:global(...)` selectors, these are unused for scoping purposes + * Discard trailing `:global(...)` selectors without a `:has(...)` modifier, these are unused for scoping purposes * @param {Compiler.Css.ComplexSelector} node */ function truncate(node) { - const i = node.children.findLastIndex(({ metadata }) => { - return !metadata.is_global && !metadata.is_global_like; + const i = node.children.findLastIndex(({ metadata, selectors }) => { + return ( + !metadata.is_global_like && + (!metadata.is_global || + selectors.some( + (selector) => selector.type === 'PseudoClassSelector' && selector.name === 'has' + )) + ); }); return node.children.slice(0, i + 1); @@ -136,9 +143,10 @@ function truncate(node) { * @param {Compiler.Css.Rule} rule * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element * @param {Compiler.Css.StyleSheet} stylesheet + * @param {boolean} separate_has Whether or not to separate the `:has(...)` selectors * @returns {boolean} */ -function apply_selector(relative_selectors, rule, element, stylesheet) { +function apply_selector(relative_selectors, rule, element, stylesheet, separate_has) { const parent_selectors = relative_selectors.slice(); const relative_selector = parent_selectors.pop(); @@ -148,7 +156,8 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { relative_selector, rule, element, - stylesheet + stylesheet, + separate_has ); if (!possible_match) { @@ -156,80 +165,112 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { } if (relative_selector.combinator) { - const name = relative_selector.combinator.name; - - switch (name) { - case ' ': - case '>': { - let parent = /** @type {Compiler.TemplateNode | null} */ (element.parent); + return apply_combinator( + relative_selector.combinator, + relative_selector, + parent_selectors, + rule, + element, + stylesheet, + separate_has + ); + } - let parent_matched = false; - let crossed_component_boundary = false; + // if this is the left-most non-global selector, mark it — we want + // `x y z {...}` to become `x.blah y z.blah {...}` + const parent = parent_selectors[parent_selectors.length - 1]; + if (!parent || is_global(parent, rule)) { + mark(relative_selector, element); + } - while (parent) { - if (parent.type === 'Component' || parent.type === 'SvelteComponent') { - crossed_component_boundary = true; - } + return true; +} - if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') { - if (apply_selector(parent_selectors, rule, parent, stylesheet)) { - // TODO the `name === ' '` causes false positives, but removing it causes false negatives... - if (name === ' ' || crossed_component_boundary) { - mark(parent_selectors[parent_selectors.length - 1], parent); - } +/** + * @param {Compiler.Css.Combinator} combinator + * @param {Compiler.Css.RelativeSelector} relative_selector + * @param {Compiler.Css.RelativeSelector[]} parent_selectors + * @param {Compiler.Css.Rule} rule + * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element + * @param {Compiler.Css.StyleSheet} stylesheet + * @param {boolean} separate_has + * @returns {boolean} + */ +function apply_combinator( + combinator, + relative_selector, + parent_selectors, + rule, + element, + stylesheet, + separate_has +) { + const name = combinator.name; + + switch (name) { + case ' ': + case '>': { + let parent = /** @type {Compiler.TemplateNode | null} */ (element.parent); + + let parent_matched = false; + let crossed_component_boundary = false; + + while (parent) { + if (parent.type === 'Component' || parent.type === 'SvelteComponent') { + crossed_component_boundary = true; + } - parent_matched = true; + if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') { + if (apply_selector(parent_selectors, rule, parent, stylesheet, separate_has)) { + // TODO the `name === ' '` causes false positives, but removing it causes false negatives... + if (name === ' ' || crossed_component_boundary) { + mark(parent_selectors[parent_selectors.length - 1], parent); } - if (name === '>') return parent_matched; + parent_matched = true; } - parent = /** @type {Compiler.TemplateNode | null} */ (parent.parent); + if (name === '>') return parent_matched; } - return parent_matched || parent_selectors.every((selector) => is_global(selector, rule)); + parent = /** @type {Compiler.TemplateNode | null} */ (parent.parent); } - case '+': - case '~': { - const siblings = get_possible_element_siblings(element, name === '+'); + return parent_matched || parent_selectors.every((selector) => is_global(selector, rule)); + } + + case '+': + case '~': { + const siblings = get_possible_element_siblings(element, name === '+'); - let sibling_matched = false; + let sibling_matched = false; - for (const possible_sibling of siblings.keys()) { - if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') { - // `{@render foo()}

foo

` with `:global(.x) + p` is a match - if (parent_selectors.length === 1 && parent_selectors[0].metadata.is_global) { - mark(relative_selector, element); - sibling_matched = true; - } - } else if (apply_selector(parent_selectors, rule, possible_sibling, stylesheet)) { + for (const possible_sibling of siblings.keys()) { + if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') { + // `{@render foo()}

foo

` with `:global(.x) + p` is a match + if (parent_selectors.length === 1 && parent_selectors[0].metadata.is_global) { mark(relative_selector, element); sibling_matched = true; } + } else if ( + apply_selector(parent_selectors, rule, possible_sibling, stylesheet, separate_has) + ) { + mark(relative_selector, element); + sibling_matched = true; } - - return ( - sibling_matched || - (get_element_parent(element) === null && - parent_selectors.every((selector) => is_global(selector, rule))) - ); } - default: - // TODO other combinators - return true; + return ( + sibling_matched || + (get_element_parent(element) === null && + parent_selectors.every((selector) => is_global(selector, rule))) + ); } - } - // if this is the left-most non-global selector, mark it — we want - // `x y z {...}` to become `x.blah y z.blah {...}` - const parent = parent_selectors[parent_selectors.length - 1]; - if (!parent || is_global(parent, rule)) { - mark(relative_selector, element); + default: + // TODO other combinators + return true; } - - return true; } /** @@ -295,10 +336,93 @@ const regex_backslash_and_following_character = /\\(.)/g; * @param {Compiler.Css.Rule} rule * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element * @param {Compiler.Css.StyleSheet} stylesheet + * @param {boolean} separate_has Whether or not to separate the `:has(...)` selectors * @returns {boolean} */ -function relative_selector_might_apply_to_node(relative_selector, rule, element, stylesheet) { +function relative_selector_might_apply_to_node( + relative_selector, + rule, + element, + stylesheet, + separate_has +) { + // Sort :has(...) selectors in one bucket and everything else into another, + // unless we're called recursively from a :has(...) selector, in which case + // we're on the way of checking if the upper selectors match. In that + // case ignore them to avoid an infinite loop. + const has_selectors = []; + const other_selectors = []; + for (const selector of relative_selector.selectors) { + if ( + separate_has && + selector.type === 'PseudoClassSelector' && + selector.name === 'has' && + selector.args + ) { + has_selectors.push(selector); + } else { + other_selectors.push(selector); + } + } + + if (has_selectors.length > 0) { + // :has(...) is special in that it means "look downwards in the CSS tree". Since our matching algorithm goes + // upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the + // selector in a way that is similar to ancestor matching. In a sense, we're treating `.x:has(.y)` as `.x .y`. + for (const has_selector of has_selectors) { + const complex_selectors = /** @type {Compiler.Css.SelectorList} */ (has_selector.args) + .children; + let matched = false; + + for (const complex_selector of complex_selectors) { + const selectors = truncate(complex_selector); + if ( + selectors.length === 0 /* is :global(...) */ || + apply_selector(selectors, rule, element, stylesheet, separate_has) + ) { + // Treat e.g. `.x:has(.y)` as `.x .y` with the .y part already being matched, + // and now looking upwards for the .x part. + if ( + apply_combinator( + selectors[0]?.combinator ?? descendant_combinator, + selectors[0] ?? [], + [relative_selector], + rule, + element, + stylesheet, + false + ) + ) { + complex_selector.metadata.used = true; + matched = true; + } + } + } + + 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; + } + } + + return true; + } + + for (const selector of other_selectors) { if (selector.type === 'Percentage' || selector.type === 'Nth') continue; const name = selector.name.replace(regex_backslash_and_following_character, '$1'); @@ -316,7 +440,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, ) { const args = selector.args; const complex_selector = args.children[0]; - return apply_selector(complex_selector.children, rule, element, stylesheet); + return apply_selector(complex_selector.children, rule, element, stylesheet, separate_has); } // We came across a :global, everything beyond it is global and therefore a potential match @@ -326,7 +450,9 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, let matched = false; for (const complex_selector of selector.args.children) { - if (apply_selector(truncate(complex_selector), rule, element, stylesheet)) { + if ( + apply_selector(truncate(complex_selector), rule, element, stylesheet, separate_has) + ) { complex_selector.metadata.used = true; matched = true; } @@ -400,7 +526,9 @@ 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(truncate(complex_selector), parent, element, stylesheet)) { + if ( + apply_selector(truncate(complex_selector), parent, element, stylesheet, separate_has) + ) { complex_selector.metadata.used = true; matched = true; } 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 f24baed32b07..31a8f472b2bb 100644 --- a/packages/svelte/src/compiler/phases/3-transform/css/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/css/index.js @@ -311,7 +311,7 @@ const visitors = { context.state.specificity.bumped = before_bumped; }, PseudoClassSelector(node, context) { - if (node.name === 'is' || node.name === 'where') { + if (node.name === 'is' || node.name === 'where' || node.name === 'has') { context.next(); } } diff --git a/packages/svelte/src/compiler/types/css.d.ts b/packages/svelte/src/compiler/types/css.d.ts index e2346e697def..3b01b0f243f3 100644 --- a/packages/svelte/src/compiler/types/css.d.ts +++ b/packages/svelte/src/compiler/types/css.d.ts @@ -62,6 +62,7 @@ export namespace Css { children: RelativeSelector[]; metadata: { rule: null | Rule; + /** True if this selector applies to an element. For global selectors, this is defined in css-analyze, for others in css-prune while scoping */ used: boolean; }; } diff --git a/packages/svelte/tests/css/samples/has/_config.js b/packages/svelte/tests/css/samples/has/_config.js new file mode 100644 index 000000000000..403a7a46c9c6 --- /dev/null +++ b/packages/svelte/tests/css/samples/has/_config.js @@ -0,0 +1,90 @@ +import { test } from '../../test'; + +export default test({ + warnings: [ + { + code: 'css_unused_selector', + message: 'Unused CSS selector ".unused:has(y)"', + start: { + character: 269, + column: 1, + line: 27 + }, + end: { + character: 283, + column: 15, + line: 27 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector ".unused:has(:global(y))"', + start: { + character: 304, + column: 1, + line: 30 + }, + end: { + character: 327, + column: 24, + line: 30 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector "x:has(.unused)"', + start: { + character: 348, + column: 1, + line: 33 + }, + end: { + character: 362, + column: 15, + line: 33 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector "x:has(y):has(.unused)"', + start: { + character: 517, + column: 1, + line: 46 + }, + end: { + character: 538, + column: 22, + line: 46 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector ".unused"', + start: { + character: 743, + column: 2, + line: 65 + }, + end: { + character: 750, + column: 9, + line: 65 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector ".unused x:has(y)"', + start: { + character: 897, + column: 1, + line: 81 + }, + end: { + character: 913, + column: 17, + line: 81 + } + } + ] +}); diff --git a/packages/svelte/tests/css/samples/has/expected.css b/packages/svelte/tests/css/samples/has/expected.css new file mode 100644 index 000000000000..51824840f15b --- /dev/null +++ b/packages/svelte/tests/css/samples/has/expected.css @@ -0,0 +1,77 @@ + + x.svelte-xyz:has(y:where(.svelte-xyz)) { + color: green; + } + x.svelte-xyz:has(z:where(.svelte-xyz)) { + color: green; + } + x.svelte-xyz:has(y) { + color: green; + } + x.svelte-xyz:has(z) { + color: green; + } + x.svelte-xyz:has(.foo) { + color: green; + } + .foo:has(y.svelte-xyz) { + color: green; + } + + /* (unused) .unused:has(y) { + color: red; + }*/ + /* (unused) .unused:has(:global(y)) { + color: red; + }*/ + /* (unused) x:has(.unused) { + color: red; + }*/ + .foo:has(.unused.svelte-xyz) { + color: red; + } + + x.svelte-xyz:has(y:where(.svelte-xyz) /* (unused) .unused*/) { + color: green; + } + x.svelte-xyz:has(y:where(.svelte-xyz), .foo) { + color: green; + } + /* (unused) x:has(y):has(.unused) { + color: red; + }*/ + x.svelte-xyz:has(y:where(.svelte-xyz)):has(.foo) { + color: green; + } + + x.svelte-xyz:has(y:where(.svelte-xyz)) z:where(.svelte-xyz) { + color: green; + } + x.svelte-xyz:has(y:where(.svelte-xyz)) { + z:where(.svelte-xyz) { + color: green; + } + } + x.svelte-xyz:has(y:where(.svelte-xyz)) .foo { + color: green; + } + /* (empty) x:has(y) { + .unused { + color: red; + } + }*/ + + x.svelte-xyz y:where(.svelte-xyz):has(z:where(.svelte-xyz)) { + color: green; + } + x.svelte-xyz { + y:where(.svelte-xyz):has(z:where(.svelte-xyz)) { + color: green; + } + } + .foo x.svelte-xyz:has(y:where(.svelte-xyz)) { + color: green; + } + /* (unused) .unused x:has(y) { + color: red; + }*/ diff --git a/packages/svelte/tests/css/samples/has/input.svelte b/packages/svelte/tests/css/samples/has/input.svelte new file mode 100644 index 000000000000..05064c650897 --- /dev/null +++ b/packages/svelte/tests/css/samples/has/input.svelte @@ -0,0 +1,84 @@ + + + + + + + diff --git a/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md b/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md index 1802966b8389..eff6a7c7438c 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md +++ b/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md @@ -293,9 +293,9 @@ Svelte 5 is more strict about the HTML structure and will throw a compiler error Assignments to destructured parts of a `@const` declaration are no longer allowed. It was an oversight that this was ever allowed. -### :is(...) and :where(...) are scoped +### :is(...), :where(...) and :has(...) are scoped -Previously, Svelte did not analyse selectors inside `:is(...)` and `:where(...)`, effectively treating them as global. Svelte 5 analyses them in the context of the current component. As such, some selectors may now be treated as unused if they were relying on this treatment. To fix this, use `:global(...)` inside the `:is(...)/:where(...)` selectors. +Previously, Svelte did not analyse selectors inside `:is(...)`, `:where(...)` and `:has(...)`, effectively treating them as global. Svelte 5 analyses them in the context of the current component. As such, some selectors may now be treated as unused if they were relying on this treatment. To fix this, use `:global(...)` inside the `:is(...)/:where(...)/:has(...)` selectors. When using Tailwind's `@apply` directive, add a `:global` selector to preserve rules that use Tailwind-generated `:is(...)` selectors: From 0d68454db878dc3f540e0fe018184db601db6e75 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 11 Oct 2024 11:21:35 +0200 Subject: [PATCH 2/4] tweak --- .../phases/2-analyze/css/css-prune.js | 48 ++++++++----------- 1 file changed, 19 insertions(+), 29 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 d42e0b97b598..c51de75fd626 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 @@ -143,10 +143,10 @@ function truncate(node) { * @param {Compiler.Css.Rule} rule * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element * @param {Compiler.Css.StyleSheet} stylesheet - * @param {boolean} separate_has Whether or not to separate the `:has(...)` selectors + * @param {boolean} check_has Whether or not to check the `:has(...)` selectors * @returns {boolean} */ -function apply_selector(relative_selectors, rule, element, stylesheet, separate_has) { +function apply_selector(relative_selectors, rule, element, stylesheet, check_has) { const parent_selectors = relative_selectors.slice(); const relative_selector = parent_selectors.pop(); @@ -157,7 +157,7 @@ function apply_selector(relative_selectors, rule, element, stylesheet, separate_ rule, element, stylesheet, - separate_has + check_has ); if (!possible_match) { @@ -172,7 +172,7 @@ function apply_selector(relative_selectors, rule, element, stylesheet, separate_ rule, element, stylesheet, - separate_has + check_has ); } @@ -193,7 +193,7 @@ function apply_selector(relative_selectors, rule, element, stylesheet, separate_ * @param {Compiler.Css.Rule} rule * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element * @param {Compiler.Css.StyleSheet} stylesheet - * @param {boolean} separate_has + * @param {boolean} check_has Whether or not to check the `:has(...)` selectors * @returns {boolean} */ function apply_combinator( @@ -203,7 +203,7 @@ function apply_combinator( rule, element, stylesheet, - separate_has + check_has ) { const name = combinator.name; @@ -221,7 +221,7 @@ function apply_combinator( } if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') { - if (apply_selector(parent_selectors, rule, parent, stylesheet, separate_has)) { + if (apply_selector(parent_selectors, rule, parent, stylesheet, check_has)) { // TODO the `name === ' '` causes false positives, but removing it causes false negatives... if (name === ' ' || crossed_component_boundary) { mark(parent_selectors[parent_selectors.length - 1], parent); @@ -253,7 +253,7 @@ function apply_combinator( sibling_matched = true; } } else if ( - apply_selector(parent_selectors, rule, possible_sibling, stylesheet, separate_has) + apply_selector(parent_selectors, rule, possible_sibling, stylesheet, check_has) ) { mark(relative_selector, element); sibling_matched = true; @@ -336,7 +336,7 @@ const regex_backslash_and_following_character = /\\(.)/g; * @param {Compiler.Css.Rule} rule * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element * @param {Compiler.Css.StyleSheet} stylesheet - * @param {boolean} separate_has Whether or not to separate the `:has(...)` selectors + * @param {boolean} check_has Whether or not to check the `:has(...)` selectors * @returns {boolean} */ function relative_selector_might_apply_to_node( @@ -344,29 +344,23 @@ function relative_selector_might_apply_to_node( rule, element, stylesheet, - separate_has + check_has ) { - // Sort :has(...) selectors in one bucket and everything else into another, - // unless we're called recursively from a :has(...) selector, in which case - // we're on the way of checking if the upper selectors match. In that - // case ignore them to avoid an infinite loop. + // Sort :has(...) selectors in one bucket and everything else into another const has_selectors = []; const other_selectors = []; for (const selector of relative_selector.selectors) { - if ( - separate_has && - selector.type === 'PseudoClassSelector' && - selector.name === 'has' && - selector.args - ) { + if (selector.type === 'PseudoClassSelector' && selector.name === 'has' && selector.args) { has_selectors.push(selector); } else { other_selectors.push(selector); } } - if (has_selectors.length > 0) { + // If we're called recursively from a :has(...) selector, we're on the way of checking if the other selectors match. + // In that case ignore this check (because we just came from this) to avoid an infinite loop. + if (check_has && has_selectors.length > 0) { // :has(...) is special in that it means "look downwards in the CSS tree". Since our matching algorithm goes // upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the // selector in a way that is similar to ancestor matching. In a sense, we're treating `.x:has(.y)` as `.x .y`. @@ -379,7 +373,7 @@ function relative_selector_might_apply_to_node( const selectors = truncate(complex_selector); if ( selectors.length === 0 /* is :global(...) */ || - apply_selector(selectors, rule, element, stylesheet, separate_has) + apply_selector(selectors, rule, element, stylesheet, check_has) ) { // Treat e.g. `.x:has(.y)` as `.x .y` with the .y part already being matched, // and now looking upwards for the .x part. @@ -440,7 +434,7 @@ function relative_selector_might_apply_to_node( ) { const args = selector.args; const complex_selector = args.children[0]; - return apply_selector(complex_selector.children, rule, element, stylesheet, separate_has); + return apply_selector(complex_selector.children, rule, element, stylesheet, check_has); } // We came across a :global, everything beyond it is global and therefore a potential match @@ -450,9 +444,7 @@ function relative_selector_might_apply_to_node( let matched = false; for (const complex_selector of selector.args.children) { - if ( - apply_selector(truncate(complex_selector), rule, element, stylesheet, separate_has) - ) { + if (apply_selector(truncate(complex_selector), rule, element, stylesheet, check_has)) { complex_selector.metadata.used = true; matched = true; } @@ -526,9 +518,7 @@ function relative_selector_might_apply_to_node( const parent = /** @type {Compiler.Css.Rule} */ (rule.metadata.parent_rule); for (const complex_selector of parent.prelude.children) { - if ( - apply_selector(truncate(complex_selector), parent, element, stylesheet, separate_has) - ) { + if (apply_selector(truncate(complex_selector), parent, element, stylesheet, check_has)) { complex_selector.metadata.used = true; matched = true; } From 3e7ddce3263550f071c6cc129d0040c7e3e8cdd1 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 11 Oct 2024 11:28:32 +0200 Subject: [PATCH 3/4] regenerate --- packages/svelte/types/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index e64abef0c5ca..a2671b76b9b8 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1331,6 +1331,7 @@ declare module 'svelte/compiler' { children: RelativeSelector[]; metadata: { rule: null | Rule; + /** True if this selector applies to an element. For global selectors, this is defined in css-analyze, for others in css-prune while scoping */ used: boolean; }; } From 3d436d691683504191a910a8d0f916638e79cfae Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 11 Oct 2024 11:29:55 +0200 Subject: [PATCH 4/4] while we're at it .. --- packages/svelte/src/compiler/types/css.d.ts | 3 +++ packages/svelte/types/index.d.ts | 23 --------------------- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/packages/svelte/src/compiler/types/css.d.ts b/packages/svelte/src/compiler/types/css.d.ts index 3b01b0f243f3..287e551aaa0b 100644 --- a/packages/svelte/src/compiler/types/css.d.ts +++ b/packages/svelte/src/compiler/types/css.d.ts @@ -30,6 +30,7 @@ export namespace Css { type: 'Rule'; prelude: SelectorList; block: Block; + /** @internal */ metadata: { parent_rule: null | Rule; has_local_selectors: boolean; @@ -60,6 +61,7 @@ export namespace Css { * The `a`, `b` and `c` in `a b c {}` */ children: RelativeSelector[]; + /** @internal */ metadata: { rule: null | Rule; /** True if this selector applies to an element. For global selectors, this is defined in css-analyze, for others in css-prune while scoping */ @@ -80,6 +82,7 @@ export namespace Css { * The `b:is(...)` in `> b:is(...)` */ selectors: SimpleSelector[]; + /** @internal */ metadata: { /** * `true` if the whole selector is unscoped, e.g. `:global(...)` or `:global` or `:global.x`. diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index a2671b76b9b8..4bb6a73a9867 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1299,14 +1299,6 @@ declare module 'svelte/compiler' { type: 'Rule'; prelude: SelectorList; block: Block; - metadata: { - parent_rule: null | Rule; - has_local_selectors: boolean; - /** - * `true` if the rule contains a `:global` selector, and therefore everything inside should be unscoped - */ - is_global_block: boolean; - }; } /** @@ -1329,11 +1321,6 @@ declare module 'svelte/compiler' { * The `a`, `b` and `c` in `a b c {}` */ children: RelativeSelector[]; - metadata: { - rule: null | Rule; - /** True if this selector applies to an element. For global selectors, this is defined in css-analyze, for others in css-prune while scoping */ - used: boolean; - }; } /** @@ -1349,16 +1336,6 @@ declare module 'svelte/compiler' { * The `b:is(...)` in `> b:is(...)` */ selectors: SimpleSelector[]; - metadata: { - /** - * `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. - */ - is_global: boolean; - /** `:root`, `:host`, `::view-transition`, or selectors after a `:global` */ - is_global_like: boolean; - scoped: boolean; - }; } export interface TypeSelector extends BaseNode {