@rangojs/router 0.20.0 → 0.22.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 (331) hide show
  1. package/dist/bin/rango.js +39 -15
  2. package/dist/types/browser/event-controller.d.ts +49 -0
  3. package/dist/types/browser/history-state.d.ts +16 -4
  4. package/dist/types/browser/navigation-store.d.ts +9 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +7 -0
  6. package/dist/types/browser/partial-update.d.ts +2 -1
  7. package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
  8. package/dist/types/browser/rango-state.d.ts +3 -1
  9. package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
  10. package/dist/types/browser/react/context.d.ts +25 -0
  11. package/dist/types/browser/react/location-state-shared.d.ts +84 -10
  12. package/dist/types/browser/react/location-state.d.ts +7 -2
  13. package/dist/types/browser/rsc-router.d.ts +13 -2
  14. package/dist/types/browser/types.d.ts +10 -0
  15. package/dist/types/build/generate-manifest.d.ts +2 -0
  16. package/dist/types/cache/background-task.d.ts +22 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +50 -5
  18. package/dist/types/cache/cache-policy.d.ts +17 -0
  19. package/dist/types/cache/cache-scope.d.ts +5 -6
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +34 -7
  21. package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
  22. package/dist/types/cache/document-cache.d.ts +8 -5
  23. package/dist/types/cache/handle-snapshot.d.ts +59 -50
  24. package/dist/types/cache/index.d.ts +3 -1
  25. package/dist/types/cache/invalidation-order.d.ts +14 -0
  26. package/dist/types/cache/memory-segment-store.d.ts +33 -1
  27. package/dist/types/cache/shell-snapshot.d.ts +29 -4
  28. package/dist/types/cache/store-scope.d.ts +22 -0
  29. package/dist/types/cache/tag-invalidation.d.ts +7 -6
  30. package/dist/types/cache/types.d.ts +84 -24
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +35 -17
  32. package/dist/types/client-urls/navigation.d.ts +7 -1
  33. package/dist/types/client-urls/optimistic-location.d.ts +10 -4
  34. package/dist/types/errors.d.ts +6 -0
  35. package/dist/types/handles/deferred-resolution.d.ts +18 -1
  36. package/dist/types/internal-suspense-audit.d.ts +1 -0
  37. package/dist/types/loader-store.d.ts +4 -8
  38. package/dist/types/outlet-provider.d.ts +2 -1
  39. package/dist/types/prerender/build-shell-capture.d.ts +8 -6
  40. package/dist/types/prerender/cloudflare.d.ts +49 -0
  41. package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
  42. package/dist/types/prerender/index.d.ts +12 -0
  43. package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
  44. package/dist/types/prerender/on-demand.d.ts +272 -0
  45. package/dist/types/prerender/producer-guard.d.ts +33 -0
  46. package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
  47. package/dist/types/prerender/warm-request.d.ts +65 -0
  48. package/dist/types/prerender/warm.d.ts +65 -0
  49. package/dist/types/prerender/writable-store.d.ts +170 -0
  50. package/dist/types/prerender.d.ts +27 -0
  51. package/dist/types/reverse.d.ts +1 -1
  52. package/dist/types/route-content-wrapper.d.ts +3 -1
  53. package/dist/types/router/error-handling.d.ts +2 -0
  54. package/dist/types/router/handler-context.d.ts +1 -1
  55. package/dist/types/router/match-middleware/cache-lookup.d.ts +15 -1
  56. package/dist/types/router/navigation-snapshot.d.ts +8 -1
  57. package/dist/types/router/pattern-matching.d.ts +2 -0
  58. package/dist/types/router/prefetch-default.d.ts +4 -4
  59. package/dist/types/router/prerender-match.d.ts +23 -1
  60. package/dist/types/router/route-trie-builder.d.ts +8 -1
  61. package/dist/types/router/router-context.d.ts +1 -0
  62. package/dist/types/router/router-interfaces.d.ts +32 -1
  63. package/dist/types/router/router-options.d.ts +39 -5
  64. package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
  65. package/dist/types/router/segment-resolution/loader-cache.d.ts +23 -10
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
  67. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
  68. package/dist/types/router/trie-matching.d.ts +2 -0
  69. package/dist/types/router-versions.d.ts +39 -0
  70. package/dist/types/rsc/handler-context.d.ts +5 -0
  71. package/dist/types/rsc/shell-build-manifest.d.ts +9 -6
  72. package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
  73. package/dist/types/rsc/shell-capture.d.ts +14 -1
  74. package/dist/types/rsc/shell-serve.d.ts +2 -2
  75. package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
  76. package/dist/types/rsc/types.d.ts +12 -6
  77. package/dist/types/segment-boundary-content.d.ts +17 -0
  78. package/dist/types/segment-loader-promise.d.ts +2 -6
  79. package/dist/types/server/build-version-table.d.ts +53 -0
  80. package/dist/types/server/context.d.ts +12 -4
  81. package/dist/types/server/handle-store.d.ts +89 -46
  82. package/dist/types/server/request-context.d.ts +84 -9
  83. package/dist/types/suspense-audit-react.d.ts +26 -0
  84. package/dist/types/suspense-audit.d.ts +157 -0
  85. package/dist/types/testing/action-form.d.ts +34 -0
  86. package/dist/types/testing/build-versions.d.ts +32 -0
  87. package/dist/types/testing/dom.entry.d.ts +1 -1
  88. package/dist/types/testing/flight.entry.d.ts +3 -1
  89. package/dist/types/testing/index.d.ts +6 -0
  90. package/dist/types/testing/internal/seed-vars.d.ts +7 -2
  91. package/dist/types/testing/internal/server-modules.d.ts +8 -0
  92. package/dist/types/testing/location-state-key.d.ts +5 -1
  93. package/dist/types/testing/render-route.d.ts +77 -6
  94. package/dist/types/testing/serve-shell-request.d.ts +55 -4
  95. package/dist/types/testing/shell-status.d.ts +14 -6
  96. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
  97. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  98. package/dist/types/thenable-status.d.ts +6 -0
  99. package/dist/types/types/handler-context.d.ts +15 -5
  100. package/dist/types/types/route-entry.d.ts +5 -0
  101. package/dist/types/urls/path-helper.d.ts +18 -0
  102. package/dist/types/vite/discovery/build-versions.d.ts +393 -0
  103. package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
  104. package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
  105. package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
  106. package/dist/types/vite/discovery/state.d.ts +25 -0
  107. package/dist/types/vite/encryption-key.d.ts +18 -0
  108. package/dist/types/vite/inject-client-debug.d.ts +6 -25
  109. package/dist/types/vite/plugin-types.d.ts +27 -0
  110. package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
  111. package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
  112. package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
  113. package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
  114. package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
  115. package/dist/types/vite/plugins/virtual-entries.d.ts +18 -1
  116. package/dist/types/vite/router-discovery.d.ts +6 -0
  117. package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
  118. package/dist/types/vite/utils/client-chunks.d.ts +2 -0
  119. package/dist/types/vite/utils/compare-strings.d.ts +7 -0
  120. package/dist/types/vite/utils/package-resolution.d.ts +8 -0
  121. package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
  122. package/dist/vite/index.js +1707 -931
  123. package/package.json +12 -3
  124. package/skills/cache-guide/SKILL.md +102 -37
  125. package/skills/caching/SKILL.md +56 -9
  126. package/skills/catalog.json +1 -1
  127. package/skills/client-urls/SKILL.md +16 -0
  128. package/skills/cloudflare/SKILL.md +133 -1
  129. package/skills/comparison/references/framework-comparison.md +17 -13
  130. package/skills/deployment-caching/SKILL.md +33 -12
  131. package/skills/document-cache/SKILL.md +16 -14
  132. package/skills/hooks/SKILL.md +25 -0
  133. package/skills/hooks/handle-and-actions.md +5 -0
  134. package/skills/hooks/navigation.md +38 -0
  135. package/skills/hooks/state.md +213 -11
  136. package/skills/hooks/urls.md +40 -1
  137. package/skills/host-router/SKILL.md +31 -0
  138. package/skills/intercept/SKILL.md +6 -0
  139. package/skills/loader/SKILL.md +25 -5
  140. package/skills/middleware/SKILL.md +2 -0
  141. package/skills/migrate-nextjs/SKILL.md +82 -9
  142. package/skills/ppr/SKILL.md +150 -48
  143. package/skills/prerender/SKILL.md +651 -10
  144. package/skills/rango/SKILL.md +9 -7
  145. package/skills/response-routes/SKILL.md +5 -5
  146. package/skills/route/SKILL.md +29 -0
  147. package/skills/router-setup/SKILL.md +15 -1
  148. package/skills/testing/SKILL.md +32 -27
  149. package/skills/testing/cache-prerender.md +18 -17
  150. package/skills/testing/client-components.md +193 -8
  151. package/skills/testing/handles.md +2 -0
  152. package/skills/testing/loader.md +1 -1
  153. package/skills/testing/middleware.md +1 -1
  154. package/skills/testing/render-handler.md +18 -18
  155. package/skills/testing/serve-shell-request.md +167 -22
  156. package/skills/testing/server-actions.md +17 -17
  157. package/skills/testing/server-tree.md +1 -1
  158. package/skills/typesafety/generated-files-and-cli.md +1 -1
  159. package/skills/use-cache/SKILL.md +25 -10
  160. package/skills/vercel/SKILL.md +5 -3
  161. package/skills/view-transitions/SKILL.md +9 -4
  162. package/src/bin/rango.ts +1 -1
  163. package/src/browser/event-controller.ts +119 -2
  164. package/src/browser/history-state.ts +39 -17
  165. package/src/browser/link-interceptor.ts +2 -2
  166. package/src/browser/navigation-bridge.ts +104 -41
  167. package/src/browser/navigation-store.ts +26 -0
  168. package/src/browser/navigation-transaction.ts +24 -12
  169. package/src/browser/network-error-handler.ts +2 -0
  170. package/src/browser/partial-update.ts +58 -14
  171. package/src/browser/prefetch/default-strategy.ts +22 -42
  172. package/src/browser/rango-state.ts +3 -1
  173. package/src/browser/react/Link.tsx +37 -43
  174. package/src/browser/react/NavigationProvider.tsx +85 -1
  175. package/src/browser/react/context.ts +30 -0
  176. package/src/browser/react/location-state-shared.ts +200 -34
  177. package/src/browser/react/location-state.ts +28 -77
  178. package/src/browser/react/use-action.ts +4 -1
  179. package/src/browser/react/use-handle.ts +3 -0
  180. package/src/browser/react/use-link-status.ts +4 -3
  181. package/src/browser/react/use-navigation.ts +4 -1
  182. package/src/browser/react/use-params.ts +4 -1
  183. package/src/browser/react/use-pathname.ts +4 -1
  184. package/src/browser/react/use-search-params.ts +5 -3
  185. package/src/browser/react/use-segments.ts +36 -54
  186. package/src/browser/rsc-router.tsx +93 -31
  187. package/src/browser/segment-reconciler.ts +3 -3
  188. package/src/browser/server-action-bridge.ts +43 -7
  189. package/src/browser/types.ts +11 -0
  190. package/src/build/generate-manifest.ts +17 -0
  191. package/src/build/merge-full-manifests.ts +5 -0
  192. package/src/build/route-trie.ts +1 -0
  193. package/src/cache/background-task.ts +61 -0
  194. package/src/cache/cache-key-utils.ts +91 -12
  195. package/src/cache/cache-policy.ts +11 -2
  196. package/src/cache/cache-runtime.ts +23 -23
  197. package/src/cache/cache-scope.ts +48 -36
  198. package/src/cache/cf/cf-cache-store.ts +91 -66
  199. package/src/cache/cf/cf-cache-types.ts +12 -4
  200. package/src/cache/document-cache.ts +35 -14
  201. package/src/cache/handle-snapshot.ts +118 -86
  202. package/src/cache/index.ts +5 -0
  203. package/src/cache/invalidation-order.ts +53 -0
  204. package/src/cache/memory-segment-store.ts +52 -1
  205. package/src/cache/shell-snapshot.ts +52 -5
  206. package/src/cache/store-scope.ts +36 -0
  207. package/src/cache/tag-invalidation.ts +7 -5
  208. package/src/cache/types.ts +90 -24
  209. package/src/cache/vercel/vercel-cache-store.ts +68 -26
  210. package/src/client-urls/client-root.tsx +1 -0
  211. package/src/client-urls/navigation.ts +10 -0
  212. package/src/client-urls/optimistic-location.ts +10 -4
  213. package/src/client.tsx +40 -3
  214. package/src/errors.ts +13 -6
  215. package/src/handles/deferred-resolution.ts +36 -0
  216. package/src/internal-suspense-audit.ts +12 -0
  217. package/src/loader-store.ts +4 -9
  218. package/src/outlet-provider.tsx +25 -1
  219. package/src/prerender/build-shell-capture.ts +11 -9
  220. package/src/prerender/cloudflare.ts +130 -0
  221. package/src/prerender/create-prerender-trigger.ts +780 -0
  222. package/src/prerender/index.ts +39 -0
  223. package/src/prerender/memory-prerender-store.ts +115 -0
  224. package/src/prerender/on-demand.ts +339 -0
  225. package/src/prerender/producer-guard.ts +66 -0
  226. package/src/prerender/shell-manifest-key.ts +12 -17
  227. package/src/prerender/warm-request.ts +154 -0
  228. package/src/prerender/warm.ts +193 -0
  229. package/src/prerender/writable-store.ts +278 -0
  230. package/src/prerender.ts +29 -0
  231. package/src/reverse.ts +1 -1
  232. package/src/route-content-wrapper.tsx +107 -13
  233. package/src/router/error-handling.ts +12 -12
  234. package/src/router/find-match.ts +1 -0
  235. package/src/router/handler-context.ts +2 -0
  236. package/src/router/intercept-resolution.ts +15 -8
  237. package/src/router/loader-resolution.ts +2 -10
  238. package/src/router/manifest.ts +2 -0
  239. package/src/router/match-middleware/background-revalidation.ts +2 -0
  240. package/src/router/match-middleware/cache-lookup.ts +239 -54
  241. package/src/router/navigation-snapshot.ts +25 -6
  242. package/src/router/pattern-matching.ts +14 -0
  243. package/src/router/prefetch-default.ts +4 -4
  244. package/src/router/prerender-match.ts +460 -352
  245. package/src/router/request-classification.ts +15 -12
  246. package/src/router/route-trie-builder.ts +9 -0
  247. package/src/router/router-context.ts +1 -0
  248. package/src/router/router-interfaces.ts +35 -1
  249. package/src/router/router-options.ts +45 -5
  250. package/src/router/segment-resolution/fresh.ts +20 -19
  251. package/src/router/segment-resolution/loader-cache.ts +110 -77
  252. package/src/router/segment-resolution/revalidation.ts +34 -24
  253. package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
  254. package/src/router/trie-matching.ts +3 -0
  255. package/src/router-versions.ts +60 -0
  256. package/src/router.ts +107 -1
  257. package/src/rsc/full-payload.ts +6 -2
  258. package/src/rsc/handler-context.ts +5 -0
  259. package/src/rsc/handler.ts +64 -7
  260. package/src/rsc/progressive-enhancement.ts +5 -1
  261. package/src/rsc/response-cache-serve.ts +10 -4
  262. package/src/rsc/rsc-rendering.ts +65 -12
  263. package/src/rsc/shell-build-manifest.ts +17 -12
  264. package/src/rsc/shell-capture-constants.ts +39 -7
  265. package/src/rsc/shell-capture.ts +144 -45
  266. package/src/rsc/shell-serve.ts +12 -6
  267. package/src/rsc/ssr-module-loader.ts +26 -0
  268. package/src/rsc/types.ts +12 -6
  269. package/src/segment-boundary-content.ts +25 -0
  270. package/src/segment-loader-promise.ts +28 -38
  271. package/src/segment-system.tsx +117 -57
  272. package/src/server/build-version-table.ts +130 -0
  273. package/src/server/context.ts +40 -17
  274. package/src/server/cookie-store.ts +5 -0
  275. package/src/server/handle-store.ts +357 -127
  276. package/src/server/request-context.ts +149 -12
  277. package/src/ssr/ssr-root.tsx +32 -13
  278. package/src/suspense-audit-react.tsx +98 -0
  279. package/src/suspense-audit.ts +1030 -0
  280. package/src/testing/action-form.ts +66 -0
  281. package/src/testing/build-versions.ts +64 -0
  282. package/src/testing/dispatch.ts +6 -0
  283. package/src/testing/dom.entry.ts +1 -0
  284. package/src/testing/flight-runtime.d.ts +27 -0
  285. package/src/testing/flight.entry.ts +4 -0
  286. package/src/testing/index.ts +22 -0
  287. package/src/testing/internal/context.ts +2 -1
  288. package/src/testing/internal/seed-vars.ts +14 -2
  289. package/src/testing/internal/server-modules.ts +32 -0
  290. package/src/testing/location-state-key.ts +18 -8
  291. package/src/testing/render-handler.ts +2 -1
  292. package/src/testing/render-route.tsx +293 -46
  293. package/src/testing/serve-shell-request.ts +262 -37
  294. package/src/testing/shell-status.ts +17 -6
  295. package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
  296. package/src/testing/vitest-stubs/version.ts +4 -1
  297. package/src/thenable-status.ts +10 -0
  298. package/src/types/handler-context.ts +15 -5
  299. package/src/types/route-entry.ts +6 -0
  300. package/src/urls/path-helper.ts +69 -9
  301. package/src/use-loader.tsx +57 -30
  302. package/src/vite/discovery/build-versions.ts +1281 -0
  303. package/src/vite/discovery/bundle-postprocess.ts +4 -0
  304. package/src/vite/discovery/discover-routers.ts +15 -0
  305. package/src/vite/discovery/installed-packages.ts +159 -0
  306. package/src/vite/discovery/prerender-collection.ts +38 -1
  307. package/src/vite/discovery/router-versions-phase.ts +336 -0
  308. package/src/vite/discovery/shell-prerender-phase.ts +33 -43
  309. package/src/vite/discovery/state.ts +36 -1
  310. package/src/vite/discovery/virtual-module-codegen.ts +7 -2
  311. package/src/vite/encryption-key.ts +101 -9
  312. package/src/vite/inject-client-debug.ts +42 -20
  313. package/src/vite/plugin-types.ts +28 -0
  314. package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
  315. package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
  316. package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
  317. package/src/vite/plugins/expose-internal-ids.ts +12 -3
  318. package/src/vite/plugins/stable-rsc-output.ts +147 -0
  319. package/src/vite/plugins/version-plugin.ts +19 -24
  320. package/src/vite/plugins/version.d.ts +17 -2
  321. package/src/vite/plugins/virtual-entries.ts +39 -6
  322. package/src/vite/rango.ts +28 -3
  323. package/src/vite/router-discovery.ts +96 -13
  324. package/src/vite/utils/bundle-analysis.ts +39 -7
  325. package/src/vite/utils/client-chunks.ts +5 -1
  326. package/src/vite/utils/compare-strings.ts +9 -0
  327. package/src/vite/utils/package-resolution.ts +9 -0
  328. package/src/vite/utils/prerender-utils.ts +4 -1
  329. package/src/vite/utils/shared-utils.ts +1 -1
  330. package/dist/types/segment-content-promise.d.ts +0 -13
  331. package/src/segment-content-promise.ts +0 -67
