Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
0745c8f
add reroute adapter primitives in kit
teemingc Jul 30, 2026
7b61281
add fix and test for netlify
teemingc Jul 30, 2026
da5a2bc
add kit changeset
teemingc Jul 30, 2026
cecfbef
Merge branch 'version-3' into fix-split-reroute
teemingc Jul 30, 2026
aebb49a
add adapter changeset
teemingc Jul 30, 2026
cca9daf
vercel fix?
teemingc Jul 30, 2026
70b1398
reserved func name
teemingc Jul 30, 2026
51ecad1
dry
teemingc Jul 30, 2026
2c6920e
Update respond.js
teemingc Jul 30, 2026
3ec93b8
default to false
teemingc Jul 31, 2026
976b003
avoid changing url
teemingc Jul 31, 2026
87cbbad
Merge branch 'version-3' into fix-split-reroute
teemingc Jul 31, 2026
44e9b3a
add split test app
teemingc Jul 31, 2026
98a197b
make basic test specific to split app
teemingc Jul 31, 2026
0110bc5
revert
teemingc Jul 31, 2026
9cadb34
Update package.json
teemingc Jul 31, 2026
85afce7
Merge branch 'chore-vercel-split-test' into fix-split-reroute
teemingc Jul 31, 2026
859bced
Apply suggestion from @teemingc
teemingc Jul 31, 2026
c6c2796
move into different branch
teemingc Jul 31, 2026
4269f7f
Merge branch 'chore-vercel-split-test' into fix-split-reroute
teemingc Jul 31, 2026
c8db289
add test
teemingc Jul 31, 2026
67c938f
move
teemingc Jul 31, 2026
9360e32
Merge branch 'chore-vercel-split-test' into fix-split-reroute
teemingc Jul 31, 2026
e8adade
Revert "Merge branch 'chore-vercel-split-test' into fix-split-reroute"
teemingc Jul 31, 2026
327e87c
Merge branch 'fix-split-reroute' of https://github.com/sveltejs/kit i…
teemingc Jul 31, 2026
078ac53
format
teemingc Jul 31, 2026
00e0505
Merge branch 'version-3' into fix-split-reroute
teemingc Aug 3, 2026
eae3139
Update packages/adapter-vercel/index.js
teemingc Aug 3, 2026
df404ce
Apply suggestions from code review
teemingc Aug 3, 2026
fd3d200
Merge branch 'version-3' into fix-split-reroute
teemingc Aug 4, 2026
de53193
generate reroute module for middlewares
teemingc Aug 4, 2026
6a70886
fix alias
teemingc Aug 4, 2026
85b6068
Delete 15-@sveltejs-kit-adapter.md
teemingc Aug 4, 2026
bbf353f
avoid bundling otel into reroute module
teemingc Aug 4, 2026
5d22322
Merge branch 'version-3' into fix-split-reroute
teemingc Aug 4, 2026
fa04c01
Apply suggestion from @teemingc
teemingc Aug 4, 2026
c82020a
Update index.js
teemingc Aug 4, 2026
de5a81c
avoid unnecessary prebundling
teemingc Aug 4, 2026
d7316b0
replace global constants used in paths file
teemingc Aug 4, 2026
12cc7b7
Merge branch 'version-3' into fix-split-reroute
teemingc Aug 5, 2026
b375a12
ensure resolved_path is updated when url.pathname is modified
teemingc Aug 5, 2026
29e21be
Update rolldown.config.js
teemingc Aug 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/cuddly-hands-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@sveltejs/adapter-netlify': major
'@sveltejs/adapter-vercel': major

@teemingc teemingc Jul 31, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using a major here to make it more noticeable that folks will have this extra middleware function deployed

---

