Skip to content

Commit 49a0908

Browse files
fix: correctly migrate $$slots with bracket member expressions & slots with static props (#13468)
- fix: correctly migrate `$$slots` with bracket member expressions - fix: wrong type for JSDoc `$$restProps` - fix: correctly migrate slots with static props - fix: `$$props` messing with `$$slots` Fixes #13467, fixes #13466, fixes #13472
1 parent 785779a commit 49a0908

File tree

10 files changed

+141
-21
lines changed

10 files changed

+141
-21
lines changed

.changeset/nine-kids-whisper.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: correctly migrate `$$slots` with bracket member expressions & slots with static props

packages/svelte/src/compiler/migrate/index.js

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ export function migrate(source) {
192192
}
193193
} else {
194194
if (analysis.uses_props || analysis.uses_rest_props) {
195-
type = `{Record<string, any>}`;
195+
type = `Record<string, any>`;
196196
} else {
197197
type = `{${state.props
198198
.map((prop) => {
@@ -294,7 +294,7 @@ export function migrate(source) {
294294
* str: MagicString;
295295
* analysis: ComponentAnalysis;
296296
* indent: string;
297-
* props: Array<{ local: string; exported: string; init: string; bindable: boolean; slot_name?: string; optional: boolean; type: string; comment?: string, type_only?: boolean }>;
297+
* props: Array<{ local: string; exported: string; init: string; bindable: boolean; slot_name?: string; optional: boolean; type: string; comment?: string; type_only?: boolean; needs_refine_type?: boolean; }>;
298298
* props_insertion_point: number;
299299
* has_props_rune: boolean;
300300
* end: number;
@@ -704,7 +704,7 @@ const template = {
704704
handle_events(node, state);
705705
next();
706706
},
707-
SlotElement(node, { state, next }) {
707+
SlotElement(node, { state, next, visit }) {
708708
if (state.analysis.custom_element) return;
709709
let name = 'children';
710710
let slot_name = 'default';
@@ -719,13 +719,22 @@ const template = {
719719
} else {
720720
const attr_value =
721721
attr.value === true || Array.isArray(attr.value) ? attr.value : [attr.value];
722-
const value =
723-
attr_value !== true
724-
? state.str.original.substring(
725-
attr_value[0].start,
726-
attr_value[attr_value.length - 1].end
727-
)
728-
: 'true';
722+
let value = 'true';
723+
if (attr_value !== true) {
724+
const first = attr_value[0];
725+
const last = attr_value[attr_value.length - 1];
726+
for (const attr of attr_value) {
727+
visit(attr);
728+
}
729+
value = state.str
730+
.snip(
731+
first.type === 'Text'
732+
? first.start - 1
733+
: /** @type {number} */ (first.expression.start),
734+
last.type === 'Text' ? last.end + 1 : /** @type {number} */ (last.expression.end)
735+
)
736+
.toString();
737+
}
729738
slot_props += value === attr.name ? `${value}, ` : `${attr.name}: ${value}, `;
730739
}
731740
}
@@ -753,18 +762,25 @@ const template = {
753762
slot_name,
754763
type: `import('svelte').${slot_props ? 'Snippet<[any]>' : 'Snippet'}`
755764
});
765+
} else if (existing_prop.needs_refine_type) {
766+
existing_prop.type = `import('svelte').${slot_props ? 'Snippet<[any]>' : 'Snippet'}`;
767+
existing_prop.needs_refine_type = false;
756768
}
757769

758770
if (node.fragment.nodes.length > 0) {
759771
next();
760772
state.str.update(
761773
node.start,
762774
node.fragment.nodes[0].start,
763-
`{#if ${name}}{@render ${name}(${slot_props})}{:else}`
775+
`{#if ${name}}{@render ${state.analysis.uses_props ? `${state.names.props}.` : ''}${name}(${slot_props})}{:else}`
764776
);
765777
state.str.update(node.fragment.nodes[node.fragment.nodes.length - 1].end, node.end, '{/if}');
766778
} else {
767-
state.str.update(node.start, node.end, `{@render ${name}?.(${slot_props})}`);
779+
state.str.update(
780+
node.start,
781+
node.end,
782+
`{@render ${state.analysis.uses_props ? `${state.names.props}.` : ''}${name}?.(${slot_props})}`
783+
);
768784
}
769785
},
770786
Comment(node, { state }) {
@@ -958,7 +974,7 @@ function handle_identifier(node, state, path) {
958974
const parent = path.at(-1);
959975
if (parent?.type === 'MemberExpression' && parent.property === node) return;
960976

961-
if (state.analysis.uses_props) {
977+
if (state.analysis.uses_props && node.name !== '$$slots') {
962978
if (node.name === '$$props' || node.name === '$$restProps') {
963979
// not 100% correct for $$restProps but it'll do
964980
state.str.update(
@@ -980,10 +996,42 @@ function handle_identifier(node, state, path) {
980996
);
981997
} else if (node.name === '$$slots' && state.analysis.uses_slots) {
982998
if (parent?.type === 'MemberExpression') {
983-
state.str.update(/** @type {number} */ (node.start), parent.property.start, '');
984-
if (parent.property.name === 'default') {
985-
state.str.update(parent.property.start, parent.property.end, 'children');
999+
if (state.analysis.custom_element) return;
1000+
1001+
let name = parent.property.type === 'Literal' ? parent.property.value : parent.property.name;
1002+
let slot_name = name;
1003+
const existing_prop = state.props.find((prop) => prop.slot_name === name);
1004+
if (existing_prop) {
1005+
name = existing_prop.local;
1006+
} else if (name !== 'default') {
1007+
name = state.scope.generate(name);
1008+
}
1009+
1010+
name = name === 'default' ? 'children' : name;
1011+
1012+
if (!existing_prop) {
1013+
state.props.push({
1014+
local: name,
1015+
exported: name,
1016+
init: '',
1017+
bindable: false,
1018+
optional: true,
1019+
slot_name,
1020+
// if it's the first time we encounter this slot
1021+
// we start with any and delegate to when the slot
1022+
// is actually rendered (it might not happen in that case)
1023+
// any is still a safe bet
1024+
type: `import('svelte').Snippet<[any]>}`,
1025+
needs_refine_type: true
1026+
});
9861027
}
1028+
1029+
state.str.update(
1030+
/** @type {number} */ (node.start),
1031+
parent.property.start,
1032+
state.analysis.uses_props ? `${state.names.props}.` : ''
1033+
);
1034+
state.str.update(parent.property.start, parent.end, name);
9871035
}
9881036
// else passed as identifier, we don't know what to do here, so let it error
9891037
} else if (

packages/svelte/tests/migrate/samples/props-rest-props/output.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script>
2-
/** @type {{Record<string, any>}} */
2+
/** @type {Record<string, any>} */
33
let { foo, ...rest } = $props();
44
</script>
55

packages/svelte/tests/migrate/samples/slots-custom-element/input.svelte

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,8 @@
2020

2121
{#if $$slots.default}foo{/if}
2222

23+
{#if $$slots['default']}foo{/if}
24+
25+
{#if $$slots['dashed-name']}foo{/if}
26+
2327
<slot name="dashed-name" />

packages/svelte/tests/migrate/samples/slots-custom-element/output.svelte

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,15 @@
1313
<slot name="foo" {foo} />
1414
{/if}
1515

16-
{#if bar}
16+
{#if $$slots.bar}
1717
{$$slots}
1818
<slot name="bar" />
1919
{/if}
2020

21-
{#if children}foo{/if}
21+
{#if $$slots.default}foo{/if}
22+
23+
{#if $$slots['default']}foo{/if}
24+
25+
{#if $$slots['dashed-name']}foo{/if}
2226

2327
<slot name="dashed-name" />
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<button><slot /></button>
2+
3+
{#if foo}
4+
<slot name="foo" {foo} />
5+
{/if}
6+
7+
{#if $$slots.bar}
8+
{$$slots}
9+
<slot name="bar" />
10+
{/if}
11+
12+
{#if $$slots.default}foo{/if}
13+
14+
{#if $$slots['default']}foo{/if}
15+
16+
{#if $$slots['dashed-name']}foo{/if}
17+
18+
<slot name="dashed-name" />
19+
20+
<slot header="something" title={$$props.cool} {id} />
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<script>
2+
/** @type {Record<string, any>} */
3+
let {
4+
...props
5+
} = $props();
6+
</script>
7+
8+
<button>{@render props.children?.()}</button>
9+
10+
{#if foo}
11+
{@render props.foo_1?.({ foo, })}
12+
{/if}
13+
14+
{#if props.bar}
15+
{$$slots}
16+
{@render props.bar?.()}
17+
{/if}
18+
19+
{#if props.children}foo{/if}
20+
21+
{#if props.children}foo{/if}
22+
23+
{#if props.dashed_name}foo{/if}
24+
25+
{@render props.dashed_name?.()}
26+
27+
{@render props.children?.({ header: "something", title: props.cool, id, })}

packages/svelte/tests/migrate/samples/slots/input.svelte

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,10 @@
1111

1212
{#if $$slots.default}foo{/if}
1313

14+
{#if $$slots['default']}foo{/if}
15+
16+
{#if $$slots['dashed-name']}foo{/if}
17+
1418
<slot name="dashed-name" />
19+
20+
<slot header="something" title={my_title} {id} />

packages/svelte/tests/migrate/samples/slots/output.svelte

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,10 @@
2121

2222
{#if children}foo{/if}
2323

24-
{@render dashed_name?.()}
24+
{#if children}foo{/if}
25+
26+
{#if dashed_name}foo{/if}
27+
28+
{@render dashed_name?.()}
29+
30+
{@render children?.({ header: "something", title: my_title, id, })}

packages/svelte/tests/migrate/samples/svelte-component/output.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script>
2-
/** @type {{Record<string, any>}} */
2+
/** @type {Record<string, any>} */
33
let { ...rest } = $props();
44
let Component;
55
let fallback;

0 commit comments

Comments
 (0)