Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/deep-parks-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/adapter-vercel': patch
---

chore: improve runtime config parsing
3 changes: 2 additions & 1 deletion packages/adapter-vercel/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Adapter } from '@sveltejs/kit';
import './ambient.js';
import { RuntimeConfigKey } from './utils.js';

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

Expand All @@ -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<RuntimeConfigKey, 'edge'>;
/**
* To which regions to deploy the app. A list of regions.
* More info: https://vercel.com/docs/concepts/edge-network/regions
Expand Down
53 changes: 6 additions & 47 deletions packages/adapter-vercel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -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'];

Expand Down Expand Up @@ -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) {
Expand All @@ -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')`
);
Expand Down Expand Up @@ -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 }),
[]
);
}
Expand Down
18 changes: 17 additions & 1 deletion packages/adapter-vercel/test/utils.spec.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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/);
});
});
46 changes: 46 additions & 0 deletions packages/adapter-vercel/utils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import process from 'node:process';

/** @param {import("@sveltejs/kit").RouteDefinition<any>} route */
export function get_pathname(route) {
let i = 1;
Expand Down Expand Up @@ -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<RuntimeKey, 'bun1.x'> | 'experimental_bun1.x'} RuntimeConfigKey */
/** @typedef {typeof valid_runtimes[number]} RuntimeKey */