diff --git a/.changeset/strong-pianos-promise.md b/.changeset/strong-pianos-promise.md
new file mode 100644
index 000000000000..f5214c7dcb9d
--- /dev/null
+++ b/.changeset/strong-pianos-promise.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: Throw on unrendered snippets in `dev`
diff --git a/documentation/docs/98-reference/.generated/shared-errors.md b/documentation/docs/98-reference/.generated/shared-errors.md
index 4c81d7b89452..6c31aaafd0df 100644
--- a/documentation/docs/98-reference/.generated/shared-errors.md
+++ b/documentation/docs/98-reference/.generated/shared-errors.md
@@ -60,6 +60,43 @@ Certain lifecycle methods can only be used during component initialisation. To f
```
+### snippet_without_render_tag
+
+```
+Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`.
+```
+
+A component throwing this error will look something like this (`children` is not being rendered):
+
+```svelte
+
+
+{children}
+```
+
+...or like this (a parent component is passing a snippet where a non-snippet value is expected):
+
+```svelte
+
+
+ {#snippet label()}
+ Hi!
+ {/snippet}
+
+```
+
+```svelte
+
+
+
+
+
{label}
+```
+
### store_invalid_shape
```
diff --git a/packages/svelte/messages/shared-errors/errors.md b/packages/svelte/messages/shared-errors/errors.md
index 20f3d193d928..4b4d3322028d 100644
--- a/packages/svelte/messages/shared-errors/errors.md
+++ b/packages/svelte/messages/shared-errors/errors.md
@@ -52,6 +52,41 @@ Certain lifecycle methods can only be used during component initialisation. To f
```
+## snippet_without_render_tag
+
+> Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`.
+
+A component throwing this error will look something like this (`children` is not being rendered):
+
+```svelte
+
+
+{children}
+```
+
+...or like this (a parent component is passing a snippet where a non-snippet value is expected):
+
+```svelte
+
+
+ {#snippet label()}
+ Hi!
+ {/snippet}
+
+```
+
+```svelte
+
+
+
+
+
{label}
+```
+
## store_invalid_shape
> `%name%` is not a store with a `subscribe` method
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js
index cae3e7d79c94..a67fcc888510 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js
@@ -1,4 +1,4 @@
-/** @import { BlockStatement } from 'estree' */
+/** @import { ArrowFunctionExpression, BlockStatement, CallExpression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { dev } from '../../../../state.js';
@@ -9,20 +9,27 @@ import * as b from '../../../../utils/builders.js';
* @param {ComponentContext} context
*/
export function SnippetBlock(node, context) {
- const fn = b.function_declaration(
- node.expression,
- [b.id('$$payload'), ...node.parameters],
- /** @type {BlockStatement} */ (context.visit(node.body))
- );
+ const body = /** @type {BlockStatement} */ (context.visit(node.body));
+
if (dev) {
- fn.body.body.unshift(b.stmt(b.call('$.validate_snippet_args', b.id('$$payload'))));
+ body.body.unshift(b.stmt(b.call('$.validate_snippet_args', b.id('$$payload'))));
}
+
+ /** @type {ArrowFunctionExpression | CallExpression} */
+ let fn = b.arrow([b.id('$$payload'), ...node.parameters], body);
+
+ if (dev) {
+ fn = b.call('$.prevent_snippet_stringification', fn);
+ }
+
+ const declaration = b.declaration('const', [b.declarator(node.expression, fn)]);
+
// @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone
fn.___snippet = true;
if (node.metadata.can_hoist) {
- context.state.hoisted.push(fn);
+ context.state.hoisted.push(declaration);
} else {
- context.state.init.push(fn);
+ context.state.init.push(declaration);
}
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js
index 695161ff9b60..f4b3dd1b09aa 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js
@@ -4,6 +4,7 @@
import { empty_comment, build_attribute_value } from './utils.js';
import * as b from '../../../../../utils/builders.js';
import { is_element_node } from '../../../../nodes.js';
+import { dev } from '../../../../../state.js';
/**
* @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node
@@ -238,7 +239,13 @@ export function build_inline_component(node, expression, context) {
)
) {
// create `children` prop...
- push_prop(b.prop('init', b.id('children'), slot_fn));
+ push_prop(
+ b.prop(
+ 'init',
+ b.id('children'),
+ dev ? b.call('$.prevent_snippet_stringification', slot_fn) : slot_fn
+ )
+ );
// and `$$slots.default: true` so that `` on the child works
serialized_slots.push(b.init(slot_name, b.true));
diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js
index b916a02ce55f..a48153900fde 100644
--- a/packages/svelte/src/internal/client/dom/blocks/snippet.js
+++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js
@@ -15,6 +15,7 @@ import * as e from '../../errors.js';
import { DEV } from 'esm-env';
import { get_first_child, get_next_sibling } from '../operations.js';
import { noop } from '../../../shared/utils.js';
+import { prevent_snippet_stringification } from '../../../shared/validate.js';
/**
* @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn
@@ -60,7 +61,7 @@ export function snippet(node, get_snippet, ...args) {
* @param {(node: TemplateNode, ...args: any[]) => void} fn
*/
export function wrap_snippet(component, fn) {
- return (/** @type {TemplateNode} */ node, /** @type {any[]} */ ...args) => {
+ const snippet = (/** @type {TemplateNode} */ node, /** @type {any[]} */ ...args) => {
var previous_component_function = dev_current_component_function;
set_dev_current_component_function(component);
@@ -70,6 +71,10 @@ export function wrap_snippet(component, fn) {
set_dev_current_component_function(previous_component_function);
}
};
+
+ prevent_snippet_stringification(snippet);
+
+ return snippet;
}
/**
diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js
index e977bf3b0fe9..14d6e29f5bb4 100644
--- a/packages/svelte/src/internal/client/index.js
+++ b/packages/svelte/src/internal/client/index.js
@@ -157,7 +157,8 @@ export {
invalid_default_snippet,
validate_dynamic_element_tag,
validate_store,
- validate_void_dynamic_element
+ validate_void_dynamic_element,
+ prevent_snippet_stringification
} from '../shared/validate.js';
export { strict_equals, equals } from './dev/equality.js';
export { log_if_contains_state } from './dev/console-log.js';
diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js
index d711778a44ea..b58a1d4372a6 100644
--- a/packages/svelte/src/internal/server/index.js
+++ b/packages/svelte/src/internal/server/index.js
@@ -509,7 +509,8 @@ export { fallback } from '../shared/utils.js';
export {
invalid_default_snippet,
validate_dynamic_element_tag,
- validate_void_dynamic_element
+ validate_void_dynamic_element,
+ prevent_snippet_stringification
} from '../shared/validate.js';
export { escape_html as escape };
diff --git a/packages/svelte/src/internal/shared/errors.js b/packages/svelte/src/internal/shared/errors.js
index 2e89dc1ad109..b8606fbf6f7d 100644
--- a/packages/svelte/src/internal/shared/errors.js
+++ b/packages/svelte/src/internal/shared/errors.js
@@ -48,6 +48,21 @@ export function lifecycle_outside_component(name) {
}
}
+/**
+ * Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`.
+ * @returns {never}
+ */
+export function snippet_without_render_tag() {
+ if (DEV) {
+ const error = new Error(`snippet_without_render_tag\nAttempted to render a snippet without a \`{@render}\` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change \`{snippet}\` to \`{@render snippet()}\`.\nhttps://svelte.dev/e/snippet_without_render_tag`);
+
+ error.name = 'Svelte error';
+ throw error;
+ } else {
+ throw new Error(`https://svelte.dev/e/snippet_without_render_tag`);
+ }
+}
+
/**
* `%name%` is not a store with a `subscribe` method
* @param {string} name
diff --git a/packages/svelte/src/internal/shared/validate.js b/packages/svelte/src/internal/shared/validate.js
index 852c0e83bfb1..bbb237594bec 100644
--- a/packages/svelte/src/internal/shared/validate.js
+++ b/packages/svelte/src/internal/shared/validate.js
@@ -35,3 +35,15 @@ export function validate_store(store, name) {
e.store_invalid_shape(name);
}
}
+
+/**
+ * @template {() => unknown} T
+ * @param {T} fn
+ */
+export function prevent_snippet_stringification(fn) {
+ fn.toString = () => {
+ e.snippet_without_render_tag();
+ return '';
+ };
+ return fn;
+}
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/_config.js
new file mode 100644
index 000000000000..94c5de10af60
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/_config.js
@@ -0,0 +1,8 @@
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true
+ },
+ runtime_error: 'snippet_without_render_tag'
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/main.svelte
new file mode 100644
index 000000000000..3f8edfe4fafc
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/main.svelte
@@ -0,0 +1,5 @@
+{testSnippet}
+
+{#snippet testSnippet()}
+
hi again
+{/snippet}
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-prod/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-prod/_config.js
new file mode 100644
index 000000000000..f47bee71df87
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-prod/_config.js
@@ -0,0 +1,3 @@
+import { test } from '../../test';
+
+export default test({});
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-prod/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-prod/main.svelte
new file mode 100644
index 000000000000..3f8edfe4fafc
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-prod/main.svelte
@@ -0,0 +1,5 @@
+{testSnippet}
+
+{#snippet testSnippet()}
+
hi again
+{/snippet}
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/_config.js
new file mode 100644
index 000000000000..f47bee71df87
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/_config.js
@@ -0,0 +1,3 @@
+import { test } from '../../test';
+
+export default test({});
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/main.svelte
new file mode 100644
index 000000000000..4a4ed3176f24
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/main.svelte
@@ -0,0 +1,5 @@
+
+
+Hi
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/unrendered-children.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/unrendered-children.svelte
new file mode 100644
index 000000000000..6b7154a5a406
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/unrendered-children.svelte
@@ -0,0 +1,5 @@
+
+
+{children}
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/_config.js
new file mode 100644
index 000000000000..94c5de10af60
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/_config.js
@@ -0,0 +1,8 @@
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true
+ },
+ runtime_error: 'snippet_without_render_tag'
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/main.svelte
new file mode 100644
index 000000000000..4a4ed3176f24
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/main.svelte
@@ -0,0 +1,5 @@
+
+
+Hi
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/unrendered-children.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/unrendered-children.svelte
new file mode 100644
index 000000000000..6b7154a5a406
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/unrendered-children.svelte
@@ -0,0 +1,5 @@
+
+
+{children}
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
index cadae2cf15c0..04bfbf6ae47b 100644
--- 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
@@ -1,9 +1,9 @@
import * as $ from 'svelte/internal/server';
import TextInput from './Child.svelte';
-function snippet($$payload) {
+const snippet = ($$payload) => {
$$payload.out += `Something`;
-}
+};
export default function Bind_component_snippet($$payload) {
let value = '';