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..157baca23f 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]; +// 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"; @@ -39,6 +40,15 @@ function getSystemNodeVersion() { return Number.isNaN(systemNodeVersion) ? 22 : systemNodeVersion; } +export function resolveNodeRuntime( + systemNodeVersion = getSystemNodeVersion() +): `nodejs${number}.x` { + const usedNodeVersion = + SUPPORTED_NODE_VERSIONS.find((version) => version >= systemNodeVersion) ?? + SUPPORTED_NODE_VERSIONS.at(-1)!; + return `nodejs${usedNodeVersion}.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..19103e9de4 --- /dev/null +++ b/test/unit/vercel-runtime.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vitest"; +import { resolveNodeRuntime } from "../../src/presets/vercel/utils"; + +describe("vercel: resolveNodeRuntime", () => { + it.each([ + [22, "nodejs22.x"], + [24, "nodejs24.x"], + [26, "nodejs26.x"], + ])( + "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, 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", () => { + 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); + } + }); +});