Skip to main content
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

source
string
required
The Svelte 4 component source code to migrate
options.filename
string
The filename of the component (used for debugging and self-referencing components)
options.use_ts
boolean
Whether to generate TypeScript syntax. If not specified, the migrator will infer from the source code.

Return Value

code
string
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

  1. Commit before migrating - Always have a clean git state
  2. Review the output - Don’t blindly accept migrations
  3. Test thoroughly - Run your test suite after migration
  4. Migrate incrementally - Start with leaf components
  5. Read migration tasks - Address all @migration-task comments