The migrate() function performs a best-effort migration of Svelte 4 code to Svelte 5, transforming legacy syntax to use runes, event attributes, and render tags.
The migration is not perfect and may require manual adjustments. Always review the migrated code and test thoroughly. The migrator may add @migration-task comments where manual intervention is needed.
Signature
function migrate(
source: string,
options?: {
filename?: string;
use_ts?: boolean;
}
): { code: string }
Parameters
The Svelte 4 component source code to migrate
The filename of the component (used for debugging and self-referencing components)
Whether to generate TypeScript syntax. If not specified, the migrator will infer from the source code.
Return Value
The migrated Svelte 5 code. May include @migration-task comments for manual review.
What Gets Migrated
The migrator handles:
- Reactive declarations (
$:) → $derived and $effect
- Exported props (
export let) → $props() destructuring
- Reactive statements →
$effect.pre() or run()
- Event handlers (
on:click) → event attributes (onclick)
- Slots → Snippets (
{@render children()})
<svelte:component> with dynamic this → {@const} or $derived
<svelte:self> → Self-import
- CSS selector scoping →
:global() where needed
beforeUpdate/afterUpdate imports → Removed if unused
Usage Examples
Basic Migration
import { migrate } from 'svelte/compiler';
const svelteV4 = `
<script>
export let count = 0;
$: doubled = count * 2;
$: console.log('count changed:', count);
</script>
<button on:click={() => count++}>
Clicked {count} times (doubled: {doubled})
</button>
`;
const result = migrate(svelteV4);
console.log(result.code);
Output:
<script>
let { count = 0 } = $props();
let doubled = $derived(count * 2);
$effect.pre(() => {
console.log('count changed:', count);
});
</script>
<button onclick={() => count++}>
Clicked {count} times (doubled: {doubled})
</button>
Migrating Props with TypeScript
import { migrate } from 'svelte/compiler';
const source = `
<script lang="ts">
export let name: string;
export let age: number = 0;
export let optional: boolean = false;
</script>
<h1>Hello {name}, age {age}</h1>
`;
const result = migrate(source, { use_ts: true });
Output:
<script lang="ts">
interface Props {
name: string;
age?: number;
optional?: boolean;
}
let { name, age = 0, optional = false }: Props = $props();
</script>
<h1>Hello {name}, age {age}</h1>
Migrating Slots to Snippets
const source = `
<script>
export let items = [];
</script>
<ul>
{#each items as item}
<li>
<slot name="item" {item}>Default: {item.name}</slot>
</li>
{/each}
</ul>
<slot>No items</slot>
`;
const result = migrate(source);
Output:
<script>
let { items = [], item, children }: {
items?: any[];
item?: import('svelte').Snippet<[any]>;
children?: import('svelte').Snippet;
} = $props();
</script>
<ul>
{#each items as item}
<li>
{#if item}{@render item({ item })}{:else}Default: {item.name}{/if}
</li>
{/each}
</ul>
{@render children?.()}
Migrating Event Handlers
const source = `
<script>
function handleClick(event) {
console.log('clicked', event);
}
</script>
<button on:click={handleClick}>Click me</button>
<button on:click|preventDefault|stopPropagation={handleClick}>Click me</button>
`;
const result = migrate(source);
Output:
<script>
import { preventDefault, stopPropagation } from 'svelte/legacy';
function handleClick(event) {
console.log('clicked', event);
}
</script>
<button onclick={handleClick}>Click me</button>
<button onclick={(e) => { preventDefault(e); stopPropagation(e); handleClick(e); }}>
Click me
</button>
Programmatic Migration
import { migrate } from 'svelte/compiler';
import fs from 'fs';
import path from 'path';
import glob from 'glob';
// Migrate all .svelte files in src/
const files = glob.sync('src/**/*.svelte');
for (const file of files) {
const source = fs.readFileSync(file, 'utf-8');
const result = migrate(source, {
filename: file,
use_ts: file.includes('lang="ts"')
});
// Check for migration tasks
if (result.code.includes('@migration-task')) {
console.warn(`⚠️ Manual review needed: ${file}`);
}
fs.writeFileSync(file, result.code);
console.log(`✅ Migrated: ${file}`);
}
Handling Migration Tasks
When the migrator can’t automatically handle certain patterns, it adds comments:
<!-- @migration-task: Error while migrating Svelte code: can't migrate `$: foo = ...` to `$derived` because there's a variable named derived. Rename the variable and try again or migrate by hand. -->
<!-- @migration-task: svelte:self is deprecated, import this Svelte file into itself instead -->
<!-- @migration-task: migrate this slot by hand, `my-slot-name` is an invalid identifier -->
Search for @migration-task in the migrated code and address these manually.
Using with CLI
Svelte provides a CLI migration tool:
# Migrate a single file
npx svelte-migrate@latest svelte-5 src/App.svelte
# Migrate entire directory
npx svelte-migrate@latest svelte-5 src/
# Preview without writing
npx svelte-migrate@latest svelte-5 src/ --dry-run
Limitations
- CSS preprocessors: CSS is blanked during migration. Use a preprocessor-aware tool if needed.
- Complex reactive patterns: Some complex
$: patterns may not migrate cleanly.
- beforeUpdate/afterUpdate: Must be migrated manually to runes or lifecycle functions.
- Store auto-subscription:
$store syntax is not migrated (still works in Svelte 5).
- Component events: Must be migrated to callback props manually.
Best Practices
- Commit before migrating - Always have a clean git state
- Review the output - Don’t blindly accept migrations
- Test thoroughly - Run your test suite after migration
- Migrate incrementally - Start with leaf components
- Read migration tasks - Address all
@migration-task comments