Repository: sveltejs/svelte (JavaScript)

Issue:
CSS + combinator not applied to svelte:elements in #each loop
[Reproduction in Svelte REPL Playground v5.16.1](https://svelte.dev/playground/40132de4874a41d99556dbecc84960cf?version=5.16.1)

## Describe the problem

When using `#each loop`, CSS `+` combinator is not being recognized as used (and thus it's absent in generated CSS):

```svelte
<script>
  let list = [1, 2 ,3]
</script>

{#each list as value}
  <svelte:element this={'p'}>Style not applied</svelte:element>
{/each}

<style>
  p + p {
    color: red;
  }
</style>
```
```
"code": "css_unused_selector"
"message": "Unused CSS selector "p+p" https://svelte.dev/e/css_unused_selector"
```

But when multiple `svelte:element` are defined one at a time, it works:

```svelte
<svelte:element this={'p'} />
<svelte:element this={'p'}>Style applied</svelte:element>

<style>
  p + p {
    color: red;
  }
</style>
```

If I make it `p + p + p` in the example above, it shows `css_unused_selector` too, because it knows there are only 2 unknown elements. Adding a 3rd one makes it happy once again:

```svelte
<svelte:element this={'p'} />
<svelte:element this={'p'} />
<svelte:element this={'p'}>Style applied</svelte:element>

<style>
  p + p + p {
    color: red;
  }
</style>
```

Which means that based on multiple defined `svelte:element`, Svelte seems to be able to recognize that the selector might be applied, so it has to be present in generated CSS.

### Why not use `:global` style?

Two reasons:

1. Because of how CSS specificity works, `:global(p + p)` is not applied when there is a style already defined for `p` in the component:

```svelte
<svelte:element this={'p'}>Both elements</svelte:element>
<svelte:element this={'p'}>are red</svelte:element>

<style>
  p {
    color: red;
  }

  :global(p + p) {
    /* not applied because of CSS specificity */
    color: green;
  }
</style>
```

It would require some additional tweaking to make it work, like adding some classes, which seems messy, clumsy and unnecessary.

2. For my use case, this style belongs to the specific component. It doesn't make sense to make it `:global` when it is not used anywhere else.

### Possible solution

Svelte's understanding of the dynamicity of `svelte:element` doesn't seem to apply to `#each` loops. Possibly it's the unpredictability of the number of rendered elements that causes this problem.

One way to handle it would be to disable `css_unused_selector` altogether when there is `svelte:element` in `#each` loop. Possibilities are endless in such case, both in terms of what kind of elements and how many of them will appear. So to me it would make sense allow basically any CSS combinators with any number of elements.

### Workaround until it is fixed

Adding multiple `svelte:element` (even as unreachable code, like below) does the trick:

```svelte
{#if false}
  <svelte:element this={''} />
  <svelte:element this={''} />
{/if}
```

To make it work for `p + p + p` there would have to be 3 of them and so on, so it is obviously not ideal. But at least it's just an additional piece of code that can be easily removed later, when the issue is fixed, without adjusting anything else in the actual code.

There is another thing that I described in the playground where when **another** component has **exactly** the same style, it suddenly makes it work in the component with `p + p` combinator with `svelte:element` in `#each` loop. I have already spent much more time than anticipated on describing this though, and maybe it should be placed in another issue anyway, so that's it for now.

### Reproduction

[Reproduction in Svelte REPL Playground v5.16.1](https://svelte.dev/playground/40132de4874a41d99556dbecc84960cf?version=5.16.1)

### System Info

```shell
Svelte REPL Playground
Version 5.16.1
```

### Severity

annoyance


Discussion:

Patch:
diff --git a/.changeset/sweet-mails-clean.md b/.changeset/sweet-mails-clean.md
new file mode 100644
index 000000000000..f82913abf786
--- /dev/null
+++ b/.changeset/sweet-mails-clean.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: encounter svelte:element in blocks as sibling during pruning css
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 e719895798e1..fc8108e46e8e 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
@@ -933,11 +933,9 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
 /**
  * @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement} node
  * @param {boolean} adjacent_only
- * @returns {Map<Compiler.AST.RegularElement, NodeExistsValue>}
+ * @returns {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement, NodeExistsValue>}
  */
 function get_possible_last_child(node, adjacent_only) {
-	/** @typedef {Map<Compiler.AST.RegularElement, NodeExistsValue>} NodeMap */
-
 	/** @type {Array<Compiler.AST.Fragment | undefined | null>} */
 	let fragments = [];
 
@@ -960,7 +958,7 @@ function get_possible_last_child(node, adjacent_only) {
 			break;
 	}
 
-	/** @type {NodeMap} */
+	/** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement, NodeExistsValue>} NodeMap */
 	const result = new Map();
 
 	let exhaustive = node.type !== 'SlotElement';
@@ -1001,9 +999,10 @@ function has_definite_elements(result) {
 }
 
 /**
- * @template T
- * @param {Map<T, NodeExistsValue>} from
- * @param {Map<T, NodeExistsValue>} to
+ * @template T2
+ * @template {T2} T1
+ * @param {Map<T1, NodeExistsValue>} from
+ * @param {Map<T2, NodeExistsValue>} to
  * @returns {void}
  */
 function add_to_map(from, to) {
@@ -1028,7 +1027,7 @@ function higher_existence(exist1, exist2) {
  * @param {boolean} adjacent_only
  */
 function loop_child(children, adjacent_only) {
-	/** @type {Map<Compiler.AST.RegularElement, NodeExistsValue>} */
+	/** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement, NodeExistsValue>} */
 	const result = new Map();
 
 	let i = children.length;
@@ -1041,6 +1040,8 @@ function loop_child(children, adjacent_only) {
 			if (adjacent_only) {
 				break;
 			}
+		} else if (child.type === 'SvelteElement') {
+			result.set(child, NODE_PROBABLY_EXISTS);
 		} else if (is_block(child)) {
 			const child_result = get_possible_last_child(child, adjacent_only);
 			add_to_map(child_result, result);


Test patch:
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
index 7c50a76bbbd8..a7d6c3a99d3d 100644
--- a/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/_config.js
+++ b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/_config.js
@@ -5,15 +5,15 @@ export default test({
 		{
 			code: 'css_unused_selector',
 			end: {
-				character: 496,
+				character: 627,
 				column: 10,
-				line: 26
+				line: 32
 			},
 			message: 'Unused CSS selector ".x + .bar"',
 			start: {
-				character: 487,
+				character: 618,
 				column: 1,
-				line: 26
+				line: 32
 			}
 		}
 	]
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
index 830d3667024b..d3fa8c97d2e1 100644
--- a/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/expected.css
+++ b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/expected.css
@@ -9,5 +9,8 @@
 	.x.svelte-xyz ~ .foo:where(.svelte-xyz) span:where(.svelte-xyz) { color: green; }
 	.x.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; }
 
+	.z.svelte-xyz + .z:where(.svelte-xyz) { color: green; }
+	.z.svelte-xyz ~ .z: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
index 1c51a2c516a1..655fd861539a 100644
--- a/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/input.svelte
+++ b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/input.svelte
@@ -10,6 +10,9 @@
 	</p>
 	<p class="bar">bar</p>
 </div>
+{#each [1]}
+	<svelte:element class="z" this={tag}></svelte:element>
+{/each}
 
 <style>
 	.before + .foo { color: green; }
@@ -22,6 +25,9 @@
 	.x ~ .foo span { color: green; }
 	.x ~ .bar { color: green; }
 
+	.z + .z { color: green; }
+	.z ~ .z { color: green; }
+
 	/* no match */
 	.x + .bar { color: green; }
 </style>