DEV Community

Max Core
Max Core

Posted on

SvelteKit 5: How to make code-based router, instead of file-based router

The idea is to create universal [...path] that will capture all, and our urls.js will look like this:

export const default_error = () => import('/src/error.svelte');

const base_layout = {page: () => import('/src/base.svelte'), default_error}

export const patterns = [
    {re: /^\/\/?$/,                                 page: () => import('/src/home.svelte'),    layouts: [base_layout], js: import('/src/home.js')},
    {re: /^\/about\/?$/,                            page: () => import('/src/about.svelte'),   layouts: [base_layout], endpoint: import('/src/about.js')},
    {re: /^\/article\/([0-9]+)\/?$/, slugs: ['id'], page: () => import('/src/article.svelte'), layouts: [base_layout], js: import('/src/article.js'),  endpoint: import('/src/article_endpoint.js')},
]

Enter fullscreen mode Exit fullscreen mode

Final structure can look like:

router/
    [...path]/
        page.js
        page.server.js
        +page.svelte
        +error.svelte
    router.js
Enter fullscreen mode Exit fullscreen mode

Since router/ is renamed routes/ putted in project root along with src/, we have to specify it in configs:

svelte.config.js:

import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
const config = {
    preprocess: vitePreprocess(),
    kit: {
        adapter: adapter(),

        // Add this:
        files: {
            routes: 'router/',
        },
    }
};
export default config;
Enter fullscreen mode Exit fullscreen mode

vite.config.js:

import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

export default defineConfig({
    plugins: [sveltekit()],
    server: {

        // Add this:
        fs: {
            allow: ['..'],  // Allow serving files from one level up to the project root
        },
    },
});
Enter fullscreen mode Exit fullscreen mode

Next, just contents of router/ folder

router.js:

import { writable } from 'svelte/store';
import { error } from '@sveltejs/kit';
import { patterns, default_error } from '/urls.js';

export const route = writable();
export const Router = {}

Router.get_pattern = function(pathname) {
    for (const pattern of patterns) {
        if (pattern.re.test(pathname)) {
            return pattern;
        }
    }
}

Router.check_404 = function(pattern) {
    if (!pattern) {
        throw error(404, 'Not found')
    }
}

Router.call_loads = async function(params, pattern, type) {
    for (const page of [pattern, ...(pattern.layouts || [])]) {
        if (!page[type]) continue;
        const load = (await page[type]()).load;
        params.data = {...params.data, ...await load(params)};
    }
    return params.data;
}

Router.get_route = async function(pattern, url) {
    const route = {}
    route.pattern = pattern;
    route.url = url;
    route.slugs = Router.get_slugs(pattern, url);
    route.error = await Router.get_error(pattern)
    return route;
}

Router.get_templates = async function(params, pattern) {
    const templates = {}
    templates.page = (await pattern.page()).default;
    templates.layouts = [];
    for (const layout of pattern.layouts || []) {
        templates.layouts.push((await layout.page()).default);
    }
    return templates;
}

Router.get_slugs = function(pattern, url) {
    const slugs = {}
    const matches = pattern.re.exec(url.pathname);
    for (const [index, match] of Object.entries(matches)) {
        const int_index = parseInt(index);
        if (int_index && int_index > 0) {
            slugs[pattern.slugs[index - 1]] = match
        }
    }
    return slugs;
}

Router.get_error = async function(pattern) {
    if (!pattern) return (await default_error()).default;
    for (const page of [...(pattern.layouts || []), pattern].reverse()) {
        if (!page.error) continue;
        const error = (await page.error()).default;
        return error;
    }
}
Enter fullscreen mode Exit fullscreen mode

[...path]/page.js:

import { Router, route } from '/router/router.js';

export async function load(params) {

    const pattern = Router.get_pattern(params.url.pathname);
    const raw_route = await Router.get_route(pattern, params.url);
    route.update((v) => ({...v, ...raw_route}));

    params.data = await Router.call_loads(params, pattern, 'js');

    const templates = await Router.get_templates(params, pattern);
    route.update((v) => ({...v, ...templates}));

    return params.data;
}
Enter fullscreen mode Exit fullscreen mode

[...path]/page.server.js

import { Router, route } from '/router/router.js';

export async function load(params) {

    const pattern = Router.get_pattern(params.url.pathname);
    Router.check_404(pattern);
    const raw_route = await Router.get_route(pattern, params.url);
    route.update((v) => ({...v, ...raw_route}));

    params.data = await Router.call_loads(params, pattern, 'server');

    return params.data;
}
Enter fullscreen mode Exit fullscreen mode

[...path]/+page.svelte:

<script>
    import {route} from '/router/router.js';
    export let data;
</script>

{#snippet draw(route, index)}
    {#if route.layouts.length && index < route.layouts.length}
        <svelte:component this={route.layouts[index]} {data}>
            {@render draw(route, index + 1)}
        </svelte:component>
    {:else}
        <svelte:component this={route.page} {data}/>
    {/if}
{/snippet}

{@render draw($route, 0)}
Enter fullscreen mode Exit fullscreen mode

[...path]/+error.svelte:

<script>
    import { Router, route } from '/router/router.js';
    import { page } from '$app/state';

    let Error = $state($route?.error);

    $effect(async() => {
        let pattern = Router.get_pattern(page.url.pathname);
        Error = await Router.get_error(pattern);
    });
</script>

{#if Error}
    <Error/>
{/if}
Enter fullscreen mode Exit fullscreen mode

Now, we just have global route store, that stores current slugs, matched page, etc.:

article.js:

<script>
    import {route} from '/router/router.js';
</script>

{$route.slugs.id}
Enter fullscreen mode Exit fullscreen mode

That's all

Top comments (0)