diff --git a/.changeset/slimy-tips-shake.md b/.changeset/slimy-tips-shake.md new file mode 100644 index 000000000000..0941c0cf62cd --- /dev/null +++ b/.changeset/slimy-tips-shake.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-vercel': minor +--- + +feat: expose `waitUntil` also for serverless runtime 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 3251ef137b3b..255e87dabea4 100644 --- a/documentation/docs/25-build-and-deploy/90-adapter-vercel.md +++ b/documentation/docs/25-build-and-deploy/90-adapter-vercel.md @@ -177,6 +177,12 @@ Cookie-based skew protection comes with one caveat: if a user has multiple versi ## Notes +### Platform-specific context + +The [`event.platform`](https://svelte.dev/docs/kit/adapters#Platform-specific-context) property is available in server load functions and endpoints and exposes the [`waitUntil`](https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package#waituntil) function. + +Use the package [`@vercel/functions`](https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package) for more utilities. + ### Vercel functions If you have Vercel functions contained in the `api` directory at the project's root, any requests for `/api/*` will _not_ be handled by SvelteKit. You should implement these as [API routes](routing#server) in your SvelteKit app instead, unless you need to use a non-JavaScript language in which case you will need to ensure that you don't have any `/api/*` routes in your SvelteKit app. diff --git a/packages/adapter-vercel/ambient.d.ts b/packages/adapter-vercel/ambient.d.ts index a106f64e3f12..f76bb8b60a83 100644 --- a/packages/adapter-vercel/ambient.d.ts +++ b/packages/adapter-vercel/ambient.d.ts @@ -1,12 +1,53 @@ -import { RequestContext } from './index.js'; +import type { waitUntil } from '@vercel/functions'; declare global { namespace App { export interface Platform { /** * `context` is only available in Edge Functions + * + * @deprecated Vercel's context is deprecated. Use top-level properties instead. */ - context?: RequestContext; + context?: { + /** + * `context` is only available in Edge Functions + * + * @deprecated Use `event.platform.waitUntil` instead. + */ + waitUntil: typeof waitUntil; + }; + + /** + * 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. + * + * The maximum duration depends on your plan and settings (see [here](https://vercel.com/docs/functions/limitations)). + * + * See https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package#waituntil. + * + * @example + * + * Perform a long-running task in the background without blocking the sending of the response + * + * ```ts + * // src/routes/+page.server.ts + * + * export const load = async (event) => { + * event.platform.waitUntil(longRunningTask()) + * + * return { + * // ...some data + * } + * } + * + * async function longRunningTask() { + * // ... + * } + * ``` + */ + waitUntil: typeof waitUntil; } } } diff --git a/packages/adapter-vercel/files/edge.js b/packages/adapter-vercel/files/edge.js index f87bb6e64a98..bbdbf1986ab0 100644 --- a/packages/adapter-vercel/files/edge.js +++ b/packages/adapter-vercel/files/edge.js @@ -2,6 +2,7 @@ Vercel Edge Runtime does not support node:process */ import { Server } from 'SERVER'; import { manifest } from 'MANIFEST'; +import { waitUntil } from '@vercel/functions'; const server = new Server(manifest); @@ -49,9 +50,8 @@ const initialized = server.init({ /** * @param {Request} request - * @param {import('../index.js').RequestContext} context */ -export default async (request, context) => { +export default async (request) => { if (!origin) { origin = new URL(request.url).origin; await initialized; @@ -62,7 +62,10 @@ export default async (request, context) => { return /** @type {string} */ (request.headers.get('x-forwarded-for')); }, platform: { - context + context: { + waitUntil + }, + waitUntil } }); }; diff --git a/packages/adapter-vercel/files/serverless.js b/packages/adapter-vercel/files/serverless.js index ea16823574f0..18d4e3a4d0d8 100644 --- a/packages/adapter-vercel/files/serverless.js +++ b/packages/adapter-vercel/files/serverless.js @@ -3,6 +3,7 @@ import { createReadableStream } from '@sveltejs/kit/node'; import { Server } from 'SERVER'; import { manifest } from 'MANIFEST'; import process from 'node:process'; +import { waitUntil } from '@vercel/functions'; installPolyfills(); @@ -39,6 +40,12 @@ export default { return server.respond(request, { getClientAddress() { return /** @type {string} */ (request.headers.get('x-forwarded-for')); + }, + platform: { + context: { + waitUntil + }, + waitUntil } }); } diff --git a/packages/adapter-vercel/index.d.ts b/packages/adapter-vercel/index.d.ts index 49d43085763c..823e14d8f378 100644 --- a/packages/adapter-vercel/index.d.ts +++ b/packages/adapter-vercel/index.d.ts @@ -103,59 +103,3 @@ export type Config = (EdgeConfig | ServerlessConfig) & { */ 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. - * - * @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/package.json b/packages/adapter-vercel/package.json index f2de210def6c..916fe4a9fbc8 100644 --- a/packages/adapter-vercel/package.json +++ b/packages/adapter-vercel/package.json @@ -40,6 +40,7 @@ "test": "vitest run" }, "dependencies": { + "@vercel/functions": "^3.1.3", "@vercel/nft": "^0.30.0", "esbuild": "^0.25.4" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74004538fd1d..cccdca992145 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -434,6 +434,9 @@ importers: packages/adapter-vercel: dependencies: + '@vercel/functions': + specifier: ^3.1.3 + version: 3.1.3 '@vercel/nft': specifier: ^0.30.0 version: 0.30.0(rollup@4.50.1) @@ -3216,6 +3219,15 @@ packages: resolution: {integrity: sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vercel/functions@3.1.3': + resolution: {integrity: sha512-dGYDSOfzjr5ZLfQ2bb4f+2NySsTLX4d6xnUr2Ckc5q/TrPTSJPLjzRzYLQAYO3/aeOv7evSYvPhijURmocu0UQ==} + engines: {node: '>= 20'} + peerDependencies: + '@aws-sdk/credential-provider-web-identity': '*' + peerDependenciesMeta: + '@aws-sdk/credential-provider-web-identity': + optional: true + '@vercel/nft@0.29.4': resolution: {integrity: sha512-6lLqMNX3TuycBPABycx7A9F1bHQR7kiQln6abjFbPrf5C/05qHM9M5E4PeTE59c7z8g6vHnx1Ioihb2AQl7BTA==} engines: {node: '>=18'} @@ -3226,6 +3238,10 @@ packages: engines: {node: '>=18'} hasBin: true + '@vercel/oidc@3.0.2': + resolution: {integrity: sha512-JekxQ0RApo4gS4un/iMGsIL1/k4KUBe3HmnGcDvzHuFBdQdudEJgTqcsJC7y6Ul4Yw5CeykgvQbX2XeEJd0+DA==} + engines: {node: '>= 20'} + '@vitest/browser@3.2.4': resolution: {integrity: sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==} peerDependencies: @@ -9108,6 +9124,10 @@ snapshots: '@typescript-eslint/types': 8.43.0 eslint-visitor-keys: 4.2.1 + '@vercel/functions@3.1.3': + dependencies: + '@vercel/oidc': 3.0.2 + '@vercel/nft@0.29.4(rollup@4.50.1)(supports-color@10.0.0)': dependencies: '@mapbox/node-pre-gyp': 2.0.0(supports-color@10.0.0) @@ -9146,6 +9166,8 @@ snapshots: - rollup - supports-color + '@vercel/oidc@3.0.2': {} + '@vitest/browser@3.2.4(playwright@1.51.1)(vite@6.3.6(@types/node@18.19.119)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0))(vitest@3.2.4)': dependencies: '@testing-library/dom': 10.4.1