Skip to content

Commit efe8463

Browse files
authored
fix: implict children tweaks (#2285)
- ignore comments when determining whether or not to add an implicit children prop #2263 - do static analysis of the destructuring to see whether or not children is destructured from `$props()`, and if not, add the implicit children prop if there's a slot. This results in less confusing type errors when transforming to runes mode and using a slot
1 parent 19694d6 commit efe8463

File tree

16 files changed

+202
-69
lines changed

16 files changed

+202
-69
lines changed

packages/svelte2tsx/src/svelte2tsx/createRenderFunction.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export function createRenderFunction({
111111

112112
const needsImplicitChildrenProp =
113113
svelte5Plus &&
114-
!exportedNames.uses$propsRune() &&
114+
!exportedNames.usesChildrenIn$propsRune() &&
115115
slots.has('default') &&
116116
!exportedNames.getExportsMap().has('default');
117117
if (needsImplicitChildrenProp) {

packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ interface ExportedName {
1717
identifierText?: string;
1818
required?: boolean;
1919
doc?: string;
20-
implicitChildren?: 'empty' | 'attributes';
20+
/** Set if this is the implicit children prop. `empty` == no parameters, else `has_params` */
21+
implicitChildren?: 'empty' | 'has_params';
2122
}
2223

2324
export class ExportedNames {
@@ -31,7 +32,8 @@ export class ExportedNames {
3132
*/
3233
private $props = {
3334
comment: '',
34-
generic: ''
35+
generic: '',
36+
mayHaveChildrenProp: false
3537
};
3638
private exports = new Map<string, ExportedName>();
3739
private possibleExports = new Map<
@@ -135,6 +137,29 @@ export class ExportedNames {
135137
initializer: ts.CallExpression & { expression: ts.Identifier };
136138
}
137139
): void {
140+
// Check if the $props() rune has a children prop
141+
if (ts.isObjectBindingPattern(node.name)) {
142+
for (const element of node.name.elements) {
143+
if (
144+
!ts.isIdentifier(element.name) ||
145+
(element.propertyName && !ts.isIdentifier(element.propertyName)) ||
146+
!!element.dotDotDotToken
147+
) {
148+
// not statically analyzable, so we assume it may have children
149+
this.$props.mayHaveChildrenProp = true;
150+
} else {
151+
const name = element.propertyName
152+
? (element.propertyName as ts.Identifier).text
153+
: element.name.text;
154+
if (name === 'children') {
155+
this.$props.mayHaveChildrenProp = true;
156+
}
157+
}
158+
}
159+
} else {
160+
this.$props.mayHaveChildrenProp = true;
161+
}
162+
138163
if (node.initializer.typeArguments?.length > 0) {
139164
const generic_arg = node.initializer.typeArguments[0];
140165
const generic = generic_arg.getText();
@@ -201,7 +226,8 @@ export class ExportedNames {
201226
}
202227

203228
if (internalHelpers.isKitRouteFile(this.basename)) {
204-
const kitType = this.basename.includes('layout')
229+
this.$props.mayHaveChildrenProp = this.basename.includes('layout');
230+
const kitType = this.$props.mayHaveChildrenProp
205231
? `{ data: import('./$types.js').LayoutData, form: import('./$types.js').ActionData, children: import('svelte').Snippet }`
206232
: `{ data: import('./$types.js').PageData, form: import('./$types.js').ActionData }`;
207233

@@ -498,12 +524,12 @@ export class ExportedNames {
498524
}
499525
}
500526

501-
addImplicitChildrenExport(hasAttributes: boolean): void {
527+
addImplicitChildrenExport(hasParams: boolean): void {
502528
if (this.exports.has('children')) return;
503529

504530
this.exports.set('children', {
505531
isLet: true,
506-
implicitChildren: hasAttributes ? 'attributes' : 'empty'
532+
implicitChildren: hasParams ? 'has_params' : 'empty'
507533
});
508534
}
509535

@@ -589,7 +615,9 @@ export class ExportedNames {
589615
const names = Array.from(this.exports.entries());
590616

591617
if (this.$props.generic) {
592-
const others = names.filter(([, { isLet }]) => !isLet);
618+
const others = names.filter(
619+
([, { isLet, implicitChildren }]) => !isLet || !!implicitChildren
620+
);
593621
return (
594622
'{} as any as ' +
595623
this.$props.generic +
@@ -600,8 +628,23 @@ export class ExportedNames {
600628
}
601629

602630
if (this.$props.comment) {
603-
// TODO: createReturnElements would need to be incorporated here, but don't bother for now.
604-
// In the long run it's probably better to have them on a different object anyway.
631+
// Try our best to incorporate createReturnElementsType here
632+
const others = names.filter(
633+
([, { isLet, implicitChildren }]) => !isLet || !!implicitChildren
634+
);
635+
let idx = this.$props.comment.indexOf('@type');
636+
if (idx !== -1 && /[\s{]/.test(this.$props.comment[idx + 5]) && others.length > 0) {
637+
idx = this.$props.comment.indexOf('{', idx);
638+
if (idx !== -1) {
639+
idx++;
640+
return (
641+
this.$props.comment.slice(0, idx) +
642+
`{${this.createReturnElementsType(others, false)}} & ` +
643+
this.$props.comment.slice(idx) +
644+
'({})'
645+
);
646+
}
647+
}
605648
return this.$props.comment + '({})';
606649
}
607650

@@ -666,7 +709,7 @@ export class ExportedNames {
666709
});
667710
}
668711

669-
private createReturnElementsType(names: Array<[string, ExportedName]>) {
712+
private createReturnElementsType(names: Array<[string, ExportedName]>, addDoc = true) {
670713
return names.map(([key, value]) => {
671714
if (value.implicitChildren) {
672715
return `children?: ${
@@ -676,9 +719,9 @@ export class ExportedNames {
676719
}`;
677720
}
678721

679-
const identifier = `${value.doc ? `\n${value.doc}` : ''}${value.identifierText || key}${
680-
value.required ? '' : '?'
681-
}`;
722+
const identifier = `${value.doc && addDoc ? `\n${value.doc}` : ''}${
723+
value.identifierText || key
724+
}${value.required ? '' : '?'}`;
682725
if (!value.type) {
683726
return `${identifier}: typeof ${key}`;
684727
}
@@ -697,7 +740,7 @@ export class ExportedNames {
697740
return this.exports;
698741
}
699742

700-
uses$propsRune() {
701-
return !!this.$props.generic || !!this.$props.comment;
743+
usesChildrenIn$propsRune() {
744+
return this.$props.mayHaveChildrenProp;
702745
}
703746
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
///<reference types="svelte" />
2+
;function render() {
3+
4+
/** @type {SomeType} */
5+
let { a, b } = $props();
6+
let x = $state(0);
7+
let y = $derived(x * 2);
8+
9+
/*Ωignore_startΩ*/;const __sveltets_createSlot = __sveltets_2_createCreateSlot();/*Ωignore_endΩ*/;
10+
async () => {
11+
12+
{ __sveltets_createSlot("default", { x,y,});}};
13+
let $$implicit_children = __sveltets_2_snippet({x:x, y:y});
14+
return { props: /** @type {{children?: typeof $$implicit_children} & SomeType} */({}), slots: {'default': {x:x, y:y}}, events: {} }}
15+
16+
export default class Input__SvelteComponent_ extends __sveltets_2_createSvelte2TsxComponent(__sveltets_2_partial(['children'], __sveltets_2_with_any_event(render()))) {
17+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
///<reference types="svelte" />
2+
;function render() {
3+
4+
/** @type {SomeType} */
5+
let { a, b } = $props();
6+
let x = $state(0);
7+
let y = $derived(x * 2);
8+
9+
/*Ωignore_startΩ*/;const __sveltets_createSlot = __sveltets_2_createCreateSlot();/*Ωignore_endΩ*/;
10+
async () => {
11+
12+
{ __sveltets_createSlot("default", { x,y,});}};
13+
return { props: /** @type {SomeType} */({}), slots: {'default': {x:x, y:y}}, events: {} }}
14+
15+
export default class Input__SvelteComponent_ extends __sveltets_2_createSvelte2TsxComponent(__sveltets_2_partial(__sveltets_2_with_any_event(render()))) {
16+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script>
2+
/** @type {SomeType} */
3+
let { a, b } = $props();
4+
let x = $state(0);
5+
let y = $derived(x * 2);
6+
</script>
7+
8+
<slot {x} {y} />
Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
///<reference types="svelte" />
22
;function render() {
33

4-
/** @type {a: number, b: string} */
4+
/** @typedef {{a: number, b: string}} $$_sveltets_Props *//** @type {$$_sveltets_Props} */
55
let { a, b } = $props();
66
let x = $state(0);
77
let y = $derived(x * 2);
8-
9-
/*Ωignore_startΩ*/;const __sveltets_createSlot = __sveltets_2_createCreateSlot();/*Ωignore_endΩ*/;
10-
async () => {
11-
12-
{ __sveltets_createSlot("default", { x,y,});}};
13-
return { props: /** @type {a: number, b: string} */({}), slots: {'default': {x:x, y:y}}, events: {} }}
8+
;
9+
async () => {};
10+
return { props: /** @type {$$_sveltets_Props} */({}), slots: {}, events: {} }}
1411

1512
export default class Input__SvelteComponent_ extends __sveltets_2_createSvelte2TsxComponent(__sveltets_2_partial(__sveltets_2_with_any_event(render()))) {
1613
}
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
<script>
2-
/** @type {a: number, b: string} */
2+
/** @type {{a: number, b: string}} */
33
let { a, b } = $props();
44
let x = $state(0);
55
let y = $derived(x * 2);
66
</script>
7-
8-
<slot {x} {y} />

packages/svelte2tsx/test/svelte2tsx/samples/sveltekit-autotypes-$props-rune-no-changes/expectedv2.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
const snapshot = {};
88
;
99
async () => {};
10-
return { props: /** @type {$$_sveltets_Props} */({}), slots: {}, events: {} }}
10+
return { props: /** @type {{snapshot?: typeof snapshot} & $$_sveltets_Props} */({}), slots: {}, events: {} }}
1111

1212
export default class Page__SvelteComponent_ extends __sveltets_2_createSvelte2TsxComponent(__sveltets_2_partial(['snapshot'], __sveltets_2_with_any_event(render()))) {
1313
get snapshot() { return __sveltets_2_nonNullable(this.$$prop_def.snapshot) }

packages/svelte2tsx/test/svelte2tsx/samples/sveltekit-autotypes-$props-rune/expectedv2.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
const snapshot/*Ωignore_startΩ*/: import('./$types.js').Snapshot/*Ωignore_endΩ*/ = {};
66
;
77
async () => {};
8-
return { props: /** @type {{ data: import('./$types.js').PageData, form: import('./$types.js').ActionData }} */({}), slots: {}, events: {} }}
8+
return { props: /** @type {{snapshot?: typeof snapshot} & { data: import('./$types.js').PageData, form: import('./$types.js').ActionData }} */({}), slots: {}, events: {} }}
99

1010
export default class Page__SvelteComponent_ extends __sveltets_2_createSvelte2TsxComponent(__sveltets_2_partial(['snapshot'], __sveltets_2_with_any_event(render()))) {
1111
get snapshot() { return __sveltets_2_nonNullable(this.$$prop_def.snapshot) }
Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
///<reference types="svelte" />
2-
;function render() {
3-
;type $$_sveltets_Props = { a: number, b: string };
2+
;function render<T>() {
3+
;type $$_sveltets_Props = { a: T, b: string };
44
let { a, b } = $props<$$_sveltets_Props>();
5-
let x = $state(0);
5+
let x = $state<T>(0);
66
let y = $derived(x * 2);
7+
;
8+
async () => {};
9+
return { props: {} as any as $$_sveltets_Props, slots: {}, events: {} }}
10+
class __sveltets_Render<T> {
11+
props() {
12+
return render<T>().props;
13+
}
14+
events() {
15+
return __sveltets_2_with_any_event(render<T>()).events;
16+
}
17+
slots() {
18+
return render<T>().slots;
19+
}
20+
}
721

8-
/*Ωignore_startΩ*/;const __sveltets_createSlot = __sveltets_2_createCreateSlot();/*Ωignore_endΩ*/;
9-
async () => {
1022

11-
{ __sveltets_createSlot("default", { x,y,});}};
12-
return { props: {} as any as $$_sveltets_Props, slots: {'default': {x:x, y:y}}, events: {} }}
13-
14-
export default class Input__SvelteComponent_ extends __sveltets_2_createSvelte2TsxComponent(__sveltets_2_with_any_event(render())) {
23+
import { SvelteComponentTyped as __SvelteComponentTyped__ } from "svelte"
24+
export default class Input__SvelteComponent_<T> extends __SvelteComponentTyped__<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> {
1525
}
Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
<script lang="ts">
2-
let { a, b } = $props<{ a: number, b: string }>();
3-
let x = $state(0);
1+
<script lang="ts" generics="T">
2+
let { a, b } = $props<{ a: T, b: string }>();
3+
let x = $state<T>(0);
44
let y = $derived(x * 2);
55
</script>
6-
7-
<slot {x} {y} />
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
///<reference types="svelte" />
2+
;function render<T>() {
3+
;type $$_sveltets_Props = { a: T, b: string };
4+
let { a, b } = $props<$$_sveltets_Props>();
5+
let x = $state<T>(0);
6+
let y = $derived(x * 2);
7+
8+
/*Ωignore_startΩ*/;const __sveltets_createSlot = __sveltets_2_createCreateSlot();/*Ωignore_endΩ*/;
9+
async () => {
10+
11+
{ __sveltets_createSlot("default", { x,y,});}};
12+
let $$implicit_children = __sveltets_2_snippet({x:x, y:y});
13+
return { props: {} as any as $$_sveltets_Props & { children?: typeof $$implicit_children }, slots: {'default': {x:x, y:y}}, events: {} }}
14+
class __sveltets_Render<T> {
15+
props() {
16+
return render<T>().props;
17+
}
18+
events() {
19+
return __sveltets_2_with_any_event(render<T>()).events;
20+
}
21+
slots() {
22+
return render<T>().slots;
23+
}
24+
}
25+
26+
27+
import { SvelteComponentTyped as __SvelteComponentTyped__ } from "svelte"
28+
export default class Input__SvelteComponent_<T> extends __SvelteComponentTyped__<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> {
29+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
///<reference types="svelte" />
2+
;function render<T>() {
3+
;type $$_sveltets_Props = { a: T, b: string };
4+
let { a, b } = $props<$$_sveltets_Props>();
5+
let x = $state<T>(0);
6+
let y = $derived(x * 2);
7+
8+
/*Ωignore_startΩ*/;const __sveltets_createSlot = __sveltets_2_createCreateSlot();/*Ωignore_endΩ*/;
9+
async () => {
10+
11+
{ __sveltets_createSlot("default", { x,y,});}};
12+
return { props: {} as any as $$_sveltets_Props, slots: {'default': {x:x, y:y}}, events: {} }}
13+
class __sveltets_Render<T> {
14+
props() {
15+
return render<T>().props;
16+
}
17+
events() {
18+
return __sveltets_2_with_any_event(render<T>()).events;
19+
}
20+
slots() {
21+
return render<T>().slots;
22+
}
23+
}
24+
25+
26+
import { SvelteComponentTyped as __SvelteComponentTyped__ } from "svelte"
27+
export default class Input__SvelteComponent_<T> extends __SvelteComponentTyped__<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> {
28+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script lang="ts" generics="T">
2+
let { a, b } = $props<{ a: T, b: string }>();
3+
let x = $state<T>(0);
4+
let y = $derived(x * 2);
5+
</script>
6+
7+
<slot {x} {y} />

0 commit comments

Comments
 (0)