fix: run `reroute` in an edge middleware if the app has been split into multiple functions
5 changes: 5 additions & 0 deletions .changeset/mighty-groups-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: add `builder.hasRerouteHook` and `builder.generateRerouteModule` helpers for running `reroute` in a middleware before the main handler
6 changes: 6 additions & 0 deletions documentation/docs/25-build-and-deploy/80-adapter-netlify.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ Additionally, you can add your own Netlify functions by creating a directory for
directory = "functions"
```

## Notes

### Individual functions and `reroute`

If the adapter `split` option is set to `true`, the [`reroute`](hooks#reroute) hook will be deployed as an edge middleware that runs before any individual function.

## Troubleshooting

### Accessing the file system
Expand Down
4 changes: 4 additions & 0 deletions documentation/docs/25-build-and-deploy/90-adapter-vercel.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ If you have Vercel functions contained in the `api` directory at the project's r

Projects created before a certain date may default to using an older Node version than what SvelteKit currently requires. You can [change the Node version in your project settings](https://vercel.com/docs/concepts/functions/serverless-functions/runtimes/node-js#node.js-version).

### Individual functions and `reroute`

When deploying multiple serverless functions, the [`reroute`](hooks#reroute) hook will be deployed as a middleware that runs before any individual function.

## Troubleshooting

### Accessing the file system
Expand Down
4 changes: 3 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ export default [
'**/test-results',
'**/dist',
'**/.custom-out-dir',
'packages/adapter-*/files',
'packages/adapter-*/files/**/*',
'!packages/adapter-cloudflare/files',
'!packages/adapter-netlify/files/edge.js',
'!packages/adapter-netlify/files/reroute.js',
'!packages/adapter-vercel/files',
'packages/kit/src/core/config/fixtures/multiple', // dir contains svelte config with multiple extensions tripping eslint
'packages/kit/src/core/sync/create_manifest_data/test/samples/**/*',
Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-netlify/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
.DS_Store
node_modules
/files
/files/serverless.js
11 changes: 11 additions & 0 deletions packages/adapter-netlify/files/reroute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { applyReroute } from 'REROUTE';

/** @type {import('@netlify/edge-functions').EdgeFunction} */
export default async function middleware(request, context) {
const new_request = await applyReroute(request);

// Netlify rewrites can cause an endless loop because it will re-run this
// function with the rewritten URL. Therefore, we use `context.next` instead
// to specifically invoke the next function in the chain with the rewritten URL
return context.next(new_request);
}
217 changes: 158 additions & 59 deletions packages/adapter-netlify/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/** @import { Builder, RouteDefinition } from '@sveltejs/kit' */
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { join, posix } from 'node:path';
import { fileURLToPath } from 'node:url';
Expand Down Expand Up @@ -92,14 +93,21 @@ export default function ({ split = false, edge = edge_set_in_env_var } = {}) {
builder.log.minor('Writing Netlify config...');
write_frameworks_config({ builder });

let reroute_middleware = false;

if (edge) {
if (split) {
throw new Error('Cannot use `split: true` alongside `edge: true`');
}

await generate_edge_functions({ builder });
await generate_edge_functions({ builder, reroute_middleware });
} else {
generate_serverless_functions({ builder, split, publish });
if (split && (await builder.hasRerouteHook())) {
await generate_reroute_middleware(builder);
reroute_middleware = true;
}

generate_serverless_functions({ builder, split, publish, reroute_middleware });
}
},

Expand All @@ -111,12 +119,13 @@ export default function ({ split = false, edge = edge_set_in_env_var } = {}) {
}

/**
* @param { object } params
* @param {import('@sveltejs/kit').Builder} params.builder
* @param { string } params.publish
* @param { boolean } params.split
* @param {object} params
* @param {Builder} params.builder
* @param {string} params.publish
* @param {boolean} params.split
* @param {boolean} params.reroute_middleware
*/
function generate_serverless_functions({ builder, publish, split }) {
function generate_serverless_functions({ builder, publish, split, reroute_middleware }) {
// https://docs.netlify.com/build/frameworks/frameworks-api/#netlifyv1functions
mkdirSync(netlify_framework_serverless_path, { recursive: true });

Expand Down Expand Up @@ -180,7 +189,8 @@ function generate_serverless_functions({ builder, publish, split }) {
builder,
routes,
patterns,
name
name,
reroute_middleware
});
}

Expand All @@ -189,14 +199,16 @@ function generate_serverless_functions({ builder, publish, split }) {
routes: [],
patterns: ['/*'],
name: `${FUNCTION_PREFIX}catch-all`,
exclude: Array.from(seen)
exclude: Array.from(seen),
reroute_middleware
});
} else {
generate_serverless_function({
builder,
routes: undefined,
patterns: ['/*'],
name: `${FUNCTION_PREFIX}render`
name: `${FUNCTION_PREFIX}render`,
reroute_middleware: false
});
}

Expand Down Expand Up @@ -227,7 +239,7 @@ function get_netlify_config() {
/**
* Writes the Netlify Frameworks API config file
* https://docs.netlify.com/build/frameworks/frameworks-api/
* @param {{ builder: import('@sveltejs/kit').Builder }} params
* @param {{ builder: Builder }} params
*/
function write_frameworks_config({ builder }) {
// https://docs.netlify.com/build/frameworks/frameworks-api/#headers
Expand All @@ -250,17 +262,26 @@ function write_frameworks_config({ builder }) {
/**
*
* @param {{
* builder: import('@sveltejs/kit').Builder,
* routes: import('@sveltejs/kit').RouteDefinition[] | undefined,
* builder: Builder,
* routes: RouteDefinition[] | undefined,
* patterns: string[],
* name: string,
* exclude?: string[]
* exclude?: string[],
* reroute_middleware: boolean
* }} opts
*/
function generate_serverless_function({ builder, routes, patterns, name, exclude }) {
function generate_serverless_function({
builder,
routes,
patterns,
name,
exclude,
reroute_middleware
}) {
const manifest = builder.generateManifest({
relativePath: '../server',
routes
routes,
rerouteMiddleware: reroute_middleware
});

const fn = generate_serverless_function_module(manifest);
Expand Down Expand Up @@ -349,10 +370,11 @@ const rolldown_config = {
};

/**
* @param { object } params
* @param {import('@sveltejs/kit').Builder} params.builder
* @param {object} params
* @param {Builder} params.builder
* @param {boolean} params.reroute_middleware
*/
async function generate_edge_functions({ builder }) {
async function generate_edge_functions({ builder, reroute_middleware }) {
const tmp = builder.getBuildDirectory('netlify-tmp');
rmSync(tmp, { force: true, recursive: true });
mkdirSync(tmp, { recursive: true });
Expand All @@ -370,45 +392,72 @@ async function generate_edge_functions({ builder }) {
}
});

const manifest = builder.generateManifest({
relativePath
await bundle_edge_function({ builder, name: 'render', reroute_middleware });
}

/**
* @param {Builder} builder
*/
async function generate_reroute_middleware(builder) {
builder.log.minor('Generating edge middleware to run reroute before split functions...');

const tmp = builder.getBuildDirectory('netlify-tmp');
builder.rimraf(tmp);
builder.mkdirp(tmp);

builder.mkdirp('.netlify/edge-functions');

const reroute_path = `${tmp}/reroute.js`;
await builder.generateRerouteModule(reroute_path);

builder.copy(`${files}/reroute.js`, `${tmp}/entry.js`, {
replace: {
REROUTE: reroute_path
}
});

writeFileSync(`${tmp}/manifest.js`, `export const manifest = ${manifest};\n`);
const path = '/*';
const excluded_paths = await get_excluded_paths(tmp, builder);

await build({
...rolldown_config,
input: `${tmp}/entry.js`,
output: {
...rolldown_config.output,
file: `${netlify_framework_edge_path}/${FUNCTION_PREFIX}reroute.js`
}
});

/** @type {{ assets: Set<string> }} */
// we have to prepend the file:// protocol because Windows doesn't support absolute path imports
const { assets } = (await import(`file://${tmp}/manifest.js`)).manifest;
add_edge_function_config({ builder, path, name: 'reroute', excluded_paths });
}

