diff --git a/.changeset/ten-worms-shine.md b/.changeset/ten-worms-shine.md new file mode 100644 index 000000000000..af24fd3da5aa --- /dev/null +++ b/.changeset/ten-worms-shine.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-vercel': major +--- + +breaking: remove support for edge and Node 20 runtimes diff --git a/documentation/docs/25-build-and-deploy/90-adapter-vercel.md b/documentation/docs/25-build-and-deploy/90-adapter-vercel.md index 29ba141a2535..eff04f2c9cfb 100644 --- a/documentation/docs/25-build-and-deploy/90-adapter-vercel.md +++ b/documentation/docs/25-build-and-deploy/90-adapter-vercel.md @@ -42,17 +42,11 @@ export const config = { }; ``` -The following options apply to all functions: +You can set the following options: -- `runtime`: `'edge'`, `'nodejs20.x'` or `'nodejs22.x'`. By default, the adapter will select the `'nodejs.x'` corresponding to the Node version your project is configured to use on the Vercel dashboard - > [!NOTE] This option is deprecated and will be removed in a future version, at which point all your functions will use whichever Node version is specified in the project configuration on Vercel -- `regions`: an array of [edge network regions](https://vercel.com/docs/concepts/edge-network/regions) (defaulting to `["iad1"]` for serverless functions) or `'all'` if `runtime` is `edge` (its default). Note that multiple regions for serverless functions are only supported on Enterprise plans +- `runtime`: `'nodejs22.x'`, `'nodejs24.x'` or `bun1.x`. By default, the adapter will select the runtime used for the build, which corresponds to the Node version your project is configured to use on the Vercel dashboard unless you [build the app with Bun](https://bun.com/docs/guides/ecosystem/vite) +- `regions`: an array of [edge network regions](https://vercel.com/docs/concepts/edge-network/regions) (defaulting to `["iad1"]`). Note that multiple regions for serverless functions are only supported on Enterprise plans - `split`: if `true`, causes a route to be deployed as an individual function. If `split` is set to `true` at the adapter level, all routes will be deployed as individual functions - -Additionally, the following option applies to edge functions: -- `external`: an array of dependencies that Rolldown should treat as external when bundling functions. This should only be used to exclude optional dependencies that will not run outside Node - -And the following option apply to serverless functions: - `memory`: the amount of memory available to the function. Defaults to `1024` Mb, and can be decreased to `128` Mb or [increased](https://vercel.com/docs/concepts/limits/overview#serverless-function-memory) in 64Mb increments up to `3008` Mb on Pro or Enterprise accounts - `maxDuration`: [maximum execution duration](https://vercel.com/docs/functions/runtimes#max-duration) of the function. Defaults to `10` seconds for Hobby accounts, `15` for Pro and `900` for Enterprise - `isr`: configuration Incremental Static Regeneration, described below @@ -209,12 +203,6 @@ Projects created before a certain date may default to using an older Node versio ### Accessing the file system -You can't use `fs` in edge functions. - -You _can_ use it in serverless functions, but it won't work as expected, since files are not copied from your project into your deployment. Instead, use the [`read`]($app-server#read) function from `$app/server` to access your files. It also works inside routes deployed as edge functions by fetching the file from the deployed public assets location. +Using `node:fs` directly in serverless functions most likely won't work as you expect, since files are not copied from your project into your deployment. Instead, use the [`read`]($app-server#read) function from `$app/server` to access your files. Alternatively, you can [prerender](page-options#prerender) the routes in question. - -### Deployment protection - -If using [`read`]($app-server#read) in an edge function, SvelteKit will `fetch` the file in question from your deployment. If you are using [Deployment Protection](https://vercel.com/docs/deployment-protection), you must also enable [Protection Bypass for Automation](https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection/protection-bypass-automation) so that the request does not result in a [401 Unauthorized](https://http.dog/401) response. diff --git a/packages/adapter-vercel/ambient.d.ts b/packages/adapter-vercel/ambient.d.ts deleted file mode 100644 index 67464d3365d9..000000000000 --- a/packages/adapter-vercel/ambient.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { RequestContext } from './index.js'; - -declare global { - namespace App { - export interface Platform { - /** - * `context` is only available in Edge Functions - * - * @deprecated Vercel's context is deprecated. Use [`@vercel/functions`](https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package) instead. - */ - context?: RequestContext; - } - } -} diff --git a/packages/adapter-vercel/files/edge.js b/packages/adapter-vercel/files/edge.js deleted file mode 100644 index 70707543b2ce..000000000000 --- a/packages/adapter-vercel/files/edge.js +++ /dev/null @@ -1,70 +0,0 @@ -/* eslint-disable n/prefer-global/process -- - Vercel Edge Runtime does not support node:process */ -import { Server } from 'SERVER'; -import { manifest } from 'MANIFEST'; - -const server = new Server(manifest); - -/** @type {HeadersInit | undefined} */ -let read_headers; -if (process.env.VERCEL_AUTOMATION_BYPASS_SECRET) { - read_headers = { - 'x-vercel-protection-bypass': process.env.VERCEL_AUTOMATION_BYPASS_SECRET - }; -} - -/** - * We don't know the origin until we receive a request, but - * that's guaranteed to happen before we call `read` - * @type {string} - */ -let origin; - -const initialized = server.init({ - env: /** @type {Record} */ (process.env), - read: async (file) => { - const url = `${origin}/${file}`; - const response = await fetch(url, { - // we need to add a bypass header if the user has deployment protection enabled - // see https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection/protection-bypass-automation - headers: read_headers - }); - - if (!response.ok) { - if (response.status === 401) { - throw new Error( - `Please enable Protection Bypass for Automation: https://svelte.dev/docs/kit/adapter-vercel#Troubleshooting-Deployment-protection` - ); - } - - // belt and braces — not sure how we could end up here - throw new Error( - `read(...) failed: could not fetch ${url} (${response.status} ${response.statusText})` - ); - } - - return response.body; - } -}); - -/** - * @param {Request} request - * @param {import('../index.js').RequestContext} context - */ -export default async (request, context) => { - if (!origin) { - origin = new URL(request.url).origin; - } - - // always await initialization to prevent race condition with concurrent requests - await initialized; - - return server.respond(request, { - getClientAddress() { - return /** @type {string} */ (request.headers.get('x-forwarded-for')); - }, - platform: { - context - } - }); -}; diff --git a/packages/adapter-vercel/index.d.ts b/packages/adapter-vercel/index.d.ts index e3333d04a88e..8cbdafd1b070 100644 --- a/packages/adapter-vercel/index.d.ts +++ b/packages/adapter-vercel/index.d.ts @@ -1,28 +1,25 @@ import { Adapter } from '@sveltejs/kit'; -import './ambient.js'; -import { RuntimeConfigKey } from './utils.js'; +import { RuntimeKey } from './utils.js'; export default function plugin(config?: Config): Adapter; 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) (`'nodejs22.x'`, `'nodejs24.x'` etc). + * Which [Serverless Function](https://vercel.com/docs/concepts/functions/serverless-functions) runtime to use (`'nodejs22.x'`, `'nodejs24.x'` etc). * @default Same as the build environment */ - runtime?: Exclude; + runtime?: RuntimeKey; /** - * To which regions to deploy the app. A list of regions. + * A list of regions to deploy the app to * More info: https://vercel.com/docs/concepts/edge-network/regions */ regions?: string[]; /** * Maximum execution duration (in seconds) that will be allowed for the Serverless Function. - * Serverless only. */ maxDuration?: number; /** * Amount of memory (RAM in MB) that will be allocated to the Serverless Function. - * Serverless only. */ memory?: number; /** @@ -32,7 +29,6 @@ export interface ServerlessConfig { /** * [Incremental Static Regeneration](https://vercel.com/docs/concepts/incremental-static-regeneration/overview) configuration. - * Serverless only. */ isr?: | { @@ -75,89 +71,9 @@ type ImagesConfig = { contentDispositionType?: string; }; -/** @deprecated */ -export interface EdgeConfig { - /** - * 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) (`'nodejs22.x'`, `'nodejs24.x'` etc). - */ - runtime?: 'edge'; - /** - * To which regions to deploy the app. A list of regions or `'all'`. - * More info: https://vercel.com/docs/concepts/edge-network/regions - */ - regions?: string[] | 'all'; - /** - * List of packages that should not be bundled into the Edge Function. - * Edge only. - */ - external?: string[]; - /** - * If `true`, this route will always be deployed as its own separate function - */ - split?: boolean; -} - -export type Config = (EdgeConfig | ServerlessConfig) & { +export type Config = ServerlessConfig & { /** * https://vercel.com/docs/build-output-api/v3/configuration#images */ images?: ImagesConfig; }; - -// we copy the RequestContext interface from `@vercel/edge` because that package can't co-exist with `@types/node`. -// see https://github.com/sveltejs/kit/pull/9280#issuecomment-1452110035 - -/** - * An extension to the standard `Request` object that is passed to every Edge Function. - * - * @deprecated - use [`@vercel/functions`](https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package) instead. - * - * @example - * ```ts - * import type { RequestContext } from '@vercel/edge'; - * - * export default async function handler(request: Request, ctx: RequestContext): Promise { - * // ctx is the RequestContext - * } - * ``` - */ -export interface RequestContext { - /** - * A method that can be used to keep the function running after a response has been sent. - * This is useful when you have an async task that you want to keep running even after the - * response has been sent and the request has ended. - * - * @example - * - * Sending an internal error to an error tracking service - * - * ```ts - * import type { RequestContext } from '@vercel/edge'; - * - * export async function handleRequest(request: Request, ctx: RequestContext): Promise { - * try { - * return await myFunctionThatReturnsResponse(); - * } catch (e) { - * ctx.waitUntil((async () => { - * // report this error to your error tracking service - * await fetch('https://my-error-tracking-service.com', { - * method: 'POST', - * body: JSON.stringify({ - * stack: e.stack, - * message: e.message, - * name: e.name, - * url: request.url, - * }), - * }); - * })()); - * return new Response('Internal Server Error', { status: 500 }); - * } - * } - * ``` - */ - waitUntil( - /** - * A promise that will be kept alive until it resolves or rejects. - */ promise: Promise - ): void; -} diff --git a/packages/adapter-vercel/index.js b/packages/adapter-vercel/index.js index 763e6474945e..13875c7e8187 100644 --- a/packages/adapter-vercel/index.js +++ b/packages/adapter-vercel/index.js @@ -4,59 +4,21 @@ import process from 'node:process'; import { fileURLToPath } from 'node:url'; import { VERSION } from '@sveltejs/kit'; import { nodeFileTrace } from '@vercel/nft'; -import { build } from 'rolldown'; import { get_pathname, parse_isr_expiration, pattern_to_src, resolve_runtime } from './utils.js'; -const name = '@sveltejs/adapter-vercel'; const INTERNAL = '![-]'; // this name is guaranteed not to conflict with user routes -// https://vercel.com/docs/functions/edge-functions/edge-runtime#compatible-node.js-modules -const compatible_node_modules = ['async_hooks', 'events', 'buffer', 'assert', 'util']; - -/** @satisfies {import('rolldown').BuildOptions} */ -const rolldown_config = { - platform: 'browser', - resolve: { - conditionNames: [ - // Vercel's Edge runtime key https://runtime-keys.proposal.wintercg.org/#edge-light - 'edge-light', - // re-include these since they are included by default when no conditions are specified - 'import', - 'browser', - 'default' - ] - }, - external: [...compatible_node_modules, ...compatible_node_modules.map((id) => `node:${id}`)], - transform: { - // minimum Node.js version supported is v14.6.0 that is mapped to ES2019 - // https://edge-runtime.vercel.app/features/polyfills - // TODO verify the latest ES version the edge runtime supports - target: 'es2022' - }, - output: { - sourcemap: true, - banner: () => 'globalThis.global = globalThis;', - codeSplitting: false - } -}; - /** @type {import('./index.js').default} **/ const plugin = function (defaults = {}) { - if ('edge' in defaults) { - throw new Error("{ edge: true } has been removed in favour of { runtime: 'edge' }"); + // @ts-ignore TODO remove this in a future version + if ('edge' in defaults || defaults.runtime === 'edge') { + throw new Error('The `edge` runtime is no longer supported'); } return { - name, + name: '@sveltejs/adapter-vercel', /** @param {import('@sveltejs/kit').Builder} builder */ async adapt(builder) { - if (!builder.routes) { - throw new Error( - '@sveltejs/adapter-vercel >=2.x (possibly installed through @sveltejs/adapter-auto) requires @sveltejs/kit version 1.5 or higher. ' + - 'Either downgrade the adapter or upgrade @sveltejs/kit' - ); - } - const dir = '.vercel/output'; const tmp = builder.getBuildDirectory('vercel-tmp'); @@ -120,99 +82,6 @@ const plugin = function (defaults = {}) { } } - let warned = false; - - /** - * @param {string} name - * @param {import('./index.js').EdgeConfig} config - * @param {import('@sveltejs/kit').RouteDefinition[]} routes - */ - async function generate_edge_function(name, config, routes) { - if (!warned) { - warned = true; - builder.log.warn( - `The \`runtime: 'edge'\` option is deprecated, and will be removed in a future version of adapter-vercel` - ); - } - - const tmp = builder.getBuildDirectory(`vercel-tmp/${name}`); - const relativePath = path.posix.relative(tmp, builder.getServerDirectory()); - - builder.copy(`${files}/edge.js`, `${tmp}/edge.js`, { - replace: { - SERVER: `${relativePath}/index.js`, - MANIFEST: './manifest.js' - } - }); - - write( - `${tmp}/manifest.js`, - `export const manifest = ${builder.generateManifest({ relativePath, routes })};\n` - ); - - try { - const outdir = `${dirs.functions}/${name}.func`; - - const build_config = { - ...rolldown_config, - external: [...rolldown_config.external, ...(config.external || [])] - }; - - await Promise.all([ - build({ - ...build_config, - input: `${tmp}/edge.js`, - output: { - ...build_config.output, - file: `${outdir}/index.js` - } - }), - builder.hasServerInstrumentationFile() && - build({ - ...build_config, - input: `${builder.getServerDirectory()}/instrumentation.server.js`, - output: { - ...build_config.output, - file: `${outdir}/instrumentation.server.js` - } - }) - ]); - - if (builder.hasServerInstrumentationFile()) { - builder.instrument({ - entrypoint: `${outdir}/index.js`, - instrumentation: `${outdir}/instrumentation.server.js`, - module: { - generateText: generate_traced_edge_module - } - }); - } - } catch (err) { - throw new Error( - 'Bundling edge function with Rolldown failed' + - (err instanceof Error ? `: ${err.message}` : ''), - { cause: err } - ); - } - - write( - `${dirs.functions}/${name}.func/.vc-config.json`, - JSON.stringify( - { - runtime: config.runtime, - regions: config.regions, - entrypoint: 'index.js', - framework: { - slug: 'sveltekit', - version: VERSION - } - }, - null, - '\t' - ) - ); - } - /** @type {Map[] }>} */ const groups = new Map(); @@ -230,7 +99,12 @@ const plugin = function (defaults = {}) { // group routes by config for (const route of builder.routes) { + if (route.config.runtime === 'edge') { + throw new Error('The `edge` runtime is no longer supported'); + } + const runtime = resolve_runtime(defaults.runtime, route.config.runtime); + const config = { ...defaults, ...route.config, runtime }; if (is_prerendered(route)) { @@ -243,12 +117,6 @@ const plugin = function (defaults = {}) { if (config.isr) { const directory = path.relative('.', builder.config.kit.files.routes + route.id); - if (runtime === 'edge') { - throw new Error( - `${directory}: Routes using \`isr\` must use a Node.js or Bun runtime (for example 'nodejs24.x' or 'experimental_bun1.x')` - ); - } - if (config.isr.allowQuery?.includes('__pathname')) { throw new Error( `${directory}: \`__pathname\` is a reserved query parameter for \`isr.allowQuery\`` @@ -307,13 +175,10 @@ const plugin = function (defaults = {}) { const singular = groups.size === 1; for (const group of groups.values()) { - const generate_function = - group.config.runtime === 'edge' ? generate_edge_function : generate_serverless_function; - // generate one function for the group const name = singular ? `${INTERNAL}/catchall` : `${INTERNAL}/${group.i}`; - await generate_function( + await generate_serverless_function( name, /** @type {any} */ (group.config), /** @type {import('@sveltejs/kit').RouteDefinition[]} */ (group.routes) @@ -329,10 +194,8 @@ const plugin = function (defaults = {}) { // by SvelteKit rather than Vercel const runtime = resolve_runtime(defaults.runtime); - const generate_function = - runtime === 'edge' ? generate_edge_function : generate_serverless_function; - await generate_function( + await generate_serverless_function( `${INTERNAL}/catchall`, /** @type {any} */ ({ ...defaults, runtime }), [] @@ -446,15 +309,14 @@ const plugin = function (defaults = {}) { } if (builder.config.kit.router.resolution === 'server') { - // Create a separate edge function just for server-side route resolution. + // Create a separate serverless function just for server-side route resolution. // By omitting all routes we're ensuring it's small (the routes will still be available // to the route resolution, because it does not rely on the server routing manifest) - await generate_edge_function( + const runtime = resolve_runtime(defaults.runtime); + + await generate_serverless_function( `${builder.config.kit.appDir}/route`, - { - external: 'external' in defaults ? defaults.external : undefined, - runtime: 'edge' - }, + /** @type {any} */ ({ ...defaults, runtime }), [] ); @@ -480,11 +342,10 @@ const plugin = function (defaults = {}) { }; }; -/** @param {import('./index.js').EdgeConfig & import('./index.js').ServerlessConfig} config */ +/** @param {import('./index.js').ServerlessConfig} config */ function hash_config(config) { return [ config.runtime ?? '', - config.external ?? '', config.regions ?? '', config.memory ?? '', config.maxDuration ?? '', @@ -821,23 +682,4 @@ function is_prerendered(route) { ); } -/** - * @param {{ instrumentation: string; start: string }} opts - */ -function generate_traced_edge_module({ instrumentation, start }) { - return `\ -import './${instrumentation}'; -const promise = import('./${start}'); - -/** - * @param {import('http').IncomingMessage} req - * @param {import('http').ServerResponse} res - */ -export default async (req, res) => { - const { default: handler } = await promise; - return handler(req, res); -} -`; -} - export default plugin; diff --git a/packages/adapter-vercel/package.json b/packages/adapter-vercel/package.json index 39285ae6e377..01c762590bd2 100644 --- a/packages/adapter-vercel/package.json +++ b/packages/adapter-vercel/package.json @@ -30,8 +30,7 @@ "files", "index.js", "utils.js", - "index.d.ts", - "ambient.d.ts" + "index.d.ts" ], "scripts": { "lint": "prettier --check .", @@ -42,8 +41,7 @@ "test": "pnpm test:unit && pnpm test:build" }, "dependencies": { - "@vercel/nft": "^1.3.2", - "rolldown": "^1.2.0" + "@vercel/nft": "^1.3.2" }, "devDependencies": { "@sveltejs/kit": "workspace:^", diff --git a/packages/adapter-vercel/test/utils.spec.js b/packages/adapter-vercel/test/utils.spec.js index 2ee4ca2030c0..537930079a65 100644 --- a/packages/adapter-vercel/test/utils.spec.js +++ b/packages/adapter-vercel/test/utils.spec.js @@ -176,12 +176,12 @@ describe('parse_isr_expiration', () => { describe('resolve_runtime', () => { test('prefers override_key over default_key', () => { - const result = resolve_runtime('nodejs20.x', 'experimental_bun1.x'); + const result = resolve_runtime('nodejs20.x', 'bun1.x'); assert.equal(result, 'bun1.x'); }); test('uses default_key when override_key is undefined', () => { - const result = resolve_runtime('experimental_bun1.x'); + const result = resolve_runtime('bun1.x'); assert.equal(result, 'bun1.x'); }); diff --git a/packages/adapter-vercel/utils.js b/packages/adapter-vercel/utils.js index d35ceea65198..e6570fd0e0fd 100644 --- a/packages/adapter-vercel/utils.js +++ b/packages/adapter-vercel/utils.js @@ -113,37 +113,47 @@ export function parse_isr_expiration(value, route_id) { * @returns {RuntimeKey} */ export function resolve_runtime(default_key, override_key) { - const key = (override_key ?? default_key ?? get_default_runtime()).replace('experimental_', ''); + const key = override_key ?? default_key ?? get_default_runtime(); assert_is_valid_runtime(key); return key; } -const valid_node_versions = [20, 22, 24]; -const formatter = new Intl.ListFormat('en', { type: 'disjunction' }); +const valid_node_versions = [22, 24]; +const formatter = new Intl.ListFormat('en-gb', { type: 'disjunction' }); /** @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 the user ran e.g. `bunx --bun vite build`, infer that they want to run the app in Bun + if (process.versions.bun) { + const major = process.versions.bun.split('.')[0]; + if (major !== '1') { + throw new Error( + `Unsupported Bun version: ${major}. Please use Bun 1.x to build your project, or explicitly specify a runtime in your adapter configuration.` + ); + } - if (!valid_node_versions.includes(major)) { - throw new Error( - `Unsupported Node.js version: ${process.version}. Please use Node ${formatter.format(valid_node_versions.map((v) => `${v}`))} to build your project, or explicitly specify a runtime in your adapter configuration.` - ); + return `bun${major}.x`; + } + + // otherwise, default to the version of Node specified in the project config + if (process.versions.node) { + const major = Number(process.versions.node.split('.')[0]); + + if (!valid_node_versions.includes(major)) { + throw new Error( + `Unsupported Node.js version: ${process.version}. Please use Node ${formatter.format(valid_node_versions.map((v) => `${v}`))} to build your project, or explicitly specify a runtime in your adapter configuration.` + ); + } + + return `nodejs${/** @type {22 | 24} */ (major)}.x`; } - return `nodejs${/** @type {20 | 22 | 24} */ (major)}.x`; + throw new Error( + 'Could not auto-detect a runtime. Please explicitly specify a runtime in your adapter configuration.' + ); } -const valid_runtimes = /** @type {const} */ ([ - 'nodejs20.x', - 'nodejs22.x', - 'nodejs24.x', - 'bun1.x', - 'edge' -]); +const valid_runtimes = /** @type {const} */ (['nodejs22.x', 'nodejs24.x', 'bun1.x']); /** * @param {string} key @@ -157,5 +167,4 @@ function assert_is_valid_runtime(key) { } } -/** @typedef {Exclude | 'experimental_bun1.x'} RuntimeConfigKey */ /** @typedef {typeof valid_runtimes[number]} RuntimeKey */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a514a18187b0..2fc6308fe7ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -416,9 +416,6 @@ importers: '@vercel/nft': specifier: ^1.3.2 version: 1.3.2(supports-color@10.2.2) - rolldown: - specifier: ^1.2.0 - version: 1.2.0 devDependencies: '@sveltejs/kit': specifier: workspace:^