@catmint/vite 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 +55 -0
- package/dist/build-entries.d.ts +8 -1
- package/dist/build-entries.d.ts.map +1 -1
- package/dist/build-entries.js +191 -8
- package/dist/build-entries.js.map +1 -1
- package/dist/dev-server.d.ts.map +1 -1
- package/dist/dev-server.js +368 -20
- package/dist/dev-server.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +1 -6
- package/dist/middleware.js.map +1 -1
- package/dist/resolve-utils.d.ts.map +1 -1
- package/dist/resolve-utils.js +2 -7
- package/dist/resolve-utils.js.map +1 -1
- package/dist/route-gen.d.ts.map +1 -1
- package/dist/route-gen.js +2 -5
- package/dist/route-gen.js.map +1 -1
- package/dist/server-fn-transform.js +49 -3
- package/dist/server-fn-transform.js.map +1 -1
- package/dist/utils.js +1 -1
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
package/dist/dev-server.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// calls createFromReadableStream → React VDOM → hydrateRoot
|
|
15
15
|
import { join, dirname, relative } from "node:path";
|
|
16
16
|
import { existsSync } from "node:fs";
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
17
18
|
import { createRequire } from "node:module";
|
|
18
19
|
import { deterministicHash, toPosixPath, CLIENT_NAVIGATION_RUNTIME, } from "./utils.js";
|
|
19
20
|
import { errorPageHtml } from "./error-page.js";
|
|
@@ -94,7 +95,7 @@ function collectLayouts(pageFilePath, appDir) {
|
|
|
94
95
|
const layouts = [];
|
|
95
96
|
let dir = dirname(pageFilePath);
|
|
96
97
|
while (dir.startsWith(appDir)) {
|
|
97
|
-
for (const name of ["layout.tsx", "layout.
|
|
98
|
+
for (const name of ["layout.tsx", "layout.ts"]) {
|
|
98
99
|
const layoutPath = join(dir, name);
|
|
99
100
|
if (existsSync(layoutPath)) {
|
|
100
101
|
layouts.unshift(layoutPath);
|
|
@@ -107,6 +108,154 @@ function collectLayouts(pageFilePath, appDir) {
|
|
|
107
108
|
}
|
|
108
109
|
return layouts;
|
|
109
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* File names checked for middleware, in priority order.
|
|
113
|
+
*/
|
|
114
|
+
const MIDDLEWARE_FILENAMES = ["middleware.ts", "middleware.tsx"];
|
|
115
|
+
/**
|
|
116
|
+
* Resolve the middleware chain for a given route directory.
|
|
117
|
+
* Walks from the page's directory up to appDir, collecting middleware files.
|
|
118
|
+
* Returns paths in root-to-tip execution order.
|
|
119
|
+
*/
|
|
120
|
+
function collectMiddleware(pageFilePath, appDir) {
|
|
121
|
+
const middlewareFiles = [];
|
|
122
|
+
let dir = dirname(pageFilePath);
|
|
123
|
+
while (dir.startsWith(appDir)) {
|
|
124
|
+
for (const name of MIDDLEWARE_FILENAMES) {
|
|
125
|
+
const middlewarePath = join(dir, name);
|
|
126
|
+
if (existsSync(middlewarePath)) {
|
|
127
|
+
middlewareFiles.unshift(middlewarePath); // prepend for root-to-tip order
|
|
128
|
+
break; // Only one middleware file per directory
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (dir === appDir)
|
|
132
|
+
break;
|
|
133
|
+
dir = dirname(dir);
|
|
134
|
+
}
|
|
135
|
+
return middlewareFiles;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Convert a Node.js IncomingMessage to a Web Request object.
|
|
139
|
+
*/
|
|
140
|
+
function nodeReqToWebRequest(req) {
|
|
141
|
+
const protocol = "http";
|
|
142
|
+
const host = req.headers.host ?? "localhost";
|
|
143
|
+
const url = new URL(req.originalUrl ?? req.url ?? "/", `${protocol}://${host}`);
|
|
144
|
+
const headers = new Headers();
|
|
145
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
146
|
+
if (value) {
|
|
147
|
+
if (Array.isArray(value)) {
|
|
148
|
+
for (const v of value) {
|
|
149
|
+
headers.append(key, v);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
headers.set(key, value);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return new Request(url.href, {
|
|
158
|
+
method: req.method ?? "GET",
|
|
159
|
+
headers,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Send a Web Response back through a Node.js ServerResponse.
|
|
164
|
+
*/
|
|
165
|
+
async function sendWebResponse(res, webResponse) {
|
|
166
|
+
res.statusCode = webResponse.status;
|
|
167
|
+
webResponse.headers.forEach((value, key) => {
|
|
168
|
+
res.setHeader(key, value);
|
|
169
|
+
});
|
|
170
|
+
const body = await webResponse.text();
|
|
171
|
+
res.end(body);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Execute the middleware chain for a route. Returns a Response from middleware
|
|
175
|
+
* if middleware short-circuits (e.g., 401), or null if middleware calls next()
|
|
176
|
+
* and page rendering should proceed. Also returns any response headers that
|
|
177
|
+
* middleware added (e.g., X-Response-Time) so they can be merged into the
|
|
178
|
+
* final page response.
|
|
179
|
+
*/
|
|
180
|
+
async function executeMiddleware(server, req, pageFilePath, appDir) {
|
|
181
|
+
const middlewarePaths = collectMiddleware(pageFilePath, appDir);
|
|
182
|
+
if (middlewarePaths.length === 0) {
|
|
183
|
+
return { shortCircuit: null, headers: new Headers() };
|
|
184
|
+
}
|
|
185
|
+
const handlers = [];
|
|
186
|
+
for (const mwPath of middlewarePaths) {
|
|
187
|
+
try {
|
|
188
|
+
const importPath = "/" + relative(server.config.root, mwPath);
|
|
189
|
+
const mod = await server.ssrLoadModule(importPath);
|
|
190
|
+
const handler = mod.default;
|
|
191
|
+
if (typeof handler === "function") {
|
|
192
|
+
handlers.push(handler);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
server.config.logger.warn(`[catmint] Failed to load middleware ${mwPath}: ${err}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (handlers.length === 0) {
|
|
200
|
+
return { shortCircuit: null, headers: new Headers() };
|
|
201
|
+
}
|
|
202
|
+
// Apply inherit boundary: walk backwards to find inherit: false
|
|
203
|
+
let boundaryIndex = 0;
|
|
204
|
+
for (let i = handlers.length - 1; i >= 0; i--) {
|
|
205
|
+
const opts = handlers[i].__catmintMiddleware;
|
|
206
|
+
if (opts && opts.inherit === false) {
|
|
207
|
+
boundaryIndex = i;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const effectiveHandlers = handlers.slice(boundaryIndex);
|
|
212
|
+
// Create the Web Request
|
|
213
|
+
const webRequest = nodeReqToWebRequest(req);
|
|
214
|
+
// Compose and execute middleware chain
|
|
215
|
+
let middlewareCalledNext = false;
|
|
216
|
+
const capturedHeaders = new Headers();
|
|
217
|
+
const executeChain = (index) => {
|
|
218
|
+
if (index >= effectiveHandlers.length) {
|
|
219
|
+
// End of chain — middleware called next(), page rendering should proceed
|
|
220
|
+
middlewareCalledNext = true;
|
|
221
|
+
return Promise.resolve(new Response(null, { status: 200 }));
|
|
222
|
+
}
|
|
223
|
+
const handler = effectiveHandlers[index];
|
|
224
|
+
return Promise.resolve(handler(webRequest, () => {
|
|
225
|
+
const nextResult = executeChain(index + 1);
|
|
226
|
+
return nextResult.then((response) => {
|
|
227
|
+
// Capture headers from inner middleware/next
|
|
228
|
+
response.headers.forEach((value, key) => {
|
|
229
|
+
capturedHeaders.set(key, value);
|
|
230
|
+
});
|
|
231
|
+
return response;
|
|
232
|
+
});
|
|
233
|
+
})).then((response) => {
|
|
234
|
+
// Capture headers from this middleware
|
|
235
|
+
response.headers.forEach((value, key) => {
|
|
236
|
+
capturedHeaders.set(key, value);
|
|
237
|
+
});
|
|
238
|
+
return response;
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
try {
|
|
242
|
+
const response = await executeChain(0);
|
|
243
|
+
if (!middlewareCalledNext) {
|
|
244
|
+
// Middleware short-circuited (returned a response without calling next)
|
|
245
|
+
return { shortCircuit: response, headers: capturedHeaders };
|
|
246
|
+
}
|
|
247
|
+
// Middleware called next() — proceed with page rendering
|
|
248
|
+
// but pass along any headers middleware added
|
|
249
|
+
return { shortCircuit: null, headers: capturedHeaders };
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
server.config.logger.error(`[catmint] Middleware error: ${err}`);
|
|
253
|
+
return {
|
|
254
|
+
shortCircuit: new Response("Internal Server Error", { status: 500 }),
|
|
255
|
+
headers: new Headers(),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
110
259
|
/**
|
|
111
260
|
* CSS file extensions that Vite processes.
|
|
112
261
|
*/
|
|
@@ -349,6 +498,52 @@ const SERVER_FN_PREFIX = "/__catmint/fn/";
|
|
|
349
498
|
* RSC flight stream prefix for client-side navigation.
|
|
350
499
|
*/
|
|
351
500
|
const RSC_NAVIGATION_PREFIX = "/__catmint/rsc";
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
// Server function error detection helpers (duck-typing)
|
|
503
|
+
//
|
|
504
|
+
// The dev-server cannot import from `catmint` directly (it's in the
|
|
505
|
+
// `@catmint/vite` package). Instead, we detect error types by checking
|
|
506
|
+
// for characteristic properties — this matches instances loaded at
|
|
507
|
+
// runtime via Vite's SSR module loader.
|
|
508
|
+
// ---------------------------------------------------------------------------
|
|
509
|
+
/**
|
|
510
|
+
* Check if a value looks like a ClientSafeError (duck-typing).
|
|
511
|
+
* Matches: instance has `name === "ClientSafeError"` OR has `statusCode`
|
|
512
|
+
* property and the constructor chain includes a class with name "ClientSafeError".
|
|
513
|
+
*/
|
|
514
|
+
function isClientSafeErrorLike(value) {
|
|
515
|
+
if (!value || typeof value !== "object")
|
|
516
|
+
return false;
|
|
517
|
+
// Direct instance check by name (supports subclasses via prototype chain)
|
|
518
|
+
if (value instanceof Error && typeof value.statusCode === "number") {
|
|
519
|
+
let proto = Object.getPrototypeOf(value);
|
|
520
|
+
while (proto && proto !== Error.prototype) {
|
|
521
|
+
if (proto.constructor?.name === "ClientSafeError")
|
|
522
|
+
return true;
|
|
523
|
+
proto = Object.getPrototypeOf(proto);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Check if a value looks like a RedirectError (duck-typing).
|
|
530
|
+
*/
|
|
531
|
+
function isRedirectErrorLike(value) {
|
|
532
|
+
if (!value || typeof value !== "object")
|
|
533
|
+
return false;
|
|
534
|
+
return (value instanceof Error &&
|
|
535
|
+
value.name === "RedirectError" &&
|
|
536
|
+
typeof value.url === "string" &&
|
|
537
|
+
typeof value.status === "number");
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Generate a short correlation hash for error tracking.
|
|
541
|
+
* Used in production to link client error messages to server logs.
|
|
542
|
+
*/
|
|
543
|
+
function errorRef(err) {
|
|
544
|
+
const content = `${Date.now()}:${err instanceof Error ? err.message : String(err)}`;
|
|
545
|
+
return createHash("sha256").update(content).digest("hex").slice(0, 8);
|
|
546
|
+
}
|
|
352
547
|
/**
|
|
353
548
|
* Handle a server function RPC call.
|
|
354
549
|
*
|
|
@@ -441,17 +636,63 @@ async function handleServerFn(server, req, res, pathname) {
|
|
|
441
636
|
// Call the server function
|
|
442
637
|
try {
|
|
443
638
|
const result = await matchedFn(input);
|
|
639
|
+
// Check if the result IS a ClientSafeError (returned, not thrown).
|
|
640
|
+
// Use duck-typing since we can't import from `catmint` directly.
|
|
641
|
+
if (isClientSafeErrorLike(result)) {
|
|
642
|
+
res.statusCode = 200;
|
|
643
|
+
res.setHeader("Content-Type", "application/json");
|
|
644
|
+
res.end(JSON.stringify({
|
|
645
|
+
__clientSafeError: true,
|
|
646
|
+
error: result.message,
|
|
647
|
+
statusCode: result.statusCode,
|
|
648
|
+
data: result.data,
|
|
649
|
+
}));
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
444
652
|
res.statusCode = 200;
|
|
445
653
|
res.setHeader("Content-Type", "application/json");
|
|
446
654
|
res.end(JSON.stringify(result));
|
|
447
655
|
}
|
|
448
656
|
catch (err) {
|
|
657
|
+
// Always log the full error server-side
|
|
449
658
|
server.config.logger.error(`[catmint] Server function error: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
|
|
450
|
-
res.statusCode = 500;
|
|
451
659
|
res.setHeader("Content-Type", "application/json");
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
660
|
+
// RedirectError — send redirect envelope to client
|
|
661
|
+
if (isRedirectErrorLike(err)) {
|
|
662
|
+
res.statusCode = 200;
|
|
663
|
+
res.end(JSON.stringify({
|
|
664
|
+
__redirect: true,
|
|
665
|
+
url: err.url,
|
|
666
|
+
status: err.status,
|
|
667
|
+
}));
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
// ClientSafeError — developer opted in, expose to client
|
|
671
|
+
if (isClientSafeErrorLike(err)) {
|
|
672
|
+
res.statusCode = err.statusCode;
|
|
673
|
+
res.end(JSON.stringify({
|
|
674
|
+
error: err.message,
|
|
675
|
+
data: err.data,
|
|
676
|
+
}));
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
// Default: sanitize error — in dev mode, leak with warning;
|
|
680
|
+
// in prod mode, use generic message with correlation hash
|
|
681
|
+
const isDev = server.config.mode === "development" || !server.config.isProduction;
|
|
682
|
+
res.statusCode = 500;
|
|
683
|
+
if (isDev) {
|
|
684
|
+
const originalMessage = err instanceof Error ? err.message : String(err);
|
|
685
|
+
res.end(JSON.stringify({
|
|
686
|
+
error: `[DEV ONLY] ${originalMessage}`,
|
|
687
|
+
}));
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
const ref = errorRef(err);
|
|
691
|
+
server.config.logger.error(`[catmint] Error reference [ref: ${ref}] — see above for full error`);
|
|
692
|
+
res.end(JSON.stringify({
|
|
693
|
+
error: `Internal Server Error [ref: ${ref}]`,
|
|
694
|
+
}));
|
|
695
|
+
}
|
|
455
696
|
}
|
|
456
697
|
}
|
|
457
698
|
/**
|
|
@@ -894,6 +1135,28 @@ export function devServerPlugin(options) {
|
|
|
894
1135
|
server = viteServer;
|
|
895
1136
|
appDir = join(server.config.root, "app");
|
|
896
1137
|
statusPages = scanStatusPages(appDir);
|
|
1138
|
+
// Invalidate route matchers when route files are added or removed.
|
|
1139
|
+
// Vite's handleHotUpdate only fires for files already in the module
|
|
1140
|
+
// graph, so newly created or deleted files need this watcher to
|
|
1141
|
+
// trigger a re-scan on the next request.
|
|
1142
|
+
const isRouteFile = (f) => f.includes("/app/") &&
|
|
1143
|
+
(f.endsWith("page.tsx") ||
|
|
1144
|
+
f.endsWith("page.mdx") ||
|
|
1145
|
+
f.endsWith("endpoint.ts"));
|
|
1146
|
+
server.watcher.on("add", (file) => {
|
|
1147
|
+
if (isRouteFile(file)) {
|
|
1148
|
+
pageMatcher = null;
|
|
1149
|
+
endpointMatcher = null;
|
|
1150
|
+
server.config.logger.info(`[catmint] Route file added: ${file} — invalidating route cache`);
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
1153
|
+
server.watcher.on("unlink", (file) => {
|
|
1154
|
+
if (isRouteFile(file)) {
|
|
1155
|
+
pageMatcher = null;
|
|
1156
|
+
endpointMatcher = null;
|
|
1157
|
+
server.config.logger.info(`[catmint] Route file removed: ${file} — invalidating route cache`);
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
897
1160
|
return () => {
|
|
898
1161
|
server.middlewares.use(async (req, res, next) => {
|
|
899
1162
|
const url = req.originalUrl ?? req.url ?? "/";
|
|
@@ -962,6 +1225,17 @@ export function devServerPlugin(options) {
|
|
|
962
1225
|
res.end(JSON.stringify({ error: "RSC not available" }));
|
|
963
1226
|
return;
|
|
964
1227
|
}
|
|
1228
|
+
// Execute middleware for the TARGET page path (not /__catmint/rsc)
|
|
1229
|
+
const middlewareResult = await executeMiddleware(server, req, match.route.filePath, appDir);
|
|
1230
|
+
if (middlewareResult.shortCircuit) {
|
|
1231
|
+
// Middleware short-circuited (e.g., returned 401)
|
|
1232
|
+
await sendWebResponse(res, middlewareResult.shortCircuit);
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
// Apply middleware response headers (e.g., X-Response-Time)
|
|
1236
|
+
middlewareResult.headers.forEach((value, key) => {
|
|
1237
|
+
res.setHeader(key, value);
|
|
1238
|
+
});
|
|
965
1239
|
await handleRscNavigation(server, req, res, match, appDir, i18nConfig);
|
|
966
1240
|
}
|
|
967
1241
|
catch (error) {
|
|
@@ -1016,6 +1290,19 @@ export function devServerPlugin(options) {
|
|
|
1016
1290
|
await sendStatusPage(res, 404, url);
|
|
1017
1291
|
return;
|
|
1018
1292
|
}
|
|
1293
|
+
// --- Execute middleware chain for this route ---
|
|
1294
|
+
const routeDir = dirname(match.route.filePath);
|
|
1295
|
+
const middlewareResult = await executeMiddleware(server, req, match.route.filePath, appDir);
|
|
1296
|
+
if (middlewareResult.shortCircuit) {
|
|
1297
|
+
// Middleware short-circuited (e.g., returned 401)
|
|
1298
|
+
await sendWebResponse(res, middlewareResult.shortCircuit);
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
// Apply middleware response headers (e.g., X-Response-Time)
|
|
1302
|
+
// These will be set on the response before page rendering streams
|
|
1303
|
+
middlewareResult.headers.forEach((value, key) => {
|
|
1304
|
+
res.setHeader(key, value);
|
|
1305
|
+
});
|
|
1019
1306
|
// Check if RSC environments are available
|
|
1020
1307
|
if (hasRscEnvironments()) {
|
|
1021
1308
|
await handlePageWithRsc(server, req, res, match, appDir, softNav, i18nConfig, sendStatusPage);
|
|
@@ -1051,10 +1338,8 @@ export function devServerPlugin(options) {
|
|
|
1051
1338
|
handleHotUpdate({ file, server: hmrServer }) {
|
|
1052
1339
|
if (file.includes("/app/") &&
|
|
1053
1340
|
(file.endsWith("page.tsx") ||
|
|
1054
|
-
file.endsWith("page.jsx") ||
|
|
1055
1341
|
file.endsWith("page.mdx") ||
|
|
1056
|
-
file.endsWith("endpoint.ts")
|
|
1057
|
-
file.endsWith("endpoint.js"))) {
|
|
1342
|
+
file.endsWith("endpoint.ts"))) {
|
|
1058
1343
|
pageMatcher = null;
|
|
1059
1344
|
endpointMatcher = null;
|
|
1060
1345
|
}
|
|
@@ -1064,7 +1349,7 @@ export function devServerPlugin(options) {
|
|
|
1064
1349
|
// status page is picked up on the next error.
|
|
1065
1350
|
if (file.includes("/app/")) {
|
|
1066
1351
|
const basename = file.split("/").pop() ?? "";
|
|
1067
|
-
if (/^\d{3}\.(tsx|
|
|
1352
|
+
if (/^\d{3}\.(tsx|ts)$/.test(basename)) {
|
|
1068
1353
|
statusPages = scanStatusPages(appDir);
|
|
1069
1354
|
hmrServer.config.logger.info(`[catmint] Status page changed: ${basename} — triggering reload`);
|
|
1070
1355
|
hmrServer.hot.send({ type: "full-reload", path: "*" });
|
|
@@ -1144,7 +1429,11 @@ async function handlePageWithRsc(server, req, res, match, appDir, softNavigation
|
|
|
1144
1429
|
// If a loading.tsx is found, wrap the page with a Suspense boundary.
|
|
1145
1430
|
// If an error.tsx is found via walk-up resolution, wrap the page
|
|
1146
1431
|
// with the error boundary component inside the nearest layout.
|
|
1147
|
-
|
|
1432
|
+
const pageProps = {};
|
|
1433
|
+
if (match.params && Object.keys(match.params).length > 0) {
|
|
1434
|
+
pageProps.params = match.params;
|
|
1435
|
+
}
|
|
1436
|
+
let element = createElement(PageComponent, Object.keys(pageProps).length > 0 ? pageProps : null);
|
|
1148
1437
|
// Resolve loading.tsx from the page directory for Suspense fallback
|
|
1149
1438
|
const loadingPath = resolveLoadingComponent(match.route.filePath);
|
|
1150
1439
|
if (loadingPath) {
|
|
@@ -1172,10 +1461,24 @@ async function handlePageWithRsc(server, req, res, match, appDir, softNavigation
|
|
|
1172
1461
|
const errorBoundaryImportPath = "/" + relative(root, errorBoundaryPath);
|
|
1173
1462
|
try {
|
|
1174
1463
|
const errorMod = await rscEnv.runner.import(errorBoundaryImportPath);
|
|
1175
|
-
const
|
|
1176
|
-
if (
|
|
1177
|
-
//
|
|
1178
|
-
|
|
1464
|
+
const ErrorFallbackComponent = errorMod.default;
|
|
1465
|
+
if (ErrorFallbackComponent) {
|
|
1466
|
+
// Import the ErrorBoundary class component to properly catch errors
|
|
1467
|
+
const errorBoundaryMod = await rscEnv.runner.import("catmint/error");
|
|
1468
|
+
const ErrorBoundary = errorBoundaryMod.ErrorBoundary ??
|
|
1469
|
+
errorBoundaryMod.default?.ErrorBoundary;
|
|
1470
|
+
if (ErrorBoundary) {
|
|
1471
|
+
element = createElement(ErrorBoundary, {
|
|
1472
|
+
fallback: ErrorFallbackComponent,
|
|
1473
|
+
children: element,
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
else {
|
|
1477
|
+
// Fallback: wrap page with error fallback component directly
|
|
1478
|
+
element = createElement(ErrorFallbackComponent, {
|
|
1479
|
+
children: element,
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1179
1482
|
}
|
|
1180
1483
|
}
|
|
1181
1484
|
catch {
|
|
@@ -1348,7 +1651,11 @@ async function handleRscNavigation(server, _req, res, match, appDir, i18nConfig
|
|
|
1348
1651
|
// If a loading.tsx is found, wrap the page with a Suspense boundary.
|
|
1349
1652
|
// If an error.tsx is found via walk-up resolution, wrap the page
|
|
1350
1653
|
// with the error boundary component inside the nearest layout.
|
|
1351
|
-
|
|
1654
|
+
const navPageProps = {};
|
|
1655
|
+
if (match.params && Object.keys(match.params).length > 0) {
|
|
1656
|
+
navPageProps.params = match.params;
|
|
1657
|
+
}
|
|
1658
|
+
let element = createElement(PageComponent, Object.keys(navPageProps).length > 0 ? navPageProps : null);
|
|
1352
1659
|
// Resolve loading.tsx from the page directory for Suspense fallback
|
|
1353
1660
|
const loadingPath = resolveLoadingComponent(match.route.filePath);
|
|
1354
1661
|
if (loadingPath) {
|
|
@@ -1376,10 +1683,24 @@ async function handleRscNavigation(server, _req, res, match, appDir, i18nConfig
|
|
|
1376
1683
|
const errorBoundaryImportPath = "/" + relative(root, errorBoundaryPath);
|
|
1377
1684
|
try {
|
|
1378
1685
|
const errorMod = await rscEnv.runner.import(errorBoundaryImportPath);
|
|
1379
|
-
const
|
|
1380
|
-
if (
|
|
1381
|
-
//
|
|
1382
|
-
|
|
1686
|
+
const ErrorFallbackComponent = errorMod.default;
|
|
1687
|
+
if (ErrorFallbackComponent) {
|
|
1688
|
+
// Import the ErrorBoundary class component to properly catch errors
|
|
1689
|
+
const errorBoundaryMod = await rscEnv.runner.import("catmint/error");
|
|
1690
|
+
const ErrorBoundary = errorBoundaryMod.ErrorBoundary ??
|
|
1691
|
+
errorBoundaryMod.default?.ErrorBoundary;
|
|
1692
|
+
if (ErrorBoundary) {
|
|
1693
|
+
element = createElement(ErrorBoundary, {
|
|
1694
|
+
fallback: ErrorFallbackComponent,
|
|
1695
|
+
children: element,
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
else {
|
|
1699
|
+
// Fallback: wrap page with error fallback component directly
|
|
1700
|
+
element = createElement(ErrorFallbackComponent, {
|
|
1701
|
+
children: element,
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1383
1704
|
}
|
|
1384
1705
|
}
|
|
1385
1706
|
catch {
|
|
@@ -1459,7 +1780,9 @@ async function handlePageLegacy(server, _req, res, match, appDir, hydrScripts, h
|
|
|
1459
1780
|
}
|
|
1460
1781
|
// Resolve generateMetadata exports from page + layout modules
|
|
1461
1782
|
const headConfig = await resolveGenerateMetadata(pageMod, layoutMods, match.params ?? {}, url);
|
|
1462
|
-
let element = React.createElement(PageComponent,
|
|
1783
|
+
let element = React.createElement(PageComponent, match.params && Object.keys(match.params).length > 0
|
|
1784
|
+
? { params: match.params }
|
|
1785
|
+
: null);
|
|
1463
1786
|
for (let i = layoutComponents.length - 1; i >= 0; i--) {
|
|
1464
1787
|
element = React.createElement(layoutComponents[i], null, element);
|
|
1465
1788
|
}
|
|
@@ -1629,6 +1952,31 @@ hydrate().then(() => {
|
|
|
1629
1952
|
setupClientNavigation(root);
|
|
1630
1953
|
}
|
|
1631
1954
|
});
|
|
1955
|
+
|
|
1956
|
+
// HMR: When server components (page.tsx, layout.tsx) are edited, the RSC
|
|
1957
|
+
// plugin sends an "rsc:update" event. Re-fetch the flight stream for the
|
|
1958
|
+
// current page and re-render so the update appears without a full reload.
|
|
1959
|
+
if (import.meta.hot) {
|
|
1960
|
+
import.meta.hot.on("rsc:update", async () => {
|
|
1961
|
+
if (!root) return;
|
|
1962
|
+
try {
|
|
1963
|
+
var rscUrl = "/__catmint/rsc?path=" + encodeURIComponent(
|
|
1964
|
+
window.location.pathname + window.location.search
|
|
1965
|
+
);
|
|
1966
|
+
var response = await fetch(rscUrl);
|
|
1967
|
+
if (!response.ok) {
|
|
1968
|
+
window.location.reload();
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
var { createFromReadableStream } = await import("@vitejs/plugin-rsc/browser");
|
|
1972
|
+
var newRoot = await createFromReadableStream(response.body);
|
|
1973
|
+
root.render(newRoot);
|
|
1974
|
+
} catch (err) {
|
|
1975
|
+
console.error("[catmint] HMR update failed, reloading:", err);
|
|
1976
|
+
window.location.reload();
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1632
1980
|
`;
|
|
1633
1981
|
}
|
|
1634
1982
|
// --------------------------------------------------------------------------
|