DEV Community

Max Core
Max Core

Posted on • Edited on

SvelteKit: How to make code-based router, instead of file-based router [February 2023]

Dear dudes.
It's the third time I've made that patch.

For those who build enterprises and need total control over files/folders and their names/structure. If it's what you've been searching — welcome reading)

In result, there is some urls.js file with routes declaration:

const layouts = {
    'marketing': {
        component: 'src/marketing.svelte',
        pages: {
            '/about': {pattern: /^\/about\/?$/, component: 'src/about.svelte'},
        },
    },
    'app': {
        component: 'src/layout.svelte',
        layouts: {
            'settings': {
                component: 'src/settings.svelte',
                pages: {
                    '/privacy': {pattern: /^\/privacy\/?$/, component: 'src/privacy.svelte'},
                    '/profile': {pattern: /^\/profile\/?$/, component: 'src/profile.svelte'},
                },
            }
        },
        pages: {
            '/':            {pattern: /^\/\/?$/,               component: 'src/home.svelte'},
            '/[username]':  {pattern: /^\/([^/]+?)\/?$/,       component: 'src/user.svelte'},
            '/post/[slug]': {pattern: /^\/post\/([^/]+?)\/?$/, component: 'src/post.svelte'},
        },
    },
}
Enter fullscreen mode Exit fullscreen mode

Here we have 2 independent layouts: marketing and app.

Each layout has its pages collection, and app layout even has nested layout — settings.

Guess you've mentioned that components paths are already nice.

More of that — order is respected. Not only between patterns, but between layout and pages: /privacy and /profile matches first, and only then its /[username] turn.

Along with component, all other params could be passed:

{
    ...
    universal: 'src/anyname.js'// aka +xxx.js
    server: 'src/anyname.js' // aka +xxx.server.js
    endpoint: 'src/anyname.js' // aka +server.js
    error: 'src/anyname.svelte' // aka +error.svelte

    // In case you need full control of params
    params: [{name: slug, matcher: undefined, optional: false, rest: false, chained: false},]
}
Enter fullscreen mode Exit fullscreen mode

If it looks nice to you, lets dive to the implementation:

SvelteKit's magic is happening there:

node_modules/@sveltejs/kit/src/core/sync/create_manifest_data/index.js
Enter fullscreen mode Exit fullscreen mode

Firstly, some walk() function creates some routes object based on file system. Then validates and enriches it.
You can print and see that routes object.

293: prevent_conflicts(routes);
+ 294: console.log(routes);
Enter fullscreen mode Exit fullscreen mode

You'll see something like:

[
  {
    id: '/',
    segment: '',
    pattern: /^\/$/,
    params: [],
    layout: {
      depth: 0,
      child_pages: [],
      component: 'src/routes/+layout.svelte'
    },
    error: null,
    leaf: null,
    page: null,
    endpoint: null
  },
  {
    id: '/home',
    segment: 'home',
    pattern: /^\/home\/?$/,
    params: [],
    layout: null,
    error: null,
    leaf: { depth: 1, component: 'src/routes/home/+page.svelte' },
    page: null,
    endpoint: null
  }
]
Enter fullscreen mode Exit fullscreen mode

So, the goal is to build that routes object somehow ourselves

So, what exactly has to be done?

1) Create urls.js somewhere (guess in root will be nice) with contents (const layouts is just for an example):

const layouts = {
    'marketing': {
        component: 'src/marketing.svelte',
        pages: {
            '/about': {pattern: /^\/about\/?$/, component: 'src/about.svelte'},
        },
    },
    'app': {
        component: 'src/layout.svelte',
        layouts: {
            'settings': {
                component: 'src/settings.svelte',
                pages: {
                    '/privacy': {pattern: /^\/privacy\/?$/, component: 'src/privacy.svelte'},
                    '/profile': {pattern: /^\/profile\/?$/, component: 'src/profile.svelte'},
                },
            }
        },
        pages: {
            '/':            {pattern: /^\/\/?$/,               component: 'src/home.svelte'},
            '/[username]':  {pattern: /^\/([^/]+?)\/?$/,       component: 'src/user.svelte'},
            '/post/[slug]': {pattern: /^\/post\/([^/]+?)\/?$/, component: 'src/post.svelte'},
        },
    },
}

export function routes() {
    const result = []
    function run(depth, items, parent) {
        for (const [id, item] of Object.entries(items)) {
            const route = {
                id: id,
                segment: id.split('/')[1] || id,
                pattern: item.pattern,
                params: item.params || [],
                error: item.error ? {depth: depth, component: item.error} : null,
                endpoint: item.endpoint ? { file: item.endpoint } : null,
                page: null,
                layout: null,
                leaf: null,
                parent: parent,
            }
            const details = {
                depth: depth,
                child_pages: [],
                universal: item.universal,
                server: item.server,
                component: item.component,
            }
            if (!id.startsWith('/')) {  // means — if layout
                route.layout = details;
            } else {
                route.leaf = details;
            }
            if ((!route.params || !route.params.length)) {
                const matches = id.match(/\[([^\]]*)]/g) || [];
                for (const match of matches) {
                    route.params.push({
                        name: match.replace('[', '').replace(']', ''),
                        matcher: undefined,
                        optional: false,
                        rest: false,
                        chained: false
                    })
                }
            }
            result.push(route)
            // Do it like this to save order
            for (const [field, object] of Object.entries(item)) {
                const layouts = (field === 'layouts') ? object : null;
                const pages = (field === 'pages') ? object : null;
                if (layouts && Object.keys(layouts).length) {
                    run(depth + 1, layouts, route)
                }
                if (pages && Object.keys(pages).length) {
                    run(depth + 1, pages, route)
                }
            }
        }
    }
    run(0, layouts, null);
    return result;
}
Enter fullscreen mode Exit fullscreen mode

2) In svelte.config.js:

...
import {routes} from './urls.js';

const config = {
    routes: routes(),
    ...
};
Enter fullscreen mode Exit fullscreen mode

3) Open:

node_modules/@sveltejs/kit/src/core/sync/create_manifest_data/index.js
Enter fullscreen mode Exit fullscreen mode

a) Find:

293: prevent_conflicts(routes);
294:
295: const root = routes[0];
Enter fullscreen mode Exit fullscreen mode

Paste between that two lines (right in 294 line):

routes.length = 0;
routes.push(...config.routes);
Enter fullscreen mode Exit fullscreen mode

b) Find:

375: routes: sort_routes(routes)
Enter fullscreen mode Exit fullscreen mode

Replace with just:

routes: routes
Enter fullscreen mode Exit fullscreen mode

No need for additional magic sorting, everything is under control.

Minor notes

1) Layouts names may be anything but unique, and must not start with "/";
2) Pages names must match pattern and start with "/";

If you want, you could install patch-package, so this changes will be automatically applied in future without manual hacks:

> npm i patch-package
> npx patch-package @sveltejs/kit
Enter fullscreen mode Exit fullscreen mode

package.json:

{
  ...
  "scripts": {
    ...
    "postinstall": "patch-package" // <— add this
Enter fullscreen mode Exit fullscreen mode

Hope it helps!

Top comments (1)

Collapse
 
greggcbs profile image
GreggHume

I think you should mention patch-package upfront. I know you said "its the third time i have made that patch" but its not clear.

patch-package will be a no from me. But thanks for showing this is possible, just not in the way i would like it.