diff --git a/.changeset/deep-parks-marry.md b/.changeset/deep-parks-marry.md new file mode 100644 index 000000000000..6d332012e916 --- /dev/null +++ b/.changeset/deep-parks-marry.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-vercel': patch +--- + +chore: improve runtime config parsing diff --git a/packages/adapter-vercel/index.d.ts b/packages/adapter-vercel/index.d.ts index 792dfe5b4f00..8d37ce4cc409 100644 --- a/packages/adapter-vercel/index.d.ts +++ b/packages/adapter-vercel/index.d.ts @@ -1,5 +1,6 @@ import { Adapter } from '@sveltejs/kit'; import './ambient.js'; +import { RuntimeConfigKey } from './utils.js'; export default function plugin(config?: Config): Adapter; @@ -8,7 +9,7 @@ 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) (`'nodejs18.x'`, `'nodejs20.x'` etc). * @default Same as the build environment */ - runtime?: `nodejs${number}.x` | `experimental_bun1.x`; + runtime?: Exclude; /** * To which regions to deploy the app. A list of regions. * More info: https://vercel.com/docs/concepts/edge-network/regions diff --git a/packages/adapter-vercel/index.js b/packages/adapter-vercel/index.js index 809c6dea3382..b001407c6560 100644 --- a/packages/adapter-vercel/index.js +++ b/packages/adapter-vercel/index.js @@ -5,7 +5,7 @@ import process from 'node:process'; import { fileURLToPath } from 'node:url'; import { nodeFileTrace } from '@vercel/nft'; import esbuild from 'esbuild'; -import { get_pathname, parse_isr_expiration, pattern_to_src } from './utils.js'; +import { get_pathname, parse_isr_expiration, pattern_to_src, resolve_runtime } from './utils.js'; import { VERSION } from '@sveltejs/kit'; /** @@ -24,30 +24,6 @@ const INTERNAL = '![-]'; // this name is guaranteed not to conflict with user ro const [kit_major, kit_minor] = VERSION.split('.'); -const get_default_runtime = () => { - const major = Number(process.version.slice(1).split('.')[0]); - - // If we're building on Vercel, we know that the version will be fine because Vercel - // provides Node (and Vercel won't provide something it doesn't support). - // Also means we're not on the hook for updating the adapter every time a new Node - // version is added to Vercel. - if (!process.env.VERCEL) { - if (major < 20 || major > 22) { - throw new Error( - `Building locally with unsupported Node.js version: ${process.version}. Please use Node 20 or 22 to build your project, or explicitly specify a runtime in your adapter configuration.` - ); - } - - if (major % 2 !== 0) { - throw new Error( - `Unsupported Node.js version: ${process.version}. Please use an even-numbered Node version to build your project, or explicitly specify a runtime in your adapter configuration.` - ); - } - } - - return `nodejs${major}.x`; -}; - // https://vercel.com/docs/functions/edge-functions/edge-runtime#compatible-node.js-modules const compatible_node_modules = ['async_hooks', 'events', 'buffer', 'assert', 'util']; @@ -294,12 +270,8 @@ const plugin = function (defaults = {}) { // group routes by config for (const route of builder.routes) { - const runtime = ( - route.config?.runtime ?? - defaults?.runtime ?? - get_default_runtime() - ).replace('experimental_', ''); - const config = { runtime, ...defaults, ...route.config }; + const runtime = resolve_runtime(defaults.runtime, route.config.runtime); + const config = { ...defaults, ...route.config, runtime }; if (is_prerendered(route)) { if (config.isr) { @@ -308,23 +280,10 @@ const plugin = function (defaults = {}) { continue; } - const node_runtime = /nodejs([0-9]+)\.x/.exec(runtime); - const bun_runtime = /^bun/.exec(runtime); - if ( - runtime !== 'edge' && - !bun_runtime && - (!node_runtime || parseInt(node_runtime[1]) < 20) - ) { - throw new Error( - `Invalid runtime '${runtime}' for route ${route.id}. Valid runtimes are 'edge', 'experimental_bun1.x', 'nodejs20.x' or 'nodejs22.x' ` + - '(see the Node.js Version section in your Vercel project settings for info on the currently supported versions).' - ); - } - if (config.isr) { const directory = path.relative('.', builder.config.kit.files.routes + route.id); - if (!runtime.startsWith('nodejs') && !bun_runtime) { + if (runtime === 'edge') { throw new Error( `${directory}: Routes using \`isr\` must use a Node.js or Bun runtime (for example 'nodejs22.x' or 'experimental_bun1.x')` ); @@ -409,13 +368,13 @@ const plugin = function (defaults = {}) { // we need to create a catch-all route so that 404s are handled // by SvelteKit rather than Vercel - const runtime = (defaults.runtime ?? get_default_runtime()).replace('experimental_', ''); + const runtime = resolve_runtime(defaults.runtime); const generate_function = runtime === 'edge' ? generate_edge_function : generate_serverless_function; await generate_function( `${INTERNAL}/catchall`, - /** @type {any} */ ({ runtime, ...defaults }), + /** @type {any} */ ({ ...defaults, runtime }), [] ); } diff --git a/packages/adapter-vercel/test/utils.spec.js b/packages/adapter-vercel/test/utils.spec.js index fbcd95996667..ffee426fa170 100644 --- a/packages/adapter-vercel/test/utils.spec.js +++ b/packages/adapter-vercel/test/utils.spec.js @@ -1,5 +1,5 @@ import { assert, test, describe } from 'vitest'; -import { get_pathname, parse_isr_expiration, pattern_to_src } from '../utils.js'; +import { get_pathname, parse_isr_expiration, pattern_to_src, resolve_runtime } from '../utils.js'; // workaround so that TypeScript doesn't follow that import which makes it pick up that file and then error on missing import aliases const { parse_route_id } = await import('../../kit/src/' + 'utils/routing.js'); @@ -171,3 +171,19 @@ describe('parse_isr_expiration', () => { ); }); }); + +describe('resolve_runtime', () => { + test('prefers override_key over default_key', () => { + const result = resolve_runtime('nodejs20.x', 'experimental_bun1.x'); + assert.equal(result, 'bun1.x'); + }); + + test('uses default_key when override_key is undefined', () => { + const result = resolve_runtime('experimental_bun1.x'); + 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/); + }); +}); diff --git a/packages/adapter-vercel/utils.js b/packages/adapter-vercel/utils.js index 0da89f84927b..983b30bbb5be 100644 --- a/packages/adapter-vercel/utils.js +++ b/packages/adapter-vercel/utils.js @@ -1,3 +1,5 @@ +import process from 'node:process'; + /** @param {import("@sveltejs/kit").RouteDefinition} route */ export function get_pathname(route) { let i = 1; @@ -106,3 +108,47 @@ export function parse_isr_expiration(value, route_id) { } return parsed; } + +/** + * @param {string | undefined} default_key + * @param {string | undefined} [override_key] + * @returns {RuntimeKey} + */ +export function resolve_runtime(default_key, override_key) { + const key = (override_key ?? default_key ?? get_default_runtime()).replace('experimental_', ''); + assert_is_valid_runtime(key); + return key; +} + +/** @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]); + + if (major !== 20 && major !== 22) { + throw new Error( + `Unsupported Node.js version: ${process.version}. Please use Node 20 or 22 to build your project, or explicitly specify a runtime in your adapter configuration.` + ); + } + + return `nodejs${major}.x`; +} + +const valid_runtimes = /** @type {const} */ (['nodejs20.x', 'nodejs22.x', 'bun1.x', 'edge']); + +/** + * @param {string} key + * @returns {asserts key is RuntimeKey} + */ +function assert_is_valid_runtime(key) { + if (!valid_runtimes.includes(/** @type {RuntimeKey} */ (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.` + ); + } +} + +/** @typedef {Exclude | 'experimental_bun1.x'} RuntimeConfigKey */ +/** @typedef {typeof valid_runtimes[number]} RuntimeKey */