From 226f1c7db98b17c6144fa9c039d853cd9df2154a Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sun, 7 Jun 2026 07:41:54 -0700 Subject: [PATCH] fix(migrate): guard magic-string ranges for @component comment + export alias The migrator passed AST-derived ranges to MagicString.remove/update without validating them. Aliased export specifiers can have undefined start/end, and the @component HTML comment was treated as a JS comment whose range overlapped an existing edit, so magic-string threw and the whole migration aborted. Add range validation helpers, remove specifiers individually through the validated path, and skip Svelte HTML comments in extract_type_and_comment. Closes #18304 Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/svelte/src/compiler/migrate/index.js | 64 +++++++++++++++---- .../input.svelte | 5 ++ .../output.svelte | 5 ++ 3 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 packages/svelte/tests/migrate/samples/export-alias-with-component-comment/input.svelte create mode 100644 packages/svelte/tests/migrate/samples/export-alias-with-component-comment/output.svelte diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index 0370155c12..b3671fbf19 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -113,6 +113,37 @@ function find_closing_parenthesis(start, code) { return end; } +/** + * @param {MagicString} str + * @param {unknown} start + * @param {unknown} end + */ +function is_valid_range(str, start, end) { + return ( + typeof start === 'number' && + typeof end === 'number' && + start >= 0 && + start < end && + end <= str.original.length + ); +} + +/** + * @param {MagicString} str + * @param {unknown} start + * @param {unknown} end + */ +function remove_if_valid(str, start, end) { + if (!is_valid_range(str, start, end)) return false; + + try { + str.remove(/** @type {number} */ (start), /** @type {number} */ (end)); + return true; + } catch { + return false; + } +} + /** * Does a best-effort migration of Svelte code towards using runes, event attributes and render tags. * May throw an error if the code is too complex to migrate automatically. @@ -547,21 +578,22 @@ const instance_script = { return; } - let count_removed = 0; + const specifiers_to_remove = /** @type {typeof node.specifiers} */ ([]); for (const specifier of node.specifiers) { if (specifier.local.type !== 'Identifier') continue; const binding = state.scope.get(specifier.local.name); if (binding?.kind === 'bindable_prop') { - state.str.remove( - /** @type {number} */ (specifier.start), - /** @type {number} */ (specifier.end) - ); - count_removed++; + specifiers_to_remove.push(specifier); } } - if (count_removed === node.specifiers.length) { - state.str.remove(/** @type {number} */ (node.start), /** @type {number} */ (node.end)); + + if (specifiers_to_remove.length === node.specifiers.length) { + remove_if_valid(state.str, node.start, node.end); + } else { + for (const specifier of specifiers_to_remove) { + remove_if_valid(state.str, specifier.start, specifier.end); + } } }, VariableDeclaration(node, { state, path, visit, next }) { @@ -1597,8 +1629,14 @@ function extract_type_and_comment(declarator, state, path) { const comment_start = /** @type {any} */ (comment_node)?.start; const comment_end = /** @type {any} */ (comment_node)?.end; - let comment = comment_node && str.original.substring(comment_start, comment_end); - if (comment_node) { + const has_comment_range = is_valid_range(str, comment_start, comment_end); + const is_svelte_comment = + has_comment_range && str.original.startsWith(' + diff --git a/packages/svelte/tests/migrate/samples/export-alias-with-component-comment/output.svelte b/packages/svelte/tests/migrate/samples/export-alias-with-component-comment/output.svelte new file mode 100644 index 0000000000..fb7a2bd935 --- /dev/null +++ b/packages/svelte/tests/migrate/samples/export-alias-with-component-comment/output.svelte @@ -0,0 +1,5 @@ + +