@pylonsync/functions 0.3.293 → 0.3.294
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
CHANGED
|
@@ -160,6 +160,38 @@ describe("ssr-client-bundler (Phase 1.5e)", () => {
|
|
|
160
160
|
expect(chunkHasReact).toBe(true);
|
|
161
161
|
});
|
|
162
162
|
|
|
163
|
+
test("client runtime wires the nav-fallback guard (uncaught render error → full page load)", async () => {
|
|
164
|
+
// Regression: a page that renders React-19-hoisted <title>/<meta>/<link> in
|
|
165
|
+
// its own tree (use the `metadata` export instead) makes a client-side nav
|
|
166
|
+
// re-render throw in React's commit phase — the URL changes but the page
|
|
167
|
+
// can't swap (white screen). hydrateRoot must carry an onUncaughtError that
|
|
168
|
+
// falls back to a full page load of the in-flight destination so nav
|
|
169
|
+
// degrades gracefully. The distinctive console string is preserved through
|
|
170
|
+
// minification, so we assert the guard actually ships in the bundle.
|
|
171
|
+
tempDir = makeFixture(
|
|
172
|
+
{ "page.tsx": PAGE_BODY("Home") },
|
|
173
|
+
{ "layout.tsx": LAYOUT_BODY },
|
|
174
|
+
);
|
|
175
|
+
originalCwd = process.cwd();
|
|
176
|
+
process.chdir(tempDir);
|
|
177
|
+
|
|
178
|
+
const { outdir } = await buildClientBundle();
|
|
179
|
+
// The runtime (hydrateRoot + guard) lands in the shared chunk with multiple
|
|
180
|
+
// entries, or inlined in the entry with one — read every emitted .js.
|
|
181
|
+
const readJsRecursive = (dir: string): string[] =>
|
|
182
|
+
fs.readdirSync(dir, { withFileTypes: true }).flatMap((d) => {
|
|
183
|
+
const full = path.join(dir, d.name);
|
|
184
|
+
if (d.isDirectory()) return readJsRecursive(full);
|
|
185
|
+
return d.name.endsWith(".js") ? [fs.readFileSync(full, "utf8")] : [];
|
|
186
|
+
});
|
|
187
|
+
const bundled = readJsRecursive(outdir).join("\n");
|
|
188
|
+
|
|
189
|
+
// onUncaughtError is a React hydrateRoot option (property name preserved).
|
|
190
|
+
expect(bundled).toMatch(/onUncaughtError/);
|
|
191
|
+
// The fallback path: a full page load of the pending destination.
|
|
192
|
+
expect(bundled).toMatch(/falling back to a full page load/);
|
|
193
|
+
});
|
|
194
|
+
|
|
163
195
|
test("manifest names every route, each with a non-empty imports list", async () => {
|
|
164
196
|
tempDir = makeFixture(
|
|
165
197
|
{
|
|
@@ -212,6 +212,10 @@ import { createPylonBoundary } from "./client-boundary";
|
|
|
212
212
|
|
|
213
213
|
const routeCache = Object.create(null);
|
|
214
214
|
let activeRoot = null;
|
|
215
|
+
// Destination of an in-flight client navigation. Read by hydrateRoot's
|
|
216
|
+
// onUncaughtError so a re-render that throws mid-nav degrades to a full page
|
|
217
|
+
// load instead of a white screen. Null when no nav is in flight.
|
|
218
|
+
let pendingNav = null;
|
|
215
219
|
let manifestPromise = null;
|
|
216
220
|
const prefetchedChunks = new Set();
|
|
217
221
|
|
|
@@ -466,7 +470,30 @@ export function hydrate(component, Page, Layouts) {
|
|
|
466
470
|
data.component,
|
|
467
471
|
navEpoch,
|
|
468
472
|
);
|
|
469
|
-
activeRoot = hydrateRoot(document, tree
|
|
473
|
+
activeRoot = hydrateRoot(document, tree, {
|
|
474
|
+
// Safety net for client navigation. If a nav re-render throws an uncaught
|
|
475
|
+
// error in React's commit phase, the URL has already changed but the page
|
|
476
|
+
// can't swap — a white/stale screen. The classic trigger is a page that
|
|
477
|
+
// renders hoisted <title>/<meta>/<link> in its own tree (use the
|
|
478
|
+
// \`metadata\` export instead); React 19 owns those head nodes on the client
|
|
479
|
+
// and reconciling them across routes can throw. Rather than strand the
|
|
480
|
+
// user, fall back to a full page load of the pending destination, which
|
|
481
|
+
// re-renders it cleanly from SSR. Non-navigation errors keep React's
|
|
482
|
+
// default reporting.
|
|
483
|
+
onUncaughtError(error) {
|
|
484
|
+
if (pendingNav) {
|
|
485
|
+
const dest = pendingNav;
|
|
486
|
+
pendingNav = null;
|
|
487
|
+
console.error(
|
|
488
|
+
"[pylon ssr] client navigation failed to render; falling back to a full page load:",
|
|
489
|
+
error,
|
|
490
|
+
);
|
|
491
|
+
window.location.href = dest;
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
console.error(error);
|
|
495
|
+
},
|
|
496
|
+
});
|
|
470
497
|
installNavHandlers();
|
|
471
498
|
return;
|
|
472
499
|
}
|
|
@@ -544,13 +571,21 @@ async function navigate(href, opts) {
|
|
|
544
571
|
setNavParams(data);
|
|
545
572
|
navEpoch++;
|
|
546
573
|
currentPageProps = withClientProps(data);
|
|
574
|
+
const target = url.pathname + url.search;
|
|
547
575
|
const tree = withBoundary(
|
|
548
576
|
buildTree(route.Page, route.Layouts, currentPageProps),
|
|
549
577
|
data.component,
|
|
550
578
|
navEpoch,
|
|
551
579
|
);
|
|
580
|
+
// Track the in-flight destination so hydrateRoot's onUncaughtError can fall
|
|
581
|
+
// back to a full page load if this re-render throws in React's commit phase
|
|
582
|
+
// (instead of leaving the URL changed but the page unswapped). Cleared on the
|
|
583
|
+
// next macrotask once the commit has settled with no error.
|
|
584
|
+
pendingNav = target;
|
|
552
585
|
activeRoot.render(tree);
|
|
553
|
-
|
|
586
|
+
setTimeout(() => {
|
|
587
|
+
if (pendingNav === target) pendingNav = null;
|
|
588
|
+
}, 0);
|
|
554
589
|
if (opts && opts.replace) {
|
|
555
590
|
history.replaceState({ component: data.component }, "", target);
|
|
556
591
|
} else if (push) {
|