diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index 4e8af2e6b4..9bd7e8a654 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -48,6 +48,10 @@ > Failed to hydrate the application +## invalid_snippet + +> Could not `{@render}` snippet due to the expression being `null` or `undefined`. Consider using optional chaining `{@render snippet?.()}` + ## lifecycle_legacy_only > `%name%(...)` cannot be used in runes mode diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js index d782c8c2aa..ad3fa6d240 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js @@ -32,7 +32,7 @@ export function RenderTag(node, context) { if (node.metadata.dynamic) { // If we have a chain expression then ensure a nullish snippet function gets turned into an empty one if (node.expression.type === 'ChainExpression') { - snippet_function = b.logical('??', snippet_function, b.id('$.empty_snippet')); + snippet_function = b.logical('??', snippet_function, b.id('$.noop')); } context.state.init.push( diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index 8a312afebc..cec57f83b7 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -11,8 +11,10 @@ import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; import { create_fragment_from_html } from '../reconciler.js'; import { assign_nodes } from '../template.js'; import * as w from '../../warnings.js'; +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'; /** * @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn @@ -25,7 +27,8 @@ export function snippet(node, get_snippet, ...args) { var anchor = node; /** @type {SnippetFn | null | undefined} */ - var snippet = /** @type {SnippetFn} */ (empty_snippet); + // @ts-ignore + var snippet = noop; /** @type {Effect | null} */ var snippet_effect; @@ -38,6 +41,10 @@ export function snippet(node, get_snippet, ...args) { snippet_effect = null; } + if (DEV && snippet == null) { + e.invalid_snippet(); + } + snippet_effect = branch(() => /** @type {SnippetFn} */ (snippet)(anchor, ...args)); }, EFFECT_TRANSPARENT); @@ -46,10 +53,6 @@ export function snippet(node, get_snippet, ...args) { } } -// Just an empty function -/** @type {(...args: any[]) => void} */ -export const empty_snippet = () => {}; - /** * In development, wrap the snippet function so that it passes validation, and so that the * correct component context is set for ownership checks diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 8b537ed7ba..26d38f0aba 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -210,6 +210,22 @@ export function hydration_failed() { } } +/** + * Could not `{@render}` snippet due to the expression being `null` or `undefined`. Consider using optional chaining `{@render snippet?.()}` + * @returns {never} + */ +export function invalid_snippet() { + if (DEV) { + const error = new Error(`invalid_snippet\nCould not \`{@render}\` snippet due to the expression being \`null\` or \`undefined\`. Consider using optional chaining \`{@render snippet?.()}\``); + + error.name = 'Svelte error'; + throw error; + } else { + // TODO print a link to the documentation + throw new Error("invalid_snippet"); + } +} + /** * `%name%(...)` cannot be used in runes mode * @param {string} name diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 407b4ff5b0..519a412486 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -18,7 +18,7 @@ export { css_props } from './dom/blocks/css-props.js'; export { index, each } from './dom/blocks/each.js'; export { html } from './dom/blocks/html.js'; export { sanitize_slots, slot } from './dom/blocks/slot.js'; -export { snippet, wrap_snippet, empty_snippet } from './dom/blocks/snippet.js'; +export { snippet, wrap_snippet } from './dom/blocks/snippet.js'; export { component } from './dom/blocks/svelte-component.js'; export { element } from './dom/blocks/svelte-element.js'; export { head } from './dom/blocks/svelte-head.js'; diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-undefined/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-undefined/_config.js index b5e2e50cc9..4a13e208b5 100644 --- a/packages/svelte/tests/runtime-runes/samples/snippet-undefined/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/snippet-undefined/_config.js @@ -8,6 +8,6 @@ export default test({ assert.throws(() => { btn?.click(); flushSync(); - }, /snippet is not a function/); + }, /invalid_snippet/); } });