@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 +15 -0
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/package.json +37 -0
- package/src/adapt.js +453 -0
- package/src/bin/adapt.js +70 -0
- package/src/index.js +10 -0
- package/tests/adapt.test.js +435 -0
- package/tests/types.test.js +84 -0
- package/types/index.d.ts +228 -0
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
|
+
}
|