Skip to content

Commit 79ad886

Browse files
chore: improve runtime validation for adapter-vercel (#14838)
* chore: improve runtime validation for adapter-vercel * Update packages/adapter-vercel/utils.js Co-authored-by: Simon H <[email protected]> --------- Co-authored-by: Simon H <[email protected]>
1 parent 8b32b6c commit 79ad886

File tree

5 files changed

+76
-49
lines changed

5 files changed

+76
-49
lines changed

.changeset/deep-parks-marry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/adapter-vercel': patch
3+
---
4+
5+
chore: improve runtime config parsing

packages/adapter-vercel/index.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Adapter } from '@sveltejs/kit';
22
import './ambient.js';
3+
import { RuntimeConfigKey } from './utils.js';
34

45
export default function plugin(config?: Config): Adapter;
56

@@ -8,7 +9,7 @@ export interface ServerlessConfig {
89
* 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).
910
* @default Same as the build environment
1011
*/
11-
runtime?: `nodejs${number}.x` | `experimental_bun1.x`;
12+
runtime?: Exclude<RuntimeConfigKey, 'edge'>;
1213
/**
1314
* To which regions to deploy the app. A list of regions.
1415
* More info: https://vercel.com/docs/concepts/edge-network/regions

packages/adapter-vercel/index.js

Lines changed: 6 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import process from 'node:process';
55
import { fileURLToPath } from 'node:url';
66
import { nodeFileTrace } from '@vercel/nft';
77
import esbuild from 'esbuild';
8-
import { get_pathname, parse_isr_expiration, pattern_to_src } from './utils.js';
8+
import { get_pathname, parse_isr_expiration, pattern_to_src, resolve_runtime } from './utils.js';
99
import { VERSION } from '@sveltejs/kit';
1010

1111
/**
@@ -24,30 +24,6 @@ const INTERNAL = '![-]'; // this name is guaranteed not to conflict with user ro
2424

2525
const [kit_major, kit_minor] = VERSION.split('.');
2626

27-
const get_default_runtime = () => {
28-
const major = Number(process.version.slice(1).split('.')[0]);
29-
30-
// If we're building on Vercel, we know that the version will be fine because Vercel
31-
// provides Node (and Vercel won't provide something it doesn't support).
32-
// Also means we're not on the hook for updating the adapter every time a new Node
33-
// version is added to Vercel.
34-
if (!process.env.VERCEL) {
35-
if (major < 20 || major > 22) {
36-
throw new Error(
37-
`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.`
38-
);
39-
}
40-
41-
if (major % 2 !== 0) {
42-
throw new Error(
43-
`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.`
44-
);
45-
}
46-
}
47-
48-
return `nodejs${major}.x`;
49-
};
50-
5127
// https://vercel.com/docs/functions/edge-functions/edge-runtime#compatible-node.js-modules
5228
const compatible_node_modules = ['async_hooks', 'events', 'buffer', 'assert', 'util'];
5329

@@ -294,12 +270,8 @@ const plugin = function (defaults = {}) {
294270

295271
// group routes by config
296272
for (const route of builder.routes) {
297-
const runtime = (
298-
route.config?.runtime ??
299-
defaults?.runtime ??
300-
get_default_runtime()
301-
).replace('experimental_', '');
302-
const config = { runtime, ...defaults, ...route.config };
273+
const runtime = resolve_runtime(defaults.runtime, route.config.runtime);
274+
const config = { ...defaults, ...route.config, runtime };
303275

304276
if (is_prerendered(route)) {
305277
if (config.isr) {
@@ -308,23 +280,10 @@ const plugin = function (defaults = {}) {
308280
continue;
309281
}
310282

311-
const node_runtime = /nodejs([0-9]+)\.x/.exec(runtime);
312-
const bun_runtime = /^bun/.exec(runtime);
313-
if (
314-
runtime !== 'edge' &&
315-
!bun_runtime &&
316-
(!node_runtime || parseInt(node_runtime[1]) < 20)
317-
) {
318-
throw new Error(
319-
`Invalid runtime '${runtime}' for route ${route.id}. Valid runtimes are 'edge', 'experimental_bun1.x', 'nodejs20.x' or 'nodejs22.x' ` +
320-
'(see the Node.js Version section in your Vercel project settings for info on the currently supported versions).'
321-
);
322-
}
323-
324283
if (config.isr) {
325284
const directory = path.relative('.', builder.config.kit.files.routes + route.id);
326285

327-
if (!runtime.startsWith('nodejs') && !bun_runtime) {
286+
if (runtime === 'edge') {
328287
throw new Error(
329288
`${directory}: Routes using \`isr\` must use a Node.js or Bun runtime (for example 'nodejs22.x' or 'experimental_bun1.x')`
330289
);
@@ -409,13 +368,13 @@ const plugin = function (defaults = {}) {
409368
// we need to create a catch-all route so that 404s are handled
410369
// by SvelteKit rather than Vercel
411370

412-
const runtime = (defaults.runtime ?? get_default_runtime()).replace('experimental_', '');
371+
const runtime = resolve_runtime(defaults.runtime);
413372
const generate_function =
414373
runtime === 'edge' ? generate_edge_function : generate_serverless_function;
415374

416375
await generate_function(
417376
`${INTERNAL}/catchall`,
418-
/** @type {any} */ ({ runtime, ...defaults }),
377+
/** @type {any} */ ({ ...defaults, runtime }),
419378
[]
420379
);
421380
}

