diff --git a/.changeset/seven-masks-end.md b/.changeset/seven-masks-end.md new file mode 100644 index 0000000000..342091267d --- /dev/null +++ b/.changeset/seven-masks-end.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: adjust keyed each block equality handling diff --git a/.changeset/spotty-houses-search.md b/.changeset/spotty-houses-search.md new file mode 100644 index 0000000000..6876029c06 --- /dev/null +++ b/.changeset/spotty-houses-search.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: improve indexed each equality diff --git a/.changeset/wet-wombats-repeat.md b/.changeset/wet-wombats-repeat.md new file mode 100644 index 0000000000..339f9ffbfa --- /dev/null +++ b/.changeset/wet-wombats-repeat.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: prevent snippet children conflict diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 213b689d3e..c31ad4a038 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -296,7 +296,9 @@ const slots = { /** @param {string} name @param {string} component */ 'duplicate-slot-name': (name, component) => `Duplicate slot name '${name}' in <${component}>`, 'invalid-default-slot-content': () => - `Found default slot content alongside an explicit slot="default"` + `Found default slot content alongside an explicit slot="default"`, + 'conflicting-children-snippet': () => + `Cannot use explicit children snippet at the same time as implicit children content. Remove either the non-whitespace content or the children snippet block` }; /** @satisfies {Errors} */ diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index ddb0d1d584..6bcfd7bb9a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -521,6 +521,24 @@ const validation = { ); } }, + SnippetBlock(node, { path }) { + if (node.expression.name !== 'children') return; + const parent = path.at(-2); + if (!parent) return; + if ( + parent.type === 'Component' || + parent.type === 'SvelteComponent' || + parent.type === 'SvelteSelf' + ) { + if ( + parent.fragment.nodes.some( + (node) => node.type !== 'SnippetBlock' && (node.type !== 'Text' || node.data.trim()) + ) + ) { + error(node, 'conflicting-children-snippet'); + } + } + }, SvelteHead(node) { const attribute = node.attributes[0]; if (attribute) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 05dd308f8a..7c68026489 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -2393,7 +2393,7 @@ export const template_visitors = { const binding = /** @type {import('#compiler').Binding} */ (context.state.scope.get(item.name)); binding.expression = (id) => { const item_with_loc = with_loc(item, id); - return each_item_is_reactive ? b.call('$.unwrap', item_with_loc) : item_with_loc; + return b.call('$.unwrap', item_with_loc); }; if (node.index) { const index_binding = /** @type {import('#compiler').Binding} */ ( @@ -2401,7 +2401,7 @@ export const template_visitors = { ); index_binding.expression = (id) => { const index_with_loc = with_loc(index, id); - return each_item_is_reactive ? b.call('$.unwrap', index_with_loc) : index_with_loc; + return b.call('$.unwrap', index_with_loc); }; } diff --git a/packages/svelte/src/compiler/phases/3-transform/utils.js b/packages/svelte/src/compiler/phases/3-transform/utils.js index 3429748e29..de163612a9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/utils.js @@ -1,8 +1,7 @@ import { regex_ends_with_whitespaces, regex_not_whitespace, - regex_starts_with_whitespaces, - regex_whitespaces_strict + regex_starts_with_whitespaces } from '../patterns.js'; import * as b from '../../utils/builders.js'; import { walk } from 'zimmerframe'; diff --git a/packages/svelte/tests/compiler-errors/samples/snippet-children-conflict/_config.js b/packages/svelte/tests/compiler-errors/samples/snippet-children-conflict/_config.js new file mode 100644 index 0000000000..3fd2caaeda --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/snippet-children-conflict/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'conflicting-children-snippet', + message: + 'Cannot use explicit children snippet at the same time as implicit children content. Remove either the non-whitespace content or the children snippet block', + position: [320, 353] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/snippet-children-conflict/main.svelte b/packages/svelte/tests/compiler-errors/samples/snippet-children-conflict/main.svelte new file mode 100644 index 0000000000..fcc55f3b9c --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/snippet-children-conflict/main.svelte @@ -0,0 +1,25 @@ + + + + + +
+ hello + {#snippet children()}hi{/snippet} +
+ + + diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-4/_config.js b/packages/svelte/tests/runtime-runes/samples/each-updates-4/_config.js new file mode 100644 index 0000000000..284f036dc4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-4/_config.js @@ -0,0 +1,21 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `100\n`, + + async test({ assert, target }) { + /** + * @type {{ click: () => void; }} + */ + let btn1; + + [btn1] = target.querySelectorAll('button'); + + flushSync(() => { + btn1.click(); + }); + + assert.htmlEqual(target.innerHTML, `1000\n`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-4/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-updates-4/main.svelte new file mode 100644 index 0000000000..f48f711976 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-4/main.svelte @@ -0,0 +1,13 @@ + + +{#each Object.values($roomState.users) as user (user.name)} + {user.value} +{/each} + + diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-5/_config.js b/packages/svelte/tests/runtime-runes/samples/each-updates-5/_config.js new file mode 100644 index 0000000000..6d8fba4181 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-5/_config.js @@ -0,0 +1,21 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `1\n1\n1\n1\n`, + + async test({ assert, target }) { + /** + * @type {{ click: () => void; }} + */ + let btn1; + + [btn1] = target.querySelectorAll('button'); + + flushSync(() => { + btn1.click(); + }); + + assert.htmlEqual(target.innerHTML, `2\n2\n2\n2\n`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-5/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-updates-5/main.svelte new file mode 100644 index 0000000000..833efe228f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-5/main.svelte @@ -0,0 +1,26 @@ + + +{#each $store as item (item)} + {item.value} +{/each} +{#each $storeDeeper.items as item (item)} + {item.value} +{/each} +{#each $store as item} + {item.value} +{/each} +{#each $storeDeeper.items as item} + {item.value} +{/each} + + diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 186fbf8fed..9c9ce4fdf1 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -1,11 +1,10 @@ import { describe, assert, it } from 'vitest'; import * as $ from '../../src/internal/client/runtime'; import { effect, render_effect, user_effect } from '../../src/internal/client/reactivity/effects'; -import { source } from '../../src/internal/client/reactivity/sources'; -import type { Derived } from '../../src/internal/client/reactivity/types'; +import { source, set } from '../../src/internal/client/reactivity/sources'; +import type { Derived } from '../../src/internal/client/types'; import { proxy } from '../../src/internal/client/proxy'; import { derived } from '../../src/internal/client/reactivity/deriveds'; -import { set } from '../../src/internal/client/reactivity/sources'; /** * @param runes runes mode