diff --git a/docs/2.deploy/20.providers/vercel.md b/docs/2.deploy/20.providers/vercel.md index a85127ba07..3d79bace3a 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 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. ```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..2687ca6964 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]; +// 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 diff --git a/test/unit/vercel-runtime.test.ts b/test/unit/vercel-runtime.test.ts index d10fc1bec5..f3109da3e2 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,34 @@ describe("vercel: resolveVercelRuntime", () => { } ); + it.skipIf(isBun).each([ + ["26.0.0", "nodejs26.x"], + ["24.1.0", "nodejs24.x"], + ["22.0.0", "nodejs22.x"], + ["20.0.0", "nodejs22.x"], + ["23.0.0", "nodejs24.x"], + ["25.0.0", "nodejs26.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); + 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("does not select versions newer than 26 when building on them", async () => { + stubNodeVersion("28.0.0"); + 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 () => { stubBun(); expect(await resolveRuntime({ engines: { node: ">=24.20.0 <25" } })).toBe("nodejs24.x"); @@ -128,7 +159,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 +224,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) {