fix: improve validation error that occurs when using `{@render ...}` to render default slotted content (#12521)

* add invalid_default_snippet error message

* fix: improve validation error that occurs when using `{@render ...}` to render default slotted content

* cheeky hack to keep treeshakeability until we can nuke this validation altogether
pull/12515/head
Rich Harris 6 months ago committed by GitHub
parent 0891fa7a18
commit ff080cbbdc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve validation error that occurs when using `{@render ...}` to render default slotted content

@ -1,3 +1,7 @@
## invalid_default_snippet
> Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead
## lifecycle_outside_component
> `%name%(...)` can only be used during component initialisation

@ -884,23 +884,27 @@ function serialize_inline_component(node, component_name, context, anchor = cont
])
);
if (
slot_name === 'default' &&
!has_children_prop &&
lets.length === 0 &&
children.default.every((node) => node.type !== 'SvelteFragment')
) {
push_prop(
b.init(
'children',
context.state.options.dev
? b.call('$.wrap_snippet', b.id(context.state.analysis.name), slot_fn)
: slot_fn
)
);
// We additionally add the default slot as a boolean, so that the slot render function on the other
// side knows it should get the content to render from $$props.children
serialized_slots.push(b.init(slot_name, b.true));
if (slot_name === 'default' && !has_children_prop) {
if (lets.length === 0 && children.default.every((node) => node.type !== 'SvelteFragment')) {
// create `children` prop...
push_prop(
b.init(
'children',
context.state.options.dev
? b.call('$.wrap_snippet', b.id(context.state.analysis.name), slot_fn)
: slot_fn
)
);
// and `$$slots.default: true` so that `<slot>` on the child works
serialized_slots.push(b.init(slot_name, b.true));
} else {
// create `$$slots.default`...
serialized_slots.push(b.init(slot_name, slot_fn));
// and a `children` prop that errors
push_prop(b.init('children', b.id('$.invalid_default_snippet')));
}
} else {
serialized_slots.push(b.init(slot_name, slot_fn));
}
@ -1865,13 +1869,7 @@ export const template_visitors = {
let snippet_function = /** @type {Expression} */ (context.visit(callee));
if (context.state.options.dev) {
snippet_function = b.call(
'$.validate_snippet',
snippet_function,
args.length && callee.type === 'Identifier' && callee.name === 'children'
? b.id('$$props')
: undefined
);
snippet_function = b.call('$.validate_snippet', snippet_function);
}
if (node.metadata.dynamic) {

@ -963,25 +963,28 @@ function serialize_inline_component(node, expression, context) {
])
);
if (
slot_name === 'default' &&
!has_children_prop &&
lets.length === 0 &&
children.default.every((node) => node.type !== 'SvelteFragment')
) {
push_prop(
b.prop(
'init',
b.id('children'),
context.state.options.dev ? b.call('$.add_snippet_symbol', slot_fn) : slot_fn
)
);
// We additionally add the default slot as a boolean, so that the slot render function on the other
// side knows it should get the content to render from $$props.children
serialized_slots.push(b.init('default', b.true));
if (slot_name === 'default' && !has_children_prop) {
if (lets.length === 0 && children.default.every((node) => node.type !== 'SvelteFragment')) {
// create `children` prop...
push_prop(
b.prop(
'init',
b.id('children'),
context.state.options.dev ? b.call('$.add_snippet_symbol', slot_fn) : slot_fn
)
);
// and `$$slots.default: true` so that `<slot>` on the child works
serialized_slots.push(b.init(slot_name, b.true));
} else {
// create `$$slots.default`...
serialized_slots.push(b.init(slot_name, slot_fn));
// and a `children` prop that errors
push_prop(b.init('children', b.id('$.invalid_default_snippet')));
}
} else {
const slot = b.prop('init', b.literal(slot_name), slot_fn);
serialized_slots.push(slot);
serialized_slots.push(b.init(slot_name, slot_fn));
}
}
@ -1211,13 +1214,7 @@ const template_visitors = {
const expression = /** @type {import('estree').Expression} */ (context.visit(callee));
const snippet_function = context.state.options.dev
? b.call(
'$.validate_snippet',
expression,
raw_args.length && callee.type === 'Identifier' && callee.name === 'children'
? b.id('$$props')
: undefined
)
? b.call('$.validate_snippet', expression)
: expression;
const snippet_args = raw_args.map((arg) => {

@ -164,6 +164,7 @@ export {
export { snapshot } from '../shared/clone.js';
export { noop } from '../shared/utils.js';
export {
invalid_default_snippet,
validate_component,
validate_dynamic_element_tag,
validate_snippet,

@ -556,6 +556,7 @@ export { snapshot } from '../shared/clone.js';
export {
add_snippet_symbol,
invalid_default_snippet,
validate_component,
validate_dynamic_element_tag,
validate_snippet,

@ -2,6 +2,22 @@
import { DEV } from 'esm-env';
/**
* Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead
* @returns {never}
*/
export function invalid_default_snippet() {
if (DEV) {
const error = new Error(`invalid_default_snippet\nCannot use \`{@render children(...)}\` if the parent component uses \`let:\` directives. Consider using a named snippet instead`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("invalid_default_snippet");
}
}
/**
* `%name%(...)` can only be used during component initialisation
* @param {string} name

@ -6,10 +6,13 @@ import * as e from './errors.js';
const snippet_symbol = Symbol.for('svelte.snippet');
export const invalid_default_snippet = add_snippet_symbol(e.invalid_default_snippet);
/**
* @param {any} fn
* @returns {import('svelte').Snippet}
*/
/*@__NO_SIDE_EFFECTS__*/
export function add_snippet_symbol(fn) {
fn[snippet_symbol] = true;
return fn;
@ -18,13 +21,9 @@ export function add_snippet_symbol(fn) {
/**
* Validate that the function handed to `{@render ...}` is a snippet function, and not some other kind of function.
* @param {any} snippet_fn
* @param {Record<string, any> | undefined} $$props Only passed if render tag receives arguments and is for the children prop
*/
export function validate_snippet(snippet_fn, $$props) {
if (
($$props?.$$slots?.default && typeof $$props.$$slots.default !== 'boolean') ||
(snippet_fn && snippet_fn[snippet_symbol] !== true)
) {
export function validate_snippet(snippet_fn) {
if (snippet_fn && snippet_fn[snippet_symbol] !== true) {
e.render_tag_invalid_argument();
}

@ -4,5 +4,5 @@ export default test({
compileOptions: {
dev: true
},
runtime_error: 'render_tag_invalid_argument'
runtime_error: 'invalid_default_snippet'
});

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

@ -0,0 +1,5 @@
<script>
let { children: x } = $props();
</script>
{@render x(true)}

@ -0,0 +1,7 @@
<script>
import Inner from './inner.svelte';
</script>
<Inner let:foo>
{foo}
</Inner>
Loading…
Cancel
Save