Skip to content

Commit 5906e97

Browse files
feat: server side route resolution (#13379)
This PR adds server side route resolution to SvelteKit. This means that instead of loading the whole routing manifest in the client, and doing the route resolution there, the server runtime is invoked for each route request. How this works: - user clicks on `<a href="/foo/bar">..</a>` - server call to `_app/route[route].js`, so in this case `_app/routes/foo/bar.js` - SvelteKit server runtime does route resolution (does this match a route, taking reroutes into account etc) on the server and returns a response that is a JavaScript file containing the route information in a format that can be parsed on the client What this enables: - Projects with massive routes can use this to not send so many kilobytes up front to the client, because the client routing manifest would no longer be sent - You can hide what routes you have - Because the server is hit for every route resolution, you can put things like edge middleware in front and be confident it is always called, and for example do rewrites (for example for A/B testing) in there --------- Co-authored-by: Rich Harris <[email protected]>
1 parent 09296d0 commit 5906e97

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+743
-152
lines changed

.changeset/nine-camels-begin.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/adapter-vercel': minor
3+
---
4+
5+
feat: generate edge function dedicated to server side route resolution when using that option in SvelteKit

.changeset/slimy-foxes-travel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': minor
3+
---
4+
5+
feat: support server-side route resolution

.github/workflows/ci.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,40 @@ jobs:
145145
retention-days: 3
146146
name: test-failure-cross-platform-${{ matrix.mode }}-${{ github.run_id }}-${{ matrix.os }}-${{ matrix.node-version }}-${{ matrix.e2e-browser }}
147147
path: test-results-cross-platform-${{ matrix.mode }}.tar.gz
148+
test-kit-server-side-route-resolution:
149+
runs-on: ubuntu-latest
150+
timeout-minutes: 30
151+
strategy:
152+
fail-fast: false
153+
matrix:
154+
include:
155+
- mode: 'dev'
156+
- mode: 'build'
157+
steps:
158+
- run: git config --global core.autocrlf false
159+
- uses: actions/checkout@v4
160+
- uses: pnpm/[email protected]
161+
- uses: actions/setup-node@v4
162+
with:
163+
node-version: 22
164+
cache: pnpm
165+
- run: pnpm install --frozen-lockfile
166+
- run: pnpm playwright install chromium
167+
- run: pnpm run sync-all
168+
- run: pnpm test:server-side-route-resolution:${{ matrix.mode }}
169+
- name: Print flaky test report
170+
run: node scripts/print-flaky-test-report.js
171+
- name: Archive test results
172+
if: failure()
173+
shell: bash
174+
run: find packages -type d -name test-results -not -empty | tar -czf test-results-server-side-route-resolution-${{ matrix.mode }}.tar.gz --files-from=-
175+
- name: Upload test results
176+
if: failure()
177+
uses: actions/upload-artifact@v4
178+
with:
179+
retention-days: 3
180+
name: test-failure-server-side-route-resolution-${{ matrix.mode }}-${{ github.run_id }}
181+
path: test-results-server-side-route-resolution-${{ matrix.mode }}.tar.gz
148182
test-others:
149183
runs-on: ubuntu-latest
150184
steps:

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
"test:kit": "pnpm run --dir packages/kit test",
99
"test:cross-platform:dev": "pnpm run --dir packages/kit test:cross-platform:dev",
1010
"test:cross-platform:build": "pnpm run --dir packages/kit test:cross-platform:build",
11+
"test:server-side-route-resolution:dev": "pnpm run --dir packages/kit test:server-side-route-resolution:dev",
12+
"test:server-side-route-resolution:build": "pnpm run --dir packages/kit test:server-side-route-resolution:build",
1113
"test:vite-ecosystem-ci": "pnpm test --dir packages/kit",
1214
"test:others": "pnpm test -r --filter=./packages/* --filter=!./packages/kit/ --workspace-concurrency=1",
1315
"check": "pnpm -r prepublishOnly && pnpm -r check",

packages/adapter-vercel/index.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,26 @@ const plugin = function (defaults = {}) {
390390
);
391391
}
392392

