@catmint/adapter-node 0.0.0-prealpha.2 → 0.0.0-prealpha.21
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/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -1
- package/dist/index.js.map +1 -1
- package/dist/server-template.d.ts +2 -0
- package/dist/server-template.d.ts.map +1 -1
- package/dist/server-template.js +288 -15
- package/dist/server-template.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,13 @@ export interface NodeAdapterOptions {
|
|
|
7
7
|
port?: number;
|
|
8
8
|
/** Host to bind to (default: '0.0.0.0') */
|
|
9
9
|
host?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Maximum allowed request body size in bytes.
|
|
12
|
+
* Applies to server function RPC calls and API endpoints.
|
|
13
|
+
*
|
|
14
|
+
* @default 1_048_576 (1 MB)
|
|
15
|
+
*/
|
|
16
|
+
maxBodySize?: number;
|
|
10
17
|
}
|
|
11
18
|
/**
|
|
12
19
|
* Create a Node.js adapter for Catmint.
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,cAAc,EAIf,MAAM,gBAAgB,CAAC;AAIxB;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,wCAAwC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,OAAO,UAAU,WAAW,CACjC,OAAO,CAAC,EAAE,kBAAkB,GAC3B,cAAc,CA+ChB"}
|
package/dist/index.js
CHANGED
|
@@ -19,12 +19,31 @@ import { generateServerEntry } from "./server-template.js";
|
|
|
19
19
|
export default function nodeAdapter(options) {
|
|
20
20
|
const port = options?.port ?? 6468;
|
|
21
21
|
const host = options?.host ?? "0.0.0.0";
|
|
22
|
+
const maxBodySize = options?.maxBodySize ?? 1_048_576;
|
|
22
23
|
return {
|
|
23
24
|
name: "@catmint/adapter-node",
|
|
25
|
+
/**
|
|
26
|
+
* Dev-time hook: provides the raw Node.js `req` and `res` objects as
|
|
27
|
+
* the platform context. This allows `getPlatform()` to return
|
|
28
|
+
* `{ req, res }` during development, matching the shape that
|
|
29
|
+
* Node.js-based production servers would provide.
|
|
30
|
+
*/
|
|
31
|
+
dev() {
|
|
32
|
+
return {
|
|
33
|
+
getPlatform(_request, nodeReq, nodeRes) {
|
|
34
|
+
return { req: nodeReq, res: nodeRes };
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
},
|
|
24
38
|
async adapt(context) {
|
|
25
39
|
context.log("@catmint/adapter-node: Generating standalone Node.js server...");
|
|
26
40
|
const manifest = context.manifest;
|
|
27
|
-
const entryContent = generateServerEntry(manifest, {
|
|
41
|
+
const entryContent = generateServerEntry(manifest, {
|
|
42
|
+
port,
|
|
43
|
+
host,
|
|
44
|
+
maxBodySize,
|
|
45
|
+
headerPreset: manifest.config.security.headerPreset,
|
|
46
|
+
});
|
|
28
47
|
// Write to dist/server/index.js (separate from the SSR bundle at dist/ssr/)
|
|
29
48
|
// so `catmint start` can still load the SSR entry for its built-in server.
|
|
30
49
|
await context.writeFile("server/index.js", entryContent);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,4DAA4D;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAS5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAmB3D;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,OAAO,UAAU,WAAW,CACjC,OAA4B;IAE5B,MAAM,IAAI,GAAG,OAAO,EAAE,IAAI,IAAI,IAAI,CAAC;IACnC,MAAM,IAAI,GAAG,OAAO,EAAE,IAAI,IAAI,SAAS,CAAC;IACxC,MAAM,WAAW,GAAG,OAAO,EAAE,WAAW,IAAI,SAAS,CAAC;IAEtD,OAAO;QACL,IAAI,EAAE,uBAAuB;QAE7B;;;;;WAKG;QACH,GAAG;YACD,OAAO;gBACL,WAAW,CAAC,QAAiB,EAAE,OAAiB,EAAE,OAAiB;oBACjE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;gBACxC,CAAC;aACF,CAAC;QACJ,CAAC;QAED,KAAK,CAAC,KAAK,CAAC,OAAuB;YACjC,OAAO,CAAC,GAAG,CACT,gEAAgE,CACjE,CAAC;YAEF,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAuB,CAAC;YACjD,MAAM,YAAY,GAAG,mBAAmB,CAAC,QAAQ,EAAE;gBACjD,IAAI;gBACJ,IAAI;gBACJ,WAAW;gBACX,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY;aACpD,CAAC,CAAC;YAEH,4EAA4E;YAC5E,2EAA2E;YAC3E,MAAM,OAAO,CAAC,SAAS,CAAC,iBAAiB,EAAE,YAAY,CAAC,CAAC;YAEzD,OAAO,CAAC,GAAG,CACT,oEAAoE,CACrE,CAAC;YACF,OAAO,CAAC,GAAG,CACT,iFAAiF,IAAI,IAAI,IAAI,GAAG,CACjG,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-template.d.ts","sourceRoot":"","sources":["../src/server-template.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"server-template.d.ts","sourceRoot":"","sources":["../src/server-template.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,UAAU,GAAG,MAAM,CAAC;CACnC;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,WAAW,EACtB,OAAO,EAAE,aAAa,GACrB,MAAM,CAunBR"}
|
package/dist/server-template.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
export function generateServerEntry(_manifest, options) {
|
|
13
13
|
return `// Auto-generated by @catmint/adapter-node — do not edit
|
|
14
14
|
import { createServer } from "node:http";
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
15
16
|
import { readFile, stat } from "node:fs/promises";
|
|
16
17
|
import { join, extname, resolve } from "node:path";
|
|
17
18
|
import { fileURLToPath } from "node:url";
|
|
@@ -19,6 +20,7 @@ import { fileURLToPath } from "node:url";
|
|
|
19
20
|
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
20
21
|
const CLIENT_DIR = resolve(__dirname, "../client");
|
|
21
22
|
const STATIC_DIR = resolve(__dirname, "../static");
|
|
23
|
+
const PRERENDERED_DIR = resolve(__dirname, "../prerendered");
|
|
22
24
|
|
|
23
25
|
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : ${options.port};
|
|
24
26
|
const HOST = process.env.HOST ?? ${JSON.stringify(options.host)};
|
|
@@ -161,14 +163,58 @@ function __catmintErrorPage(statusCode, detail) {
|
|
|
161
163
|
}
|
|
162
164
|
|
|
163
165
|
function collectBody(req) {
|
|
166
|
+
const MAX_BODY_SIZE = ${options.maxBodySize};
|
|
164
167
|
return new Promise((resolve, reject) => {
|
|
165
168
|
const chunks = [];
|
|
166
|
-
|
|
169
|
+
let totalLength = 0;
|
|
170
|
+
req.on("data", (chunk) => {
|
|
171
|
+
totalLength += chunk.length;
|
|
172
|
+
if (totalLength > MAX_BODY_SIZE) {
|
|
173
|
+
req.destroy();
|
|
174
|
+
reject(new Error("Request body too large"));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
chunks.push(chunk);
|
|
178
|
+
});
|
|
167
179
|
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
168
180
|
req.on("error", reject);
|
|
169
181
|
});
|
|
170
182
|
}
|
|
171
183
|
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Server function error detection helpers (duck-typing)
|
|
186
|
+
//
|
|
187
|
+
// The adapter cannot import from \`catmint\` at runtime. Instead we detect
|
|
188
|
+
// error types by checking for characteristic properties.
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
function __isClientSafeErrorLike(value) {
|
|
192
|
+
if (!value || typeof value !== "object") return false;
|
|
193
|
+
if (value instanceof Error && typeof value.statusCode === "number") {
|
|
194
|
+
var proto = Object.getPrototypeOf(value);
|
|
195
|
+
while (proto && proto !== Error.prototype) {
|
|
196
|
+
if (proto.constructor && proto.constructor.name === "ClientSafeError") return true;
|
|
197
|
+
proto = Object.getPrototypeOf(proto);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function __isRedirectErrorLike(value) {
|
|
204
|
+
if (!value || typeof value !== "object") return false;
|
|
205
|
+
return (
|
|
206
|
+
value instanceof Error &&
|
|
207
|
+
value.name === "RedirectError" &&
|
|
208
|
+
typeof value.url === "string" &&
|
|
209
|
+
typeof value.status === "number"
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function __errorRef(err) {
|
|
214
|
+
var content = String(Date.now()) + ":" + (err instanceof Error ? err.message : String(err));
|
|
215
|
+
return createHash("sha256").update(content).digest("hex").slice(0, 8);
|
|
216
|
+
}
|
|
217
|
+
|
|
172
218
|
// Import the RSC and SSR entries for the rendering pipeline
|
|
173
219
|
const rscEntry = await import("../rsc/index.js");
|
|
174
220
|
const ssrEntry = await import("../ssr/index.js");
|
|
@@ -189,14 +235,76 @@ async function pipeWebStreamToResponse(webStream, res) {
|
|
|
189
235
|
}
|
|
190
236
|
}
|
|
191
237
|
|
|
238
|
+
function nodeReqToWebRequest(req) {
|
|
239
|
+
const protocol = "http";
|
|
240
|
+
const host = req.headers.host || \`\${HOST}:\${PORT}\`;
|
|
241
|
+
const reqUrl = new URL(req.url || "/", \`\${protocol}://\${host}\`);
|
|
242
|
+
const headers = new Headers();
|
|
243
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
244
|
+
if (value) {
|
|
245
|
+
if (Array.isArray(value)) {
|
|
246
|
+
for (const v of value) {
|
|
247
|
+
headers.append(key, v);
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
headers.set(key, value);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return new Request(reqUrl.href, {
|
|
255
|
+
method: req.method || "GET",
|
|
256
|
+
headers,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
192
260
|
const server = createServer(async (req, res) => {
|
|
261
|
+
${options.headerPreset === "baseline"
|
|
262
|
+
? ` // Baseline security headers
|
|
263
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
264
|
+
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
265
|
+
res.setHeader("X-Frame-Options", "SAMEORIGIN");
|
|
266
|
+
`
|
|
267
|
+
: ""}
|
|
193
268
|
const url = new URL(req.url || "/", \`http://\${HOST}:\${PORT}\`);
|
|
194
269
|
const pathname = url.pathname;
|
|
195
270
|
const method = (req.method || "GET").toUpperCase();
|
|
196
271
|
|
|
272
|
+
// Establish the request-scoped AsyncLocalStorage context.
|
|
273
|
+
// This makes cookies(), headers(), getPlatform() etc. available
|
|
274
|
+
// to server functions, middleware, API endpoints, and RSC rendering.
|
|
275
|
+
const __webRequest = nodeReqToWebRequest(req);
|
|
276
|
+
const __store = ssrEntry.createRequestStore(__webRequest, { req, res });
|
|
277
|
+
await ssrEntry.runWithRequestContext(__store, async () => {
|
|
278
|
+
|
|
279
|
+
// Helper: flush pending Set-Cookie headers and accumulated response
|
|
280
|
+
// headers from the request store onto the Node.js response object.
|
|
281
|
+
function __flushPendingHeaders() {
|
|
282
|
+
// Apply accumulated response headers
|
|
283
|
+
__store.responseHeaders.forEach(function(value, key) {
|
|
284
|
+
res.setHeader(key, value);
|
|
285
|
+
});
|
|
286
|
+
// Apply pending Set-Cookie headers
|
|
287
|
+
var cookies = [];
|
|
288
|
+
__store.pendingCookies.forEach(function(pending) {
|
|
289
|
+
cookies.push(pending.serialized);
|
|
290
|
+
});
|
|
291
|
+
if (cookies.length > 0) {
|
|
292
|
+
res.setHeader("Set-Cookie", cookies);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
197
296
|
try {
|
|
198
|
-
// 1. Serve
|
|
297
|
+
// 1. Serve client assets from dist/client/ (immutable for hashed assets)
|
|
298
|
+
// These have hashed filenames and never conflict with app routes.
|
|
199
299
|
{
|
|
300
|
+
const filePath = join(CLIENT_DIR, pathname);
|
|
301
|
+
const served = await serveStaticFile(res, filePath, pathname.includes("/assets/"));
|
|
302
|
+
if (served) return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// 2. Pre-rendered static pages WITHOUT middleware: serve directly from
|
|
306
|
+
// dist/static/ as a fast path — no middleware execution needed.
|
|
307
|
+
if (method === "GET") {
|
|
200
308
|
let staticPath = pathname;
|
|
201
309
|
if (!extname(staticPath)) {
|
|
202
310
|
staticPath = staticPath.endsWith("/")
|
|
@@ -208,41 +316,146 @@ const server = createServer(async (req, res) => {
|
|
|
208
316
|
if (served) return;
|
|
209
317
|
}
|
|
210
318
|
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
319
|
+
// 3. Middleware execution — runs for all remaining routes (server functions,
|
|
320
|
+
// endpoints, prerendered pages with middleware, RSC-rendered pages).
|
|
321
|
+
// For RSC flight requests, run middleware against the TARGET page path
|
|
322
|
+
// (from ?path= query param), not the literal /__catmint/rsc pathname.
|
|
323
|
+
var middlewareHeaders = null;
|
|
324
|
+
if (ssrEntry.executeMiddleware) {
|
|
325
|
+
try {
|
|
326
|
+
var middlewarePath = pathname;
|
|
327
|
+
if (pathname === "/__catmint/rsc") {
|
|
328
|
+
var rscTargetPath = url.searchParams.get("path");
|
|
329
|
+
if (rscTargetPath) middlewarePath = rscTargetPath;
|
|
330
|
+
}
|
|
331
|
+
var webRequest = nodeReqToWebRequest(req);
|
|
332
|
+
var mwResult = await ssrEntry.executeMiddleware(middlewarePath, webRequest);
|
|
333
|
+
|
|
334
|
+
if (mwResult.shortCircuit) {
|
|
335
|
+
// Middleware short-circuited — return its response directly
|
|
336
|
+
res.writeHead(mwResult.shortCircuit.status,
|
|
337
|
+
Object.fromEntries(mwResult.shortCircuit.headers));
|
|
338
|
+
__flushPendingHeaders();
|
|
339
|
+
var scBody = await mwResult.shortCircuit.text();
|
|
340
|
+
res.end(scBody);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Middleware passed — capture headers to merge into final response
|
|
345
|
+
if (mwResult.headers && typeof mwResult.headers.forEach === "function") {
|
|
346
|
+
middlewareHeaders = mwResult.headers;
|
|
347
|
+
}
|
|
348
|
+
} catch (err) {
|
|
349
|
+
console.error("Middleware error:", err);
|
|
350
|
+
if (!res.headersSent) {
|
|
351
|
+
__flushPendingHeaders();
|
|
352
|
+
res.writeHead(500);
|
|
353
|
+
res.end("Internal Server Error");
|
|
354
|
+
}
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Helper: merge middleware headers into the response before sending.
|
|
360
|
+
function applyMiddlewareHeaders() {
|
|
361
|
+
if (middlewareHeaders) {
|
|
362
|
+
middlewareHeaders.forEach(function(value, key) {
|
|
363
|
+
res.setHeader(key, value);
|
|
364
|
+
});
|
|
365
|
+
}
|
|
216
366
|
}
|
|
217
367
|
|
|
218
|
-
//
|
|
368
|
+
// 4. Handle server function RPC calls (/__catmint/fn/*)
|
|
219
369
|
if (pathname.startsWith("/__catmint/fn/") && ssrEntry.handleServerFn) {
|
|
220
370
|
try {
|
|
371
|
+
// Enforce POST method for server function calls
|
|
372
|
+
if (method !== "POST") {
|
|
373
|
+
__flushPendingHeaders();
|
|
374
|
+
sendJson(res, 405, { error: "Method " + method + " not allowed, expected POST" });
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Enforce Content-Type: application/json
|
|
379
|
+
const ct = (req.headers["content-type"] || "").toLowerCase();
|
|
380
|
+
if (ct && !ct.startsWith("application/json")) {
|
|
381
|
+
__flushPendingHeaders();
|
|
382
|
+
sendJson(res, 415, { error: "Unsupported Content-Type, expected application/json" });
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Validate Origin header to mitigate cross-site invocation
|
|
387
|
+
const origin = req.headers["origin"];
|
|
388
|
+
if (origin) {
|
|
389
|
+
const expected = \`http://\${HOST}:\${PORT}\`;
|
|
390
|
+
if (origin !== expected && origin !== \`https://\${HOST}:\${PORT}\`) {
|
|
391
|
+
__flushPendingHeaders();
|
|
392
|
+
sendJson(res, 403, { error: "Origin not allowed" });
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
221
397
|
const body = await collectBody(req);
|
|
222
398
|
let parsed;
|
|
223
399
|
try {
|
|
224
400
|
parsed = body.length > 0 ? JSON.parse(body.toString("utf-8")) : undefined;
|
|
225
401
|
} catch {
|
|
402
|
+
__flushPendingHeaders();
|
|
226
403
|
sendJson(res, 400, { error: "Invalid JSON body" });
|
|
227
404
|
return;
|
|
228
405
|
}
|
|
229
406
|
const result = await ssrEntry.handleServerFn(pathname, parsed);
|
|
230
407
|
if (result) {
|
|
408
|
+
// Check if the result IS a ClientSafeError (returned, not thrown)
|
|
409
|
+
if (__isClientSafeErrorLike(result.result)) {
|
|
410
|
+
applyMiddlewareHeaders();
|
|
411
|
+
__flushPendingHeaders();
|
|
412
|
+
sendJson(res, 200, {
|
|
413
|
+
__clientSafeError: true,
|
|
414
|
+
error: result.result.message,
|
|
415
|
+
statusCode: result.result.statusCode,
|
|
416
|
+
data: result.result.data,
|
|
417
|
+
});
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
applyMiddlewareHeaders();
|
|
421
|
+
__flushPendingHeaders();
|
|
231
422
|
sendJson(res, 200, result.result);
|
|
232
423
|
return;
|
|
233
424
|
}
|
|
425
|
+
__flushPendingHeaders();
|
|
234
426
|
sendJson(res, 404, { error: "Server function not found" });
|
|
235
427
|
} catch (err) {
|
|
236
|
-
|
|
237
|
-
|
|
428
|
+
// Always log full error server-side
|
|
429
|
+
console.error("[catmint] Server function error:", err);
|
|
430
|
+
|
|
431
|
+
// RedirectError — send redirect envelope
|
|
432
|
+
if (__isRedirectErrorLike(err)) {
|
|
433
|
+
__flushPendingHeaders();
|
|
434
|
+
sendJson(res, 200, { __redirect: true, url: err.url, status: err.status });
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ClientSafeError — developer opted in, expose to client
|
|
439
|
+
if (__isClientSafeErrorLike(err)) {
|
|
440
|
+
__flushPendingHeaders();
|
|
441
|
+
sendJson(res, err.statusCode, { error: err.message, data: err.data });
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Default: sanitize — generic message with correlation hash
|
|
446
|
+
const ref = __errorRef(err);
|
|
447
|
+
console.error("[catmint] Error reference [ref: " + ref + "] — see above for full error");
|
|
448
|
+
__flushPendingHeaders();
|
|
449
|
+
sendJson(res, 500, { error: "Internal Server Error [ref: " + ref + "]" });
|
|
238
450
|
}
|
|
239
451
|
return;
|
|
240
452
|
}
|
|
241
453
|
|
|
242
|
-
//
|
|
454
|
+
// 5. RSC flight stream for client-side navigation (/__catmint/rsc?path=...)
|
|
243
455
|
if (pathname === "/__catmint/rsc" && method === "GET" && rscEntry.render) {
|
|
244
456
|
const targetPath = url.searchParams.get("path");
|
|
245
457
|
if (!targetPath) {
|
|
458
|
+
__flushPendingHeaders();
|
|
246
459
|
res.setHeader("Content-Type", "application/json");
|
|
247
460
|
res.writeHead(400);
|
|
248
461
|
res.end(JSON.stringify({ error: "Missing ?path= parameter" }));
|
|
@@ -251,6 +464,8 @@ const server = createServer(async (req, res) => {
|
|
|
251
464
|
try {
|
|
252
465
|
const rscResult = await rscEntry.render(targetPath);
|
|
253
466
|
if (rscResult) {
|
|
467
|
+
applyMiddlewareHeaders();
|
|
468
|
+
__flushPendingHeaders();
|
|
254
469
|
res.writeHead(200, {
|
|
255
470
|
"Content-Type": "text/x-component; charset=utf-8",
|
|
256
471
|
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
@@ -258,12 +473,14 @@ const server = createServer(async (req, res) => {
|
|
|
258
473
|
await pipeWebStreamToResponse(rscResult.stream, res);
|
|
259
474
|
return;
|
|
260
475
|
}
|
|
476
|
+
__flushPendingHeaders();
|
|
261
477
|
res.setHeader("Content-Type", "application/json");
|
|
262
478
|
res.writeHead(404);
|
|
263
479
|
res.end(JSON.stringify({ error: "No matching route" }));
|
|
264
480
|
} catch (err) {
|
|
265
481
|
console.error("RSC navigation error:", err);
|
|
266
482
|
if (!res.headersSent) {
|
|
483
|
+
__flushPendingHeaders();
|
|
267
484
|
res.setHeader("Content-Type", "application/json");
|
|
268
485
|
res.writeHead(500);
|
|
269
486
|
res.end(JSON.stringify({ error: "RSC navigation error" }));
|
|
@@ -272,7 +489,7 @@ const server = createServer(async (req, res) => {
|
|
|
272
489
|
return;
|
|
273
490
|
}
|
|
274
491
|
|
|
275
|
-
//
|
|
492
|
+
// 6. API endpoint handling
|
|
276
493
|
if (ssrEntry.hasEndpoint && ssrEntry.hasEndpoint(pathname)) {
|
|
277
494
|
try {
|
|
278
495
|
const protocol = "http";
|
|
@@ -295,7 +512,11 @@ const server = createServer(async (req, res) => {
|
|
|
295
512
|
const result = await ssrEntry.handleEndpoint(pathname, method, webRequest);
|
|
296
513
|
|
|
297
514
|
if (result) {
|
|
298
|
-
|
|
515
|
+
// Copy response headers then merge middleware headers
|
|
516
|
+
var epHeaders = Object.fromEntries(result.response.headers);
|
|
517
|
+
res.writeHead(result.response.status, epHeaders);
|
|
518
|
+
applyMiddlewareHeaders();
|
|
519
|
+
__flushPendingHeaders();
|
|
299
520
|
if (result.response.body) {
|
|
300
521
|
await pipeWebStreamToResponse(result.response.body, res);
|
|
301
522
|
} else {
|
|
@@ -305,24 +526,64 @@ const server = createServer(async (req, res) => {
|
|
|
305
526
|
}
|
|
306
527
|
|
|
307
528
|
// Endpoint exists but method not handled
|
|
529
|
+
__flushPendingHeaders();
|
|
308
530
|
res.writeHead(405, { "Content-Type": "text/plain" });
|
|
309
531
|
res.end(\`Method \${method} not allowed\`);
|
|
310
532
|
return;
|
|
311
533
|
} catch (err) {
|
|
534
|
+
// redirect() throws a RedirectError — convert to a real HTTP redirect
|
|
535
|
+
if (__isRedirectErrorLike(err)) {
|
|
536
|
+
__flushPendingHeaders();
|
|
537
|
+
res.writeHead(err.status, { "Location": err.url });
|
|
538
|
+
res.end();
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
312
541
|
console.error("Endpoint error:", err);
|
|
313
542
|
if (!res.headersSent) {
|
|
543
|
+
__flushPendingHeaders();
|
|
314
544
|
sendText(res, 500, "Internal Server Error");
|
|
315
545
|
}
|
|
316
546
|
return;
|
|
317
547
|
}
|
|
318
548
|
}
|
|
319
549
|
|
|
320
|
-
//
|
|
550
|
+
// 7. Pre-rendered pages WITH middleware: serve from dist/prerendered/
|
|
551
|
+
// These are static routes that have middleware ancestors — their HTML
|
|
552
|
+
// was pre-rendered but they must go through the middleware pipeline.
|
|
553
|
+
if (method === "GET") {
|
|
554
|
+
let prerenderedPath = pathname;
|
|
555
|
+
if (!extname(prerenderedPath)) {
|
|
556
|
+
prerenderedPath = prerenderedPath.endsWith("/")
|
|
557
|
+
? prerenderedPath + "index.html"
|
|
558
|
+
: prerenderedPath + ".html";
|
|
559
|
+
}
|
|
560
|
+
try {
|
|
561
|
+
const prFilePath = join(PRERENDERED_DIR, prerenderedPath);
|
|
562
|
+
const prStats = await stat(prFilePath);
|
|
563
|
+
if (prStats.isFile()) {
|
|
564
|
+
const content = await readFile(prFilePath);
|
|
565
|
+
applyMiddlewareHeaders();
|
|
566
|
+
__flushPendingHeaders();
|
|
567
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
568
|
+
res.setHeader("Content-Length", content.byteLength);
|
|
569
|
+
res.setHeader("Cache-Control", "public, max-age=0, must-revalidate");
|
|
570
|
+
res.writeHead(200);
|
|
571
|
+
res.end(content);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
} catch {
|
|
575
|
+
// No prerendered file — continue to RSC rendering
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// 8. RSC → SSR page rendering pipeline
|
|
321
580
|
if (method === "GET" && rscEntry.render) {
|
|
322
581
|
try {
|
|
323
582
|
const rscResult = await rscEntry.render(pathname);
|
|
324
583
|
if (rscResult) {
|
|
325
584
|
const htmlStream = await ssrEntry.renderToHtml(rscResult.stream, rscResult.headConfig);
|
|
585
|
+
applyMiddlewareHeaders();
|
|
586
|
+
__flushPendingHeaders();
|
|
326
587
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
327
588
|
await pipeWebStreamToResponse(htmlStream, res);
|
|
328
589
|
return;
|
|
@@ -330,18 +591,30 @@ const server = createServer(async (req, res) => {
|
|
|
330
591
|
} catch (err) {
|
|
331
592
|
console.error("RSC render error:", err);
|
|
332
593
|
if (!res.headersSent) {
|
|
594
|
+
__flushPendingHeaders();
|
|
333
595
|
await sendStatusPage(res, 500, pathname);
|
|
334
596
|
}
|
|
335
597
|
return;
|
|
336
598
|
}
|
|
337
599
|
}
|
|
338
600
|
|
|
339
|
-
//
|
|
601
|
+
// 9. Static assets fallback (dist/static/ for public files like favicon, etc.)
|
|
602
|
+
{
|
|
603
|
+
const filePath = join(STATIC_DIR, pathname);
|
|
604
|
+
const served = await serveStaticFile(res, filePath, false);
|
|
605
|
+
if (served) return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// 10. 404
|
|
609
|
+
__flushPendingHeaders();
|
|
340
610
|
await sendStatusPage(res, 404, pathname);
|
|
341
611
|
} catch (err) {
|
|
342
612
|
console.error("Request error:", err);
|
|
613
|
+
__flushPendingHeaders();
|
|
343
614
|
await sendStatusPage(res, 500, pathname);
|
|
344
615
|
}
|
|
616
|
+
|
|
617
|
+
}); // end runWithRequestContext
|
|
345
618
|
});
|
|
346
619
|
|
|
347
620
|
server.listen(PORT, HOST, () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-template.js","sourceRoot":"","sources":["../src/server-template.ts"],"names":[],"mappings":"AAAA,gEAAgE;
|
|
1
|
+
{"version":3,"file":"server-template.js","sourceRoot":"","sources":["../src/server-template.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAWhE;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CACjC,SAAsB,EACtB,OAAsB;IAEtB,OAAO;;;;;;;;;;;;mEAY0D,OAAO,CAAC,IAAI;mCAC5C,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0BA4IrC,OAAO,CAAC,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgG3C,OAAO,CAAC,YAAY,KAAK,UAAU;QACjC,CAAC,CAAC;;;;CAIL;QACG,CAAC,CAAC,EACN;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqXC,CAAC;AACF,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@catmint/adapter-node",
|
|
3
|
-
"version": "0.0.0-prealpha.
|
|
3
|
+
"version": "0.0.0-prealpha.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
|
-
"license": "
|
|
6
|
+
"license": "GPL-2.0",
|
|
7
7
|
"files": [
|
|
8
8
|
"dist"
|
|
9
9
|
],
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"catmint": "0.0.0-prealpha.
|
|
17
|
+
"catmint": "0.0.0-prealpha.21"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"typescript": "^5.7.0"
|