@rangojs/router 0.24.4 → 0.25.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 (267) hide show
  1. package/README.md +1 -0
  2. package/dist/bin/rango.js +363 -39
  3. package/dist/testing/vitest.js +37 -4
  4. package/dist/types/browser/event-controller.d.ts +24 -5
  5. package/dist/types/browser/intercept-utils.d.ts +5 -2
  6. package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
  7. package/dist/types/browser/navigation-transaction.d.ts +5 -5
  8. package/dist/types/browser/partial-update.d.ts +21 -13
  9. package/dist/types/browser/prefetch/cache.d.ts +8 -0
  10. package/dist/types/browser/rango-marker.d.ts +7 -0
  11. package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
  12. package/dist/types/browser/react/use-handle.d.ts +20 -0
  13. package/dist/types/browser/react/use-href.d.ts +6 -0
  14. package/dist/types/browser/react/use-router.d.ts +6 -1
  15. package/dist/types/browser/response-adapter.d.ts +5 -3
  16. package/dist/types/browser/scroll-restoration.d.ts +9 -3
  17. package/dist/types/browser/segment-reconciler.d.ts +10 -13
  18. package/dist/types/browser/types.d.ts +38 -8
  19. package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
  20. package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
  21. package/dist/types/cache/types.d.ts +8 -0
  22. package/dist/types/client-urls/client-urls.d.ts +6 -2
  23. package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
  24. package/dist/types/client-urls/server-projection.d.ts +9 -5
  25. package/dist/types/client-urls/types.d.ts +29 -16
  26. package/dist/types/client.d.ts +2 -2
  27. package/dist/types/client.rsc.d.ts +2 -2
  28. package/dist/types/decode-loader-results.d.ts +28 -7
  29. package/dist/types/href-client.d.ts +3 -1
  30. package/dist/types/index.rsc.d.ts +1 -1
  31. package/dist/types/loader-redirect.d.ts +2 -3
  32. package/dist/types/outlet-context.d.ts +7 -6
  33. package/dist/types/prefetch-markers.d.ts +14 -5
  34. package/dist/types/route-content-wrapper.d.ts +17 -14
  35. package/dist/types/route-definition/helpers-types.d.ts +7 -2
  36. package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
  37. package/dist/types/route-definition/rewrite.d.ts +48 -5
  38. package/dist/types/route-types.d.ts +2 -2
  39. package/dist/types/router/handler-context.d.ts +14 -2
  40. package/dist/types/router/intercept-resolution.d.ts +19 -46
  41. package/dist/types/router/lazy-includes.d.ts +7 -0
  42. package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
  43. package/dist/types/router/middleware.d.ts +0 -16
  44. package/dist/types/router/navigation-snapshot.d.ts +2 -0
  45. package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
  46. package/dist/types/router/revalidation.d.ts +8 -1
  47. package/dist/types/router/router-context.d.ts +4 -16
  48. package/dist/types/router/router-interfaces.d.ts +0 -6
  49. package/dist/types/router/router-options.d.ts +4 -7
  50. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
  51. package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
  52. package/dist/types/router/segment-resolution.d.ts +1 -1
  53. package/dist/types/router/segment-wrappers.d.ts +3 -5
  54. package/dist/types/router/substitute-pattern-params.d.ts +3 -2
  55. package/dist/types/router/tracing.d.ts +2 -2
  56. package/dist/types/rsc/full-payload.d.ts +14 -2
  57. package/dist/types/rsc/helpers.d.ts +8 -0
  58. package/dist/types/rsc/manifest-init.d.ts +20 -0
  59. package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
  60. package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
  61. package/dist/types/rsc/rewrite-passes.d.ts +12 -2
  62. package/dist/types/rsc/server-action.d.ts +6 -0
  63. package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
  64. package/dist/types/rsc/shell-capture.d.ts +7 -0
  65. package/dist/types/rsc/types.d.ts +5 -0
  66. package/dist/types/segment-system.d.ts +15 -0
  67. package/dist/types/server/context.d.ts +64 -4
  68. package/dist/types/server/request-context.d.ts +41 -4
  69. package/dist/types/server/request-headers.d.ts +12 -0
  70. package/dist/types/server/rewrite-trail.d.ts +8 -1
  71. package/dist/types/testing/e2e/index.d.ts +1 -1
  72. package/dist/types/testing/flight.entry.d.ts +1 -1
  73. package/dist/types/testing/index.d.ts +1 -1
  74. package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
  75. package/dist/types/testing/render-route.d.ts +21 -11
  76. package/dist/types/testing/run-loader.d.ts +7 -0
  77. package/dist/types/testing/run-middleware.d.ts +7 -0
  78. package/dist/types/testing/run-revalidate.d.ts +5 -4
  79. package/dist/types/testing/serve-shell-request.d.ts +41 -4
  80. package/dist/types/testing/shell-status.d.ts +17 -0
  81. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  82. package/dist/types/testing/vitest.d.ts +10 -1
  83. package/dist/types/thenable-status.d.ts +0 -6
  84. package/dist/types/types/handler-context.d.ts +12 -1
  85. package/dist/types/types/loader-types.d.ts +4 -3
  86. package/dist/types/types/route-entry.d.ts +2 -1
  87. package/dist/types/urls/path-helper-types.d.ts +4 -2
  88. package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
  89. package/dist/types/vite/discovery/state.d.ts +20 -0
  90. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
  91. package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
  92. package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
  93. package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
  94. package/dist/vite/index.js +6816 -6547
  95. package/package.json +1 -1
  96. package/skills/app-structure/SKILL.md +255 -0
  97. package/skills/app-structure/content-site.md +333 -0
  98. package/skills/app-structure/e-commerce.md +435 -0
  99. package/skills/app-structure/large-app.md +885 -0
  100. package/skills/app-structure/multi-app.md +280 -0
  101. package/skills/breadcrumbs/SKILL.md +6 -1
  102. package/skills/caching/SKILL.md +3 -1
  103. package/skills/catalog.json +12 -0
  104. package/skills/client-urls/SKILL.md +185 -32
  105. package/skills/composability/SKILL.md +10 -8
  106. package/skills/handler-use/SKILL.md +48 -9
  107. package/skills/hooks/handle-and-actions.md +104 -0
  108. package/skills/hooks/navigation.md +24 -10
  109. package/skills/hooks/urls.md +1 -1
  110. package/skills/i18n/SKILL.md +2 -1
  111. package/skills/intercept/SKILL.md +60 -29
  112. package/skills/layout/SKILL.md +6 -3
  113. package/skills/links/SKILL.md +151 -17
  114. package/skills/loader/SKILL.md +32 -10
  115. package/skills/middleware/SKILL.md +111 -15
  116. package/skills/observability/SKILL.md +11 -0
  117. package/skills/parallel/SKILL.md +5 -5
  118. package/skills/ppr/SKILL.md +43 -8
  119. package/skills/prerender/SKILL.md +1 -2
  120. package/skills/rango/SKILL.md +3 -0
  121. package/skills/response-routes/SKILL.md +4 -1
  122. package/skills/scripts/SKILL.md +12 -1
  123. package/skills/server-actions/SKILL.md +60 -20
  124. package/skills/testing/SKILL.md +1 -0
  125. package/skills/testing/client-components.md +13 -10
  126. package/skills/testing/loader.md +25 -24
  127. package/skills/testing/middleware.md +11 -11
  128. package/skills/testing/response-routes.md +4 -1
  129. package/skills/testing/serve-shell-request.md +85 -21
  130. package/skills/ux-architecture/SKILL.md +980 -0
  131. package/skills/view-transitions/SKILL.md +43 -10
  132. package/src/__augment-tests__/augmented.check.ts +28 -0
  133. package/src/browser/event-controller.ts +44 -18
  134. package/src/browser/intercept-utils.ts +3 -2
  135. package/src/browser/merge-segment-loaders.ts +0 -85
  136. package/src/browser/navigation-bridge.ts +112 -84
  137. package/src/browser/navigation-client.ts +20 -15
  138. package/src/browser/navigation-store.ts +4 -2
  139. package/src/browser/navigation-transaction.ts +9 -8
  140. package/src/browser/partial-update.ts +90 -32
  141. package/src/browser/prefetch/cache.ts +8 -0
  142. package/src/browser/prefetch/fetch.ts +13 -3
  143. package/src/browser/rango-marker.ts +13 -0
  144. package/src/browser/react/NavigationProvider.tsx +25 -7
  145. package/src/browser/react/filter-segment-order.ts +13 -0
  146. package/src/browser/react/use-action.ts +9 -15
  147. package/src/browser/react/use-handle.ts +63 -71
  148. package/src/browser/react/use-href.tsx +24 -5
  149. package/src/browser/react/use-link-status.ts +1 -1
  150. package/src/browser/react/use-navigation.ts +30 -40
  151. package/src/browser/react/use-router.ts +37 -6
  152. package/src/browser/react/use-segments.ts +8 -12
  153. package/src/browser/response-adapter.ts +11 -5
  154. package/src/browser/rsc-router.tsx +3 -1
  155. package/src/browser/scroll-restoration.ts +34 -20
  156. package/src/browser/segment-reconciler.ts +11 -29
  157. package/src/browser/server-action-bridge.ts +52 -16
  158. package/src/browser/types.ts +40 -8
  159. package/src/build/generate-manifest.ts +12 -1
  160. package/src/build/route-types/codegen.ts +6 -2
  161. package/src/build/route-types/per-module-writer.ts +94 -61
  162. package/src/cache/cache-runtime.ts +9 -0
  163. package/src/cache/cache-scope.ts +2 -1
  164. package/src/cache/cf/cf-cache-store.ts +2 -0
  165. package/src/cache/cf/cf-shell-frame.ts +3 -0
  166. package/src/cache/types.ts +8 -0
  167. package/src/cache/vercel/vercel-cache-store.ts +6 -1
  168. package/src/client-urls/client-urls.ts +202 -153
  169. package/src/client-urls/revalidate-registry.ts +12 -3
  170. package/src/client-urls/server-projection.ts +137 -66
  171. package/src/client-urls/types.ts +30 -15
  172. package/src/client.rsc.tsx +11 -1
  173. package/src/client.tsx +9 -1
  174. package/src/decode-loader-results.ts +64 -31
  175. package/src/href-client.ts +3 -1
  176. package/src/index.rsc.ts +1 -0
  177. package/src/index.ts +12 -4
  178. package/src/loader-redirect.tsx +2 -3
  179. package/src/outlet-context.ts +7 -6
  180. package/src/prefetch-markers.ts +15 -5
  181. package/src/reverse.ts +3 -3
  182. package/src/route-content-wrapper.tsx +19 -17
  183. package/src/route-definition/dsl-helpers.ts +27 -7
  184. package/src/route-definition/helpers-types.ts +7 -2
  185. package/src/route-definition/resolve-handler-use.ts +23 -10
  186. package/src/route-definition/rewrite.ts +86 -19
  187. package/src/route-types.ts +1 -2
  188. package/src/router/find-match.ts +37 -1
  189. package/src/router/handler-context.ts +85 -40
  190. package/src/router/intercept-resolution.ts +161 -307
  191. package/src/router/lazy-includes.ts +43 -27
  192. package/src/router/loader-resolution.ts +21 -1
  193. package/src/router/manifest.ts +6 -8
  194. package/src/router/match-api.ts +11 -1
  195. package/src/router/match-middleware/background-revalidation.ts +2 -4
  196. package/src/router/match-middleware/cache-lookup.ts +16 -20
  197. package/src/router/match-middleware/intercept-resolution.ts +41 -60
  198. package/src/router/match-result.ts +31 -2
  199. package/src/router/middleware.ts +6 -147
  200. package/src/router/navigation-snapshot.ts +38 -2
  201. package/src/router/prefetch-cache-ttl.ts +10 -18
  202. package/src/router/prerender-match.ts +2 -1
  203. package/src/router/revalidation.ts +24 -3
  204. package/src/router/router-context.ts +5 -18
  205. package/src/router/router-interfaces.ts +0 -7
  206. package/src/router/router-options.ts +4 -7
  207. package/src/router/segment-resolution/fresh.ts +6 -2
  208. package/src/router/segment-resolution/helpers.ts +7 -1
  209. package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
  210. package/src/router/segment-resolution/revalidation.ts +75 -35
  211. package/src/router/segment-resolution.ts +0 -1
  212. package/src/router/segment-wrappers.ts +8 -13
  213. package/src/router/substitute-pattern-params.ts +3 -2
  214. package/src/router/tracing.ts +2 -2
  215. package/src/router.ts +2 -9
  216. package/src/rsc/full-payload.ts +26 -4
  217. package/src/rsc/handler.ts +83 -41
  218. package/src/rsc/helpers.ts +13 -0
  219. package/src/rsc/manifest-init.ts +56 -3
  220. package/src/rsc/prefetch-render-error.ts +38 -0
  221. package/src/rsc/progressive-enhancement.ts +161 -211
  222. package/src/rsc/rewrite-passes.ts +56 -9
  223. package/src/rsc/rsc-rendering.ts +79 -23
  224. package/src/rsc/server-action.ts +25 -2
  225. package/src/rsc/shell-capture-constants.ts +22 -0
  226. package/src/rsc/shell-capture.ts +11 -0
  227. package/src/rsc/types.ts +5 -0
  228. package/src/segment-system.tsx +125 -119
  229. package/src/server/context.ts +96 -5
  230. package/src/server/handle-store.ts +3 -2
  231. package/src/server/request-context.ts +42 -3
  232. package/src/server/request-headers.ts +32 -0
  233. package/src/server/rewrite-trail.ts +55 -9
  234. package/src/ssr/ssr-root.tsx +6 -10
  235. package/src/testing/dispatch.ts +4 -5
  236. package/src/testing/e2e/index.ts +1 -0
  237. package/src/testing/flight.entry.ts +1 -0
  238. package/src/testing/index.ts +1 -0
  239. package/src/testing/internal/client-urls-projection.ts +31 -11
  240. package/src/testing/render-route.tsx +83 -40
  241. package/src/testing/run-loader.ts +26 -2
  242. package/src/testing/run-middleware.ts +10 -0
  243. package/src/testing/run-revalidate.ts +13 -6
  244. package/src/testing/serve-shell-request.ts +77 -4
  245. package/src/testing/shell-status.ts +40 -0
  246. package/src/testing/vitest-stubs/version.ts +1 -0
  247. package/src/testing/vitest.ts +74 -7
  248. package/src/thenable-status.ts +0 -11
  249. package/src/types/handler-context.ts +12 -1
  250. package/src/types/loader-types.ts +4 -3
  251. package/src/types/route-entry.ts +2 -1
  252. package/src/urls/include-helper.ts +38 -11
  253. package/src/urls/path-helper-types.ts +4 -2
  254. package/src/vite/discovery/client-urls-projection.ts +205 -16
  255. package/src/vite/discovery/discover-routers.ts +2 -2
  256. package/src/vite/discovery/prerender-collection.ts +2 -6
  257. package/src/vite/discovery/state.ts +26 -0
  258. package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
  259. package/src/vite/plugins/expose-internal-ids.ts +46 -40
  260. package/src/vite/plugins/hoist/source.ts +8 -7
  261. package/src/vite/plugins/transition-when-hoist.ts +1 -0
  262. package/src/vite/plugins/version-plugin.ts +7 -0
  263. package/src/vite/plugins/version.d.ts +7 -0
  264. package/src/vite/plugins/virtual-entries.ts +20 -0
  265. package/src/vite/rango.ts +4 -1
  266. package/src/vite/router-discovery.ts +39 -7
  267. package/src/vite/utils/prerender-utils.ts +17 -0
