Skip to content
Draft
Show file tree
Hide file tree
Changes from 42 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
5 changes: 5 additions & 0 deletions .changeset/brave-trains-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/adapter-vercel': minor
---

feat: support edge middleware
5 changes: 5 additions & 0 deletions .changeset/eleven-snakes-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/adapter-node': minor
---

feat: add possibility for adding polka/express middleware
5 changes: 5 additions & 0 deletions .changeset/famous-boats-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: allow adapters to influence compilation entry points and to intercept requests at dev/preview time
5 changes: 5 additions & 0 deletions .changeset/nice-tools-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/adapter-vercel': minor
---

feat: add possibility of defining edge middleware
5 changes: 5 additions & 0 deletions .changeset/tasty-shirts-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/adapter-cloudflare': minor
---

feat: add pages-like middleware
48 changes: 48 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,54 @@ 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. 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
// @filename: ambient.d.ts
declare module 'polka';

// @filename: index.js
// ---cut---

/**
* @param {import('polka').Request} req
* @param {import('polka').Response} res
* @param {import('polka').NextHandler} next
*/
export default function middleware(req, res, next) {
if (req.url !== '/') return next();

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

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

// Get destination URL based on the feature flag
req.url = flag === 'a' ? '/home-a' : '/home-b';

// Set a cookie to remember the feature flags for this visitor
res.appendHeader('Set-Cookie', `flag=${flag}; Path=/`);

return next();
}

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

## Options

The adapter can be configured with various options:
Expand Down
64 changes: 64 additions & 0 deletions documentation/docs/25-build-and-deploy/60-adapter-cloudflare.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,70 @@ Cloudflare Workers specific values in the `platform` property are emulated durin

