From 1e996b19a20aebdd86489ba8e27ee0449bb3315f Mon Sep 17 00:00:00 2001 From: Ali Smesseim Date: Fri, 30 Oct 2026 10:53:06 +0100 Subject: [PATCH 1/3] fix(vercel): use even Node.js build versions as the function runtime (v2) The preset picked the runtime from a hard-coded list of Node.js versions, so a build on Node.js 26 silently produced `nodejs24.x` in `.vc-config.json`. Use `nodejs.x` for any even major from 20, so new Vercel runtimes need no code change. Odd majors use the previous even one; majors below 20 use `nodejs20.x`. Co-Authored-By: Claude Opus 5.5 --- src/presets/vercel/types.ts | 8 ++++- src/presets/vercel/utils.ts | 19 +++++++----- test/presets/vercel.test.ts | 24 +++++++++++++++ test/unit/vercel-runtime.test.ts | 50 ++++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 8 deletions(-) create mode 100644 test/unit/vercel-runtime.test.ts diff --git a/src/presets/vercel/types.ts b/src/presets/vercel/types.ts index e700714802..67aa2108e1 100644 --- a/src/presets/vercel/types.ts +++ b/src/presets/vercel/types.ts @@ -107,7 +107,13 @@ export interface VercelServerlessFunctionConfig { /** * The runtime to use. Defaults to the auto-detected Node.js version. */ - runtime?: "nodejs20.x" | "nodejs22.x" | "bun1.x" | (string & {}); + runtime?: + | "nodejs20.x" + | "nodejs22.x" + | "nodejs24.x" + | "nodejs26.x" + | "bun1.x" + | (string & {}); [key: string]: unknown; } diff --git a/src/presets/vercel/utils.ts b/src/presets/vercel/utils.ts index 1bc4e332eb..d8cc9e8f6a 100644 --- a/src/presets/vercel/utils.ts +++ b/src/presets/vercel/utils.ts @@ -17,7 +17,8 @@ import { ISR_URL_PARAM } from "./runtime/consts"; // https://vercel.com/docs/build-output-api/configuration // https://vercel.com/docs/functions/runtimes/node-js/node-js-versions -const SUPPORTED_NODE_VERSIONS = [18, 20, 22, 24]; +// Vercel runs the even (LTS) Node.js majors from 20 onwards. +const MIN_NODE_VERSION = 20; const FALLBACK_ROUTE = "/__fallback"; @@ -39,6 +40,15 @@ function getSystemNodeVersion() { return Number.isNaN(systemNodeVersion) ? 22 : systemNodeVersion; } +export function resolveNodeRuntime( + systemNodeVersion = getSystemNodeVersion() +): `nodejs${number}.x` { + // Odd (non-LTS) majors are not Vercel runtimes: use the previous even one. + // Majors below the minimum use the oldest runtime. + const evenVersion = systemNodeVersion - (systemNodeVersion % 2); + return `nodejs${Math.max(MIN_NODE_VERSION, evenVersion)}.x`; +} + export async function generateFunctionFiles(nitro: Nitro) { const o11Routes = getObservabilityRoutes(nitro); @@ -58,12 +68,7 @@ export async function generateFunctionFiles(nitro: Nitro) { runtime = `bun${vercelConfig.bunVersion || "1.x"}`; } else { // 3. Auto-detect runtime based on system Node.js version - const systemNodeVersion = getSystemNodeVersion(); - const usedNodeVersion = - SUPPORTED_NODE_VERSIONS.find( - (version) => version >= systemNodeVersion - ) ?? SUPPORTED_NODE_VERSIONS.at(-1); - runtime = `nodejs${usedNodeVersion}.x`; + runtime = resolveNodeRuntime(); } } diff --git a/test/presets/vercel.test.ts b/test/presets/vercel.test.ts index 7583950b30..5eaa07638b 100644 --- a/test/presets/vercel.test.ts +++ b/test/presets/vercel.test.ts @@ -741,6 +741,30 @@ describe("nitro:preset:vercel:bun", async () => { }); }); +describe("nitro:preset:vercel:node-runtime", async () => { + const ctx = await setupTest("vercel", { + config: { + preset: "vercel", + vercel: { + functions: { + runtime: "nodejs26.x", + }, + }, + }, + }); + // Read now: the other vercel groups build into the same output dir + const config = await fsp + .readFile( + resolve(ctx.outDir, "functions/__fallback.func/.vc-config.json"), + "utf8" + ) + .then((r) => JSON.parse(r)); + + it("should keep an explicit nodejs26.x runtime", () => { + expect(config.runtime).toBe("nodejs26.x"); + }); +}); + describe("nitro:preset:vercel:skew-protection", async () => { const prevDeploymentId = process.env.VERCEL_DEPLOYMENT_ID; process.env.VERCEL_DEPLOYMENT_ID = "dpl_test"; diff --git a/test/unit/vercel-runtime.test.ts b/test/unit/vercel-runtime.test.ts new file mode 100644 index 0000000000..8cb7aef8ff --- /dev/null +++ b/test/unit/vercel-runtime.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; +import { resolveNodeRuntime } from "../../src/presets/vercel/utils"; + +describe("vercel: resolveNodeRuntime", () => { + it.each([ + [20, "nodejs20.x"], + [22, "nodejs22.x"], + [24, "nodejs24.x"], + [26, "nodejs26.x"], + // Future even majors need no code change + [28, "nodejs28.x"], + [30, "nodejs30.x"], + ])("uses nodejs%s.x for an even Node.js major", (major, expected) => { + expect(resolveNodeRuntime(major)).toBe(expected); + }); + + it.each([ + [21, "nodejs20.x"], + [23, "nodejs22.x"], + [25, "nodejs24.x"], + [27, "nodejs26.x"], + ])( + "falls back to the previous even major for odd Node.js %s", + (major, expected) => { + expect(resolveNodeRuntime(major)).toBe(expected); + } + ); + + it.each([16, 18, 19])("uses the oldest runtime for Node.js %s", (major) => { + expect(resolveNodeRuntime(major)).toBe("nodejs20.x"); + }); + + it("detects the Node.js major of the running process", () => { + const node = Object.getOwnPropertyDescriptor(process.versions, "node")!; + try { + Object.defineProperty(process.versions, "node", { + value: "26.1.0", + configurable: true, + }); + expect(resolveNodeRuntime()).toBe("nodejs26.x"); + Object.defineProperty(process.versions, "node", { + value: "24.0.0", + configurable: true, + }); + expect(resolveNodeRuntime()).toBe("nodejs24.x"); + } finally { + Object.defineProperty(process.versions, "node", node); + } + }); +}); From 36d4c44c82038684327afb751f91148822202977 Mon Sep 17 00:00:00 2001 From: Ali Smesseim Date: Fri, 30 Oct 2026 22:40:21 +0100 Subject: [PATCH 2/3] fix(vercel): use the build's runtime for every Node.js version from 26 (v2) Vercel supports every Node.js major from 26 onwards, not only even ones, so a Node.js 27 build should deploy on `nodejs27.x`. Older majors keep using the next supported version, as before. Co-Authored-By: Claude Opus 5.5 --- src/presets/vercel/utils.ts | 17 +++++++++++------ test/unit/vercel-runtime.test.ts | 23 ++++++++++++----------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/presets/vercel/utils.ts b/src/presets/vercel/utils.ts index d8cc9e8f6a..7990fb8650 100644 --- a/src/presets/vercel/utils.ts +++ b/src/presets/vercel/utils.ts @@ -17,8 +17,9 @@ import { ISR_URL_PARAM } from "./runtime/consts"; // https://vercel.com/docs/build-output-api/configuration // https://vercel.com/docs/functions/runtimes/node-js/node-js-versions -// Vercel runs the even (LTS) Node.js majors from 20 onwards. -const MIN_NODE_VERSION = 20; +// Vercel supports Node.js 20, 22, 24, and every major from 26 onwards. +const SUPPORTED_NODE_VERSIONS = [20, 22, 24]; +const FIRST_CONSECUTIVE_NODE_VERSION = 26; const FALLBACK_ROUTE = "/__fallback"; @@ -43,10 +44,14 @@ function getSystemNodeVersion() { export function resolveNodeRuntime( systemNodeVersion = getSystemNodeVersion() ): `nodejs${number}.x` { - // Odd (non-LTS) majors are not Vercel runtimes: use the previous even one. - // Majors below the minimum use the oldest runtime. - const evenVersion = systemNodeVersion - (systemNodeVersion % 2); - return `nodejs${Math.max(MIN_NODE_VERSION, evenVersion)}.x`; + if (systemNodeVersion >= FIRST_CONSECUTIVE_NODE_VERSION) { + return `nodejs${systemNodeVersion}.x`; + } + // Older majors without a Vercel runtime use the next supported one. + const usedNodeVersion = + SUPPORTED_NODE_VERSIONS.find((version) => version >= systemNodeVersion) ?? + FIRST_CONSECUTIVE_NODE_VERSION; + return `nodejs${usedNodeVersion}.x`; } export async function generateFunctionFiles(nitro: Nitro) { diff --git a/test/unit/vercel-runtime.test.ts b/test/unit/vercel-runtime.test.ts index 8cb7aef8ff..ea524eed74 100644 --- a/test/unit/vercel-runtime.test.ts +++ b/test/unit/vercel-runtime.test.ts @@ -7,25 +7,26 @@ describe("vercel: resolveNodeRuntime", () => { [22, "nodejs22.x"], [24, "nodejs24.x"], [26, "nodejs26.x"], - // Future even majors need no code change + // Future majors need no code change + [27, "nodejs27.x"], [28, "nodejs28.x"], + [29, "nodejs29.x"], [30, "nodejs30.x"], - ])("uses nodejs%s.x for an even Node.js major", (major, expected) => { - expect(resolveNodeRuntime(major)).toBe(expected); - }); - - it.each([ - [21, "nodejs20.x"], - [23, "nodejs22.x"], - [25, "nodejs24.x"], - [27, "nodejs26.x"], ])( - "falls back to the previous even major for odd Node.js %s", + "uses the runtime of a supported Node.js major (%s)", (major, expected) => { expect(resolveNodeRuntime(major)).toBe(expected); } ); + it.each([ + [21, "nodejs22.x"], + [23, "nodejs24.x"], + [25, "nodejs26.x"], + ])("uses the next supported major for Node.js %s", (major, expected) => { + expect(resolveNodeRuntime(major)).toBe(expected); + }); + it.each([16, 18, 19])("uses the oldest runtime for Node.js %s", (major) => { expect(resolveNodeRuntime(major)).toBe("nodejs20.x"); }); From bbfac98d9c863a50b43add4f0133ac03d4ccd69e Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Sun, 1 Nov 2026 04:12:37 +0000 Subject: [PATCH 3/3] fix(vercel): only support listed Node.js versions (22, 24, 26) (v2) Matches Vercel's build-utils NODE_VERSIONS. Node.js 20 is discontinued on Vercel, and newer builds clamp to 26 instead of emitting a runtime Vercel does not offer yet. --- src/presets/vercel/utils.ts | 11 +++-------- test/unit/vercel-runtime.test.ts | 14 ++++++-------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/presets/vercel/utils.ts b/src/presets/vercel/utils.ts index 7990fb8650..157baca23f 100644 --- a/src/presets/vercel/utils.ts +++ b/src/presets/vercel/utils.ts @@ -17,9 +17,8 @@ import { ISR_URL_PARAM } from "./runtime/consts"; // https://vercel.com/docs/build-output-api/configuration // https://vercel.com/docs/functions/runtimes/node-js/node-js-versions -// Vercel supports Node.js 20, 22, 24, and every major from 26 onwards. -const SUPPORTED_NODE_VERSIONS = [20, 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]; const FALLBACK_ROUTE = "/__fallback"; @@ -44,13 +43,9 @@ function getSystemNodeVersion() { export function resolveNodeRuntime( systemNodeVersion = getSystemNodeVersion() ): `nodejs${number}.x` { - if (systemNodeVersion >= FIRST_CONSECUTIVE_NODE_VERSION) { - return `nodejs${systemNodeVersion}.x`; - } - // Older majors without a Vercel runtime use the next supported one. const usedNodeVersion = SUPPORTED_NODE_VERSIONS.find((version) => version >= systemNodeVersion) ?? - FIRST_CONSECUTIVE_NODE_VERSION; + SUPPORTED_NODE_VERSIONS.at(-1)!; return `nodejs${usedNodeVersion}.x`; } diff --git a/test/unit/vercel-runtime.test.ts b/test/unit/vercel-runtime.test.ts index ea524eed74..19103e9de4 100644 --- a/test/unit/vercel-runtime.test.ts +++ b/test/unit/vercel-runtime.test.ts @@ -3,15 +3,9 @@ import { resolveNodeRuntime } from "../../src/presets/vercel/utils"; describe("vercel: resolveNodeRuntime", () => { it.each([ - [20, "nodejs20.x"], [22, "nodejs22.x"], [24, "nodejs24.x"], [26, "nodejs26.x"], - // Future majors need no code change - [27, "nodejs27.x"], - [28, "nodejs28.x"], - [29, "nodejs29.x"], - [30, "nodejs30.x"], ])( "uses the runtime of a supported Node.js major (%s)", (major, expected) => { @@ -27,8 +21,12 @@ describe("vercel: resolveNodeRuntime", () => { expect(resolveNodeRuntime(major)).toBe(expected); }); - it.each([16, 18, 19])("uses the oldest runtime for Node.js %s", (major) => { - expect(resolveNodeRuntime(major)).toBe("nodejs20.x"); + it.each([16, 18, 20])("uses the oldest runtime for Node.js %s", (major) => { + expect(resolveNodeRuntime(major)).toBe("nodejs22.x"); + }); + + it.each([27, 28, 30])("uses the newest runtime for Node.js %s", (major) => { + expect(resolveNodeRuntime(major)).toBe("nodejs26.x"); }); it("detects the Node.js major of the running process", () => {