/**
*
* @param {object} params
* @param {Builder} params.builder
* @param {string} params.name
* @param {boolean} params.reroute_middleware
*/
async function bundle_edge_function({ builder, name, reroute_middleware }) {
const tmp = builder.getBuildDirectory('netlify-tmp');

const relativePath = posix.relative(tmp, builder.getServerDirectory());
const manifest = builder.generateManifest({
relativePath,
rerouteMiddleware: reroute_middleware
});
writeFileSync(`${tmp}/manifest.js`, `export const manifest = ${manifest};\n`);

const path = '/*';
// We only need to specify paths without the trailing slash because
// Netlify will handle the optional trailing slash for us
const excluded_paths = [
// Contains static files
`/${builder.getAppPath()}/immutable/*`,
`/${builder.getAppPath()}/version.json`,
...builder.prerendered.paths,
...Array.from(assets).flatMap((asset) => {
if (asset.endsWith('/index.html')) {
const dir = asset.replace(/\/index\.html$/, '');
return [
`${builder.config.kit.paths.base}/${asset}`,
`${builder.config.kit.paths.base}/${dir}`
];
}
return `${builder.config.kit.paths.base}/${asset}`;
}),
// Should not be served by SvelteKit at all
'/.netlify/*'
];
const excluded_paths = await get_excluded_paths(tmp, builder);

await Promise.all([
build({
...rolldown_config,
input: `${tmp}/entry.js`,
output: {
...rolldown_config.output,
file: `${netlify_framework_edge_path}/${FUNCTION_PREFIX}render.js`
file: `${netlify_framework_edge_path}/${FUNCTION_PREFIX}${name}.js`
}
}),
builder.hasServerInstrumentationFile() &&
Expand All @@ -424,33 +473,83 @@ async function generate_edge_functions({ builder }) {

if (builder.hasServerInstrumentationFile()) {
builder.instrument({
entrypoint: `${netlify_framework_edge_path}/${FUNCTION_PREFIX}render.js`,
entrypoint: `${netlify_framework_edge_path}/${FUNCTION_PREFIX}${name}.js`,
instrumentation: `${netlify_framework_edge_path}/${FUNCTION_PREFIX}instrumentation.server.js`,
start: `${netlify_framework_edge_path}/${FUNCTION_PREFIX}start.js`
});
}

add_edge_function_config({ builder, path, excluded_paths });
add_edge_function_config({ builder, path, name, excluded_paths });
}

/**
* Adds edge function configuration to the Frameworks API config file `config.json`
* https://docs.netlify.com/build/frameworks/frameworks-api/#netlifyv1edge-functions
* @param {{ builder: import('@sveltejs/kit').Builder, path: string, excluded_paths: string[] }} params
* @param {{ builder: Builder, path: string, name: string, excluded_paths: string[] }} params
*/
function add_edge_function_config({ path, excluded_paths }) {
function add_edge_function_config({ path, name, excluded_paths }) {
const config = JSON.parse(readFileSync(netlify_framework_config_path, 'utf-8'));

// https://docs.netlify.com/build/frameworks/frameworks-api/#configuration-options-1
config.edge_functions = [
{
function: `${FUNCTION_PREFIX}render`,
name: 'SvelteKit server',
generator: generator_string,
path,
excludedPath: excluded_paths
}
];
const edge_function_config = {
function: `${FUNCTION_PREFIX}${name}`,
name: `SvelteKit server for ${name}`,
generator: generator_string,
path,
excludedPath: excluded_paths
};

if (Array.isArray(config.edge_functions)) {
config.edge_functions.push(edge_function_config);
} else {
config.edge_functions = [edge_function_config];
}

writeFileSync(netlify_framework_config_path, s(config));
}

/** @type {string[]} */
let excluded_paths;

/**
*
* @param {string} tmp
* @param {Builder} builder
* @returns {Promise<string[]>}
*/
async function get_excluded_paths(tmp, builder) {
if (excluded_paths) return excluded_paths;

const relativePath = posix.relative(tmp, builder.getServerDirectory());
const manifest = builder.generateManifest({
relativePath
});
writeFileSync(`${tmp}/manifest.js`, `export const manifest = ${manifest};\n`);

/** @type {{ assets: Set<string> }} */
// we have to prepend the file:// protocol because Windows doesn't support absolute path imports
const { assets } = (await import(`file://${tmp}/manifest.js`)).manifest;

// We only need to specify paths without the trailing slash because
// Netlify will handle the optional trailing slash for us
excluded_paths = [
// Contains static files
`/${builder.getAppPath()}/immutable/*`,
`/${builder.getAppPath()}/version.json`,
...builder.prerendered.paths,
...Array.from(assets).flatMap((asset) => {
if (asset.endsWith('/index.html')) {
const dir = asset.replace(/\/index\.html$/, '');
return [
`${builder.config.kit.paths.base}/${asset}`,
`${builder.config.kit.paths.base}/${dir}`
];
}
return `${builder.config.kit.paths.base}/${asset}`;
}),
// Should not be served by SvelteKit at all
'/.netlify/*'
];

return excluded_paths;
}
Loading
Loading