@rangojs/router 0.32.0 → 0.33.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.
Files changed (180) hide show
  1. package/README.md +3 -2
  2. package/dist/bin/browser-7KIJ5HSV.js +15 -0
  3. package/dist/bin/chunk-3B7CKBRO.js +463 -0
  4. package/dist/bin/chunk-524VACCG.js +71 -0
  5. package/dist/bin/chunk-LD7UBHT4.js +196 -0
  6. package/dist/bin/chunk-TO6CRWLN.js +1429 -0
  7. package/dist/bin/chunk-TTXC2U3G.js +2477 -0
  8. package/dist/bin/client-urls-projection-DQLWPDAT.js +28 -0
  9. package/dist/bin/forward-user-plugins-URBZKJP4.js +62 -0
  10. package/dist/bin/rango.js +42 -10173
  11. package/dist/bin/runtime-EPUDEHO4.js +596 -0
  12. package/dist/bin/runtime-discovery-CZ2QHT4Q.js +352 -0
  13. package/dist/bin/state-ACD2MGKJ.js +16 -0
  14. package/dist/bin/temp-rsc-server-QRR6HD6T.js +3190 -0
  15. package/dist/types/bin/rango.d.ts +1 -0
  16. package/dist/types/browser/action-fence.d.ts +20 -0
  17. package/dist/types/browser/cookie-name.d.ts +51 -10
  18. package/dist/types/browser/invalidate-client-cache.d.ts +8 -6
  19. package/dist/types/browser/navigation-bridge.d.ts +36 -1
  20. package/dist/types/browser/navigation-client.d.ts +19 -0
  21. package/dist/types/browser/navigation-store-handle.d.ts +37 -4
  22. package/dist/types/browser/navigation-store.d.ts +2 -2
  23. package/dist/types/browser/navigation-transaction.d.ts +0 -1
  24. package/dist/types/browser/partial-update.d.ts +13 -0
  25. package/dist/types/browser/prefetch/cache.d.ts +96 -8
  26. package/dist/types/browser/prefetch/fetch.d.ts +26 -2
  27. package/dist/types/browser/prefetch/queue.d.ts +3 -2
  28. package/dist/types/browser/rango-state.d.ts +34 -5
  29. package/dist/types/browser/react/Link.d.ts +15 -6
  30. package/dist/types/browser/types.d.ts +25 -0
  31. package/dist/types/build/generate-route-types.d.ts +1 -1
  32. package/dist/types/build/route-types/include-resolution.d.ts +9 -1
  33. package/dist/types/build/route-types/router-processing.d.ts +3 -1
  34. package/dist/types/cache/cache-key-utils.d.ts +12 -8
  35. package/dist/types/cache/cache-scope.d.ts +4 -2
  36. package/dist/types/cache/cf/cf-cache-types.d.ts +9 -5
  37. package/dist/types/cache/document-cache-policy.d.ts +9 -1
  38. package/dist/types/cache/memory-segment-store.d.ts +3 -2
  39. package/dist/types/cache/segment-codec.d.ts +2 -2
  40. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  41. package/dist/types/cache/types.d.ts +12 -7
  42. package/dist/types/cache/vercel/vercel-cache-store.d.ts +2 -1
  43. package/dist/types/client-urls/types.d.ts +7 -5
  44. package/dist/types/prefetch-markers.d.ts +9 -0
  45. package/dist/types/prefetch-source-scope.d.ts +66 -0
  46. package/dist/types/route-definition/helpers-types.d.ts +8 -5
  47. package/dist/types/router/intercept-resolution.d.ts +25 -11
  48. package/dist/types/router/match-context.d.ts +1 -0
  49. package/dist/types/router/match-handlers.d.ts +1 -1
  50. package/dist/types/router/prefetch-cache-ttl.d.ts +11 -3
  51. package/dist/types/router/router-context.d.ts +1 -1
  52. package/dist/types/router/router-options.d.ts +6 -3
  53. package/dist/types/router/segment-resolution/loader-cache.d.ts +8 -0
  54. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +23 -7
  55. package/dist/types/router/segment-resolution/revalidation.d.ts +19 -0
  56. package/dist/types/router/source-reads.d.ts +53 -0
  57. package/dist/types/router/types.d.ts +1 -1
  58. package/dist/types/rsc/helpers.d.ts +5 -2
  59. package/dist/types/rsc/prefetch-render-error.d.ts +7 -2
  60. package/dist/types/rsc/request-stages.d.ts +2 -0
  61. package/dist/types/rsc/shell-serve.d.ts +6 -1
  62. package/dist/types/server/context.d.ts +23 -6
  63. package/dist/types/server/cookie-store.d.ts +15 -1
  64. package/dist/types/server/document-location-state.d.ts +5 -2
  65. package/dist/types/server/handle-store.d.ts +2 -0
  66. package/dist/types/server/request-context.d.ts +43 -7
  67. package/dist/types/testing/e2e/server.d.ts +16 -1
  68. package/dist/types/testing/render-route.d.ts +66 -2
  69. package/dist/types/testing/serve-shell-request.d.ts +6 -3
  70. package/dist/types/types/handler-context.d.ts +7 -5
  71. package/dist/types/types/loader-types.d.ts +7 -4
  72. package/dist/types/types/segments.d.ts +15 -4
  73. package/dist/types/urls/path-helper-types.d.ts +7 -5
  74. package/dist/types/vite/temp-rsc-server.d.ts +2 -2
  75. package/dist/vite/index.js +449 -189
  76. package/package.json +2 -2
  77. package/skills/app-structure/SKILL.md +74 -0
  78. package/skills/app-structure/e-commerce.md +10 -9
  79. package/skills/app-structure/large-app.md +15 -2
  80. package/skills/caching/SKILL.md +10 -5
  81. package/skills/client-urls/SKILL.md +18 -11
  82. package/skills/comparison/references/framework-comparison.md +13 -8
  83. package/skills/document-cache/SKILL.md +8 -0
  84. package/skills/hooks/navigation.md +8 -8
  85. package/skills/hooks/state.md +24 -8
  86. package/skills/intercept/SKILL.md +33 -12
  87. package/skills/layout/SKILL.md +11 -0
  88. package/skills/links/SKILL.md +101 -4
  89. package/skills/loader/SKILL.md +49 -22
  90. package/skills/migrate-nextjs/SKILL.md +8 -2
  91. package/skills/migrate-react-router/data-and-actions.md +5 -0
  92. package/skills/ppr/SKILL.md +26 -5
  93. package/skills/prerender/SKILL.md +4 -1
  94. package/skills/route/SKILL.md +6 -6
  95. package/skills/router-setup/SKILL.md +2 -1
  96. package/skills/server-actions/SKILL.md +41 -8
  97. package/skills/testing/client-components.md +29 -13
  98. package/skills/testing/serve-shell-request.md +7 -7
  99. package/skills/testing/server-actions.md +8 -0
  100. package/skills/ux-architecture/SKILL.md +67 -18
  101. package/src/bin/rango.ts +39 -13
  102. package/src/browser/action-fence.ts +72 -0
  103. package/src/browser/cookie-name.ts +82 -14
  104. package/src/browser/invalidate-client-cache.ts +9 -7
  105. package/src/browser/navigation-bridge.ts +164 -32
  106. package/src/browser/navigation-client.ts +95 -20
  107. package/src/browser/navigation-store-handle.ts +67 -5
  108. package/src/browser/navigation-store.ts +86 -17
  109. package/src/browser/navigation-transaction.ts +0 -6
  110. package/src/browser/partial-update.ts +31 -12
  111. package/src/browser/prefetch/cache.ts +188 -11
  112. package/src/browser/prefetch/fetch.ts +211 -51
  113. package/src/browser/prefetch/queue.ts +3 -2
  114. package/src/browser/rango-state.ts +50 -8
  115. package/src/browser/react/Link.tsx +20 -9
  116. package/src/browser/react/use-navigation.ts +1 -1
  117. package/src/browser/rsc-router.tsx +8 -1
  118. package/src/browser/server-action-bridge.ts +3 -3
  119. package/src/browser/types.ts +25 -0
  120. package/src/build/generate-route-types.ts +1 -0
  121. package/src/build/route-types/include-resolution.ts +40 -3
  122. package/src/build/route-types/router-processing.ts +170 -25
  123. package/src/build/route-types/scan-filter.ts +1 -0
  124. package/src/cache/cache-key-utils.ts +12 -8
  125. package/src/cache/cache-runtime.ts +5 -1
  126. package/src/cache/cache-scope.ts +28 -7
  127. package/src/cache/cf/cf-cache-types.ts +9 -5
  128. package/src/cache/document-cache-policy.ts +27 -0
  129. package/src/cache/document-cache.ts +5 -0
  130. package/src/cache/memory-segment-store.ts +3 -2
  131. package/src/cache/segment-codec.ts +2 -2
  132. package/src/cache/shell-snapshot.ts +1 -1
  133. package/src/cache/types.ts +12 -7
  134. package/src/cache/vercel/vercel-cache-store.ts +2 -1
  135. package/src/client-urls/types.ts +7 -5
  136. package/src/prefetch-markers.ts +10 -0
  137. package/src/prefetch-source-scope.ts +137 -0
  138. package/src/route-definition/helpers-types.ts +8 -5
  139. package/src/router/intercept-resolution.ts +94 -50
  140. package/src/router/loader-resolution.ts +8 -6
  141. package/src/router/match-api.ts +33 -29
  142. package/src/router/match-context.ts +5 -0
  143. package/src/router/match-handlers.ts +2 -1
  144. package/src/router/match-middleware/cache-lookup.ts +21 -13
  145. package/src/router/match-result.ts +39 -87
  146. package/src/router/navigation-snapshot.ts +18 -3
  147. package/src/router/prefetch-cache-ttl.ts +15 -3
  148. package/src/router/revalidation.ts +53 -23
  149. package/src/router/router-context.ts +2 -1
  150. package/src/router/router-options.ts +6 -3
  151. package/src/router/segment-resolution/loader-cache.ts +21 -3
  152. package/src/router/segment-resolution/prefetch-deferral.ts +38 -31
  153. package/src/router/segment-resolution/revalidation.ts +40 -12
  154. package/src/router/source-reads.ts +122 -0
  155. package/src/router/types.ts +2 -1
  156. package/src/router.ts +11 -12
  157. package/src/rsc/helpers.ts +8 -3
  158. package/src/rsc/prefetch-render-error.ts +7 -2
  159. package/src/rsc/progressive-enhancement.ts +3 -2
  160. package/src/rsc/request-stages.ts +59 -0
  161. package/src/rsc/rsc-rendering.ts +49 -33
  162. package/src/rsc/server-action.ts +2 -1
  163. package/src/rsc/shell-capture.ts +4 -5
  164. package/src/rsc/shell-serve.ts +12 -1
  165. package/src/segment-system.tsx +1 -1
  166. package/src/server/context.ts +37 -6
  167. package/src/server/cookie-store.ts +22 -1
  168. package/src/server/document-location-state.ts +23 -2
  169. package/src/server/handle-store.ts +2 -0
  170. package/src/server/request-context.ts +113 -28
  171. package/src/testing/dispatch.ts +3 -0
  172. package/src/testing/e2e/fixture.ts +4 -12
  173. package/src/testing/e2e/server.ts +91 -4
  174. package/src/testing/render-route.tsx +269 -4
  175. package/src/testing/serve-shell-request.ts +6 -3
  176. package/src/types/handler-context.ts +7 -5
  177. package/src/types/loader-types.ts +7 -4
  178. package/src/types/segments.ts +15 -4
  179. package/src/urls/path-helper-types.ts +7 -5
  180. 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 state, a
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 { setupNavigationBridgeDelegatedPrefetch } from "../browser/navigation-bridge.js";
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 it as a `<Link>` prefetch does (the
103
- * `X-Rango-Prefetch` header), so `prefetch: false` loaders and `loading()`
104
- * entries are deferred: `result.readDeferred()` lists them. `fill: true`
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 that source only (#1331). Set by
524
- * evaluateRevalidation (router/revalidation.ts); match-result.ts turns it
525
- * into `sourceHeld`, which scopes a prefetch of the response to its source.
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?: true;
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 `<Link>`
211
- * prefetch (a partial request with `X-Rango-Prefetch`) does not execute it;
212
- * the payload marks it deferred. A document request and a navigation with
213
- * no prefetch to adopt run it as usual.
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>`
@@ -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 that source's. The RSC handler
305
- * scopes its prefetch entry to it, like `interceptTargeted` (#1331).
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?: true;
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 prefetches: a
368
- * prefetch skips it and the click that adopts the prefetch fetches it in
369
- * one follow-up request (see {@link LoaderOptions}). Only on a segment the
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 prefetches, when this
384
- * entry's segment is new to the client: the entry's handler, its loaders,
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/rango.js) ship the hook at the same relative
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";