diff --git a/.changeset/giant-plants-grin.md b/.changeset/giant-plants-grin.md new file mode 100644 index 000000000000..4f5db1712ac8 --- /dev/null +++ b/.changeset/giant-plants-grin.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: keep sibling selectors when dealing with slots/render tags/`svelte:element` tags 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 c299612fd140..a5010c543a90 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 @@ -175,7 +175,13 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { let sibling_matched = false; for (const possible_sibling of siblings.keys()) { - if (apply_selector(parent_selectors, rule, possible_sibling, stylesheet)) { + 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)) { mark(relative_selector, element); sibling_matched = true; } @@ -564,38 +570,39 @@ function get_element_parent(node) { function find_previous_sibling(node) { /** @type {import('#compiler').SvelteNode} */ let current_node = node; - do { - if (current_node.type === 'SlotElement') { - const slot_children = current_node.fragment.nodes; - if (slot_children.length > 0) { - current_node = slot_children.slice(-1)[0]; // go to its last child first - continue; - } - } - while ( - // @ts-expect-error TODO - !current_node.prev && - // @ts-expect-error TODO - current_node.parent && - // @ts-expect-error TODO - current_node.parent.type === 'SlotElement' - ) { - // @ts-expect-error TODO - current_node = current_node.parent; + + while ( + // @ts-expect-error TODO + !current_node.prev && + // @ts-expect-error TODO + current_node.parent?.type === 'SlotElement' + ) { + // @ts-expect-error TODO + current_node = current_node.parent; + } + + // @ts-expect-error + current_node = current_node.prev; + + while (current_node?.type === 'SlotElement') { + const slot_children = current_node.fragment.nodes; + if (slot_children.length > 0) { + current_node = slot_children.slice(-1)[0]; + } else { + break; } - // @ts-expect-error - current_node = current_node.prev; - } while (current_node && current_node.type === 'SlotElement'); + } + return current_node; } /** * @param {import('#compiler').SvelteNode} node * @param {boolean} adjacent_only - * @returns {Map} + * @returns {Map} */ function get_possible_element_siblings(node, adjacent_only) { - /** @type {Map} */ + /** @type {Map} */ const result = new Map(); /** @type {import('#compiler').SvelteNode} */ @@ -618,6 +625,14 @@ function get_possible_element_siblings(node, adjacent_only) { if (adjacent_only && has_definite_elements(possible_last_child)) { return result; } + } else if ( + prev.type === 'SlotElement' || + prev.type === 'RenderTag' || + prev.type === 'SvelteElement' + ) { + result.set(prev, NODE_PROBABLY_EXISTS); + // Special case: slots, render tags and svelte:element tags could resolve to no siblings, + // so we want to continue until we find a definite sibling even with the adjacent-only combinator } } @@ -720,7 +735,7 @@ function get_possible_last_child(relative_selector, adjacent_only) { } /** - * @param {Map} result + * @param {Map} result * @returns {boolean} */ function has_definite_elements(result) { @@ -734,8 +749,9 @@ function has_definite_elements(result) { } /** - * @param {Map} from - * @param {Map} to + * @template T + * @param {Map} from + * @param {Map} to * @returns {void} */ function add_to_map(from, to) { diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/_config.js b/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/_config.js new file mode 100644 index 000000000000..2143e5e57555 --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/_config.js @@ -0,0 +1,13 @@ +import { test } from '../../test'; + +export default test({ + warnings: [ + // TODO + // { + // code: 'css-unused-selector', + // message: 'Unused CSS selector ".a ~ .b"', + // start: { character: 111, column: 1, line: 10 }, + // end: { character: 118, column: 8, line: 10 } + // }, + ] +}); diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/expected.css b/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/expected.css new file mode 100644 index 000000000000..5495a803ef7c --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/expected.css @@ -0,0 +1,13 @@ + + .before.svelte-xyz + .foo:where(.svelte-xyz) { color: green; } + .before.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; } + .before.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; } + + .x + .foo.svelte-xyz { color: green; } + .x + .foo.svelte-xyz span:where(.svelte-xyz) { color: green; } + .x ~ .foo.svelte-xyz { color: green; } + .x ~ .foo.svelte-xyz span:where(.svelte-xyz) { color: green; } + .x ~ .bar.svelte-xyz { color: green; } + + /* no match */ + /* (unused) :global(.x) + .bar { color: green; }*/ diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/input.svelte b/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/input.svelte new file mode 100644 index 000000000000..1e2c6fdc2dd6 --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/input.svelte @@ -0,0 +1,23 @@ +
+

before

+ {@render children()} +

+ foo +

+

bar

+
+ + diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/_config.js b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/_config.js new file mode 100644 index 000000000000..2143e5e57555 --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/_config.js @@ -0,0 +1,13 @@ +import { test } from '../../test'; + +export default test({ + warnings: [ + // TODO + // { + // code: 'css-unused-selector', + // message: 'Unused CSS selector ".a ~ .b"', + // start: { character: 111, column: 1, line: 10 }, + // end: { character: 118, column: 8, line: 10 } + // }, + ] +}); diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/expected.css b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/expected.css new file mode 100644 index 000000000000..5495a803ef7c --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/expected.css @@ -0,0 +1,13 @@ + + .before.svelte-xyz + .foo:where(.svelte-xyz) { color: green; } + .before.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; } + .before.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; } + + .x + .foo.svelte-xyz { color: green; } + .x + .foo.svelte-xyz span:where(.svelte-xyz) { color: green; } + .x ~ .foo.svelte-xyz { color: green; } + .x ~ .foo.svelte-xyz span:where(.svelte-xyz) { color: green; } + .x ~ .bar.svelte-xyz { color: green; } + + /* no match */ + /* (unused) :global(.x) + .bar { color: green; }*/ diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/input.svelte b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/input.svelte new file mode 100644 index 000000000000..d556ea6b8b11 --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/input.svelte @@ -0,0 +1,23 @@ +
+

before

+ +

+ foo +

+

bar

+
+ + diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/_config.js b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/_config.js new file mode 100644 index 000000000000..2143e5e57555 --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/_config.js @@ -0,0 +1,13 @@ +import { test } from '../../test'; + +export default test({ + warnings: [ + // TODO + // { + // code: 'css-unused-selector', + // message: 'Unused CSS selector ".a ~ .b"', + // start: { character: 111, column: 1, line: 10 }, + // end: { character: 118, column: 8, line: 10 } + // }, + ] +}); diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/expected.css b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/expected.css new file mode 100644 index 000000000000..830d3667024b --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/expected.css @@ -0,0 +1,13 @@ + + .before.svelte-xyz + .foo:where(.svelte-xyz) { color: green; } + .before.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; } + .before.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; } + + .x.svelte-xyz + .foo:where(.svelte-xyz) { color: green; } + .x.svelte-xyz + .foo:where(.svelte-xyz) span:where(.svelte-xyz) { color: green; } + .x.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; } + .x.svelte-xyz ~ .foo:where(.svelte-xyz) span:where(.svelte-xyz) { color: green; } + .x.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; } + + /* no match */ + /* (unused) .x + .bar { color: green; }*/ diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/input.svelte b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/input.svelte new file mode 100644 index 000000000000..1c51a2c516a1 --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/input.svelte @@ -0,0 +1,27 @@ + + +
+

before

+ +

+ foo +

+

bar

+
+ +