fix: properly validate snippet/slot interop (#12421)

The previous validation for checking if slots with let directives were rendered with `{@render children(...)}` had false positives
- threw an error even if the other side didn't make use of the arguments, i.e. wasn't actually using a let directive
- didn't check that the rendered snippet actually was the children property

This fixes the validation by only applying it to children render tags, and by adding the slot to `$$slots.default` instead of `$$props.children` in more cases (when it's using `<svelte:fragment>` or `let:` directives, which both mean you're using old slot syntax)

Fixes #12414
pull/12408/head
Simon H 6 months ago committed by GitHub
parent 70cec4e40e
commit 51f12d243d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: properly validate snippet/slot interop

@ -880,7 +880,12 @@ function serialize_inline_component(node, component_name, context, anchor = cont
])
);
if (slot_name === 'default' && !has_children_prop) {
if (
slot_name === 'default' &&
!has_children_prop &&
lets.length === 0 &&
children.default.every((node) => node.type !== 'SvelteFragment')
) {
push_prop(
b.init(
'children',
@ -1859,7 +1864,9 @@ export const template_visitors = {
snippet_function = b.call(
'$.validate_snippet',
snippet_function,
args.length ? b.id('$$props') : undefined
args.length && callee.type === 'Identifier' && callee.name === 'children'
? b.id('$$props')
: undefined
);
}

@ -959,7 +959,12 @@ function serialize_inline_component(node, expression, context) {
])
);
if (slot_name === 'default' && !has_children_prop) {
if (
slot_name === 'default' &&
!has_children_prop &&
lets.length === 0 &&
children.default.every((node) => node.type !== 'SvelteFragment')
) {
push_prop(
b.prop(
'init',
@ -1202,7 +1207,13 @@ const template_visitors = {
const expression = /** @type {import('estree').Expression} */ (context.visit(callee));
const snippet_function = context.state.options.dev
? b.call('$.validate_snippet', expression)
? b.call(
'$.validate_snippet',
expression,
raw_args.length && callee.type === 'Identifier' && callee.name === 'children'
? b.id('$$props')
: undefined
)
: expression;
const snippet_args = raw_args.map((arg) => {

@ -15,10 +15,13 @@ 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
* @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 || (snippet_fn && snippet_fn[snippet_symbol] !== true)) {
if (
($$props?.$$slots?.default && typeof $$props.$$slots.default !== 'boolean') ||
(snippet_fn && snippet_fn[snippet_symbol] !== true)
) {
e.render_tag_invalid_argument();
}

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

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

@ -0,0 +1,5 @@
<script>
import Inner from './inner.svelte';
</script>
<Inner>I don't need to use the argument if I don't want to</Inner>
Loading…
Cancel
Save