@pylonsync/functions 0.3.236 → 0.3.237
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/package.json +1 -1
- package/src/ssr-runtime.ts +292 -80
package/package.json
CHANGED
package/src/ssr-runtime.ts
CHANGED
|
@@ -55,6 +55,14 @@ export interface RenderRouteMessage {
|
|
|
55
55
|
tenant_id: string | null;
|
|
56
56
|
roles: string[];
|
|
57
57
|
};
|
|
58
|
+
/**
|
|
59
|
+
* Initial HTTP status the response controller starts at (default 200).
|
|
60
|
+
* The host sets this to 404 when dispatching a `not-found.tsx` render
|
|
61
|
+
* for an unmatched URL, so the boundary streams at 404 without the
|
|
62
|
+
* component having to call `response.setStatus`. A page can still
|
|
63
|
+
* override it via `response.setStatus`.
|
|
64
|
+
*/
|
|
65
|
+
initial_status?: number;
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
type Send = (msg: Record<string, unknown>) => void;
|
|
@@ -167,7 +175,12 @@ export interface SsrResponse {
|
|
|
167
175
|
setCookie(name: string, value: string, opts?: SsrCookieOptions): void;
|
|
168
176
|
/** Throw to send a 3xx (default 307) + Location, no body. Shell-render only. */
|
|
169
177
|
redirect(url: string, status?: number): never;
|
|
170
|
-
/**
|
|
178
|
+
/**
|
|
179
|
+
* Throw to send a 404. Renders the nearest `not-found.tsx` (walking up
|
|
180
|
+
* from the page's directory, wrapped in the route's layout chain), or a
|
|
181
|
+
* minimal framework body if none is defined. Shell-render only — a throw
|
|
182
|
+
* below a Suspense boundary is swallowed by React.
|
|
183
|
+
*/
|
|
171
184
|
notFound(): never;
|
|
172
185
|
}
|
|
173
186
|
|
|
@@ -364,14 +377,207 @@ async function buildLayoutTree(
|
|
|
364
377
|
return tree;
|
|
365
378
|
}
|
|
366
379
|
|
|
380
|
+
/**
|
|
381
|
+
* Walk up from a page's directory to the nearest boundary file
|
|
382
|
+
* (not-found / error) — the same render-time, filesystem-resolved model
|
|
383
|
+
* the page + layouts already use, so no build-time manifest threading.
|
|
384
|
+
* Returns the project-relative path (no extension) or null.
|
|
385
|
+
*/
|
|
386
|
+
function findBoundary(componentPath: string, fileName: string): string | null {
|
|
387
|
+
const fs = require("node:fs");
|
|
388
|
+
const path = require("node:path");
|
|
389
|
+
const cwd = process.cwd();
|
|
390
|
+
// Component paths use "/" — walk up directory by directory.
|
|
391
|
+
let dir = componentPath.replace(/\\/g, "/");
|
|
392
|
+
dir = dir.includes("/") ? dir.slice(0, dir.lastIndexOf("/")) : "";
|
|
393
|
+
while (dir && dir !== "." && dir !== "/") {
|
|
394
|
+
for (const ext of MODULE_EXTS) {
|
|
395
|
+
if (fs.existsSync(path.join(cwd, dir, `${fileName}${ext}`))) {
|
|
396
|
+
return `${dir}/${fileName}`;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const slash = dir.lastIndexOf("/");
|
|
400
|
+
dir = slash >= 0 ? dir.slice(0, slash) : "";
|
|
401
|
+
}
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Drain a `renderToReadableStream` reader, injecting `headBlob` immediately
|
|
407
|
+
* before the first `</head>` (or, if the document has none, the blob is
|
|
408
|
+
* never emitted — fragment renders have no head). `</head>` can straddle a
|
|
409
|
+
* chunk boundary, so a small carry buffer (len("</head>") − 1 bytes) is
|
|
410
|
+
* withheld at each chunk's tail until the next read confirms the match.
|
|
411
|
+
* Each emitted slice is handed to `sendChunk` as utf-8 text.
|
|
412
|
+
*
|
|
413
|
+
* Shared by the page render and the boundary render so head injection has
|
|
414
|
+
* exactly one implementation.
|
|
415
|
+
*/
|
|
416
|
+
async function streamWithHeadInjection(
|
|
417
|
+
reader: ReadableStreamDefaultReader<Uint8Array>,
|
|
418
|
+
headBlob: string,
|
|
419
|
+
sendChunk: (text: string) => void,
|
|
420
|
+
): Promise<void> {
|
|
421
|
+
let headInjected = headBlob.length === 0;
|
|
422
|
+
let carry = "";
|
|
423
|
+
const HEAD_CLOSE = "</head>";
|
|
424
|
+
for (;;) {
|
|
425
|
+
const { value, done } = await reader.read();
|
|
426
|
+
if (done) break;
|
|
427
|
+
if (!value || value.byteLength === 0) continue;
|
|
428
|
+
const text = Buffer.from(value).toString("utf8");
|
|
429
|
+
if (!headInjected) {
|
|
430
|
+
const combined = carry + text;
|
|
431
|
+
const idx = combined.indexOf(HEAD_CLOSE);
|
|
432
|
+
if (idx >= 0) {
|
|
433
|
+
sendChunk(combined.slice(0, idx));
|
|
434
|
+
sendChunk(headBlob);
|
|
435
|
+
sendChunk(HEAD_CLOSE);
|
|
436
|
+
const after = combined.slice(idx + HEAD_CLOSE.length);
|
|
437
|
+
if (after) sendChunk(after);
|
|
438
|
+
headInjected = true;
|
|
439
|
+
carry = "";
|
|
440
|
+
} else {
|
|
441
|
+
const keep = HEAD_CLOSE.length - 1;
|
|
442
|
+
if (combined.length > keep) {
|
|
443
|
+
sendChunk(combined.slice(0, combined.length - keep));
|
|
444
|
+
carry = combined.slice(combined.length - keep);
|
|
445
|
+
} else {
|
|
446
|
+
carry = combined;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
} else {
|
|
450
|
+
sendChunk(text);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (carry) sendChunk(carry);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Build the <head> blob for a boundary render: the union of every route's
|
|
458
|
+
* stylesheet links from the client build manifest. Boundary modules aren't
|
|
459
|
+
* bundled as their own client entry, but they render inside the same
|
|
460
|
+
* layout/shell as pages, so without the app's global CSS a 404/500 page
|
|
461
|
+
* would look broken. Returns "" if the manifest can't be loaded — the
|
|
462
|
+
* boundary still renders (unstyled); CSS must never block the error path.
|
|
463
|
+
*/
|
|
464
|
+
async function collectBoundaryHeadBlob(): Promise<string> {
|
|
465
|
+
try {
|
|
466
|
+
const { getManifest } = await import("./ssr-client-bundler");
|
|
467
|
+
const manifest = await getManifest();
|
|
468
|
+
const prefix = manifest.public_prefix || "/_pylon/build/";
|
|
469
|
+
const seen = new Set<string>();
|
|
470
|
+
let blob = "";
|
|
471
|
+
for (const route of Object.values(manifest.routes || {}) as any[]) {
|
|
472
|
+
for (const css of (route.css || []) as string[]) {
|
|
473
|
+
if (seen.has(css)) continue;
|
|
474
|
+
seen.add(css);
|
|
475
|
+
blob += `<link rel="stylesheet" href="${prefix}${css}">`;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return blob;
|
|
479
|
+
} catch {
|
|
480
|
+
return "";
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Render a boundary (not-found/error) tree and stream it as the response
|
|
486
|
+
* body at `status`. Boundaries render server-side only (no hydration
|
|
487
|
+
* payload) — they're informational pages, consistent with the keystone's
|
|
488
|
+
* fixed 404 body that this replaces — but they DO get the app's global
|
|
489
|
+
* stylesheet injected so they match the rest of the site.
|
|
490
|
+
*/
|
|
491
|
+
async function renderBoundaryToClient(
|
|
492
|
+
React: any,
|
|
493
|
+
renderToReadableStream: any,
|
|
494
|
+
tree: any,
|
|
495
|
+
send: Send,
|
|
496
|
+
callId: string,
|
|
497
|
+
status: number,
|
|
498
|
+
headers: Record<string, string>,
|
|
499
|
+
): Promise<void> {
|
|
500
|
+
const stream: ReadableStream<Uint8Array> = await renderToReadableStream(tree, {
|
|
501
|
+
onError(e: unknown) {
|
|
502
|
+
// eslint-disable-next-line no-console
|
|
503
|
+
console.error("[ssr] boundary render error:", e);
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
const headBlob = await collectBoundaryHeadBlob();
|
|
507
|
+
// renderToReadableStream resolved without throwing → safe to commit the
|
|
508
|
+
// head now, then drain the (already-rendered) shell, injecting CSS.
|
|
509
|
+
send({ type: "response_start", call_id: callId, status, headers });
|
|
510
|
+
const sendChunk = (text: string) => {
|
|
511
|
+
if (!text) return;
|
|
512
|
+
send({
|
|
513
|
+
type: "render_chunk",
|
|
514
|
+
call_id: callId,
|
|
515
|
+
data: Buffer.from(text, "utf8").toString("base64"),
|
|
516
|
+
});
|
|
517
|
+
};
|
|
518
|
+
await streamWithHeadInjection(stream.getReader(), headBlob, sendChunk);
|
|
519
|
+
send({ type: "render_done", call_id: callId });
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Resolve + render a boundary component wrapped in the route's layout
|
|
524
|
+
* chain. Returns true if it rendered (caller returns), false to fall back.
|
|
525
|
+
*/
|
|
526
|
+
async function tryRenderBoundary(
|
|
527
|
+
opts: {
|
|
528
|
+
React: any;
|
|
529
|
+
renderToReadableStream: any;
|
|
530
|
+
cwd: string;
|
|
531
|
+
componentPath: string;
|
|
532
|
+
fileName: "not-found" | "error";
|
|
533
|
+
layouts: string[] | undefined;
|
|
534
|
+
props: any;
|
|
535
|
+
send: Send;
|
|
536
|
+
callId: string;
|
|
537
|
+
status: number;
|
|
538
|
+
headers: Record<string, string>;
|
|
539
|
+
},
|
|
540
|
+
): Promise<boolean> {
|
|
541
|
+
const { React, renderToReadableStream, cwd, componentPath, fileName, layouts, props, send, callId, status, headers } =
|
|
542
|
+
opts;
|
|
543
|
+
if (!React || !renderToReadableStream || !props) return false;
|
|
544
|
+
const rel = findBoundary(componentPath, fileName);
|
|
545
|
+
if (!rel) return false;
|
|
546
|
+
try {
|
|
547
|
+
const mod = await importModule(cwd, rel);
|
|
548
|
+
const Comp = mod.default ?? mod.Component ?? mod.NotFound ?? mod.Error;
|
|
549
|
+
if (typeof Comp !== "function") return false;
|
|
550
|
+
let tree = React.createElement(Comp, props);
|
|
551
|
+
tree = await buildLayoutTree(cwd, tree, layouts, props, React);
|
|
552
|
+
await renderBoundaryToClient(React, renderToReadableStream, tree, send, callId, status, headers);
|
|
553
|
+
return true;
|
|
554
|
+
} catch (e) {
|
|
555
|
+
// Boundary render itself failed — no tertiary fallback; let the caller
|
|
556
|
+
// emit its default (fixed 404 body / type:"error" → 500).
|
|
557
|
+
// eslint-disable-next-line no-console
|
|
558
|
+
console.error(`[ssr] ${fileName}.tsx boundary failed to render:`, e);
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
367
563
|
export async function handleRenderRoute(
|
|
368
564
|
msg: RenderRouteMessage,
|
|
369
565
|
send: Send,
|
|
370
566
|
): Promise<void> {
|
|
371
567
|
// Declared OUTSIDE the try so the catch can read page-set status/
|
|
372
568
|
// cookies when turning a redirect()/notFound() throw into a response.
|
|
373
|
-
const responseState: ResponseState = {
|
|
569
|
+
const responseState: ResponseState = {
|
|
570
|
+
status: msg.initial_status ?? 200,
|
|
571
|
+
headers: {},
|
|
572
|
+
cookies: [],
|
|
573
|
+
};
|
|
374
574
|
const response = makeResponseController(responseState);
|
|
575
|
+
// Hoisted out of the try so the catch can render not-found.tsx /
|
|
576
|
+
// error.tsx boundaries (which need React + the renderer + cwd + props).
|
|
577
|
+
const cwd = process.cwd();
|
|
578
|
+
let React: any = null;
|
|
579
|
+
let renderToReadableStream: any = null;
|
|
580
|
+
let props: any = null;
|
|
375
581
|
try {
|
|
376
582
|
// react + react-dom are USER deps. ssr-runtime.ts lives in
|
|
377
583
|
// packages/functions/src/, but the user's react install is under
|
|
@@ -379,7 +585,6 @@ export async function handleRenderRoute(
|
|
|
379
585
|
// would resolve against pylon's own node_modules (which doesn't
|
|
380
586
|
// declare react), so we route through a Bun-resolveSync against
|
|
381
587
|
// the user's cwd.
|
|
382
|
-
const cwd = process.cwd();
|
|
383
588
|
const resolveFromUser = (spec: string): string =>
|
|
384
589
|
(Bun as any).resolveSync
|
|
385
590
|
? (Bun as any).resolveSync(spec, cwd)
|
|
@@ -405,8 +610,8 @@ export async function handleRenderRoute(
|
|
|
405
610
|
const reactImport = await import(
|
|
406
611
|
/* @vite-ignore */ resolveFromUser("react")
|
|
407
612
|
);
|
|
408
|
-
|
|
409
|
-
|
|
613
|
+
React = reactImport.default ?? reactImport;
|
|
614
|
+
renderToReadableStream =
|
|
410
615
|
reactDomServerImport.renderToReadableStream ??
|
|
411
616
|
reactDomServerImport.default?.renderToReadableStream;
|
|
412
617
|
if (typeof renderToReadableStream !== "function") {
|
|
@@ -431,7 +636,7 @@ export async function handleRenderRoute(
|
|
|
431
636
|
);
|
|
432
637
|
}
|
|
433
638
|
|
|
434
|
-
|
|
639
|
+
props = {
|
|
435
640
|
url: msg.url,
|
|
436
641
|
params: msg.params,
|
|
437
642
|
searchParams: msg.search_params,
|
|
@@ -530,16 +735,25 @@ export async function handleRenderRoute(
|
|
|
530
735
|
for (const chunk of preloadManifestRoute.imports) {
|
|
531
736
|
headBlob += `<link rel="modulepreload" href="${preloadPublicPrefix}${chunk}">`;
|
|
532
737
|
}
|
|
738
|
+
} else {
|
|
739
|
+
// No per-route client entry. This is the unmatched-URL not-found
|
|
740
|
+
// dispatch (the host renders `app/not-found` by name at 404) or any
|
|
741
|
+
// other component without a hydration bundle. It still renders inside
|
|
742
|
+
// the app shell, so inject the global stylesheet(s) — otherwise the
|
|
743
|
+
// 404 page is unstyled. Hydration stays disabled (handled below).
|
|
744
|
+
headBlob += await collectBoundaryHeadBlob();
|
|
533
745
|
}
|
|
534
746
|
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
747
|
+
// The host can dispatch a boundary module (`app/not-found` / `app/error`)
|
|
748
|
+
// by name for an unmatched-URL 404. Boundaries render server-only — no
|
|
749
|
+
// hydration payload, and no "hydration disabled" warning (that warning is
|
|
750
|
+
// for a real page whose client bundle is missing).
|
|
751
|
+
const isBoundaryComponent = /(^|\/)(not-found|error)$/.test(msg.component);
|
|
752
|
+
|
|
753
|
+
// Stream-rewrite: watch for `</head>` and inject `headBlob` before it.
|
|
754
|
+
// `</head>` may straddle chunk boundaries; the shared helper keeps a
|
|
755
|
+
// small carry buffer to catch a split tag. base64 of each utf-8 slice
|
|
756
|
+
// happens in `sendChunk` (Buffer ships with Bun).
|
|
543
757
|
const sendChunk = (text: string) => {
|
|
544
758
|
if (!text) return;
|
|
545
759
|
send({
|
|
@@ -548,50 +762,7 @@ export async function handleRenderRoute(
|
|
|
548
762
|
data: Buffer.from(text, "utf8").toString("base64"),
|
|
549
763
|
});
|
|
550
764
|
};
|
|
551
|
-
|
|
552
|
-
const { value, done } = await reader.read();
|
|
553
|
-
if (done) break;
|
|
554
|
-
if (!value || value.byteLength === 0) continue;
|
|
555
|
-
let text = Buffer.from(value).toString("utf8");
|
|
556
|
-
if (!headInjected) {
|
|
557
|
-
const combined = carry + text;
|
|
558
|
-
const idx = combined.indexOf(HEAD_CLOSE);
|
|
559
|
-
if (idx >= 0) {
|
|
560
|
-
// Send everything up to the </head> position, then the
|
|
561
|
-
// headBlob, then </head>, then the remainder.
|
|
562
|
-
const before = combined.slice(0, idx);
|
|
563
|
-
const after = combined.slice(idx + HEAD_CLOSE.length);
|
|
564
|
-
// Drop the carry portion from `before` that we already
|
|
565
|
-
// emitted as part of the previous chunk's send. But since
|
|
566
|
-
// we DIDN'T emit `carry` previously (it was withheld), we
|
|
567
|
-
// can send the full `before` here.
|
|
568
|
-
sendChunk(before);
|
|
569
|
-
sendChunk(headBlob);
|
|
570
|
-
sendChunk(HEAD_CLOSE);
|
|
571
|
-
if (after) sendChunk(after);
|
|
572
|
-
headInjected = true;
|
|
573
|
-
carry = "";
|
|
574
|
-
} else {
|
|
575
|
-
// No </head> yet — emit everything except the last
|
|
576
|
-
// (HEAD_CLOSE.length - 1) bytes so a tag split across
|
|
577
|
-
// chunk boundaries still gets caught next pass.
|
|
578
|
-
const keep = HEAD_CLOSE.length - 1;
|
|
579
|
-
if (combined.length > keep) {
|
|
580
|
-
sendChunk(combined.slice(0, combined.length - keep));
|
|
581
|
-
carry = combined.slice(combined.length - keep);
|
|
582
|
-
} else {
|
|
583
|
-
carry = combined;
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
} else {
|
|
587
|
-
// base64 in pure JS via Buffer (Bun ships it). For large
|
|
588
|
-
// pages this is O(n) per chunk; fine for Phase 1.
|
|
589
|
-
sendChunk(text);
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
// Flush any residual carry (head close never seen — page
|
|
593
|
-
// didn't have a </head>, which is fine for fragment renders).
|
|
594
|
-
if (carry) sendChunk(carry);
|
|
765
|
+
await streamWithHeadInjection(stream.getReader(), headBlob, sendChunk);
|
|
595
766
|
|
|
596
767
|
// Hydration tail. After React's stream EOFs we append the
|
|
597
768
|
// hydration markers so the browser can hydrate:
|
|
@@ -611,28 +782,30 @@ export async function handleRenderRoute(
|
|
|
611
782
|
// and `getManifest` parses with mtime-keyed caching. Falls back
|
|
612
783
|
// to a no-hydration warning if the manifest can't be loaded
|
|
613
784
|
// (rare — usually means the bundler crashed).
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
785
|
+
if (!isBoundaryComponent) {
|
|
786
|
+
const hydrationPayload = {
|
|
787
|
+
component: msg.component,
|
|
788
|
+
layouts: msg.layouts ?? [],
|
|
789
|
+
props,
|
|
790
|
+
};
|
|
791
|
+
const json = JSON.stringify(hydrationPayload).replaceAll("<", "\\u003c");
|
|
620
792
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
793
|
+
let tail = `<script id="__PYLON_DATA__" type="application/json">${json}</script>`;
|
|
794
|
+
if (preloadManifestRoute) {
|
|
795
|
+
// Per-route entry script comes last — it needs the inline
|
|
796
|
+
// `__PYLON_DATA__` above to have been parsed before it runs.
|
|
797
|
+
// CSS + modulepreload links were already injected into `<head>`
|
|
798
|
+
// above so they could start fetching as early as possible.
|
|
799
|
+
tail += `<script type="module" src="${preloadPublicPrefix}${preloadManifestRoute.file}"></script>`;
|
|
800
|
+
} else {
|
|
801
|
+
tail += `<script>console.warn(${JSON.stringify(`[pylon ssr] hydration disabled: ${preloadManifestErr}`)})</script>`;
|
|
802
|
+
}
|
|
803
|
+
send({
|
|
804
|
+
type: "render_chunk",
|
|
805
|
+
call_id: msg.call_id,
|
|
806
|
+
data: Buffer.from(tail, "utf8").toString("base64"),
|
|
807
|
+
});
|
|
630
808
|
}
|
|
631
|
-
send({
|
|
632
|
-
type: "render_chunk",
|
|
633
|
-
call_id: msg.call_id,
|
|
634
|
-
data: Buffer.from(tail, "utf8").toString("base64"),
|
|
635
|
-
});
|
|
636
809
|
|
|
637
810
|
send({ type: "render_done", call_id: msg.call_id });
|
|
638
811
|
} catch (err: any) {
|
|
@@ -650,7 +823,26 @@ export async function handleRenderRoute(
|
|
|
650
823
|
send({ type: "render_done", call_id: msg.call_id });
|
|
651
824
|
return;
|
|
652
825
|
}
|
|
653
|
-
// notFound() →
|
|
826
|
+
// notFound() → look for the nearest not-found.tsx walking up from the
|
|
827
|
+
// page's directory; render it (wrapped in the route's layouts) at 404.
|
|
828
|
+
// Falls back to a minimal framework body if none is defined.
|
|
829
|
+
if (
|
|
830
|
+
await tryRenderBoundary({
|
|
831
|
+
React,
|
|
832
|
+
renderToReadableStream,
|
|
833
|
+
cwd,
|
|
834
|
+
componentPath: msg.component,
|
|
835
|
+
fileName: "not-found",
|
|
836
|
+
layouts: msg.layouts,
|
|
837
|
+
props,
|
|
838
|
+
send,
|
|
839
|
+
callId: msg.call_id,
|
|
840
|
+
status: 404,
|
|
841
|
+
headers: finalizeHeaders(responseState),
|
|
842
|
+
})
|
|
843
|
+
) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
654
846
|
const body404 =
|
|
655
847
|
'<!DOCTYPE html><html><head><meta charset="utf-8"><title>404 — Not Found</title></head><body><h1>404</h1><p>This page could not be found.</p></body></html>';
|
|
656
848
|
send({
|
|
@@ -667,7 +859,27 @@ export async function handleRenderRoute(
|
|
|
667
859
|
send({ type: "render_done", call_id: msg.call_id });
|
|
668
860
|
return;
|
|
669
861
|
}
|
|
670
|
-
// Real pre-first-chunk error →
|
|
862
|
+
// Real pre-first-chunk error → look for the nearest error.tsx walking up
|
|
863
|
+
// from the page's directory; render it (wrapped in the route's layouts)
|
|
864
|
+
// at 500 with the thrown error passed in props. Falls back to a host-level
|
|
865
|
+
// 500 (type:"error") if none is defined or the boundary itself throws.
|
|
866
|
+
if (
|
|
867
|
+
await tryRenderBoundary({
|
|
868
|
+
React,
|
|
869
|
+
renderToReadableStream,
|
|
870
|
+
cwd,
|
|
871
|
+
componentPath: msg.component,
|
|
872
|
+
fileName: "error",
|
|
873
|
+
layouts: msg.layouts,
|
|
874
|
+
props: props ? { ...props, error: err } : null,
|
|
875
|
+
send,
|
|
876
|
+
callId: msg.call_id,
|
|
877
|
+
status: 500,
|
|
878
|
+
headers: finalizeHeaders(responseState),
|
|
879
|
+
})
|
|
880
|
+
) {
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
671
883
|
send({
|
|
672
884
|
type: "error",
|
|
673
885
|
call_id: msg.call_id,
|