From 0613905648a9b8e8def3a55dafe19f15a2f87ebb Mon Sep 17 00:00:00 2001 From: Ali Smesseim Date: Fri, 30 Oct 2026 10:57:05 +0100 Subject: [PATCH 1/3] fix(vercel): support Node.js 26 and newer even versions The preset hard-coded Node.js 22 and 24, so a Node 26 build (or `engines.node: 26.x`) deployed on nodejs24.x. Derive the supported versions from the even majors from 22 up to the newest known one or the build's own major, so future even versions need no change. Co-Authored-By: Claude Opus 5.5 --- docs/2.deploy/20.providers/vercel.md | 2 ++ src/presets/vercel/types.ts | 2 +- src/presets/vercel/utils.ts | 32 ++++++++++++++----- test/unit/vercel-runtime.test.ts | 48 ++++++++++++++++++++++++++-- 4 files changed, 72 insertions(+), 12 deletions(-) diff --git a/docs/2.deploy/20.providers/vercel.md b/docs/2.deploy/20.providers/vercel.md index a85127ba07..391cf5d945 100644 --- a/docs/2.deploy/20.providers/vercel.md +++ b/docs/2.deploy/20.providers/vercel.md @@ -40,6 +40,8 @@ Nitro checks these sources, in order, to pick the runtime of the server function 3. `bunVersion` in `vercel.json`. 4. The runtime running the build: Bun when building with Bun, otherwise the build's Node.js major version, clamped to the versions Vercel supports. +Vercel supports the even (LTS) Node.js versions from 22. A build on an even version from 22 uses it for the function, including versions newer than `26`. A build on an odd version uses the next even version, or the previous one if there is no newer known version. + Steps 2 and 3 follow the same precedence as Vercel. ```json [package.json] diff --git a/src/presets/vercel/types.ts b/src/presets/vercel/types.ts index d0c24749ad..2a5fcc582c 100644 --- a/src/presets/vercel/types.ts +++ b/src/presets/vercel/types.ts @@ -106,7 +106,7 @@ export interface VercelServerlessFunctionConfig { /** * The runtime to use. Defaults to `engines` in `package.json`, then `bunVersion` in `vercel.json`, then the runtime used to build. */ - runtime?: "nodejs22.x" | "nodejs24.x" | "bun1.x" | "bun1.4.x" | (string & {}); + runtime?: "nodejs22.x" | "nodejs24.x" | "nodejs26.x" | "bun1.x" | "bun1.4.x" | (string & {}); /** * Experimental trigger configuration (e.g., Vercel Queues). diff --git a/src/presets/vercel/utils.ts b/src/presets/vercel/utils.ts index abf943fcdb..f075469113 100644 --- a/src/presets/vercel/utils.ts +++ b/src/presets/vercel/utils.ts @@ -34,7 +34,8 @@ import { ISR_URL_PARAM } from "./runtime/isr.ts"; // https://vercel.com/docs/build-output-api/configuration // https://vercel.com/docs/functions/runtimes/node-js/node-js-versions -const SUPPORTED_NODE_VERSIONS = [22, 24]; +const MIN_NODE_VERSION = 22; +const LATEST_KNOWN_NODE_VERSION = 26; // h3 ProxyOptions that Vercel CDN rewrites cannot handle at the edge. // https://vercel.com/docs/rewrites @@ -69,7 +70,18 @@ const DEFAULT_PUBLIC_ASSET_MAX_AGE = 31_536_000; // 1 year function getSystemNodeVersion() { const systemNodeVersion = Number.parseInt(process.versions.node.split(".")[0]); - return Number.isNaN(systemNodeVersion) ? 22 : systemNodeVersion; + return Number.isNaN(systemNodeVersion) ? MIN_NODE_VERSION : systemNodeVersion; +} + +// Vercel only offers even (LTS) Node.js versions. The build's own version is included +// so that a new even version works before `LATEST_KNOWN_NODE_VERSION` is bumped. +function getSupportedNodeVersions() { + const newest = Math.max(LATEST_KNOWN_NODE_VERSION, getSystemNodeVersion()); + const versions: number[] = []; + for (let version = MIN_NODE_VERSION; version <= newest; version += 2) { + versions.push(version); + } + return versions; } export async function generateFunctionFiles(nitro: Nitro) { @@ -548,11 +560,15 @@ export async function resolveVercelRuntime(nitro: Nitro) { ); } + const supportedNodeVersions = getSupportedNodeVersions(); + // Newest supported Node.js version allowed by engines.node - let usedNodeVersion = engines?.node ? await getEnginesNodeVersion(engines.node) : undefined; + let usedNodeVersion = engines?.node + ? await getEnginesNodeVersion(engines.node, supportedNodeVersions) + : undefined; if (engines?.node && !usedNodeVersion) { nitro.logger.warn( - `\`engines.node\` (\`${engines.node}\`) in \`package.json\` does not match a supported Vercel Node.js version (${SUPPORTED_NODE_VERSIONS.join(", ")}). Using the Node.js version of the build instead.` + `\`engines.node\` (\`${engines.node}\`) in \`package.json\` does not match a supported Vercel Node.js version (${supportedNodeVersions.join(", ")}). Using the Node.js version of the build instead.` ); } @@ -560,8 +576,8 @@ export async function resolveVercelRuntime(nitro: Nitro) { if (!usedNodeVersion) { const systemNodeVersion = getSystemNodeVersion(); usedNodeVersion = - SUPPORTED_NODE_VERSIONS.find((version) => version >= systemNodeVersion) ?? - SUPPORTED_NODE_VERSIONS.at(-1); + supportedNodeVersions.find((version) => version >= systemNodeVersion) ?? + supportedNodeVersions.at(-1); } runtime = `nodejs${usedNodeVersion}.x`; } @@ -862,13 +878,13 @@ function hasQueryWildcard(to: string): boolean { return index !== -1 && to.includes("**", index); } -async function getEnginesNodeVersion(range: string) { +async function getEnginesNodeVersion(range: string, supportedVersions: number[]) { const { default: validRange } = await import("semver/ranges/valid.js"); if (!validRange(range)) { return; } const { default: intersects } = await import("semver/ranges/intersects.js"); - return SUPPORTED_NODE_VERSIONS.filter((version) => intersects(`${version}.x`, range)).at(-1); + return supportedVersions.filter((version) => intersects(`${version}.x`, range)).at(-1); } async function getEnginesBunRuntime(range: string, opts: { source: string }) { diff --git a/test/unit/vercel-runtime.test.ts b/test/unit/vercel-runtime.test.ts index d10fc1bec5..a5d2fbbdf1 100644 --- a/test/unit/vercel-runtime.test.ts +++ b/test/unit/vercel-runtime.test.ts @@ -14,10 +14,12 @@ describe("vercel: resolveVercelRuntime", () => { beforeEach(async () => { rootDir = await realpath(await mkdtemp(join(tmpdir(), "nitro-vercel-runtime-"))); warn = vi.fn(); + stubNodeVersion("26.0.0"); }); afterEach(async () => { vi.unstubAllGlobals(); + restoreNodeVersion(); await rm(rootDir, { recursive: true, force: true }); }); @@ -51,9 +53,10 @@ describe("vercel: resolveVercelRuntime", () => { ["22.x", "nodejs22.x"], ["^22", "nodejs22.x"], [">=20 <24", "nodejs22.x"], - [">=22.0.0", "nodejs24.x"], + ["26.x", "nodejs26.x"], + [">=22.0.0", "nodejs26.x"], [">=24.20.0 <25", "nodejs24.x"], - ["^20.19.0 || >=22.12.0", "nodejs24.x"], + ["^20.19.0 || >=22.12.0", "nodejs26.x"], ])( "uses the newest supported Node.js version matching engines.node %s", async (node, expected) => { @@ -62,6 +65,35 @@ describe("vercel: resolveVercelRuntime", () => { } ); + it.skipIf(isBun).each([ + ["26.0.0", "nodejs26.x"], + ["24.1.0", "nodejs24.x"], + ["22.0.0", "nodejs22.x"], + ["28.0.0", "nodejs28.x"], + ["30.2.1", "nodejs30.x"], + ["20.0.0", "nodejs22.x"], + ["23.0.0", "nodejs24.x"], + ["25.0.0", "nodejs26.x"], + ["27.0.0", "nodejs26.x"], + ["29.0.0", "nodejs28.x"], + ])("uses the runtime for the build Node.js version %s", async (version, expected) => { + stubNodeVersion(version); + expect(await resolveRuntime()).toBe(expected); + expect(warn).not.toHaveBeenCalled(); + }); + + it("accepts an explicit nodejs26.x runtime", async () => { + stubNodeVersion("24.0.0"); + expect(await resolveRuntime({ runtime: "nodejs26.x" })).toBe("nodejs26.x"); + }); + + it("matches engines.node against even versions newer than the known ones when building on them", async () => { + stubNodeVersion("28.0.0"); + expect(await resolveRuntime({ engines: { node: "28.x" } })).toBe("nodejs28.x"); + expect(await resolveRuntime({ engines: { node: ">=22" } })).toBe("nodejs28.x"); + expect(warn).not.toHaveBeenCalled(); + }); + it("uses engines.node when building with Bun", async () => { stubBun(); expect(await resolveRuntime({ engines: { node: ">=24.20.0 <25" } })).toBe("nodejs24.x"); @@ -128,7 +160,7 @@ describe("vercel: resolveVercelRuntime", () => { expect(await resolveRuntime()).toBe("bun1.x"); }); - it.each(["20.x", "18.x", ">=26", "lts/*"])( + it.each(["20.x", "18.x", ">=28", "lts/*"])( "warns and uses the build Node.js version when engines.node %s matches no supported version", async (node) => { const fallback = await resolveRuntime({ engines: { node } }); @@ -193,6 +225,16 @@ describe("vercel preset: function runtime from engines", () => { }); }); +const originalNodeVersion = Object.getOwnPropertyDescriptor(process.versions, "node")!; + +function stubNodeVersion(version: string) { + Object.defineProperty(process.versions, "node", { ...originalNodeVersion, value: version }); +} + +function restoreNodeVersion() { + Object.defineProperty(process.versions, "node", originalNodeVersion); +} + // `Bun` is a non-configurable global on Bun function stubBun() { if (!isBun) { From 111da3cd4e5309092ff190698bceaf7fd1278a6b Mon Sep 17 00:00:00 2001 From: Ali Smesseim Date: Fri, 30 Oct 2026 22:40:57 +0100 Subject: [PATCH 2/3] fix(vercel): support every Node.js version from 26 Vercel supports every Node.js major from 26 onwards, not only even ones, so a Node.js 27 build or `engines.node: 27.x` should deploy on `nodejs27.x`. Co-Authored-By: Claude Opus 5.5 --- docs/2.deploy/20.providers/vercel.md | 2 +- src/presets/vercel/utils.ts | 17 +++++++++-------- test/unit/vercel-runtime.test.ts | 7 ++++--- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/2.deploy/20.providers/vercel.md b/docs/2.deploy/20.providers/vercel.md index 391cf5d945..27de86716b 100644 --- a/docs/2.deploy/20.providers/vercel.md +++ b/docs/2.deploy/20.providers/vercel.md @@ -40,7 +40,7 @@ Nitro checks these sources, in order, to pick the runtime of the server function 3. `bunVersion` in `vercel.json`. 4. The runtime running the build: Bun when building with Bun, otherwise the build's Node.js major version, clamped to the versions Vercel supports. -Vercel supports the even (LTS) Node.js versions from 22. A build on an even version from 22 uses it for the function, including versions newer than `26`. A build on an odd version uses the next even version, or the previous one if there is no newer known version. +Vercel supports Node.js 22, 24, and every version from 26. A build on a supported version uses it for the function, including versions newer than `26`. A build on Node.js 23 or 25 uses the next supported version. Steps 2 and 3 follow the same precedence as Vercel. diff --git a/src/presets/vercel/utils.ts b/src/presets/vercel/utils.ts index f075469113..dda625f804 100644 --- a/src/presets/vercel/utils.ts +++ b/src/presets/vercel/utils.ts @@ -34,8 +34,9 @@ import { ISR_URL_PARAM } from "./runtime/isr.ts"; // https://vercel.com/docs/build-output-api/configuration // https://vercel.com/docs/functions/runtimes/node-js/node-js-versions -const MIN_NODE_VERSION = 22; -const LATEST_KNOWN_NODE_VERSION = 26; +// Vercel supports Node.js 22, 24, and every major from 26 onwards. +const SUPPORTED_NODE_VERSIONS = [22, 24]; +const FIRST_CONSECUTIVE_NODE_VERSION = 26; // h3 ProxyOptions that Vercel CDN rewrites cannot handle at the edge. // https://vercel.com/docs/rewrites @@ -70,15 +71,15 @@ const DEFAULT_PUBLIC_ASSET_MAX_AGE = 31_536_000; // 1 year function getSystemNodeVersion() { const systemNodeVersion = Number.parseInt(process.versions.node.split(".")[0]); - return Number.isNaN(systemNodeVersion) ? MIN_NODE_VERSION : systemNodeVersion; + return Number.isNaN(systemNodeVersion) ? 22 : systemNodeVersion; } -// Vercel only offers even (LTS) Node.js versions. The build's own version is included -// so that a new even version works before `LATEST_KNOWN_NODE_VERSION` is bumped. +// Lists every supported version up to the build's own one, so that `engines.node` +// can match versions newer than 26. function getSupportedNodeVersions() { - const newest = Math.max(LATEST_KNOWN_NODE_VERSION, getSystemNodeVersion()); - const versions: number[] = []; - for (let version = MIN_NODE_VERSION; version <= newest; version += 2) { + const versions = [...SUPPORTED_NODE_VERSIONS]; + const newest = Math.max(FIRST_CONSECUTIVE_NODE_VERSION, getSystemNodeVersion()); + for (let version = FIRST_CONSECUTIVE_NODE_VERSION; version <= newest; version++) { versions.push(version); } return versions; diff --git a/test/unit/vercel-runtime.test.ts b/test/unit/vercel-runtime.test.ts index a5d2fbbdf1..53b9bd6eef 100644 --- a/test/unit/vercel-runtime.test.ts +++ b/test/unit/vercel-runtime.test.ts @@ -74,8 +74,8 @@ describe("vercel: resolveVercelRuntime", () => { ["20.0.0", "nodejs22.x"], ["23.0.0", "nodejs24.x"], ["25.0.0", "nodejs26.x"], - ["27.0.0", "nodejs26.x"], - ["29.0.0", "nodejs28.x"], + ["27.0.0", "nodejs27.x"], + ["29.0.0", "nodejs29.x"], ])("uses the runtime for the build Node.js version %s", async (version, expected) => { stubNodeVersion(version); expect(await resolveRuntime()).toBe(expected); @@ -87,8 +87,9 @@ describe("vercel: resolveVercelRuntime", () => { expect(await resolveRuntime({ runtime: "nodejs26.x" })).toBe("nodejs26.x"); }); - it("matches engines.node against even versions newer than the known ones when building on them", async () => { + it("matches engines.node against versions newer than 26 when building on them", async () => { stubNodeVersion("28.0.0"); + expect(await resolveRuntime({ engines: { node: "27.x" } })).toBe("nodejs27.x"); expect(await resolveRuntime({ engines: { node: "28.x" } })).toBe("nodejs28.x"); expect(await resolveRuntime({ engines: { node: ">=22" } })).toBe("nodejs28.x"); expect(warn).not.toHaveBeenCalled(); From 91d3de66f040a3340d54aee977d27cf115b308b4 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Sun, 1 Nov 2026 04:06:53 +0000 Subject: [PATCH 3/3] fix(vercel): only support listed Node.js versions (22, 24, 26) Vercel lists each supported major explicitly (build-utils NODE_VERSIONS). Newer builds clamp to 26 instead of emitting a runtime Vercel does not offer yet. --- docs/2.deploy/20.providers/vercel.md | 2 +- src/presets/vercel/utils.ts | 32 +++++++--------------------- test/unit/vercel-runtime.test.ts | 14 ++++++------ 3 files changed, 15 insertions(+), 33 deletions(-) diff --git a/docs/2.deploy/20.providers/vercel.md b/docs/2.deploy/20.providers/vercel.md index 27de86716b..3d79bace3a 100644 --- a/docs/2.deploy/20.providers/vercel.md +++ b/docs/2.deploy/20.providers/vercel.md @@ -40,7 +40,7 @@ Nitro checks these sources, in order, to pick the runtime of the server function 3. `bunVersion` in `vercel.json`. 4. The runtime running the build: Bun when building with Bun, otherwise the build's Node.js major version, clamped to the versions Vercel supports. -Vercel supports Node.js 22, 24, and every version from 26. A build on a supported version uses it for the function, including versions newer than `26`. A build on Node.js 23 or 25 uses the next supported version. +Vercel supports Node.js 22, 24, and 26. A build on Node.js 23 or 25 uses the next supported version, and a build on a newer version uses `26`. Steps 2 and 3 follow the same precedence as Vercel. diff --git a/src/presets/vercel/utils.ts b/src/presets/vercel/utils.ts index dda625f804..2687ca6964 100644 --- a/src/presets/vercel/utils.ts +++ b/src/presets/vercel/utils.ts @@ -34,9 +34,8 @@ import { ISR_URL_PARAM } from "./runtime/isr.ts"; // https://vercel.com/docs/build-output-api/configuration // https://vercel.com/docs/functions/runtimes/node-js/node-js-versions -// Vercel supports Node.js 22, 24, and every major from 26 onwards. -const SUPPORTED_NODE_VERSIONS = [22, 24]; -const FIRST_CONSECUTIVE_NODE_VERSION = 26; +// https://github.com/vercel/vercel/blob/main/packages/build-utils/src/fs/node-version.ts +const SUPPORTED_NODE_VERSIONS = [22, 24, 26]; // h3 ProxyOptions that Vercel CDN rewrites cannot handle at the edge. // https://vercel.com/docs/rewrites @@ -74,17 +73,6 @@ function getSystemNodeVersion() { return Number.isNaN(systemNodeVersion) ? 22 : systemNodeVersion; } -// Lists every supported version up to the build's own one, so that `engines.node` -// can match versions newer than 26. -function getSupportedNodeVersions() { - const versions = [...SUPPORTED_NODE_VERSIONS]; - const newest = Math.max(FIRST_CONSECUTIVE_NODE_VERSION, getSystemNodeVersion()); - for (let version = FIRST_CONSECUTIVE_NODE_VERSION; version <= newest; version++) { - versions.push(version); - } - return versions; -} - export async function generateFunctionFiles(nitro: Nitro) { const o11Routes = getObservabilityRoutes(nitro); @@ -561,15 +549,11 @@ export async function resolveVercelRuntime(nitro: Nitro) { ); } - const supportedNodeVersions = getSupportedNodeVersions(); - // Newest supported Node.js version allowed by engines.node - let usedNodeVersion = engines?.node - ? await getEnginesNodeVersion(engines.node, supportedNodeVersions) - : undefined; + let usedNodeVersion = engines?.node ? await getEnginesNodeVersion(engines.node) : undefined; if (engines?.node && !usedNodeVersion) { nitro.logger.warn( - `\`engines.node\` (\`${engines.node}\`) in \`package.json\` does not match a supported Vercel Node.js version (${supportedNodeVersions.join(", ")}). Using the Node.js version of the build instead.` + `\`engines.node\` (\`${engines.node}\`) in \`package.json\` does not match a supported Vercel Node.js version (${SUPPORTED_NODE_VERSIONS.join(", ")}). Using the Node.js version of the build instead.` ); } @@ -577,8 +561,8 @@ export async function resolveVercelRuntime(nitro: Nitro) { if (!usedNodeVersion) { const systemNodeVersion = getSystemNodeVersion(); usedNodeVersion = - supportedNodeVersions.find((version) => version >= systemNodeVersion) ?? - supportedNodeVersions.at(-1); + SUPPORTED_NODE_VERSIONS.find((version) => version >= systemNodeVersion) ?? + SUPPORTED_NODE_VERSIONS.at(-1); } runtime = `nodejs${usedNodeVersion}.x`; } @@ -879,13 +863,13 @@ function hasQueryWildcard(to: string): boolean { return index !== -1 && to.includes("**", index); } -async function getEnginesNodeVersion(range: string, supportedVersions: number[]) { +async function getEnginesNodeVersion(range: string) { const { default: validRange } = await import("semver/ranges/valid.js"); if (!validRange(range)) { return; } const { default: intersects } = await import("semver/ranges/intersects.js"); - return supportedVersions.filter((version) => intersects(`${version}.x`, range)).at(-1); + return SUPPORTED_NODE_VERSIONS.filter((version) => intersects(`${version}.x`, range)).at(-1); } async function getEnginesBunRuntime(range: string, opts: { source: string }) { diff --git a/test/unit/vercel-runtime.test.ts b/test/unit/vercel-runtime.test.ts index 53b9bd6eef..f3109da3e2 100644 --- a/test/unit/vercel-runtime.test.ts +++ b/test/unit/vercel-runtime.test.ts @@ -69,13 +69,11 @@ describe("vercel: resolveVercelRuntime", () => { ["26.0.0", "nodejs26.x"], ["24.1.0", "nodejs24.x"], ["22.0.0", "nodejs22.x"], - ["28.0.0", "nodejs28.x"], - ["30.2.1", "nodejs30.x"], ["20.0.0", "nodejs22.x"], ["23.0.0", "nodejs24.x"], ["25.0.0", "nodejs26.x"], - ["27.0.0", "nodejs27.x"], - ["29.0.0", "nodejs29.x"], + ["27.0.0", "nodejs26.x"], + ["30.2.1", "nodejs26.x"], ])("uses the runtime for the build Node.js version %s", async (version, expected) => { stubNodeVersion(version); expect(await resolveRuntime()).toBe(expected); @@ -87,12 +85,12 @@ describe("vercel: resolveVercelRuntime", () => { expect(await resolveRuntime({ runtime: "nodejs26.x" })).toBe("nodejs26.x"); }); - it("matches engines.node against versions newer than 26 when building on them", async () => { + it("does not select versions newer than 26 when building on them", async () => { stubNodeVersion("28.0.0"); - expect(await resolveRuntime({ engines: { node: "27.x" } })).toBe("nodejs27.x"); - expect(await resolveRuntime({ engines: { node: "28.x" } })).toBe("nodejs28.x"); - expect(await resolveRuntime({ engines: { node: ">=22" } })).toBe("nodejs28.x"); + expect(await resolveRuntime({ engines: { node: ">=22" } })).toBe("nodejs26.x"); expect(warn).not.toHaveBeenCalled(); + expect(await resolveRuntime({ engines: { node: "28.x" } })).toBe("nodejs26.x"); + expect(warn).toHaveBeenCalledWith(expect.stringContaining("(22, 24, 26)")); }); it("uses engines.node when building with Bun", async () => {