@@ -15,6 +15,11 @@ import {
15
15
  filterRouteSegmentIds,
16
16
  } from "./react/filter-segment-order.js";
17
17
  import { notifyListeners } from "./notify-listeners.js";
18
+ import { cloneHandleData } from "./navigation-store.js";
19
+ import {
20
+ locationStateSnapshot,
21
+ type LocationStateSnapshot,
22
+ } from "./react/location-state-shared.js";
18
23
 
19
24
  // Polyfill Symbol.dispose for Safari and older browsers
20
25
  if (typeof Symbol.dispose === "undefined") {
@@ -137,6 +142,16 @@ export interface HandleState {
137
142
  routeSegmentIds: string[];
138
143
  }
139
144
 
145
+ /**
146
+ * What the document's HTML was rendered with, kept for the hydration window
147
+ * (EventController.lockHydration).
148
+ */
149
+ export interface HydrationSnapshot {
150
+ state: DerivedNavigationState;
151
+ params: Record<string, string>;
152
+ handles: HandleState;
153
+ }
154
+
140
155
  /**
141
156
  * Result from starting a navigation
142
157
  * Implements Disposable for use with `using` keyword
@@ -224,7 +239,8 @@ export interface EventController {
224
239
  getActionState(actionId: string): TrackedActionState;
225
240
  getLocation(): NavigationLocation;
226
241
 
227
- // Location updates (for popstate where navigation doesn't go through startNavigation)
242
+ // Location of a commit no transaction completes: a shallow navigation, a
243
+ // back/forward restored from the history cache, a back/forward that failed.
228
244
  setLocation(location: NavigationLocation): void;
229
245
 
230
246
  // Subscriptions
@@ -256,6 +272,31 @@ export interface EventController {
256
272
  resolvedIds?: string[],
257
273
  ): void;
258
274
  getHandleState(): HandleState;
275
+ /**
276
+ * Open the hydration window. Until releaseHydrationLock(), getHandleState()
277
+ * returns the state the document's HTML was rendered with, and handle writes
278
+ * (setHandleData, setRouteSegmentIds) wait in order. initBrowserApp opens it
279
+ * before hydrateRoot, once the document's handle snapshot is applied: a hook
280
+ * reading the handle state when its boundary hydrates, however late, reads
281
+ * what SSR rendered (issue #1035).
282
+ */
283
+ lockHydration(): void;
284
+ /**
285
+ * While the hydration window is open, the state the document's HTML was
286
+ * rendered with; undefined once it closes. A router hook's first render
287
+ * reads it: a component hydrating in a boundary that streamed in late
288
+ * renders what SSR rendered, even after a navigation or an action started.
289
+ * The hook's effect then catches up with the live state, and its
290
+ * subscription stays live, so pending UI is not held back.
291
+ */
292
+ getHydrationSnapshot(): HydrationSnapshot | undefined;
293
+ /**
294
+ * Close the hydration window: apply the writes that waited, in order, and
295
+ * notify handle subscribers. NavigationProvider calls it after the commit
296
+ * that made React hydrate every boundary still dehydrated (its barrier).
297
+ * Idempotent.
298
+ */
299
+ releaseHydrationLock(): void;
259
300
  /**
260
301
  * Update ONLY `routeSegmentIds` (what `useSegments` reads) from `matched`,
261
302
  * leaving `data` and `segmentOrder` (what `useHandle` collects over) untouched.
@@ -269,6 +310,21 @@ export interface EventController {
269
310
  setParams(params: Record<string, string>): void;
270
311
  getParams(): Record<string, string>;
271
312
 
313
+ /**
314
+ * Record the committed history entry's location state from `entryState`:
315
+ * the state object the commit pushed, restored or merged into the entry,
316
+ * so the entry and its readers have one source. Notifies nobody:
317
+ * NavigationProvider takes the snapshot with the entry's payload
318
+ * (LocationStateContext). `treeless` marks a commit no payload follows (a
319
+ * shallow navigation, a commit that keeps every segment, an action's
320
+ * state): the provider takes it with this commit's state notification, in
321
+ * the batch of the location.
322
+ */
323
+ commitLocationState(entryState: unknown, treeless?: boolean): void;
324
+ getLocationState(): LocationStateSnapshot;
325
+ /** True once after a `treeless` commit. */
326
+ takeTreelessLocationState(): boolean;
327
+
272
328
  // Direct state access for advanced use
273
329
  getCurrentNavigation(): NavigationEntry | null;
274
330
  getInflightActions(): Map<string, ActionEntry>;
@@ -460,9 +516,16 @@ export function createEventController(
460
516
  let handleData: HandleData = {};
461
517
  let handleSegmentOrder: string[] = [];
462
518
  let routeSegmentIds: string[] = [];
519
+ // The hydration window (lockHydration): what the document's HTML was
520
+ // rendered with, and the handle writes waiting for its release.
521
+ let hydrationSnapshot: HydrationSnapshot | undefined;
522
+ let writesAfterHydration: (() => void)[] = [];
463
523
 
464
524
  let routeParams: Record<string, string> = {};
465
525
 
526
+ let locationState: LocationStateSnapshot;
527
+ let treelessLocationState = false;
528
+
466
529
  const stateListeners = new Set<StateListener>();
467
530
  const actionListeners = new Map<string, Set<ActionStateListener>>();
468
531
  const handleListeners = new Set<HandleListener>();
@@ -985,6 +1048,12 @@ export function createEventController(
985
1048
  isPartial?: boolean,
986
1049
  resolvedIds?: string[],
987
1050
  ): void {
1051
+ if (hydrationSnapshot) {
1052
+ writesAfterHydration.push(() =>
1053
+ setHandleData(data, matched, isPartial, resolvedIds),
1054
+ );
1055
+ return;
1056
+ }
988
1057
  const rawMatched = matched ?? [];
989
1058
  const newSegmentOrder = filterSegmentOrder(rawMatched);
990
1059
  // Separate list for useSegments(): "layouts and routes only" — strip
@@ -992,7 +1061,10 @@ export function createEventController(
992
1061
  const newRouteSegmentIds = filterRouteSegmentIds(rawMatched);
993
1062
 
994
1063
  if (isPartial && newSegmentOrder.length > 0) {
995
- // Partial update: merge new data with existing
1064
+ // Partial update: merge new data with existing, into a copy of the
1065
+ // containers. The object a reader was handed is never written to: a
1066
+ // history entry restored from the store's cache can be that object.
1067
+ handleData = cloneHandleData(handleData);
996
1068
  for (const handleName of Object.keys(data)) {
997
1069
  if (!handleData[handleName]) {
998
1070
  handleData[handleName] = {};
@@ -1029,6 +1101,7 @@ export function createEventController(
1029
1101
  }
1030
1102
 
1031
1103
  function getHandleState(): HandleState {
1104
+ if (hydrationSnapshot) return hydrationSnapshot.handles;
1032
1105
  return {
1033
1106
  data: handleData,
1034
1107
  segmentOrder: handleSegmentOrder,
@@ -1036,7 +1109,29 @@ export function createEventController(
1036
1109
  };
1037
1110
  }
1038
1111
 
1112
+ function lockHydration(): void {
1113
+ if (hydrationSnapshot) return;
1114
+ hydrationSnapshot = {
1115
+ state: getState(),
1116
+ params: routeParams,
1117
+ handles: getHandleState(),
1118
+ };
1119
+ }
1120
+
1121
+ function releaseHydrationLock(): void {
1122
+ if (!hydrationSnapshot) return;
1123
+ hydrationSnapshot = undefined;
1124
+ const writes = writesAfterHydration;
1125
+ writesAfterHydration = [];
1126
+ for (const write of writes) write();
1127
+ notifyHandles.flush();
1128
+ }
1129
+
1039
1130
  function setRouteSegmentIds(matched: string[]): void {
1131
+ if (hydrationSnapshot) {
1132
+ writesAfterHydration.push(() => setRouteSegmentIds(matched));
1133
+ return;
1134
+ }
1040
1135
  const next = filterRouteSegmentIds(matched);
1041
1136
  if (
1042
1137
  next.length === routeSegmentIds.length &&
@@ -1094,6 +1189,20 @@ export function createEventController(
1094
1189
  return routeParams;
1095
1190
  }
1096
1191
 
1192
+ function commitLocationState(entryState: unknown, treeless?: boolean): void {
1193
+ locationState = locationStateSnapshot(entryState, locationState);
1194
+ if (treeless) {
1195
+ treelessLocationState = true;
1196
+ notify();
1197
+ }
1198
+ }
1199
+
1200
+ function takeTreelessLocationState(): boolean {
1201
+ const taken = treelessLocationState;
1202
+ treelessLocationState = false;
1203
+ return taken;
1204
+ }
1205
+
1097
1206
  // ========================================================================
1098
1207
  // Return Controller
1099
1208
  // ========================================================================
@@ -1116,12 +1225,20 @@ export function createEventController(
1116
1225
  // Handles
1117
1226
  setHandleData,
1118
1227
  getHandleState,
1228
+ lockHydration,
1229
+ getHydrationSnapshot: () => hydrationSnapshot,
1230
+ releaseHydrationLock,
1119
1231
  setRouteSegmentIds,
1120
1232
 
1121
1233
  // Params
1122
1234
  setParams,
1123
1235
  getParams,
1124
1236
 
1237
+ // Location state
1238
+ commitLocationState,
1239
+ getLocationState: () => locationState,
1240
+ takeTreelessLocationState,
1241
+
1125
1242
  // Subscriptions
1126
1243
  subscribe,
1127
1244
  subscribeToAction,
@@ -1,9 +1,13 @@
1
1
  import {
2
+ addLocationState,
3
+ CLEAR_ON_RELOAD_KEY_SUFFIX,
2
4
  isLocationStateDefinition,
3
5
  isLocationStateEntry,
6
+ isLocationStateKey,
4
7
  peekLocationStateKey,
5
8
  replaceCurrentHistoryState,
6
9
  resolveLocationStateEntries,
10
+ stampLocationState,
7
11
  } from "./react/location-state-shared.js";
8
12
 
9
13
  /**
@@ -13,7 +17,7 @@ function isTypedLocationState(
13
17
  state: unknown,
14
18
  ): state is Record<string, unknown> {
15
19
  if (state === null || typeof state !== "object") return false;
16
- return Object.keys(state).some((key) => key.startsWith("__rsc_ls_"));
20
+ return Object.keys(state).some(isLocationStateKey);
17
21
  }
18
22
 
19
23
  /**
@@ -100,16 +104,15 @@ export function buildHistoryState(
100
104
  Object.assign(result, serverState);
101
105
  }
102
106
 
107
+ if (hasLocationState(result)) stampLocationState(result);
108
+
103
109
  return Object.keys(result).length > 0 ? result : null;
104
110
  }
105
111
 
106
112
  /** Check if a history state object contains location state keys. */
107
113
  export function hasLocationState(state: unknown): boolean {
108
114
  if (!state || typeof state !== "object") return false;
109
- return (
110
- "state" in state ||
111
- Object.keys(state).some((k) => k.startsWith("__rsc_ls_"))
112
- );
115
+ return "state" in state || Object.keys(state).some(isLocationStateKey);
113
116
  }
114
117
 
115
118
  /**
@@ -157,19 +160,38 @@ export function stripShellMissMarker(): void {
157
160
  }
158
161
 
159
162
  /**
160
- * Merge server-set location state into the current history entry.
161
- * Replaces the current history state and dispatches notification event
162
- * so useLocationState hooks re-read from history.state.
163
+ * Remove the `clearOnReload` slots of the entry a document load starts on:
164
+ * the server rendered this document without them. One pass before hydration
165
+ * (initBrowserApp; renderRoute's `hydrate` mode), so no reader needs to know
166
+ * the page began as a document load, and a slot nobody reads is removed too.
167
+ * No server-set state exists yet to lose: a document response carries none
168
+ * (rsc-rendering.ts attaches it to partial payloads only).
169
+ *
170
+ * A slot is recognised by its key alone (the `~r` suffix): at start-up the
171
+ * module that defines it may not be loaded.
172
+ */
173
+ export function clearLocationStateOnDocumentLoad(): void {
174
+ const state: unknown = window.history.state;
175
+ if (state === null || typeof state !== "object") return;
176
+ let next: Record<string, unknown> | undefined;
177
+ for (const key in state) {
178
+ if (isLocationStateKey(key) && key.endsWith(CLEAR_ON_RELOAD_KEY_SUFFIX)) {
179
+ next ??= { ...state };
180
+ delete next[key];
181
+ }
182
+ }
183
+ if (next) window.history.replaceState(next, "");
184
+ }
185
+
186
+ /**
187
+ * Merge server-set location state into the current history entry and return
188
+ * the entry state written. Readers see it when the caller commits it
189
+ * (EventController.commitLocationState).
163
190
  */
164
191
  export function mergeLocationState(
165
192
  locationState: Record<string, unknown>,
166
- ): void {
167
- const merged = {
168
- ...window.history.state,
169
- ...locationState,
170
- };
171
- replaceCurrentHistoryState(merged);
172
- if (Object.keys(locationState).some((k) => k.startsWith("__rsc_ls_"))) {
173
- window.dispatchEvent(new Event("__rsc_locationstate"));
174
- }
193
+ ): Record<string, unknown> {
194
+ const entryState = addLocationState(window.history.state, locationState);
195
+ replaceCurrentHistoryState(entryState);
196
+ return entryState;
175
197
  }
@@ -4,7 +4,7 @@ import {
4
4
  type EventController,
5
5
  } from "./event-controller.js";
6
6
  import {
7
- getDefaultPrefetchStrategy,
7
+ ENVIRONMENT_DEFAULT_PREFETCH,
8
8
  resolveAdaptiveStrategy,
9
9
  subscribeToAdaptiveStrategyChange,
10
10
  } from "./prefetch/default-strategy.js";
@@ -383,7 +383,7 @@ export function setupDelegatedLinkPrefetch(
383
383
  options: DelegatedPrefetchOptions,
384
384
  ): () => void {
385
385
  const defaultStrategy =
386
- options.defaultPrefetch ?? getDefaultPrefetchStrategy();
386
+ options.defaultPrefetch ?? ENVIRONMENT_DEFAULT_PREFETCH;
387
387
  if (defaultStrategy === "none") return () => {};
388
388
 
389
389
  const basename = canonicalizeBasenamePathname(options.basename);
@@ -14,11 +14,7 @@ import {
14
14
  createNavigationTransaction,
15
15
  resolveNavigationState,
16
16
  } from "./navigation-transaction.js";
17
- import {
18
- buildHistoryState,
19
- hasLocationState,
20
- pushHistoryWithIdx,
21
- } from "./history-state.js";
17
+ import { buildHistoryState, pushHistoryWithIdx } from "./history-state.js";
22
18
  import {
23
19
  handleNavigationStart,
24
20
  handleNavigationEnd,
@@ -39,7 +35,10 @@ import {
39
35
  shouldStartViewTransition,
40
36
  } from "./partial-update.js";
41
37
  import { decideCommitGatedOff, mergeSegmentParams } from "./transition-when.js";
42
- import { generateHistoryKey } from "./navigation-store.js";
38
+ import {
39
+ generateHistoryKey,
40
+ liveHandleDataForCache,
41
+ } from "./navigation-store.js";
43
42
  import type { EventController } from "./event-controller.js";
44
43
  import { isInterceptOnlyCache } from "./intercept-utils.js";
45
44
  import {
@@ -58,6 +57,7 @@ import {
58
57
  } from "./prefetch/loader.js";
59
58
  import type { PrefetchStrategy } from "../router/prefetch-default.js";
60
59
  import { beginClientUrlNavigation } from "../client-urls/navigation.js";
60
+ import { auditTreeCause } from "../suspense-audit.js";
61
61
 
62
62
  // Polyfill Symbol.dispose for Safari and older browsers
63
63
  if (typeof Symbol.dispose === "undefined") {
@@ -66,6 +66,9 @@ if (typeof Symbol.dispose === "undefined") {
66
66
 
67
67
  export { createNavigationTransaction };
68
68
 
69
+ // Mirrors the redirect-chain limit browsers apply to document navigations.
70
+ const MAX_REDIRECT_HOPS = 20;
71
+
69
72
  /**
70
73
  * Extended configuration for navigation bridge with event controller
71
74
  */
@@ -153,6 +156,45 @@ export function createNavigationBridge(
153
156
  getVersion: () => version,
154
157
  });
155
158
 
159
+ /**
160
+ * Re-navigates to a server redirect target, counting hops so a target that
161
+ * redirects back ends in the error boundary instead of looping.
162
+ */
163
+ async function followServerRedirect(
164
+ self: {
165
+ navigate: (
166
+ url: string,
167
+ options?: NavigateOptionsInternal,
168
+ ) => Promise<void>;
169
+ },
170
+ error: ServerRedirect,
171
+ url: string,
172
+ hops: number,
173
+ options: { replace?: boolean; transition?: boolean },
174
+ ): Promise<void> {
175
+ const redirectUrl = validateRedirectOrigin(
176
+ error.url,
177
+ window.location.origin,
178
+ );
179
+ if (!redirectUrl) {
180
+ return;
181
+ }
182
+ if (hops >= MAX_REDIRECT_HOPS) {
183
+ const loopError = new Error(
184
+ `[rango] Server redirect loop: stopped after following ${hops} redirects, at ${url}`,
185
+ );
186
+ console.error(loopError.message);
187
+ emitNavigationError(onUpdate, loopError, url);
188
+ return;
189
+ }
190
+ return self.navigate(redirectUrl, {
191
+ state: error.state,
192
+ ...options,
193
+ _skipCache: true,
194
+ _redirectHops: hops + 1,
195
+ });
196
+ }
197
+
156
198
  return {
157
199
  /**
158
200
  * Navigate to a URL
@@ -209,7 +251,11 @@ export function createNavigationBridge(
209
251
  const currentKey = store.getHistoryKey();
210
252
  const currentCache = store.getCachedSegments(currentKey);
211
253
  if (currentCache?.segments) {
212
- const currentHandleData = eventController.getHandleState().data;
254
+ const currentHandleData = liveHandleDataForCache(
255
+ eventController,
256
+ store,
257
+ currentKey,
258
+ );
213
259
  store.cacheSegmentsForHistory(
214
260
  historyKey,
215
261
  currentCache.segments,
@@ -220,8 +266,6 @@ export function createNavigationBridge(
220
266
  // Save current scroll position before changing URL
221
267
  handleNavigationStart();
222
268
 
223
- // Snapshot old state before pushState/replaceState overwrites it
224
- const oldState = window.history.state;
225
269
  const shallowRouteName = store.getHistoryEntryMemory()?.routeName;
226
270
 
227
271
  // Update browser URL (carry intercept context into history state)
@@ -239,11 +283,8 @@ export function createNavigationBridge(
239
283
  ensureHistoryKey();
240
284
  // Same route, new entry (transition({ when }) back/forward source).
241
285
  store.rememberDisplayedEntry(shallowRouteName);
242
-
243
- // Notify useLocationState() hooks when state changes
244
- if (hasLocationState(oldState) || hasLocationState(historyState)) {
245
- window.dispatchEvent(new Event("__rsc_locationstate"));
246
- }
286
+ // Treeless: readers take the entry's state with setLocation below.
287
+ eventController.commitLocationState(historyState, true);
247
288
 
248
289
  // Update store history key so future navigations reference the right cache
249
290
  store.setHistoryKey(historyKey);
@@ -292,7 +333,11 @@ export function createNavigationBridge(
292
333
  const sourceHistoryKey = store.getHistoryKey();
293
334
  const sourceCached = store.getCachedSegments(sourceHistoryKey);
294
335
  if (sourceCached?.segments && sourceCached.segments.length > 0) {
295
- const currentHandleData = eventController.getHandleState().data;
336
+ const currentHandleData = liveHandleDataForCache(
337
+ eventController,
338
+ store,
339
+ sourceHistoryKey,
340
+ );
296
341
  store.updateCacheHandleData(sourceHistoryKey, currentHandleData);
297
342
  }
298
343
 
@@ -357,6 +402,8 @@ export function createNavigationBridge(
357
402
  const clientUrlPresentation = beginClientUrlNavigation(
358
403
  targetUrl,
359
404
  tx.handle.signal,
405
+ // The entry the commit will push, without what the server adds.
406
+ () => buildHistoryState(resolvedState),
360
407
  transitionOptOut
361
408
  ? () => true
362
409
  : (destination) =>
@@ -378,7 +425,7 @@ export function createNavigationBridge(
378
425
  url: targetUrl,
379
426
  params: { ...params, ...destination.params },
380
427
  routeName: destination.routeName,
381
- state: buildHistoryState(resolvedState),
428
+ state: destination.state,
382
429
  };
383
430
  },
384
431
  extra: destination.when,
@@ -420,29 +467,12 @@ export function createNavigationBridge(
420
467
  },
421
468
  );
422
469
  } catch (error) {
423
- // Server-side redirect with location state: the current transaction's
424
- // cleanup resets loading state. Re-navigate to the redirect
425
- // target carrying the server-set state into history.pushState.
426
- if (error instanceof ServerRedirect) {
427
- const redirectUrl = validateRedirectOrigin(
428
- error.url,
429
- window.location.origin,
430
- );
431
- if (!redirectUrl) {
432
- return;
433
- }
434
- return this.navigate(redirectUrl, {
435
- state: error.state,
436
- replace: options?.replace,
437
- transition: options?.transition,
438
- _skipCache: true,
439
- } as NavigateOptionsInternal);
440
- }
441
-
442
470
  // Aborted, or superseded by a newer navigation. A superseded nav may
443
471
  // reject with a non-AbortError (e.g. a Flight decode that fails after its
444
472
  // signal was aborted), so check the signal too -- otherwise we would
445
- // render a boundary that clobbers the newer navigation's content.
473
+ // render a boundary that clobbers the newer navigation's content. Before
474
+ // the redirect branch: a superseded nav must not follow its redirect
475
+ // over the navigation that replaced it.
446
476
  if (
447
477
  (error instanceof DOMException && error.name === "AbortError") ||
448
478
  tx.handle.signal.aborted
@@ -451,6 +481,19 @@ export function createNavigationBridge(
451
481
  return;
452
482
  }
453
483
 
484
+ // Server-side redirect with location state: the current transaction's
485
+ // cleanup resets loading state. Re-navigate to the redirect
486
+ // target carrying the server-set state into history.pushState.
487
+ if (error instanceof ServerRedirect) {
488
+ return followServerRedirect(
489
+ this,
490
+ error,
491
+ url,
492
+ options?._redirectHops ?? 0,
493
+ { replace: options?.replace, transition: options?.transition },
494
+ );
495
+ }
496
+
454
497
  const networkError = toNetworkError(error, {
455
498
  url,
456
499
  operation: "navigation",
@@ -578,9 +621,6 @@ export function createNavigationBridge(
578
621
  historyKey,
579
622
  );
580
623
 
581
- // Update location in event controller
582
- eventController.setLocation(new URL(url));
583
-
584
624
  // If this is an intercept, restore the intercept context
585
625
  if (isIntercept && interceptSourceUrl) {
586
626
  store.setInterceptSourceUrl(interceptSourceUrl);
@@ -648,9 +688,16 @@ export function createNavigationBridge(
648
688
  transitionGatedOff: gatedOff,
649
689
  });
650
690
  store.rememberDisplayedEntry();
651
- // Set params on event controller before onUpdate so both location
652
- // and params are current when the debounced notify() fires.
691
+ // The restored entry's location and params, set before onUpdate so
692
+ // the hooks take both in the batch that renders its tree. Not at
693
+ // the popstate event: the page being left is on screen until here
694
+ // (#1031). A restore that has to fetch gets its location from
695
+ // tx.commit() instead (navigation-transaction.ts).
696
+ eventController.setLocation(new URL(url));
653
697
  eventController.setParams(cachedParams);
698
+ // The entry this handler found at the event, which the cached tree
699
+ // was built for: history may have moved during the render above.
700
+ eventController.commitLocationState(historyState);
654
701
 
655
702
  const popstateUpdate = {
656
703
  root,
@@ -673,6 +720,7 @@ export function createNavigationBridge(
673
720
  !isIntercept &&
674
721
  !isLeavingIntercept &&
675
722
  shouldStartViewTransition(cachedSegments, gatedOff);
723
+ if (process.env.NODE_ENV !== "production") auditTreeCause("popstate");
676
724
  if (hasTransition) {
677
725
  startTransition(() => {
678
726
  if (addTransitionType) {
@@ -783,6 +831,21 @@ export function createNavigationBridge(
783
831
  return;
784
832
  }
785
833
 
834
+ // The browser is already on the entry that redirected. Replacing makes
835
+ // that entry the redirect target, so Back from the target reaches the
836
+ // entry before it; a push would leave the redirecting entry in history
837
+ // and Back would hit the redirect again. Before setLocation below, or
838
+ // the hooks would report the redirecting URL first.
839
+ if (error instanceof ServerRedirect) {
840
+ return followServerRedirect(this, error, url, 0, { replace: true });
841
+ }
842
+
843
+ // tx.commit() never ran, so the location is still the page being
844
+ // left. The error boundary below replaces that page, and history is
845
+ // already on the entry that failed: the hooks report that entry, in
846
+ // the boundary's batch.
847
+ eventController.setLocation(new URL(url));
848
+
786
849
  const networkError = toNetworkError(error, {
787
850
  url,
788
851
  operation: "navigation",
@@ -8,7 +8,9 @@ import type {
8
8
  ResolvedSegment,
9
9
  HandleData,
10
10
  } from "./types.js";
11
+ import type { EventController } from "./event-controller.js";
11
12
  import { clearPrefetchCache } from "./prefetch/cache.js";
13
+ import { LoaderStore } from "../loader-store.js";
12
14
  import {
13
15
  adoptRangoState,
14
16
  getRangoState,
@@ -73,6 +75,28 @@ export function cloneHandleData(handleData: HandleData): HandleData {
73
75
  return cloned;
74
76
  }
75
77
 
78
+ /**
79
+ * Handle data to write into the history cache for the page on screen. During
80
+ * the hydration window getHandleState() is the document snapshot while late
81
+ * pushes sit queued in the controller (they are already in the cache entry,
82
+ * written by rsc-router), so writing the snapshot back would drop them; the
83
+ * entry's own data is the live view then.
84
+ */
85
+ export function liveHandleDataForCache(
86
+ eventController: Pick<
87
+ EventController,
88
+ "getHandleState" | "getHydrationSnapshot"
89
+ >,
90
+ store: Pick<NavigationStore, "getCachedSegments">,
91
+ historyKey: string,
92
+ ): HandleData {
93
+ if (eventController.getHydrationSnapshot()) {
94
+ const cached = store.getCachedSegments(historyKey)?.handleData;
95
+ if (cached) return cached;
96
+ }
97
+ return eventController.getHandleState().data;
98
+ }
99
+
76
100
  // BroadcastChannel for cross-tab cache invalidation
77
101
  const CACHE_INVALIDATION_CHANNEL = "rsc-router-cache-invalidation";
78
102
 
@@ -390,6 +414,8 @@ export function createNavigationStore(
390
414
  }
391
415
 
392
416
  const store: NavigationStore = {
417
+ loaders: new LoaderStore(),
418
+
393
419
  // ========================================================================
394
420
  // Internal Segment State (for bridges)
395
421
  // ========================================================================