@@ -9,8 +9,8 @@ import { isPlaceholder } from "../types/segments.js";
9
9
  import { setAppVersion } from "./app-version.js";
10
10
  import { isActionFenceActive } from "./action-fence.js";
11
11
  import { getRangoState } from "./rango-state.js";
12
- import * as React from "react";
13
12
  import { startTransition } from "react";
13
+ import { flushSync } from "react-dom";
14
14
  import {
15
15
  createNavigationTransaction,
16
16
  resolveNavigationState,
@@ -29,15 +29,12 @@ import {
29
29
  ensureHistoryKey,
30
30
  } from "./scroll-restoration.js";
31
31
 
32
- // addTransitionType is only available in React experimental
33
- const addTransitionType: ((type: string) => void) | undefined =
34
- "addTransitionType" in React ? (React as any).addTransitionType : undefined;
35
-
36
32
  import {
37
33
  setupDelegatedLinkPrefetch,
38
34
  setupLinkInterception,
39
35
  } from "./link-interceptor.js";
40
36
  import {
37
+ addTransitionType,
41
38
  createPartialUpdater,
42
39
  shouldStartViewTransition,
43
40
  } from "./partial-update.js";
@@ -168,6 +165,8 @@ export function createNavigationBridge(
168
165
  basename,
169
166
  } = config;
170
167
  let version = config.version;
168
+ // The latest back/forward: an earlier one's deferred restore yields to it.
169
+ let popGeneration = 0;
171
170
 
172
171
  // Create shared partial updater
173
172
  const fetchPartialUpdate = createPartialUpdater({
@@ -188,7 +187,8 @@ export function createNavigationBridge(
188
187
  handles: (metadata) => {
189
188
  if (metadata.handles) consumeHandles(metadata, eventController, store);
190
189
  },
191
- flush: eventController.flushRouteState,
190
+ // Committed before the page is handed over (PartialUpdateConfig.fill).
191
+ flush: () => flushSync(eventController.flushRouteState),
192
192
  },
193
193
  });
194
194
 
@@ -304,7 +304,7 @@ export function createNavigationBridge(
304
304
  // Save current scroll position before changing URL
305
305
  handleNavigationStart();
306
306
 
307
- const shallowRouteName = store.getHistoryEntryMemory()?.routeName;
307
+ const shallowShown = store.getHistoryEntryMemory();
308
308
 
309
309
  // Update browser URL (carry intercept context into history state)
310
310
  const historyState = buildHistoryState(
@@ -320,7 +320,7 @@ export function createNavigationBridge(
320
320
  // Ensure new history entry has a scroll restoration key
321
321
  ensureHistoryKey();
322
322
  // Same route, new entry (transition({ when }) back/forward source).
323
- store.rememberDisplayedEntry(shallowRouteName);
323
+ store.rememberDisplayedEntry(shallowShown);
324
324
  // Treeless: readers take the entry's state with setLocation below.
325
325
  eventController.commitLocationState(historyState, true);
326
326
 
@@ -628,7 +628,8 @@ export function createNavigationBridge(
628
628
  * Handle browser back/forward navigation
629
629
  * Uses cached segments when available for instant restoration
630
630
  */
631
- async handlePopstate(): Promise<void> {
631
+ async handlePopstate(event?: PopStateEvent): Promise<void> {
632
+ const generation = ++popGeneration;
632
633
  // Abort any pending navigation to prevent race conditions, and the
633
634
  // fill of the page being left (prefetch: false), which its committed
634
635
  // navigation still owns.
@@ -738,18 +739,6 @@ export function createNavigationBridge(
738
739
  forceAwait: true,
739
740
  transitionGatedOff: gatedOff,
740
741
  });
741
- store.rememberDisplayedEntry();
742
- // The restored entry's location and params, set before onUpdate so
743
- // the hooks take both in the batch that renders its tree. Not at
744
- // the popstate event: the page being left is on screen until here
745
- // (#1031). A restore that has to fetch gets its location from
746
- // tx.commit() instead (navigation-transaction.ts).
747
- eventController.setLocation(new URL(url));
748
- eventController.setParams(cachedParams);
749
- // The entry this handler found at the event, which the cached tree
750
- // was built for: history may have moved during the render above.
751
- eventController.commitLocationState(historyState);
752
-
753
742
  const popstateUpdate = {
754
743
  root,
755
744
  metadata: {
@@ -771,68 +760,106 @@ export function createNavigationBridge(
771
760
  !isIntercept &&
772
761
  !isLeavingIntercept &&
773
762
  shouldStartViewTransition(cachedSegments, gatedOff);
774
- if (process.env.NODE_ENV !== "production") auditTreeCause("popstate");
775
- if (hasTransition) {
776
- startTransition(() => {
777
- if (addTransitionType) {
778
- addTransitionType("navigation-back");
779
- }
780
- onUpdate(popstateUpdate);
781
- });
782
- } else {
783
- onUpdate(popstateUpdate);
784
- }
785
-
786
- // SWR: If stale, trigger background revalidation
787
- if (isStale) {
788
- debugLog("[Browser] Cache is stale, background revalidating...");
789
- // Background revalidation - don't await, just fire and forget.
790
- // When the entry's handles are incomplete (a deferred Meta was still
791
- // pending when the user navigated away — see handlesPending), send NO
792
- // segment IDs so the server returns a FULL re-render with the handle
793
- // stream. A normal stale revalidation sends the cached IDs and the
794
- // server returns a diff-only payload that omits unchanged segments'
795
- // handles, so a deferred Meta would never re-stream and the title
796
- // would stay the pre-resolution carry. handlesPending is set only for
797
- // the deferred-Meta-aborted case, so action/cross-tab SWR keeps the
798
- // cheap diff path.
799
- const segmentIds = cached?.handlesPending
800
- ? []
801
- : cachedSegments.map((s) => s.id);
802
-
803
- const tx = createNavigationTransaction(
804
- store,
805
- eventController,
806
- url,
807
- { skipLoadingState: true, replace: true },
808
- );
763
+ // Not when the browser animated the swipe itself, nor when it
764
+ // restores this entry's scroll itself ("auto": no
765
+ // <Html.ScrollRestoration>), which it does right after the event,
766
+ // on whatever page is then on screen.
767
+ const animate =
768
+ hasTransition &&
769
+ !event?.hasUAVisualTransition &&
770
+ window.history.scrollRestoration === "manual";
771
+ const commit = (): void => {
772
+ // A later back/forward, or a navigation that committed first
773
+ // (a shallow one from an app's popstate listener), owns the
774
+ // screen now.
775
+ if (
776
+ generation !== popGeneration ||
777
+ store.getHistoryKey() !== historyKey
778
+ ) {
779
+ return;
780
+ }
781
+ store.rememberDisplayedEntry();
782
+ // The restored entry's location and params, set here so the hooks
783
+ // take both in the batch that renders its tree. Not at the
784
+ // popstate event: the page being left is on screen until here
785
+ // (#1031, #1164). A restore that has to fetch gets its location
786
+ // from tx.commit() instead (navigation-transaction.ts).
787
+ eventController.setLocation(new URL(url));
788
+ eventController.setParams(cachedParams);
789
+ // The entry this handler found at the event, which the cached
790
+ // tree was built for: history may have moved during the render.
791
+ eventController.commitLocationState(historyState);
792
+ if (process.env.NODE_ENV !== "production") {
793
+ auditTreeCause("popstate");
794
+ }
795
+ if (!hasTransition) onUpdate(popstateUpdate);
796
+ else {
797
+ startTransition(() => {
798
+ if (animate) {
799
+ addTransitionType?.("pop");
800
+ addTransitionType?.("navigation-back");
801
+ }
802
+ onUpdate({ ...popstateUpdate, inTransition: true });
803
+ });
804
+ }
805
+ // SWR: If stale, trigger background revalidation
806
+ if (isStale) {
807
+ debugLog("[Browser] Cache is stale, background revalidating...");
808
+ // Background revalidation - don't await, just fire and forget.
809
+ // When the entry's handles are incomplete (a deferred Meta was still
810
+ // pending when the user navigated away — see handlesPending), send NO
811
+ // segment IDs so the server returns a FULL re-render with the handle
812
+ // stream. A normal stale revalidation sends the cached IDs and the
813
+ // server returns a diff-only payload that omits unchanged segments'
814
+ // handles, so a deferred Meta would never re-stream and the title
815
+ // would stay the pre-resolution carry. handlesPending is set only for
816
+ // the deferred-Meta-aborted case, so action/cross-tab SWR keeps the
817
+ // cheap diff path.
818
+ const segmentIds = cached?.handlesPending
819
+ ? []
820
+ : cachedSegments.map((s) => s.id);
821
+
822
+ const tx = createNavigationTransaction(
823
+ store,
824
+ eventController,
825
+ url,
826
+ { skipLoadingState: true, replace: true },
827
+ );
809
828
 
810
- fetchPartialUpdate(
811
- url,
812
- segmentIds,
813
- false,
814
- tx.handle.signal,
815
- tx.with({
829
+ fetchPartialUpdate(
816
830
  url,
817
- replace: true,
818
- scroll: false,
819
- intercept: isIntercept,
820
- interceptSourceUrl,
821
- cacheOnly: true,
822
- }),
823
- { type: "stale-revalidation", interceptSourceUrl },
824
- )
825
- .catch((error) => {
826
- if (isBackgroundSuppressible(error)) return;
827
- console.error(
828
- "[Browser] Background revalidation failed:",
829
- error,
830
- );
831
- })
832
- .finally(() => {
833
- tx[Symbol.dispose]();
834
- });
835
- }
831
+ segmentIds,
832
+ false,
833
+ tx.handle.signal,
834
+ tx.with({
835
+ url,
836
+ replace: true,
837
+ scroll: false,
838
+ intercept: isIntercept,
839
+ interceptSourceUrl,
840
+ cacheOnly: true,
841
+ }),
842
+ { type: "stale-revalidation", interceptSourceUrl },
843
+ )
844
+ .catch((error) => {
845
+ if (isBackgroundSuppressible(error)) return;
846
+ console.error(
847
+ "[Browser] Background revalidation failed:",
848
+ error,
849
+ );
850
+ })
851
+ .finally(() => {
852
+ tx[Symbol.dispose]();
853
+ });
854
+ }
855
+ };
856
+ // React commits a transition started while window.event is the
857
+ // popstate event synchronously, with no view transition, and leaves
858
+ // its types queued for the next transition commit (react-dom
859
+ // shouldAttemptEagerTransition). An animated restore commits on the
860
+ // next task: the page being left stays on screen one task longer.
861
+ if (animate) setTimeout(commit, 0);
862
+ else commit();
836
863
  return;
837
864
  } catch (error) {
838
865
  console.warn(
@@ -898,8 +925,9 @@ export function createNavigationBridge(
898
925
  // tx.commit() never ran, so the location is still the page being
899
926
  // left. The error boundary below replaces that page, and history is
900
927
  // already on the entry that failed: the hooks report that entry, in
901
- // the boundary's batch.
928
+ // the boundary's batch, and scroll restoration tracks it as shown.
902
929
  eventController.setLocation(new URL(url));
930
+ ensureHistoryKey();
903
931
 
904
932
  const networkError = toNetworkError(error, {
905
933
  url,
@@ -932,8 +960,8 @@ export function createNavigationBridge(
932
960
  this.navigate(url, options);
933
961
  });
934
962
 
935
- const handlePopstate = () => {
936
- this.handlePopstate();
963
+ const handlePopstate = (event: PopStateEvent) => {
964
+ this.handlePopstate(event);
937
965
  };
938
966
 
939
967
  // When the browser restores a page from bfcache (back-forward cache),
@@ -79,6 +79,7 @@ export function createNavigationClient(
79
79
  targetUrl,
80
80
  segmentIds,
81
81
  previousUrl,
82
+ rewriteTarget,
82
83
  signal,
83
84
  staleRevalidation,
84
85
  interceptSourceUrl,
@@ -115,6 +116,11 @@ export function createNavigationClient(
115
116
  if (routerId) {
116
117
  fetchUrl.searchParams.set("_rsc_rid", routerId);
117
118
  }
119
+ // The page left's rewrite target (#1144), in both prefetch keys like
120
+ // the params above: buildPrefetchUrl sets it in the same place.
121
+ if (rewriteTarget) {
122
+ fetchUrl.searchParams.set("_rsc_rt", rewriteTarget);
123
+ }
118
124
  // A param, not a header: the document cache decides from the raw URL
119
125
  // (cache/document-cache.ts), and it keeps the fill out of every
120
126
  // prefetch key below.
@@ -317,18 +323,15 @@ export function createNavigationClient(
317
323
  // so only a fully-warmed prefetch commits in a transition (no fallback flash).
318
324
  let fullyPrefetched = false;
319
325
 
320
- if (cachedEntry) {
326
+ // The prefetch entry this navigation serves.
327
+ let adopted = cachedEntry;
328
+ if (adopted) {
321
329
  if (tx) {
322
330
  browserDebugLog(tx, "prefetch cache hit (warm)", {
323
331
  key: hitKey,
324
332
  wildcard: hitKey === wildcardKey,
325
333
  });
326
334
  }
327
- payloadPromise = cachedEntry.payload;
328
- streamCompletePromise = cachedEntry.streamComplete;
329
- // Only a hit whose stream already fully drained is "fully prefetched";
330
- // a still-streaming hit must keep streaming its fallbacks like a cold load.
331
- fullyPrefetched = cachedEntry.complete;
332
335
  } else if (inflightEntryPromise) {
333
336
  if (tx) {
334
337
  browserDebugLog(tx, "reusing inflight prefetch", {
@@ -342,8 +345,6 @@ export function createNavigationClient(
342
345
  if (tx) {
343
346
  browserDebugLog(tx, "inflight prefetch unavailable, refetching");
344
347
  }
345
- ({ payload: payloadPromise, streamComplete: streamCompletePromise } =
346
- freshResult());
347
348
  } else if (adoptedViaWildcard && entry.scope === "source") {
348
349
  // A wildcard-adopted inflight that turned out source-scoped was
349
350
  // built for a different source page. Discard and refetch.
@@ -353,15 +354,19 @@ export function createNavigationClient(
353
354
  "wildcard inflight turned out source-scoped, refetching",
354
355
  );
355
356
  }
356
- ({ payload: payloadPromise, streamComplete: streamCompletePromise } =
357
- freshResult());
358
357
  } else {
359
- payloadPromise = entry.payload;
360
- streamCompletePromise = entry.streamComplete;
361
- // Adopted inflight is normally still streaming (false), but read the
362
- // flag in case it completed between publish and adoption.
363
- fullyPrefetched = entry.complete;
358
+ adopted = entry;
364
359
  }
360
+ }
361
+ if (adopted) {
362
+ // The click owns the stream from here on (DecodedPrefetch.stallTimer).
363
+ clearTimeout(adopted.stallTimer);
364
+ payloadPromise = adopted.payload;
365
+ streamCompletePromise = adopted.streamComplete;
366
+ // Only an entry whose stream already fully drained is "fully
367
+ // prefetched"; a still-streaming one keeps streaming its fallbacks like
368
+ // a cold load.
369
+ fullyPrefetched = adopted.complete;
365
370
  } else {
366
371
  ({ payload: payloadPromise, streamComplete: streamCompletePromise } =
367
372
  freshResult());
@@ -7,6 +7,7 @@ import type {
7
7
  UpdateSubscriber,
8
8
  ResolvedSegment,
9
9
  HandleData,
10
+ ShownEntry,
10
11
  } from "./types.js";
11
12
  import type { EventController } from "./event-controller.js";
12
13
  import { clearPrefetchCache } from "./prefetch/cache.js";
@@ -466,13 +467,14 @@ export function createNavigationStore(
466
467
  currentHistoryKey = key;
467
468
  },
468
469
 
469
- rememberDisplayedEntry(routeName?: string): void {
470
+ rememberDisplayedEntry(shown?: ShownEntry): void {
470
471
  if (typeof window === "undefined") return;
471
472
  const key = getHistoryStateKey();
472
473
  const previous = entryMemory.get(key);
473
474
  entryMemory.delete(key);
474
475
  entryMemory.set(key, {
475
- routeName: routeName ?? previous?.routeName,
476
+ routeName: shown?.routeName ?? previous?.routeName,
477
+ rewriteTarget: (shown ?? previous)?.rewriteTarget,
476
478
  state: window.history.state,
477
479
  });
478
480
  if (entryMemory.size > HISTORY_ENTRY_MEMORY_SIZE) {
@@ -2,6 +2,7 @@ import type {
2
2
  NavigateOptionsInternal,
3
3
  NavigationStore,
4
4
  ResolvedSegment,
5
+ ShownEntry,
5
6
  StreamingToken,
6
7
  } from "./types.js";
7
8
  import {
@@ -53,8 +54,8 @@ interface CommitOptions {
53
54
  * merged in. The page left was saved by handleTraversalStart.
54
55
  */
55
56
  traversal?: boolean;
56
- /** The committed route's name (payload metadata), remembered per history entry. */
57
- routeName?: string;
57
+ /** The payload's metadata: its route name and rewrite target, remembered per history entry. */
58
+ metadata?: ShownEntry;
58
59
  /**
59
60
  * transition({ when }) decision already made at this navigation's first
60
61
  * presentation (the optimistic clientUrls() swap). Not a commit input: the
@@ -86,8 +87,8 @@ interface BoundCommitOverrides {
86
87
  cacheOnly?: boolean;
87
88
  /** Server-set location state to merge into history.pushState */
88
89
  serverState?: Record<string, unknown>;
89
- /** The committed route's name (payload metadata), remembered per history entry. */
90
- routeName?: string;
90
+ /** The payload's metadata: its route name and rewrite target, remembered per history entry. */
91
+ metadata?: ShownEntry;
91
92
  /** No payload follows this commit (CommitOptions.treeless). */
92
93
  treeless?: boolean;
93
94
  }
@@ -210,7 +211,7 @@ export function createNavigationTransaction(
210
211
  if (storeOnly) {
211
212
  // Same entry (an action refetch): refresh its state, keep its name
212
213
  // unless the payload names it.
213
- store.rememberDisplayedEntry(opts.routeName);
214
+ store.rememberDisplayedEntry(opts.metadata);
214
215
  debugLog("[Browser] Store updated (action)");
215
216
  handle.complete(parsedUrl);
216
217
  return {};
@@ -222,7 +223,7 @@ export function createNavigationTransaction(
222
223
  serverState && Object.keys(serverState).length > 0
223
224
  ? mergeLocationState(serverState)
224
225
  : window.history.state;
225
- store.rememberDisplayedEntry(opts.routeName);
226
+ store.rememberDisplayedEntry(opts.metadata);
226
227
  eventController.commitLocationState(entryState, opts.treeless);
227
228
  handle.complete(parsedUrl);
228
229
  debugLog("[Browser] Traversal committed, historyKey:", historyKey);
@@ -237,7 +238,7 @@ export function createNavigationTransaction(
237
238
 
238
239
  pushHistoryWithIdx(historyState, url, replace ?? false);
239
240
  ensureHistoryKey();
240
- store.rememberDisplayedEntry(opts.routeName);
241
+ store.rememberDisplayedEntry(opts.metadata);
241
242
  eventController.commitLocationState(historyState, opts.treeless);
242
243
 
243
244
  handle.complete(parsedUrl);
@@ -296,7 +297,7 @@ export function createNavigationTransaction(
296
297
  interceptSourceUrl,
297
298
  cacheOnly,
298
299
  serverState,
299
- routeName: overrides?.routeName ?? opts.routeName,
300
+ metadata: overrides?.metadata ?? opts.metadata,
300
301
  treeless: overrides?.treeless,
301
302
  });
302
303
  },