fix: Throw on unrendered snippets in `dev` (#15766)

pull/15777/head
Elliott Johnson 5 months ago committed by GitHub
parent 6a7e53feaa
commit e079ac92b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: Throw on unrendered snippets in `dev`

@ -60,6 +60,43 @@ Certain lifecycle methods can only be used during component initialisation. To f
<button onclick={handleClick}>click me</button>
```
### 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
<script>
let { children } = $props();
</script>
{children}
```
...or like this (a parent component is passing a snippet where a non-snippet value is expected):
```svelte
<!--- file: Parent.svelte --->
<ChildComponent>
{#snippet label()}
<span>Hi!</span>
{/snippet}
</ChildComponent>
```
```svelte
<!--- file: Child.svelte --->
<script>
let { label } = $props();
</script>
<!-- This component doesn't expect a snippet, but the parent provided one -->
<p>{label}</p>
```
### store_invalid_shape
```

@ -52,6 +52,41 @@ Certain lifecycle methods can only be used during component initialisation. To f
<button onclick={handleClick}>click me</button>
```
## 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
<script>
let { children } = $props();
</script>
{children}
```
...or like this (a parent component is passing a snippet where a non-snippet value is expected):
```svelte
<!--- file: Parent.svelte --->
<ChildComponent>
{#snippet label()}
<span>Hi!</span>
{/snippet}
</ChildComponent>
```
```svelte
<!--- file: Child.svelte --->
<script>
let { label } = $props();
</script>
<!-- This component doesn't expect a snippet, but the parent provided one -->
<p>{label}</p>
```
## store_invalid_shape
> `%name%` is not a store with a `subscribe` method

@ -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);
}
}

@ -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 `<slot>` on the child works
serialized_slots.push(b.init(slot_name, b.true));

@ -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;
}
/**

@ -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';

@ -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 };

@ -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

@ -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;
}

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
runtime_error: 'snippet_without_render_tag'
});

@ -0,0 +1,5 @@
{testSnippet}
{#snippet testSnippet()}
<p>hi again</p>
{/snippet}

@ -0,0 +1,3 @@
import { test } from '../../test';
export default test({});

@ -0,0 +1,5 @@
{testSnippet}
{#snippet testSnippet()}
<p>hi again</p>
{/snippet}

@ -0,0 +1,5 @@
<script>
import UnrenderedChildren from './unrendered-children.svelte';
</script>
<UnrenderedChildren>Hi</UnrenderedChildren>

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
runtime_error: 'snippet_without_render_tag'
});

@ -0,0 +1,5 @@
<script>
import UnrenderedChildren from './unrendered-children.svelte';
</script>
<UnrenderedChildren>Hi</UnrenderedChildren>

@ -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 = '';

Loading…
Cancel
Save