@rangojs/router 0.24.3 → 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 (274) 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-root.d.ts +4 -1
  23. package/dist/types/client-urls/client-urls.d.ts +28 -2
  24. package/dist/types/client-urls/navigation.d.ts +6 -8
  25. package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
  26. package/dist/types/client-urls/server-projection.d.ts +20 -5
  27. package/dist/types/client-urls/types.d.ts +35 -17
  28. package/dist/types/client.d.ts +2 -2
  29. package/dist/types/client.rsc.d.ts +2 -2
  30. package/dist/types/decode-loader-results.d.ts +28 -7
  31. package/dist/types/href-client.d.ts +3 -1
  32. package/dist/types/index.rsc.d.ts +1 -1
  33. package/dist/types/loader-redirect.d.ts +2 -3
  34. package/dist/types/outlet-context.d.ts +7 -6
  35. package/dist/types/prefetch-markers.d.ts +14 -5
  36. package/dist/types/route-content-wrapper.d.ts +17 -14
  37. package/dist/types/route-definition/helpers-types.d.ts +7 -2
  38. package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
  39. package/dist/types/route-definition/rewrite.d.ts +48 -5
  40. package/dist/types/route-types.d.ts +2 -2
  41. package/dist/types/router/handler-context.d.ts +14 -2
  42. package/dist/types/router/intercept-resolution.d.ts +19 -46
  43. package/dist/types/router/lazy-includes.d.ts +7 -0
  44. package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
  45. package/dist/types/router/middleware.d.ts +0 -16
  46. package/dist/types/router/navigation-snapshot.d.ts +2 -0
  47. package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
  48. package/dist/types/router/revalidation.d.ts +8 -1
  49. package/dist/types/router/router-context.d.ts +4 -16
  50. package/dist/types/router/router-interfaces.d.ts +0 -6
  51. package/dist/types/router/router-options.d.ts +4 -7
  52. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
  53. package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
  54. package/dist/types/router/segment-resolution.d.ts +1 -1
  55. package/dist/types/router/segment-wrappers.d.ts +3 -5
  56. package/dist/types/router/substitute-pattern-params.d.ts +19 -2
  57. package/dist/types/router/tracing.d.ts +2 -2
  58. package/dist/types/rsc/full-payload.d.ts +14 -2
  59. package/dist/types/rsc/helpers.d.ts +8 -0
  60. package/dist/types/rsc/manifest-init.d.ts +20 -0
  61. package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
  62. package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
  63. package/dist/types/rsc/rewrite-passes.d.ts +12 -2
  64. package/dist/types/rsc/server-action.d.ts +6 -0
  65. package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
  66. package/dist/types/rsc/shell-capture.d.ts +7 -0
  67. package/dist/types/rsc/types.d.ts +5 -0
  68. package/dist/types/segment-system.d.ts +15 -0
  69. package/dist/types/server/context.d.ts +64 -4
  70. package/dist/types/server/request-context.d.ts +41 -4
  71. package/dist/types/server/request-headers.d.ts +12 -0
  72. package/dist/types/server/rewrite-trail.d.ts +8 -1
  73. package/dist/types/testing/e2e/index.d.ts +1 -1
  74. package/dist/types/testing/flight.entry.d.ts +1 -1
  75. package/dist/types/testing/index.d.ts +1 -1
  76. package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
  77. package/dist/types/testing/render-route.d.ts +21 -11
  78. package/dist/types/testing/run-loader.d.ts +7 -0
  79. package/dist/types/testing/run-middleware.d.ts +7 -0
  80. package/dist/types/testing/run-revalidate.d.ts +5 -4
  81. package/dist/types/testing/serve-shell-request.d.ts +41 -4
  82. package/dist/types/testing/shell-status.d.ts +17 -0
  83. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  84. package/dist/types/testing/vitest.d.ts +10 -1
  85. package/dist/types/thenable-status.d.ts +0 -6
  86. package/dist/types/types/handler-context.d.ts +12 -1
  87. package/dist/types/types/loader-types.d.ts +4 -3
  88. package/dist/types/types/route-entry.d.ts +2 -1
  89. package/dist/types/urls/path-helper-types.d.ts +4 -2
  90. package/dist/types/urls/path-helper.d.ts +5 -0
  91. package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
  92. package/dist/types/vite/discovery/state.d.ts +20 -0
  93. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
  94. package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
  95. package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
  96. package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
  97. package/dist/vite/index.js +6816 -6547
  98. package/package.json +1 -1
  99. package/skills/app-structure/SKILL.md +255 -0
  100. package/skills/app-structure/content-site.md +333 -0
  101. package/skills/app-structure/e-commerce.md +435 -0
  102. package/skills/app-structure/large-app.md +885 -0
  103. package/skills/app-structure/multi-app.md +280 -0
  104. package/skills/breadcrumbs/SKILL.md +6 -1
  105. package/skills/caching/SKILL.md +3 -1
  106. package/skills/catalog.json +12 -0
  107. package/skills/client-urls/SKILL.md +195 -32
  108. package/skills/composability/SKILL.md +10 -8
  109. package/skills/handler-use/SKILL.md +48 -9
  110. package/skills/hooks/handle-and-actions.md +104 -0
  111. package/skills/hooks/navigation.md +24 -10
  112. package/skills/hooks/urls.md +1 -1
  113. package/skills/i18n/SKILL.md +2 -1
  114. package/skills/intercept/SKILL.md +60 -29
  115. package/skills/layout/SKILL.md +6 -3
  116. package/skills/links/SKILL.md +151 -17
  117. package/skills/loader/SKILL.md +32 -10
  118. package/skills/middleware/SKILL.md +111 -15
  119. package/skills/observability/SKILL.md +11 -0
  120. package/skills/parallel/SKILL.md +5 -5
  121. package/skills/ppr/SKILL.md +43 -8
  122. package/skills/prerender/SKILL.md +1 -2
  123. package/skills/rango/SKILL.md +3 -0
  124. package/skills/response-routes/SKILL.md +4 -1
  125. package/skills/scripts/SKILL.md +12 -1
  126. package/skills/server-actions/SKILL.md +60 -20
  127. package/skills/testing/SKILL.md +1 -0
  128. package/skills/testing/client-components.md +13 -10
  129. package/skills/testing/loader.md +25 -24
  130. package/skills/testing/middleware.md +11 -11
  131. package/skills/testing/response-routes.md +4 -1
  132. package/skills/testing/serve-shell-request.md +85 -21
  133. package/skills/ux-architecture/SKILL.md +980 -0
  134. package/skills/view-transitions/SKILL.md +43 -10
  135. package/src/__augment-tests__/augmented.check.ts +28 -0
  136. package/src/browser/event-controller.ts +44 -18
  137. package/src/browser/intercept-utils.ts +3 -2
  138. package/src/browser/merge-segment-loaders.ts +0 -85
  139. package/src/browser/navigation-bridge.ts +119 -98
  140. package/src/browser/navigation-client.ts +20 -15
  141. package/src/browser/navigation-store.ts +4 -2
  142. package/src/browser/navigation-transaction.ts +9 -8
  143. package/src/browser/partial-update.ts +90 -32
  144. package/src/browser/prefetch/cache.ts +8 -0
  145. package/src/browser/prefetch/fetch.ts +13 -3
  146. package/src/browser/rango-marker.ts +13 -0
  147. package/src/browser/react/NavigationProvider.tsx +25 -7
  148. package/src/browser/react/filter-segment-order.ts +13 -0
  149. package/src/browser/react/use-action.ts +9 -15
  150. package/src/browser/react/use-handle.ts +63 -71
  151. package/src/browser/react/use-href.tsx +24 -5
  152. package/src/browser/react/use-link-status.ts +1 -1
  153. package/src/browser/react/use-navigation.ts +30 -40
  154. package/src/browser/react/use-reverse.ts +4 -17
  155. package/src/browser/react/use-router.ts +37 -6
  156. package/src/browser/react/use-segments.ts +8 -12
  157. package/src/browser/response-adapter.ts +11 -5
  158. package/src/browser/rsc-router.tsx +3 -1
  159. package/src/browser/scroll-restoration.ts +34 -20
  160. package/src/browser/segment-reconciler.ts +11 -29
  161. package/src/browser/server-action-bridge.ts +52 -16
  162. package/src/browser/types.ts +40 -8
  163. package/src/build/generate-manifest.ts +12 -1
  164. package/src/build/route-types/codegen.ts +6 -2
  165. package/src/build/route-types/per-module-writer.ts +94 -61
  166. package/src/cache/cache-runtime.ts +9 -0
  167. package/src/cache/cache-scope.ts +2 -1
  168. package/src/cache/cf/cf-cache-store.ts +2 -0
  169. package/src/cache/cf/cf-shell-frame.ts +3 -0
  170. package/src/cache/types.ts +8 -0
  171. package/src/cache/vercel/vercel-cache-store.ts +6 -1
  172. package/src/client-urls/client-root.tsx +10 -5
  173. package/src/client-urls/client-urls.ts +246 -166
  174. package/src/client-urls/navigation.ts +45 -38
  175. package/src/client-urls/revalidate-registry.ts +12 -3
  176. package/src/client-urls/server-projection.ts +286 -75
  177. package/src/client-urls/types.ts +40 -16
  178. package/src/client.rsc.tsx +11 -1
  179. package/src/client.tsx +9 -1
  180. package/src/decode-loader-results.ts +64 -31
  181. package/src/href-client.ts +3 -1
  182. package/src/index.rsc.ts +1 -0
  183. package/src/index.ts +12 -4
  184. package/src/loader-redirect.tsx +2 -3
  185. package/src/outlet-context.ts +7 -6
  186. package/src/prefetch-markers.ts +15 -5
  187. package/src/reverse.ts +3 -3
  188. package/src/route-content-wrapper.tsx +19 -17
  189. package/src/route-definition/dsl-helpers.ts +27 -7
  190. package/src/route-definition/helpers-types.ts +7 -2
  191. package/src/route-definition/resolve-handler-use.ts +23 -10
  192. package/src/route-definition/rewrite.ts +86 -19
  193. package/src/route-types.ts +1 -2
  194. package/src/router/find-match.ts +37 -1
  195. package/src/router/handler-context.ts +85 -40
  196. package/src/router/intercept-resolution.ts +161 -307
  197. package/src/router/lazy-includes.ts +43 -27
  198. package/src/router/loader-resolution.ts +21 -1
  199. package/src/router/manifest.ts +6 -8
  200. package/src/router/match-api.ts +11 -1
  201. package/src/router/match-middleware/background-revalidation.ts +2 -4
  202. package/src/router/match-middleware/cache-lookup.ts +16 -20
  203. package/src/router/match-middleware/intercept-resolution.ts +41 -60
  204. package/src/router/match-result.ts +31 -2
  205. package/src/router/middleware.ts +6 -147
  206. package/src/router/navigation-snapshot.ts +38 -2
  207. package/src/router/prefetch-cache-ttl.ts +10 -18
  208. package/src/router/prerender-match.ts +2 -1
  209. package/src/router/revalidation.ts +24 -3
  210. package/src/router/router-context.ts +5 -18
  211. package/src/router/router-interfaces.ts +0 -7
  212. package/src/router/router-options.ts +4 -7
  213. package/src/router/segment-resolution/fresh.ts +6 -2
  214. package/src/router/segment-resolution/helpers.ts +7 -1
  215. package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
  216. package/src/router/segment-resolution/revalidation.ts +75 -35
  217. package/src/router/segment-resolution.ts +0 -1
  218. package/src/router/segment-wrappers.ts +8 -13
  219. package/src/router/substitute-pattern-params.ts +37 -2
  220. package/src/router/tracing.ts +2 -2
  221. package/src/router.ts +2 -9
  222. package/src/rsc/full-payload.ts +26 -4
  223. package/src/rsc/handler.ts +83 -41
  224. package/src/rsc/helpers.ts +13 -0
  225. package/src/rsc/manifest-init.ts +56 -3
  226. package/src/rsc/prefetch-render-error.ts +38 -0
  227. package/src/rsc/progressive-enhancement.ts +161 -211
  228. package/src/rsc/rewrite-passes.ts +56 -9
  229. package/src/rsc/rsc-rendering.ts +79 -23
  230. package/src/rsc/server-action.ts +25 -2
  231. package/src/rsc/shell-capture-constants.ts +22 -0
  232. package/src/rsc/shell-capture.ts +11 -0
  233. package/src/rsc/types.ts +5 -0
  234. package/src/segment-system.tsx +125 -119
  235. package/src/server/context.ts +96 -5
  236. package/src/server/handle-store.ts +3 -2
  237. package/src/server/request-context.ts +42 -3
  238. package/src/server/request-headers.ts +32 -0
  239. package/src/server/rewrite-trail.ts +55 -9
  240. package/src/ssr/ssr-root.tsx +6 -10
  241. package/src/testing/dispatch.ts +4 -5
  242. package/src/testing/e2e/index.ts +1 -0
  243. package/src/testing/flight.entry.ts +1 -0
  244. package/src/testing/index.ts +1 -0
  245. package/src/testing/internal/client-urls-projection.ts +31 -11
  246. package/src/testing/render-route.tsx +83 -40
  247. package/src/testing/run-loader.ts +26 -2
  248. package/src/testing/run-middleware.ts +10 -0
  249. package/src/testing/run-revalidate.ts +13 -6
  250. package/src/testing/serve-shell-request.ts +77 -4
  251. package/src/testing/shell-status.ts +40 -0
  252. package/src/testing/vitest-stubs/version.ts +1 -0
  253. package/src/testing/vitest.ts +74 -7
  254. package/src/thenable-status.ts +0 -11
  255. package/src/types/handler-context.ts +12 -1
  256. package/src/types/loader-types.ts +4 -3
  257. package/src/types/route-entry.ts +2 -1
  258. package/src/urls/include-helper.ts +38 -11
  259. package/src/urls/path-helper-types.ts +4 -2
  260. package/src/urls/path-helper.ts +16 -3
  261. package/src/vite/discovery/client-urls-projection.ts +205 -16
  262. package/src/vite/discovery/discover-routers.ts +2 -2
  263. package/src/vite/discovery/prerender-collection.ts +2 -6
  264. package/src/vite/discovery/state.ts +26 -0
  265. package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
  266. package/src/vite/plugins/expose-internal-ids.ts +46 -40
  267. package/src/vite/plugins/hoist/source.ts +8 -7
  268. package/src/vite/plugins/transition-when-hoist.ts +1 -0
  269. package/src/vite/plugins/version-plugin.ts +7 -0
  270. package/src/vite/plugins/version.d.ts +7 -0
  271. package/src/vite/plugins/virtual-entries.ts +20 -0
  272. package/src/vite/rango.ts +4 -1
  273. package/src/vite/router-discovery.ts +39 -7
  274. 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
 
