-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathindex.ts
More file actions
201 lines (164 loc) · 6.49 KB
/
index.ts
File metadata and controls
201 lines (164 loc) · 6.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
/**
* Vercel Serverless API Entrypoint
*
* Boots the ObjectStack kernel from the shared objectstack.config.ts
* and delegates all /api/* traffic to the ObjectStack Hono adapter.
*/
import { ObjectKernel } from '@objectstack/runtime';
import { createHonoApp } from '@objectstack/hono';
import { getRequestListener } from '@hono/node-server';
import { serveStatic } from '@hono/node-server/serve-static';
import type { Hono } from 'hono';
import stackConfig from '../objectstack.config';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
// ---------------------------------------------------------------------------
// Singleton state — persists across warm Vercel invocations
// ---------------------------------------------------------------------------
let _kernel: ObjectKernel | null = null;
let _app: Hono | null = null;
/** Shared boot promise — prevents concurrent cold-start races. */
let _bootPromise: Promise<ObjectKernel> | null = null;
// ---------------------------------------------------------------------------
// Kernel bootstrap
// ---------------------------------------------------------------------------
async function ensureKernel(): Promise<ObjectKernel> {
if (_kernel) return _kernel;
if (_bootPromise) return _bootPromise;
_bootPromise = (async () => {
console.log('[Vercel] Booting ObjectStack Kernel...');
try {
const kernel = new ObjectKernel();
// Register all plugins from shared config
if (!stackConfig.plugins || stackConfig.plugins.length === 0) {
throw new Error(`[Vercel] No plugins found in stackConfig`);
}
for (const plugin of stackConfig.plugins) {
await kernel.use(plugin as any);
}
await kernel.bootstrap();
console.log('[Vercel] Kernel ready.');
return kernel;
} catch (err) {
// Clear the lock so the next request can retry
_bootPromise = null;
console.error('[Vercel] Kernel boot failed:', (err as any)?.message || err);
throw err;
}
})();
return _bootPromise;
}
// ---------------------------------------------------------------------------
// Hono app factory
// ---------------------------------------------------------------------------
async function ensureApp(): Promise<Hono> {
if (_app) return _app;
const kernel = await ensureKernel();
_app = createHonoApp({ kernel, prefix: '/api/v1' });
// Serve studio at /_studio
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const studioPath = join(__dirname, '_studio');
// Serve static files from /_studio
_app.get('/_studio/*', serveStatic({
root: __dirname,
rewriteRequestPath: (path) => {
// Rewrite /_studio/assets/x.js -> /_studio/assets/x.js
return path;
}
}));
// SPA fallback for studio
_app.get('/_studio/*', serveStatic({
root: __dirname,
rewriteRequestPath: () => '/_studio/index.html'
}));
// Serve studio index at /_studio root
_app.get('/_studio', serveStatic({
root: __dirname,
rewriteRequestPath: () => '/_studio/index.html'
}));
return _app;
}
// ---------------------------------------------------------------------------
// Body extraction — reads Vercel's pre-buffered request body.
// ---------------------------------------------------------------------------
interface VercelIncomingMessage {
rawBody?: Buffer | string;
body?: unknown;
headers?: Record<string, string | string[] | undefined>;
}
interface VercelEnv {
incoming?: VercelIncomingMessage;
}
function extractBody(
incoming: VercelIncomingMessage,
method: string,
contentType: string | undefined,
): BodyInit | null {
if (method === 'GET' || method === 'HEAD' || method === 'OPTIONS') return null;
if (incoming.rawBody != null) {
return incoming.rawBody;
}
if (incoming.body != null) {
if (typeof incoming.body === 'string') return incoming.body;
if (contentType?.includes('application/json')) return JSON.stringify(incoming.body);
return String(incoming.body);
}
return null;
}
function resolvePublicUrl(
requestUrl: string,
incoming: VercelIncomingMessage | undefined,
): string {
if (!incoming) return requestUrl;
const fwdProto = incoming.headers?.['x-forwarded-proto'];
const rawProto = Array.isArray(fwdProto) ? fwdProto[0] : fwdProto;
const proto = rawProto === 'https' || rawProto === 'http' ? rawProto : undefined;
if (proto === 'https' && requestUrl.startsWith('http:')) {
return requestUrl.replace(/^http:/, 'https:');
}
return requestUrl;
}
// ---------------------------------------------------------------------------
// Vercel Node.js serverless handler
// ---------------------------------------------------------------------------
export default getRequestListener(async (request, env) => {
let app: Hono;
try {
app = await ensureApp();
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
console.error('[Vercel] Handler error — bootstrap did not complete:', message);
return new Response(
JSON.stringify({
success: false,
error: {
message: 'Service Unavailable — kernel bootstrap failed.',
code: 503,
},
}),
{ status: 503, headers: { 'content-type': 'application/json' } },
);
}
const method = request.method.toUpperCase();
const incoming = (env as VercelEnv)?.incoming;
const url = resolvePublicUrl(request.url, incoming);
console.log(`[Vercel] ${method} ${url}`);
if (method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && incoming) {
const contentType = incoming.headers?.['content-type'];
const contentTypeStr = Array.isArray(contentType) ? contentType[0] : contentType;
const body = extractBody(incoming, method, contentTypeStr);
if (body != null) {
return await app.fetch(
new Request(url, { method, headers: request.headers, body }),
);
}
}
return await app.fetch(
new Request(url, { method, headers: request.headers }),
);
});
export const config = {
maxDuration: 60,
};