Svelte - v5 Migration

Big fan of SvelteJS as framework and in particular the Svelte monolith that is SvelteKit.

I maintain a scaffolding repo which act as a base to most Svelte projects I create. You can see that scaffold here.

As of October 2024 Svelte released Svelte 5, their biggest release yet as covered here, but it brings some significant changes to day to day Svelte writing and patterns.

This article aims to show:

  • How to easily upgrade a SvelteKit 2.0 project using Svelte 4
  • What the changes are and why they've come about

Upgrading

Upgrading in Svelte couldn't be easier, because of the sv migrate tooling.

Usage

Within the repo run npx sv migrate TARGET where target here is svelte-5, so npx sv migrate svelte-5. Check the sv migrate docs for other available targets.

sv migrate will then ask:

  • Ask about current dir, since some of the change will impact a mono-repo
  • Ask which folders to refactor
  • Ask if you want to tooling to convert components to new syntax

My repo is fairly simple so I went ahead and said yes and selected all the option on the above.

Changes - Packages

One thing of note here about sv migrate vs a manual package upgrade is that you don't need to trial a bunch of module versions before you reach the right mix.

Having tried it before I will tell you now that: upgrading all packages to latest will break your build.

sv migrate already contains the right module combinations and here are the changes.

Updated svelte to ^5.0.0
Updated @sveltejs/kit to ^2.5.27
Updated @sveltejs/vite-plugin-svelte to ^4.0.0
Updated prettier-plugin-svelte to ^3.2.6
Updated eslint-plugin-svelte to ^2.45.1
Updated typescript to ^5.5.0
Updated vite to ^5.4.4

Latest versions for reference

svelte                      5.20.0
@sveltejs/kit               2.17.1
@sveltejs/vite-plugin-svelte 5.0.3
prettier-plugin-svelte       3.3.3
eslint-plugin-svelte        2.46.1
typescript                   5.7.3
vite 	                     6.1.0

As you can see things move fast, and in the 3 months since release we've already had a fair bit of drift. This is mostly fine since Svelte is relatively stable.

Of most concerns is the vite major release of v6, and the large number of minor versions on the svelte packages.

Vite v5 vs v6

In my context I'm ok with a lagging codebase and will upgrade whenever crutial.

For anyone else who might be impacted I wanted to summarise what changed in vite v6, Read further in the migration guide, https://vite.dev/guide/migration.html.

v6 updates:

  • Environement API refactors; experimental api included which changes to interface; opt-in only
  • Runtime API: experimental Module Runner API, requires using the new API after updating
  • resolve.conditions: impacts those with .conditions config; from v6 conditions need to be included manually as oposed to being defaulted, allows ssr and regular configs to be different (whilst before ssr would inherit from regular config), if you have previous config in v6 you need to add the defaults per resolution
  • JSON.stringify: in v6 json.strigify: true no longer changes json.namedExports: disabled; json.namedExports is respected; strigify also has a new value of auto, which only stringifies large objects.
  • Larger support for asset references in HTML element: v5 only and could reference assets. v6 has an extended list https://vite.dev/guide/features#html
  • CSS: postcss-load-config updated in v6, sass uses modern api by default, custom CSS output file in library mode

Svelte minor releases difference

Because of the volume of releases there's a possibility here that we might be missing key bug fixes.

Installing packages

Now lets install the migrated packages and see what happens.

  1. Remove previous installed packages

rm -rf node_modules/ && rm package-lock.json

  1. Install with package of choice (npm)

npm install

Issues:

    1. @sveltejs/vite-plugin-svelte@^4.0.0 not found (see logs below)
    1. {@render children?.()} throwing error about unrecognised @ symbol
