Skip to content
Draft
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
8269fd8
provide emulate hook to intercept request early on
dummdidumm Feb 14, 2025
9d0beac
vercel WIP
dummdidumm Feb 15, 2025
fd8c296
fix
dummdidumm Feb 15, 2025
b5ea739
bundle ourselves instead
dummdidumm Feb 15, 2025
ac0ca4b
wrap rewrite and url automatically
dummdidumm Feb 15, 2025
9a38b48
devtimefix
dummdidumm Feb 15, 2025
2d406bd
add capability for adapters to provide additional entry points
dummdidumm Feb 18, 2025
253318a
vercel middleware
dummdidumm Feb 18, 2025
05e9b62
node
dummdidumm Feb 18, 2025
8b8c4ac
lint
dummdidumm Feb 19, 2025
344dd7b
expose new helpers from Kit
dummdidumm Feb 19, 2025
cb2d693
incorporate new helpers in Vercel adapter + fix a few bugs
dummdidumm Feb 19, 2025
db4ed04
do base path normalization within Vite middleware so adapters don't h…
dummdidumm Feb 19, 2025
b6e623f
tweak adapter API: switch allowed to disallowed, restrict imports to …
dummdidumm Feb 19, 2025
1c9b512
tests
dummdidumm Feb 19, 2025
fe1b82b
writing adapters docs
dummdidumm Feb 19, 2025
45b4706
fix
dummdidumm Feb 19, 2025
c20d423
fix lint / test
dummdidumm Feb 19, 2025
62289cb
vercel-middleware -> edge-middleware
dummdidumm Feb 19, 2025
9a3c6e2
netlify edge middleware
dummdidumm Feb 19, 2025
e994082
cloudflare adapter
dummdidumm Feb 20, 2025
05c941e
fix
dummdidumm Feb 20, 2025
1d9839a
fix
dummdidumm Feb 20, 2025
5d5eb26
fix, docs
dummdidumm Feb 20, 2025
70ad0f8
fix, docs tweaks
dummdidumm Feb 20, 2025
b159f21
fix
dummdidumm Feb 20, 2025
4335dc5
changesets
dummdidumm Feb 20, 2025
b5f68cb
drive-by notes fix
dummdidumm Feb 21, 2025
37bc902
reuse `supports` mechanism, simplify additionalEntryPoints to a recor…
dummdidumm Feb 21, 2025
5585df2
fix
dummdidumm Feb 21, 2025
0b5396e
do normalization inside adapters
dummdidumm Feb 22, 2025
76f5a9b
don't bundle sveltekit
dummdidumm Feb 22, 2025
679759f
put them into src so that they benefit from better intellisense
dummdidumm Feb 22, 2025
e39683e
fix docs
dummdidumm Feb 22, 2025
c1f7591
Merge branch 'main' into middleware-take-2
dummdidumm Feb 22, 2025
0876716
tweaks
dummdidumm Feb 24, 2025
8524d6f
netlify: make where it runs on configurable; node/preview: run it on …
dummdidumm Feb 24, 2025
6e90adc
beforeRequest -> interceptRequest
dummdidumm Feb 24, 2025
083b22d
fix test config
dummdidumm Feb 24, 2025
bec3bed
Merge branch 'main' into middleware-take-2
dummdidumm Feb 24, 2025
4e948fa
Merge remote-tracking branch 'origin/main' into middleware-take-2
dummdidumm Feb 24, 2025
b367434
Update tsconfig.json
dummdidumm Feb 24, 2025
1ee1d04
docs tweaks
dummdidumm Feb 28, 2025
a2c5222
fix folder path
dummdidumm Mar 3, 2025
30a3680
tweak
dummdidumm Mar 3, 2025
a2bb531
Merge branch 'main' into middleware-take-2
Rich-Harris Mar 3, 2025
cd116e9
docs feedback
dummdidumm Mar 3, 2025
663d59d
warn on stream read (don't error because you can't guard against it i…
dummdidumm Mar 3, 2025
f9efcbd
fix test
dummdidumm Mar 3, 2025
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
12 changes: 12 additions & 0 deletions documentation/docs/25-build-and-deploy/40-adapter-node.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,18 @@ The number of seconds to wait before forcefully closing any remaining connection

When using systemd socket activation, `IDLE_TIMEOUT` specifies the number of seconds after which the app is automatically put to sleep when receiving no requests. If not set, the app runs continuously. See [Socket activation](#Socket-activation) for more details.

## Middleware

You can integrate Express or Polka middleware into your SvelteKit application built with the Node adapter by placing a `node-middleware.js` file at the root of your project. It must export a default function which receives the same arguments as [Polka middleware](https://github.com/lukeed/polka?tab=readme-ov-file#middleware). The middleware runs on all requests except those to files inside `_app/immutable`. Combined with using [server-side route resolution](configuration#router) you can make sure it runs prior to all navigations, no matter prerendered or not and no matter client- or server-side.

```js
/// file: node-middleware.js
export default function middleware(req, res, next) {
console.log(`Received ${req.method} on ${req.url}`);
next(); // move on
}
```

## Options

The adapter can be configured with various options:
Expand Down
47 changes: 47 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 @@ -141,6 +141,53 @@ A list of valid query parameters that contribute to the cache key. Other paramet

> Pages that are [prerendered](page-options#prerender) will ignore ISR configuration.

## Edge Middleware

You can make use of [Vercel Edge Middleware](https://vercel.com/docs/functions/edge-middleware) by placing a `vercel-middleware.js` file at the root of your project. You can use it to intercept requests even for prerendered or ISR'd pages. Combined with using [server-side route resolution](configuration#router) you can make sure it runs prior to all navigations, no matter client- or server-side. This allows you to for example run A/B-tests on prerendered or ISR'd pages by rerouting a user to either variant A or B depending on a cookie.

```js
/// file: vercel-middleware.js
import { rewrite, next } from '@vercel/edge';

export default async function middleware(request: Request) {
const url = new URL(request.url);

if (url.pathname !== '/') return next();

// Retrieve feature flag from cookies
let flag = split_cookies(request.headers.get('cookie') ?? '')?.flag;

// Fall back to random value if this is a new visitor
flag ||= Math.random() > 0.5 ? 'a' : 'b';

return rewrite(
// Get destination URL based on the feature flag
flag === 'a' ? '/home-a' : '/home-b',
{
headers: {
// Set a cookie to remember the feature flags for this visitor
'Set-Cookie': `flag=${flag}; Path=/`
}
}
);
}

function split_cookies(cookies: string) {
return cookies.split(';').reduce(
(acc, cookie) => {
const [name, value] = cookie.trim().split('=');
acc[name] = value;
return acc;
},
{} as Record<string, string>
);
}
```

By default, middleware runs on all requests except for files within `_app/immutable`. You can customize this by exporting a `config` object with a `matcher` property as described in Vercel's [API documentation](https://vercel.com/docs/functions/edge-middleware/middleware-api#match-paths-based-on-custom-matcher-config).

If you want to run code prior to a request but neither have prerendered nor ISR'd pages and have no rerouting logic, then it makes more sense to use the [handle hook](hooks#Server-hooks-handle) instead.

## Environment variables

Vercel makes a set of [deployment-specific environment variables](https://vercel.com/docs/concepts/projects/environment-variables#system-environment-variables) available. Like other environment variables, these are accessible from `$env/static/private` and `$env/dynamic/private` (sometimes — more on that later), and inaccessible from their public counterparts. To access one of these variables from the client:
Expand Down
49 changes: 45 additions & 4 deletions packages/adapter-node/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { readFileSync, writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { rollup } from 'rollup';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import { VERSION } from '@sveltejs/kit';

const [major, minor] = VERSION.split('.').map(Number);
const can_use_middleware = major > 2 || (major === 2 && minor > 17);

/** @type {string | null} */
let middleware_path = can_use_middleware ? 'node-middleware.js' : null;
if (middleware_path && !existsSync(middleware_path)) {
middleware_path = 'node-middleware.ts';
if (!existsSync(middleware_path)) middleware_path = null;
}

const files = fileURLToPath(new URL('./files', import.meta.url).href);

Expand Down Expand Up @@ -46,6 +57,10 @@ export default function (opts = {}) {
].join('\n\n')
);

if (!middleware_path) {
writeFileSync(`${tmp}/node-middleware.js`, 'export default (req, res, next) => next();');
}

const pkg = JSON.parse(readFileSync('package.json', 'utf8'));

// we bundle the Vite output so that deployments only need
Expand All @@ -54,7 +69,8 @@ export default function (opts = {}) {
const bundle = await rollup({
input: {
index: `${tmp}/index.js`,
manifest: `${tmp}/manifest.js`
manifest: `${tmp}/manifest.js`,
'node-middleware': `${tmp}/node-middleware.js`
},
external: [
// dependencies could have deep exports, so we need a regex
Expand Down Expand Up @@ -86,11 +102,36 @@ export default function (opts = {}) {
MANIFEST: './server/manifest.js',
SERVER: './server/index.js',
SHIMS: './shims.js',
ENV_PREFIX: JSON.stringify(envPrefix)
ENV_PREFIX: JSON.stringify(envPrefix),
MIDDLEWARE: './server/node-middleware.js'
}
});
},

emulate: ({ importFile }) => {
if (!existsSync(middleware_path)) return {};

return {
beforeRequest: async (req, res, next) => {
// We have to import this here or else we wouldn't notice when the middleware file changes
const middleware = await importFile(pathToFileURL(middleware_path).href);
return middleware.default(req, res, next);
}
};
},

additionalEntryPoints: () => {
if (!middleware_path) return [];

return [
{
name: 'node-middleware',
file: middleware_path,
allowedFeatures: ['$app/server:read']
}
];
},

supports: {
read: () => true
}
Expand Down
5 changes: 5 additions & 0 deletions packages/adapter-node/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ declare module 'MANIFEST' {
declare module 'SERVER' {
export { Server } from '@sveltejs/kit';
}

declare module 'MIDDLEWARE' {
const middleware: import('polka').Middleware;
export default middleware;
}
13 changes: 13 additions & 0 deletions packages/adapter-node/src/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { getRequest, setResponse, createReadableStream } from '@sveltejs/kit/nod
import { Server } from 'SERVER';
import { manifest, prerendered, base } from 'MANIFEST';
import { env } from 'ENV';
import node_middleware from 'MIDDLEWARE';

/* global ENV_PREFIX */

Expand Down Expand Up @@ -51,6 +52,17 @@ await server.init({
read: (file) => createReadableStream(`${asset_dir}/${file}`)
});

/** @type {import('polka').Middleware} */
const middleware = async (req, res, next) => {
const { pathname } = polka_url_parser(req);

if (pathname.startsWith(`/${manifest.appPath}/immutable/`)) {
return next();
}

return node_middleware(req, res, next);
};

/**
* @param {string} path
* @param {boolean} client
Expand Down Expand Up @@ -206,6 +218,7 @@ function get_origin(headers) {

export const handler = sequence(
[
middleware,
serve(path.join(dir, 'client'), true),
serve(path.join(dir, 'static')),
serve_prerendered(),
Expand Down
8 changes: 8 additions & 0 deletions packages/adapter-vercel/files/edge.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ const initialized = server.init({
export default async (request, context) => {
await initialized;

const pathname = request.headers.get('REWRITE_HEADER');
if (pathname) {
let url = new URL(request.url);
url.pathname = pathname;
request = new Request(url, request);
request.headers.delete('x-sveltekit-vercel-rewrite');
}

return server.respond(request, {
getClientAddress() {
return /** @type {string} */ (request.headers.get('x-forwarded-for'));
Expand Down
106 changes: 106 additions & 0 deletions packages/adapter-vercel/files/middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { initServer } from 'SERVER_INIT';
import * as user_middleware from 'MIDDLEWARE';

initServer({
env: {
env: /** @type {Record<string, string>} */ (process.env),
public_prefix: 'PUBLIC_PREFIX',
private_prefix: 'PRIVATE_PREFIX'
}
});

export const config = user_middleware.config;

/**
* @param {Request} request
* @param {any} context
*/
export default async function middleware(request, context) {
const url = new URL(request.url);

const is_route_resolution = has_resolution_suffix(url.pathname);
const is_data_request = has_data_suffix(url.pathname);

if (is_route_resolution) {
url.pathname = strip_resolution_suffix(url.pathname);
} else if (is_data_request) {
url.pathname = strip_data_suffix(url.pathname);
}

if (is_route_resolution || is_data_request) {
request = new Request(url, request);
}

const response = await user_middleware.default(request, context);

if (response instanceof Response && response.headers.has('x-middleware-rewrite')) {
const rewritten = new URL(
/** @type {string} */ (response.headers.get('x-middleware-rewrite')),
url
);

if (rewritten.hostname === url.hostname) {
if (is_route_resolution) {
rewritten.pathname = add_resolution_suffix(rewritten.pathname);
} else if (is_data_request) {
rewritten.pathname = add_data_suffix(rewritten.pathname);
}

response.headers.set('REWRITE_HEADER', rewritten.pathname);
}
}

return response;
}

// the following internal helpers are a copy-paste of kit/src/runtime/pathname.js - should we expose them publicly?

const DATA_SUFFIX = '/__data.json';
const HTML_DATA_SUFFIX = '.html__data.json';

/** @param {string} pathname */
function has_data_suffix(pathname) {
return pathname.endsWith(DATA_SUFFIX) || pathname.endsWith(HTML_DATA_SUFFIX);
}

/** @param {string} pathname */
function add_data_suffix(pathname) {
if (pathname.endsWith('.html')) return pathname.replace(/\.html$/, HTML_DATA_SUFFIX);
return pathname.replace(/\/$/, '') + DATA_SUFFIX;
}

/** @param {string} pathname */
function strip_data_suffix(pathname) {
if (pathname.endsWith(HTML_DATA_SUFFIX)) {
return pathname.slice(0, -HTML_DATA_SUFFIX.length) + '.html';
}

return pathname.slice(0, -DATA_SUFFIX.length);
}

const ROUTE_SUFFIX = '/__route.js';

/**
* @param {string} pathname
* @returns {boolean}
*/
function has_resolution_suffix(pathname) {
return pathname.endsWith(ROUTE_SUFFIX);
}

/**
* Convert a regular URL to a route to send to SvelteKit's server-side route resolution endpoint
* @param {string} pathname
* @returns {string}
*/
function add_resolution_suffix(pathname) {
return pathname.replace(/\/$/, '') + ROUTE_SUFFIX;
}

/**
* @param {string} pathname
* @returns {string}
*/
function strip_resolution_suffix(pathname) {
return pathname.slice(0, -ROUTE_SUFFIX.length);
}
6 changes: 6 additions & 0 deletions packages/adapter-vercel/files/serverless.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ export default async (req, res) => {
// Optional routes' pathname replacements look like `/foo/$1/bar` which means we could end up with an url like /foo//bar
pathname = pathname.replace(/\/+/g, '/');
req.url = `${pathname}${path.endsWith(DATA_SUFFIX) ? DATA_SUFFIX : ''}?${params}`;
} else {
pathname = /** @type {string | null} */ (req.headers['REWRITE_HEADER']);
if (pathname) {
req.url = pathname;
delete req.headers['REWRITE_HEADER'];
}
}
}

Expand Down
Loading
Loading