-
-
Save ryoppippi/3734444129ef1403d0504f9932df6a9c to your computer and use it in GitHub Desktop.
SvelteKIt type-safe router
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { writeFile } from 'fs/promises'; | |
import { glob } from 'glob'; | |
import { format } from 'prettier'; | |
import { watch } from 'chokidar'; | |
export default async function generateRoutes() { | |
const routes = await glob('./src/routes/**/+page.svelte', { withFileTypes: true }).then((files) => | |
files | |
.filter((file) => file.isFile()) | |
.sort((a, b) => (a.path > b.path ? 1 : -1)) | |
.map<Route>((file) => | |
file | |
.relative() | |
.split(file.sep) | |
// slice removes first 2 elements("src" & "routes") and last one("+page.svelte") | |
.slice(2, -1) | |
.map(stringToSegment) | |
.filter((x): x is Segment => !!x) | |
) | |
); | |
const routeType = routesToType(routes); | |
writeRouteFile(routeType); | |
} | |
export function generateRoutesWatcher() { | |
const watcher = watch('./src/routes/**/+page.svelte'); | |
watcher.on('add', generateRoutes); | |
// note: doesn't trigger when a folder containing page is deleted - I don't know how to circumvent this | |
watcher.on('unlink', generateRoutes); | |
return () => { | |
watcher.close(); | |
}; | |
} | |
function routesToType(routes: Route[]) { | |
const types = routes | |
.flatMap((route) => { | |
const routeForks = forkify(route.map(segmentToType)); | |
return routeForks.map((fork) => '/' + fork.filter(Boolean).join('/')); | |
}) | |
.map((route) => `\`${route}\``); | |
return [...new Set(types)].join(' | '); | |
} | |
type Segment = { type: 'STATIC' | 'DYNAMIC' | 'OPTIONAL' | 'REST'; key: string }; | |
type Route = Segment[]; | |
function stringToSegment(segment: string): Segment | null { | |
if (segment.startsWith('(') && segment.endsWith(')')) return null; | |
if (!(segment.startsWith('[') || segment.endsWith(']'))) return { type: 'STATIC', key: segment }; | |
// remove [], dots & matchers(=matcher) | |
const key = segment.replaceAll(/[[\].]|(=.+)/g, ''); | |
if (segment.startsWith('[[')) return { type: 'OPTIONAL', key }; | |
if (segment.startsWith('[...')) return { type: 'REST', key }; | |
return { type: 'DYNAMIC', key }; | |
} | |
function segmentToType(segment: Segment): string | [string, null] { | |
switch (segment.type) { | |
case 'STATIC': | |
return segment.key; | |
case 'DYNAMIC': | |
return '${Param}'; | |
case 'OPTIONAL': | |
return ['${Param}', null]; | |
case 'REST': | |
return ['${RestParam}', null]; | |
default: { | |
const x: never = segment.type; | |
return x; | |
} | |
} | |
} | |
async function writeRouteFile(routeType: string) { | |
const fileData = ` | |
// This file is auto-generated. Please do not modify it. | |
declare const Brand: unique symbol; | |
type TemplateToken = string | number; | |
type Param = TemplateToken & { readonly [Brand]: unique symbol }; | |
type RestParam = (TemplateToken & { readonly [Brand]: unique symbol }) | Param; | |
type Route = ${routeType}; | |
export { Param, RestParam, Route, TemplateToken } | |
`; | |
writeFile('./src/lib/router.d.ts', await format(fileData, { parser: 'typescript' })) | |
.catch((e) => { | |
console.error('Error while trying to write router.d.ts file'); | |
console.error(e); | |
}) | |
.then(() => { | |
console.log('Sucessfully saved router.d.ts file'); | |
}); | |
} | |
/** | |
* Flattens the array producing forks from provided array-elements. | |
* | |
* Example: `[1, [2, 3], 4]` will produce `[[1, 2, 4], [1, 3, 4]]` | |
*/ | |
function forkify<T>(array: Array<T | T[]>) { | |
return array.reduce<T[][]>( | |
(forks, value) => { | |
if (!Array.isArray(value)) { | |
forks.forEach((fork) => fork.push(value)); | |
return forks; | |
} | |
return value.flatMap((variant) => forks.map((fork) => [...fork, variant])); | |
}, | |
[[]] | |
); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import type { Param, RestParam, Route, TemplateToken } from '$lib/router'; // this will be your generated 'router.d.ts' file | |
function parseParam<T extends TemplateToken | [TemplateToken, ...TemplateToken[]]>(x: T) { | |
return (Array.isArray(x) ? x.join('/') : x) as T extends TemplateToken[] ? RestParam : Param; | |
} | |
/** | |
* Function which ensures that provided route exists in SvelteKit's file-router. | |
* @param path for static paths you may provide just a string. | |
* | |
* For dynamic routes it has to be a function. It's first argument will be a transform function through which you will need to pass route params. | |
* | |
* Exmaple: `route(p => '/user/${p(id)}')` | |
* | |
* For multiple rest params an array needs to be passed in: | |
* | |
* Example: `route(p => '/compare/${p([id1, id2, id3])}')` | |
* @returns a string with a resolved route | |
*/ | |
export function route(path: Route | ((param: typeof parseParam) => Route)) { | |
return typeof path === 'string' ? path : path(parseParam); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { sveltekit } from '@sveltejs/kit/vite'; | |
import { defineConfig } from 'vite'; | |
import { generateRoutesWatcher } from './scripts/router-gen'; | |
let cleanup = () => void 0; | |
export default defineConfig({ | |
plugins: [ | |
sveltekit(), | |
{ | |
name: 'codegen', | |
buildStart() { | |
if (process.env.NODE_ENV !== 'development') return; | |
const routegenCleanup = generateRoutesWatcher(); | |
cleanup = () => { | |
routegenCleanup(); | |
}; | |
}, | |
buildEnd() { | |
cleanup(); | |
} | |
} | |
], | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Oh good thanks!!