SvelteKit: How to make code-based router, instead of file-based router [February 2023]
Tags: svelte, sveltekit
Published: 2023-02-24 04:39:01

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:
```javascript
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'},
        },
    },
}
```
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 `component`s 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:
```javascript
{
    ...
    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},]
}
```

## 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
```
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.
```javascript
293: prevent_conflicts(routes);
+ 294: console.log(routes);
```
You'll see something like:

```javascript
[
  {
    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
  }
]
```
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):**
```javascript
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;
}
```
**2) In `svelte.config.js`:**
```javascript
...
import {routes} from './urls.js';

const config = {
    routes: routes(),
    ...
};
```
**3) Open:**
```
node_modules/@sveltejs/kit/src/core/sync/create_manifest_data/index.js
```
a) Find:
```javascript
293: prevent_conflicts(routes);
294:
295: const root = routes[0];
```
Paste between that two lines (right in 294 line):
```javascript
routes.length = 0;
routes.push(...config.routes);
```
b) Find:
```javascript
375: routes: sort_routes(routes)
```
Replace with just:
```javascript
routes: routes
```
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
```
`package.json`:
```javascript
{
  ...
  "scripts": {
    ...
    "postinstall": "patch-package" // <— add this
```

## Hope it helps!