Svelte 5 Migration Analyst
A Claude Code skill that measures what migrating your app to Svelte 5 + SvelteKit would actually buy — then plans the migration.
Framework migrations get sold on numbers nobody measured. "3× faster, 60% smaller" turns into a six-week rewrite and a bundle that shrank by 8%. This skill exists to produce numbers you can defend in a review, including the ones that argue against migrating.
What makes it different
It has been validated against a complete production migration. A 49-component Angular 17 → Svelte 5 port of WowUp, an Electron addon manager, was run end to end against this skill's assessment — and the assessment was then scored against what actually happened. That found two genuine correctness bugs in the tooling, both now fixed and regression-tested, plus a methodological error worth more than either.
Everything is public and reproducible:
gageracer/WowUp @ migrate/svelte5 —
the ported renderer
alongside the untouched Angular one, and the full
write-ups.
It measures instead of guessing. Three Node scripts do the work, and they only report what they can read off disk:
analyze.mjsresolves the real transitive dependency closure from yournode_modules(npm, yarn, and pnpm layouts), detects your deployment target, counts declared routes rather than guessing from directory names, and checks which dependencies your UI actually imports.attribute.mjsdecodes your build's sourcemapmappingsVLQ and attributes emitted bytes per npm package — not original source length, which is off by 26× on tree-shaken packages (see below).predict.mjsrecords the report's claims as falsifiable predictions, then scores them against fresh measurements at every phase gate.
It reports the numbers that go the wrong way. Five findings the skill states up front rather than burying:
| Common claim | What this skill reports |
|---|---|
"node_modules gets smaller" | It gets bigger. SvelteKit ships a compiler and Vite toolchain as devDependencies — measured at 57 packages / 67 MB. The honest headline is that SvelteKit has 0 runtime dependencies. |
| "We removed lodash from every component" | 78.7 KB of it still shipped, via a sibling library the migration never touched. "Removed from your source" ≠ "removed from the bundle." |
| "Swapping component library A for B is net zero" | Usually wrong, in migration's favour. Measured: 697 KB of Angular Material became 72 KB, because most of it existed only to paper over the framework. <mat-dialog> was really <dialog showModal()>. |
| "We halved the bundle, so it's twice as fast" | A measured 47% bundle reduction bought 17% renderer startup and 1 MB off a 131 MB artifact. |
| "Remote functions are the modern SvelteKit data layer" | They are experimental as of Kit 2.70.1 — the docs say "likely to contain bugs and subject to change without notice." Kept off the critical path. |
"Do not migrate" is a valid verdict. If your baseline Lighthouse shows the bottleneck is a 900 ms API call or 4 MB of hero images, a framework swap changes nothing users can perceive, and the report says so in the verdict — not a footnote. A tool that always recommends migrating is a sales pitch, not an analysis.
It knows bundle size isn't always worth anything. For Electron, Tauri and Capacitor the bundle is read from local disk next to a ~120 MB runtime — Lighthouse doesn't apply and bytes are a few milliseconds of parse nobody waits for. The skill detects the target and switches the whole value model, including how to benchmark a packaged build with a control metric.
It translates line by line. Per-framework mapping tables for Angular, React/Next and Vue/Nuxt — API, template syntax, state, DI, routing, forms, component libraries — with the naive-but-wrong translations called out. The Angular one is validated against the real port.
It writes current Svelte 5. The characteristic failure of an AI-assisted migration is Svelte 4
idioms inside Svelte 5 files — export let, on:click, $:, <slot />. That code compiles, runs,
and discards the entire point of the migration. This skill wires in the official Svelte MCP
server and runs svelte-autofixer as a per-component gate.
Install
As a plugin (recommended)
/plugin marketplace add gageracer/svelte5-migration-analyst
/plugin install svelte5-migration-analyst@gageracer-svelte-tools
Manually
git clone https://github.com/gageracer/svelte5-migration-analyst.git
cp -r svelte5-migration-analyst/skills/svelte5-migration-analyst ~/.claude/skills/
~/.claude/skills/ makes it available in every project. Use <project>/.claude/skills/ to scope
it to one repo.
Recommended: the Svelte MCP server
claude mcp add -t http -s user svelte https://mcp.svelte.dev/mcp
Optional but strongly recommended — it's what keeps generated code current instead of relying on the model's memory. The skill degrades gracefully without it and says so in the report.
Requirements: Node.js ≥ 18. No npm install; the scripts use Node builtins only.
Usage
Point Claude at a project:
Analyze this project for a Svelte 5 migration
Should we move our Next.js dashboard to SvelteKit? Measure it.
Which of our dependencies would Svelte's built-ins replace?
You can also run the scripts directly, without Claude:
SKILL=~/.claude/skills/svelte5-migration-analyst
# dependency closure + component inventory + hazards
node $SKILL/scripts/analyze.mjs . --json --out baseline/deps.json
# shipped-byte attribution (needs a production build with sourcemaps)
npm run build
node $SKILL/scripts/attribute.mjs . --json --out baseline/bundle.json
Script flags
analyze.mjs [projectRoot] [flags]
| Flag | Effect |
|---|---|
--json | Emit JSON to stdout instead of the human summary |
--out FILE | Write JSON to a file |
--svelte-ref DIR | Measure a real scaffolded SvelteKit app for a true net figure instead of an upper bound |
--aggressive | Also treat partial packages as removable (optimistic scenario — label it as such) |
attribute.mjs [projectRoot] [flags]
| Flag | Effect |
|---|---|
--dir BUILD_DIR | Point at the build output explicitly instead of auto-detecting |
--top N | Rows in the attribution table (default 25) |
--json, --out FILE | As above |
Auto-detects .next/static, .svelte-kit/output/client, dist, build, out/_next/static,
.output/public, and .nuxt/dist/client.
Sample output
analyze.mjs against a Next.js app:
── acme-dashboard ──────────────────────────────────
Framework Next.js 15.0.0
Source 5 files · 291 LOC · 4 components · 2 routes
Complexity 2 simple / 1 medium / 1 complex
Hooks 20 calls (useState×6, useEffect×4, useMemo×4, useCallback×2, useRef×2)
Tests 0 test files
DEPENDENCIES (install size on disk; measured)
REMOVED — framework runtime — compiles away
next SvelteKit
react Svelte 5 compiler output
react-dom Svelte 5 compiler output
REMOVED — replaced by a Svelte built-in — nothing installed in its place
zustand $state in a .svelte.ts module
react-hook-form bind:value + SvelteKit form actions
clsx class={{ ... }} / class={[ ... ]}
axios fetch (SvelteKit passes a request-scoped fetch into load)
styled-components scoped <style> block in .svelte
SWAPPED — needs a Svelte equivalent — no dependency saved; rewrite cost
@mui/material skeleton, bits-ui + Tailwind, or a hand-rolled design system
react-window @tanstack/svelte-virtual or virtua
DEPENDENCY DELTA (transitive closure, measured from node_modules)
Direct deps 7 today → 2 survive + 5 Svelte packages
Transitive 32 today → 2 survive
Packages cut 30
Svelte runtime deps 0 packages · 0.0 MB
Svelte toolchain 57 packages · 67.0 MB (devDependencies — build-time only, never shipped)
NET RUNTIME DEPS 32 → 2 packages
⚠ node_modules will likely GROW: SvelteKit ships a compiler and Vite toolchain as
devDependencies. That is a build-time cost, not a shipped cost — report it, do not hide it.
HAZARDS
[dependency] @mui/material
Very large component surface with deep theming. Component-for-component
reimplementation is usually the single biggest line item in the plan.
[architecture] React Server Components
3 "use client" boundaries. SvelteKit has no RSC equivalent — the server/client
split must be redesigned around load functions and +page.server.
[process] No test files found
Nothing verifies parity. Behavioural test coverage is the precondition for a
safe migration — build an E2E smoke suite before touching anything.
EFFORT BAND 11–32 person-days
attribute.mjs against a build:
── shipped bundle: .svelte-kit/output/client ──────────────────────────────
JS 9 chunks raw 71.5 KB gzip 28.3 KB brotli 25.4 KB
WHERE THE BYTES GO (gzip share)
package gzip fate
svelte 14.0 KB kept
@sveltejs/kit 10.4 KB kept
(your app code) 3.8 KB your code — shrinks, amount TBD
CEILING ESTIMATE: bundle ≤ X KB gzip before any app-code shrink.
Do not publish a tighter number than this until the Phase 1 pilot gives you a measured one.
How the report is structured
- Verdict — migrate / migrate incrementally / partial / don't. Numbers that drive it, headline cost, largest risk.
- What you have today — framework, routes, components by complexity, test coverage.
- Bundle — measured totals, per-package attribution, provable ceiling.
- Dependencies — removed / reduced / swapped / unclassified, as separate tables.
- Performance — baseline Lighthouse, what migration changes and what it can't touch.
- Hazards — everything with no clean path.
- Effort — a range, never a point estimate.
- Roadmap — phases with exit criteria, adapted to your actual routes and files.
- What would change this assessment.
Every figure is tagged [measured], [bounded], or [projected]. There is no fourth category.
Section 9 is the prediction ledger: every quantitative claim written down as a metric, a comparison and a value, before the work starts. At each phase gate:
node scripts/predict.mjs score --predictions migration/predictions.json \
--analysis deps.json --bundle bundle.json --md
...re-reads the same metrics and prints a scorecard — hit, miss, unresolved, with the error on each miss. A forecast nobody scores gets remembered as roughly right; this is what stops that. It also refuses to let unexercised predictions read as confirmed ones, which is the failure mode of every migration status report.
Before writing the roadmap, the skill stops and asks you about the dependency decisions that
are genuinely judgement calls — component library strategy, what replaces react-query, whether
to adopt experimental remote functions. It won't ask you to approve deleting clsx.
Case study: WowUp (Angular 17 → Svelte 5, 49 components)
The skill's assessment said do not migrate. The owner did it anyway, in full, specifically to test whether the assessment was right — which makes it the most useful data this project has.
Branch: gageracer/WowUp @ migrate/svelte5
| Document | What it is |
|---|---|
svelte5-assessment.md | The pre-migration report and the "do not migrate" verdict |
phase1-results.md | 3-component pilot scored against the predictions |
full-migration-results.md | All 49 components, packaged AppImage benchmarks, every defect found |
migration/baseline/ | Raw measurement JSON and the independent attributor that caught the 26× bug |
renderer-svelte/ | The ported app — Angular tree left untouched beside it |
The verdict held; several numbers underneath it did not.
| Predicted | Measured | |
|---|---|---|
| Bundle ceiling ~9% reduction | 47% total / 59% first screen | ❌ off by 5×, in migration's favour |
| Material + CDK is a swap, net zero | 697 KB → 72.5 KB | ❌ badly wrong |
| ag-grid is a swap, saving zero | 958 KB, still there | ✅ |
| App code shrinks, amount unknown | 25,494 → 19,235 LOC (−25%) | ✅ |
rxjs is framework-agnostic, kept | gone: 75 files → 0 | ❌ |
| 60–110 person-days | never validated | ⚠️ unresolved |
The verdict survived being wrong about the prize because it never rested on the bundle number: the renderer loads from local disk, so a 2 MB reduction is ~15 ms nobody waits for. Being wrong about the size of a prize that wasn't the point is survivable. Being wrong about which number matters isn't. The measured end state — a 47% bundle reduction buying 17% renderer startup and 1 MB off a 131 MB artifact — is the number the assessment was actually arguing about.
What generalises is distilled in
field-notes.md; the Angular mappings
it produced are in translate-angular.md.
The migration roadmap
Eight phases, each with an exit criterion. The shape:
- Phase 0 — Baseline and safety net. Nothing is migrated. Capture measurements; build the E2E suite against the current app. If there are no tests, this is the largest item in the plan.
- Phase 1 — Scaffold and pilot slice. One vertical slice, end to end. This is the go/no-go gate: re-measure, and replace the report's projections with real numbers. If the measured win is materially below projection, the plan gets revisited before the rest of the budget is spent.
- Phases 2–5 — shared primitives, state/data layer, routes, cross-cutting concerns.
- Phase 6 — Parity gate and cutover. Evidence, not assertion. Tested rollback.
- Phase 7 — Cleanup. Delete the reference tree, confirm the dependency claim held.
The workspace strategy keeps the old source in a read-only original/ tree on a migration branch,
so old and new sit side by side while porting — with guardrails (excluded from tsconfig, eslint,
prettier, and CI; deleted in its own commit at the end). The playbook also says when to reject that
approach: if the branch would live longer than ~6 weeks, go incremental behind a proxy, because
long branches fail at the merge, not during the work.
Repo layout
.claude-plugin/
marketplace.json # plugin catalog
plugin.json # plugin manifest
skills/svelte5-migration-analyst/
SKILL.md # workflow and ground rules
references/
measurement.md # what may be claimed, and how each number is obtained
svelte5-idioms.md # current Svelte 5 API surface + anti-patterns
translate-angular.md # line-by-line Angular → Svelte (validated on a real port)
translate-react.md # line-by-line React/Next → Svelte
translate-vue.md # line-by-line Vue/Nuxt → Svelte
desktop-apps.md # Electron/Tauri: what to measure when bytes aren't latency
field-notes.md # lessons from completed migrations
mcp-workflow.md # Svelte MCP integration
migration-playbook.md # workspace strategy, phases, exit criteria
report-template.md # report structure
scripts/
analyze.mjs # dependency closure, inventory, target, import sites
attribute.mjs # emitted-span bundle attribution
predict.mjs # prediction ledger + scorecard
dep-map.json # package → Svelte fate mapping (154 packages)
test/smoke.mjs # 36 tests: scripts, schema, doc cross-references
The dependency map
dep-map.json drives classification.
Every package lands in one of five classes:
| Class | Meaning |
|---|---|
runtime | The source framework itself. Disappears entirely. |
native | Fully replaced by a Svelte built-in. Dependency deleted, nothing installed. |
partial | Common cases covered; advanced uses need work. Audit before counting. |
swap | Still needed. A Svelte package replaces it. Net saving zero. |
keep | Framework-agnostic. Carries over untouched. |
~130 packages mapped so far. PRs welcome — see CONTRIBUTING.md. The one hard rule: an entry must name the exact Svelte API that replaces it. An entry with no named replacement is a guess, not a saving.
Limitations
- Component and route counts are regex-based. Good enough to size the work; not a parser. Expect a few percent of noise on unusual code styles.
- Per-package raw figures are emitted spans, decoded from the sourcemap
mappingsVLQ — near-exact, validated against an independent count to 2.5%. Per-package gzip is raw × the whole-payload ratio, so it's an estimate. The totals are exact and are what you should quote for compressed size. - No sourcemaps, no attribution. You get totals only. Enable
build.sourcemapand rebuild. - The effort band is a heuristic, not a measurement. Phase 1 exists to replace it.
- Client bundle only. Server payload and hydration cost come from Lighthouse, separately.
- Monorepos need the script pointed at each package individually.
Verified against
svelte 5.56.8 · @sveltejs/kit 2.70.1 · vite 8.1.5 · Svelte MCP server (live) — checked 2026-07-29.
Validated against a complete 49-component Angular 17 → Svelte 5 production port. Entries in
dep-map.json marked MEASURED carry numbers from that port, and field-notes.md records what
the assessment got right and wrong.
The fast-moving parts — remote functions especially — are re-verified against the docs rather than
recalled. If you're reading this much later, dep-map.json carries a verifiedAgainst field and
the reference docs carry dates.