Trying Out Type Safe Navigation in SvelteKit
qaynam
Posted on January 11, 2024
Recently Switched from Next.js to SvelteKit and Loving It
Motivation
I wanted to implement tansack-router typing in Svelte like thisπ
So I built my own and wanted to document it here.
Implementation
SvelteKit puts a lot of emphasis on type safety between server and client. When you start the dev server, it generates various types for data returned by load functions in .sveltekit/types
.
You can import and use them from the $types
alias like this π
https://kit.svelte.dev/docs/load#page-data
I thought I could do something similar with code generation. Looking through the SvelteKit source code, I found the create_manifest_data
function
export default function create_manifest_data({
config,
fallback = `${runtime_directory}/components`,
cwd = process.cwd()
}) {
const assets = create_assets(config);
const matchers = create_matchers(config, cwd);
const { nodes, routes } = create_routes_and_nodes(cwd, config, fallback);
for (const route of routes) {
for (const param of route.params) {
if (param.matcher && !matchers[param.matcher]) {
throw new Error(`No matcher found for parameter '${param.matcher}' in route ${route.id}`);
}
}
}
return {
assets,
matchers,
nodes,
routes
};
}
I created a codegen.js
file in the project root. It runs node codegen.js
when starting dev server to continually watch for file changes.
import create_manifest_data from './node_modules/@sveltejs/kit/src/core/sync/create_manifest_data/index.js';
import { load_config } from './node_modules/@sveltejs/kit/src/core/config/index.js';
import fs from 'node:fs';
import { format } from 'prettier';
const specialRouteIdPattern = /(\/(\(|\[).*(\]|\)))?/g;
async function getRoutes() {
/** @type import('@sveltejs/kit').ValidatedConfig */
const config = await load_config();
/** @type import("@sveltejs/kit").ManifestData */
const manifest = await create_manifest_data({ config });
return manifest.routes;
}
async function generateRouteTypeFile(
/** @type import("@sveltejs/kit").RouteData[] */
routes
) {
const ids = routes
.filter(
(route) => Boolean(route.page))
.map((route) => route.id.split('').join('').replace(specialRouteIdPattern, '')).filter(Boolean);
const type = `export type RouteList = ${ids.map((id) => `"${id}"`).join(' | ')}`;
fs.writeFileSync('./src/lib/type.d.ts', await format(type, { parser: 'typescript' }));
return { routes: ids };
}
const { routes } = await generateRouteTypeFile(await getRoutes());
console.log('Routes:');
console.table(routes);
console.log('Generated route type file β
');
console.log('Watching for changes...');
fs.watch('./src/routes', { recursive: true }, async (event, filename) => {
console.log(`Change detected in ${filename} `);
if (event === 'rename') {
const routes = await getRoutes();
await generateRouteTypeFile(routes);
console.log('Generated route type file β
');
}
});
To briefly explain the code, we use a module that is not publicly available, get a route, exclude specific routes such as [.... .path] (gourp)
, generate the ts code, write it to the file, and keep waiting for the file to change.
When dev server starts, it generates src/lib/type.d.ts
with route ids like:
export type RouteList = "/" | "/auth" | "/auth/forgot-password" | "/auth/reset-password" | "/auth/sign-in" | "/auth/sign-up"
Since SvelteKit has $app/navigation
instead of react-router, I wrapped it in lib/navigation.ts
file like thisπ
import { goto } from '$app/navigation';
import type { RouteList } from '../routes/type';
export const navigate = (url: RouteList | URL, opt?: GotoParams[1]) => goto(url, opt);
Now I get type hints like:
Warning
This module is not exported from @sveltejs/kit, so it may stop working if destructive changes are made.
Posted on January 11, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.