diff --git a/.changeset/lazy-carrots-fry.md b/.changeset/lazy-carrots-fry.md new file mode 100644 index 000000000000..072bc5dc6348 --- /dev/null +++ b/.changeset/lazy-carrots-fry.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't hoist snippets with `bind:group` diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index 95d7d006779c..e8f07091cffd 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -391,7 +391,7 @@ function open(parser) { parameters: function_expression.params, body: create_fragment(), metadata: { - can_hoist: false, + can_hoist: undefined, sites: new Set() } }); diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js index 7719eee6772e..113522ae1b60 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js @@ -199,6 +199,12 @@ export function BindDirective(node, context) { throw new Error('Cannot find declaration for bind:group'); } + const snippet_parent = context.path.find((parent) => parent.type === 'SnippetBlock'); + + if (snippet_parent) { + snippet_parent.metadata.can_hoist = false; + } + // Traverse the path upwards and find all EachBlocks who are (indirectly) contributing to bind:group, // i.e. one of their declarations is referenced in the binding. This allows group bindings to work // correctly when referencing a variable declared in an EachBlock by using the index of the each block diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js index 2f6bbd785a71..645f79766e03 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js @@ -42,7 +42,8 @@ export function SnippetBlock(node, context) { } } - node.metadata.can_hoist = can_hoist; + node.metadata.can_hoist = + node.metadata.can_hoist != null ? node.metadata.can_hoist && can_hoist : can_hoist; const { path } = context; const parent = path.at(-2); diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 8be9aed17723..857b542eb129 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -463,7 +463,7 @@ export namespace AST { body: Fragment; /** @internal */ metadata: { - can_hoist: boolean; + can_hoist?: boolean; /** The set of components/render tags that could render this snippet, * used for CSS pruning */ sites: Set; diff --git a/packages/svelte/tests/runtime-runes/samples/bind-group-non-hoistable-snippet/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-group-non-hoistable-snippet/_config.js new file mode 100644 index 000000000000..4890d4abb598 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-group-non-hoistable-snippet/_config.js @@ -0,0 +1,19 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [radio1, radio2] = target.querySelectorAll('input'); + const p = target.querySelector('p'); + + assert.equal(p?.innerHTML, ''); + flushSync(() => { + radio1.click(); + }); + assert.equal(p?.innerHTML, 'cool'); + flushSync(() => { + radio2.click(); + }); + assert.equal(p?.innerHTML, 'cooler'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-group-non-hoistable-snippet/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-group-non-hoistable-snippet/main.svelte new file mode 100644 index 000000000000..2ebd0e560f08 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-group-non-hoistable-snippet/main.svelte @@ -0,0 +1,12 @@ + + +{#snippet radio(group, value)} + +{/snippet} + +{@render radio(group, "cool")} +{@render radio(group, "cooler")} + +

{group.selected}

\ No newline at end of file