Svelte 5 Runes Migration Guide — KoproGo
Strategy
KoproGo migrates from Svelte 5 legacy mode (export let + $:) to runes
($props() + $state() + $derived() + $effect()) incrementally.
Svelte 5.55 auto-detects runes vs legacy per-component. No global config
needed. A component that uses $props() is compiled in runes mode; one
that uses export let stays in legacy mode. Both can coexist in the same
project, even as parent-child.
Migration Pattern (leaf components)
Leaf components (no child Svelte components) are the simplest to migrate:
<!-- BEFORE (legacy) -->
<script lang="ts">
export let status: string;
$: config = statusMap[status] || defaultConfig;
</script>
<!-- AFTER (runes) -->
<script lang="ts">
let { status }: { status: string } = $props();
let config = $derived(statusMap[status] || defaultConfig);
</script>
Checklist per component
export let X→let { X }: { X: Type } = $props()$: Y = expr→let Y = $derived(expr)$: { sideEffect(); }→$effect(() => { sideEffect(); })let Z = initialValue(mutable) →let Z = $state(initialValue)Remove
createEventDispatcher→ use callback propsRun
npx vitest run <test-file>→ must passCommit
Parent-Child Interop
When a runes parent uses a legacy child (e.g. <Modal on:close={...}>):
on:directive for component events DOES NOT WORK in runes modeWorkaround: pass callback props (
onClose,onCreated) insteadThis requires the child to accept and call the callback prop
Plan: migrate entire modules at once (all ticket components, then all meeting components) to minimize interop issues
When a legacy parent uses a runes child:
Works seamlessly. The runes child accepts props normally.
bind:valuefrom legacy parent to runes child works.
Known Issues
vitest + bind:value on <select> (Svelte 5 DEV mode)
Svelte 5’s DEV-mode runtime throws $state rune outside svelte when a
component using bind:value on a <select> element is mounted in vitest/jsdom.
This is because the compiled output references $state from the Svelte runtime’s
index-client.js, and the DEV guard fires in non-.svelte files.
Workaround: mode: "production" in vitest.config.ts (partially helps) or
write spec-only tests that don’t mount the component.
Tracking: https://github.com/sveltejs/svelte/issues/12865
Progress
Module |
Components |
Migrated |
Tests |
|---|---|---|---|
Ticket badges |
2 |
2 ✅ |
11 |
Resolution badges |
1 |
1 ✅ |
3 |
Poll badges |
1 |
1 ✅ |
4 |
Ticket forms/modals |
3 |
0 (needs module-wide migration) |
17 |
Meeting components |
7 |
0 |
5 |
Total |
14 |
4 |
40 |