From 35e87625bae007a5f29bd97a3ea6c7d382bd9636 Mon Sep 17 00:00:00 2001 From: Philippe Serhal Date: Sat, 24 Jan 2026 20:20:57 -0500 Subject: [PATCH 1/9] refactor!: migrate to the modern Netlify Functions API The Netlify adapter now generates "v2" Netlify Functions, which uses modern standards (ESM, Request, Response) instead of the legacy "Lambda-compatible" or "v1" format. Under the hood, this greatly simplifies the adapter code and improves maintainability. See more details in this guide: https://developers.netlify.com/guides/migrating-to-the-modern-netlify-functions/. Some key changes: - The use of Request/Resonse eliminated the need for special header parsing, special multi-value handling header, base64 encoding of binary responses, and more. - Configuration of function routing directly in code eliminated the need for `_redirects` file generation. - As a bonus, this removes the limitation that users cannot specify redirects in `netlify.toml`. BREAKING CHANGES: - `platform.context` type changed to https://docs.netlify.com/build/functions/api/#netlify-specific-context-object --- .changeset/tender-kings-knock.md | 17 +++++ .../25-build-and-deploy/80-adapter-netlify.md | 9 +-- packages/adapter-netlify/index.js | 38 +++------- packages/adapter-netlify/package.json | 4 +- packages/adapter-netlify/src/headers.js | 31 --------- packages/adapter-netlify/src/headers.spec.js | 51 -------------- packages/adapter-netlify/src/serverless.js | 69 ++----------------- .../apps/basic/netlify/functions/render.mjs | 39 +---------- pnpm-lock.yaml | 6 -- 9 files changed, 34 insertions(+), 230 deletions(-) create mode 100644 .changeset/tender-kings-knock.md delete mode 100644 packages/adapter-netlify/src/headers.js delete mode 100644 packages/adapter-netlify/src/headers.spec.js diff --git a/.changeset/tender-kings-knock.md b/.changeset/tender-kings-knock.md new file mode 100644 index 000000000000..839344ac91e8 --- /dev/null +++ b/.changeset/tender-kings-knock.md @@ -0,0 +1,17 @@ +--- +'@sveltejs/adapter-netlify': minor +--- + +chore!: Migrate to the modern Netlify Functions API + +The Netlify adapter now generates "v2" Netlify Functions, which uses modern standards (ESM, Request, +Response) instead of the legacy "Lambda-compatible" or "v1" format. Under the hood, this greatly +simplifies the adapter code and improves maintainability. + +For more details on features this unlocks for your SvelteKit app, see +https://developers.netlify.com/guides/migrating-to-the-modern-netlify-functions/. + +BREAKING CHANGES: + +- `platform.context` is now the [modern Netlify Functions + context](https://docs.netlify.com/build/functions/api/#netlify-specific-context-object) diff --git a/documentation/docs/25-build-and-deploy/80-adapter-netlify.md b/documentation/docs/25-build-and-deploy/80-adapter-netlify.md index 78bdeaefa1fb..4e44a34ef100 100644 --- a/documentation/docs/25-build-and-deploy/80-adapter-netlify.md +++ b/documentation/docs/25-build-and-deploy/80-adapter-netlify.md @@ -76,14 +76,7 @@ You may build your app using functionality provided directly by SvelteKit withou ### `_headers` and `_redirects` -The [`_headers`](https://docs.netlify.com/routing/headers/#syntax-for-the-headers-file) and [`_redirects`](https://docs.netlify.com/routing/redirects/redirect-options/) files specific to Netlify can be used for static asset responses (like images) by putting them into the project root folder. - -### Redirect rules - -During compilation, redirect rules are automatically appended to your `_redirects` file. (If it doesn't exist yet, it will be created.) That means: - -- `[[redirects]]` in `netlify.toml` will never match as `_redirects` has a [higher priority](https://docs.netlify.com/routing/redirects/#rule-processing-order). So always put your rules in the [`_redirects` file](https://docs.netlify.com/routing/redirects/#syntax-for-the-redirects-file). -- `_redirects` shouldn't have any custom "catch all" rules such as `/* /foobar/:splat`. Otherwise the automatically appended rule will never be applied as Netlify is only processing [the first matching rule](https://docs.netlify.com/routing/redirects/#rule-processing-order). +The [`_headers`](https://docs.netlify.com/routing/headers/#syntax-for-the-headers-file) and [`_redirects`](https://docs.netlify.com/routing/redirects/redirect-options/) files specific to Netlify can be used for static asset responses (like images) by putting them into the project root folder. You can also use [`[[redirects]]`](https://docs.netlify.com/routing/redirects/#syntax-for-the-netlify-configuration-file) in your `netlify.toml`. ### Netlify Forms diff --git a/packages/adapter-netlify/index.js b/packages/adapter-netlify/index.js index 955a323f5c21..eaa8b47c8702 100644 --- a/packages/adapter-netlify/index.js +++ b/packages/adapter-netlify/index.js @@ -1,6 +1,6 @@ /** @import { BuildOptions } from 'esbuild' */ import { appendFileSync, existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'; -import { dirname, join, resolve, posix } from 'node:path'; +import { join, resolve, posix } from 'node:path'; import { fileURLToPath } from 'node:url'; import { builtinModules } from 'node:module'; import process from 'node:process'; @@ -241,8 +241,6 @@ async function generate_edge_functions({ builder }) { function generate_lambda_functions({ builder, publish, split }) { builder.mkdirp('.netlify/functions-internal/.svelte-kit'); - /** @type {string[]} */ - const redirects = []; builder.writeServer('.netlify/server'); const replace = { @@ -251,9 +249,6 @@ function generate_lambda_functions({ builder, publish, split }) { builder.copy(files, '.netlify', { replace, filter: (name) => !name.endsWith('edge.js') }); - // Configuring the function to use ESM as the output format. - const fn_config = JSON.stringify({ config: { nodeModuleFormat: 'esm' }, version: 1 }); - builder.log.minor('Generating serverless functions...'); if (split) { @@ -302,33 +297,27 @@ function generate_lambda_functions({ builder, publish, split }) { routes }); - const fn = `import { init } from '../serverless.js';\n\nexport const handler = init(${manifest});\n`; + const fn = `import { init } from '../serverless.js';\n\nexport default init(${manifest});\n\nexport const config = {\n\tpath: "${pattern}",\n\tpreferStatic: true\n};\n`; writeFileSync(`.netlify/functions-internal/${name}.mjs`, fn); - writeFileSync(`.netlify/functions-internal/${name}.json`, fn_config); if (builder.hasServerInstrumentationFile?.()) { builder.instrument?.({ entrypoint: `.netlify/functions-internal/${name}.mjs`, instrumentation: '.netlify/server/instrumentation.server.js', start: `.netlify/functions-start/${name}.start.mjs`, module: { - exports: ['handler'] + exports: ['default'] } }); } - - const redirect = `/.netlify/functions/${name} 200`; - redirects.push(`${pattern} ${redirect}`); - redirects.push(`${pattern === '/' ? '' : pattern}/__data.json ${redirect}`); } } else { const manifest = builder.generateManifest({ relativePath: '../server' }); - const fn = `import { init } from '../serverless.js';\n\nexport const handler = init(${manifest});\n`; + const fn = `import { init } from '../serverless.js';\n\nexport default init(${manifest});\n\nexport const config = {\n\tpath: "/*",\n\tpreferStatic: true\n};\n`; - writeFileSync(`.netlify/functions-internal/${FUNCTION_PREFIX}render.json`, fn_config); writeFileSync(`.netlify/functions-internal/${FUNCTION_PREFIX}render.mjs`, fn); if (builder.hasServerInstrumentationFile?.()) { builder.instrument?.({ @@ -336,24 +325,18 @@ function generate_lambda_functions({ builder, publish, split }) { instrumentation: '.netlify/server/instrumentation.server.js', start: `.netlify/functions-start/${FUNCTION_PREFIX}render.start.mjs`, module: { - exports: ['handler'] + exports: ['default'] } }); } - - redirects.push(`* /.netlify/functions/${FUNCTION_PREFIX}render 200`); } - // this should happen at the end, after builder.writeClient(...), - // so that generated redirects are appended to custom redirects - // rather than replaced by them - builder.log.minor('Writing redirects...'); - const redirects_file = join(publish, '_redirects'); + // Copy user's custom _redirects file if it exists if (existsSync('_redirects')) { + builder.log.minor('Copying user redirects...'); + const redirects_file = join(publish, '_redirects'); builder.copy('_redirects', redirects_file); } - builder.mkdirp(dirname(redirects_file)); - appendFileSync(redirects_file, `\n\n${redirects.join('\n')}`); } function get_netlify_config() { @@ -378,11 +361,6 @@ function get_publish_directory(netlify_config, builder) { return; } - if (netlify_config.redirects) { - throw new Error( - "Redirects are not supported in netlify.toml. Use _redirects instead. For more details consult the readme's troubleshooting section." - ); - } if (resolve(netlify_config.build.publish) === process.cwd()) { throw new Error( 'The publish directory cannot be set to the site root. Please change it to another value such as "build" in netlify.toml.' diff --git a/packages/adapter-netlify/package.json b/packages/adapter-netlify/package.json index 628ebf689fcb..ca8f37e36ede 100644 --- a/packages/adapter-netlify/package.json +++ b/packages/adapter-netlify/package.json @@ -44,8 +44,7 @@ }, "dependencies": { "@iarna/toml": "^2.2.5", - "esbuild": "^0.25.4", - "set-cookie-parser": "^2.6.0" + "esbuild": "^0.25.4" }, "devDependencies": { "@netlify/dev": "catalog:", @@ -59,7 +58,6 @@ "@sveltejs/kit": "workspace:^", "@sveltejs/vite-plugin-svelte": "catalog:", "@types/node": "catalog:", - "@types/set-cookie-parser": "catalog:", "rollup": "^4.14.2", "typescript": "^5.3.3", "vitest": "catalog:" diff --git a/packages/adapter-netlify/src/headers.js b/packages/adapter-netlify/src/headers.js deleted file mode 100644 index 8fee7ddb38eb..000000000000 --- a/packages/adapter-netlify/src/headers.js +++ /dev/null @@ -1,31 +0,0 @@ -import * as set_cookie_parser from 'set-cookie-parser'; - -/** - * Splits headers into two categories: single value and multi value - * @param {Headers} headers - * @returns {{ - * headers: Record, - * multiValueHeaders: Record - * }} - */ -export function split_headers(headers) { - /** @type {Record} */ - const h = {}; - - /** @type {Record} */ - const m = {}; - - headers.forEach((value, key) => { - if (key === 'set-cookie') { - if (!m[key]) m[key] = []; - m[key].push(...set_cookie_parser.splitCookiesString(value)); - } else { - h[key] = value; - } - }); - - return { - headers: h, - multiValueHeaders: m - }; -} diff --git a/packages/adapter-netlify/src/headers.spec.js b/packages/adapter-netlify/src/headers.spec.js deleted file mode 100644 index 352987f3bdc5..000000000000 --- a/packages/adapter-netlify/src/headers.spec.js +++ /dev/null @@ -1,51 +0,0 @@ -import { expect, test } from 'vitest'; -import './shims.js'; -import { split_headers } from './headers.js'; - -test('empty headers', () => { - const headers = new Headers(); - - const result = split_headers(headers); - - expect(result).toEqual({ - headers: {}, - multiValueHeaders: {} - }); -}); - -test('single-value headers', () => { - const headers = new Headers(); - headers.append('Location', '/apple'); - headers.append('Content-Type', 'application/json'); - - const result = split_headers(headers); - - expect(result).toEqual({ - headers: { - // Note: becomes lowercase even if specified as uppercase - location: '/apple', - 'content-type': 'application/json' - }, - multiValueHeaders: {} - }); -}); - -test('multi-value headers', () => { - // https://httpwg.org/specs/rfc7231.html#http.date - const wednesday = 'Wed, 23 Feb 2022 21:01:48 GMT'; - const thursday = 'Thu, 24 Feb 2022 21:01:48 GMT'; - - const headers = new Headers(); - headers.append('Set-Cookie', `flavor=sugar; Expires=${wednesday}`); - headers.append('Set-Cookie', `diameter=6cm; Expires=${thursday}`); - - const result = split_headers(headers); - - // it splits at actual cookie boundaries, not the commas in the dates - expect(result).toEqual({ - headers: {}, - multiValueHeaders: { - 'set-cookie': [`flavor=sugar; Expires=${wednesday}`, `diameter=6cm; Expires=${thursday}`] - } - }); -}); diff --git a/packages/adapter-netlify/src/serverless.js b/packages/adapter-netlify/src/serverless.js index 132de92b3295..c8a6447b6ad9 100644 --- a/packages/adapter-netlify/src/serverless.js +++ b/packages/adapter-netlify/src/serverless.js @@ -1,12 +1,11 @@ import './shims'; import { Server } from '0SERVER'; -import { split_headers } from './headers.js'; import { createReadableStream } from '@sveltejs/kit/node'; import process from 'node:process'; /** * @param {import('@sveltejs/kit').SSRManifest} manifest - * @returns {import('@netlify/functions').Handler} + * @returns {(request: Request, context: import('@netlify/functions').Context) => Promise} */ export function init(manifest) { const server = new Server(manifest); @@ -17,77 +16,17 @@ export function init(manifest) { read: (file) => createReadableStream(`.netlify/server/${file}`) }); - return async (event, context) => { + return async (request, context) => { if (init_promise !== null) { await init_promise; init_promise = null; } - const response = await server.respond(to_request(event), { + return server.respond(request, { platform: { context }, getClientAddress() { - return /** @type {string} */ (event.headers['x-nf-client-connection-ip']); + return context.ip; } }); - - const partial_response = { - statusCode: response.status, - ...split_headers(response.headers) - }; - - if (!is_text(response.headers.get('content-type'))) { - // Function responses should be strings (or undefined), and responses with binary - // content should be base64 encoded and set isBase64Encoded to true. - // https://github.com/netlify/functions/blob/main/src/function/response.ts - return { - ...partial_response, - isBase64Encoded: true, - body: Buffer.from(await response.arrayBuffer()).toString('base64') - }; - } - - return { - ...partial_response, - body: await response.text() - }; - }; -} - -/** - * @param {import('@netlify/functions').HandlerEvent} event - * @returns {Request} - */ -function to_request({ httpMethod, headers, rawUrl, body, isBase64Encoded }) { - /** @type {RequestInit} */ - const init = { - method: httpMethod, - headers: new Headers(/** @type {Record} */ (headers)) }; - - if (httpMethod !== 'GET' && httpMethod !== 'HEAD') { - const encoding = isBase64Encoded ? 'base64' : 'utf-8'; - init.body = typeof body === 'string' ? Buffer.from(body, encoding) : body; - } - - return new Request(rawUrl, init); -} - -const text_types = new Set([ - 'application/xml', - 'application/json', - 'application/x-www-form-urlencoded', - 'multipart/form-data' -]); - -/** - * Decides how the body should be parsed based on its mime type - * - * @param {string | undefined | null} content_type The `content-type` header of a request/response. - * @returns {boolean} - */ -function is_text(content_type) { - if (!content_type) return true; // defaults to json - const type = content_type.split(';')[0].toLowerCase(); // get the mime type - - return type.startsWith('text/') || type.endsWith('+xml') || text_types.has(type); } diff --git a/packages/adapter-netlify/test/apps/basic/netlify/functions/render.mjs b/packages/adapter-netlify/test/apps/basic/netlify/functions/render.mjs index 343db73977d2..9444ce6e2bf6 100644 --- a/packages/adapter-netlify/test/apps/basic/netlify/functions/render.mjs +++ b/packages/adapter-netlify/test/apps/basic/netlify/functions/render.mjs @@ -1,39 +1,6 @@ -// This is a temporary workaround to be compatible with Netlify's new dev server -// TODO: remove this once we overhaul the Netlify adapter to use Netlify's new serverless function format https://docs.netlify.com/build/functions/get-started/?data-tab=TypeScript#write-a-function - -import { handler } from '../../.netlify/functions-internal/sveltekit-render.mjs'; - -/** - * @param {Request} request - * @param {import('@netlify/functions').HandlerContext} context - */ -export default async function (request, context) { - const [rawUrl, rawQuery] = request.url.split('?'); - /** @type {import('@netlify/functions').HandlerEvent} */ - const event = { - rawUrl, - rawQuery: rawQuery || '', - headers: Object.fromEntries(request.headers), - httpMethod: request.method, - isBase64Encoded: false, - path: new URL(request.url).pathname, - queryStringParameters: Object.fromEntries(new URL(request.url).searchParams), - body: request.body && (await request.text()), - multiValueHeaders: {}, - multiValueQueryStringParameters: null - }; - const result = await handler(event, context); - if (result) { - return new Response(result.body, { - status: result.statusCode, - // @ts-ignore - headers: result.headers - }); - } - - return new Response('Not Found', { status: 404 }); -} +export { default } from '../../.netlify/functions-internal/sveltekit-render.mjs'; export const config = { - path: '/*' + path: '/*', + preferStatic: true }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index efa6d0e858f5..1a3a9b5e4945 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -243,9 +243,6 @@ importers: esbuild: specifier: ^0.25.4 version: 0.25.9 - set-cookie-parser: - specifier: ^2.6.0 - version: 2.6.0 devDependencies: '@netlify/dev': specifier: 'catalog:' @@ -280,9 +277,6 @@ importers: '@types/node': specifier: 'catalog:' version: 18.19.119 - '@types/set-cookie-parser': - specifier: 'catalog:' - version: 2.4.7 rollup: specifier: ^4.14.2 version: 4.50.1 From 0d1439981fb66f9cb67956aa2b68897db5b9f5a8 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sun, 25 Jan 2026 21:17:55 +0800 Subject: [PATCH 2/9] there are no unit tests anymore --- packages/adapter-netlify/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/adapter-netlify/package.json b/packages/adapter-netlify/package.json index ca8f37e36ede..d4b58c266ce9 100644 --- a/packages/adapter-netlify/package.json +++ b/packages/adapter-netlify/package.json @@ -37,7 +37,7 @@ "check": "tsc", "lint": "prettier --check .", "format": "pnpm lint --write", - "test": "pnpm test:unit && pnpm test:integration", + "test": "pnpm test:integration", "test:unit": "vitest run", "test:integration": "pnpm build && pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test", "prepublishOnly": "pnpm build" From 324129ff4a33c355f0025f6dcf58696c523f8804 Mon Sep 17 00:00:00 2001 From: Philippe Serhal Date: Sun, 25 Jan 2026 11:18:04 -0500 Subject: [PATCH 3/9] chore: better changesets --- .changeset/petite-cars-dress.md | 11 +++++++++++ .changeset/tender-kings-knock.md | 7 +------ .changeset/weak-dryers-bow.md | 8 ++++++++ 3 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 .changeset/petite-cars-dress.md create mode 100644 .changeset/weak-dryers-bow.md diff --git a/.changeset/petite-cars-dress.md b/.changeset/petite-cars-dress.md new file mode 100644 index 000000000000..994e481ed3cc --- /dev/null +++ b/.changeset/petite-cars-dress.md @@ -0,0 +1,11 @@ +--- +'@sveltejs/adapter-netlify': major +--- + +breaking: `platform.context` is now the [modern Netlify Functions +context](https://docs.netlify.com/build/functions/api/#netlify-specific-context-object) + +Previously, this was the [AWS Lambda-style +context](https://github.com/netlify/primitives/blob/c1ae30f2745f0a73e26e83334695e205a04ab47d/packages/functions/prod/src/function/handler_context.ts). + +If you were using this in your app (unlikely), you will need to update your code to read from new fields. diff --git a/.changeset/tender-kings-knock.md b/.changeset/tender-kings-knock.md index 839344ac91e8..9afd0d6a4f7c 100644 --- a/.changeset/tender-kings-knock.md +++ b/.changeset/tender-kings-knock.md @@ -2,7 +2,7 @@ '@sveltejs/adapter-netlify': minor --- -chore!: Migrate to the modern Netlify Functions API +feat: Migrate to the modern Netlify Functions API The Netlify adapter now generates "v2" Netlify Functions, which uses modern standards (ESM, Request, Response) instead of the legacy "Lambda-compatible" or "v1" format. Under the hood, this greatly @@ -10,8 +10,3 @@ simplifies the adapter code and improves maintainability. For more details on features this unlocks for your SvelteKit app, see https://developers.netlify.com/guides/migrating-to-the-modern-netlify-functions/. - -BREAKING CHANGES: - -- `platform.context` is now the [modern Netlify Functions - context](https://docs.netlify.com/build/functions/api/#netlify-specific-context-object) diff --git a/.changeset/weak-dryers-bow.md b/.changeset/weak-dryers-bow.md new file mode 100644 index 000000000000..a4466b508176 --- /dev/null +++ b/.changeset/weak-dryers-bow.md @@ -0,0 +1,8 @@ +--- +'@sveltejs/adapter-netlify': minor +--- + +feat: allow configuring redirects in `netlify.toml` + +The limitation of only being able to configure redirects via the +`_redirects` file has been removed. From 312d3bf07cc5429535562e7bade29a9454cfd3f1 Mon Sep 17 00:00:00 2001 From: Philippe Serhal Date: Sun, 25 Jan 2026 11:20:20 -0500 Subject: [PATCH 4/9] fix: exclude special .netlify/ path from generated function --- packages/adapter-netlify/index.js | 4 ++-- .../test/apps/basic/netlify/functions/render.mjs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/adapter-netlify/index.js b/packages/adapter-netlify/index.js index eaa8b47c8702..18672f182e9d 100644 --- a/packages/adapter-netlify/index.js +++ b/packages/adapter-netlify/index.js @@ -297,7 +297,7 @@ function generate_lambda_functions({ builder, publish, split }) { routes }); - const fn = `import { init } from '../serverless.js';\n\nexport default init(${manifest});\n\nexport const config = {\n\tpath: "${pattern}",\n\tpreferStatic: true\n};\n`; + const fn = `import { init } from '../serverless.js';\n\nexport default init(${manifest});\n\nexport const config = {\n\tpath: "${pattern}",\n\texcludedPath: "/.netlify/*",\n\tpreferStatic: true\n};\n`; writeFileSync(`.netlify/functions-internal/${name}.mjs`, fn); if (builder.hasServerInstrumentationFile?.()) { @@ -316,7 +316,7 @@ function generate_lambda_functions({ builder, publish, split }) { relativePath: '../server' }); - const fn = `import { init } from '../serverless.js';\n\nexport default init(${manifest});\n\nexport const config = {\n\tpath: "/*",\n\tpreferStatic: true\n};\n`; + const fn = `import { init } from '../serverless.js';\n\nexport default init(${manifest});\n\nexport const config = {\n\tpath: "/*",\n\texcludedPath: "/.netlify/*",\n\tpreferStatic: true\n};\n`; writeFileSync(`.netlify/functions-internal/${FUNCTION_PREFIX}render.mjs`, fn); if (builder.hasServerInstrumentationFile?.()) { diff --git a/packages/adapter-netlify/test/apps/basic/netlify/functions/render.mjs b/packages/adapter-netlify/test/apps/basic/netlify/functions/render.mjs index 9444ce6e2bf6..ae9928a35911 100644 --- a/packages/adapter-netlify/test/apps/basic/netlify/functions/render.mjs +++ b/packages/adapter-netlify/test/apps/basic/netlify/functions/render.mjs @@ -2,5 +2,6 @@ export { default } from '../../.netlify/functions-internal/sveltekit-render.mjs' export const config = { path: '/*', + excludedPath: '/.netlify/*', preferStatic: true }; From 3f8178099cb247914cc2dd069153f62092315852 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Fri, 6 Feb 2026 02:39:30 +0800 Subject: [PATCH 5/9] Apply suggestion from @benmccann Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> --- .changeset/weak-dryers-bow.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.changeset/weak-dryers-bow.md b/.changeset/weak-dryers-bow.md index a4466b508176..dcad45b23f27 100644 --- a/.changeset/weak-dryers-bow.md +++ b/.changeset/weak-dryers-bow.md @@ -4,5 +4,4 @@ feat: allow configuring redirects in `netlify.toml` -The limitation of only being able to configure redirects via the -`_redirects` file has been removed. +The limitation of only being able to configure redirects via the `_redirects` file has been removed. From 600efd11afebe695df7197e2b2abeba1d385ca8e Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Fri, 6 Feb 2026 02:39:45 +0800 Subject: [PATCH 6/9] Apply suggestion from @benmccann Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> --- .changeset/tender-kings-knock.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.changeset/tender-kings-knock.md b/.changeset/tender-kings-knock.md index 9afd0d6a4f7c..21e428afdd30 100644 --- a/.changeset/tender-kings-knock.md +++ b/.changeset/tender-kings-knock.md @@ -4,9 +4,7 @@ feat: Migrate to the modern Netlify Functions API -The Netlify adapter now generates "v2" Netlify Functions, which uses modern standards (ESM, Request, -Response) instead of the legacy "Lambda-compatible" or "v1" format. Under the hood, this greatly -simplifies the adapter code and improves maintainability. +The Netlify adapter now generates "v2" Netlify Functions, which uses modern standards (ESM, `Request`, `Response`) instead of the legacy "Lambda-compatible" or "v1" format. Under the hood, this greatly simplifies the adapter code and improves maintainability. For more details on features this unlocks for your SvelteKit app, see https://developers.netlify.com/guides/migrating-to-the-modern-netlify-functions/. From 4d06096706083923acadee518070b424729c29ca Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Fri, 6 Feb 2026 02:54:08 +0800 Subject: [PATCH 7/9] add ambient types --- .changeset/great-parents-share.md | 5 +++++ packages/adapter-netlify/ambient.d.ts | 9 +++++++++ packages/adapter-netlify/index.d.ts | 1 + packages/adapter-netlify/package.json | 3 ++- 4 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 .changeset/great-parents-share.md create mode 100644 packages/adapter-netlify/ambient.d.ts diff --git a/.changeset/great-parents-share.md b/.changeset/great-parents-share.md new file mode 100644 index 000000000000..0b196723c26c --- /dev/null +++ b/.changeset/great-parents-share.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-netlify': patch +--- + +fix: populate `App.Platform` with `context` property diff --git a/packages/adapter-netlify/ambient.d.ts b/packages/adapter-netlify/ambient.d.ts new file mode 100644 index 000000000000..a776cd3568f3 --- /dev/null +++ b/packages/adapter-netlify/ambient.d.ts @@ -0,0 +1,9 @@ +declare global { + namespace App { + export interface Platform { + context: import('@netlify/functions').Context; + } + } +} + +export {}; diff --git a/packages/adapter-netlify/index.d.ts b/packages/adapter-netlify/index.d.ts index 6bdc8db882dc..580b30cb1060 100644 --- a/packages/adapter-netlify/index.d.ts +++ b/packages/adapter-netlify/index.d.ts @@ -1,3 +1,4 @@ import { Adapter } from '@sveltejs/kit'; +import './ambient.d.ts'; export default function plugin(opts?: { split?: boolean; edge?: boolean }): Adapter; diff --git a/packages/adapter-netlify/package.json b/packages/adapter-netlify/package.json index d4b58c266ce9..953aef668c7a 100644 --- a/packages/adapter-netlify/package.json +++ b/packages/adapter-netlify/package.json @@ -29,7 +29,8 @@ "files": [ "files", "index.js", - "index.d.ts" + "index.d.ts", + "ambient.d.ts" ], "scripts": { "dev": "rollup -cw", From ed8bc9d70f417c20834dbf55318943cc60c66032 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Fri, 6 Feb 2026 03:03:30 +0800 Subject: [PATCH 8/9] bump peer version of kit --- packages/adapter-netlify/index.js | 33 ++++----------------------- packages/adapter-netlify/package.json | 2 +- 2 files changed, 6 insertions(+), 29 deletions(-) diff --git a/packages/adapter-netlify/index.js b/packages/adapter-netlify/index.js index 18672f182e9d..2f51e130c860 100644 --- a/packages/adapter-netlify/index.js +++ b/packages/adapter-netlify/index.js @@ -6,9 +6,6 @@ import { builtinModules } from 'node:module'; import process from 'node:process'; import esbuild from 'esbuild'; import toml from '@iarna/toml'; -import { VERSION } from '@sveltejs/kit'; - -const [kit_major, kit_minor] = VERSION.split('.'); /** * @typedef {{ @@ -17,17 +14,6 @@ const [kit_major, kit_minor] = VERSION.split('.'); * } & toml.JsonMap} NetlifyConfig */ -/** - * @template T - * @template {keyof T} K - * @typedef {Partial> & Required>} PartialExcept - */ - -/** - * We use a custom `Builder` type here to support the minimum version of SvelteKit. - * @typedef {PartialExcept} Builder2_4_0 - */ - const name = '@sveltejs/adapter-netlify'; const files = fileURLToPath(new URL('./files', import.meta.url).href); @@ -41,7 +27,7 @@ const FUNCTION_PREFIX = 'sveltekit-'; export default function ({ split = false, edge = edge_set_in_env_var } = {}) { return { name, - /** @param {Builder2_4_0} builder */ + /** @param {import('@sveltejs/kit').Builder} builder */ async adapt(builder) { if (!builder.routes) { throw new Error( @@ -109,23 +95,14 @@ export default function ({ split = false, edge = edge_set_in_env_var } = {}) { }, supports: { - read: ({ route }) => { - // TODO bump peer dep in next adapter major to simplify this - if (edge && kit_major === '2' && kit_minor < '25') { - throw new Error( - `${name}: Cannot use \`read\` from \`$app/server\` in route \`${route.id}\` when using edge functions and SvelteKit < 2.25.0` - ); - } - - return true; - }, + read: () => true, instrumentation: () => true } }; } /** * @param { object } params - * @param {Builder2_4_0} params.builder + * @param {import('@sveltejs/kit').Builder} params.builder */ async function generate_edge_functions({ builder }) { const tmp = builder.getBuildDirectory('netlify-tmp'); @@ -234,7 +211,7 @@ async function generate_edge_functions({ builder }) { } /** * @param { object } params - * @param {Builder2_4_0} params.builder + * @param {import('@sveltejs/kit').Builder} params.builder * @param { string } params.publish * @param { boolean } params.split */ @@ -352,7 +329,7 @@ function get_netlify_config() { /** * @param {NetlifyConfig | null} netlify_config - * @param {Builder2_4_0} builder + * @param {import('@sveltejs/kit').Builder} builder **/ function get_publish_directory(netlify_config, builder) { if (netlify_config) { diff --git a/packages/adapter-netlify/package.json b/packages/adapter-netlify/package.json index 953aef668c7a..078b97c1f6fe 100644 --- a/packages/adapter-netlify/package.json +++ b/packages/adapter-netlify/package.json @@ -64,6 +64,6 @@ "vitest": "catalog:" }, "peerDependencies": { - "@sveltejs/kit": "^2.4.0" + "@sveltejs/kit": "^2.31.0" } } From 01184a9c6260c3bc6a403006cf059a2663f79c2f Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Fri, 6 Feb 2026 03:03:40 +0800 Subject: [PATCH 9/9] chores --- packages/adapter-netlify/test/apps/basic/netlify.toml | 2 +- packages/adapter-netlify/test/apps/basic/vite.config.ts | 4 ++-- packages/adapter-netlify/test/apps/edge/netlify.toml | 3 +-- packages/adapter-netlify/test/apps/edge/vite.config.ts | 4 ++-- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/adapter-netlify/test/apps/basic/netlify.toml b/packages/adapter-netlify/test/apps/basic/netlify.toml index 75cbe2e5f61e..0183026741cd 100644 --- a/packages/adapter-netlify/test/apps/basic/netlify.toml +++ b/packages/adapter-netlify/test/apps/basic/netlify.toml @@ -1,2 +1,2 @@ -[dev] +[build] publish = "build" diff --git a/packages/adapter-netlify/test/apps/basic/vite.config.ts b/packages/adapter-netlify/test/apps/basic/vite.config.ts index 29ad08debe6a..72a307cc0e56 100644 --- a/packages/adapter-netlify/test/apps/basic/vite.config.ts +++ b/packages/adapter-netlify/test/apps/basic/vite.config.ts @@ -1,7 +1,7 @@ import { sveltekit } from '@sveltejs/kit/vite'; +import type { UserConfig } from 'vite'; -/** @type {import('vite').UserConfig} */ -const config = { +const config: UserConfig = { build: { minify: false }, diff --git a/packages/adapter-netlify/test/apps/edge/netlify.toml b/packages/adapter-netlify/test/apps/edge/netlify.toml index 0d9af7524b9d..fba5510330d6 100644 --- a/packages/adapter-netlify/test/apps/edge/netlify.toml +++ b/packages/adapter-netlify/test/apps/edge/netlify.toml @@ -1,9 +1,8 @@ -[dev] +[build] publish = "build" # TODO: remove these once we overhaul the Netlify adapter to use the new edge declarations https://docs.netlify.com/build/edge-functions/declarations/#declare-edge-functions-inline -[build] # defaults to "netlify/edge-functions" (without the . prefix) edge_functions = ".netlify/edge-functions" diff --git a/packages/adapter-netlify/test/apps/edge/vite.config.ts b/packages/adapter-netlify/test/apps/edge/vite.config.ts index 29ad08debe6a..72a307cc0e56 100644 --- a/packages/adapter-netlify/test/apps/edge/vite.config.ts +++ b/packages/adapter-netlify/test/apps/edge/vite.config.ts @@ -1,7 +1,7 @@ import { sveltekit } from '@sveltejs/kit/vite'; +import type { UserConfig } from 'vite'; -/** @type {import('vite').UserConfig} */ -const config = { +const config: UserConfig = { build: { minify: false },