@rangojs/router 0.20.0 → 0.21.0
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/bin/rango.js +26 -14
- package/dist/types/browser/event-controller.d.ts +32 -0
- package/dist/types/browser/history-state.d.ts +16 -4
- package/dist/types/browser/navigation-transaction.d.ts +7 -0
- package/dist/types/browser/rango-state.d.ts +3 -1
- package/dist/types/browser/react/context.d.ts +11 -0
- package/dist/types/browser/react/location-state-shared.d.ts +84 -10
- package/dist/types/browser/react/location-state.d.ts +7 -2
- package/dist/types/browser/react/subscribe-to-nothing.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +4 -5
- package/dist/types/cache/cf/cf-cache-store.d.ts +27 -6
- package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
- package/dist/types/cache/handle-snapshot.d.ts +36 -50
- package/dist/types/cache/index.d.ts +2 -0
- package/dist/types/cache/shell-snapshot.d.ts +19 -3
- package/dist/types/cache/types.d.ts +21 -10
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +28 -16
- package/dist/types/client-urls/navigation.d.ts +7 -1
- package/dist/types/client-urls/optimistic-location.d.ts +10 -4
- package/dist/types/handles/deferred-resolution.d.ts +11 -1
- package/dist/types/prerender/build-shell-capture.d.ts +8 -6
- package/dist/types/router/match-middleware/cache-lookup.d.ts +1 -1
- package/dist/types/router/navigation-snapshot.d.ts +8 -1
- package/dist/types/router/router-interfaces.d.ts +2 -1
- package/dist/types/router/router-options.d.ts +17 -7
- package/dist/types/router/segment-resolution/loader-cache.d.ts +21 -9
- package/dist/types/router-versions.d.ts +39 -0
- package/dist/types/rsc/handler-context.d.ts +5 -0
- package/dist/types/rsc/shell-build-manifest.d.ts +3 -2
- package/dist/types/rsc/types.d.ts +4 -6
- package/dist/types/server/build-version-table.d.ts +44 -0
- package/dist/types/server/handle-store.d.ts +87 -46
- package/dist/types/server/request-context.d.ts +22 -8
- package/dist/types/testing/build-versions.d.ts +32 -0
- package/dist/types/testing/dom.entry.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +2 -0
- package/dist/types/testing/internal/seed-vars.d.ts +7 -2
- package/dist/types/testing/location-state-key.d.ts +5 -1
- package/dist/types/testing/render-route.d.ts +77 -6
- package/dist/types/testing/serve-shell-request.d.ts +21 -0
- package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/types/handler-context.d.ts +8 -5
- package/dist/types/vite/discovery/build-versions.d.ts +393 -0
- package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
- package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
- package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
- package/dist/types/vite/discovery/state.d.ts +14 -0
- package/dist/types/vite/encryption-key.d.ts +18 -0
- package/dist/types/vite/plugin-types.d.ts +27 -0
- package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
- package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
- package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
- package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
- package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
- package/dist/types/vite/plugins/virtual-entries.d.ts +18 -1
- package/dist/types/vite/router-discovery.d.ts +6 -0
- package/dist/types/vite/utils/client-chunks.d.ts +2 -0
- package/dist/types/vite/utils/compare-strings.d.ts +7 -0
- package/dist/types/vite/utils/package-resolution.d.ts +8 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
- package/dist/vite/index.js +1537 -444
- package/package.json +1 -2
- package/skills/cache-guide/SKILL.md +84 -20
- package/skills/caching/SKILL.md +16 -5
- package/skills/client-urls/SKILL.md +16 -0
- package/skills/cloudflare/SKILL.md +2 -1
- package/skills/comparison/references/framework-comparison.md +16 -12
- package/skills/hooks/SKILL.md +3 -0
- package/skills/hooks/handle-and-actions.md +6 -0
- package/skills/hooks/navigation.md +28 -0
- package/skills/hooks/state.md +209 -8
- package/skills/hooks/urls.md +40 -1
- package/skills/host-router/SKILL.md +13 -0
- package/skills/intercept/SKILL.md +6 -0
- package/skills/loader/SKILL.md +19 -1
- package/skills/middleware/SKILL.md +2 -0
- package/skills/ppr/SKILL.md +119 -40
- package/skills/prerender/SKILL.md +7 -3
- package/skills/rango/SKILL.md +8 -7
- package/skills/router-setup/SKILL.md +15 -1
- package/skills/testing/SKILL.md +30 -26
- package/skills/testing/client-components.md +193 -8
- package/skills/testing/handles.md +2 -0
- package/skills/testing/loader.md +1 -1
- package/skills/testing/middleware.md +1 -1
- package/skills/testing/render-handler.md +18 -18
- package/skills/testing/serve-shell-request.md +63 -14
- package/skills/testing/server-actions.md +17 -17
- package/skills/testing/server-tree.md +1 -1
- package/skills/typesafety/generated-files-and-cli.md +1 -1
- package/skills/use-cache/SKILL.md +16 -5
- package/skills/vercel/SKILL.md +3 -3
- package/src/bin/rango.ts +1 -1
- package/src/browser/event-controller.ts +75 -2
- package/src/browser/history-state.ts +39 -17
- package/src/browser/navigation-bridge.ts +41 -18
- package/src/browser/navigation-transaction.ts +15 -10
- package/src/browser/partial-update.ts +10 -2
- package/src/browser/rango-state.ts +3 -1
- package/src/browser/react/Link.tsx +2 -3
- package/src/browser/react/NavigationProvider.tsx +25 -1
- package/src/browser/react/context.ts +13 -0
- package/src/browser/react/location-state-shared.ts +200 -34
- package/src/browser/react/location-state.ts +40 -75
- package/src/browser/react/subscribe-to-nothing.ts +7 -0
- package/src/browser/react/use-handle.ts +20 -1
- package/src/browser/rsc-router.tsx +33 -11
- package/src/browser/server-action-bridge.ts +17 -1
- package/src/cache/cache-scope.ts +18 -8
- package/src/cache/cf/cf-cache-store.ts +75 -55
- package/src/cache/cf/cf-cache-types.ts +12 -4
- package/src/cache/handle-snapshot.ts +69 -87
- package/src/cache/index.ts +4 -0
- package/src/cache/shell-snapshot.ts +35 -5
- package/src/cache/types.ts +21 -10
- package/src/cache/vercel/vercel-cache-store.ts +48 -20
- package/src/client-urls/client-root.tsx +1 -0
- package/src/client-urls/navigation.ts +10 -0
- package/src/client-urls/optimistic-location.ts +10 -4
- package/src/handles/deferred-resolution.ts +31 -0
- package/src/prerender/build-shell-capture.ts +11 -9
- package/src/router/manifest.ts +2 -0
- package/src/router/match-middleware/cache-lookup.ts +11 -18
- package/src/router/navigation-snapshot.ts +25 -6
- package/src/router/request-classification.ts +15 -12
- package/src/router/router-interfaces.ts +2 -1
- package/src/router/router-options.ts +17 -7
- package/src/router/segment-resolution/loader-cache.ts +95 -60
- package/src/router-versions.ts +60 -0
- package/src/rsc/full-payload.ts +6 -2
- package/src/rsc/handler-context.ts +5 -0
- package/src/rsc/handler.ts +14 -4
- package/src/rsc/rsc-rendering.ts +12 -1
- package/src/rsc/shell-build-manifest.ts +10 -7
- package/src/rsc/shell-capture.ts +36 -26
- package/src/rsc/types.ts +4 -6
- package/src/server/build-version-table.ts +115 -0
- package/src/server/handle-store.ts +344 -127
- package/src/server/request-context.ts +62 -11
- package/src/ssr/ssr-root.tsx +16 -4
- package/src/testing/build-versions.ts +64 -0
- package/src/testing/dispatch.ts +6 -0
- package/src/testing/dom.entry.ts +1 -0
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +3 -0
- package/src/testing/internal/context.ts +2 -1
- package/src/testing/internal/seed-vars.ts +14 -2
- package/src/testing/location-state-key.ts +18 -8
- package/src/testing/render-handler.ts +2 -1
- package/src/testing/render-route.tsx +277 -43
- package/src/testing/serve-shell-request.ts +80 -11
- package/src/testing/vitest-stubs/version.ts +4 -1
- package/src/types/handler-context.ts +8 -5
- package/src/vite/discovery/build-versions.ts +1281 -0
- package/src/vite/discovery/installed-packages.ts +159 -0
- package/src/vite/discovery/prerender-collection.ts +38 -1
- package/src/vite/discovery/router-versions-phase.ts +336 -0
- package/src/vite/discovery/shell-prerender-phase.ts +28 -38
- package/src/vite/discovery/state.ts +19 -0
- package/src/vite/discovery/virtual-module-codegen.ts +7 -2
- package/src/vite/encryption-key.ts +101 -9
- package/src/vite/plugin-types.ts +28 -0
- package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
- package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
- package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
- package/src/vite/plugins/expose-internal-ids.ts +12 -3
- package/src/vite/plugins/stable-rsc-output.ts +147 -0
- package/src/vite/plugins/version-plugin.ts +19 -24
- package/src/vite/plugins/version.d.ts +17 -2
- package/src/vite/plugins/virtual-entries.ts +31 -5
- package/src/vite/rango.ts +28 -3
- package/src/vite/router-discovery.ts +82 -13
- package/src/vite/utils/client-chunks.ts +5 -1
- package/src/vite/utils/compare-strings.ts +9 -0
- package/src/vite/utils/package-resolution.ts +9 -0
- package/src/vite/utils/prerender-utils.ts +4 -1
- package/src/vite/utils/shared-utils.ts +1 -1
|
@@ -35,11 +35,19 @@
|
|
|
35
35
|
* useRouter().replace kind "replace", refresh() kind "revalidate".
|
|
36
36
|
* Back/forward, action and optimistic clientUrls() swap decisions are
|
|
37
37
|
* e2e territory.
|
|
38
|
+
* - Location state reaches readers the way it does in production:
|
|
39
|
+
* NavigationProvider holds the committed entry's state next to the
|
|
40
|
+
* payload, so a navigation whose loader the test holds keeps the entry
|
|
41
|
+
* being left on screen with its own state. A `popstate` event is a
|
|
42
|
+
* back/forward onto the entry history.state holds; the tree has one
|
|
43
|
+
* location, so no page is restored with it. A server action's state, a
|
|
44
|
+
* redirect's and an intercept's are e2e territory.
|
|
38
45
|
* What it DOES cover: client hooks that read NavigationProvider /
|
|
39
46
|
* OutletContext — useParams, useReverse, useHref, useMount, useNavigation,
|
|
40
47
|
* useRouter, usePathname, useSearchParams, Outlet/useOutlet nesting and seeded
|
|
41
48
|
* descendant pending state, useLoader/useFetchLoader (seeded data),
|
|
42
|
-
* useLocationState (seeded), and useHandle
|
|
49
|
+
* useLocationState (seeded, or written by a navigation), and useHandle
|
|
50
|
+
* (seeded).
|
|
43
51
|
* Basename-mounted apps: pass the `basename` option so useRouter().basename,
|
|
44
52
|
* <Link> prefixing, and useMount/useHref resolve against the mount prefix
|
|
45
53
|
* (without it they resolve at the root "/"). For an include("/shop", ...)
|
|
@@ -48,7 +56,12 @@
|
|
|
48
56
|
*/
|
|
49
57
|
|
|
50
58
|
import { decideTransitionGatedOff } from "../browser/transition-when.js";
|
|
51
|
-
import {
|
|
59
|
+
import {
|
|
60
|
+
StrictMode,
|
|
61
|
+
useEffect,
|
|
62
|
+
type ReactNode,
|
|
63
|
+
type ComponentType,
|
|
64
|
+
} from "react";
|
|
52
65
|
import type { RenderResult } from "@testing-library/react";
|
|
53
66
|
import { renderSegments } from "../segment-system.js";
|
|
54
67
|
import {
|
|
@@ -62,6 +75,7 @@ import {
|
|
|
62
75
|
shouldStartViewTransition,
|
|
63
76
|
} from "../browser/partial-update.js";
|
|
64
77
|
import type {
|
|
78
|
+
NavigateOptions,
|
|
65
79
|
NavigateOptionsInternal,
|
|
66
80
|
NavigationStore,
|
|
67
81
|
NavigationBridge,
|
|
@@ -80,10 +94,13 @@ import type {
|
|
|
80
94
|
TransitionConfig,
|
|
81
95
|
TransitionWhenKind,
|
|
82
96
|
} from "../types.js";
|
|
83
|
-
import
|
|
97
|
+
import {
|
|
98
|
+
setLocationStateVersion,
|
|
99
|
+
type LocationStateDefinition,
|
|
100
|
+
} from "../browser/react/location-state-shared.js";
|
|
84
101
|
import {
|
|
85
102
|
buildHistoryState,
|
|
86
|
-
|
|
103
|
+
clearLocationStateOnDocumentLoad,
|
|
87
104
|
pushHistoryWithIdx,
|
|
88
105
|
resolveNavigationState,
|
|
89
106
|
} from "../browser/history-state.js";
|
|
@@ -230,6 +247,8 @@ export interface RenderRouteOptions {
|
|
|
230
247
|
* `useLocationState(StateDef)`. Like loaders, a real `createLocationState()`
|
|
231
248
|
* handle has an empty injected key in a bare test, so pass `[def, value]`
|
|
232
249
|
* pairs; renderRoute assigns a synthetic key and writes it to `history.state`.
|
|
250
|
+
* With `hydrate: true` a `clearOnReload` definition's seed is removed before
|
|
251
|
+
* hydration, as on a document load.
|
|
233
252
|
*
|
|
234
253
|
* @example
|
|
235
254
|
* renderRoute([{ path: "/", Component: FlashBanner }], {
|
|
@@ -265,6 +284,24 @@ export interface RenderRouteOptions {
|
|
|
265
284
|
* computes the segment id for you). Merged with `handles`.
|
|
266
285
|
*/
|
|
267
286
|
handle?: HandleDataSeed;
|
|
287
|
+
/**
|
|
288
|
+
* Handle values a document delivers after hydration, as a loader's push
|
|
289
|
+
* that is not in the document's handle snapshot is (a push made after the
|
|
290
|
+
* handler output settled, or any loader push on a PPR shell HIT). Each
|
|
291
|
+
* entry is `[handle, values]`: the handle's values after the update, which
|
|
292
|
+
* replace its `handles` seed once the root has hydrated (or mounted). With
|
|
293
|
+
* `hydrate: true` the server HTML is rendered from `handles`, and a reader
|
|
294
|
+
* inside a `<Suspense>` boundary hydrates after the update was applied, as
|
|
295
|
+
* a boundary that hydrates after the root does in production.
|
|
296
|
+
*
|
|
297
|
+
* @example
|
|
298
|
+
* const { serverHtml, recoverableErrors } = await renderRoute(routes, {
|
|
299
|
+
* hydrate: true,
|
|
300
|
+
* handles: [[Notes, ["from-handler"]]],
|
|
301
|
+
* lateHandles: [[Notes, ["from-handler", "from-loader"]]],
|
|
302
|
+
* });
|
|
303
|
+
*/
|
|
304
|
+
lateHandles?: ReadonlyArray<readonly [Handle<any, any>, unknown[]]>;
|
|
268
305
|
/**
|
|
269
306
|
* Route name -> pattern map. Informational for parity with the server test
|
|
270
307
|
* context; client useReverse takes its map directly as an argument, so this
|
|
@@ -326,6 +363,25 @@ export interface RenderRouteOptions {
|
|
|
326
363
|
* not override a `"none"` default.
|
|
327
364
|
*/
|
|
328
365
|
defaultPrefetch?: PrefetchStrategy;
|
|
366
|
+
/**
|
|
367
|
+
* Hydrate instead of mounting fresh: the same element is rendered to HTML
|
|
368
|
+
* (react-dom/server `renderToString`), placed in the container and hydrated.
|
|
369
|
+
* The result gains `serverHtml` and `recoverableErrors`
|
|
370
|
+
* (RenderRouteHydrateResult). `data-hydrated` is set on `<html>` after
|
|
371
|
+
* hydration, as production's root does, and removed on unmount.
|
|
372
|
+
*
|
|
373
|
+
* Limits:
|
|
374
|
+
* - The server pass runs without `window` and `document`, and only those. A
|
|
375
|
+
* hook that reads a global that stays defined there (`history`,
|
|
376
|
+
* `localStorage`, `navigator`) outside a server snapshot renders the same
|
|
377
|
+
* in both passes here and can still mismatch in real SSR.
|
|
378
|
+
* - Content that suspends in the server pass (`lazy`, a pending or untracked
|
|
379
|
+
* `use()` promise) is emitted as its fallback and client-rendered, not
|
|
380
|
+
* hydrated; React reports the boundary in `recoverableErrors`. Outside any
|
|
381
|
+
* `<Suspense>` it rejects.
|
|
382
|
+
* - Needs @testing-library/react >= 16.2.0; throws on older versions.
|
|
383
|
+
*/
|
|
384
|
+
hydrate?: boolean;
|
|
329
385
|
}
|
|
330
386
|
|
|
331
387
|
/**
|
|
@@ -356,10 +412,20 @@ export interface TestRouterHandle {
|
|
|
356
412
|
* `options.transition: false` is the per-navigation opt-out
|
|
357
413
|
* (`router.push(url, { transition: false })`, `<Link transition={false}>`):
|
|
358
414
|
* the commit is urgent and no transition({ when }) predicate is called.
|
|
415
|
+
*
|
|
416
|
+
* `options.state` and `options.replace: true` write the history entry the
|
|
417
|
+
* way `useRouter().push(url, { state })` and `.replace(url, { state })` do,
|
|
418
|
+
* so a test can carry location state on a navigation whose loader it
|
|
419
|
+
* holds: `state` pushes an entry with it, `replace: true` replaces the
|
|
420
|
+
* current entry (with `state`, or with none, which drops the entry's
|
|
421
|
+
* location state as a production replace does). With neither, history is
|
|
422
|
+
* left alone and readers keep the state they have: `navigate(url)` and
|
|
423
|
+
* `navigate(url, { replace: false })` are the same call.
|
|
359
424
|
*/
|
|
360
425
|
navigate(
|
|
361
426
|
url: string,
|
|
362
|
-
options?: Pick<RenderRouteOptions, "loaders"> &
|
|
427
|
+
options?: Pick<RenderRouteOptions, "loaders"> &
|
|
428
|
+
Pick<NavigateOptions, "state" | "replace" | "transition">,
|
|
363
429
|
): Promise<void>;
|
|
364
430
|
/**
|
|
365
431
|
* Re-render the current location, as router.refresh() does: a spec's
|
|
@@ -379,6 +445,21 @@ export interface TestRouterHandle {
|
|
|
379
445
|
/** Result of renderRoute: RTL's render result plus the router handle. */
|
|
380
446
|
export type RenderRouteResult = RenderResult & { router: TestRouterHandle };
|
|
381
447
|
|
|
448
|
+
/** Result of renderRoute with `hydrate: true`. */
|
|
449
|
+
export type RenderRouteHydrateResult = RenderRouteResult & {
|
|
450
|
+
/** The HTML the server pass produced, as placed in the container. */
|
|
451
|
+
serverHtml: string;
|
|
452
|
+
/**
|
|
453
|
+
* Messages of the errors React recovered from (`onRecoverableError`): a
|
|
454
|
+
* hydration mismatch that made it re-render on the client, or a boundary the
|
|
455
|
+
* server pass could not finish. Empty means the tree hydrated as rendered.
|
|
456
|
+
* Live: React appends for as long as the root is mounted. An attribute-only
|
|
457
|
+
* mismatch is NOT here — React keeps the server attribute and only logs it
|
|
458
|
+
* with `console.error` in development.
|
|
459
|
+
*/
|
|
460
|
+
recoverableErrors: string[];
|
|
461
|
+
};
|
|
462
|
+
|
|
382
463
|
interface ResolvedMatch {
|
|
383
464
|
params: Record<string, string>;
|
|
384
465
|
pathname: string;
|
|
@@ -419,6 +500,45 @@ function DelegatedPrefetchRegistration({
|
|
|
419
500
|
return null;
|
|
420
501
|
}
|
|
421
502
|
|
|
503
|
+
/**
|
|
504
|
+
* initBrowserApp calls the bridge's registerLinkInterception, which is where
|
|
505
|
+
* production listens for `popstate` (navigation-bridge.ts); a rendered tree
|
|
506
|
+
* has no initBrowserApp.
|
|
507
|
+
*/
|
|
508
|
+
function LinkInterceptionRegistration({
|
|
509
|
+
bridge,
|
|
510
|
+
}: {
|
|
511
|
+
bridge: NavigationBridge;
|
|
512
|
+
}): null {
|
|
513
|
+
useEffect(() => bridge.registerLinkInterception(), [bridge]);
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* The marker production's root sets from its effect after hydration
|
|
519
|
+
* (browser/rsc-router.tsx Rango), so a boundary that hydrates in a later pass
|
|
520
|
+
* finds it set, as in production. Removed on unmount: a test unmounts its
|
|
521
|
+
* root, production never does.
|
|
522
|
+
*/
|
|
523
|
+
function HydratedMarker(): null {
|
|
524
|
+
useEffect(() => {
|
|
525
|
+
document.documentElement.dataset.hydrated = "";
|
|
526
|
+
return () => document.documentElement.removeAttribute("data-hydrated");
|
|
527
|
+
}, []);
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Applies `lateHandles` from an effect after the tree's: where production's
|
|
533
|
+
* root effect releases the document's late handle channel
|
|
534
|
+
* (browser/rsc-router.tsx hydrationCommitted). A boundary that hydrates in a
|
|
535
|
+
* later pass finds the update applied.
|
|
536
|
+
*/
|
|
537
|
+
function LateHandles({ apply }: { apply: () => void }): null {
|
|
538
|
+
useEffect(apply, [apply]);
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
|
|
422
542
|
function matchLeaf(
|
|
423
543
|
pattern: string,
|
|
424
544
|
pathname: string,
|
|
@@ -504,6 +624,14 @@ function buildSegments(
|
|
|
504
624
|
return segments;
|
|
505
625
|
}
|
|
506
626
|
|
|
627
|
+
export function renderRoute(
|
|
628
|
+
routes: RenderRouteSpec[],
|
|
629
|
+
options: RenderRouteOptions & { hydrate: true },
|
|
630
|
+
): Promise<RenderRouteHydrateResult>;
|
|
631
|
+
export function renderRoute(
|
|
632
|
+
routes: RenderRouteSpec[],
|
|
633
|
+
options?: RenderRouteOptions,
|
|
634
|
+
): Promise<RenderRouteResult>;
|
|
507
635
|
export async function renderRoute(
|
|
508
636
|
routes: RenderRouteSpec[],
|
|
509
637
|
options: RenderRouteOptions = {},
|
|
@@ -539,11 +667,17 @@ export async function renderRoute(
|
|
|
539
667
|
const loaderData = seedLoaders(options.loaderData ?? {}, options.loaders);
|
|
540
668
|
|
|
541
669
|
if (typeof window !== "undefined") {
|
|
670
|
+
// The tree has no app version (its bridge reports none), so a seed needs
|
|
671
|
+
// none either. Reset what an initBrowserApp in the same file may have left.
|
|
672
|
+
setLocationStateVersion(undefined);
|
|
542
673
|
const stateObj: Record<string, unknown> = {};
|
|
543
674
|
for (const [def, value] of options.locationState ?? []) {
|
|
544
675
|
stateObj[withLocationStateKey(def).__rsc_ls_key] = value;
|
|
545
676
|
}
|
|
546
677
|
window.history.replaceState(stateObj, "");
|
|
678
|
+
// Hydrate mode is a document load of the seeded entry: run start-up's
|
|
679
|
+
// step (initBrowserApp), not a copy of it.
|
|
680
|
+
if (options.hydrate) clearLocationStateOnDocumentLoad();
|
|
547
681
|
}
|
|
548
682
|
|
|
549
683
|
const resolve = (pathname: string): ResolvedMatch => {
|
|
@@ -609,14 +743,33 @@ export async function renderRoute(
|
|
|
609
743
|
|
|
610
744
|
const eventController = createEventController({ initialLocation: url });
|
|
611
745
|
eventController.setParams(initialMatch.params);
|
|
746
|
+
// The seeded entry, as initBrowserApp commits the document's.
|
|
747
|
+
eventController.commitLocationState(window.history.state);
|
|
612
748
|
// Resolve-by-default: resolve any deferred (Promise) seeded handle values
|
|
613
749
|
// before applying, so the seeded handles reach collect/useHandle resolved —
|
|
614
750
|
// matching what the server/client do in a real app.
|
|
615
751
|
const resolvedSeed = await resolveDeferredHandleValues(handleSeed);
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
);
|
|
752
|
+
const initialSegmentIds = initialSegments.map((s) => s.id);
|
|
753
|
+
eventController.setHandleData(resolvedSeed, initialSegmentIds);
|
|
754
|
+
// As initBrowserApp before hydrateRoot: what a hydrating reader reads.
|
|
755
|
+
eventController.freezeHydrationHandleState();
|
|
756
|
+
// A late update carries the full state: the seed with these handles' values
|
|
757
|
+
// replaced, resolved as the browser resolves a late yield before applying.
|
|
758
|
+
let applyLateHandles: (() => void) | undefined;
|
|
759
|
+
if (options.lateHandles && leafRouteSegmentId !== undefined) {
|
|
760
|
+
const lateSeed = cloneHandleSeed(resolvedSeed);
|
|
761
|
+
for (const [handle, values] of options.lateHandles) {
|
|
762
|
+
const id = (handle as unknown as { $$id: string }).$$id;
|
|
763
|
+
(lateSeed[id] ??= {})[leafRouteSegmentId] = values;
|
|
764
|
+
}
|
|
765
|
+
const lateState = await resolveDeferredHandleValues(lateSeed);
|
|
766
|
+
// Flushed, not left to the notify timer: renderRoute's act() has
|
|
767
|
+
// returned by then.
|
|
768
|
+
applyLateHandles = () => {
|
|
769
|
+
eventController.setHandleData(lateState, initialSegmentIds);
|
|
770
|
+
eventController.flushRouteState();
|
|
771
|
+
};
|
|
772
|
+
}
|
|
620
773
|
|
|
621
774
|
let warnedNavLifecycle = false;
|
|
622
775
|
const navigate = async (
|
|
@@ -633,10 +786,11 @@ export async function renderRoute(
|
|
|
633
786
|
// A useRouter().push/replace or <Link> navigation (`history`) writes its
|
|
634
787
|
// entry the way production does (navigation-bridge.ts navigate ->
|
|
635
788
|
// navigation-transaction.ts commit): the dev state check, typed entries
|
|
636
|
-
// spread onto history.state, the idx stamp, and the
|
|
637
|
-
//
|
|
638
|
-
// put: renderRoute tracks location on the event
|
|
639
|
-
// window.location. router.navigate()
|
|
789
|
+
// spread onto history.state, the idx stamp, and the entry state committed
|
|
790
|
+
// on the event controller, which the provider takes with the payload.
|
|
791
|
+
// The URL stays put: renderRoute tracks location on the event
|
|
792
|
+
// controller, not window.location. router.navigate() without `state` or
|
|
793
|
+
// `replace: true`, and refresh(), leave history alone.
|
|
640
794
|
const history = navOptions?.history;
|
|
641
795
|
const historyState = history
|
|
642
796
|
? buildHistoryState(
|
|
@@ -702,16 +856,13 @@ export async function renderRoute(
|
|
|
702
856
|
eventController.setParams(match.params);
|
|
703
857
|
store.setCurrentUrl(nextUrl.href);
|
|
704
858
|
store.setSegmentIds(segments.map((s) => s.id));
|
|
705
|
-
let notifyLocationState = false;
|
|
706
859
|
if (history) {
|
|
707
|
-
notifyLocationState =
|
|
708
|
-
hasLocationState(window.history.state) ||
|
|
709
|
-
hasLocationState(historyState);
|
|
710
860
|
pushHistoryWithIdx(
|
|
711
861
|
historyState ?? null,
|
|
712
862
|
window.location.href,
|
|
713
863
|
history.replace ?? false,
|
|
714
864
|
);
|
|
865
|
+
eventController.commitLocationState(historyState);
|
|
715
866
|
}
|
|
716
867
|
const emit: UpdateSubscriber = (update) => store.emitUpdate(update);
|
|
717
868
|
await act(async () => {
|
|
@@ -723,9 +874,6 @@ export async function renderRoute(
|
|
|
723
874
|
} else {
|
|
724
875
|
emit({ root, metadata });
|
|
725
876
|
}
|
|
726
|
-
if (notifyLocationState) {
|
|
727
|
-
window.dispatchEvent(new Event("__rsc_locationstate"));
|
|
728
|
-
}
|
|
729
877
|
});
|
|
730
878
|
};
|
|
731
879
|
|
|
@@ -739,8 +887,20 @@ export async function renderRoute(
|
|
|
739
887
|
navigate: (target, navigateOptions) =>
|
|
740
888
|
navigate(target, { history: navigateOptions ?? {} }),
|
|
741
889
|
refresh: () => refresh(),
|
|
742
|
-
|
|
743
|
-
|
|
890
|
+
// Back/forward onto the entry history.state holds. The tree has one
|
|
891
|
+
// location, so nothing is restored: a treeless commit of that entry, as
|
|
892
|
+
// production's traversal that keeps every segment
|
|
893
|
+
// (navigation-transaction.ts commit). Flushed, not left to the notify
|
|
894
|
+
// timer: the dispatching test's act() has returned by then.
|
|
895
|
+
handlePopstate: async () => {
|
|
896
|
+
eventController.commitLocationState(window.history.state, true);
|
|
897
|
+
eventController.flushRouteState();
|
|
898
|
+
},
|
|
899
|
+
registerLinkInterception: () => {
|
|
900
|
+
const onPopstate = (): void => void bridge.handlePopstate();
|
|
901
|
+
window.addEventListener("popstate", onPopstate);
|
|
902
|
+
return () => window.removeEventListener("popstate", onPopstate);
|
|
903
|
+
},
|
|
744
904
|
registerDelegatedPrefetch: () =>
|
|
745
905
|
setupNavigationBridgeDelegatedPrefetch(
|
|
746
906
|
store,
|
|
@@ -764,6 +924,52 @@ export async function renderRoute(
|
|
|
764
924
|
outletPending: options.outletPending,
|
|
765
925
|
});
|
|
766
926
|
|
|
927
|
+
// The marker sits after the provider so its effect runs after the tree's,
|
|
928
|
+
// like the effect of production's root component.
|
|
929
|
+
const ui = (
|
|
930
|
+
<>
|
|
931
|
+
<NavigationProvider
|
|
932
|
+
store={store}
|
|
933
|
+
eventController={eventController}
|
|
934
|
+
initialPayload={{ root: initialTree, metadata: initialMetadata }}
|
|
935
|
+
bridge={bridge}
|
|
936
|
+
basename={basename}
|
|
937
|
+
themeConfig={
|
|
938
|
+
options.theme === undefined ? null : resolveThemeConfig(options.theme)
|
|
939
|
+
}
|
|
940
|
+
nonce={options.nonce}
|
|
941
|
+
/>
|
|
942
|
+
<DelegatedPrefetchRegistration bridge={bridge} />
|
|
943
|
+
<LinkInterceptionRegistration bridge={bridge} />
|
|
944
|
+
{options.hydrate && <HydratedMarker />}
|
|
945
|
+
{applyLateHandles && <LateHandles apply={applyLateHandles} />}
|
|
946
|
+
</>
|
|
947
|
+
);
|
|
948
|
+
|
|
949
|
+
let serverHtml: string | undefined;
|
|
950
|
+
const recoverableErrors: string[] = [];
|
|
951
|
+
if (options.hydrate) {
|
|
952
|
+
// RTL forwards onRecoverableError to hydrateRoot from 16.2.0. 16.0-16.1
|
|
953
|
+
// are inside the peer range and drop it (their render() never names the
|
|
954
|
+
// option), which would leave `recoverableErrors` empty on a real mismatch.
|
|
955
|
+
if (!String(render).includes("onRecoverableError")) {
|
|
956
|
+
throw new Error(
|
|
957
|
+
"renderRoute: `hydrate` needs @testing-library/react >= 16.2.0. Older " +
|
|
958
|
+
"versions do not pass onRecoverableError to hydrateRoot, so a " +
|
|
959
|
+
"hydration mismatch would go unreported.",
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
const { getConfig } = await import("@testing-library/react");
|
|
963
|
+
const { renderToString } = await import("react-dom/server");
|
|
964
|
+
// RTL wraps the hydrated element the same way.
|
|
965
|
+
const serverUi = getConfig().reactStrictMode ? (
|
|
966
|
+
<StrictMode>{ui}</StrictMode>
|
|
967
|
+
) : (
|
|
968
|
+
ui
|
|
969
|
+
);
|
|
970
|
+
serverHtml = withoutBrowserGlobals(() => renderToString(serverUi));
|
|
971
|
+
}
|
|
972
|
+
|
|
767
973
|
// Wrap render in an awaited async act so a tree that suspends (async loaders,
|
|
768
974
|
// loading states, deferred handle entries that arrive as a Promise) settles its
|
|
769
975
|
// Suspense within act — otherwise React orphans the resolution ("a component
|
|
@@ -772,30 +978,32 @@ export async function renderRoute(
|
|
|
772
978
|
let result!: Awaited<ReturnType<typeof render>>;
|
|
773
979
|
const container = document.body.appendChild(document.createElement("div"));
|
|
774
980
|
prefetchRoot = container;
|
|
981
|
+
if (serverHtml !== undefined) container.innerHTML = serverHtml;
|
|
775
982
|
await act(async () => {
|
|
776
|
-
result = render(
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
}
|
|
789
|
-
nonce={options.nonce}
|
|
790
|
-
/>
|
|
791
|
-
<DelegatedPrefetchRegistration bridge={bridge} />
|
|
792
|
-
</>,
|
|
793
|
-
{ baseElement: document.body, container },
|
|
794
|
-
);
|
|
983
|
+
result = render(ui, {
|
|
984
|
+
baseElement: document.body,
|
|
985
|
+
container,
|
|
986
|
+
...(serverHtml !== undefined && {
|
|
987
|
+
hydrate: true,
|
|
988
|
+
onRecoverableError: (error: unknown) => {
|
|
989
|
+
recoverableErrors.push(
|
|
990
|
+
error instanceof Error ? error.message : String(error),
|
|
991
|
+
);
|
|
992
|
+
},
|
|
993
|
+
}),
|
|
994
|
+
});
|
|
795
995
|
});
|
|
796
996
|
|
|
797
997
|
const router: TestRouterHandle = {
|
|
798
|
-
navigate,
|
|
998
|
+
navigate: (target, navOptions) =>
|
|
999
|
+
navigate(target, {
|
|
1000
|
+
loaders: navOptions?.loaders,
|
|
1001
|
+
transition: navOptions?.transition,
|
|
1002
|
+
history:
|
|
1003
|
+
navOptions?.state !== undefined || navOptions?.replace
|
|
1004
|
+
? navOptions
|
|
1005
|
+
: undefined,
|
|
1006
|
+
}),
|
|
799
1007
|
refresh,
|
|
800
1008
|
pathname: () => new URL(eventController.getLocation().href).pathname,
|
|
801
1009
|
params: () => eventController.getParams(),
|
|
@@ -803,7 +1011,33 @@ export async function renderRoute(
|
|
|
803
1011
|
eventController,
|
|
804
1012
|
};
|
|
805
1013
|
|
|
806
|
-
return Object.assign(result, { router });
|
|
1014
|
+
if (serverHtml === undefined) return Object.assign(result, { router });
|
|
1015
|
+
return Object.assign(result, { router, serverHtml, recoverableErrors });
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Runs the server pass without `window` and `document`, the two globals
|
|
1020
|
+
* SSR-aware code probes. A DOM test environment defines both, so with them in
|
|
1021
|
+
* place a `typeof window` branch takes its client side, renders the client
|
|
1022
|
+
* HTML, and the mismatch it causes in production hydrates clean. `fn` must be
|
|
1023
|
+
* synchronous (renderToString, not a streaming entry): nothing else may run
|
|
1024
|
+
* while they are gone.
|
|
1025
|
+
*/
|
|
1026
|
+
function withoutBrowserGlobals<T>(fn: () => T): T {
|
|
1027
|
+
const scope = globalThis as { window?: unknown; document?: unknown };
|
|
1028
|
+
const windowDescriptor = Object.getOwnPropertyDescriptor(scope, "window")!;
|
|
1029
|
+
const documentDescriptor = Object.getOwnPropertyDescriptor(
|
|
1030
|
+
scope,
|
|
1031
|
+
"document",
|
|
1032
|
+
)!;
|
|
1033
|
+
try {
|
|
1034
|
+
delete scope.window;
|
|
1035
|
+
delete scope.document;
|
|
1036
|
+
return fn();
|
|
1037
|
+
} finally {
|
|
1038
|
+
Object.defineProperty(scope, "window", windowDescriptor);
|
|
1039
|
+
Object.defineProperty(scope, "document", documentDescriptor);
|
|
1040
|
+
}
|
|
807
1041
|
}
|
|
808
1042
|
|
|
809
1043
|
function makeMetadata(
|
|
@@ -44,6 +44,8 @@ import {
|
|
|
44
44
|
SHELL_CAPTURE_MAX_WAIT_MS,
|
|
45
45
|
} from "../rsc/shell-capture-constants.js";
|
|
46
46
|
import { SEGMENT_FRAGMENT_CAPABILITY_HEADER } from "../segment-fragments.js";
|
|
47
|
+
import { resolveDeferredHandleValues } from "../handles/deferred-resolution.js";
|
|
48
|
+
import type { HandleData } from "../server/handle-store.js";
|
|
47
49
|
import { _getRequestContext } from "../server/request-context.js";
|
|
48
50
|
import {
|
|
49
51
|
parsePprReplayStatus,
|
|
@@ -77,6 +79,22 @@ export interface ServeShellRequestOptions<TEnv = any> {
|
|
|
77
79
|
partial?: true | { from?: string; segments?: readonly string[] };
|
|
78
80
|
}
|
|
79
81
|
|
|
82
|
+
/** A response's handle data, as the browser consumes it. */
|
|
83
|
+
export interface ShellRequestHandles {
|
|
84
|
+
/**
|
|
85
|
+
* What the document hydrates with: `metadata.handles`, read to its end
|
|
86
|
+
* before hydration starts. For a `partial` request, the last state the
|
|
87
|
+
* navigation streamed.
|
|
88
|
+
*/
|
|
89
|
+
hydration: HandleData;
|
|
90
|
+
/**
|
|
91
|
+
* The states `metadata.handlesLate` delivered, in order. Each one replaces
|
|
92
|
+
* the client's handle data after hydration. Empty when nothing arrived
|
|
93
|
+
* late, and for a `partial` request.
|
|
94
|
+
*/
|
|
95
|
+
late: HandleData[];
|
|
96
|
+
}
|
|
97
|
+
|
|
80
98
|
/** Result of {@link serveShellRequest}. */
|
|
81
99
|
export interface ServeShellRequestResult {
|
|
82
100
|
/** The handler's Response. Its body is already read: see `body`. */
|
|
@@ -113,6 +131,11 @@ export interface ServeShellRequestResult {
|
|
|
113
131
|
* (CFCacheStore, VercelCacheStore) it warms the memo like any other read.
|
|
114
132
|
*/
|
|
115
133
|
readEntry(): Promise<ShellCacheEntry | null>;
|
|
134
|
+
/**
|
|
135
|
+
* Decode the handle data of `flight` as the browser reads it, deferred
|
|
136
|
+
* values resolved. Undefined when no Flight was rendered.
|
|
137
|
+
*/
|
|
138
|
+
readHandles(): Promise<ShellRequestHandles | undefined>;
|
|
116
139
|
}
|
|
117
140
|
|
|
118
141
|
type ShellHandler = ReturnType<typeof createRSCHandler>;
|
|
@@ -203,8 +226,39 @@ const SSR_STUB: SSRModule = {
|
|
|
203
226
|
},
|
|
204
227
|
};
|
|
205
228
|
|
|
206
|
-
|
|
207
|
-
|
|
229
|
+
type HandleChannel = AsyncIterable<HandleData> | undefined;
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Read a payload's two handle channels as browser/rsc-router.tsx does:
|
|
233
|
+
* `handles` to its end, then every `handlesLate` state.
|
|
234
|
+
*/
|
|
235
|
+
async function decodePayloadHandles(
|
|
236
|
+
flight: string,
|
|
237
|
+
): Promise<ShellRequestHandles> {
|
|
238
|
+
const { deserializeResult } = await import("../cache/segment-codec.js");
|
|
239
|
+
const { metadata } = await deserializeResult<{
|
|
240
|
+
metadata?: { handles?: HandleChannel; handlesLate?: HandleChannel };
|
|
241
|
+
}>(flight);
|
|
242
|
+
let hydration: HandleData = {};
|
|
243
|
+
for await (const data of metadata?.handles ?? []) hydration = data;
|
|
244
|
+
const late: HandleData[] = [];
|
|
245
|
+
for await (const data of metadata?.handlesLate ?? []) {
|
|
246
|
+
late.push(await resolveDeferredHandleValues(data));
|
|
247
|
+
}
|
|
248
|
+
return { hydration: await resolveDeferredHandleValues(hydration), late };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Handlers per router, keyed by the `cacheStore` override or the router. A
|
|
253
|
+
* handler resolves its router's versions when it is created, so an entry is
|
|
254
|
+
* reused only while the router would still resolve the versions it was made
|
|
255
|
+
* with: a setBuildVersions() call (a simulated deploy) that changes them gets
|
|
256
|
+
* a new handler.
|
|
257
|
+
*/
|
|
258
|
+
const handlers = new WeakMap<
|
|
259
|
+
object,
|
|
260
|
+
WeakMap<object, { handler: ShellHandler; versions: string }>
|
|
261
|
+
>();
|
|
208
262
|
|
|
209
263
|
async function getHandler(
|
|
210
264
|
router: Rango<any, any>,
|
|
@@ -212,10 +266,21 @@ async function getHandler(
|
|
|
212
266
|
): Promise<ShellHandler> {
|
|
213
267
|
let byStore = handlers.get(router);
|
|
214
268
|
if (!byStore) handlers.set(router, (byStore = new WeakMap()));
|
|
269
|
+
const internal = toInternal(router);
|
|
270
|
+
// Imported here, like the handler below: the module binds the build-only
|
|
271
|
+
// `@rangojs/router:version` virtual, which a flight-only test config (one
|
|
272
|
+
// that never serves a request) does not alias.
|
|
273
|
+
const { resolveRouterVersions } =
|
|
274
|
+
await import("../server/build-version-table.js");
|
|
275
|
+
// What createRSCHandler would resolve for this router now.
|
|
276
|
+
const { data, document } = resolveRouterVersions(
|
|
277
|
+
internal.id,
|
|
278
|
+
internal.version,
|
|
279
|
+
);
|
|
280
|
+
const versions = `${data}\0${document}`;
|
|
215
281
|
const cached = byStore.get(cacheStore ?? router);
|
|
216
|
-
if (cached) return cached;
|
|
282
|
+
if (cached?.versions === versions) return cached.handler;
|
|
217
283
|
|
|
218
|
-
const internal = toInternal(router);
|
|
219
284
|
const routerCache = internal.cache;
|
|
220
285
|
// Resolved per request, as the handler resolves the router's own config.
|
|
221
286
|
const cache = (env: unknown, ctx?: ExecutionContext): HandlerCacheConfig => {
|
|
@@ -230,11 +295,10 @@ async function getHandler(
|
|
|
230
295
|
const handler = createRSCHandler({
|
|
231
296
|
router: internal,
|
|
232
297
|
nonce: internal.nonce,
|
|
233
|
-
version: internal.version,
|
|
234
298
|
loadSSRModule: async () => SSR_STUB,
|
|
235
299
|
cache: routerCache || cacheStore ? cache : undefined,
|
|
236
300
|
});
|
|
237
|
-
byStore.set(cacheStore ?? router, handler);
|
|
301
|
+
byStore.set(cacheStore ?? router, { handler, versions });
|
|
238
302
|
return handler;
|
|
239
303
|
}
|
|
240
304
|
|
|
@@ -358,16 +422,18 @@ export async function serveShellRequest<TEnv = any>(
|
|
|
358
422
|
const isFlight =
|
|
359
423
|
recorder.rendered ||
|
|
360
424
|
response.headers.get("content-type")?.includes("text/x-component");
|
|
425
|
+
const prelude =
|
|
426
|
+
tail !== undefined && body.endsWith(tail)
|
|
427
|
+
? body.slice(0, body.length - tail.length)
|
|
428
|
+
: undefined;
|
|
429
|
+
const flight = tail ?? (isFlight && body ? body : undefined);
|
|
361
430
|
return {
|
|
362
431
|
response,
|
|
363
432
|
body,
|
|
364
433
|
shellStatus: parseShellStatus(response),
|
|
365
434
|
replayStatus: parsePprReplayStatus(response),
|
|
366
|
-
prelude
|
|
367
|
-
|
|
368
|
-
? body.slice(0, body.length - tail.length)
|
|
369
|
-
: undefined,
|
|
370
|
-
flight: tail ?? (isFlight && body ? body : undefined),
|
|
435
|
+
prelude,
|
|
436
|
+
flight,
|
|
371
437
|
key,
|
|
372
438
|
async readEntry() {
|
|
373
439
|
// Passive, like the serve path's own reads: a stale entry's SWR
|
|
@@ -377,5 +443,8 @@ export async function serveShellRequest<TEnv = any>(
|
|
|
377
443
|
});
|
|
378
444
|
return read?.entry ?? null;
|
|
379
445
|
},
|
|
446
|
+
async readHandles() {
|
|
447
|
+
return flight ? decodePayloadHandles(flight) : undefined;
|
|
448
|
+
},
|
|
380
449
|
};
|
|
381
450
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// Stub for the build-only `@rangojs/router:version` virtual module, shipped so
|
|
2
2
|
// consumers do not have to author it. The rango Vite plugin injects this at
|
|
3
3
|
// build time; in a bare Vitest process it must be aliased to a stub. Empty
|
|
4
|
-
// string keeps generated URLs free of a version path segment.
|
|
4
|
+
// string keeps generated URLs free of a version path segment. There is no
|
|
5
|
+
// build table here; setBuildVersions (@rangojs/router/testing) installs one
|
|
6
|
+
// for a test that needs per-router versions.
|
|
5
7
|
export const VERSION = "";
|
|
8
|
+
export const ROUTER_VERSIONS: undefined = undefined;
|
|
@@ -462,11 +462,14 @@ export type InternalHandlerContext<
|
|
|
462
462
|
*/
|
|
463
463
|
_loaderCacheOriginalUse?: (item: any) => any;
|
|
464
464
|
/**
|
|
465
|
-
* @internal A loader-cache HIT asks before replaying a
|
|
466
|
-
* pushes: true when that loader has not run in this
|
|
467
|
-
* replay delivered it; its later run then replaces
|
|
468
|
-
* (HandleStore.pushReplayed). One copy per loader per
|
|
469
|
-
*
|
|
465
|
+
* @internal A loader-cache or "use cache" HIT asks before replaying a
|
|
466
|
+
* loader's recorded pushes: true when that loader has not run in this
|
|
467
|
+
* request and no other replay delivered it; its later run then replaces
|
|
468
|
+
* the replayed values (HandleStore.pushReplayed). One copy per loader per
|
|
469
|
+
* request. A record's restore does not ask (restoreHandles): its copies
|
|
470
|
+
* are the pinned loader's own, or placeholders the claimed replay
|
|
471
|
+
* replaces. Absent on a stale refresh's ctx, whose pushes are diverted
|
|
472
|
+
* (handle-snapshot.ts appendHandles). See setupLoaderAccess.
|
|
470
473
|
*/
|
|
471
474
|
_claimLoaderPushes?: (loaderId: string) => boolean;
|
|
472
475
|
/**
|