Skip to content

Commit 378a17e

Browse files
committed
feat: provide isSnippet type, deduplicate children prop from default slot
fixes #10790 part of #9774
1 parent ffb27f6 commit 378a17e

File tree

16 files changed

+125
-40
lines changed

16 files changed

+125
-40
lines changed

.changeset/cold-cheetahs-judge.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: deduplicate children prop and default slot

.changeset/famous-grapes-refuse.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+
feat: provide `isSnippet` function to determine whether a given value is a snippet

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

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,13 @@ function serialize_inline_component(node, component_name, context) {
774774
*/
775775
let slot_scope_applies_to_itself = false;
776776

777+
/**
778+
* Components may have a children prop and also have child nodes. In this case, we assume
779+
* that the child component isn't using render tags yet and pass the slot as $$slots.default.
780+
* We're not doing it for spread attributes, as this would result in too many false positives.
781+
*/
782+
let has_children_prop = false;
783+
777784
/**
778785
* @param {import('estree').Property} prop
779786
*/
@@ -823,6 +830,10 @@ function serialize_inline_component(node, component_name, context) {
823830
slot_scope_applies_to_itself = true;
824831
}
825832

833+
if (attribute.name === 'children') {
834+
has_children_prop = true;
835+
}
836+
826837
const [, value] = serialize_attribute_value(attribute.value, context);
827838

828839
if (attribute.metadata.dynamic) {
@@ -944,13 +955,8 @@ function serialize_inline_component(node, component_name, context) {
944955
b.block([...(slot_name === 'default' && !slot_scope_applies_to_itself ? lets : []), ...body])
945956
);
946957

947-
if (slot_name === 'default') {
948-
push_prop(
949-
b.init(
950-
'children',
951-
context.state.options.dev ? b.call('$.add_snippet_symbol', slot_fn) : slot_fn
952-
)
953-
);
958+
if (slot_name === 'default' && !has_children_prop) {
959+
push_prop(b.init('children', b.call('$.add_snippet_symbol', slot_fn)));
954960
} else {
955961
serialized_slots.push(b.init(slot_name, slot_fn));
956962
}
@@ -2685,9 +2691,7 @@ export const template_visitors = {
26852691
} else {
26862692
context.state.init.push(b.const(node.expression, b.arrow(args, body)));
26872693
}
2688-
if (context.state.options.dev) {
2689-
context.state.init.push(b.stmt(b.call('$.add_snippet_symbol', node.expression)));
2690-
}
2694+
context.state.init.push(b.stmt(b.call('$.add_snippet_symbol', node.expression)));
26912695
},
26922696
FunctionExpression: function_visitor,
26932697
ArrowFunctionExpression: function_visitor,
@@ -3106,7 +3110,7 @@ export const template_visitors = {
31063110
);
31073111

31083112
const expression = is_default
3109-
? b.member(b.id('$$props'), b.id('children'))
3113+
? b.call('$.default_slot', b.id('$$props'))
31103114
: b.member(b.member(b.id('$$props'), b.id('$$slots')), name, true, true);
31113115

31123116
const slot = b.call('$.slot', context.state.node, expression, props_expression, fallback);

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

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -947,6 +947,13 @@ function serialize_inline_component(node, component_name, context) {
947947
*/
948948
let slot_scope_applies_to_itself = false;
949949

950+
/**
951+
* Components may have a children prop and also have child nodes. In this case, we assume
952+
* that the child component isn't using render tags yet and pass the slot as $$slots.default.
953+
* We're not doing it for spread attributes, as this would result in too many false positives.
954+
*/
955+
let has_children_prop = false;
956+
950957
/**
951958
* @param {import('estree').Property} prop
952959
*/
@@ -975,6 +982,10 @@ function serialize_inline_component(node, component_name, context) {
975982
slot_scope_applies_to_itself = true;
976983
}
977984

985+
if (attribute.name === 'children') {
986+
has_children_prop = true;
987+
}
988+
978989
const value = serialize_attribute_value(attribute.value, context, false, true);
979990
push_prop(b.prop('init', b.key(attribute.name), value));
980991
} else if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
@@ -1049,14 +1060,8 @@ function serialize_inline_component(node, component_name, context) {
10491060
b.block([...(slot_name === 'default' && !slot_scope_applies_to_itself ? lets : []), ...body])
10501061
);
10511062

1052-
if (slot_name === 'default') {
1053-
push_prop(
1054-
b.prop(
1055-
'init',
1056-
b.id('children'),
1057-
context.state.options.dev ? b.call('$.add_snippet_symbol', slot_fn) : slot_fn
1058-
)
1059-
);
1063+
if (slot_name === 'default' && !has_children_prop) {
1064+
push_prop(b.prop('init', b.id('children'), b.call('$.add_snippet_symbol', slot_fn)));
10601065
} else {
10611066
const slot = b.prop('init', b.literal(slot_name), slot_fn);
10621067
serialized_slots.push(slot);
@@ -1614,9 +1619,7 @@ const template_visitors = {
16141619
)
16151620
);
16161621

1617-
if (context.state.options.dev) {
1618-
context.state.init.push(b.stmt(b.call('$.add_snippet_symbol', node.expression)));
1619-
}
1622+
context.state.init.push(b.stmt(b.call('$.add_snippet_symbol', node.expression)));
16201623
},
16211624
Component(node, context) {
16221625
const state = context.state;
@@ -1744,7 +1747,7 @@ const template_visitors = {
17441747
const lets = [];
17451748

17461749
/** @type {import('estree').Expression} */
1747-
let expression = b.member_id('$$props.children');
1750+
let expression = b.call('$.default_slot', b.id('$$props'));
17481751

17491752
for (const attribute of node.attributes) {
17501753
if (attribute.type === 'SpreadAttribute') {

packages/svelte/src/internal/client/dom/blocks/snippet.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,22 @@ export function snippet(get_snippet, node, ...args) {
3838
};
3939
}, block);
4040
}
41+
42+
const snippet_symbol = Symbol.for('svelte.snippet');
43+
44+
/**
45+
* @param {any} fn
46+
*/
47+
export function add_snippet_symbol(fn) {
48+
fn[snippet_symbol] = true;
49+
return fn;
50+
}
51+
52+
/**
53+
* Returns true if given parameter is a snippet.
54+
* @param {any} maybeSnippet
55+
* @returns {maybeSnippet is import('svelte').Snippet}
56+
*/
57+
export function isSnippet(maybeSnippet) {
58+
return /** @type {any} */ (maybeSnippet)?.[snippet_symbol] === true;
59+
}

packages/svelte/src/internal/client/dom/legacy/misc.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { set, source } from '../../reactivity/sources.js';
22
import { get } from '../../runtime.js';
33
import { is_array } from '../../utils.js';
4+
import { isSnippet } from '../blocks/snippet.js';
45

56
/**
67
* Under some circumstances, imports may be reactive in legacy mode. In that case,
@@ -66,3 +67,17 @@ export function update_legacy_props($$new_props) {
6667
}
6768
}
6869
}
70+
71+
/**
72+
* @param {Record<string, any>} $$props
73+
*/
74+
export function default_slot($$props) {
75+
var children = $$props.$$slots?.default;
76+
if (children) {
77+
return children;
78+
}
79+
children = $$props.children;
80+
if (isSnippet(children)) {
81+
return children;
82+
}
83+
}

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

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isSnippet } from './dom/blocks/snippet.js';
12
import { untrack } from './runtime.js';
23
import { is_array } from './utils.js';
34

@@ -103,22 +104,12 @@ export function loop_guard(timeout) {
103104
};
104105
}
105106

106-
const snippet_symbol = Symbol.for('svelte.snippet');
107-
108-
/**
109-
* @param {any} fn
110-
*/
111-
export function add_snippet_symbol(fn) {
112-
fn[snippet_symbol] = true;
113-
return fn;
114-
}
115-
116107
/**
117108
* Validate that the function handed to `{@render ...}` is a snippet function, and not some other kind of function.
118109
* @param {any} snippet_fn
119110
*/
120111
export function validate_snippet(snippet_fn) {
121-
if (snippet_fn && snippet_fn[snippet_symbol] !== true) {
112+
if (snippet_fn && !isSnippet(snippet_fn)) {
122113
throw new Error(
123114
'The argument to `{@render ...}` must be a snippet function, not a component or some other kind of function. ' +
124115
'If you want to dynamically render one snippet or another, use `$derived` and pass its result to `{@render ...}`.'
@@ -132,7 +123,7 @@ export function validate_snippet(snippet_fn) {
132123
* @param {any} component_fn
133124
*/
134125
export function validate_component(component_fn) {
135-
if (component_fn?.[snippet_symbol] === true) {
126+
if (isSnippet(component_fn)) {
136127
throw new Error('A snippet must be rendered with `{@render ...}`');
137128
}
138129
return component_fn;

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { DEV } from 'esm-env';
1111
import { UNINITIALIZED } from '../client/constants.js';
1212

1313
export * from '../client/validate.js';
14+
export { add_snippet_symbol } from '../client/dom/blocks/snippet.js';
15+
export { default_slot } from '../client/dom/legacy/misc.js';
1416

1517
/**
1618
* @typedef {{

packages/svelte/src/legacy/legacy-client.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import * as $ from '../internal/index.js';
1212
* @template {Record<string, any>} Slots
1313
*
1414
* @param {import('../main/public.js').ComponentConstructorOptions<Props> & {
15-
* component: import('../main/public.js').SvelteComponent<Props, Events, Slots>;
15+
* component: typeof import('../main/public.js').SvelteComponent<Props, Events, Slots>;
1616
* immutable?: boolean;
1717
* hydrate?: boolean;
1818
* recover?: boolean;

packages/svelte/src/main/main-client.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,5 +181,6 @@ export {
181181
hasContext,
182182
getContext,
183183
getAllContexts,
184-
setContext
184+
setContext,
185+
isSnippet
185186
} from '../internal/index.js';

packages/svelte/src/main/main-server.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ export {
1212
tick,
1313
unmount,
1414
untrack,
15-
createRoot
15+
createRoot,
16+
isSnippet
1617
} from './main-client.js';
1718

1819
/** @returns {void} */
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script>
2+
export let children;
3+
</script>
4+
5+
{children}
6+
<slot />
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: `foo bar`
5+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
import A from "./A.svelte";
3+
</script>
4+
5+
<A children="foo">
6+
bar
7+
</A>

packages/svelte/tests/types/snippet.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Snippet } from 'svelte';
1+
import { type Snippet, isSnippet } from 'svelte';
22

33
const return_type: ReturnType<Snippet> = null as any;
44

@@ -38,3 +38,8 @@ const h: Snippet<[{ a: true }]> = (a) => {
3838
const i: Snippet = () => {
3939
return return_type;
4040
};
41+
42+
let j = null as any;
43+
if (isSnippet(j)) {
44+
let x: Snippet = j;
45+
}

packages/svelte/types/index.d.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,12 @@ declare module 'svelte' {
291291
* Anything except a function
292292
*/
293293
type NotFunction<T> = T extends Function ? never : T;
294+
/**
295+
* Returns true if given parameter is a snippet.
296+
* */
297+
export function isSnippet(maybeSnippet: any): maybeSnippet is (this: void) => unique symbol & {
298+
_: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\"";
299+
};
294300
/**
295301
* @deprecated Use `mount` or `hydrate` instead
296302
*/
@@ -1729,7 +1735,17 @@ declare module 'svelte/legacy' {
17291735
*
17301736
* */
17311737
export function createClassComponent<Props extends Record<string, any>, Exports extends Record<string, any>, Events extends Record<string, any>, Slots extends Record<string, any>>(options: ComponentConstructorOptions<Props> & {
1732-
component: SvelteComponent<Props, Events, Slots>;
1738+
component: {
1739+
new (options: ComponentConstructorOptions<Props & (Props extends {
1740+
children?: any;
1741+
} ? {} : Slots extends {
1742+
default: any;
1743+
} ? {
1744+
children?: ((this: void) => unique symbol & {
1745+
_: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\"";
1746+
}) | undefined;
1747+
} : {})>): SvelteComponent<Props, Events, Slots>;
1748+
};
17331749
immutable?: boolean | undefined;
17341750
hydrate?: boolean | undefined;
17351751
recover?: boolean | undefined;

0 commit comments

Comments
 (0)