From aacd836957463498cdd4f8ab7fa11d61bb71664a Mon Sep 17 00:00:00 2001 From: Ali Smesseim Date: Fri, 30 Oct 2026 10:52:24 +0100 Subject: [PATCH] fix: support Node.js 26 in adapter-vercel (2.x) Backport of #17570 to version-2. Accept any even Node.js major >= 20 (`/^nodejs[2-9][02468]\.x$/`) instead of a hard-coded list, so Node 26 builds resolve to `nodejs26.x` instead of throwing `Unsupported Node.js version`. (cherry picked from commit 1120bd66f3c2eaf37a0e08831695eda607177a6a) Co-Authored-By: Claude Opus 5.5 --- .changeset/quiet-nodes-twenty-six.md | 5 ++ .../25-build-and-deploy/90-adapter-vercel.md | 2 +- packages/adapter-vercel/index.d.ts | 4 +- packages/adapter-vercel/test/utils.spec.js | 67 ++++++++++++++++++- packages/adapter-vercel/utils.js | 30 ++++----- 5 files changed, 88 insertions(+), 20 deletions(-) create mode 100644 .changeset/quiet-nodes-twenty-six.md diff --git a/.changeset/quiet-nodes-twenty-six.md b/.changeset/quiet-nodes-twenty-six.md new file mode 100644 index 000000000000..b8515bff3f6d --- /dev/null +++ b/.changeset/quiet-nodes-twenty-six.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-vercel': patch +--- + +fix: support Node.js 26 (`nodejs26.x`) and future even Node.js versions diff --git a/documentation/docs/25-build-and-deploy/90-adapter-vercel.md b/documentation/docs/25-build-and-deploy/90-adapter-vercel.md index 50f25f4a9299..1599cc73707d 100644 --- a/documentation/docs/25-build-and-deploy/90-adapter-vercel.md +++ b/documentation/docs/25-build-and-deploy/90-adapter-vercel.md @@ -42,7 +42,7 @@ export const config = { The following options apply to all functions: -- `runtime`: `'edge'`, `'nodejs20.x'` or `'nodejs22.x'`. By default, the adapter will select the `'nodejs.x'` corresponding to the Node version your project is configured to use on the Vercel dashboard +- `runtime`: `'edge'`, or an even Node version (for example `'nodejs20.x'`, `'nodejs22.x'`, `'nodejs24.x'`, `'nodejs26.x'`). By default, the adapter will select the `'nodejs.x'` corresponding to the Node version your project is configured to use on the Vercel dashboard > [!NOTE] This option is deprecated and will be removed in a future version, at which point all your functions will use whichever Node version is specified in the project configuration on Vercel - `regions`: an array of [edge network regions](https://vercel.com/docs/concepts/edge-network/regions) (defaulting to `["iad1"]` for serverless functions) or `'all'` if `runtime` is `edge` (its default). Note that multiple regions for serverless functions are only supported on Enterprise plans - `split`: if `true`, causes a route to be deployed as an individual function. If `split` is set to `true` at the adapter level, all routes will be deployed as individual functions diff --git a/packages/adapter-vercel/index.d.ts b/packages/adapter-vercel/index.d.ts index e3333d04a88e..56b129a6fcd2 100644 --- a/packages/adapter-vercel/index.d.ts +++ b/packages/adapter-vercel/index.d.ts @@ -6,7 +6,7 @@ export default function plugin(config?: Config): Adapter; export interface ServerlessConfig { /** - * Whether to use [Edge Functions](https://vercel.com/docs/concepts/functions/edge-functions) (`'edge'`) or [Serverless Functions](https://vercel.com/docs/concepts/functions/serverless-functions) (`'nodejs22.x'`, `'nodejs24.x'` etc). + * Whether to use [Edge Functions](https://vercel.com/docs/concepts/functions/edge-functions) (`'edge'`) or [Serverless Functions](https://vercel.com/docs/concepts/functions/serverless-functions) (`'nodejs22.x'`, `'nodejs24.x'`, `'nodejs26.x'` etc). * @default Same as the build environment */ runtime?: Exclude; @@ -78,7 +78,7 @@ type ImagesConfig = { /** @deprecated */ export interface EdgeConfig { /** - * Whether to use [Edge Functions](https://vercel.com/docs/concepts/functions/edge-functions) (`'edge'`) or [Serverless Functions](https://vercel.com/docs/concepts/functions/serverless-functions) (`'nodejs22.x'`, `'nodejs24.x'` etc). + * Whether to use [Edge Functions](https://vercel.com/docs/concepts/functions/edge-functions) (`'edge'`) or [Serverless Functions](https://vercel.com/docs/concepts/functions/serverless-functions) (`'nodejs22.x'`, `'nodejs24.x'`, `'nodejs26.x'` etc). */ runtime?: 'edge'; /** diff --git a/packages/adapter-vercel/test/utils.spec.js b/packages/adapter-vercel/test/utils.spec.js index 2ee4ca2030c0..0bfbf1724050 100644 --- a/packages/adapter-vercel/test/utils.spec.js +++ b/packages/adapter-vercel/test/utils.spec.js @@ -1,3 +1,4 @@ +import process from 'node:process'; import { assert, test, describe } from 'vitest'; import { get_pathname, parse_isr_expiration, pattern_to_src, resolve_runtime } from '../utils.js'; @@ -185,7 +186,69 @@ describe('resolve_runtime', () => { assert.equal(result, 'bun1.x'); }); - test('throws an error when resolving to an invalid runtime', () => { - assert.throws(() => resolve_runtime('node18.x', undefined), /Unsupported runtime: node18.x/); + test.each(['node18.x', 'nodejs028.x', 'nodejs18.x', 'nodejs33.x', 'nodejs100.x', 'nodejs2,.x'])( + 'throws an error when resolving to an invalid runtime (%s)', + (runtime) => { + assert.throws(() => resolve_runtime(runtime), /Unsupported runtime/); + } + ); + + test.each(['nodejs20.x', 'nodejs22.x', 'nodejs24.x', 'nodejs26.x', 'nodejs96.x', 'nodejs98.x'])( + 'accepts %s', + (runtime) => { + assert.equal(resolve_runtime(runtime), runtime); + } + ); + + test.each(['bun1.x', 'edge'])('accepts %s', (runtime) => { + assert.equal(resolve_runtime(runtime), runtime); }); }); + +describe('resolve_runtime with node detected', () => { + /** + * @param {string} node_version + * @param {() => void} fn + */ + function with_node(node_version, fn) { + const version = Object.getOwnPropertyDescriptor(process, 'version'); + Object.defineProperty(process, 'version', { value: `v${node_version}`, configurable: true }); + try { + fn(); + } finally { + if (version) Object.defineProperty(process, 'version', version); + } + } + + test.each([ + ['20.0.0', 'nodejs20.x'], + ['22.0.0', 'nodejs22.x'], + ['24.1.0', 'nodejs24.x'], + ['26.0.0', 'nodejs26.x'], + ['28.0.0', 'nodejs28.x'], + ['96.1.0', 'nodejs96.x'], + ['98.0.0', 'nodejs98.x'] + ])('detects Node %s', (version, runtime) => { + with_node(version, () => { + assert.equal(resolve_runtime(undefined, undefined), runtime); + }); + }); + + test('an explicit nodejs26.x override wins over the detected version', () => { + with_node('24.0.0', () => { + assert.equal(resolve_runtime(undefined, 'nodejs26.x'), 'nodejs26.x'); + }); + }); + + test.each(['18.0.0', '25.0.0', '27.0.0'])( + 'throws for an unsupported Node major (%s)', + (version) => { + with_node(version, () => { + assert.throws( + () => resolve_runtime(undefined, undefined), + /Unsupported Node\.js version.*Please use an even Node version to build your project/ + ); + }); + } + ); +}); diff --git a/packages/adapter-vercel/utils.js b/packages/adapter-vercel/utils.js index d35ceea65198..abb2c76c9097 100644 --- a/packages/adapter-vercel/utils.js +++ b/packages/adapter-vercel/utils.js @@ -118,8 +118,7 @@ export function resolve_runtime(default_key, override_key) { return key; } -const valid_node_versions = [20, 22, 24]; -const formatter = new Intl.ListFormat('en', { type: 'disjunction' }); +const valid_node_version_regex = /^nodejs[2-9][02468]\.x$/; /** @returns {RuntimeKey} */ function get_default_runtime() { @@ -127,35 +126,36 @@ function get_default_runtime() { // with Bun but not have your serverless runtime be in Bun. Vercel will likely have to attach something to `globalThis` or similar // to tell us what the bun configuration is. const major = Number(process.version.slice(1).split('.')[0]); + const version_key = /** @type {const} */ (`nodejs${major}.x`); - if (!valid_node_versions.includes(major)) { + if (!valid_node_version_regex.test(version_key)) { throw new Error( - `Unsupported Node.js version: ${process.version}. Please use Node ${formatter.format(valid_node_versions.map((v) => `${v}`))} to build your project, or explicitly specify a runtime in your adapter configuration.` + `Unsupported Node.js version: ${process.version}. Please use an even Node version to build your project, or explicitly specify a runtime in your adapter configuration.` ); } - return `nodejs${/** @type {20 | 22 | 24} */ (major)}.x`; + return version_key; } -const valid_runtimes = /** @type {const} */ ([ - 'nodejs20.x', - 'nodejs22.x', - 'nodejs24.x', - 'bun1.x', - 'edge' -]); +const valid_non_node_runtimes = /** @type {const} */ (['bun1.x', 'edge']); /** * @param {string} key * @returns {asserts key is RuntimeKey} */ function assert_is_valid_runtime(key) { - if (!valid_runtimes.includes(/** @type {RuntimeKey} */ (key))) { + if ( + !valid_non_node_runtimes.includes( + /** @type {typeof valid_non_node_runtimes[number]} */ (key) + ) && + !valid_node_version_regex.test(key) + ) { throw new Error( - `Unsupported runtime: ${key}. Supported runtimes are: ${valid_runtimes.join(', ')}. See the Node.js Version section in your Vercel project settings for info on the currently supported versions.` + `Unsupported runtime: ${key}. Supported runtimes are: ${valid_non_node_runtimes.join(', ')}, and even Node versions. See the Node.js Version section in your Vercel project settings for info on the currently supported versions.` ); } } /** @typedef {Exclude | 'experimental_bun1.x'} RuntimeConfigKey */ -/** @typedef {typeof valid_runtimes[number]} RuntimeKey */ +// this is what to update when Vercel adds a new Node runtime or deprecates an old one! leave the rest of the code alone +/** @typedef {typeof valid_non_node_runtimes[number] | 'nodejs20.x' | 'nodejs22.x' | 'nodejs24.x' | 'nodejs26.x' | ({} & `nodejs${number}.x`)} RuntimeKey */