@rangojs/router 0.32.0 → 0.33.1
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 +3 -2
- package/dist/bin/browser-7KIJ5HSV.js +15 -0
- package/dist/bin/chunk-3B7CKBRO.js +463 -0
- package/dist/bin/chunk-524VACCG.js +71 -0
- package/dist/bin/chunk-LD7UBHT4.js +196 -0
- package/dist/bin/chunk-TO6CRWLN.js +1429 -0
- package/dist/bin/chunk-TTXC2U3G.js +2477 -0
- package/dist/bin/client-urls-projection-DQLWPDAT.js +28 -0
- package/dist/bin/forward-user-plugins-URBZKJP4.js +62 -0
- package/dist/bin/rango.js +42 -10173
- package/dist/bin/runtime-EPUDEHO4.js +596 -0
- package/dist/bin/runtime-discovery-3HH6BP3K.js +352 -0
- package/dist/bin/state-ACD2MGKJ.js +16 -0
- package/dist/bin/temp-rsc-server-JALTIWLT.js +3190 -0
- package/dist/types/bin/rango.d.ts +1 -0
- package/dist/types/browser/action-fence.d.ts +20 -0
- package/dist/types/browser/cookie-name.d.ts +51 -10
- package/dist/types/browser/invalidate-client-cache.d.ts +8 -6
- package/dist/types/browser/navigation-bridge.d.ts +36 -1
- package/dist/types/browser/navigation-client.d.ts +19 -0
- package/dist/types/browser/navigation-store-handle.d.ts +37 -4
- package/dist/types/browser/navigation-store.d.ts +2 -2
- package/dist/types/browser/navigation-transaction.d.ts +0 -1
- package/dist/types/browser/partial-update.d.ts +13 -0
- package/dist/types/browser/prefetch/cache.d.ts +96 -8
- package/dist/types/browser/prefetch/fetch.d.ts +26 -2
- package/dist/types/browser/prefetch/queue.d.ts +3 -2
- package/dist/types/browser/rango-state.d.ts +34 -5
- package/dist/types/browser/react/Link.d.ts +15 -6
- package/dist/types/browser/types.d.ts +25 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/include-resolution.d.ts +9 -1
- package/dist/types/build/route-types/router-processing.d.ts +3 -1
- package/dist/types/cache/cache-key-utils.d.ts +12 -8
- package/dist/types/cache/cache-scope.d.ts +4 -2
- package/dist/types/cache/cf/cf-cache-types.d.ts +9 -5
- package/dist/types/cache/document-cache-policy.d.ts +9 -1
- package/dist/types/cache/memory-segment-store.d.ts +3 -2
- package/dist/types/cache/segment-codec.d.ts +2 -2
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/types.d.ts +12 -7
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +2 -1
- package/dist/types/client-urls/types.d.ts +7 -5
- package/dist/types/prefetch-markers.d.ts +9 -0
- package/dist/types/prefetch-source-scope.d.ts +66 -0
- package/dist/types/route-definition/helpers-types.d.ts +8 -5
- package/dist/types/router/intercept-resolution.d.ts +25 -11
- package/dist/types/router/match-context.d.ts +1 -0
- package/dist/types/router/match-handlers.d.ts +1 -1
- package/dist/types/router/prefetch-cache-ttl.d.ts +11 -3
- package/dist/types/router/router-context.d.ts +1 -1
- package/dist/types/router/router-options.d.ts +6 -3
- package/dist/types/router/segment-resolution/loader-cache.d.ts +8 -0
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +23 -7
- package/dist/types/router/segment-resolution/revalidation.d.ts +19 -0
- package/dist/types/router/source-reads.d.ts +53 -0
- package/dist/types/router/types.d.ts +1 -1
- package/dist/types/rsc/helpers.d.ts +5 -2
- package/dist/types/rsc/prefetch-render-error.d.ts +7 -2
- package/dist/types/rsc/request-stages.d.ts +2 -0
- package/dist/types/rsc/shell-serve.d.ts +6 -1
- package/dist/types/server/context.d.ts +23 -6
- package/dist/types/server/cookie-store.d.ts +15 -1
- package/dist/types/server/document-location-state.d.ts +5 -2
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +43 -7
- package/dist/types/testing/e2e/server.d.ts +16 -1
- package/dist/types/testing/render-route.d.ts +66 -2
- package/dist/types/testing/serve-shell-request.d.ts +6 -3
- package/dist/types/types/handler-context.d.ts +7 -5
- package/dist/types/types/loader-types.d.ts +7 -4
- package/dist/types/types/segments.d.ts +15 -4
- package/dist/types/urls/path-helper-types.d.ts +7 -5
- package/dist/types/vite/temp-rsc-server.d.ts +2 -2
- package/dist/vite/index.js +449 -189
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +81 -0
- package/skills/app-structure/e-commerce.md +10 -9
- package/skills/app-structure/large-app.md +18 -2
- package/skills/caching/SKILL.md +10 -5
- package/skills/client-urls/SKILL.md +29 -11
- package/skills/comparison/references/framework-comparison.md +13 -8
- package/skills/document-cache/SKILL.md +8 -0
- package/skills/hooks/navigation.md +8 -8
- package/skills/hooks/outlets.md +4 -0
- package/skills/hooks/state.md +24 -8
- package/skills/intercept/SKILL.md +33 -12
- package/skills/layout/SKILL.md +60 -0
- package/skills/links/SKILL.md +101 -4
- package/skills/loader/SKILL.md +49 -22
- package/skills/migrate-nextjs/SKILL.md +8 -2
- package/skills/migrate-react-router/data-and-actions.md +5 -0
- package/skills/ppr/SKILL.md +26 -5
- package/skills/prerender/SKILL.md +4 -1
- package/skills/route/SKILL.md +6 -6
- package/skills/router-setup/SKILL.md +2 -1
- package/skills/server-actions/SKILL.md +41 -8
- package/skills/testing/client-components.md +29 -13
- package/skills/testing/serve-shell-request.md +7 -7
- package/skills/testing/server-actions.md +8 -0
- package/skills/ux-architecture/SKILL.md +70 -19
- package/src/bin/rango.ts +39 -13
- package/src/browser/action-fence.ts +72 -0
- package/src/browser/cookie-name.ts +82 -14
- package/src/browser/invalidate-client-cache.ts +9 -7
- package/src/browser/navigation-bridge.ts +164 -32
- package/src/browser/navigation-client.ts +95 -20
- package/src/browser/navigation-store-handle.ts +67 -5
- package/src/browser/navigation-store.ts +86 -17
- package/src/browser/navigation-transaction.ts +0 -6
- package/src/browser/partial-update.ts +31 -12
- package/src/browser/prefetch/cache.ts +188 -11
- package/src/browser/prefetch/fetch.ts +211 -51
- package/src/browser/prefetch/queue.ts +3 -2
- package/src/browser/rango-state.ts +50 -8
- package/src/browser/react/Link.tsx +20 -9
- package/src/browser/react/use-navigation.ts +1 -1
- package/src/browser/rsc-router.tsx +8 -1
- package/src/browser/server-action-bridge.ts +16 -11
- package/src/browser/types.ts +25 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/include-resolution.ts +40 -3
- package/src/build/route-types/router-processing.ts +170 -25
- package/src/build/route-types/scan-filter.ts +1 -0
- package/src/cache/cache-key-utils.ts +12 -8
- package/src/cache/cache-runtime.ts +5 -1
- package/src/cache/cache-scope.ts +28 -7
- package/src/cache/cf/cf-cache-types.ts +9 -5
- package/src/cache/document-cache-policy.ts +27 -0
- package/src/cache/document-cache.ts +5 -0
- package/src/cache/memory-segment-store.ts +3 -2
- package/src/cache/segment-codec.ts +2 -2
- package/src/cache/shell-snapshot.ts +1 -1
- package/src/cache/types.ts +12 -7
- package/src/cache/vercel/vercel-cache-store.ts +2 -1
- package/src/client-urls/types.ts +7 -5
- package/src/prefetch-markers.ts +10 -0
- package/src/prefetch-source-scope.ts +137 -0
- package/src/route-definition/helpers-types.ts +8 -5
- package/src/router/intercept-resolution.ts +94 -50
- package/src/router/loader-resolution.ts +8 -6
- package/src/router/match-api.ts +33 -29
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +2 -1
- package/src/router/match-middleware/cache-lookup.ts +21 -13
- package/src/router/match-result.ts +39 -87
- package/src/router/navigation-snapshot.ts +18 -3
- package/src/router/prefetch-cache-ttl.ts +15 -3
- package/src/router/revalidation.ts +53 -23
- package/src/router/router-context.ts +2 -1
- package/src/router/router-options.ts +6 -3
- package/src/router/segment-resolution/loader-cache.ts +21 -3
- package/src/router/segment-resolution/prefetch-deferral.ts +38 -31
- package/src/router/segment-resolution/revalidation.ts +40 -12
- package/src/router/source-reads.ts +122 -0
- package/src/router/types.ts +2 -1
- package/src/router.ts +11 -12
- package/src/rsc/helpers.ts +8 -3
- package/src/rsc/prefetch-render-error.ts +7 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/request-stages.ts +59 -0
- package/src/rsc/rsc-rendering.ts +49 -33
- package/src/rsc/server-action.ts +2 -1
- package/src/rsc/shell-capture.ts +4 -5
- package/src/rsc/shell-serve.ts +12 -1
- package/src/segment-system.tsx +1 -1
- package/src/server/context.ts +37 -6
- package/src/server/cookie-store.ts +22 -1
- package/src/server/document-location-state.ts +23 -2
- package/src/server/handle-store.ts +2 -0
- package/src/server/request-context.ts +113 -28
- package/src/testing/dispatch.ts +3 -0
- package/src/testing/e2e/fixture.ts +4 -12
- package/src/testing/e2e/server.ts +91 -4
- package/src/testing/render-route.tsx +269 -4
- package/src/testing/serve-shell-request.ts +6 -3
- package/src/types/handler-context.ts +7 -5
- package/src/types/loader-types.ts +7 -4
- package/src/types/segments.ts +15 -4
- package/src/urls/path-helper-types.ts +7 -5
- package/src/vite/temp-rsc-server.ts +2 -2
|
@@ -35,6 +35,16 @@
|
|
|
35
35
|
* not modeled). navigate(url, { streamUntil }) gives the navigation a
|
|
36
36
|
* response stream that stays open past the commit (isStreaming).
|
|
37
37
|
* useAction().state stays idle: no action runs.
|
|
38
|
+
* - prefetch(url) keeps a page as a prefetch made now carries it, and
|
|
39
|
+
* action() lands a server action's response: the page on screen
|
|
40
|
+
* re-renders with its post-write loader data, and, without
|
|
41
|
+
* `keepClientCache`, production's store marks every history entry and
|
|
42
|
+
* prefetch stale (browser/navigation-store.ts). A navigate(url) to a
|
|
43
|
+
* prefetched page commits the prefetch's data; when an action made it
|
|
44
|
+
* stale, the page then revalidates once the navigation settled, in the
|
|
45
|
+
* lane production's Back uses (useNavigation() idle), with the loader
|
|
46
|
+
* data current then. The prefetch request, its decode and the HTTP cache
|
|
47
|
+
* are e2e territory.
|
|
38
48
|
* - A spec's transition({ when }) decides every commit with production's
|
|
39
49
|
* browser decision: navigate() and useRouter().push model kind "push",
|
|
40
50
|
* useRouter().replace kind "replace", refresh() kind "revalidate", and a
|
|
@@ -49,8 +59,8 @@
|
|
|
49
59
|
* written there, and another page is on screen, its page is restored
|
|
50
60
|
* from the history cache, with the handle values it had, as production's
|
|
51
61
|
* restore is; otherwise the tree keeps its location. A stale entry's
|
|
52
|
-
* revalidation, a restore that fetches, a server action's
|
|
53
|
-
* redirect's and an intercept's are e2e territory.
|
|
62
|
+
* revalidation on Back, a restore that fetches, a server action's
|
|
63
|
+
* location state, a redirect's and an intercept's are e2e territory.
|
|
54
64
|
* What it DOES cover: client hooks that read NavigationProvider /
|
|
55
65
|
* OutletContext — useParams, useReverse, useHref, useMount, useNavigation,
|
|
56
66
|
* useRouter, usePathname, useSearchParams, Outlet/useOutlet nesting and seeded
|
|
@@ -126,10 +136,17 @@ import type { Handle } from "../handle.js";
|
|
|
126
136
|
import type { ThemeConfig } from "../theme/types.js";
|
|
127
137
|
import { resolveThemeConfig } from "../theme/constants.js";
|
|
128
138
|
import { isUnderTestRunner } from "../runtime-env.js";
|
|
129
|
-
import {
|
|
139
|
+
import {
|
|
140
|
+
scheduleStalePrefetchRevalidation,
|
|
141
|
+
setupNavigationBridgeDelegatedPrefetch,
|
|
142
|
+
} from "../browser/navigation-bridge.js";
|
|
130
143
|
import { resetPrefetchObserverForTesting } from "../browser/prefetch/observer.js";
|
|
131
144
|
import type { PrefetchStrategy } from "../router/prefetch-default.js";
|
|
132
145
|
import { auditTreeCause, forgetSuspenseAudit } from "../suspense-audit.js";
|
|
146
|
+
import {
|
|
147
|
+
currentPrefetchEpoch,
|
|
148
|
+
isPrefetchStale,
|
|
149
|
+
} from "../browser/prefetch/cache.js";
|
|
133
150
|
|
|
134
151
|
const TEST_ORIGIN = "http://localhost";
|
|
135
152
|
let activePrefetchRegistrations = 0;
|
|
@@ -517,6 +534,59 @@ export interface TestRouterHandle {
|
|
|
517
534
|
* transition({ when }) decides it with kind "revalidate" (`to` is `from`).
|
|
518
535
|
*/
|
|
519
536
|
refresh(): Promise<void>;
|
|
537
|
+
/**
|
|
538
|
+
* Prefetch `url`, as a `<Link>` hover or viewport trigger does: the page is
|
|
539
|
+
* kept with the loader data its response would carry now (the render-time
|
|
540
|
+
* seeds, `options.loaders` merged over). A later `navigate(url)` without
|
|
541
|
+
* `loaders` commits that data, whatever the seeds are by then.
|
|
542
|
+
*
|
|
543
|
+
* The prefetch is stamped with production's stale epoch
|
|
544
|
+
* (browser/prefetch/cache.ts): an `action()` without `keepClientCache`
|
|
545
|
+
* makes it stale. A click on a stale prefetch commits it at once, then the
|
|
546
|
+
* page revalidates once the navigation settled (its `streamUntil` ended),
|
|
547
|
+
* in Back's stale-revalidation lane: `useNavigation().state` stays "idle"
|
|
548
|
+
* and `isNavigating` reads true while its request is open, `useLinkStatus`
|
|
549
|
+
* shows no pending state, and the commit runs through commitInTransition
|
|
550
|
+
* with kind "revalidate", reading the loader data current then. A newer
|
|
551
|
+
* navigation aborts it.
|
|
552
|
+
*
|
|
553
|
+
* @example
|
|
554
|
+
* router.prefetch("/cart"); // carries count 2
|
|
555
|
+
* await router.action({ loaders: [[BasketLoader, { count: 3 }]] });
|
|
556
|
+
* await router.navigate("/cart"); // count 2, at once
|
|
557
|
+
* await waitFor(() => expect(count()).toBe("3")); // revalidated
|
|
558
|
+
*/
|
|
559
|
+
prefetch(url: string, options?: Pick<RenderRouteOptions, "loaders">): void;
|
|
560
|
+
/**
|
|
561
|
+
* Land a server action's response on the page on screen. `loaders` are the
|
|
562
|
+
* loader values after the action's write: they become the data every
|
|
563
|
+
* later response carries, and the page on screen re-renders with them in
|
|
564
|
+
* the action lane (commitInTransition, kind "action", unless
|
|
565
|
+
* transition({ when }) gates it off). Without `keepClientCache`,
|
|
566
|
+
* production's store then marks every history entry and every prefetch
|
|
567
|
+
* stale (`markCacheAsStaleAndBroadcast()`), as the action bridge does:
|
|
568
|
+
* nothing is flushed and the state cookie is not rotated.
|
|
569
|
+
* `keepClientCache: true` is the `keepClientCache()` directive: caches stay
|
|
570
|
+
* fresh. The action's own lifecycle (`useAction()`), its follow-up and its
|
|
571
|
+
* redirect are e2e territory.
|
|
572
|
+
*/
|
|
573
|
+
action(
|
|
574
|
+
options?: Pick<RenderRouteOptions, "loaders"> & {
|
|
575
|
+
keepClientCache?: boolean;
|
|
576
|
+
},
|
|
577
|
+
): Promise<void>;
|
|
578
|
+
/**
|
|
579
|
+
* The background revalidations a click on a stale prefetch started, in
|
|
580
|
+
* order: the page's pathname and search, and the segment ids the request
|
|
581
|
+
* holds, as production's bridge plans them
|
|
582
|
+
* (navigation-bridge.ts scheduleStalePrefetchRevalidation). A click on a
|
|
583
|
+
* fresh prefetch, or after a `keepClientCache` action, starts none.
|
|
584
|
+
*
|
|
585
|
+
* @example
|
|
586
|
+
* await router.navigate("/cart"); // a stale prefetch
|
|
587
|
+
* await waitFor(() => expect(router.revalidations()).toHaveLength(1));
|
|
588
|
+
*/
|
|
589
|
+
revalidations(): ReadonlyArray<{ url: string; segmentIds: string[] }>;
|
|
520
590
|
/** The current committed pathname. */
|
|
521
591
|
pathname(): string;
|
|
522
592
|
/** The current committed params. */
|
|
@@ -891,6 +961,8 @@ export async function renderRoute(
|
|
|
891
961
|
}
|
|
892
962
|
|
|
893
963
|
type NavigateCall = Pick<RenderRouteOptions, "loaders" | "handles"> & {
|
|
964
|
+
/** The loader data the response carries: a prefetch's (prefetch()). */
|
|
965
|
+
seeds?: Record<string, unknown>;
|
|
894
966
|
history?: Pick<NavigateOptionsInternal, "state" | "replace" | "transition">;
|
|
895
967
|
kind?: TransitionWhenKind;
|
|
896
968
|
transition?: boolean;
|
|
@@ -931,6 +1003,30 @@ export async function renderRoute(
|
|
|
931
1003
|
const written = entries.get(entryIndex());
|
|
932
1004
|
if (written) written.state = window.history.state;
|
|
933
1005
|
});
|
|
1006
|
+
/**
|
|
1007
|
+
* The history-cache reads production's stale-prefetch guards make
|
|
1008
|
+
* (navigation-bridge.ts scheduleStalePrefetchRevalidation), over this
|
|
1009
|
+
* harness's history: the store's own history key never moves here, so the
|
|
1010
|
+
* key on screen is its location's, and the one cached page is the entry on
|
|
1011
|
+
* screen. Marking it stale writes nothing: a stale entry's revalidation on
|
|
1012
|
+
* Back is not modeled.
|
|
1013
|
+
*/
|
|
1014
|
+
const historyView: Pick<
|
|
1015
|
+
NavigationStore,
|
|
1016
|
+
"getCachedSegments" | "getHistoryKey" | "updateCacheHandleData"
|
|
1017
|
+
> = {
|
|
1018
|
+
getHistoryKey: () => generateHistoryKey(eventController.getLocation().href),
|
|
1019
|
+
getCachedSegments: (key) => {
|
|
1020
|
+
const page = entries.get(entryOnScreen);
|
|
1021
|
+
if (!page || generateHistoryKey(page.url.href) !== key) return undefined;
|
|
1022
|
+
return {
|
|
1023
|
+
segments: page.segments,
|
|
1024
|
+
stale: false,
|
|
1025
|
+
handleData: page.handleData,
|
|
1026
|
+
};
|
|
1027
|
+
},
|
|
1028
|
+
updateCacheHandleData: () => {},
|
|
1029
|
+
};
|
|
934
1030
|
/** The page being left keeps its pushes for a back/forward onto it. */
|
|
935
1031
|
const leaveEntry = (): void => {
|
|
936
1032
|
const left = entries.get(entryOnScreen);
|
|
@@ -945,9 +1041,38 @@ export async function renderRoute(
|
|
|
945
1041
|
target: string,
|
|
946
1042
|
navOptions?: NavigateCall,
|
|
947
1043
|
): Promise<void> => {
|
|
1044
|
+
// A click on a prefetched page commits the prefetch's data (prefetch());
|
|
1045
|
+
// refresh() and a navigation that seeds its own loaders fetch.
|
|
1046
|
+
const prefetchedPage =
|
|
1047
|
+
navOptions?.kind === undefined && navOptions?.loaders === undefined
|
|
1048
|
+
? prefetchedPages.get(prefetchKeyOf(target))
|
|
1049
|
+
: undefined;
|
|
1050
|
+
if (prefetchedPage) {
|
|
1051
|
+
navOptions = { ...navOptions, seeds: prefetchedPage.loaderData };
|
|
1052
|
+
// A stale entry is served once (cache.ts consumePrefetch): the next
|
|
1053
|
+
// click on the link fetches fresh.
|
|
1054
|
+
if (isPrefetchStale(prefetchedPage)) {
|
|
1055
|
+
prefetchedPages.delete(prefetchKeyOf(target));
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
948
1058
|
const handle = eventController.startNavigation(
|
|
949
1059
|
new URL(target, TEST_ORIGIN).href,
|
|
950
1060
|
);
|
|
1061
|
+
// An action made the prefetch stale: once this navigation settled (its
|
|
1062
|
+
// commit and its stream), the page revalidates. Production's own guards
|
|
1063
|
+
// and plan decide it (navigation-bridge.ts
|
|
1064
|
+
// scheduleStalePrefetchRevalidation), over the history this harness
|
|
1065
|
+
// keeps (historyView).
|
|
1066
|
+
const entry = eventController.getUnsettledNavigation();
|
|
1067
|
+
if (prefetchedPage && isPrefetchStale(prefetchedPage) && entry) {
|
|
1068
|
+
scheduleStalePrefetchRevalidation(
|
|
1069
|
+
historyView,
|
|
1070
|
+
eventController,
|
|
1071
|
+
entry,
|
|
1072
|
+
generateHistoryKey(new URL(target, TEST_ORIGIN).href),
|
|
1073
|
+
({ segmentIds }) => void revalidateStalePrefetch(target, segmentIds),
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
951
1076
|
// Runs in the caller's task, so a <Link> click's own act() takes it.
|
|
952
1077
|
// Awaited: React retries a held page with this update, and an unawaited
|
|
953
1078
|
// act() drops that suspended retry for good.
|
|
@@ -1009,7 +1134,7 @@ export async function renderRoute(
|
|
|
1009
1134
|
const segments = buildSegments(
|
|
1010
1135
|
routes,
|
|
1011
1136
|
match.params,
|
|
1012
|
-
seedLoaders(loaderData, navOptions?.loaders),
|
|
1137
|
+
seedLoaders(navOptions?.seeds ?? loaderData, navOptions?.loaders),
|
|
1013
1138
|
mount,
|
|
1014
1139
|
);
|
|
1015
1140
|
const metadata = makeMetadata(nextUrl.pathname, segments, match.params);
|
|
@@ -1109,6 +1234,143 @@ export async function renderRoute(
|
|
|
1109
1234
|
return navigate(current.pathname + current.search, { kind: "revalidate" });
|
|
1110
1235
|
};
|
|
1111
1236
|
|
|
1237
|
+
// prefetch(): the pages prefetched so far, by pathname and search, each with
|
|
1238
|
+
// the loader data its response carried and the stale epoch it began under.
|
|
1239
|
+
interface PrefetchedPage {
|
|
1240
|
+
loaderData: Record<string, unknown>;
|
|
1241
|
+
epoch: number;
|
|
1242
|
+
}
|
|
1243
|
+
const prefetchedPages = new Map<string, PrefetchedPage>();
|
|
1244
|
+
const prefetchKeyOf = (target: string): string => {
|
|
1245
|
+
const parsed = new URL(target, TEST_ORIGIN);
|
|
1246
|
+
return parsed.pathname + parsed.search;
|
|
1247
|
+
};
|
|
1248
|
+
const prefetch = (
|
|
1249
|
+
target: string,
|
|
1250
|
+
prefetchOptions?: Pick<RenderRouteOptions, "loaders">,
|
|
1251
|
+
): void => {
|
|
1252
|
+
prefetchedPages.set(prefetchKeyOf(target), {
|
|
1253
|
+
loaderData: seedLoaders(loaderData, prefetchOptions?.loaders),
|
|
1254
|
+
epoch: currentPrefetchEpoch(),
|
|
1255
|
+
});
|
|
1256
|
+
};
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* Re-render the page at `url` with the loader data current now, decided by
|
|
1260
|
+
* transition({ when }) as `kind`. `commit()` hands it over as production's
|
|
1261
|
+
* action and stale-revalidation commits do (browser/partial-update.ts):
|
|
1262
|
+
* through commitInTransition unless gated off.
|
|
1263
|
+
*/
|
|
1264
|
+
const renderPageAgain = async (
|
|
1265
|
+
url: URL,
|
|
1266
|
+
kind: "action" | "revalidate",
|
|
1267
|
+
renderOptions: { forceAwait?: boolean; isAction?: boolean },
|
|
1268
|
+
): Promise<{ commit(): Promise<void> }> => {
|
|
1269
|
+
const match = resolve(url.pathname);
|
|
1270
|
+
const segments = buildSegments(routes, match.params, loaderData, mount);
|
|
1271
|
+
const metadata = makeMetadata(url.pathname, segments, match.params);
|
|
1272
|
+
const gatedOff = decideTransitionGatedOff(segments, () => ({
|
|
1273
|
+
kind,
|
|
1274
|
+
from: {
|
|
1275
|
+
url: eventController.getLocation().href,
|
|
1276
|
+
params: eventController.getParams(),
|
|
1277
|
+
routeName: leaf.name,
|
|
1278
|
+
state: window.history.state,
|
|
1279
|
+
},
|
|
1280
|
+
to: {
|
|
1281
|
+
url,
|
|
1282
|
+
params: match.params,
|
|
1283
|
+
routeName: leaf.name,
|
|
1284
|
+
state: window.history.state,
|
|
1285
|
+
},
|
|
1286
|
+
}));
|
|
1287
|
+
const root = await renderSegments(segments, {
|
|
1288
|
+
...renderOptions,
|
|
1289
|
+
outletPending: options.outletPending,
|
|
1290
|
+
transitionGatedOff: gatedOff,
|
|
1291
|
+
});
|
|
1292
|
+
return {
|
|
1293
|
+
commit: async () => {
|
|
1294
|
+
store.setSegmentIds(segments.map((s) => s.id));
|
|
1295
|
+
const shown = entries.get(entryOnScreen);
|
|
1296
|
+
if (shown) shown.segments = segments;
|
|
1297
|
+
const emit: UpdateSubscriber = (update) => store.emitUpdate(update);
|
|
1298
|
+
await act(async () => {
|
|
1299
|
+
auditTreeCause(kind === "action" ? "action" : "stale-revalidation");
|
|
1300
|
+
if (gatedOff) emit({ root, metadata });
|
|
1301
|
+
else {
|
|
1302
|
+
commitInTransition(
|
|
1303
|
+
store.loaders,
|
|
1304
|
+
emit,
|
|
1305
|
+
segments,
|
|
1306
|
+
{ root, metadata },
|
|
1307
|
+
kind,
|
|
1308
|
+
shouldStartViewTransition(segments, gatedOff) ? ["action"] : [],
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
});
|
|
1312
|
+
},
|
|
1313
|
+
};
|
|
1314
|
+
};
|
|
1315
|
+
|
|
1316
|
+
/**
|
|
1317
|
+
* Back's stale-revalidation lane (navigation-bridge.ts revalidateInBackground),
|
|
1318
|
+
* after a click on a stale prefetch: a background navigation
|
|
1319
|
+
* (skipLoadingState) whose response arrives a task later, rendered with
|
|
1320
|
+
* the loader data current then, kind "revalidate". `segmentIds` are the
|
|
1321
|
+
* ids production's plan holds; the request is recorded (revalidations()).
|
|
1322
|
+
*/
|
|
1323
|
+
const revalidations: Array<{ url: string; segmentIds: string[] }> = [];
|
|
1324
|
+
const revalidateStalePrefetch = async (
|
|
1325
|
+
target: string,
|
|
1326
|
+
segmentIds: string[],
|
|
1327
|
+
): Promise<void> => {
|
|
1328
|
+
const nextUrl = new URL(target, TEST_ORIGIN);
|
|
1329
|
+
revalidations.push({
|
|
1330
|
+
url: nextUrl.pathname + nextUrl.search,
|
|
1331
|
+
segmentIds,
|
|
1332
|
+
});
|
|
1333
|
+
const handle = eventController.startNavigation(nextUrl.href, {
|
|
1334
|
+
skipLoadingState: true,
|
|
1335
|
+
});
|
|
1336
|
+
try {
|
|
1337
|
+
await act(() => eventController.flushRouteState());
|
|
1338
|
+
// Outside act(): a navigate() the test starts meanwhile runs its own.
|
|
1339
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1340
|
+
if (handle.signal.aborted) return;
|
|
1341
|
+
const page = await renderPageAgain(nextUrl, "revalidate", {
|
|
1342
|
+
forceAwait: true,
|
|
1343
|
+
});
|
|
1344
|
+
if (handle.signal.aborted) return;
|
|
1345
|
+
handle.complete(nextUrl);
|
|
1346
|
+
await page.commit();
|
|
1347
|
+
} finally {
|
|
1348
|
+
await act(() => {
|
|
1349
|
+
handle[Symbol.dispose]();
|
|
1350
|
+
eventController.flushRouteState();
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
};
|
|
1354
|
+
|
|
1355
|
+
// action(): the response of a server action, landing on the page on screen.
|
|
1356
|
+
const action = async (
|
|
1357
|
+
actionOptions?: Pick<RenderRouteOptions, "loaders"> & {
|
|
1358
|
+
keepClientCache?: boolean;
|
|
1359
|
+
},
|
|
1360
|
+
): Promise<void> => {
|
|
1361
|
+
for (const [loader, data] of actionOptions?.loaders ?? []) {
|
|
1362
|
+
loaderData[ensureSyntheticId(loader)] = data;
|
|
1363
|
+
}
|
|
1364
|
+
const page = await renderPageAgain(
|
|
1365
|
+
new URL(eventController.getLocation().href),
|
|
1366
|
+
"action",
|
|
1367
|
+
{ isAction: true },
|
|
1368
|
+
);
|
|
1369
|
+
// The action bridge's landing (server-action-bridge.ts finalizeAction).
|
|
1370
|
+
if (!actionOptions?.keepClientCache) store.markCacheAsStaleAndBroadcast();
|
|
1371
|
+
await page.commit();
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1112
1374
|
// A back/forward onto the mounted entry or one a navigation wrote, whose
|
|
1113
1375
|
// page is not the one on screen: its page is restored from the entry, as production's
|
|
1114
1376
|
// history-cache restore does (navigation-bridge.ts handlePopstate). Not
|
|
@@ -1323,6 +1585,9 @@ export async function renderRoute(
|
|
|
1323
1585
|
: undefined,
|
|
1324
1586
|
}),
|
|
1325
1587
|
refresh,
|
|
1588
|
+
prefetch,
|
|
1589
|
+
action,
|
|
1590
|
+
revalidations: () => [...revalidations],
|
|
1326
1591
|
pathname: () => new URL(eventController.getLocation().href).pathname,
|
|
1327
1592
|
params: () => eventController.getParams(),
|
|
1328
1593
|
store,
|
|
@@ -99,9 +99,12 @@ export interface ServeShellRequestOptions<TEnv = any> {
|
|
|
99
99
|
* (default none), advertising fragment passthrough. The replay decision is
|
|
100
100
|
* `result.replayStatus`.
|
|
101
101
|
*
|
|
102
|
-
* `prefetch: true` sends
|
|
103
|
-
* `X-Rango-Prefetch`
|
|
104
|
-
* entries are deferred: `result.readDeferred()`
|
|
102
|
+
* `prefetch: true` sends the speculative kind a viewport or render
|
|
103
|
+
* `<Link>` prefetch sends (`X-Rango-Prefetch: 1`), so `prefetch: false`
|
|
104
|
+
* loaders and `loading()` entries are deferred: `result.readDeferred()`
|
|
105
|
+
* lists them. A hover's or `prefetch()`'s intent prefetch defers nothing;
|
|
106
|
+
* serve it with `prefetch: true` and `headers: { "X-Rango-Prefetch":
|
|
107
|
+
* "intent" }`, which `prefetch: true` leaves as given. `fill: true`
|
|
105
108
|
* sends the request the browser follows an adopted prefetch with
|
|
106
109
|
* (`_rsc_fill`): pass the ids the prefetch delivered, without the deferred
|
|
107
110
|
* ones, as `segments`, and the same URL as `from`. `interceptSource` sends
|
|
@@ -520,11 +520,13 @@ export type InternalHandlerContext<
|
|
|
520
520
|
/**
|
|
521
521
|
* @internal A revalidate() predicate that read the source page
|
|
522
522
|
* (`currentParams`, `currentUrl`, `fromRouteName`) kept a held segment, so
|
|
523
|
-
* this match's diff is right for
|
|
524
|
-
*
|
|
525
|
-
*
|
|
523
|
+
* this match's diff is right only for pages with equal values of what the
|
|
524
|
+
* kept decisions read (#1331). Set by evaluateRevalidation
|
|
525
|
+
* (router/revalidation.ts) and the stored-entry keep
|
|
526
|
+
* (match-middleware/cache-lookup.ts); match-result.ts turns it into
|
|
527
|
+
* `sourceHeld`, which scopes a prefetch of the response to that group.
|
|
526
528
|
*/
|
|
527
|
-
_sourceHeld?:
|
|
529
|
+
_sourceHeld?: import("../router/source-reads.js").SourceHeld;
|
|
528
530
|
/**
|
|
529
531
|
* @internal DSL loaders registered and not started, by `$$id`: a prefetch
|
|
530
532
|
* deferred them, or a pass that skips loaders (`skipLoaders`) did not run
|
|
@@ -679,7 +681,7 @@ export type IsActionFn = (...actions: ActionRef[]) => boolean;
|
|
|
679
681
|
export type ShouldRevalidateFn<TParams = GenericParams, TEnv = any> = (args: {
|
|
680
682
|
/** Route params from the page being navigated away from. */
|
|
681
683
|
currentParams: TParams;
|
|
682
|
-
/** Full URL of the page being navigated away from. */
|
|
684
|
+
/** Full URL of the page being navigated away from, without its #fragment. */
|
|
683
685
|
currentUrl: URL;
|
|
684
686
|
/** Route params for the navigation target. */
|
|
685
687
|
nextParams: TParams;
|
|
@@ -207,10 +207,13 @@ export type LoaderFn<
|
|
|
207
207
|
export type LoaderOptions = {
|
|
208
208
|
ssr?: boolean;
|
|
209
209
|
/**
|
|
210
|
-
* `prefetch: false` keeps this loader out of prefetches. A
|
|
211
|
-
* prefetch (a partial request with
|
|
212
|
-
*
|
|
213
|
-
*
|
|
210
|
+
* `prefetch: false` keeps this loader out of speculative prefetches. A
|
|
211
|
+
* viewport or render `<Link>` prefetch (a partial request with
|
|
212
|
+
* `X-Rango-Prefetch: 1`) does not execute it; the payload marks it
|
|
213
|
+
* deferred. A hover prefetch and `useRouter().prefetch()`
|
|
214
|
+
* (`X-Rango-Prefetch: intent`) count as intent to click and run it, so a
|
|
215
|
+
* click after a hover needs no follow-up request. A document request and a
|
|
216
|
+
* navigation with no prefetch to adopt run it as usual.
|
|
214
217
|
*
|
|
215
218
|
* A deferred loader never blocks the navigation that adopts the prefetch:
|
|
216
219
|
* it behaves like a loader that is still streaming. Its own `<Suspense>`
|
package/src/types/segments.ts
CHANGED
|
@@ -161,7 +161,6 @@ export interface ResolvedSegment {
|
|
|
161
161
|
parallelName?: string; // For parallels: the parallel group name (used to match with revalidations)
|
|
162
162
|
// Loader-specific fields
|
|
163
163
|
loaderId?: string; // For loaders: the loader $$id identifier
|
|
164
|
-
_inherited?: boolean; // For inherited loaders: dedup marker for buildMatchResult
|
|
165
164
|
loaderData?: any; // For loaders: the resolved data from loader execution
|
|
166
165
|
/**
|
|
167
166
|
* True when this loader was awaited before first flush
|
|
@@ -301,10 +300,22 @@ export interface MatchResult {
|
|
|
301
300
|
interceptTargeted?: true;
|
|
302
301
|
/**
|
|
303
302
|
* Set on a partial match when a revalidate() predicate read the source page
|
|
304
|
-
* and kept a held segment: the diff is
|
|
305
|
-
* scopes its prefetch entry to
|
|
303
|
+
* and kept a held segment: the diff is right for the pages with equal values
|
|
304
|
+
* of what it read. The RSC handler scopes its prefetch entry to that group,
|
|
305
|
+
* or to the source page when the predicate read `currentUrl` (#1331).
|
|
306
306
|
*/
|
|
307
|
-
sourceHeld?:
|
|
307
|
+
sourceHeld?: import("../router/source-reads.js").SourceHeld;
|
|
308
|
+
/**
|
|
309
|
+
* Set on a partial match whose answer has nothing live in it: the cache
|
|
310
|
+
* lookup served it (a cache() record, a Prerender entry, a shell replay's
|
|
311
|
+
* record, not a stale cache() record or stale overlay entry), it sends a
|
|
312
|
+
* segment, every segment it
|
|
313
|
+
* sends is one that lookup decoded, and no loader ran. A stored segment
|
|
314
|
+
* cannot fail after the headers left, since every writer refuses a failed
|
|
315
|
+
* render; a live segment or a loader can. The RSC handler marks the request
|
|
316
|
+
* `_servedFromStorage` from it.
|
|
317
|
+
*/
|
|
318
|
+
fromStorage?: true;
|
|
308
319
|
/**
|
|
309
320
|
* Redirect URL for trailing slash normalization.
|
|
310
321
|
* When set, the RSC handler should return a 308 redirect to this URL
|
|
@@ -364,9 +364,10 @@ export type PathHelpers<TEnv> = {
|
|
|
364
364
|
* freezes into the shell (nested promises stay live holes) — the pre-flush
|
|
365
365
|
* promise applied to the prelude.
|
|
366
366
|
*
|
|
367
|
-
* Pass `{ prefetch: false }` to keep the loader out of
|
|
368
|
-
* prefetch skips it and the click
|
|
369
|
-
* one follow-up request
|
|
367
|
+
* Pass `{ prefetch: false }` to keep the loader out of speculative
|
|
368
|
+
* prefetches (viewport, render): such a prefetch skips it and the click
|
|
369
|
+
* that adopts it fetches it in one follow-up request; a hover prefetch runs
|
|
370
|
+
* it (see {@link LoaderOptions}). Only on a segment the
|
|
370
371
|
* client does not have yet: a loader the client already holds runs in the
|
|
371
372
|
* prefetch when it revalidates.
|
|
372
373
|
*/
|
|
@@ -380,8 +381,9 @@ export type PathHelpers<TEnv> = {
|
|
|
380
381
|
* Attach a loading component to the current route/layout.
|
|
381
382
|
*
|
|
382
383
|
* `{ ssr: false }` skips the fallback on document requests. `{ prefetch:
|
|
383
|
-
* false }` keeps what the fallback covers out of
|
|
384
|
-
*
|
|
384
|
+
* false }` keeps what the fallback covers out of viewport and render
|
|
385
|
+
* prefetches (a hover prefetch runs it), when this entry's segment is new
|
|
386
|
+
* to the client: the entry's handler, its loaders,
|
|
385
387
|
* its orphan layouts and parallel slots and, for a layout, every deeper
|
|
386
388
|
* entry. The click that adopts the prefetch shows the fallback and fetches
|
|
387
389
|
* them in one follow-up request, which reads cache() and "use cache" as
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Sits in src/vite/, not src/vite/discovery/: the loader hook URL below
|
|
9
9
|
* resolves against this module's import.meta.url, and the published bundles
|
|
10
|
-
* (dist/vite/index.js, dist/bin/
|
|
11
|
-
* path.
|
|
10
|
+
* (dist/vite/index.js, and the CLI's code-split chunks in dist/bin/) ship the
|
|
11
|
+
* hook at the same relative path.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { ViteDevServer } from "vite";
|