Skip to content

Commit e6dd871

Browse files
rgonGonzalo Ruizpaoloricciuti
authored
fix: ensure migrate keeps inline/trailing comments in $props type definition and produces non-broken code (#14143)
* feat[sv migrate]: keep inline/trailing comments when migrating export let types to type definition * test: add tests for inline comment migration * chore: add changeset * fix: migrate trailing multiline comment parsing no longer results in broken code, FIXES PR #14143#issuecomment-2455702689 * test: add migrate test with same-line trailing multiline comments and same-line leading multiline comments * chore: add changeset * fix: lint --------- Co-authored-by: Gonzalo Ruiz <[email protected]> Co-authored-by: paoloricciuti <[email protected]>
1 parent 7dbe812 commit e6dd871

File tree

7 files changed

+134
-11
lines changed

7 files changed

+134
-11
lines changed

.changeset/mean-seas-give.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: ensure trailing multiline comments on props produce correct code (#14143#issuecomment-2455702689)

.changeset/purple-owls-hug.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: ensure migrate keeps inline/trailing comments in $props type definition

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

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ export function migrate(source, { filename, use_ts } = {}) {
279279
type = `interface ${type_name} {${newline_separator}${state.props
280280
.map((prop) => {
281281
const comment = prop.comment ? `${prop.comment}${newline_separator}` : '';
282-
return `${comment}${prop.exported}${prop.optional ? '?' : ''}: ${prop.type};`;
282+
return `${comment}${prop.exported}${prop.optional ? '?' : ''}: ${prop.type};${prop.trailing_comment ? ' ' + prop.trailing_comment : ''}`;
283283
})
284284
.join(newline_separator)}`;
285285
if (analysis.uses_props || analysis.uses_rest_props) {
@@ -289,7 +289,7 @@ export function migrate(source, { filename, use_ts } = {}) {
289289
} else {
290290
type = `/**\n${indent} * @typedef {Object} ${type_name}${state.props
291291
.map((prop) => {
292-
return `\n${indent} * @property {${prop.type}} ${prop.optional ? `[${prop.exported}]` : prop.exported}${prop.comment ? ` - ${prop.comment}` : ''}`;
292+
return `\n${indent} * @property {${prop.type}} ${prop.optional ? `[${prop.exported}]` : prop.exported}${prop.comment ? ` - ${prop.comment}` : ''}${prop.trailing_comment ? ` - ${prop.trailing_comment.trim()}` : ''}`;
293293
})
294294
.join(``)}\n${indent} */`;
295295
}
@@ -414,7 +414,7 @@ export function migrate(source, { filename, use_ts } = {}) {
414414
* analysis: ComponentAnalysis;
415415
* filename?: string;
416416
* indent: string;
417-
* 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; }>;
417+
* props: Array<{ local: string; exported: string; init: string; bindable: boolean; slot_name?: string; optional: boolean; type: string; comment?: string; trailing_comment?: string; type_only?: boolean; needs_refine_type?: boolean; }>;
418418
* props_insertion_point: number;
419419
* has_props_rune: boolean;
420420
* has_type_or_fallback: boolean;
@@ -1497,13 +1497,28 @@ function extract_type_and_comment(declarator, state, path) {
14971497
str.update(comment_start, comment_end, '');
14981498
}
14991499

1500+
// Find trailing comments
1501+
const trailing_comment_node = /** @type {Node} */ (parent)?.trailingComments?.at(0);
1502+
const trailing_comment_start = /** @type {any} */ (trailing_comment_node)?.start;
1503+
const trailing_comment_end = /** @type {any} */ (trailing_comment_node)?.end;
1504+
let trailing_comment =
1505+
trailing_comment_node && str.original.substring(trailing_comment_start, trailing_comment_end);
1506+
1507+
if (trailing_comment_node) {
1508+
str.update(trailing_comment_start, trailing_comment_end, '');
1509+
}
1510+
15001511
if (declarator.id.typeAnnotation) {
15011512
state.has_type_or_fallback = true;
15021513
let start = declarator.id.typeAnnotation.start + 1; // skip the colon
15031514
while (str.original[start] === ' ') {
15041515
start++;
15051516
}
1506-
return { type: str.original.substring(start, declarator.id.typeAnnotation.end), comment };
1517+
return {
1518+
type: str.original.substring(start, declarator.id.typeAnnotation.end),
1519+
comment,
1520+
trailing_comment
1521+
};
15071522
}
15081523

15091524
let cleaned_comment_arr = comment
@@ -1526,12 +1541,43 @@ function extract_type_and_comment(declarator, state, path) {
15261541
?.slice(0, first_at_comment !== -1 ? first_at_comment : cleaned_comment_arr.length)
15271542
.join('\n');
15281543

1544+
let cleaned_comment_arr_trailing = trailing_comment
1545+
?.split('\n')
1546+
.map((line) =>
1547+
line
1548+
.trim()
1549+
// replace `// ` for one liners
1550+
.replace(/^\/\/\s*/g, '')
1551+
// replace `\**` for the initial JSDoc
1552+
.replace(/^\/\*\*?\s*/g, '')
1553+
// migrate `*/` for the end of JSDoc
1554+
.replace(/\s*\*\/$/g, '')
1555+
// remove any initial `* ` to clean the comment
1556+
.replace(/^\*\s*/g, '')
1557+
)
1558+
.filter(Boolean);
1559+
const first_at_comment_trailing = cleaned_comment_arr_trailing?.findIndex((line) =>
1560+
line.startsWith('@')
1561+
);
1562+
let cleaned_comment_trailing = cleaned_comment_arr_trailing
1563+
?.slice(
1564+
0,
1565+
first_at_comment_trailing !== -1
1566+
? first_at_comment_trailing
1567+
: cleaned_comment_arr_trailing.length
1568+
)
1569+
.join('\n');
1570+
15291571
// try to find a comment with a type annotation, hinting at jsdoc
15301572
if (parent?.type === 'ExportNamedDeclaration' && comment_node) {
15311573
state.has_type_or_fallback = true;
15321574
const match = /@type {(.+)}/.exec(comment_node.value);
15331575
if (match) {
1534-
return { type: match[1], comment: cleaned_comment };
1576+
return {
1577+
type: match[1],
1578+
comment: cleaned_comment,
1579+
trailing_comment: cleaned_comment_trailing
1580+
};
15351581
}
15361582
}
15371583

@@ -1540,11 +1586,19 @@ function extract_type_and_comment(declarator, state, path) {
15401586
state.has_type_or_fallback = true; // only assume type if it's trivial to infer - else someone would've added a type annotation
15411587
const type = typeof declarator.init.value;
15421588
if (type === 'string' || type === 'number' || type === 'boolean') {
1543-
return { type, comment: state.uses_ts ? comment : cleaned_comment };
1589+
return {
1590+
type,
1591+
comment: state.uses_ts ? comment : cleaned_comment,
1592+
trailing_comment: state.uses_ts ? trailing_comment : cleaned_comment_trailing
1593+
};
15441594
}
15451595
}
15461596

1547-
return { type: 'any', comment: state.uses_ts ? comment : cleaned_comment };
1597+
return {
1598+
type: 'any',
1599+
comment: state.uses_ts ? comment : cleaned_comment,
1600+
trailing_comment: state.uses_ts ? trailing_comment : cleaned_comment_trailing
1601+
};
15481602
}
15491603

15501604
// Ensure modifiers are applied in the same order as Svelte 4
@@ -1779,10 +1833,13 @@ function handle_identifier(node, state, path) {
17791833
comment = state.str.original.substring(comment_node.start, comment_node.end);
17801834
}
17811835

1836+
const trailing_comment = member.trailingComments?.at(0)?.value;
1837+
17821838
if (prop) {
17831839
prop.type = type;
17841840
prop.optional = member.optional;
17851841
prop.comment = comment ?? prop.comment;
1842+
prop.trailing_comment = trailing_comment ?? prop.trailing_comment;
17861843
} else {
17871844
state.props.push({
17881845
local: member.key.name,
@@ -1792,6 +1849,7 @@ function handle_identifier(node, state, path) {
17921849
optional: member.optional,
17931850
type,
17941851
comment,
1852+
trailing_comment,
17951853
type_only: true
17961854
});
17971855
}

packages/svelte/tests/migrate/samples/jsdoc-with-comments/input.svelte

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,20 @@
2424
/**
2525
* This is optional
2626
*/
27-
export let optional = {stuff: true};
27+
export let optional = {stuff: true};
28+
29+
export let inline_commented; // this should stay a comment
30+
31+
/**
32+
* This comment should be merged
33+
*/
34+
export let inline_commented_merged; // with this inline comment
35+
36+
/*
37+
* this is a same-line leading multiline comment
38+
**/ export let inline_multiline_leading_comment = 'world';
39+
40+
export let inline_multiline_trailing_comment = 'world'; /*
41+
* this is a same-line trailing multiline comment
42+
**/
2843
</script>

packages/svelte/tests/migrate/samples/jsdoc-with-comments/output.svelte

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@
99
1010
1111
12+
13+
14+
15+
16+
17+
1218
/**
1319
* @typedef {Object} Props
1420
* @property {string} comment - My wonderful comment
@@ -17,6 +23,10 @@
1723
* @property {any} no_comment
1824
* @property {boolean} type_no_comment
1925
* @property {any} [optional] - This is optional
26+
* @property {any} inline_commented - this should stay a comment
27+
* @property {any} inline_commented_merged - This comment should be merged - with this inline comment
28+
* @property {string} [inline_multiline_leading_comment] - this is a same-line leading multiline comment
29+
* @property {string} [inline_multiline_trailing_comment] - this is a same-line trailing multiline comment
2030
*/
2131
2232
/** @type {Props} */
@@ -26,6 +36,10 @@
2636
one_line,
2737
no_comment,
2838
type_no_comment,
29-
optional = {stuff: true}
39+
optional = {stuff: true},
40+
inline_commented,
41+
inline_commented_merged,
42+
inline_multiline_leading_comment = 'world',
43+
inline_multiline_trailing_comment = 'world'
3044
} = $props();
3145
</script>

packages/svelte/tests/migrate/samples/props-ts/input.svelte

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@
66
export let bindingOptional: string | undefined = 'bar';
77
/** this should stay a comment */
88
export let no_type_but_comment = 0;
9+
export let type_and_inline_comment:number; // this should also stay a comment
10+
export let no_type_and_inline_comment = 0; // this should stay as well
11+
12+
/*
13+
* this is a same-line leading multiline comment
14+
**/ export let inline_multiline_leading_comment = 'world';
15+
16+
export let inline_multiline_trailing_comment = 'world'; /*
17+
* this is a same-line trailing multiline comment
18+
**/
919
</script>
1020

1121
{readonly}
Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
<script lang="ts">
22
33
4+
5+
6+
47
interface Props {
58
/** some comment */
69
readonly: number;
@@ -9,18 +12,31 @@
912
bindingOptional?: string | undefined;
1013
/** this should stay a comment */
1114
no_type_but_comment?: number;
15+
type_and_inline_comment: number; // this should also stay a comment
16+
no_type_and_inline_comment?: number; // this should stay as well
17+
/*
18+
* this is a same-line leading multiline comment
19+
**/
20+
inline_multiline_leading_comment?: string;
21+
inline_multiline_trailing_comment?: string; /*
22+
* this is a same-line trailing multiline comment
23+
**/
1224
}
1325
1426
let {
1527
readonly,
1628
optional = 'foo',
1729
binding = $bindable(),
1830
bindingOptional = $bindable('bar'),
19-
no_type_but_comment = 0
31+
no_type_but_comment = 0,
32+
type_and_inline_comment,
33+
no_type_and_inline_comment = 0,
34+
inline_multiline_leading_comment = 'world',
35+
inline_multiline_trailing_comment = 'world'
2036
}: Props = $props();
2137
</script>
2238

2339
{readonly}
2440
{optional}
2541
<input bind:value={binding} />
26-
<input bind:value={bindingOptional} />
42+
<input bind:value={bindingOptional} />

0 commit comments

Comments
 (0)