@@ -457,20 +457,13 @@ export function createNavigationBridge(
457
457
  ).filter((segment) => segment.clientGroup === undefined),
458
458
  options?.replace ? "replace" : "push",
459
459
  {
460
- to: (from) => {
461
- // Mount (include prefix) params carry over; the origin
462
- // route's own params do not.
463
- const params = { ...from.params };
464
- for (const name of destination.originLocalParamNames) {
465
- delete params[name];
466
- }
467
- return {
468
- url: targetUrl,
469
- params: { ...params, ...destination.params },
470
- routeName: destination.routeName,
471
- state: destination.state,
472
- };
473
- },
460
+ // The destination's params include the mount's.
461
+ to: () => ({
462
+ url: targetUrl,
463
+ params: destination.params,
464
+ routeName: destination.routeName,
465
+ state: destination.state,
466
+ }),
474
467
  extra: destination.when,
475
468
  },
476
469
  ),
@@ -635,7 +628,8 @@ export function createNavigationBridge(
635
628
  * Handle browser back/forward navigation
636
629
  * Uses cached segments when available for instant restoration
637
630
  */
638
- async handlePopstate(): Promise<void> {
631
+ async handlePopstate(event?: PopStateEvent): Promise<void> {
632
+ const generation = ++popGeneration;
639
633
  // Abort any pending navigation to prevent race conditions, and the
640
634
  // fill of the page being left (prefetch: false), which its committed
641
635
  // navigation still owns.
@@ -745,18 +739,6 @@ export function createNavigationBridge(
745
739
  forceAwait: true,
746
740
  transitionGatedOff: gatedOff,
747
741
  });
748
- store.rememberDisplayedEntry();
749
- // The restored entry's location and params, set before onUpdate so
750
- // the hooks take both in the batch that renders its tree. Not at
751
- // the popstate event: the page being left is on screen until here
752
- // (#1031). A restore that has to fetch gets its location from
753
- // tx.commit() instead (navigation-transaction.ts).
754
- eventController.setLocation(new URL(url));
755
- eventController.setParams(cachedParams);
756
- // The entry this handler found at the event, which the cached tree
757
- // was built for: history may have moved during the render above.
758
- eventController.commitLocationState(historyState);
759
-
760
742
  const popstateUpdate = {
761
743
  root,
762
744
  metadata: {
@@ -778,68 +760,106 @@ export function createNavigationBridge(
778
760
  !isIntercept &&
779
761
  !isLeavingIntercept &&
780
762
  shouldStartViewTransition(cachedSegments, gatedOff);
781
- if (process.env.NODE_ENV !== "production") auditTreeCause("popstate");
782
- if (hasTransition) {
783
- startTransition(() => {
784
- if (addTransitionType) {
785
- addTransitionType("navigation-back");
786
- }
787
- onUpdate(popstateUpdate);
788
- });
789
- } else {
790
- onUpdate(popstateUpdate);
791
- }
792
-
793
- // SWR: If stale, trigger background revalidation
794
- if (isStale) {
795
- debugLog("[Browser] Cache is stale, background revalidating...");
796
- // Background revalidation - don't await, just fire and forget.
797
- // When the entry's handles are incomplete (a deferred Meta was still
798
- // pending when the user navigated away — see handlesPending), send NO
799
- // segment IDs so the server returns a FULL re-render with the handle
800
- // stream. A normal stale revalidation sends the cached IDs and the
801
- // server returns a diff-only payload that omits unchanged segments'
802
- // handles, so a deferred Meta would never re-stream and the title
803
- // would stay the pre-resolution carry. handlesPending is set only for
804
- // the deferred-Meta-aborted case, so action/cross-tab SWR keeps the
805
- // cheap diff path.
806
- const segmentIds = cached?.handlesPending
807
- ? []
808
- : cachedSegments.map((s) => s.id);
809
-
810
- const tx = createNavigationTransaction(
811
- store,
812
- eventController,
813
- url,
814
- { skipLoadingState: true, replace: true },
815
- );
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
+ );
816
828
 
817
- fetchPartialUpdate(
818
- url,
819
- segmentIds,
820
- false,
821
- tx.handle.signal,
822
- tx.with({
829
+ fetchPartialUpdate(
823
830
  url,
824
- replace: true,
825
- scroll: false,
826
- intercept: isIntercept,
827
- interceptSourceUrl,
828
- cacheOnly: true,
829
- }),
830
- { type: "stale-revalidation", interceptSourceUrl },
831
- )
832
- .catch((error) => {
833
- if (isBackgroundSuppressible(error)) return;
834
- console.error(
835
- "[Browser] Background revalidation failed:",
836
- error,
837
- );
838
- })
839
- .finally(() => {
840
- tx[Symbol.dispose]();
841
- });
842
- }
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();
843
863
  return;
844
864
  } catch (error) {
845
865
  console.warn(
@@ -905,8 +925,9 @@ export function createNavigationBridge(
905
925
  // tx.commit() never ran, so the location is still the page being
906
926
  // left. The error boundary below replaces that page, and history is
907
927
  // already on the entry that failed: the hooks report that entry, in
908
- // the boundary's batch.
928
+ // the boundary's batch, and scroll restoration tracks it as shown.
909
929
  eventController.setLocation(new URL(url));
930
+ ensureHistoryKey();
910
931
 
911
932
  const networkError = toNetworkError(error, {
912
933
  url,
@@ -939,8 +960,8 @@ export function createNavigationBridge(
939
960
  this.navigate(url, options);
940
961
  });
941
962
 
942
- const handlePopstate = () => {
943
- this.handlePopstate();
963
+ const handlePopstate = (event: PopStateEvent) => {
964
+ this.handlePopstate(event);
944
965
  };
945
966
 
946
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
  },