For testing the build, you should use [Wrangler](https://developers.cloudflare.com/workers/wrangler/) **version 3**. Once you have built your site, run `wrangler pages dev .svelte-kit/cloudflare`.

## Pages Middleware

You can deploy one middleware function that closely follows the [Pages Middleware API](https://developers.cloudflare.com/pages/functions/middleware/). You can use it to intercept requests even for prerendered 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 pages by rerouting a user to either variant A or B depending on a cookie.

> [!NOTE] It isn't really Pages Middleware because the adapter compiles to a [single `_worker.js` file](https://developers.cloudflare.com/pages/platform/functions/#advanced-mode) (also see the [Notes](#Notes) section), which ignores middleware, but it closely mirrors its capabilities.
Copy link
Member

Choose a reason for hiding this comment

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

It isn't really Pages Middleware because the adapter compiles to a single _worker.js file

Pages Middleware and Functions are all compiled to a single _worker.js file too so I think we can omit this statement


To get started, place a `cloudflare-middleware.js` file at the root of your project and export a `onRequest` function from it:

```js
/// file: cloudflare-middleware.js
// @filename: ambient.d.ts
declare module '@cloudflare/workers-types';

// @filename: index.js
// ---cut---
import { normalizeUrl } from '@sveltejs/kit';

/**
* @param {import('@cloudflare/workers-types'.EventContext)} context
*/
export function onRequest({ request, next }) {
const url = new URL(request.url);

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

// Retrieve cookies which contain the feature flags.
let flag = split_cookies(request.headers.get('cookie') ?? '')?.['flags'];

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

// Get destination URL based on the feature flag
request = new Request(new URL(flag === 'a' ? '/home-a' : '/home-b', url), request);

const response = await next(request);

// Set a cookie to remember the feature flags for this visitor
response.headers.set('Set-Cookie', `flags=${flag}; Path=/`);

return response;
}

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

```

The `context` parameter closely follows the [EventContext](https://developers.cloudflare.com/pages/functions/api-reference/#eventcontext) object but is missing some Pages-specific parameters such as `data`, `params` and `functionPath`.

The middleware runs on all requests that your worker is invoked for, which is dependent on the [`include/exlcude` options](#Options-routes).

> [!NOTE] Locally during dev and preview this only approximates the capabilities of middleware. Notably, you cannot read the request or response body, and middleware runs on all requests except those that would end up in `_app/immutable`.
Copy link
Member

Choose a reason for hiding this comment

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

The stuff about _app/immutable doesn't appear to be accurate? It does run on _app/immutable in preview. For dev I realise that it's not running on files that are being transformed by Vite, though IIUC that's not adapter-specific, and I'm not totally sure I understand what we're excluding and why — will try and get my head round this

Suggested change
> [!NOTE] Locally during dev and preview this only approximates the capabilities of middleware. Notably, you cannot read the request or response body, and middleware runs on all requests except those that would end up in `_app/immutable`.
> [!NOTE] During `dev` and `preview` you cannot read the request or response body.

Copy link
Member Author

Choose a reason for hiding this comment

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

For dev I realise that it's not running on files that are being transformed by Vite, though IIUC that's not adapter-specific, and I'm not totally sure I understand what we're excluding and why

It's excluding everything that looks like unbundled files that will not ever be requested like that during production (because it's bundled and the path will be totally different). In other words everything that would end up in _app/immutable.

Copy link
Member

Choose a reason for hiding this comment

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

I guess this pairs with a separate concern I have, which is that someone might expect to be able to do this sort of thing...

const response = await next();

return new Response(response.body!.pipeThrough(transform), response);

...which is currently prohibited in these middlewares. If it wasn't, then I'd expect to be able to transform source code at runtime whether in dev or prod. (I feel like it has to be possible, I have distinct memories of monkey-patching res.write and res.end before calling next() in Express apps of yore.)

Perhaps that's an unreasonable expectation, though if we're not going to expose the full capabilities of the platform then I find myself wondering anew about lowest-common-denominator cross-platform APIs...

Copy link
Member Author

@dummdidumm dummdidumm Mar 3, 2025

Choose a reason for hiding this comment

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

I mean, we are exposing them, but not at dev/preview time.
Is it realistic that someone would do these things in a SvelteKit app. Feels like something you'd do in the handle hook.

So how to proceed here? Investigate if this is possible to replicate? And go with the lowest common denominator after all if not? Or just do that right away? I get the feeling that you're not really happy with this uncanny valley after looking more closely.


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

## Notes

Functions contained in the [`/functions` directory](https://developers.cloudflare.com/pages/functions/routing/) at the project's root will _not_ be included in the deployment. Instead, functions should be implemented as [server endpoints](routing#server) in your SvelteKit app, which is compiled to a [single `_worker.js` file](https://developers.cloudflare.com/pages/functions/advanced-mode/).
Expand Down
42 changes: 42 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 @@ -66,6 +66,48 @@ export default {
};
```

## Edge Middleware

You can deploy one Netlify Edge Function [as middleware](https://docs.netlify.com/edge-functions/api/#modify-a-response) by placing an `edge-middleware.js` file at the root of your project. You can use it to intercept requests even for prerendered 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 pages by rerouting a user to either variant A or B depending on a cookie.

```js
/// file: edge-middleware.js
// @filename: ambient.d.ts
declare module '@netlify/edge-functions';

// @filename: index.js
// ---cut---
import { normalizeUrl } from '@sveltejs/kit';

/**
* @param {Request} request
* @param {import('@netlify/edge-functions').Context} context
*/
export default async function middleware(request, { next, cookies }) {
const url = new URL(request.url);

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

// Retrieve feature flag from cookies
let flag = cookies.get('flag');

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

// Set a cookie to remember the feature flags for this visitor
cookies.set('flag', flag);

// Get destination URL based on the feature flag
return new URL(flag === 'a' ? '/home-a' : '/home-b', url);
}
```

By default middleware runs on all requests except for files within `_app/`. You can customize this by exporting a `export const config = { pattern: '<regex string>' }` object from the file similar to [how you can do it for native edge functions](https://docs.netlify.com/edge-functions/declarations/#declare-edge-functions-inline).

> [!NOTE] Locally during dev and preview this only approximates the capabilities of edge functions. Notably, you cannot read the request or response body, and many properties on the context object are `null`ed.

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

## Netlify alternatives to SvelteKit functionality

You may build your app using functionality provided directly by SvelteKit without relying on any Netlify functionality. Using the SvelteKit versions of these features will allow them to be used in dev mode, tested with integration tests, and to work with other adapters should you ever decide to switch away from Netlify. However, in some scenarios you may find it beneficial to use the Netlify versions of these features. One example would be if you're migrating an app that's already hosted on Netlify to SvelteKit.
Expand Down
62 changes: 59 additions & 3 deletions documentation/docs/25-build-and-deploy/90-adapter-vercel.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export default {

Vercel supports [Incremental Static Regeneration](https://vercel.com/docs/incremental-static-regeneration) (ISR), which provides the performance and cost advantages of prerendered content with the flexibility of dynamically rendered content.

> Use ISR only on routes where every visitor should see the same content (much like when you prerender). If there's anything user-specific happening (like session cookies), they should happen on the client via JavaScript only to not leak sensitive information across visits
> [!NOTE] Use ISR only on routes where every visitor should see the same content (much like when you prerender). If there's anything user-specific happening (like session cookies), they should happen on the client via JavaScript only to not leak sensitive information across visits

To add ISR to a route, include the `isr` property in your `config` object:

Expand All @@ -107,7 +107,7 @@ export const config = {
};
```

> Using ISR on a route with `export const prerender = true` will have no effect, since the route is prerendered at build time
> [!NOTE] Using ISR on a route with `export const prerender = true` will have no effect, since the route is prerendered at build time

The `expiration` property is required; all others are optional. The properties are discussed in more detail below.

Expand Down Expand Up @@ -139,7 +139,63 @@ vercel env pull .env.development.local

A list of valid query parameters that contribute to the cache key. Other parameters (such as utm tracking codes) will be ignored, ensuring that they do not result in content being re-generated unnecessarily. By default, query parameters are ignored.

> Pages that are [prerendered](page-options#prerender) will ignore ISR configuration.
> [!NOTE] 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 an `edge-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: edge-middleware.js
// @filename: ambient.d.ts
declare module '@vercel/edge';

// @filename: index.js
// ---cut---
import { rewrite, next } from '@vercel/edge';

/**
* @param {Request} request
*/
export default async function middleware(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=/`
}
}
);
}

/** @param {string} cookies */
function split_cookies(cookies) {
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).

> [!NOTE] 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

Expand Down
15 changes: 14 additions & 1 deletion documentation/docs/25-build-and-deploy/99-writing-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,18 @@ export default function (options) {
async adapt(builder) {
// adapter implementation
},
async emulate() {
async emulate({ importEntryPoint }) {
return {
async platform({ config, prerender }) {
// the returned object becomes `event.platform` during dev, build and
// preview. Its shape is that of `App.Platform`
},
async interceptRequest(req, res, next) {
// Allows you to run code before a request to a prerendered page, a static asset,
// or a regular request to the SvelteKit runtime, both in dev and preview mode.
// Allows you to for example replicate middleware during dev and preview.
const module = await importEntryPoint('additional-entry-point');
module.default(req, res, next);
}
}
},
Expand All @@ -35,6 +42,12 @@ export default function (options) {
// from `$app/server` in production, return `false` if it can't.
// Or throw a descriptive error describing how to configure the deployment
}
},
// Allows you to configure additional entry points for compilation.
// You can use these via `importEntryPoint` within `emulate` and reference them
// from `${builder.getServerDirectory()}/adapter/<name>.js` for further compilation/bundling.
additionalEntryPoints: {
'additional-entry-point': 'my-project-root-relative-file.js'
}
};

Expand Down
Loading
Loading