Skip to content

Commit 3b88b88

Browse files
authored
fix: prevent nullish snippet for rendering empty content (#13083)
* fix: prevent nullish snippet for rendering empty content * fix: prevent nullish snippet for rendering empty content * lint * fix message * alternative approach * tweak * feedback
1 parent e1448f2 commit 3b88b88

File tree

7 files changed

+64
-3
lines changed

7 files changed

+64
-3
lines changed

.changeset/tame-frogs-shave.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: prevent nullish snippet for rendering empty content

packages/svelte/messages/client-errors/errors.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@
4848

4949
> Failed to hydrate the application
5050
51+
## invalid_snippet
52+
53+
> Could not `{@render}` snippet due to the expression being `null` or `undefined`. Consider using optional chaining `{@render snippet?.()}`
54+
5155
## lifecycle_legacy_only
5256

5357
> `%name%(...)` cannot be used in runes mode

packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ export function RenderTag(node, context) {
3030
let snippet_function = /** @type {Expression} */ (context.visit(callee));
3131

3232
if (node.metadata.dynamic) {
33+
// If we have a chain expression then ensure a nullish snippet function gets turned into an empty one
34+
if (node.expression.type === 'ChainExpression') {
35+
snippet_function = b.logical('??', snippet_function, b.id('$.noop'));
36+
}
37+
3338
context.state.init.push(
3439
b.stmt(b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args))
3540
);

packages/svelte/src/internal/client/dom/blocks/snippet.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
1111
import { create_fragment_from_html } from '../reconciler.js';
1212
import { assign_nodes } from '../template.js';
1313
import * as w from '../../warnings.js';
14+
import * as e from '../../errors.js';
1415
import { DEV } from 'esm-env';
1516
import { get_first_child, get_next_sibling } from '../operations.js';
17+
import { noop } from '../../../shared/utils.js';
1618

1719
/**
1820
* @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn
@@ -25,7 +27,8 @@ export function snippet(node, get_snippet, ...args) {
2527
var anchor = node;
2628

2729
/** @type {SnippetFn | null | undefined} */
28-
var snippet;
30+
// @ts-ignore
31+
var snippet = noop;
2932

3033
/** @type {Effect | null} */
3134
var snippet_effect;
@@ -38,9 +41,11 @@ export function snippet(node, get_snippet, ...args) {
3841
snippet_effect = null;
3942
}
4043

41-
if (snippet) {
42-
snippet_effect = branch(() => /** @type {SnippetFn} */ (snippet)(anchor, ...args));
44+
if (DEV && snippet == null) {
45+
e.invalid_snippet();
4346
}
47+
48+
snippet_effect = branch(() => /** @type {SnippetFn} */ (snippet)(anchor, ...args));
4449
}, EFFECT_TRANSPARENT);
4550

4651
if (hydrating) {

packages/svelte/src/internal/client/errors.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,22 @@ export function hydration_failed() {
210210
}
211211
}
212212

213+
/**
214+
* Could not `{@render}` snippet due to the expression being `null` or `undefined`. Consider using optional chaining `{@render snippet?.()}`
215+
* @returns {never}
216+
*/
217+
export function invalid_snippet() {
218+
if (DEV) {
219+
const error = new Error(`invalid_snippet\nCould not \`{@render}\` snippet due to the expression being \`null\` or \`undefined\`. Consider using optional chaining \`{@render snippet?.()}\``);
220+
221+
error.name = 'Svelte error';
222+
throw error;
223+
} else {
224+
// TODO print a link to the documentation
225+
throw new Error("invalid_snippet");
226+
}
227+
}
228+
213229
/**
214230
* `%name%(...)` cannot be used in runes mode
215231
* @param {string} name
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
test({ assert, target }) {
6+
const btn = target.querySelector('button');
7+
8+
assert.throws(() => {
9+
btn?.click();
10+
flushSync();
11+
}, /invalid_snippet/);
12+
}
13+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
let state = $state({
3+
value: counter
4+
});
5+
</script>
6+
7+
{#snippet counter()}
8+
Test
9+
{/snippet}
10+
11+
{@render state.value()}
12+
13+
<button onclick={() => state.value = undefined}>change</button>

0 commit comments

Comments
 (0)