🤤

SvelteKitでtype safeなナビゲーションを作ってみる

2024/01/11に公開
2

最近nextjsからsveltekitに乗り換えしていて、すごく気にいっています。

きっかけ

tansack routerのような👇をsvelteでもやりたいと思い、自前で作ったので、書き残しておきたいと思います。

nextjs-typesafe-route

実装

sveltekitの場合はサーバーとクライアントでの間でのtype safetyにかなり力を入れていて、devサーバーを立ち上げると.sveltekit/typesの中にload関数で返したtypeを生成してくれます。
👇のように$typesエリアスからインポートして使うことができます。

load-type

https://kit.svelte.dev/docs/load#page-data

同じような形でcodegenしたらいけるんじゃない?と思い、sveltekitのソースコードを漁ったら、create_manifest_dataという関数を見つけました。

https://github.com/sveltejs/kit/blob/d93eb42bee7fd06d4d3f7e3dde7381ab032b076c/packages/kit/src/core/sync/create_manifest_data/index.js#L19-L42

トップディレクトリにcodegen.jsファイルを作成し、devサーバーを立ち上げると同時にnode codegen.jsを実行し、常にファイルの変更を待ち受ける状態にしています。

codegen.js
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('Route type file Generated ✅');

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('Route type file Generated ✅');
	}
});

簡単にコードの説明すると、公開されてないモジュールを使い、ルートを取得し、[...path]``(gourp)といった特有ルートを除外して、tsコードを生成し、ファイルに書き込む、そしてファイルの変更をまち続ける。

Devサーバーを立ち上げるとsrc/lib/type.d.tsファイルが生成され👇のようなtypeが書き込まれます。


export type RouteList = "/" | "/auth" | "/auth/forgot-password" | "/auth/reset-password" | "/auth/sign-in" | "/auth/sign-up"

sveltekitではreact-routerのようなもののかわりとして、$app/navigationが用意されています。
なので、そのラッパーを書きます。

lib/navigation.ts
import { goto } from '$app/navigation';
import type { RouteList } from '../routes/type';

type GotoParams = Parameters<typeof goto>;
export const navigate = (url: RouteList | URL, opt?: GotoParams[1]) => goto(url, opt);

そうすると、👇のようにタイプヒントが現れます。

sample

注意

@sveltejs/kitからexportされているモジュールでは無いため、破壊的な変更が入った場合に動か無くなる可能性があります。

Discussion