@catmint/adapter-vercel 0.0.0-prealpha.1
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/LICENSE +339 -0
- package/dist/config-gen.d.ts +48 -0
- package/dist/config-gen.d.ts.map +1 -0
- package/dist/config-gen.js +89 -0
- package/dist/config-gen.js.map +1 -0
- package/dist/function-gen.d.ts +41 -0
- package/dist/function-gen.d.ts.map +1 -0
- package/dist/function-gen.js +504 -0
- package/dist/function-gen.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +131 -0
- package/dist/index.js.map +1 -0
- package/dist/vercel-cache-adapter.d.ts +45 -0
- package/dist/vercel-cache-adapter.d.ts.map +1 -0
- package/dist/vercel-cache-adapter.js +81 -0
- package/dist/vercel-cache-adapter.js.map +1 -0
- package/package.json +26 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
// @catmint/adapter-vercel — Serverless function wrapper generator
|
|
2
|
+
/**
|
|
3
|
+
* Build the tag-to-routes mapping from the manifest for cache invalidation.
|
|
4
|
+
*/
|
|
5
|
+
function buildTagToRoutes(manifest) {
|
|
6
|
+
const tagToRoutes = {};
|
|
7
|
+
for (const route of manifest.routes) {
|
|
8
|
+
if (route.cache?.tags) {
|
|
9
|
+
for (const tag of route.cache.tags) {
|
|
10
|
+
if (!tagToRoutes[tag]) {
|
|
11
|
+
tagToRoutes[tag] = [];
|
|
12
|
+
}
|
|
13
|
+
tagToRoutes[tag].push(route.path);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return tagToRoutes;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Generate the platform cache adapter setup code.
|
|
21
|
+
*
|
|
22
|
+
* This code is injected at the top of the generated serverless function
|
|
23
|
+
* to register the Vercel cache adapter before any request handling.
|
|
24
|
+
*
|
|
25
|
+
* Returns an empty string if no cached routes or bypass token are present.
|
|
26
|
+
*/
|
|
27
|
+
function generateCacheAdapterSetup(manifest) {
|
|
28
|
+
const bypassToken = manifest.cache?.invalidation?.bypassToken;
|
|
29
|
+
const hasCachedRoutes = manifest.routes.some((r) => r.cache);
|
|
30
|
+
if (!bypassToken || !hasCachedRoutes) {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
const tagToRoutes = buildTagToRoutes(manifest);
|
|
34
|
+
const tagToRoutesJson = JSON.stringify(tagToRoutes, null, 2);
|
|
35
|
+
return `
|
|
36
|
+
// ── Vercel Platform Cache Adapter ──────────────────────────────────
|
|
37
|
+
// Registers a cache adapter that delegates invalidateCache() calls to
|
|
38
|
+
// Vercel's on-demand ISR revalidation API (x-prerender-revalidate header).
|
|
39
|
+
const __BYPASS_TOKEN = ${JSON.stringify(bypassToken)};
|
|
40
|
+
const __TAG_TO_ROUTES = ${tagToRoutesJson};
|
|
41
|
+
|
|
42
|
+
const __catmintBaseUrl = typeof process !== "undefined" && process.env.VERCEL_URL
|
|
43
|
+
? \`https://\${process.env.VERCEL_URL}\`
|
|
44
|
+
: null;
|
|
45
|
+
|
|
46
|
+
const __catmintPlatformAdapter = {
|
|
47
|
+
name: "@catmint/adapter-vercel",
|
|
48
|
+
async invalidate(options) {
|
|
49
|
+
if (!__catmintBaseUrl) return;
|
|
50
|
+
const routesToRevalidate = new Set();
|
|
51
|
+
if (options.tag && __TAG_TO_ROUTES[options.tag]) {
|
|
52
|
+
for (const route of __TAG_TO_ROUTES[options.tag]) {
|
|
53
|
+
routesToRevalidate.add(route);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (options.route) {
|
|
57
|
+
routesToRevalidate.add(options.route);
|
|
58
|
+
}
|
|
59
|
+
await Promise.all([...routesToRevalidate].map(async (routePath) => {
|
|
60
|
+
try {
|
|
61
|
+
await fetch(\`\${__catmintBaseUrl}\${routePath}\`, {
|
|
62
|
+
method: "HEAD",
|
|
63
|
+
headers: { "x-prerender-revalidate": __BYPASS_TOKEN },
|
|
64
|
+
});
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.warn("[@catmint/adapter-vercel] ISR revalidation error for " + routePath + ":", err);
|
|
67
|
+
}
|
|
68
|
+
}));
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Register the adapter on globalThis so all environments (RSC/SSR) share it
|
|
73
|
+
const __ga = globalThis;
|
|
74
|
+
__ga.__catmintPlatformCacheAdapter = __catmintPlatformAdapter;
|
|
75
|
+
// ────────────────────────────────────────────────────────────────────
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Generate the .vc-config.json for the serverless function.
|
|
80
|
+
*/
|
|
81
|
+
export function generateVcConfig(runtime, regions) {
|
|
82
|
+
if (runtime === "edge") {
|
|
83
|
+
const config = {
|
|
84
|
+
runtime: "edge",
|
|
85
|
+
entrypoint: "handler.js",
|
|
86
|
+
};
|
|
87
|
+
if (regions?.length) {
|
|
88
|
+
config.regions = regions;
|
|
89
|
+
}
|
|
90
|
+
return config;
|
|
91
|
+
}
|
|
92
|
+
const config = {
|
|
93
|
+
runtime: "nodejs22.x",
|
|
94
|
+
handler: "handler.js",
|
|
95
|
+
launcherType: "Nodejs",
|
|
96
|
+
};
|
|
97
|
+
if (regions?.length) {
|
|
98
|
+
config.regions = regions;
|
|
99
|
+
}
|
|
100
|
+
return config;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Generate the serverless function entry point wrapper.
|
|
104
|
+
*
|
|
105
|
+
* For Node.js runtime: exports a default handler function compatible with
|
|
106
|
+
* Vercel's Node.js serverless function signature.
|
|
107
|
+
*
|
|
108
|
+
* For Edge runtime: exports a default fetch handler compatible with
|
|
109
|
+
* Vercel's Edge runtime.
|
|
110
|
+
*
|
|
111
|
+
* When the manifest contains cached routes with a bypass token, the generated
|
|
112
|
+
* function registers a platform cache adapter that delegates invalidation
|
|
113
|
+
* to Vercel's on-demand ISR revalidation API.
|
|
114
|
+
*/
|
|
115
|
+
export function generateServerlessFunction(manifest, runtime) {
|
|
116
|
+
if (runtime === "edge") {
|
|
117
|
+
return generateEdgeFunction(manifest);
|
|
118
|
+
}
|
|
119
|
+
return generateNodejsFunction(manifest);
|
|
120
|
+
}
|
|
121
|
+
function generateNodejsFunction(manifest) {
|
|
122
|
+
const cacheAdapterSetup = generateCacheAdapterSetup(manifest);
|
|
123
|
+
return `// Auto-generated by @catmint/adapter-vercel (Node.js runtime) — do not edit
|
|
124
|
+
import * as rscEntry from "./rsc/index.js";
|
|
125
|
+
import * as ssrEntry from "./ssr/index.js";
|
|
126
|
+
${cacheAdapterSetup}
|
|
127
|
+
function collectBody(req) {
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
const chunks = [];
|
|
130
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
131
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
132
|
+
req.on("error", reject);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function pipeWebStreamToResponse(webStream, res) {
|
|
137
|
+
const reader = webStream.getReader();
|
|
138
|
+
try {
|
|
139
|
+
while (true) {
|
|
140
|
+
const { done, value } = await reader.read();
|
|
141
|
+
if (done) break;
|
|
142
|
+
if (!res.write(value)) {
|
|
143
|
+
await new Promise((resolve) => res.once("drain", resolve));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} finally {
|
|
147
|
+
reader.releaseLock();
|
|
148
|
+
res.end();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function sendStatusPage(res, statusCode, pathname) {
|
|
153
|
+
var html = null;
|
|
154
|
+
if (rscEntry.renderStatusPage && ssrEntry.renderToHtml) {
|
|
155
|
+
try {
|
|
156
|
+
var rscResult = await rscEntry.renderStatusPage(statusCode, pathname);
|
|
157
|
+
if (rscResult) {
|
|
158
|
+
var htmlStream = await ssrEntry.renderToHtml(rscResult.stream, rscResult.headConfig);
|
|
159
|
+
var reader = htmlStream.getReader();
|
|
160
|
+
var decoder = new TextDecoder();
|
|
161
|
+
var result = "";
|
|
162
|
+
while (true) {
|
|
163
|
+
var chunk = await reader.read();
|
|
164
|
+
if (chunk.done) break;
|
|
165
|
+
result += decoder.decode(chunk.value, { stream: true });
|
|
166
|
+
}
|
|
167
|
+
html = result;
|
|
168
|
+
}
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.error("Failed to render status page for " + statusCode + ":", err);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (!html) html = __catmintErrorPage(statusCode);
|
|
174
|
+
res.statusCode = statusCode;
|
|
175
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
176
|
+
res.end(html);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function __catmintErrorPage(statusCode, detail) {
|
|
180
|
+
var titles = { 404: "Not Found", 500: "Internal Server Error" };
|
|
181
|
+
var descs = {
|
|
182
|
+
404: "The page you are looking for does not exist.",
|
|
183
|
+
500: "Something went wrong. Please try again later."
|
|
184
|
+
};
|
|
185
|
+
var title = titles[statusCode] || "Error";
|
|
186
|
+
var desc = descs[statusCode] || "An error occurred.";
|
|
187
|
+
function esc(s) {
|
|
188
|
+
return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
|
189
|
+
}
|
|
190
|
+
var detailBlock = detail
|
|
191
|
+
? '\\n <div class="detail">' + esc(detail) + '</div>'
|
|
192
|
+
: '';
|
|
193
|
+
return '<!DOCTYPE html>'
|
|
194
|
+
+ '<html lang="en"><head>'
|
|
195
|
+
+ '<meta charset="utf-8">'
|
|
196
|
+
+ '<meta name="viewport" content="width=device-width,initial-scale=1">'
|
|
197
|
+
+ '<title>' + statusCode + ' \\u2014 ' + title + '</title>'
|
|
198
|
+
+ '<style>'
|
|
199
|
+
+ '*{margin:0;padding:0;box-sizing:border-box}'
|
|
200
|
+
+ 'html,body{height:100%}'
|
|
201
|
+
+ 'body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;color:#111;background:#fafafa;display:flex;align-items:center;justify-content:center;text-align:center}'
|
|
202
|
+
+ '.c{max-width:480px;padding:2rem}'
|
|
203
|
+
+ '.code{font-size:4rem;font-weight:700;letter-spacing:-.02em;color:#222;line-height:1}'
|
|
204
|
+
+ '.divider{width:48px;height:2px;background:#e5e5e5;margin:1.25rem auto}'
|
|
205
|
+
+ '.title{font-size:1.125rem;font-weight:500;color:#444;margin-bottom:.5rem}'
|
|
206
|
+
+ '.desc{font-size:.875rem;color:#888;line-height:1.5}'
|
|
207
|
+
+ '.detail{margin-top:1.5rem;padding:1rem;background:#f0f0f0;border-radius:6px;font-family:"SF Mono",SFMono-Regular,Consolas,"Liberation Mono",Menlo,monospace;font-size:.75rem;color:#666;text-align:left;white-space:pre-wrap;word-break:break-word;max-height:200px;overflow:auto}'
|
|
208
|
+
+ '</style></head><body>'
|
|
209
|
+
+ '<div class="c">'
|
|
210
|
+
+ '<div class="code">' + statusCode + '</div>'
|
|
211
|
+
+ '<div class="divider"></div>'
|
|
212
|
+
+ '<div class="title">' + esc(title) + '</div>'
|
|
213
|
+
+ '<div class="desc">' + esc(desc) + '</div>'
|
|
214
|
+
+ detailBlock
|
|
215
|
+
+ '</div></body></html>';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export default async function handler(req, res) {
|
|
219
|
+
const url = new URL(req.url || "/", \`https://\${req.headers.host || "localhost"}\`);
|
|
220
|
+
const pathname = url.pathname;
|
|
221
|
+
const method = (req.method || "GET").toUpperCase();
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
// 1. Handle server function RPC calls (/__catmint/fn/*)
|
|
225
|
+
if (pathname.startsWith("/__catmint/fn/") && ssrEntry.handleServerFn) {
|
|
226
|
+
const body = await collectBody(req);
|
|
227
|
+
let parsed;
|
|
228
|
+
try {
|
|
229
|
+
parsed = body.length > 0 ? JSON.parse(body.toString("utf-8")) : undefined;
|
|
230
|
+
} catch {
|
|
231
|
+
res.statusCode = 400;
|
|
232
|
+
res.setHeader("Content-Type", "application/json");
|
|
233
|
+
res.end(JSON.stringify({ error: "Invalid JSON body" }));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const result = await ssrEntry.handleServerFn(pathname, parsed);
|
|
237
|
+
if (result) {
|
|
238
|
+
res.statusCode = 200;
|
|
239
|
+
res.setHeader("Content-Type", "application/json");
|
|
240
|
+
res.end(JSON.stringify(result.result));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
res.statusCode = 404;
|
|
244
|
+
res.setHeader("Content-Type", "application/json");
|
|
245
|
+
res.end(JSON.stringify({ error: "Server function not found" }));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 2. RSC flight stream for client-side navigation (/__catmint/rsc?path=...)
|
|
250
|
+
if (pathname === "/__catmint/rsc" && method === "GET" && rscEntry.render) {
|
|
251
|
+
const targetPath = url.searchParams.get("path");
|
|
252
|
+
if (!targetPath) {
|
|
253
|
+
res.statusCode = 400;
|
|
254
|
+
res.setHeader("Content-Type", "application/json");
|
|
255
|
+
res.end(JSON.stringify({ error: "Missing ?path= parameter" }));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
const rscResult = await rscEntry.render(targetPath);
|
|
260
|
+
if (rscResult) {
|
|
261
|
+
res.statusCode = 200;
|
|
262
|
+
res.setHeader("Content-Type", "text/x-component; charset=utf-8");
|
|
263
|
+
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
264
|
+
await pipeWebStreamToResponse(rscResult.stream, res);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
res.statusCode = 404;
|
|
268
|
+
res.setHeader("Content-Type", "application/json");
|
|
269
|
+
res.end(JSON.stringify({ error: "No matching route" }));
|
|
270
|
+
} catch (err) {
|
|
271
|
+
console.error("RSC navigation error:", err);
|
|
272
|
+
if (!res.headersSent) {
|
|
273
|
+
res.statusCode = 500;
|
|
274
|
+
res.setHeader("Content-Type", "application/json");
|
|
275
|
+
res.end(JSON.stringify({ error: "RSC navigation error" }));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 3. API endpoint handling
|
|
282
|
+
if (ssrEntry.hasEndpoint && ssrEntry.hasEndpoint(pathname)) {
|
|
283
|
+
const fullUrl = \`https://\${req.headers.host || "localhost"}\${req.url || "/"}\`;
|
|
284
|
+
const requestInit = {
|
|
285
|
+
method,
|
|
286
|
+
headers: Object.fromEntries(
|
|
287
|
+
Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : v || ""])
|
|
288
|
+
),
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
292
|
+
const body = await collectBody(req);
|
|
293
|
+
requestInit.body = new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const webRequest = new Request(fullUrl, requestInit);
|
|
297
|
+
const result = await ssrEntry.handleEndpoint(pathname, method, webRequest);
|
|
298
|
+
|
|
299
|
+
if (result) {
|
|
300
|
+
result.response.headers.forEach((value, key) => {
|
|
301
|
+
res.setHeader(key, value);
|
|
302
|
+
});
|
|
303
|
+
res.statusCode = result.response.status;
|
|
304
|
+
if (result.response.body) {
|
|
305
|
+
await pipeWebStreamToResponse(result.response.body, res);
|
|
306
|
+
} else {
|
|
307
|
+
res.end();
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
res.statusCode = 405;
|
|
313
|
+
res.setHeader("Content-Type", "text/plain");
|
|
314
|
+
res.end(\`Method \${method} not allowed\`);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// 4. RSC → SSR page rendering pipeline
|
|
319
|
+
if (method === "GET" && rscEntry.render) {
|
|
320
|
+
const rscResult = await rscEntry.render(pathname);
|
|
321
|
+
if (rscResult) {
|
|
322
|
+
const htmlStream = await ssrEntry.renderToHtml(rscResult.stream, rscResult.headConfig);
|
|
323
|
+
res.statusCode = 200;
|
|
324
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
325
|
+
await pipeWebStreamToResponse(htmlStream, res);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 5. Fallback 404
|
|
331
|
+
await sendStatusPage(res, 404, pathname);
|
|
332
|
+
} catch (err) {
|
|
333
|
+
console.error("Serverless function error:", err);
|
|
334
|
+
await sendStatusPage(res, 500, pathname);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
`;
|
|
338
|
+
}
|
|
339
|
+
function generateEdgeFunction(manifest) {
|
|
340
|
+
const cacheAdapterSetup = generateCacheAdapterSetup(manifest);
|
|
341
|
+
return `// Auto-generated by @catmint/adapter-vercel (Edge runtime) — do not edit
|
|
342
|
+
import * as rscEntry from "./rsc/index.js";
|
|
343
|
+
import * as ssrEntry from "./ssr/index.js";
|
|
344
|
+
${cacheAdapterSetup}
|
|
345
|
+
async function statusPageResponse(statusCode, pathname) {
|
|
346
|
+
if (rscEntry.renderStatusPage && ssrEntry.renderToHtml) {
|
|
347
|
+
try {
|
|
348
|
+
var rscResult = await rscEntry.renderStatusPage(statusCode, pathname);
|
|
349
|
+
if (rscResult) {
|
|
350
|
+
var htmlStream = await ssrEntry.renderToHtml(rscResult.stream, rscResult.headConfig);
|
|
351
|
+
return new Response(htmlStream, {
|
|
352
|
+
status: statusCode,
|
|
353
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
} catch (err) {
|
|
357
|
+
console.error("Failed to render status page for " + statusCode + ":", err);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return new Response(__catmintErrorPage(statusCode), {
|
|
361
|
+
status: statusCode,
|
|
362
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function __catmintErrorPage(statusCode, detail) {
|
|
367
|
+
var titles = { 404: "Not Found", 500: "Internal Server Error" };
|
|
368
|
+
var descs = {
|
|
369
|
+
404: "The page you are looking for does not exist.",
|
|
370
|
+
500: "Something went wrong. Please try again later."
|
|
371
|
+
};
|
|
372
|
+
var title = titles[statusCode] || "Error";
|
|
373
|
+
var desc = descs[statusCode] || "An error occurred.";
|
|
374
|
+
function esc(s) {
|
|
375
|
+
return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
|
376
|
+
}
|
|
377
|
+
var detailBlock = detail
|
|
378
|
+
? '\\n <div class="detail">' + esc(detail) + '</div>'
|
|
379
|
+
: '';
|
|
380
|
+
return '<!DOCTYPE html>'
|
|
381
|
+
+ '<html lang="en"><head>'
|
|
382
|
+
+ '<meta charset="utf-8">'
|
|
383
|
+
+ '<meta name="viewport" content="width=device-width,initial-scale=1">'
|
|
384
|
+
+ '<title>' + statusCode + ' \\u2014 ' + title + '</title>'
|
|
385
|
+
+ '<style>'
|
|
386
|
+
+ '*{margin:0;padding:0;box-sizing:border-box}'
|
|
387
|
+
+ 'html,body{height:100%}'
|
|
388
|
+
+ 'body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;color:#111;background:#fafafa;display:flex;align-items:center;justify-content:center;text-align:center}'
|
|
389
|
+
+ '.c{max-width:480px;padding:2rem}'
|
|
390
|
+
+ '.code{font-size:4rem;font-weight:700;letter-spacing:-.02em;color:#222;line-height:1}'
|
|
391
|
+
+ '.divider{width:48px;height:2px;background:#e5e5e5;margin:1.25rem auto}'
|
|
392
|
+
+ '.title{font-size:1.125rem;font-weight:500;color:#444;margin-bottom:.5rem}'
|
|
393
|
+
+ '.desc{font-size:.875rem;color:#888;line-height:1.5}'
|
|
394
|
+
+ '.detail{margin-top:1.5rem;padding:1rem;background:#f0f0f0;border-radius:6px;font-family:"SF Mono",SFMono-Regular,Consolas,"Liberation Mono",Menlo,monospace;font-size:.75rem;color:#666;text-align:left;white-space:pre-wrap;word-break:break-word;max-height:200px;overflow:auto}'
|
|
395
|
+
+ '</style></head><body>'
|
|
396
|
+
+ '<div class="c">'
|
|
397
|
+
+ '<div class="code">' + statusCode + '</div>'
|
|
398
|
+
+ '<div class="divider"></div>'
|
|
399
|
+
+ '<div class="title">' + esc(title) + '</div>'
|
|
400
|
+
+ '<div class="desc">' + esc(desc) + '</div>'
|
|
401
|
+
+ detailBlock
|
|
402
|
+
+ '</div></body></html>';
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export default async function handler(request) {
|
|
406
|
+
const url = new URL(request.url);
|
|
407
|
+
const pathname = url.pathname;
|
|
408
|
+
const method = request.method.toUpperCase();
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
// 1. Handle server function RPC calls (/__catmint/fn/*)
|
|
412
|
+
if (pathname.startsWith("/__catmint/fn/") && ssrEntry.handleServerFn) {
|
|
413
|
+
const bodyText = await request.text();
|
|
414
|
+
let parsed;
|
|
415
|
+
try {
|
|
416
|
+
parsed = bodyText.length > 0 ? JSON.parse(bodyText) : undefined;
|
|
417
|
+
} catch {
|
|
418
|
+
return new Response(
|
|
419
|
+
JSON.stringify({ error: "Invalid JSON body" }),
|
|
420
|
+
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
const result = await ssrEntry.handleServerFn(pathname, parsed);
|
|
424
|
+
if (result) {
|
|
425
|
+
return new Response(JSON.stringify(result.result), {
|
|
426
|
+
status: 200,
|
|
427
|
+
headers: { "Content-Type": "application/json" },
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
return new Response(
|
|
431
|
+
JSON.stringify({ error: "Server function not found" }),
|
|
432
|
+
{ status: 404, headers: { "Content-Type": "application/json" } },
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// 2. RSC flight stream for client-side navigation (/__catmint/rsc?path=...)
|
|
437
|
+
if (pathname === "/__catmint/rsc" && method === "GET" && rscEntry.render) {
|
|
438
|
+
const targetPath = url.searchParams.get("path");
|
|
439
|
+
if (!targetPath) {
|
|
440
|
+
return new Response(
|
|
441
|
+
JSON.stringify({ error: "Missing ?path= parameter" }),
|
|
442
|
+
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
const rscResult = await rscEntry.render(targetPath);
|
|
447
|
+
if (rscResult) {
|
|
448
|
+
return new Response(rscResult.stream, {
|
|
449
|
+
status: 200,
|
|
450
|
+
headers: {
|
|
451
|
+
"Content-Type": "text/x-component; charset=utf-8",
|
|
452
|
+
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
return new Response(
|
|
457
|
+
JSON.stringify({ error: "No matching route" }),
|
|
458
|
+
{ status: 404, headers: { "Content-Type": "application/json" } },
|
|
459
|
+
);
|
|
460
|
+
} catch (err) {
|
|
461
|
+
console.error("RSC navigation error:", err);
|
|
462
|
+
return new Response(
|
|
463
|
+
JSON.stringify({ error: "RSC navigation error" }),
|
|
464
|
+
{ status: 500, headers: { "Content-Type": "application/json" } },
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// 3. API endpoint handling
|
|
470
|
+
if (ssrEntry.hasEndpoint && ssrEntry.hasEndpoint(pathname)) {
|
|
471
|
+
const result = await ssrEntry.handleEndpoint(pathname, method, request);
|
|
472
|
+
|
|
473
|
+
if (result) {
|
|
474
|
+
return result.response;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return new Response(\`Method \${method} not allowed\`, {
|
|
478
|
+
status: 405,
|
|
479
|
+
headers: { "Content-Type": "text/plain" },
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// 4. RSC → SSR page rendering pipeline
|
|
484
|
+
if (method === "GET" && rscEntry.render) {
|
|
485
|
+
const rscResult = await rscEntry.render(pathname);
|
|
486
|
+
if (rscResult) {
|
|
487
|
+
const htmlStream = await ssrEntry.renderToHtml(rscResult.stream, rscResult.headConfig);
|
|
488
|
+
return new Response(htmlStream, {
|
|
489
|
+
status: 200,
|
|
490
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// 5. Fallback 404
|
|
496
|
+
return await statusPageResponse(404, pathname);
|
|
497
|
+
} catch (err) {
|
|
498
|
+
console.error("Edge function error:", err);
|
|
499
|
+
return await statusPageResponse(500, pathname);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
`;
|
|
503
|
+
}
|
|
504
|
+
//# sourceMappingURL=function-gen.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"function-gen.js","sourceRoot":"","sources":["../src/function-gen.ts"],"names":[],"mappings":"AAAA,kEAAkE;AA2BlE;;GAEG;AACH,SAAS,gBAAgB,CAAC,QAAqB;IAC7C,MAAM,WAAW,GAA6B,EAAE,CAAC;IACjD,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;QACpC,IAAI,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC;YACtB,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;gBACnC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;oBACtB,WAAW,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;gBACxB,CAAC;gBACD,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACpC,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,WAAW,CAAC;AACrB,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,yBAAyB,CAAC,QAAqB;IACtD,MAAM,WAAW,GAAG,QAAQ,CAAC,KAAK,EAAE,YAAY,EAAE,WAAW,CAAC;IAC9D,MAAM,eAAe,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IAE7D,IAAI,CAAC,WAAW,IAAI,CAAC,eAAe,EAAE,CAAC;QACrC,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,WAAW,GAAG,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IAC/C,MAAM,eAAe,GAAG,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAE7D,OAAO;;;;yBAIgB,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC;0BAC1B,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoCxC,CAAC;AACF,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAC9B,OAA0B,EAC1B,OAAkB;IAElB,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;QACvB,MAAM,MAAM,GAAiB;YAC3B,OAAO,EAAE,MAAM;YACf,UAAU,EAAE,YAAY;SACzB,CAAC;QACF,IAAI,OAAO,EAAE,MAAM,EAAE,CAAC;YACpB,MAAM,CAAC,OAAO,GAAG,OAAO,CAAC;QAC3B,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,MAAM,GAAmB;QAC7B,OAAO,EAAE,YAAY;QACrB,OAAO,EAAE,YAAY;QACrB,YAAY,EAAE,QAAQ;KACvB,CAAC;IACF,IAAI,OAAO,EAAE,MAAM,EAAE,CAAC;QACpB,MAAM,CAAC,OAAO,GAAG,OAAO,CAAC;IAC3B,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,0BAA0B,CACxC,QAAqB,EACrB,OAA0B;IAE1B,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;QACvB,OAAO,oBAAoB,CAAC,QAAQ,CAAC,CAAC;IACxC,CAAC;IACD,OAAO,sBAAsB,CAAC,QAAQ,CAAC,CAAC;AAC1C,CAAC;AAED,SAAS,sBAAsB,CAAC,QAAqB;IACnD,MAAM,iBAAiB,GAAG,yBAAyB,CAAC,QAAQ,CAAC,CAAC;IAE9D,OAAO;;;EAGP,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmNlB,CAAC;AACF,CAAC;AAED,SAAS,oBAAoB,CAAC,QAAqB;IACjD,MAAM,iBAAiB,GAAG,yBAAyB,CAAC,QAAQ,CAAC,CAAC;IAE9D,OAAO;;;EAGP,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8JlB,CAAC;AACF,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CatmintAdapter } from "catmint/config";
|
|
2
|
+
/**
|
|
3
|
+
* Vercel adapter configuration options.
|
|
4
|
+
*/
|
|
5
|
+
export interface VercelAdapterOptions {
|
|
6
|
+
/** Runtime for serverless functions: 'nodejs' | 'edge' */
|
|
7
|
+
runtime?: "nodejs" | "edge";
|
|
8
|
+
/** Vercel regions to deploy to */
|
|
9
|
+
regions?: string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Create a Vercel adapter for Catmint.
|
|
13
|
+
*
|
|
14
|
+
* Transforms Catmint build output into the Vercel Build Output API v3 format.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { defineConfig } from 'catmint/config'
|
|
19
|
+
* import vercel from '@catmint/adapter-vercel'
|
|
20
|
+
*
|
|
21
|
+
* export default defineConfig({
|
|
22
|
+
* adapter: vercel({ runtime: 'edge' }),
|
|
23
|
+
* })
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export default function vercelAdapter(options?: VercelAdapterOptions): CatmintAdapter;
|
|
27
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAA+B,MAAM,gBAAgB,CAAC;AAoBlF;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,0DAA0D;IAC1D,OAAO,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC5B,kCAAkC;IAClC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,OAAO,UAAU,aAAa,CACnC,OAAO,CAAC,EAAE,oBAAoB,GAC7B,cAAc,CA0JhB"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// @catmint/adapter-vercel — Vercel deployment adapter
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { generateVercelConfig } from "./config-gen.js";
|
|
4
|
+
import { generateServerlessFunction, generateVcConfig, } from "./function-gen.js";
|
|
5
|
+
/**
|
|
6
|
+
* Create a Vercel adapter for Catmint.
|
|
7
|
+
*
|
|
8
|
+
* Transforms Catmint build output into the Vercel Build Output API v3 format.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { defineConfig } from 'catmint/config'
|
|
13
|
+
* import vercel from '@catmint/adapter-vercel'
|
|
14
|
+
*
|
|
15
|
+
* export default defineConfig({
|
|
16
|
+
* adapter: vercel({ runtime: 'edge' }),
|
|
17
|
+
* })
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export default function vercelAdapter(options) {
|
|
21
|
+
const runtime = options?.runtime ?? "nodejs";
|
|
22
|
+
const regions = options?.regions;
|
|
23
|
+
return {
|
|
24
|
+
name: "@catmint/adapter-vercel",
|
|
25
|
+
async adapt(context) {
|
|
26
|
+
context.log("@catmint/adapter-vercel: Generating Vercel Build Output API v3...");
|
|
27
|
+
const manifest = context.manifest;
|
|
28
|
+
// Validate edge runtime compatibility
|
|
29
|
+
if (runtime === "edge") {
|
|
30
|
+
validateEdgeCompatibility(manifest, context);
|
|
31
|
+
}
|
|
32
|
+
const outputDir = join(context.serverDir, "..", "..", ".vercel", "output");
|
|
33
|
+
// 1. Generate config.json
|
|
34
|
+
const configJson = generateVercelConfig(manifest);
|
|
35
|
+
await context.writeFile(join(outputDir, "config.json"), JSON.stringify(configJson, null, 2));
|
|
36
|
+
context.log("@catmint/adapter-vercel: Generated .vercel/output/config.json");
|
|
37
|
+
// 2. Copy client assets to static/
|
|
38
|
+
const staticOutputDir = join(outputDir, "static");
|
|
39
|
+
await context.copyDir(context.clientDir, staticOutputDir);
|
|
40
|
+
context.log("@catmint/adapter-vercel: Copied client assets to static/");
|
|
41
|
+
// 3. Copy pre-rendered static pages to static/
|
|
42
|
+
await context.copyDir(context.staticDir, staticOutputDir);
|
|
43
|
+
context.log("@catmint/adapter-vercel: Copied pre-rendered pages to static/");
|
|
44
|
+
// 4. Generate serverless function (only if not frontend-only mode)
|
|
45
|
+
if (manifest.mode !== "frontend") {
|
|
46
|
+
const funcDir = join(outputDir, "functions", "index.func");
|
|
47
|
+
// Derive the RSC directory from the server (SSR) directory.
|
|
48
|
+
// Build output layout: dist/ssr/ (serverDir), dist/rsc/ (RSC entry).
|
|
49
|
+
const rscDir = join(context.serverDir, "..", "rsc");
|
|
50
|
+
// Copy SSR bundle into ssr/ subdirectory and RSC bundle into rsc/
|
|
51
|
+
// subdirectory within the function directory, so the handler can
|
|
52
|
+
// import both without filename conflicts.
|
|
53
|
+
await context.copyDir(context.serverDir, join(funcDir, "ssr"));
|
|
54
|
+
await context.copyDir(rscDir, join(funcDir, "rsc"));
|
|
55
|
+
// Generate the serverless function entry as handler.js.
|
|
56
|
+
// This imports from ./ssr/index.js and ./rsc/index.js and wraps
|
|
57
|
+
// them with the Vercel serverless function handler signature.
|
|
58
|
+
const functionEntry = generateServerlessFunction(manifest, runtime);
|
|
59
|
+
await context.writeFile(join(funcDir, "handler.js"), functionEntry);
|
|
60
|
+
// Add package.json with "type": "module" so Node.js treats .js as ESM.
|
|
61
|
+
// The function directory is isolated from the project root, so without
|
|
62
|
+
// this file Node.js defaults to CJS and fails on import statements.
|
|
63
|
+
await context.writeFile(join(funcDir, "package.json"), JSON.stringify({ type: "module" }, null, 2));
|
|
64
|
+
// Generate .vc-config.json
|
|
65
|
+
const vcConfig = generateVcConfig(runtime, regions);
|
|
66
|
+
await context.writeFile(join(funcDir, ".vc-config.json"), JSON.stringify(vcConfig, null, 2));
|
|
67
|
+
context.log("@catmint/adapter-vercel: Generated serverless function at functions/index.func/");
|
|
68
|
+
// 5. Emit Prerender Functions for cached routes
|
|
69
|
+
const bypassToken = manifest.cache?.invalidation?.bypassToken;
|
|
70
|
+
const cachedRoutes = manifest.routes.filter((r) => r.cache);
|
|
71
|
+
if (cachedRoutes.length > 0 && bypassToken) {
|
|
72
|
+
for (const route of cachedRoutes) {
|
|
73
|
+
// Convert route path to a filesystem-safe function name
|
|
74
|
+
// e.g., "/examples/cached-route" → "examples/cached-route"
|
|
75
|
+
const routeName = route.path === "/" ? "index" : route.path.replace(/^\//, "");
|
|
76
|
+
// Skip the catch-all "index" route (already handled above)
|
|
77
|
+
if (routeName === "index")
|
|
78
|
+
continue;
|
|
79
|
+
// Create the function directory for this route
|
|
80
|
+
const routeFuncDir = join(outputDir, "functions", `${routeName}.func`);
|
|
81
|
+
// The route function re-uses the same serverless entry and server bundle
|
|
82
|
+
await context.copyDir(context.serverDir, join(routeFuncDir, "ssr"));
|
|
83
|
+
await context.copyDir(rscDir, join(routeFuncDir, "rsc"));
|
|
84
|
+
await context.writeFile(join(routeFuncDir, "handler.js"), functionEntry);
|
|
85
|
+
await context.writeFile(join(routeFuncDir, "package.json"), JSON.stringify({ type: "module" }, null, 2));
|
|
86
|
+
await context.writeFile(join(routeFuncDir, ".vc-config.json"), JSON.stringify(vcConfig, null, 2));
|
|
87
|
+
// Emit the prerender config
|
|
88
|
+
const prerenderConfig = {
|
|
89
|
+
expiration: route.cache?.revalidate ?? false,
|
|
90
|
+
bypassToken,
|
|
91
|
+
};
|
|
92
|
+
await context.writeFile(join(outputDir, "functions", `${routeName}.prerender-config.json`), JSON.stringify(prerenderConfig, null, 2));
|
|
93
|
+
context.log(`@catmint/adapter-vercel: Prerender function for ${route.path} (TTL: ${route.cache?.revalidate ?? "none"}s)`);
|
|
94
|
+
}
|
|
95
|
+
context.log(`@catmint/adapter-vercel: Emitted ${cachedRoutes.length} Prerender Function(s)`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
context.log("@catmint/adapter-vercel: Build output ready at .vercel/output/");
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Basic validation that edge runtime doesn't use Node.js-specific APIs.
|
|
104
|
+
* This is a best-effort check based on the manifest.
|
|
105
|
+
*/
|
|
106
|
+
function validateEdgeCompatibility(manifest, context) {
|
|
107
|
+
// Warn about potential issues with edge runtime
|
|
108
|
+
if (manifest.serverFunctions.length > 0) {
|
|
109
|
+
context.log("@catmint/adapter-vercel: Warning — Edge runtime selected with server functions. " +
|
|
110
|
+
"Ensure server functions do not use Node.js-specific APIs (fs, child_process, etc.).");
|
|
111
|
+
}
|
|
112
|
+
const nodeOnlyIndicators = [
|
|
113
|
+
"node:",
|
|
114
|
+
"fs",
|
|
115
|
+
"child_process",
|
|
116
|
+
"cluster",
|
|
117
|
+
"dgram",
|
|
118
|
+
"dns",
|
|
119
|
+
"net",
|
|
120
|
+
"tls",
|
|
121
|
+
];
|
|
122
|
+
for (const fn of manifest.serverFunctions) {
|
|
123
|
+
for (const indicator of nodeOnlyIndicators) {
|
|
124
|
+
if (fn.source.includes(indicator)) {
|
|
125
|
+
context.log(`@catmint/adapter-vercel: Warning — Server function "${fn.name}" source path ` +
|
|
126
|
+
`contains "${indicator}" which may not be compatible with edge runtime.`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=index.js.map
|