diff --git a/.changeset/lucky-drinks-push.md b/.changeset/lucky-drinks-push.md
new file mode 100644
index 0000000000..f272bb0ac4
--- /dev/null
+++ b/.changeset/lucky-drinks-push.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: handle `$$Props` interface during migration
diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js
index e4c759083d..c60d2c8719 100644
--- a/packages/svelte/src/compiler/migrate/index.js
+++ b/packages/svelte/src/compiler/migrate/index.js
@@ -335,7 +335,8 @@ const instance_script = {
// }
}
- const binding = /** @type {Binding} */ (state.scope.get(declarator.id.name));
+ const name = declarator.id.name;
+ const binding = /** @type {Binding} */ (state.scope.get(name));
if (state.analysis.uses_props && (declarator.init || binding.updated)) {
throw new Error(
@@ -343,19 +344,33 @@ const instance_script = {
);
}
- state.props.push({
- local: declarator.id.name,
- exported: binding.prop_alias ? binding.prop_alias : declarator.id.name,
- init: declarator.init
+ const prop = state.props.find((prop) => prop.exported === (binding.prop_alias || name));
+ if (prop) {
+ // $$Props type was used
+ prop.init = declarator.init
? state.str.original.substring(
/** @type {number} */ (declarator.init.start),
/** @type {number} */ (declarator.init.end)
)
- : '',
- optional: !!declarator.init,
- bindable: binding.updated,
- ...extract_type_and_comment(declarator, state.str, path)
- });
+ : '';
+ prop.bindable = binding.updated;
+ prop.exported = binding.prop_alias || name;
+ } else {
+ state.props.push({
+ local: name,
+ exported: binding.prop_alias ? binding.prop_alias : name,
+ init: declarator.init
+ ? state.str.original.substring(
+ /** @type {number} */ (declarator.init.start),
+ /** @type {number} */ (declarator.init.end)
+ )
+ : '',
+ optional: !!declarator.init,
+ bindable: binding.updated,
+ ...extract_type_and_comment(declarator, state.str, path)
+ });
+ }
+
state.props_insertion_point = /** @type {number} */ (declarator.end);
state.str.update(
/** @type {number} */ (declarator.start),
@@ -944,6 +959,48 @@ function handle_identifier(node, state, path) {
}
}
// else passed as identifier, we don't know what to do here, so let it error
+ } else if (
+ parent?.type === 'TSInterfaceDeclaration' ||
+ parent?.type === 'TSTypeAliasDeclaration'
+ ) {
+ const members =
+ parent.type === 'TSInterfaceDeclaration' ? parent.body.body : parent.typeAnnotation?.members;
+ if (Array.isArray(members)) {
+ if (node.name === '$$Props') {
+ for (const member of members) {
+ const prop = state.props.find((prop) => prop.exported === member.key.name);
+
+ const type = state.str.original.substring(
+ member.typeAnnotation.typeAnnotation.start,
+ member.typeAnnotation.typeAnnotation.end
+ );
+
+ let comment;
+ const comment_node = member.leadingComments?.at(-1);
+ if (comment_node?.type === 'Block') {
+ comment = state.str.original.substring(comment_node.start, comment_node.end);
+ }
+
+ if (prop) {
+ prop.type = type;
+ prop.optional = member.optional;
+ prop.comment = comment ?? prop.comment;
+ } else {
+ state.props.push({
+ local: member.key.name,
+ exported: member.key.name,
+ init: '',
+ bindable: false,
+ optional: member.optional,
+ type,
+ comment
+ });
+ }
+ }
+
+ state.str.remove(parent.start, parent.end);
+ }
+ }
}
}
diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js
index c1cf1e4055..95c8bd32e0 100644
--- a/packages/svelte/src/compiler/phases/scope.js
+++ b/packages/svelte/src/compiler/phases/scope.js
@@ -343,7 +343,14 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
// references
Identifier(node, { path, state }) {
const parent = path.at(-1);
- if (parent && is_reference(node, /** @type {Node} */ (parent))) {
+ if (
+ parent &&
+ is_reference(node, /** @type {Node} */ (parent)) &&
+ // TSTypeAnnotation, TSInterfaceDeclaration etc - these are normally already filtered out,
+ // but for the migration they aren't, so we need to filter them out here
+ // -> once migration script is gone we can remove this check
+ !parent.type.startsWith('TS')
+ ) {
references.push([state.scope, { node, path: path.slice() }]);
}
},
diff --git a/packages/svelte/tests/migrate/samples/props-interface/input.svelte b/packages/svelte/tests/migrate/samples/props-interface/input.svelte
new file mode 100644
index 0000000000..0f7ee86d99
--- /dev/null
+++ b/packages/svelte/tests/migrate/samples/props-interface/input.svelte
@@ -0,0 +1,12 @@
+
diff --git a/packages/svelte/tests/migrate/samples/props-interface/output.svelte b/packages/svelte/tests/migrate/samples/props-interface/output.svelte
new file mode 100644
index 0000000000..e9d115cd5c
--- /dev/null
+++ b/packages/svelte/tests/migrate/samples/props-interface/output.svelte
@@ -0,0 +1,13 @@
+
\ No newline at end of file