npm error code ERESOLVE
npm error ERESOLVE could not resolve
npm error
npm error While resolving: streak-tracker--svelte@0.0.1
npm error Found: @sveltejs/vite-plugin-svelte@3.1.2
npm error node_modules/@sveltejs/vite-plugin-svelte
npm error   dev @sveltejs/vite-plugin-svelte@"^4.0.0" from the root project
npm error   peer @sveltejs/vite-plugin-svelte@"^3.0.0 || ^4.0.0-next.1" from @sveltejs/kit@2.6.4
npm error   node_modules/@sveltejs/kit
npm error     dev @sveltejs/kit@"^2.5.27" from the root project
npm error     peer @sveltejs/kit@"^2.0.0" from @sveltejs/adapter-auto@3.2.5
npm error     node_modules/@sveltejs/adapter-auto
npm error       dev @sveltejs/adapter-auto@"^3.0.0" from the root project
npm error   1 more (@sveltejs/vite-plugin-svelte-inspector)
npm error
npm error Could not resolve dependency:
npm error dev @sveltejs/vite-plugin-svelte@"^4.0.0" from the root project
npm error
npm error Conflicting peer dependency: svelte@5.20.0
npm error node_modules/svelte
npm error   peer svelte@"^5.0.0-next.96 || ^5.0.0" from @sveltejs/vite-plugin-svelte@4.0.4
npm error   node_modules/@sveltejs/vite-plugin-svelte
npm error     dev @sveltejs/vite-plugin-svelte@"^4.0.0" from the root project
npm error
npm error Fix the upstream dependency conflict, or retry
npm error this command with --force or --legacy-peer-deps
npm error to accept an incorrect (and potentially broken) dependency resolution.
npm error
npm error
npm error For a full report see:

Fixing errors

    1. @sveltejs/vite-plugin-svelte: not a real error, but I suspect an issue with timeout during the npm i process; making sure I have a clean install by removing node_modules and package-lock.json allowed me to get a successful install without the error
    1. script tag issue with "@sveltejs/adapter-auto": fixed it by updating the version to ^4.0.0
    1. noticed an issue with tsconfig.json, whereby "moduleResolution" required "module" to be present and set to preserve; after adding that value the IDE refreshed and the @render issue went away

Changes - Components

Runes

Multiple changes stem from the new Runes API: compiler instructions that tell Svelte about reactivity. Runes start with $

let -> $props

In v4 props were achieve using export let xyz = "xyz" declarations. These declarations made content available from into the main body of the component.

In v5 all props come from the $props rune as seen below:

<script>
	let { optional = 'unset', required } = $props();
</script>

In my case, the app is also using Typescript so there are further changes on the type definitions, which will be covered later.

v5

<script lang="ts">
	import type { HTMLAttributes } from "svelte/elements";
	import { cn } from "$lib/shadcn.js";

	type $$Props = HTMLAttributes<HTMLDivElement>;

	let className: $$Props["class"] = undefined;
	export { className as class };
</script>

<div
	class={cn("bg-card text-card-foreground rounded-lg border shadow-sm", className)}
	{...$$restProps}
>
	<slot />
</div>

v6

<script lang="ts">
	import type { HTMLAttributes } from "svelte/elements";
	import { cn } from "$lib/shadcn.js";

	type $$Props = HTMLAttributes<HTMLDivElement>;

	interface Props {
		class?: $$Props["class"];
		children?: import('svelte').Snippet;
		[key: string]: any
	}

	let { class: className = undefined, children, ...rest }: Props = $props();
	
</script>

<div
	class={cn("bg-card text-card-foreground rounded-lg border shadow-sm", className)}
	{...rest}
>
	{@render children?.()}
</div>

render method no longer compiled for SSR

This is specifically related to SSR application, but worth mentioning.

children() and snippets

In v4 the main paradigm for creating higher order components was to use .

In v5 they are deprecated in favour of constructs which increase the flexibility. Also added in v6 a clearer construct for children.

In my case I've only used for HOCs and those instances have been refactored to use children instead

v4

<script>
	import '../app.css';
</script>

<slot></slot>

v5

<script lang="ts">
	import '../app.css';
	interface Props {
		children?: import('svelte').Snippet;
	}

	let { children }: Props = $props();
</script>

{@render children?.()}

Component typing changes

Most of the codebase saw change related to the new children component which is typed as Snippet from 'svelte' package.

No other significant changes present, but be advised that there are further changes detailed in the release note https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes-Component-typing-changes

let -> $state

Covered here last because I haven't implemented any states within this template.

I can see this being an important change to keep in mind in production apps since the let definition is common.

This essentially brings the biggest headaches, since prior to v5 developers built up this pattern of separation between prop definitions and state. This separation is still present in v5 but the change of let -> $props will confuse readers who will see more let -> $state.

I actually agree with the pattern separation, but getting used to it will take some time, mostly because the old definitions were worse.

v4

<script>
	let count = 0;
</script>

v5

<script>
	let count = $state(0);
</script>

Summary

Big update, one of the biggest yet! Largely positive, but with significant gotchas and implementation details.

Overall a move in the right direction, but shifting into the new paradigms will cause some headscratching.