Skip to content

Commit ff080cb

Browse files
authored
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
1 parent 0891fa7 commit ff080cb

File tree

12 files changed

+97
-56
lines changed

12 files changed

+97
-56
lines changed

.changeset/forty-bikes-buy.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: improve validation error that occurs when using `{@render ...}` to render default slotted content

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## invalid_default_snippet
2+
3+
> Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead
4+
15
## lifecycle_outside_component
26

37
> `%name%(...)` can only be used during component initialisation

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

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -884,23 +884,27 @@ function serialize_inline_component(node, component_name, context, anchor = cont
884884
])
885885
);
886886

887-
if (
888-
slot_name === 'default' &&
889-
!has_children_prop &&
890-
lets.length === 0 &&
891-
children.default.every((node) => node.type !== 'SvelteFragment')
892-
) {
893-
push_prop(
894-
b.init(
895-
'children',
896-
context.state.options.dev
897-
? b.call('$.wrap_snippet', b.id(context.state.analysis.name), slot_fn)
898-
: slot_fn
899-
)
900-
);
901-
// We additionally add the default slot as a boolean, so that the slot render function on the other
902-
// side knows it should get the content to render from $$props.children
903-
serialized_slots.push(b.init(slot_name, b.true));
887+
if (slot_name === 'default' && !has_children_prop) {
888+
if (lets.length === 0 && children.default.every((node) => node.type !== 'SvelteFragment')) {
889+
// create `children` prop...
890+
push_prop(
891+
b.init(
892+
'children',
893+
context.state.options.dev
894+
? b.call('$.wrap_snippet', b.id(context.state.analysis.name), slot_fn)
895+
: slot_fn
896+
)
897+
);
898+
899+
// and `$$slots.default: true` so that `<slot>` on the child works
900+
serialized_slots.push(b.init(slot_name, b.true));
901+
} else {
902+
// create `$$slots.default`...
903+
serialized_slots.push(b.init(slot_name, slot_fn));
904+
905+
// and a `children` prop that errors
906+
push_prop(b.init('children', b.id('$.invalid_default_snippet')));
907+
}
904908
} else {
905909
serialized_slots.push(b.init(slot_name, slot_fn));
906910
}
@@ -1865,13 +1869,7 @@ export const template_visitors = {
18651869

18661870
let snippet_function = /** @type {Expression} */ (context.visit(callee));
18671871
if (context.state.options.dev) {
1868-
snippet_function = b.call(
1869-
'$.validate_snippet',
1870-
snippet_function,
1871-
args.length && callee.type === 'Identifier' && callee.name === 'children'
1872-
? b.id('$$props')
1873-
: undefined
1874-
);
1872+
snippet_function = b.call('$.validate_snippet', snippet_function);
18751873
}
18761874

18771875
if (node.metadata.dynamic) {

packages/svelte/src/compiler/phases/3-transform/server/transform-server.js

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -963,25 +963,28 @@ function serialize_inline_component(node, expression, context) {
963963
])
964964
);
965965

966-
if (
967-
slot_name === 'default' &&
968-
!has_children_prop &&
969-
lets.length === 0 &&
970-
children.default.every((node) => node.type !== 'SvelteFragment')
971-
) {
972-
push_prop(
973-
b.prop(
974-
'init',
975-
b.id('children'),
976-
context.state.options.dev ? b.call('$.add_snippet_symbol', slot_fn) : slot_fn
977-
)
978-
);
979-
// We additionally add the default slot as a boolean, so that the slot render function on the other
980-
// side knows it should get the content to render from $$props.children
981-
serialized_slots.push(b.init('default', b.true));
966+
if (slot_name === 'default' && !has_children_prop) {
967+
if (lets.length === 0 && children.default.every((node) => node.type !== 'SvelteFragment')) {
968+
// create `children` prop...
969+
push_prop(
970+
b.prop(
971+
'init',
972+
b.id('children'),
973+
context.state.options.dev ? b.call('$.add_snippet_symbol', slot_fn) : slot_fn
974+
)
975+
);
976+
977+
// and `$$slots.default: true` so that `<slot>` on the child works
978+
serialized_slots.push(b.init(slot_name, b.true));
979+
} else {
980+
// create `$$slots.default`...
981+
serialized_slots.push(b.init(slot_name, slot_fn));
982+
983+
// and a `children` prop that errors
984+
push_prop(b.init('children', b.id('$.invalid_default_snippet')));
985+
}
982986
} else {
983-
const slot = b.prop('init', b.literal(slot_name), slot_fn);
984-
serialized_slots.push(slot);
987+
serialized_slots.push(b.init(slot_name, slot_fn));
985988
}
986989
}
987990

@@ -1211,13 +1214,7 @@ const template_visitors = {
12111214

12121215
const expression = /** @type {import('estree').Expression} */ (context.visit(callee));
12131216
const snippet_function = context.state.options.dev
1214-
? b.call(
1215-
'$.validate_snippet',
1216-
expression,
1217-
raw_args.length && callee.type === 'Identifier' && callee.name === 'children'
1218-
? b.id('$$props')
1219-
: undefined
1220-
)
1217+
? b.call('$.validate_snippet', expression)
12211218
: expression;
12221219

12231220
const snippet_args = raw_args.map((arg) => {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ export {
164164
export { snapshot } from '../shared/clone.js';
165165
export { noop } from '../shared/utils.js';
166166
export {
167+
invalid_default_snippet,
167168
validate_component,
168169
validate_dynamic_element_tag,
169170
validate_snippet,

packages/svelte/src/internal/server/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,7 @@ export { snapshot } from '../shared/clone.js';
556556

557557
export {
558558
add_snippet_symbol,
559+
invalid_default_snippet,
559560
validate_component,
560561
validate_dynamic_element_tag,
561562
validate_snippet,

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22

33
import { DEV } from 'esm-env';
44

5+
/**
6+
* Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead
7+
* @returns {never}
8+
*/
9+
export function invalid_default_snippet() {
10+
if (DEV) {
11+
const error = new Error(`invalid_default_snippet\nCannot use \`{@render children(...)}\` if the parent component uses \`let:\` directives. Consider using a named snippet instead`);
12+
13+
error.name = 'Svelte error';
14+
throw error;
15+
} else {
16+
// TODO print a link to the documentation
17+
throw new Error("invalid_default_snippet");
18+
}
19+
}
20+
521
/**
622
* `%name%(...)` can only be used during component initialisation
723
* @param {string} name

packages/svelte/src/internal/shared/validate.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@ import * as e from './errors.js';
66

77
const snippet_symbol = Symbol.for('svelte.snippet');
88

9+
export const invalid_default_snippet = add_snippet_symbol(e.invalid_default_snippet);
10+
911
/**
1012
* @param {any} fn
1113
* @returns {import('svelte').Snippet}
1214
*/
15+
/*@__NO_SIDE_EFFECTS__*/
1316
export function add_snippet_symbol(fn) {
1417
fn[snippet_symbol] = true;
1518
return fn;
@@ -18,13 +21,9 @@ export function add_snippet_symbol(fn) {
1821
/**
1922
* Validate that the function handed to `{@render ...}` is a snippet function, and not some other kind of function.
2023
* @param {any} snippet_fn
21-
* @param {Record<string, any> | undefined} $$props Only passed if render tag receives arguments and is for the children prop
2224
*/
23-
export function validate_snippet(snippet_fn, $$props) {
24-
if (
25-
($$props?.$$slots?.default && typeof $$props.$$slots.default !== 'boolean') ||
26-
(snippet_fn && snippet_fn[snippet_symbol] !== true)
27-
) {
25+
export function validate_snippet(snippet_fn) {
26+
if (snippet_fn && snippet_fn[snippet_symbol] !== true) {
2827
e.render_tag_invalid_argument();
2928
}
3029

packages/svelte/tests/runtime-runes/samples/snippet-slot-let-error/_config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ export default test({
44
compileOptions: {
55
dev: true
66
},
7-
runtime_error: 'render_tag_invalid_argument'
7+
runtime_error: 'invalid_default_snippet'
88
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
compileOptions: {
5+
dev: true
6+
},
7+
runtime_error: 'invalid_default_snippet'
8+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
let { children: x } = $props();
3+
</script>
4+
5+
{@render x(true)}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
import Inner from './inner.svelte';
3+
</script>
4+
5+
<Inner let:foo>
6+
{foo}
7+
</Inner>

0 commit comments

Comments
 (0)