393+
// optional chaining to support older versions that don't have this setting yet
394+
if (builder.config.kit.router?.resolution === 'server') {
395+
// Create a separate edge function just for server-side route resolution.
396+
// By omitting all routes we're ensuring it's small (the routes will still be available
397+
// to the route resolution, becaue it does not rely on the server routing manifest)
398+
await generate_edge_function(
399+
`${builder.config.kit.appDir}/routes`,
400+
{
401+
external: 'external' in defaults ? defaults.external : undefined,
402+
runtime: 'edge'
403+
},
404+
[]
405+
);
406+
407+
static_config.routes.push({
408+
src: `${builder.config.kit.paths.base}/${builder.config.kit.appDir}/routes(\\.js|/.*)`,
409+
dest: `${builder.config.kit.paths.base}/${builder.config.kit.appDir}/routes`
410+
});
411+
}
412+
393413
// Catch-all route must come at the end, otherwise it will swallow all other routes,
394414
// including ISR aliases if there is only one function
395415
static_config.routes.push({ src: '/.*', dest: `/${DEFAULT_FUNCTION_NAME}` });

packages/kit/kit.vitest.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { fileURLToPath } from 'node:url';
12
import { defineConfig } from 'vitest/config';
23

34
// this file needs a custom name so that the numerous test subprojects don't all pick it up
@@ -8,6 +9,9 @@ export default defineConfig({
89
}
910
},
1011
test: {
12+
alias: {
13+
'__sveltekit/paths': fileURLToPath(new URL('./test/mocks/path.js', import.meta.url))
14+
},
1115
// shave a couple seconds off the tests
1216
isolate: false,
1317
poolOptions: {

packages/kit/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@
6969
"test:integration": "pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test",
7070
"test:cross-platform:dev": "pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:cross-platform:dev",
7171
"test:cross-platform:build": "pnpm test:unit && pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:cross-platform:build",
72+
"test:server-side-route-resolution:dev": "pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:server-side-route-resolution:dev",
73+
"test:server-side-route-resolution:build": "pnpm test:unit && pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:server-side-route-resolution:build",
7274
"test:unit": "vitest --config kit.vitest.config.js run",
7375
"prepublishOnly": "pnpm generate:types",
7476
"generate:version": "node scripts/generate-version.js",

packages/kit/src/core/config/index.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,5 +115,20 @@ export function validate_config(config) {
115115
);
116116
}
117117

118-
return options(config, 'config');
118+
const validated = options(config, 'config');
119+
120+
if (validated.kit.router.resolution === 'server') {
121+
if (validated.kit.router.type === 'hash') {
122+
throw new Error(
123+
"The `router.resolution` option cannot be 'server' if `router.type` is 'hash'"
124+
);
125+
}
126+
if (validated.kit.output.bundleStrategy !== 'split') {
127+
throw new Error(
128+
"The `router.resolution` option cannot be 'server' if `output.bundleStrategy` is 'inline' or 'single'"
129+
);
130+
}
131+
}
132+
133+
return validated;
119134
}

packages/kit/src/core/config/index.spec.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ const get_defaults = (prefix = '') => ({
9595
output: { preloadStrategy: 'modulepreload', bundleStrategy: 'split' },
9696
outDir: join(prefix, '.svelte-kit'),
9797
router: {
98-
type: 'pathname'
98+
type: 'pathname',
99+
resolution: 'client'
99100
},
100101
serviceWorker: {
101102
register: true

packages/kit/src/core/config/options.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,8 @@ const options = object(
261261
}),
262262

263263
router: object({
264-
type: list(['pathname', 'hash'])
264+
type: list(['pathname', 'hash']),
265+
resolution: list(['client', 'server'])
265266
}),
266267

267268
serviceWorker: object({

0 commit comments

Comments
 (0)