diff --git a/.changeset/mean-seas-give.md b/.changeset/mean-seas-give.md
new file mode 100644
index 0000000000..0c16ac6d55
--- /dev/null
+++ b/.changeset/mean-seas-give.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: ensure trailing multiline comments on props produce correct code (#14143#issuecomment-2455702689)
diff --git a/.changeset/purple-owls-hug.md b/.changeset/purple-owls-hug.md
new file mode 100644
index 0000000000..f3dcb0a5e3
--- /dev/null
+++ b/.changeset/purple-owls-hug.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: ensure migrate keeps inline/trailing comments in $props type definition
diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js
index 3513098a20..8e8670c824 100644
--- a/packages/svelte/src/compiler/migrate/index.js
+++ b/packages/svelte/src/compiler/migrate/index.js
@@ -279,7 +279,7 @@ export function migrate(source, { filename, use_ts } = {}) {
type = `interface ${type_name} {${newline_separator}${state.props
.map((prop) => {
const comment = prop.comment ? `${prop.comment}${newline_separator}` : '';
- return `${comment}${prop.exported}${prop.optional ? '?' : ''}: ${prop.type};`;
+ return `${comment}${prop.exported}${prop.optional ? '?' : ''}: ${prop.type};${prop.trailing_comment ? ' ' + prop.trailing_comment : ''}`;
})
.join(newline_separator)}`;
if (analysis.uses_props || analysis.uses_rest_props) {
@@ -289,7 +289,7 @@ export function migrate(source, { filename, use_ts } = {}) {
} else {
type = `/**\n${indent} * @typedef {Object} ${type_name}${state.props
.map((prop) => {
- return `\n${indent} * @property {${prop.type}} ${prop.optional ? `[${prop.exported}]` : prop.exported}${prop.comment ? ` - ${prop.comment}` : ''}`;
+ return `\n${indent} * @property {${prop.type}} ${prop.optional ? `[${prop.exported}]` : prop.exported}${prop.comment ? ` - ${prop.comment}` : ''}${prop.trailing_comment ? ` - ${prop.trailing_comment.trim()}` : ''}`;
})
.join(``)}\n${indent} */`;
}
@@ -414,7 +414,7 @@ export function migrate(source, { filename, use_ts } = {}) {
* analysis: ComponentAnalysis;
* filename?: string;
* indent: string;
- * 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; }>;
+ * 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; }>;
* props_insertion_point: number;
* has_props_rune: boolean;
* has_type_or_fallback: boolean;
@@ -1497,13 +1497,28 @@ function extract_type_and_comment(declarator, state, path) {
str.update(comment_start, comment_end, '');
}
+ // Find trailing comments
+ const trailing_comment_node = /** @type {Node} */ (parent)?.trailingComments?.at(0);
+ const trailing_comment_start = /** @type {any} */ (trailing_comment_node)?.start;
+ const trailing_comment_end = /** @type {any} */ (trailing_comment_node)?.end;
+ let trailing_comment =
+ trailing_comment_node && str.original.substring(trailing_comment_start, trailing_comment_end);
+
+ if (trailing_comment_node) {
+ str.update(trailing_comment_start, trailing_comment_end, '');
+ }
+
if (declarator.id.typeAnnotation) {
state.has_type_or_fallback = true;
let start = declarator.id.typeAnnotation.start + 1; // skip the colon
while (str.original[start] === ' ') {
start++;
}
- return { type: str.original.substring(start, declarator.id.typeAnnotation.end), comment };
+ return {
+ type: str.original.substring(start, declarator.id.typeAnnotation.end),
+ comment,
+ trailing_comment
+ };
}
let cleaned_comment_arr = comment
@@ -1526,12 +1541,43 @@ function extract_type_and_comment(declarator, state, path) {
?.slice(0, first_at_comment !== -1 ? first_at_comment : cleaned_comment_arr.length)
.join('\n');
+ let cleaned_comment_arr_trailing = trailing_comment
+ ?.split('\n')
+ .map((line) =>
+ line
+ .trim()
+ // replace `// ` for one liners
+ .replace(/^\/\/\s*/g, '')
+ // replace `\**` for the initial JSDoc
+ .replace(/^\/\*\*?\s*/g, '')
+ // migrate `*/` for the end of JSDoc
+ .replace(/\s*\*\/$/g, '')
+ // remove any initial `* ` to clean the comment
+ .replace(/^\*\s*/g, '')
+ )
+ .filter(Boolean);
+ const first_at_comment_trailing = cleaned_comment_arr_trailing?.findIndex((line) =>
+ line.startsWith('@')
+ );
+ let cleaned_comment_trailing = cleaned_comment_arr_trailing
+ ?.slice(
+ 0,
+ first_at_comment_trailing !== -1
+ ? first_at_comment_trailing
+ : cleaned_comment_arr_trailing.length
+ )
+ .join('\n');
+
// try to find a comment with a type annotation, hinting at jsdoc
if (parent?.type === 'ExportNamedDeclaration' && comment_node) {
state.has_type_or_fallback = true;
const match = /@type {(.+)}/.exec(comment_node.value);
if (match) {
- return { type: match[1], comment: cleaned_comment };
+ return {
+ type: match[1],
+ comment: cleaned_comment,
+ trailing_comment: cleaned_comment_trailing
+ };
}
}
@@ -1540,11 +1586,19 @@ function extract_type_and_comment(declarator, state, path) {
state.has_type_or_fallback = true; // only assume type if it's trivial to infer - else someone would've added a type annotation
const type = typeof declarator.init.value;
if (type === 'string' || type === 'number' || type === 'boolean') {
- return { type, comment: state.uses_ts ? comment : cleaned_comment };
+ return {
+ type,
+ comment: state.uses_ts ? comment : cleaned_comment,
+ trailing_comment: state.uses_ts ? trailing_comment : cleaned_comment_trailing
+ };
}
}
- return { type: 'any', comment: state.uses_ts ? comment : cleaned_comment };
+ return {
+ type: 'any',
+ comment: state.uses_ts ? comment : cleaned_comment,
+ trailing_comment: state.uses_ts ? trailing_comment : cleaned_comment_trailing
+ };
}
// Ensure modifiers are applied in the same order as Svelte 4
@@ -1779,10 +1833,13 @@ function handle_identifier(node, state, path) {
comment = state.str.original.substring(comment_node.start, comment_node.end);
}
+ const trailing_comment = member.trailingComments?.at(0)?.value;
+
if (prop) {
prop.type = type;
prop.optional = member.optional;
prop.comment = comment ?? prop.comment;
+ prop.trailing_comment = trailing_comment ?? prop.trailing_comment;
} else {
state.props.push({
local: member.key.name,
@@ -1792,6 +1849,7 @@ function handle_identifier(node, state, path) {
optional: member.optional,
type,
comment,
+ trailing_comment,
type_only: true
});
}
diff --git a/packages/svelte/tests/migrate/samples/jsdoc-with-comments/input.svelte b/packages/svelte/tests/migrate/samples/jsdoc-with-comments/input.svelte
index 333980513a..f2efb1db80 100644
--- a/packages/svelte/tests/migrate/samples/jsdoc-with-comments/input.svelte
+++ b/packages/svelte/tests/migrate/samples/jsdoc-with-comments/input.svelte
@@ -24,5 +24,20 @@
/**
* This is optional
*/
- export let optional = {stuff: true};
+ export let optional = {stuff: true};
+
+ export let inline_commented; // this should stay a comment
+
+ /**
+ * This comment should be merged
+ */
+ export let inline_commented_merged; // with this inline comment
+
+ /*
+ * this is a same-line leading multiline comment
+ **/ export let inline_multiline_leading_comment = 'world';
+
+ export let inline_multiline_trailing_comment = 'world'; /*
+ * this is a same-line trailing multiline comment
+ **/
\ No newline at end of file
diff --git a/packages/svelte/tests/migrate/samples/jsdoc-with-comments/output.svelte b/packages/svelte/tests/migrate/samples/jsdoc-with-comments/output.svelte
index 271dd23ffb..19fbe38b50 100644
--- a/packages/svelte/tests/migrate/samples/jsdoc-with-comments/output.svelte
+++ b/packages/svelte/tests/migrate/samples/jsdoc-with-comments/output.svelte
@@ -9,6 +9,12 @@
+
+
+
+
+
+
/**
* @typedef {Object} Props
* @property {string} comment - My wonderful comment
@@ -17,6 +23,10 @@
* @property {any} no_comment
* @property {boolean} type_no_comment
* @property {any} [optional] - This is optional
+ * @property {any} inline_commented - this should stay a comment
+ * @property {any} inline_commented_merged - This comment should be merged - with this inline comment
+ * @property {string} [inline_multiline_leading_comment] - this is a same-line leading multiline comment
+ * @property {string} [inline_multiline_trailing_comment] - this is a same-line trailing multiline comment
*/
/** @type {Props} */
@@ -26,6 +36,10 @@
one_line,
no_comment,
type_no_comment,
- optional = {stuff: true}
+ optional = {stuff: true},
+ inline_commented,
+ inline_commented_merged,
+ inline_multiline_leading_comment = 'world',
+ inline_multiline_trailing_comment = 'world'
} = $props();
\ No newline at end of file
diff --git a/packages/svelte/tests/migrate/samples/props-ts/input.svelte b/packages/svelte/tests/migrate/samples/props-ts/input.svelte
index 34007c08b7..9378c54ecf 100644
--- a/packages/svelte/tests/migrate/samples/props-ts/input.svelte
+++ b/packages/svelte/tests/migrate/samples/props-ts/input.svelte
@@ -6,6 +6,16 @@
export let bindingOptional: string | undefined = 'bar';
/** this should stay a comment */
export let no_type_but_comment = 0;
+ export let type_and_inline_comment:number; // this should also stay a comment
+ export let no_type_and_inline_comment = 0; // this should stay as well
+
+ /*
+ * this is a same-line leading multiline comment
+ **/ export let inline_multiline_leading_comment = 'world';
+
+ export let inline_multiline_trailing_comment = 'world'; /*
+ * this is a same-line trailing multiline comment
+ **/
{readonly}
diff --git a/packages/svelte/tests/migrate/samples/props-ts/output.svelte b/packages/svelte/tests/migrate/samples/props-ts/output.svelte
index 36a0b0f4fb..e9deea1384 100644
--- a/packages/svelte/tests/migrate/samples/props-ts/output.svelte
+++ b/packages/svelte/tests/migrate/samples/props-ts/output.svelte
@@ -1,6 +1,9 @@
{readonly}
{optional}
-
\ No newline at end of file
+