Skip to content

Commit f5ec504

Browse files
authored
feat: support rename/find reference for $props() (#2255)
Generate a virtual type that is reused for the type/generics and the generated props type which makes renaming/find references etc "just work" without additional code in the language server #2253
1 parent b05c8ee commit f5ec504

File tree

14 files changed

+131
-46
lines changed

14 files changed

+131
-46
lines changed

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

Lines changed: 78 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -136,23 +136,64 @@ export class ExportedNames {
136136
}
137137
): void {
138138
if (node.initializer.typeArguments?.length > 0) {
139-
this.$props.generic = node.initializer.typeArguments[0].getText();
139+
const generic_arg = node.initializer.typeArguments[0];
140+
const generic = generic_arg.getText();
141+
if (!generic.includes('{')) {
142+
this.$props.generic = generic;
143+
} else {
144+
// Create a virtual type alias for the unnamed generic and reuse it for the props return type
145+
// so that rename, find references etc works seamlessly across components
146+
this.$props.generic = '$$_sveltets_Props';
147+
preprendStr(
148+
this.str,
149+
generic_arg.pos + this.astOffset,
150+
`;type ${this.$props.generic} = `
151+
);
152+
this.str.appendLeft(generic_arg.end + this.astOffset, ';');
153+
this.str.move(
154+
generic_arg.pos + this.astOffset,
155+
generic_arg.end + this.astOffset,
156+
node.parent.pos + this.astOffset
157+
);
158+
this.str.appendRight(generic_arg.end + this.astOffset, this.$props.generic);
159+
}
140160
} else {
141161
if (!this.isTsFile) {
142162
const text = node.getSourceFile().getFullText();
143-
let comments = ts
144-
.getLeadingCommentRanges(text, node.pos)
145-
?.map((c) => text.substring(c.pos, c.end))
146-
.find((c) => c.includes('@type'));
147-
if (!comments) {
148-
comments = ts
149-
.getLeadingCommentRanges(text, node.parent.pos)
150-
?.map((c) => text.substring(c.pos, c.end))
151-
.find((c) => c.includes('@type'));
163+
let start = -1;
164+
let comment: string;
165+
for (const c of ts.getLeadingCommentRanges(text, node.pos) || []) {
166+
const potential_match = text.substring(c.pos, c.end);
167+
if (potential_match.includes('@type')) {
168+
comment = potential_match;
169+
start = c.pos + this.astOffset;
170+
break;
171+
}
172+
}
173+
if (!comment) {
174+
for (const c of ts.getLeadingCommentRanges(text, node.parent.pos) || []) {
175+
const potential_match = text.substring(c.pos, c.end);
176+
if (potential_match.includes('@type')) {
177+
comment = potential_match;
178+
start = c.pos + this.astOffset;
179+
break;
180+
}
181+
}
152182
}
153183

154-
// We don't bother extracting the type, we just use the comment as-is
155-
this.$props.comment = comments || '';
184+
if (comment && /\/\*\*[^@]*?@type\s*{\s*{.*}\s*}\s*\*\//.test(comment)) {
185+
// Create a virtual type alias for the unnamed generic and reuse it for the props return type
186+
// so that rename, find references etc works seamlessly across components
187+
this.$props.comment = '/** @type {$$_sveltets_Props} */';
188+
const type_start = this.str.original.indexOf('@type', start);
189+
this.str.overwrite(type_start, type_start + 5, '@typedef');
190+
const end = this.str.original.indexOf('*/', start);
191+
this.str.overwrite(end, end + 2, ' $$_sveltets_Props */' + this.$props.comment);
192+
} else {
193+
// Complex comment or simple `@type {AType}` comment which we just use as-is.
194+
// For the former this means things like rename won't work properly across components.
195+
this.$props.comment = comment || '';
196+
}
156197
}
157198

158199
if (this.$props.comment) {
@@ -183,9 +224,16 @@ export class ExportedNames {
183224

184225
if (ts.isObjectBindingPattern(node.name)) {
185226
for (const element of node.name.elements) {
186-
if (!ts.isIdentifier(element.name) || !!element.dotDotDotToken) {
227+
if (
228+
!ts.isIdentifier(element.name) ||
229+
(element.propertyName && !ts.isIdentifier(element.propertyName)) ||
230+
!!element.dotDotDotToken
231+
) {
187232
withUnknown = true;
188233
} else {
234+
const name = element.propertyName
235+
? (element.propertyName as ts.Identifier).text
236+
: element.name.text;
189237
if (element.initializer) {
190238
const type = ts.isAsExpression(element.initializer)
191239
? element.initializer.type.getText()
@@ -199,9 +247,9 @@ export class ExportedNames {
199247
: ts.isIdentifier(element.initializer)
200248
? `typeof ${element.initializer.text}`
201249
: 'unknown';
202-
props.push(`${element.name.text}?: ${type}`);
250+
props.push(`${name}?: ${type}`);
203251
} else {
204-
props.push(`${element.name.text}: unknown`);
252+
props.push(`${name}: unknown`);
205253
}
206254
}
207255
}
@@ -219,19 +267,30 @@ export class ExportedNames {
219267
propsStr = 'Record<string, unknown>';
220268
}
221269

270+
// Create a virtual type alias for the unnamed generic and reuse it for the props return type
271+
// so that rename, find references etc works seamlessly across components
222272
if (this.isTsFile) {
223-
this.$props.generic = propsStr;
273+
this.$props.generic = '$$_sveltets_Props';
224274
if (props.length > 0 || withUnknown) {
275+
preprendStr(
276+
this.str,
277+
node.parent.pos + this.astOffset,
278+
surroundWithIgnoreComments(`;type $$_sveltets_Props = ${propsStr};`)
279+
);
225280
preprendStr(
226281
this.str,
227282
node.initializer.expression.end + this.astOffset,
228-
surroundWithIgnoreComments(`<${propsStr}>`)
283+
`<${this.$props.generic}>`
229284
);
230285
}
231286
} else {
232-
this.$props.comment = `/** @type {${propsStr}} */`;
287+
this.$props.comment = '/** @type {$$_sveltets_Props} */';
233288
if (props.length > 0 || withUnknown) {
234-
preprendStr(this.str, node.pos + this.astOffset, this.$props.comment);
289+
preprendStr(
290+
this.str,
291+
node.pos + this.astOffset,
292+
`/** @typedef {${propsStr}} $$_sveltets_Props */${this.$props.comment}`
293+
);
235294
}
236295
}
237296
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
///<reference types="svelte" />
22
;function render() {
33

4-
let/** @type {{ a: unknown, b?: boolean, c?: number, d?: string, e?: unknown, f?: unknown, g?: typeof foo }} */ { a, b = true, c = 1, d = '', e = null, f = {}, g = foo } = $props();
4+
let/** @typedef {{ a: unknown, b?: boolean, c?: number, d?: string, e?: unknown, f?: unknown, g?: typeof foo }} $$_sveltets_Props *//** @type {$$_sveltets_Props} */ { a, b = true, c = 1, d = '', e = null, f = {}, g = foo } = $props();
55
;
66
async () => {};
7-
return { props: /** @type {{ a: unknown, b?: boolean, c?: number, d?: string, e?: unknown, f?: unknown, g?: typeof foo }} */({}), slots: {}, events: {} }}
7+
return { props: /** @type {$$_sveltets_Props} */({}), slots: {}, events: {} }}
88

99
export default class Input__SvelteComponent_ extends __sveltets_2_createSvelte2TsxComponent(__sveltets_2_partial(__sveltets_2_with_any_event(render()))) {
1010
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
///<reference types="svelte" />
22
;function render() {
33

4-
let/** @type {{ props: unknown }} */ { props } = $props();
4+
let/** @typedef {{ props: unknown }} $$_sveltets_Props *//** @type {$$_sveltets_Props} */ { props } = $props();
55
let state = $state(0);
66
let derived = $derived(state * 2);
77
;
88
async () => {
99

1010
state; derived;};
11-
return { props: /** @type {{ props: unknown }} */({}), slots: {}, events: {} }}
11+
return { props: /** @type {$$_sveltets_Props} */({}), slots: {}, events: {} }}
1212

1313
export default class Input__SvelteComponent_ extends __sveltets_2_createSvelte2TsxComponent(__sveltets_2_partial(__sveltets_2_with_any_event(render()))) {
1414
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script>
2+
/** @type {{form: boolean, data: true }} */
3+
let { form, data } = $props();
4+
/** @type {any} */
5+
export const snapshot = {};
6+
</script>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
///<reference types="svelte" />
2+
;function render() {
3+
4+
/** @typedef {{form: boolean, data: true }} $$_sveltets_Props *//** @type {$$_sveltets_Props} */
5+
let { form, data } = $props();
6+
/** @type {any} */
7+
const snapshot = {};
8+
;
9+
async () => {};
10+
return { props: /** @type {$$_sveltets_Props} */({}), slots: {}, events: {} }}
11+
12+
export default class Page__SvelteComponent_ extends __sveltets_2_createSvelte2TsxComponent(__sveltets_2_partial(['snapshot'], __sveltets_2_with_any_event(render()))) {
13+
get snapshot() { return __sveltets_2_nonNullable(this.$$prop_def.snapshot) }
14+
}
Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
<script>
22
let { form, data } = $props();
33
export const snapshot = {};
4-
5-
/** @type {{form: boolean, data: true }} */
6-
let { form, data } = $props();
74
</script>

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@
33

44
let/** @type {{ data: import('./$types.js').PageData, form: import('./$types.js').ActionData }} */ { form, data } = $props();
55
const snapshot/*Ωignore_startΩ*/: import('./$types.js').Snapshot/*Ωignore_endΩ*/ = {};
6-
7-
/** @type {{form: boolean, data: true }} */
8-
let { form, data } = $props();
96
;
107
async () => {};
11-
return { props: /** @type {{form: boolean, data: true }} */({}), slots: {}, events: {} }}
8+
return { props: /** @type {{ data: import('./$types.js').PageData, form: import('./$types.js').ActionData }} */({}), slots: {}, events: {} }}
129

1310
export default class Page__SvelteComponent_ extends __sveltets_2_createSvelte2TsxComponent(__sveltets_2_partial(['snapshot'], __sveltets_2_with_any_event(render()))) {
1411
get snapshot() { return __sveltets_2_nonNullable(this.$$prop_def.snapshot) }
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
///<reference types="svelte" />
22
;function render() {
3-
4-
let { a, b = true, c = 1, d = '', e = null, f = {}, g = foo, h = null as Bar, i = null as any as Baz } = $props/*Ωignore_startΩ*/<{ a: unknown, b?: boolean, c?: number, d?: string, e?: unknown, f?: unknown, g?: typeof foo, h?: Bar, i?: Baz }>/*Ωignore_endΩ*/();
3+
/*Ωignore_startΩ*/;type $$_sveltets_Props = { a: unknown, b?: boolean, c?: number, d?: string, e?: unknown, f?: unknown, g?: typeof foo, h?: Bar, i?: Baz };/*Ωignore_endΩ*/
4+
let { a, b = true, c = 1, d = '', e = null, f = {}, g = foo, h = null as Bar, i = null as any as Baz } = $props<$$_sveltets_Props>();
55
;
66
async () => {};
7-
return { props: {} as any as { a: unknown, b?: boolean, c?: number, d?: string, e?: unknown, f?: unknown, g?: typeof foo, h?: Bar, i?: Baz }, slots: {}, events: {} }}
7+
return { props: {} as any as $$_sveltets_Props, slots: {}, events: {} }}
88

99
export default class Input__SvelteComponent_ extends __sveltets_2_createSvelte2TsxComponent(__sveltets_2_with_any_event(render())) {
1010
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
///<reference types="svelte" />
22
;function render() {
3-
4-
let { a, b } = $props<{ a: number, b: string }>();
3+
;type $$_sveltets_Props = { a: number, b: string };
4+
let { a, b } = $props<$$_sveltets_Props>();
55
let x = $state(0);
66
let y = $derived(x * 2);
77

88
/*Ωignore_startΩ*/;const __sveltets_createSlot = __sveltets_2_createCreateSlot();/*Ωignore_endΩ*/;
99
async () => {
1010

1111
{ __sveltets_createSlot("default", { x,y,});}};
12-
return { props: {} as any as { a: number, b: string }, slots: {'default': {x:x, y:y}}, events: {} }}
12+
return { props: {} as any as $$_sveltets_Props, slots: {'default': {x:x, y:y}}, events: {} }}
1313

1414
export default class Input__SvelteComponent_ extends __sveltets_2_createSvelte2TsxComponent(__sveltets_2_with_any_event(render())) {
1515
}

packages/svelte2tsx/test/svelte2tsx/samples/ts-runes/expectedv2.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
///<reference types="svelte" />
22
;function render<T>() {
3-
4-
let { a, b } = $props<{ a: T, b: string }>();
3+
;type $$_sveltets_Props = { a: T, b: string };
4+
let { a, b } = $props<$$_sveltets_Props>();
55
let x = $state<T>(0);
66
let y = $derived(x * 2);
77

88
/*Ωignore_startΩ*/;const __sveltets_createSlot = __sveltets_2_createCreateSlot();/*Ωignore_endΩ*/;
99
async () => {
1010

1111
{ __sveltets_createSlot("default", { x,y,});}};
12-
return { props: {} as any as { a: T, b: string }, slots: {'default': {x:x, y:y}}, events: {} }}
12+
return { props: {} as any as $$_sveltets_Props, slots: {'default': {x:x, y:y}}, events: {} }}
1313
class __sveltets_Render<T> {
1414
props() {
1515
return render<T>().props;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<script>
2+
export const snapshot: any = {};
3+
let { form, data } = $props<{form: boolean, data: true }>();
4+
</script>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
///<reference types="svelte" />
2+
;function render() {
3+
4+
const snapshot: any = {};;type $$_sveltets_Props = {form: boolean, data: true };
5+
let { form, data } = $props<$$_sveltets_Props>();
6+
;
7+
async () => {};
8+
return { props: {} as any as $$_sveltets_Props & { snapshot?: any }, slots: {}, events: {} }}
9+
10+
export default class Page__SvelteComponent_ extends __sveltets_2_createSvelte2TsxComponent(__sveltets_2_with_any_event(render())) {
11+
get snapshot() { return __sveltets_2_nonNullable(this.$$prop_def.snapshot) }
12+
}
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
<script>
2-
let { form, data } = $props();
32
export const snapshot = {};
4-
5-
let { form, data } = $props<{form: boolean, data: true }>();
3+
let { form, data } = $props();
64
</script>

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
///<reference types="svelte" />
22
;function render() {
33

4-
let { form, data } = $props/*Ωignore_startΩ*/<{ data: import('./$types.js').PageData, form: import('./$types.js').ActionData }>/*Ωignore_endΩ*/();
54
const snapshot/*Ωignore_startΩ*/: import('./$types.js').Snapshot/*Ωignore_endΩ*/ = {};
6-
7-
let { form, data } = $props<{form: boolean, data: true }>();
5+
let { form, data } = $props/*Ωignore_startΩ*/<{ data: import('./$types.js').PageData, form: import('./$types.js').ActionData }>/*Ωignore_endΩ*/();
86
;
97
async () => {};
10-
return { props: {} as any as {form: boolean, data: true } & { snapshot?: typeof snapshot }, slots: {}, events: {} }}
8+
return { props: {} as any as { data: import('./$types.js').PageData, form: import('./$types.js').ActionData } & { snapshot?: typeof snapshot }, slots: {}, events: {} }}
119

1210
export default class Page__SvelteComponent_ extends __sveltets_2_createSvelte2TsxComponent(__sveltets_2_with_any_event(render())) {
1311
get snapshot() { return __sveltets_2_nonNullable(this.$$prop_def.snapshot) }

0 commit comments

Comments
 (0)