@ripple-ts/adapter-vercel 0.2.214

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # @ripple-ts/adapter-vercel
2
+
3
+ ## 0.2.214
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies []:
8
+ - @ripple-ts/adapter@0.2.214
9
+ - @ripple-ts/adapter-node@0.2.214
10
+
11
+ ## 0.2.213
12
+
13
+ ### Minor Changes
14
+
15
+ - Initial release of the Vercel adapter for the Ripple metaframework.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Dominic Gannaway
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,168 @@
1
+ # @ripple-ts/adapter-vercel
2
+
3
+ Vercel adapter for the Ripple metaframework.
4
+
5
+ Deploys your Ripple SSR application to [Vercel](https://vercel.com) using the
6
+ [Build Output API v3](https://vercel.com/docs/build-output-api/v3).
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ pnpm add @ripple-ts/adapter-vercel
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ### ripple.config.ts
17
+
18
+ ```typescript
19
+ import { defineConfig } from '@ripple-ts/vite-plugin';
20
+ import { serve, runtime } from '@ripple-ts/adapter-vercel';
21
+ import { routes } from './src/routes.ts';
22
+
23
+ export default defineConfig({
24
+ adapter: { serve, runtime },
25
+ router: { routes },
26
+ });
27
+ ```
28
+
29
+ ### Build & Deploy
30
+
31
+ The adapter generates Vercel's
32
+ [Build Output API v3](https://vercel.com/docs/build-output-api/v3) structure.
33
+
34
+ **Option 1: Post-build CLI**
35
+
36
+ ```bash
37
+ pnpm vite build && ripple-adapt-vercel
38
+ ```
39
+
40
+ Or add to your package.json:
41
+
42
+ ```json
43
+ {
44
+ "scripts": {
45
+ "build": "vite build",
46
+ "vercel-build": "vite build && ripple-adapt-vercel"
47
+ }
48
+ }
49
+ ```
50
+
51
+ **Option 2: Use `adapt()` programmatically**
52
+
53
+ ```javascript
54
+ import { adapt } from '@ripple-ts/adapter-vercel';
55
+
56
+ await adapt({
57
+ outDir: 'dist',
58
+ // options...
59
+ });
60
+ ```
61
+
62
+ ### Options
63
+
64
+ | Option | Type | Default | Description |
65
+ | ------------------------ | -------------------- | ------------- | ------------------------------------------------------------------------ |
66
+ | `outDir` | `string` | `'dist'` | Build output directory (from vite build) |
67
+ | `serverless` | `ServerlessConfig` | `{}` | Serverless function configuration |
68
+ | `serverless.runtime` | `string` | auto-detected | Node.js runtime version (`'nodejs20.x'`, `'nodejs22.x'`, `'nodejs24.x'`) |
69
+ | `serverless.regions` | `string[]` | `undefined` | Deployment regions |
70
+ | `serverless.memory` | `number` | `undefined` | Memory (MB) allocated to the function |
71
+ | `serverless.maxDuration` | `number` | `undefined` | Maximum execution time (seconds) |
72
+ | `isr` | `ISRConfig \| false` | `false` | Incremental Static Regeneration config |
73
+ | `isr.expiration` | `number \| false` | — | Seconds before background revalidation (`false` = never expire) |
74
+ | `isr.bypassToken` | `string` | `undefined` | Token to bypass the ISR cache |
75
+ | `isr.allowQuery` | `string[]` | `undefined` | Query params included in the cache key (empty = ignore query) |
76
+ | `images` | `ImagesConfig` | `undefined` | Vercel Image Optimization config |
77
+ | `headers` | `VercelHeader[]` | `[]` | Custom response headers |
78
+ | `redirects` | `VercelRedirect[]` | `[]` | Custom redirects |
79
+ | `rewrites` | `VercelRewrite[]` | `[]` | Additional rewrites (prepended before catch-all) |
80
+ | `cleanUrls` | `boolean` | `true` | Remove `.html` extensions from URLs |
81
+ | `trailingSlash` | `boolean` | `undefined` | Enforce trailing slash behavior |
82
+
83
+ ## How It Works
84
+
85
+ 1. **`vite build`** produces `dist/client/` (static assets) and
86
+ `dist/server/entry.js` (server bundle)
87
+ 2. **`adapt()`** restructures the output into `.vercel/output/`:
88
+ - Copies `dist/client/` → `.vercel/output/static/`
89
+ - Creates a serverless function at `.vercel/output/functions/index.func/`
90
+ - Generates `.vercel/output/config.json` with routing rules
91
+ - Uses `@vercel/nft` to trace and bundle server dependencies
92
+
93
+ The generated structure:
94
+
95
+ ```
96
+ .vercel/output/
97
+ ├── config.json # Build Output API v3 config
98
+ ├── static/ # Static files (served by Vercel CDN)
99
+ │ ├── assets/
100
+ │ ├── index.html
101
+ │ └── ...
102
+ └── functions/
103
+ └── index.func/ # Serverless function
104
+ ├── index.js # Handler entry point
105
+ ├── .vc-config.json # Function configuration
106
+ ├── package.json # ESM marker
107
+ └── ... (traced deps)
108
+ ```
109
+
110
+ ## Local Development
111
+
112
+ The adapter re-exports `serve` and `runtime` from `@ripple-ts/adapter-node`, so
113
+ local development with `vite dev` and `ripple-preview` works exactly the same as
114
+ with adapter-node.
115
+
116
+ ## Incremental Static Regeneration (ISR)
117
+
118
+ Enable ISR to let Vercel cache serverless responses at the edge and revalidate
119
+ them in the background:
120
+
121
+ ```javascript
122
+ await adapt({
123
+ isr: {
124
+ // Re-generate cached pages every 60 seconds
125
+ expiration: 60,
126
+ },
127
+ });
128
+ ```
129
+
130
+ **Never-expiring cache** (only revalidated via on-demand revalidation):
131
+
132
+ ```javascript
133
+ await adapt({
134
+ isr: {
135
+ expiration: false,
136
+ bypassToken: process.env.REVALIDATION_TOKEN,
137
+ },
138
+ });
139
+ ```
140
+
141
+ **Cache key control** — only include specific query parameters:
142
+
143
+ ```javascript
144
+ await adapt({
145
+ isr: {
146
+ expiration: 300,
147
+ allowQuery: ['page', 'q'], // /search?q=foo and /search?q=bar are cached separately
148
+ },
149
+ });
150
+ ```
151
+
152
+ The ISR config is emitted as a `prerender` field in the function's
153
+ `.vc-config.json`, following Vercel's
154
+ [Build Output API v3 prerender configuration](https://vercel.com/docs/build-output-api/v3/configuration#prerender-configuration).
155
+
156
+ ## Vercel Configuration
157
+
158
+ No `vercel.json` configuration is needed — the adapter generates all necessary
159
+ routing rules via the Build Output API.
160
+
161
+ If you do have a `vercel.json`, the adapter respects your `buildCommand` and
162
+ `outputDirectory` settings. Set your build command to run the adapter:
163
+
164
+ ```json
165
+ {
166
+ "buildCommand": "pnpm vite build && pnpm ripple-adapt-vercel"
167
+ }
168
+ ```
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@ripple-ts/adapter-vercel",
3
+ "description": "Vercel adapter for Ripple metaframework (Build Output API v3)",
4
+ "license": "MIT",
5
+ "author": "Dominic Gannaway",
6
+ "version": "0.2.214",
7
+ "type": "module",
8
+ "module": "src/index.js",
9
+ "main": "src/index.js",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./types/index.d.ts",
13
+ "import": "./src/index.js",
14
+ "default": "./src/index.js"
15
+ }
16
+ },
17
+ "bin": {
18
+ "ripple-adapt-vercel": "./src/bin/adapt.js"
19
+ },
20
+ "dependencies": {
21
+ "@vercel/nft": "^1.0.0",
22
+ "@ripple-ts/adapter": "0.2.214",
23
+ "@ripple-ts/adapter-node": "0.2.214"
24
+ },
25
+ "homepage": "https://ripple-ts.com",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/Ripple-TS/ripple.git",
29
+ "directory": "packages/adapter-vercel"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/Ripple-TS/ripple/issues"
33
+ },
34
+ "scripts": {
35
+ "test": "pnpm -w test --project adapter-vercel"
36
+ }
37
+ }
package/src/adapt.js ADDED
@@ -0,0 +1,453 @@
1
+ /**
2
+ * Build output generator for Vercel's Build Output API v3.
3
+ *
4
+ * Takes the Ripple build output (dist/client + dist/server) and restructures
5
+ * it into `.vercel/output/` with proper routing, function config, and
6
+ * dependency tracing.
7
+ *
8
+ * Modeled after @sveltejs/adapter-vercel but adapted for the Ripple
9
+ * metaframework's architecture.
10
+ */
11
+
12
+ /**
13
+ @import {
14
+ AdaptOptions,
15
+ VercelRoute,
16
+ VercelConfig,
17
+ } from '@ripple-ts/adapter-vercel';
18
+ */
19
+
20
+ import {
21
+ existsSync,
22
+ mkdirSync,
23
+ cpSync,
24
+ writeFileSync,
25
+ rmSync,
26
+ copyFileSync,
27
+ statSync,
28
+ symlinkSync,
29
+ realpathSync,
30
+ } from 'node:fs';
31
+ import { resolve, join, dirname, relative, sep } from 'node:path';
32
+ import { createRequire } from 'node:module';
33
+ import { nodeFileTrace } from '@vercel/nft';
34
+
35
+ const require = createRequire(import.meta.url);
36
+ const { version: ADAPTER_VERSION } = require('../package.json');
37
+
38
+ // ============================================================================
39
+ // Constants
40
+ // ============================================================================
41
+
42
+ const VERCEL_OUTPUT_DIR = '.vercel/output';
43
+
44
+ /**
45
+ * Detect the default Node.js runtime version for the current environment.
46
+ *
47
+ * @returns {string}
48
+ */
49
+ function get_default_runtime() {
50
+ const major = Number(process.version.slice(1).split('.')[0]);
51
+ const valid = [20, 22, 24];
52
+
53
+ if (!valid.includes(major)) {
54
+ throw new Error(
55
+ `Unsupported Node.js version: ${process.version}. ` +
56
+ `Please use Node ${valid.join(' or ')} to build your project, ` +
57
+ `or explicitly specify a runtime in adapter options.`,
58
+ );
59
+ }
60
+
61
+ return `nodejs${major}.x`;
62
+ }
63
+
64
+ // ============================================================================
65
+ // File utilities
66
+ // ============================================================================
67
+
68
+ /**
69
+ * Write a file, creating parent directories as needed.
70
+ *
71
+ * @param {string} file_path
72
+ * @param {string} data
73
+ */
74
+ function write(file_path, data) {
75
+ mkdirSync(dirname(file_path), { recursive: true });
76
+ writeFileSync(file_path, data);
77
+ }
78
+
79
+ /**
80
+ * Copy a directory recursively, creating the destination if needed.
81
+ *
82
+ * @param {string} src
83
+ * @param {string} dest
84
+ */
85
+ function copy_dir(src, dest) {
86
+ mkdirSync(dest, { recursive: true });
87
+ cpSync(src, dest, { recursive: true });
88
+ }
89
+
90
+ // ============================================================================
91
+ // Dependency tracing
92
+ // ============================================================================
93
+
94
+ /**
95
+ * @typedef {Object} TraceResult
96
+ * @property {string} entry_path - Path to the traced entry file inside func_dir
97
+ * (relative to func_dir, e.g. "dist/server/entry.js")
98
+ */
99
+
100
+ /**
101
+ * Trace dependencies using @vercel/nft and copy them into the function directory.
102
+ *
103
+ * This ensures the serverless function bundle contains exactly the files it needs
104
+ * at runtime, keeping cold start times minimal.
105
+ *
106
+ * Uses the project root as the nft `base` so that traced file paths are
107
+ * project-relative. Files are copied into `func_dir` preserving their
108
+ * project-relative structure, which keeps import paths correct.
109
+ *
110
+ * @param {string} entry - Absolute path to the entry file
111
+ * @param {string} func_dir - Absolute path to the function output directory
112
+ * @param {string} project_root - Absolute path to the project root
113
+ * @returns {Promise<TraceResult>}
114
+ */
115
+ async function trace_and_copy_dependencies(entry, func_dir, project_root) {
116
+ // Use project root as the base for nft so paths are project-relative
117
+ const base = project_root.endsWith(sep) ? project_root : project_root + sep;
118
+
119
+ const traced = await nodeFileTrace([entry], { base: project_root });
120
+
121
+ // Log non-fatal tracing warnings
122
+ for (const warning of traced.warnings) {
123
+ if (warning.message.startsWith('Failed to resolve dependency node:')) continue;
124
+ if (warning.message.startsWith('Failed to parse')) continue;
125
+
126
+ if (warning.message.startsWith('Failed to resolve dependency')) {
127
+ console.warn(`[adapter-vercel] Warning: ${warning.message}`);
128
+ }
129
+ }
130
+
131
+ // Determine the entry file's project-relative path so the caller can
132
+ // derive a correct import path for the generated handler.
133
+ const entry_relative = relative(project_root, entry);
134
+
135
+ for (const file of traced.fileList) {
136
+ const source = join(project_root, file);
137
+ const dest = join(func_dir, file);
138
+
139
+ const stats = statSync(source);
140
+ const is_dir = stats.isDirectory();
141
+ const realpath = realpathSync(source);
142
+
143
+ mkdirSync(dirname(dest), { recursive: true });
144
+
145
+ if (source !== realpath) {
146
+ const real_relative = relative(project_root, realpath);
147
+ const realdest = join(func_dir, real_relative);
148
+ symlinkSync(relative(dirname(dest), realdest), dest, is_dir ? 'dir' : 'file');
149
+ } else if (!is_dir) {
150
+ copyFileSync(source, dest);
151
+ }
152
+ }
153
+
154
+ return { entry_path: entry_relative };
155
+ }
156
+
157
+ // ============================================================================
158
+ // Handler template generation
159
+ // ============================================================================
160
+
161
+ /**
162
+ * Generate the serverless function handler source code.
163
+ *
164
+ * Vercel's Node.js Serverless Functions invoke handlers with
165
+ * `(IncomingMessage, ServerResponse)`, but Ripple's production handler is
166
+ * `Request => Response`. The generated handler bridges these two worlds
167
+ * using adapter-node's conversion utilities.
168
+ *
169
+ * @param {string} server_entry_relative - Relative path from the function dir to
170
+ * the server entry file
171
+ * @returns {string}
172
+ */
173
+ function generate_handler_source(server_entry_relative) {
174
+ return `\
175
+ // Auto-generated by @ripple-ts/adapter-vercel
176
+ // Vercel Serverless Function handler for Ripple
177
+ //
178
+ // Bridges Vercel's Node.js (req, res) interface with Ripple's Web
179
+ // fetch-style handler (Request => Response).
180
+ import { handler } from ${JSON.stringify(server_entry_relative)};
181
+ import { nodeRequestToWebRequest, webResponseToNodeResponse } from '@ripple-ts/adapter-node';
182
+
183
+ export default async function (req, res) {
184
+ const controller = new AbortController();
185
+ req.on('close', () => controller.abort());
186
+
187
+ try {
188
+ const request = nodeRequestToWebRequest(req, controller.signal, true);
189
+ const response = await handler(request);
190
+ webResponseToNodeResponse(response, res, req.method ?? 'GET');
191
+ } catch (err) {
192
+ console.error('[ripple] Serverless handler error:', err);
193
+ if (!res.headersSent) {
194
+ res.statusCode = 500;
195
+ res.end('Internal Server Error');
196
+ }
197
+ }
198
+ };
199
+ `;
200
+ }
201
+
202
+ // ============================================================================
203
+ // Vercel config generation
204
+ // ============================================================================
205
+
206
+ /**
207
+ * Generate the Build Output API v3 config.json.
208
+ *
209
+ * @param {AdaptOptions} options
210
+ * @returns {VercelConfig}
211
+ */
212
+ function generate_vercel_config(options) {
213
+ const {
214
+ cleanUrls = true,
215
+ trailingSlash,
216
+ images,
217
+ headers = [],
218
+ redirects = [],
219
+ rewrites = [],
220
+ } = options;
221
+
222
+ /** @type {VercelRoute[]} */
223
+ const routes = [];
224
+
225
+ // User-defined redirects
226
+ for (const redirect of redirects) {
227
+ routes.push({
228
+ src: redirect.source,
229
+ headers: { Location: redirect.destination },
230
+ status: redirect.permanent ? 308 : 307,
231
+ });
232
+ }
233
+
234
+ // Immutable cache headers for hashed assets
235
+ routes.push({
236
+ src: '/assets/.+',
237
+ headers: {
238
+ 'Cache-Control': 'public, max-age=31536000, immutable',
239
+ },
240
+ continue: true,
241
+ });
242
+
243
+ // User-defined headers
244
+ for (const header of headers) {
245
+ routes.push({
246
+ src: header.source,
247
+ headers: Object.fromEntries(header.headers.map((h) => [h.key, h.value])),
248
+ continue: true,
249
+ });
250
+ }
251
+
252
+ // Let Vercel handle filesystem (static) routes first
253
+ routes.push({ handle: 'filesystem' });
254
+
255
+ // User-defined rewrites (inserted before the catch-all)
256
+ for (const rewrite of rewrites) {
257
+ routes.push({
258
+ src: rewrite.source,
259
+ dest: rewrite.destination,
260
+ });
261
+ }
262
+
263
+ // Catch-all: send everything else to the serverless function
264
+ routes.push({
265
+ src: '/.*',
266
+ dest: '/index',
267
+ });
268
+
269
+ /** @type {VercelConfig} */
270
+ const config = {
271
+ version: 3,
272
+ routes,
273
+ };
274
+
275
+ if (cleanUrls !== undefined) {
276
+ config.cleanUrls = cleanUrls;
277
+ }
278
+
279
+ if (trailingSlash !== undefined) {
280
+ config.trailingSlash = trailingSlash;
281
+ }
282
+
283
+ if (images) {
284
+ config.images = images;
285
+ }
286
+
287
+ return config;
288
+ }
289
+
290
+ // ============================================================================
291
+ // Main adapt function
292
+ // ============================================================================
293
+
294
+ /**
295
+ * Generate Vercel Build Output API v3 from a Ripple build.
296
+ *
297
+ * Transforms the standard Ripple build output (`dist/client` + `dist/server`)
298
+ * into `.vercel/output/` with:
299
+ * - Static files served from Vercel's CDN
300
+ * - A serverless function for SSR, API routes, and RPC
301
+ * - Routing rules for proper request handling
302
+ * - Dependency tracing via @vercel/nft for minimal bundle size
303
+ *
304
+ * @param {AdaptOptions} [options]
305
+ * @returns {Promise<void>}
306
+ */
307
+ export async function adapt(options = {}) {
308
+ const { outDir = 'dist', serverless = {}, isr = false } = options;
309
+
310
+ const project_root = process.cwd();
311
+ const build_dir = resolve(project_root, outDir);
312
+ const client_dir = join(build_dir, 'client');
313
+ const server_dir = join(build_dir, 'server');
314
+ const server_entry = join(server_dir, 'entry.js');
315
+ const output_dir = resolve(project_root, VERCEL_OUTPUT_DIR);
316
+
317
+ // ------------------------------------------------------------------
318
+ // Validate build output exists
319
+ // ------------------------------------------------------------------
320
+
321
+ if (!existsSync(client_dir)) {
322
+ throw new Error(
323
+ `[adapter-vercel] Client build output not found at ${client_dir}. ` +
324
+ `Run "vite build" before running the adapter.`,
325
+ );
326
+ }
327
+
328
+ if (!existsSync(server_entry)) {
329
+ throw new Error(
330
+ `[adapter-vercel] Server entry not found at ${server_entry}. ` +
331
+ `Make sure your project has a ripple.config.ts with an adapter configured.`,
332
+ );
333
+ }
334
+
335
+ // ------------------------------------------------------------------
336
+ // Clean and create output directory
337
+ // ------------------------------------------------------------------
338
+
339
+ console.log('[adapter-vercel] Generating Vercel Build Output...');
340
+
341
+ rmSync(output_dir, { recursive: true, force: true });
342
+ mkdirSync(output_dir, { recursive: true });
343
+
344
+ // ------------------------------------------------------------------
345
+ // 1. Copy static assets
346
+ // ------------------------------------------------------------------
347
+
348
+ const static_dir = join(output_dir, 'static');
349
+
350
+ console.log('[adapter-vercel] Copying static assets...');
351
+ copy_dir(client_dir, static_dir);
352
+
353
+ // Remove index.html from static output — SSR handles the root route.
354
+ // Vercel would serve the static index.html instead of the SSR function
355
+ // if we leave it in place.
356
+ const static_index_html = join(static_dir, 'index.html');
357
+ if (existsSync(static_index_html)) {
358
+ rmSync(static_index_html);
359
+ }
360
+
361
+ // ------------------------------------------------------------------
362
+ // 2. Create the serverless function
363
+ // ------------------------------------------------------------------
364
+
365
+ const func_dir = join(output_dir, 'functions', 'index.func');
366
+ mkdirSync(func_dir, { recursive: true });
367
+
368
+ console.log('[adapter-vercel] Tracing server dependencies...');
369
+
370
+ // Trace and copy all dependencies of the server entry.
371
+ // The trace result tells us the project-relative path where the entry
372
+ // was copied, which we need to derive a correct import.
373
+ const trace_result = await trace_and_copy_dependencies(server_entry, func_dir, project_root);
374
+
375
+ // Generate the handler that imports the server entry.
376
+ // The entry lives at func_dir/<entry_path>, and the handler at func_dir/index.js,
377
+ // so the import is relative from handler to entry.
378
+ const handler_path = join(func_dir, 'index.js');
379
+ const entry_in_func = join(func_dir, trace_result.entry_path);
380
+ const server_entry_relative = './' + relative(dirname(handler_path), entry_in_func);
381
+
382
+ write(handler_path, generate_handler_source(server_entry_relative));
383
+ write(join(func_dir, 'package.json'), JSON.stringify({ type: 'module' }));
384
+
385
+ // Function configuration
386
+ const runtime = serverless.runtime ?? get_default_runtime();
387
+
388
+ /** @type {Record<string, unknown>} */
389
+ const vc_config = {
390
+ runtime,
391
+ handler: 'index.js',
392
+ launcherType: 'Nodejs',
393
+ experimentalResponseStreaming: true,
394
+ framework: {
395
+ slug: 'ripple',
396
+ version: ADAPTER_VERSION,
397
+ },
398
+ };
399
+
400
+ if (serverless.regions) {
401
+ vc_config.regions = serverless.regions;
402
+ }
403
+ if (serverless.memory) {
404
+ vc_config.memory = serverless.memory;
405
+ }
406
+ if (serverless.maxDuration) {
407
+ vc_config.maxDuration = serverless.maxDuration;
408
+ }
409
+
410
+ // ISR (Incremental Static Regeneration) — adds a `prerender` config
411
+ // that tells Vercel to cache the serverless response at the edge and
412
+ // revalidate in the background after `expiration` seconds.
413
+ if (isr) {
414
+ /** @type {Record<string, unknown>} */
415
+ const prerender = {
416
+ expiration: isr.expiration,
417
+ };
418
+
419
+ if (isr.bypassToken) {
420
+ prerender.bypassToken = isr.bypassToken;
421
+ }
422
+
423
+ if (isr.allowQuery !== undefined) {
424
+ prerender.allowQuery = isr.allowQuery;
425
+ }
426
+
427
+ vc_config.prerender = prerender;
428
+
429
+ console.log(
430
+ `[adapter-vercel] ISR enabled (expiration: ${isr.expiration === false ? 'never' : isr.expiration + 's'})`,
431
+ );
432
+ }
433
+
434
+ write(join(func_dir, '.vc-config.json'), JSON.stringify(vc_config, null, '\t'));
435
+
436
+ // ------------------------------------------------------------------
437
+ // 3. Generate the Build Output API config
438
+ // ------------------------------------------------------------------
439
+
440
+ console.log('[adapter-vercel] Writing config...');
441
+
442
+ const vercel_config = generate_vercel_config(options);
443
+ write(join(output_dir, 'config.json'), JSON.stringify(vercel_config, null, '\t'));
444
+
445
+ // ------------------------------------------------------------------
446
+ // Summary
447
+ // ------------------------------------------------------------------
448
+
449
+ console.log('[adapter-vercel] Build output generated at .vercel/output/');
450
+ console.log(` Static: ${static_dir}`);
451
+ console.log(` Function: ${func_dir}`);
452
+ console.log(` Runtime: ${runtime}`);
453
+ }