@catmint/cli 0.0.0-prealpha.1 → 0.0.0-prealpha.10
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/README.md +52 -0
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +269 -108
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +103 -23
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +305 -6
- package/dist/commands/start.js.map +1 -1
- package/dist/route-analysis.d.ts +41 -0
- package/dist/route-analysis.d.ts.map +1 -0
- package/dist/route-analysis.js +177 -0
- package/dist/route-analysis.js.map +1 -0
- package/package.json +4 -3
package/dist/commands/dev.js
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
import { resolve, basename } from "node:path";
|
|
3
3
|
import { networkInterfaces } from "node:os";
|
|
4
4
|
import { watch } from "node:fs";
|
|
5
|
-
import { createServer } from "vite";
|
|
5
|
+
import { createServer, createLogger } from "vite";
|
|
6
6
|
import { loadConfig } from "catmint/config";
|
|
7
7
|
import { scanRoutes } from "catmint/routing";
|
|
8
8
|
import { loadEnv } from "catmint/env";
|
|
9
9
|
import catmintVite from "@catmint/vite";
|
|
10
|
+
import pc from "picocolors";
|
|
11
|
+
import { detectRouteMode } from "../route-analysis.js";
|
|
10
12
|
const VERSION = "0.1.0";
|
|
11
13
|
function parseFlags(args) {
|
|
12
14
|
const flags = {};
|
|
@@ -37,36 +39,98 @@ function getNetworkAddress() {
|
|
|
37
39
|
return undefined;
|
|
38
40
|
}
|
|
39
41
|
/**
|
|
40
|
-
*
|
|
42
|
+
* Rendering mode symbols and colors.
|
|
43
|
+
*/
|
|
44
|
+
const MODE_SYMBOL = {
|
|
45
|
+
static: "○",
|
|
46
|
+
cached: "●",
|
|
47
|
+
dynamic: "λ",
|
|
48
|
+
};
|
|
49
|
+
const MODE_COLOR = {
|
|
50
|
+
static: pc.white,
|
|
51
|
+
cached: pc.green,
|
|
52
|
+
dynamic: pc.cyan,
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Color a HTTP method string.
|
|
56
|
+
*/
|
|
57
|
+
function colorMethod(method) {
|
|
58
|
+
switch (method) {
|
|
59
|
+
case "GET":
|
|
60
|
+
return pc.green(method);
|
|
61
|
+
case "POST":
|
|
62
|
+
return pc.yellow(method);
|
|
63
|
+
case "PUT":
|
|
64
|
+
return pc.blue(method);
|
|
65
|
+
case "DELETE":
|
|
66
|
+
return pc.red(method);
|
|
67
|
+
case "PATCH":
|
|
68
|
+
return pc.magenta(method);
|
|
69
|
+
default:
|
|
70
|
+
return pc.white(method);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Build a formatted route table for display with rendering mode indicators.
|
|
41
75
|
*/
|
|
42
76
|
function formatRouteTable(routes) {
|
|
43
|
-
|
|
77
|
+
// Build route entries with rendering mode, collecting display + plain lines
|
|
78
|
+
const entries = [];
|
|
44
79
|
for (const route of routes) {
|
|
45
80
|
const file = basename(route.filePath);
|
|
81
|
+
const mode = detectRouteMode(route.filePath, route.type);
|
|
82
|
+
const symbol = MODE_COLOR[mode](MODE_SYMBOL[mode]);
|
|
46
83
|
if (route.type === "page") {
|
|
47
|
-
|
|
84
|
+
const plain = ` ${MODE_SYMBOL[mode]} ${padEnd("GET", 6)}${padEnd(route.pattern, 20)} → ${file}`;
|
|
85
|
+
const display = ` ${symbol} ${padEnd(colorMethod("GET"), 6 + (colorMethod("GET").length - 3))}${padEnd(route.pattern, 20)} → ${file}`;
|
|
86
|
+
entries.push({ display, plain });
|
|
48
87
|
}
|
|
49
88
|
else if (route.methods && route.methods.length > 0) {
|
|
50
89
|
for (const method of route.methods) {
|
|
51
90
|
const m = method === "default" ? "ALL" : method;
|
|
52
|
-
|
|
91
|
+
const plain = ` ${MODE_SYMBOL[mode]} ${padEnd(m, 6)}${padEnd(route.pattern, 20)} → ${file}`;
|
|
92
|
+
const display = ` ${symbol} ${padEnd(colorMethod(m), 6 + (colorMethod(m).length - m.length))}${padEnd(route.pattern, 20)} → ${file}`;
|
|
93
|
+
entries.push({ display, plain });
|
|
53
94
|
}
|
|
54
95
|
}
|
|
55
96
|
else {
|
|
56
|
-
|
|
97
|
+
const plain = ` ${MODE_SYMBOL[mode]} ${padEnd("ALL", 6)}${padEnd(route.pattern, 20)} → ${file}`;
|
|
98
|
+
const display = ` ${symbol} ${padEnd(colorMethod("ALL"), 6 + (colorMethod("ALL").length - 3))}${padEnd(route.pattern, 20)} → ${file}`;
|
|
99
|
+
entries.push({ display, plain });
|
|
57
100
|
}
|
|
58
101
|
}
|
|
59
|
-
if (
|
|
60
|
-
|
|
102
|
+
if (entries.length === 0) {
|
|
103
|
+
const noRoutes = " (no routes found)";
|
|
104
|
+
entries.push({ display: noRoutes, plain: noRoutes });
|
|
61
105
|
}
|
|
62
|
-
// Calculate box width
|
|
63
|
-
const maxLen = Math.max(...
|
|
106
|
+
// Calculate box width based on plain-text (no ANSI) widths
|
|
107
|
+
const maxLen = Math.max(...entries.map((e) => e.plain.length), 20);
|
|
64
108
|
const boxWidth = maxLen + 4;
|
|
65
109
|
const top = ` ┌${"─".repeat(boxWidth)}┐`;
|
|
66
110
|
const bottom = ` └${"─".repeat(boxWidth)}┘`;
|
|
67
|
-
const header = ` │ Routes:${" ".repeat(boxWidth - 10)}│`;
|
|
68
|
-
const boxedLines =
|
|
69
|
-
|
|
111
|
+
const header = ` │ Routes:${" ".repeat(boxWidth - 10)} │`;
|
|
112
|
+
const boxedLines = entries.map((e) => {
|
|
113
|
+
const pad = boxWidth - e.plain.length;
|
|
114
|
+
return ` │${e.display}${" ".repeat(pad)}│`;
|
|
115
|
+
});
|
|
116
|
+
// Route count summary
|
|
117
|
+
const pageCount = routes.filter((r) => r.type === "page").length;
|
|
118
|
+
const endpointCount = routes.filter((r) => r.type === "endpoint").length;
|
|
119
|
+
const parts = [];
|
|
120
|
+
if (pageCount > 0)
|
|
121
|
+
parts.push(`${pageCount} page${pageCount !== 1 ? "s" : ""}`);
|
|
122
|
+
if (endpointCount > 0)
|
|
123
|
+
parts.push(`${endpointCount} endpoint${endpointCount !== 1 ? "s" : ""}`);
|
|
124
|
+
const countLine = parts.length > 0
|
|
125
|
+
? ` ${pc.dim(`${routes.length} route${routes.length !== 1 ? "s" : ""} (${parts.join(", ")})`)}`
|
|
126
|
+
: "";
|
|
127
|
+
// Legend
|
|
128
|
+
const legend = ` ${MODE_COLOR.static(MODE_SYMBOL.static)} Static ${MODE_COLOR.cached(MODE_SYMBOL.cached)} Cached ${MODE_COLOR.dynamic(MODE_SYMBOL.dynamic)} Dynamic`;
|
|
129
|
+
const result = [top, header, ...boxedLines, bottom];
|
|
130
|
+
if (countLine)
|
|
131
|
+
result.push(countLine);
|
|
132
|
+
result.push(legend);
|
|
133
|
+
return result.join("\n");
|
|
70
134
|
}
|
|
71
135
|
function padEnd(str, len) {
|
|
72
136
|
return str.length >= len ? str + " " : str + " ".repeat(len - str.length + 1);
|
|
@@ -102,23 +166,27 @@ export async function dev(args) {
|
|
|
102
166
|
catch {
|
|
103
167
|
routes = [];
|
|
104
168
|
}
|
|
105
|
-
// Display startup
|
|
169
|
+
// Display startup header (before Vite starts, so user sees immediate feedback)
|
|
106
170
|
console.log();
|
|
107
171
|
console.log(` catmint v${VERSION} dev server`);
|
|
108
172
|
console.log();
|
|
109
173
|
console.log(formatRouteTable(routes));
|
|
110
174
|
console.log();
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
175
|
+
// Create a custom logger that suppresses Vite's port-in-use message
|
|
176
|
+
// (we display our own, more informative version after server.listen())
|
|
177
|
+
const logger = createLogger(undefined, { allowClearScreen: false });
|
|
178
|
+
const originalInfo = logger.info.bind(logger);
|
|
179
|
+
logger.info = (msg, options) => {
|
|
180
|
+
if (msg.includes("Port") && msg.includes("is in use"))
|
|
181
|
+
return;
|
|
182
|
+
originalInfo(msg, options);
|
|
183
|
+
};
|
|
117
184
|
// Create Vite dev server with Catmint plugin
|
|
118
185
|
// Merge user's vite.plugins with catmintVite() instead of overwriting
|
|
119
186
|
const { plugins: userPlugins, ...viteRest } = config.vite;
|
|
120
187
|
const server = await createServer({
|
|
121
188
|
root: rootDir,
|
|
189
|
+
customLogger: logger,
|
|
122
190
|
server: {
|
|
123
191
|
port,
|
|
124
192
|
host,
|
|
@@ -138,6 +206,20 @@ export async function dev(args) {
|
|
|
138
206
|
...viteRest,
|
|
139
207
|
});
|
|
140
208
|
await server.listen();
|
|
209
|
+
// Display the actual URLs after Vite has resolved the port
|
|
210
|
+
// (the port may differ from the requested one if it was already in use)
|
|
211
|
+
const addr = server.httpServer?.address();
|
|
212
|
+
const resolvedPort = typeof addr === "object" && addr ? addr.port : port;
|
|
213
|
+
if (resolvedPort !== port) {
|
|
214
|
+
console.log(pc.yellow(` Port ${port} is in use, using ${resolvedPort} instead`));
|
|
215
|
+
console.log();
|
|
216
|
+
}
|
|
217
|
+
console.log(` Local: http://${host}:${resolvedPort}`);
|
|
218
|
+
const networkAddr = getNetworkAddress();
|
|
219
|
+
if (networkAddr) {
|
|
220
|
+
console.log(` Network: http://${networkAddr}:${resolvedPort}`);
|
|
221
|
+
}
|
|
222
|
+
console.log();
|
|
141
223
|
// Watch app/ directory for route changes
|
|
142
224
|
try {
|
|
143
225
|
const watcher = watch(appDir, { recursive: true }, async (eventType, filename) => {
|
|
@@ -145,10 +227,8 @@ export async function dev(args) {
|
|
|
145
227
|
return;
|
|
146
228
|
const base = basename(filename);
|
|
147
229
|
if (base === "page.tsx" ||
|
|
148
|
-
base === "page.jsx" ||
|
|
149
230
|
base === "page.mdx" ||
|
|
150
|
-
base === "endpoint.ts"
|
|
151
|
-
base === "endpoint.js") {
|
|
231
|
+
base === "endpoint.ts") {
|
|
152
232
|
try {
|
|
153
233
|
const updatedRoutes = await scanRoutes(appDir);
|
|
154
234
|
console.log();
|
package/dist/commands/dev.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev.js","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAElD,OAAO,EAAE,OAAO,EAAY,QAAQ,EAAE,MAAM,WAAW,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,YAAY,EAAsB,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"dev.js","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start.d.ts","sourceRoot":"","sources":["../../src/commands/start.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"start.d.ts","sourceRoot":"","sources":["../../src/commands/start.ts"],"names":[],"mappings":"AA4SA;;;;;GAKG;AACH,wBAAsB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA2mBzD"}
|
package/dist/commands/start.js
CHANGED
|
@@ -14,6 +14,7 @@ import { networkInterfaces } from "node:os";
|
|
|
14
14
|
import { pathToFileURL } from "node:url";
|
|
15
15
|
import { errorPageHtml } from "@catmint/vite";
|
|
16
16
|
import { shouldAutoPreflight, buildPreflightHeaders } from "../cors-utils.js";
|
|
17
|
+
import pc from "picocolors";
|
|
17
18
|
function parseFlags(args) {
|
|
18
19
|
const flags = {};
|
|
19
20
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -25,6 +26,119 @@ function parseFlags(args) {
|
|
|
25
26
|
}
|
|
26
27
|
return flags;
|
|
27
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Rendering mode symbols and colors.
|
|
31
|
+
*/
|
|
32
|
+
const MODE_SYMBOL = {
|
|
33
|
+
static: "○",
|
|
34
|
+
cached: "●",
|
|
35
|
+
dynamic: "λ",
|
|
36
|
+
};
|
|
37
|
+
const MODE_COLOR = {
|
|
38
|
+
static: pc.white,
|
|
39
|
+
cached: pc.green,
|
|
40
|
+
dynamic: pc.cyan,
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Color a HTTP method string.
|
|
44
|
+
*/
|
|
45
|
+
function colorMethod(method) {
|
|
46
|
+
switch (method) {
|
|
47
|
+
case "GET":
|
|
48
|
+
return pc.green(method);
|
|
49
|
+
case "POST":
|
|
50
|
+
return pc.yellow(method);
|
|
51
|
+
case "PUT":
|
|
52
|
+
return pc.blue(method);
|
|
53
|
+
case "DELETE":
|
|
54
|
+
return pc.red(method);
|
|
55
|
+
case "PATCH":
|
|
56
|
+
return pc.magenta(method);
|
|
57
|
+
default:
|
|
58
|
+
return pc.white(method);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function padEnd(str, len) {
|
|
62
|
+
return str.length >= len ? str + " " : str + " ".repeat(len - str.length + 1);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Derive the rendering mode from a manifest route entry.
|
|
66
|
+
*
|
|
67
|
+
* - Has explicit `renderMode` → use it directly
|
|
68
|
+
* - Has `cache` options → `"cached"` (ISR)
|
|
69
|
+
* - Endpoint → `"dynamic"`
|
|
70
|
+
* - Page without cache → `"dynamic"`
|
|
71
|
+
*/
|
|
72
|
+
function manifestRouteMode(route) {
|
|
73
|
+
if (route.renderMode)
|
|
74
|
+
return route.renderMode;
|
|
75
|
+
if (route.type === "endpoint")
|
|
76
|
+
return "dynamic";
|
|
77
|
+
if (route.cache)
|
|
78
|
+
return "cached";
|
|
79
|
+
return "dynamic";
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Build a formatted route table from manifest data for production display.
|
|
83
|
+
*/
|
|
84
|
+
function formatManifestRouteTable(manifest) {
|
|
85
|
+
const allRoutes = [...manifest.routes, ...manifest.endpoints];
|
|
86
|
+
const entries = [];
|
|
87
|
+
for (const route of allRoutes) {
|
|
88
|
+
const source = route.source.split("/").pop() ?? route.source;
|
|
89
|
+
const mode = manifestRouteMode(route);
|
|
90
|
+
const symbol = MODE_COLOR[mode](MODE_SYMBOL[mode]);
|
|
91
|
+
if (route.type === "page") {
|
|
92
|
+
const plain = ` ${MODE_SYMBOL[mode]} ${padEnd("GET", 6)}${padEnd(route.path, 20)} → ${source}`;
|
|
93
|
+
const display = ` ${symbol} ${padEnd(colorMethod("GET"), 6 + (colorMethod("GET").length - 3))}${padEnd(route.path, 20)} → ${source}`;
|
|
94
|
+
entries.push({ display, plain });
|
|
95
|
+
}
|
|
96
|
+
else if (route.methods && route.methods.length > 0) {
|
|
97
|
+
for (const method of route.methods) {
|
|
98
|
+
const m = method === "default" ? "ALL" : method;
|
|
99
|
+
const plain = ` ${MODE_SYMBOL[mode]} ${padEnd(m, 6)}${padEnd(route.path, 20)} → ${source}`;
|
|
100
|
+
const display = ` ${symbol} ${padEnd(colorMethod(m), 6 + (colorMethod(m).length - m.length))}${padEnd(route.path, 20)} → ${source}`;
|
|
101
|
+
entries.push({ display, plain });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
const plain = ` ${MODE_SYMBOL[mode]} ${padEnd("ALL", 6)}${padEnd(route.path, 20)} → ${source}`;
|
|
106
|
+
const display = ` ${symbol} ${padEnd(colorMethod("ALL"), 6 + (colorMethod("ALL").length - 3))}${padEnd(route.path, 20)} → ${source}`;
|
|
107
|
+
entries.push({ display, plain });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (entries.length === 0) {
|
|
111
|
+
const noRoutes = " (no routes found)";
|
|
112
|
+
entries.push({ display: noRoutes, plain: noRoutes });
|
|
113
|
+
}
|
|
114
|
+
const maxLen = Math.max(...entries.map((e) => e.plain.length), 20);
|
|
115
|
+
const boxWidth = maxLen + 4;
|
|
116
|
+
const top = ` ┌${"─".repeat(boxWidth)}┐`;
|
|
117
|
+
const bottom = ` └${"─".repeat(boxWidth)}┘`;
|
|
118
|
+
const header = ` │ Routes:${" ".repeat(boxWidth - 10)} │`;
|
|
119
|
+
const boxedLines = entries.map((e) => {
|
|
120
|
+
const pad = boxWidth - e.plain.length;
|
|
121
|
+
return ` │${e.display}${" ".repeat(pad)}│`;
|
|
122
|
+
});
|
|
123
|
+
// Route count summary
|
|
124
|
+
const pageCount = manifest.routes.length;
|
|
125
|
+
const endpointCount = manifest.endpoints.length;
|
|
126
|
+
const totalCount = pageCount + endpointCount;
|
|
127
|
+
const parts = [];
|
|
128
|
+
if (pageCount > 0)
|
|
129
|
+
parts.push(`${pageCount} page${pageCount !== 1 ? "s" : ""}`);
|
|
130
|
+
if (endpointCount > 0)
|
|
131
|
+
parts.push(`${endpointCount} endpoint${endpointCount !== 1 ? "s" : ""}`);
|
|
132
|
+
const countLine = parts.length > 0
|
|
133
|
+
? ` ${pc.dim(`${totalCount} route${totalCount !== 1 ? "s" : ""} (${parts.join(", ")})`)}`
|
|
134
|
+
: "";
|
|
135
|
+
const legend = ` ${MODE_COLOR.static(MODE_SYMBOL.static)} Static ${MODE_COLOR.cached(MODE_SYMBOL.cached)} Cached ${MODE_COLOR.dynamic(MODE_SYMBOL.dynamic)} Dynamic`;
|
|
136
|
+
const result = [top, header, ...boxedLines, bottom];
|
|
137
|
+
if (countLine)
|
|
138
|
+
result.push(countLine);
|
|
139
|
+
result.push(legend);
|
|
140
|
+
return result.join("\n");
|
|
141
|
+
}
|
|
28
142
|
/**
|
|
29
143
|
* Common MIME types for static file serving.
|
|
30
144
|
*/
|
|
@@ -123,6 +237,32 @@ async function pipeWebStreamToResponse(webStream, res) {
|
|
|
123
237
|
res.end();
|
|
124
238
|
}
|
|
125
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Convert a Node.js IncomingMessage to a Web Request object.
|
|
242
|
+
* Used for middleware execution which operates on Web standards.
|
|
243
|
+
*/
|
|
244
|
+
function nodeReqToWebRequest(req) {
|
|
245
|
+
const protocol = "http";
|
|
246
|
+
const host = req.headers.host ?? "localhost";
|
|
247
|
+
const url = new URL(req.url ?? "/", `${protocol}://${host}`);
|
|
248
|
+
const headers = new Headers();
|
|
249
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
250
|
+
if (value) {
|
|
251
|
+
if (Array.isArray(value)) {
|
|
252
|
+
for (const v of value) {
|
|
253
|
+
headers.append(key, v);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
headers.set(key, value);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return new Request(url.href, {
|
|
262
|
+
method: req.method ?? "GET",
|
|
263
|
+
headers,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
126
266
|
/**
|
|
127
267
|
* Start the Catmint production server.
|
|
128
268
|
*
|
|
@@ -133,7 +273,7 @@ export async function start(args) {
|
|
|
133
273
|
const flags = parseFlags(args);
|
|
134
274
|
const rootDir = process.cwd();
|
|
135
275
|
// Read port/host from manifest config first, then CLI flags override
|
|
136
|
-
let port =
|
|
276
|
+
let port = 6468;
|
|
137
277
|
let host = "0.0.0.0";
|
|
138
278
|
// We'll read these from the manifest after loading it, but CLI flags take precedence
|
|
139
279
|
const portFlag = typeof flags.port === "string" ? parseInt(flags.port, 10) : undefined;
|
|
@@ -158,13 +298,15 @@ export async function start(args) {
|
|
|
158
298
|
console.error(" Error: Failed to parse app_manifest.json:", err);
|
|
159
299
|
process.exit(1);
|
|
160
300
|
}
|
|
161
|
-
// Apply port/host from manifest config, then override with CLI flags
|
|
301
|
+
// Apply port/host/maxBodySize from manifest config, then override with CLI flags
|
|
162
302
|
if (manifest.config?.server?.port) {
|
|
163
303
|
port = manifest.config.server.port;
|
|
164
304
|
}
|
|
165
305
|
if (manifest.config?.server?.host) {
|
|
166
306
|
host = manifest.config.server.host;
|
|
167
307
|
}
|
|
308
|
+
const maxBodySize = manifest.config?.server?.maxBodySize ?? 1_048_576;
|
|
309
|
+
const headerPreset = manifest.config?.security?.headerPreset ?? "baseline";
|
|
168
310
|
if (portFlag !== undefined) {
|
|
169
311
|
port = portFlag;
|
|
170
312
|
}
|
|
@@ -175,6 +317,7 @@ export async function start(args) {
|
|
|
175
317
|
const rscDir = join(distDir, "rsc");
|
|
176
318
|
const ssrDir = join(distDir, "ssr");
|
|
177
319
|
const staticDir = join(distDir, "static");
|
|
320
|
+
const prerenderedDir = join(distDir, "prerendered");
|
|
178
321
|
// 3. Load RSC entry — renders pages to RSC flight streams
|
|
179
322
|
let rscEntry;
|
|
180
323
|
const rscEntryPath = join(rscDir, "index.js");
|
|
@@ -248,6 +391,12 @@ export async function start(args) {
|
|
|
248
391
|
}
|
|
249
392
|
// 5. Create HTTP server
|
|
250
393
|
const server = createServer(async (req, res) => {
|
|
394
|
+
// Baseline security headers (configurable via security.headerPreset)
|
|
395
|
+
if (headerPreset === "baseline") {
|
|
396
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
397
|
+
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
398
|
+
res.setHeader("X-Frame-Options", "SAMEORIGIN");
|
|
399
|
+
}
|
|
251
400
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
252
401
|
const urlPath = url.pathname;
|
|
253
402
|
// Client assets (dist/client/) have hashed filenames and never conflict
|
|
@@ -255,14 +404,125 @@ export async function start(args) {
|
|
|
255
404
|
if (tryServeStatic(req, res, urlPath, clientDir, true)) {
|
|
256
405
|
return;
|
|
257
406
|
}
|
|
407
|
+
// Pre-rendered static pages WITHOUT middleware: serve directly from
|
|
408
|
+
// dist/static/ as a fast path — no middleware execution needed.
|
|
409
|
+
if (req.method === "GET") {
|
|
410
|
+
let staticHtmlPath = urlPath;
|
|
411
|
+
if (!extname(staticHtmlPath)) {
|
|
412
|
+
staticHtmlPath = staticHtmlPath.endsWith("/")
|
|
413
|
+
? staticHtmlPath + "index.html"
|
|
414
|
+
: staticHtmlPath + ".html";
|
|
415
|
+
}
|
|
416
|
+
if (tryServeStatic(req, res, staticHtmlPath, staticDir, false)) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// --- Middleware execution ---
|
|
421
|
+
// For all remaining routes (server functions, endpoints, prerendered
|
|
422
|
+
// pages with middleware, RSC-rendered pages), execute the middleware
|
|
423
|
+
// chain first. If middleware short-circuits, return its response.
|
|
424
|
+
// For RSC flight requests, run middleware against the TARGET page path
|
|
425
|
+
// (from ?path= query param), not the literal /__catmint/rsc pathname.
|
|
426
|
+
let middlewareHeaders;
|
|
427
|
+
if (ssrEntry?.executeMiddleware) {
|
|
428
|
+
try {
|
|
429
|
+
let middlewarePath = urlPath;
|
|
430
|
+
if (urlPath === "/__catmint/rsc") {
|
|
431
|
+
const parsedUrl = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
432
|
+
const rscTargetPath = parsedUrl.searchParams.get("path");
|
|
433
|
+
if (rscTargetPath)
|
|
434
|
+
middlewarePath = rscTargetPath;
|
|
435
|
+
}
|
|
436
|
+
const webRequest = nodeReqToWebRequest(req);
|
|
437
|
+
const mwResult = await ssrEntry.executeMiddleware(middlewarePath, webRequest);
|
|
438
|
+
if (mwResult.shortCircuit) {
|
|
439
|
+
// Middleware short-circuited — return its response directly
|
|
440
|
+
res.statusCode = mwResult.shortCircuit.status;
|
|
441
|
+
mwResult.shortCircuit.headers.forEach((value, key) => {
|
|
442
|
+
res.setHeader(key, value);
|
|
443
|
+
});
|
|
444
|
+
const body = await mwResult.shortCircuit.text();
|
|
445
|
+
res.end(body);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
// Middleware passed — capture headers to merge into final response
|
|
449
|
+
if (mwResult.headers &&
|
|
450
|
+
typeof mwResult.headers.forEach === "function") {
|
|
451
|
+
middlewareHeaders = mwResult.headers;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
catch (err) {
|
|
455
|
+
console.error(" Middleware error:", err);
|
|
456
|
+
if (!res.headersSent) {
|
|
457
|
+
res.statusCode = 500;
|
|
458
|
+
res.end("Internal Server Error");
|
|
459
|
+
}
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Helper: merge middleware headers into the response before sending.
|
|
465
|
+
*/
|
|
466
|
+
function applyMiddlewareHeaders() {
|
|
467
|
+
if (middlewareHeaders) {
|
|
468
|
+
middlewareHeaders.forEach((value, key) => {
|
|
469
|
+
res.setHeader(key, value);
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
258
473
|
// Server function RPC handling (/__catmint/fn/<basePath>/<hash>)
|
|
474
|
+
// NOTE: No built-in rate limiting is applied here. For production
|
|
475
|
+
// deployments, configure rate limiting at the edge/WAF/reverse-proxy
|
|
476
|
+
// layer or use a framework middleware hook.
|
|
259
477
|
const SERVER_FN_PREFIX = "/__catmint/fn/";
|
|
260
478
|
if (urlPath.startsWith(SERVER_FN_PREFIX) && ssrEntry?.handleServerFn) {
|
|
261
479
|
try {
|
|
262
|
-
|
|
480
|
+
const reqMethod = (req.method ?? "GET").toUpperCase();
|
|
481
|
+
// Enforce POST method for server function calls
|
|
482
|
+
if (reqMethod !== "POST") {
|
|
483
|
+
res.statusCode = 405;
|
|
484
|
+
res.setHeader("Content-Type", "application/json");
|
|
485
|
+
res.end(JSON.stringify({
|
|
486
|
+
error: `Method ${reqMethod} not allowed, expected POST`,
|
|
487
|
+
}));
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
// Enforce Content-Type: application/json
|
|
491
|
+
const ct = (req.headers["content-type"] ?? "").toLowerCase();
|
|
492
|
+
if (ct && !ct.startsWith("application/json")) {
|
|
493
|
+
res.statusCode = 415;
|
|
494
|
+
res.setHeader("Content-Type", "application/json");
|
|
495
|
+
res.end(JSON.stringify({
|
|
496
|
+
error: "Unsupported Content-Type, expected application/json",
|
|
497
|
+
}));
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
// Validate Origin header to mitigate cross-site invocation
|
|
501
|
+
const origin = req.headers["origin"];
|
|
502
|
+
if (origin) {
|
|
503
|
+
const expectedHost = req.headers.host ?? "localhost";
|
|
504
|
+
if (origin !== `http://${expectedHost}` &&
|
|
505
|
+
origin !== `https://${expectedHost}`) {
|
|
506
|
+
res.statusCode = 403;
|
|
507
|
+
res.setHeader("Content-Type", "application/json");
|
|
508
|
+
res.end(JSON.stringify({ error: "Origin not allowed" }));
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
// Read request body with size limit
|
|
513
|
+
const MAX_BODY_SIZE = maxBodySize;
|
|
263
514
|
const chunks = [];
|
|
515
|
+
let totalLength = 0;
|
|
264
516
|
await new Promise((resolve, reject) => {
|
|
265
|
-
req.on("data", (chunk) =>
|
|
517
|
+
req.on("data", (chunk) => {
|
|
518
|
+
totalLength += chunk.length;
|
|
519
|
+
if (totalLength > MAX_BODY_SIZE) {
|
|
520
|
+
req.destroy();
|
|
521
|
+
reject(new Error("Request body too large"));
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
chunks.push(chunk);
|
|
525
|
+
});
|
|
266
526
|
req.on("end", () => resolve());
|
|
267
527
|
req.on("error", reject);
|
|
268
528
|
});
|
|
@@ -280,6 +540,7 @@ export async function start(args) {
|
|
|
280
540
|
}
|
|
281
541
|
const result = await ssrEntry.handleServerFn(urlPath, body);
|
|
282
542
|
if (result) {
|
|
543
|
+
applyMiddlewareHeaders();
|
|
283
544
|
res.statusCode = 200;
|
|
284
545
|
res.setHeader("Content-Type", "application/json");
|
|
285
546
|
res.end(JSON.stringify(result.result));
|
|
@@ -321,8 +582,17 @@ export async function start(args) {
|
|
|
321
582
|
// Attach body for methods that have one
|
|
322
583
|
if (method !== "GET" && method !== "HEAD") {
|
|
323
584
|
const chunks = [];
|
|
585
|
+
let totalLength = 0;
|
|
324
586
|
await new Promise((resolve, reject) => {
|
|
325
|
-
req.on("data", (chunk) =>
|
|
587
|
+
req.on("data", (chunk) => {
|
|
588
|
+
totalLength += chunk.length;
|
|
589
|
+
if (totalLength > maxBodySize) {
|
|
590
|
+
req.destroy();
|
|
591
|
+
reject(new Error("Request body too large"));
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
chunks.push(chunk);
|
|
595
|
+
});
|
|
326
596
|
req.on("end", () => resolve());
|
|
327
597
|
req.on("error", reject);
|
|
328
598
|
});
|
|
@@ -337,6 +607,8 @@ export async function start(args) {
|
|
|
337
607
|
result.response.headers.forEach((value, key) => {
|
|
338
608
|
res.setHeader(key, value);
|
|
339
609
|
});
|
|
610
|
+
// Merge middleware headers
|
|
611
|
+
applyMiddlewareHeaders();
|
|
340
612
|
// Stream the response body
|
|
341
613
|
if (result.response.body) {
|
|
342
614
|
await pipeWebStreamToResponse(result.response.body, res);
|
|
@@ -378,6 +650,31 @@ export async function start(args) {
|
|
|
378
650
|
return;
|
|
379
651
|
}
|
|
380
652
|
}
|
|
653
|
+
// Pre-rendered pages WITH middleware: serve from dist/prerendered/
|
|
654
|
+
// These are static routes that have middleware ancestors — their HTML
|
|
655
|
+
// was pre-rendered but they must go through the middleware pipeline.
|
|
656
|
+
if (req.method === "GET" && existsSync(prerenderedDir)) {
|
|
657
|
+
let prerenderedHtmlPath = urlPath;
|
|
658
|
+
if (!extname(prerenderedHtmlPath)) {
|
|
659
|
+
prerenderedHtmlPath = prerenderedHtmlPath.endsWith("/")
|
|
660
|
+
? prerenderedHtmlPath + "index.html"
|
|
661
|
+
: prerenderedHtmlPath + ".html";
|
|
662
|
+
}
|
|
663
|
+
// Serve the prerendered HTML with middleware headers applied
|
|
664
|
+
const safePath = prerenderedHtmlPath
|
|
665
|
+
.replace(/\.\./g, "")
|
|
666
|
+
.replace(/\/+/g, "/");
|
|
667
|
+
const filePath = join(prerenderedDir, safePath);
|
|
668
|
+
if (filePath.startsWith(prerenderedDir) &&
|
|
669
|
+
existsSync(filePath) &&
|
|
670
|
+
statSync(filePath).isFile()) {
|
|
671
|
+
applyMiddlewareHeaders();
|
|
672
|
+
res.statusCode = 200;
|
|
673
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
674
|
+
createReadStream(filePath).pipe(res);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
381
678
|
// RSC → SSR: render the page using the two-phase pipeline
|
|
382
679
|
// 1. RSC entry renders the page to a flight stream
|
|
383
680
|
// 2. SSR entry converts flight stream to HTML with embedded RSC payload
|
|
@@ -389,6 +686,7 @@ export async function start(args) {
|
|
|
389
686
|
const result = await rscEntry.render(urlPath);
|
|
390
687
|
if (result) {
|
|
391
688
|
const htmlStream = await ssrEntry.renderToHtml(result.stream, result.headConfig);
|
|
689
|
+
applyMiddlewareHeaders();
|
|
392
690
|
res.statusCode = 200;
|
|
393
691
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
394
692
|
await pipeWebStreamToResponse(htmlStream, res);
|
|
@@ -418,7 +716,8 @@ export async function start(args) {
|
|
|
418
716
|
console.log(" catmint production server");
|
|
419
717
|
console.log();
|
|
420
718
|
console.log(` Build ID: ${manifest.buildId}`);
|
|
421
|
-
console.log(
|
|
719
|
+
console.log();
|
|
720
|
+
console.log(formatManifestRouteTable(manifest));
|
|
422
721
|
if (hasRscPipeline) {
|
|
423
722
|
console.log(` Mode: RSC`);
|
|
424
723
|
}
|