diff --git a/.changeset/gold-tools-nail.md b/.changeset/gold-tools-nail.md
new file mode 100644
index 0000000000..7e99bd67a4
--- /dev/null
+++ b/.changeset/gold-tools-nail.md
@@ -0,0 +1,5 @@
+---
+"svelte": patch
+---
+
+fix: ensure top level snippets are defined when binding to component prop
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
index 3f07e4dbfb..8f35d8897a 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
@@ -1614,14 +1614,15 @@ const template_visitors = {
state.template.push(block_close);
},
SnippetBlock(node, context) {
- // TODO hoist where possible
- context.state.init.push(
- b.function_declaration(
- node.expression,
- [b.id('$$payload'), ...node.parameters],
- /** @type {import('estree').BlockStatement} */ (context.visit(node.body))
- )
+ const fn = b.function_declaration(
+ node.expression,
+ [b.id('$$payload'), ...node.parameters],
+ /** @type {import('estree').BlockStatement} */ (context.visit(node.body))
);
+ // @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone
+ fn.___snippet = true;
+ // TODO hoist where possible
+ context.state.init.push(fn);
if (context.state.options.dev) {
context.state.init.push(b.stmt(b.call('$.add_snippet_symbol', node.expression)));
@@ -2221,14 +2222,27 @@ export function server_component(analysis, options) {
// If the component binds to a child, we need to put the template in a loop and repeat until legacy bindings are stable.
// We can remove this once the legacy syntax is gone.
if (analysis.uses_component_bindings) {
+ const snippets = template.body.filter(
+ (node) =>
+ node.type === 'FunctionDeclaration' &&
+ // @ts-expect-error
+ node.___snippet
+ );
+ const rest = template.body.filter(
+ (node) =>
+ node.type !== 'FunctionDeclaration' ||
+ // @ts-expect-error
+ !node.___snippet
+ );
template.body = [
+ ...snippets,
b.let('$$settled', b.true),
b.let('$$inner_payload'),
b.stmt(
b.function(
b.id('$$render_inner'),
[b.id('$$payload')],
- b.block(/** @type {import('estree').Statement[]} */ (template.body))
+ b.block(/** @type {import('estree').Statement[]} */ (rest))
)
),
b.do_while(
diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js
new file mode 100644
index 0000000000..e9bfe03063
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js
@@ -0,0 +1,39 @@
+// index.svelte (Svelte VERSION)
+// Note: compiler output will change before 5.0 is released!
+import "svelte/internal/disclose-version";
+import * as $ from "svelte/internal/client";
+import TextInput from './Child.svelte';
+
+var root_1 = $.template(`Something`, 1);
+var root = $.template(` `, 1);
+
+export default function Bind_component_snippet($$anchor, $$props) {
+ $.push($$props, true);
+
+ let value = $.source('');
+ const _snippet = snippet;
+ var fragment_1 = root();
+
+ function snippet($$anchor) {
+ var fragment = root_1();
+
+ $.append($$anchor, fragment);
+ }
+
+ var node = $.first_child(fragment_1);
+
+ TextInput(node, {
+ get value() {
+ return $.get(value);
+ },
+ set value($$value) {
+ $.set(value, $.proxy($$value));
+ }
+ });
+
+ var text = $.sibling(node, true);
+
+ $.render_effect(() => $.set_text(text, ` value: ${$.stringify($.get(value))}`));
+ $.append($$anchor, fragment_1);
+ $.pop();
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js
new file mode 100644
index 0000000000..b1f83fed42
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js
@@ -0,0 +1,43 @@
+// index.svelte (Svelte VERSION)
+// Note: compiler output will change before 5.0 is released!
+import * as $ from "svelte/internal/server";
+import TextInput from './Child.svelte';
+
+export default function Bind_component_snippet($$payload, $$props) {
+ $.push(true);
+
+ let value = '';
+ const _snippet = snippet;
+
+ function snippet($$payload) {
+ $$payload.out += `Something`;
+ }
+
+ let $$settled = true;
+ let $$inner_payload;
+
+ function $$render_inner($$payload) {
+ $$payload.out += ``;
+
+ TextInput($$payload, {
+ get value() {
+ return value;
+ },
+ set value($$value) {
+ value = $$value;
+ $$settled = false;
+ }
+ });
+
+ $$payload.out += ` value: ${$.escape(value)}`;
+ };
+
+ do {
+ $$settled = true;
+ $$inner_payload = $.copy_payload($$payload);
+ $$render_inner($$inner_payload);
+ } while (!$$settled);
+
+ $.assign_payload($$payload, $$inner_payload);
+ $.pop();
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/index.svelte b/packages/svelte/tests/snapshot/samples/bind-component-snippet/index.svelte
new file mode 100644
index 0000000000..1d7a8f4f54
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/index.svelte
@@ -0,0 +1,13 @@
+
+
+{#snippet snippet()}
+ Something
+{/snippet}
+
+
+value: {value}