packages/adapter-vercel/test/utils.spec.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { assert, test, describe } from 'vitest';
2-
import { get_pathname, parse_isr_expiration, pattern_to_src } from '../utils.js';
2+
import { get_pathname, parse_isr_expiration, pattern_to_src, resolve_runtime } from '../utils.js';
33

44
// workaround so that TypeScript doesn't follow that import which makes it pick up that file and then error on missing import aliases
55
const { parse_route_id } = await import('../../kit/src/' + 'utils/routing.js');
@@ -171,3 +171,19 @@ describe('parse_isr_expiration', () => {
171171
);
172172
});
173173
});
174+
175+
describe('resolve_runtime', () => {
176+
test('prefers override_key over default_key', () => {
177+
const result = resolve_runtime('nodejs20.x', 'experimental_bun1.x');
178+
assert.equal(result, 'bun1.x');
179+
});
180+
181+
test('uses default_key when override_key is undefined', () => {
182+
const result = resolve_runtime('experimental_bun1.x');
183+
assert.equal(result, 'bun1.x');
184+
});
185+
186+
test('throws an error when resolving to an invalid runtime', () => {
187+
assert.throws(() => resolve_runtime('node18.x', undefined), /Unsupported runtime: node18.x/);
188+
});
189+
});

packages/adapter-vercel/utils.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import process from 'node:process';
2+
13
/** @param {import("@sveltejs/kit").RouteDefinition<any>} route */
24
export function get_pathname(route) {
35
let i = 1;
@@ -106,3 +108,47 @@ export function parse_isr_expiration(value, route_id) {
106108
}
107109
return parsed;
108110
}
111+
112+
/**
113+
* @param {string | undefined} default_key
114+
* @param {string | undefined} [override_key]
115+
* @returns {RuntimeKey}
116+
*/
117+
export function resolve_runtime(default_key, override_key) {
118+
const key = (override_key ?? default_key ?? get_default_runtime()).replace('experimental_', '');
119+
assert_is_valid_runtime(key);
120+
return key;
121+
}
122+
123+
/** @returns {RuntimeKey} */
124+
function get_default_runtime() {
125+
// TODO may someday need to auto-detect Bun, but this will be complicated because you may want to run your build
126+
// with Bun but not have your serverless runtime be in Bun. Vercel will likely have to attach something to `globalThis` or similar
127+
// to tell us what the bun configuration is.
128+
const major = Number(process.version.slice(1).split('.')[0]);
129+
130+
if (major !== 20 && major !== 22) {
131+
throw new Error(
132+
`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.`
133+
);
134+
}
135+
136+
return `nodejs${major}.x`;
137+
}
138+
139+
const valid_runtimes = /** @type {const} */ (['nodejs20.x', 'nodejs22.x', 'bun1.x', 'edge']);
140+
141+
/**
142+
* @param {string} key
143+
* @returns {asserts key is RuntimeKey}
144+
*/
145+
function assert_is_valid_runtime(key) {
146+
if (!valid_runtimes.includes(/** @type {RuntimeKey} */ (key))) {
147+
throw new Error(
148+
`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.`
149+
);
150+
}
151+
}
152+
153+
/** @typedef {Exclude<RuntimeKey, 'bun1.x'> | 'experimental_bun1.x'} RuntimeConfigKey */
154+
/** @typedef {typeof valid_runtimes[number]} RuntimeKey */

0 commit comments

Comments
 (0)