Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/quiet-nodes-twenty-six.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/adapter-vercel': patch
---

fix: support Node.js 26 (`nodejs26.x`) and future even Node.js versions
Original file line number Diff line number Diff line change
Expand Up @@ -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<version>.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<version>.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
Expand Down
4 changes: 2 additions & 2 deletions packages/adapter-vercel/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<RuntimeConfigKey, 'edge'>;
Expand Down Expand Up @@ -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';
/**
Expand Down
67 changes: 65 additions & 2 deletions packages/adapter-vercel/test/utils.spec.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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/
);
});
}
);
});
30 changes: 15 additions & 15 deletions packages/adapter-vercel/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,44 +118,44 @@ 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() {
// TODO may someday need to auto-detect Bun, but this will be complicated because you may want to run your build
// 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<RuntimeKey, 'bun1.x'> | '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 */
Loading