@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
@@ -35,11 +35,19 @@
35
35
  * useRouter().replace kind "replace", refresh() kind "revalidate".
36
36
  * Back/forward, action and optimistic clientUrls() swap decisions are
37
37
  * e2e territory.
38
+ * - Location state reaches readers the way it does in production:
39
+ * NavigationProvider holds the committed entry's state next to the
40
+ * payload, so a navigation whose loader the test holds keeps the entry
41
+ * being left on screen with its own state. A `popstate` event is a
42
+ * back/forward onto the entry history.state holds; the tree has one
43
+ * location, so no page is restored with it. A server action's state, a
44
+ * redirect's and an intercept's are e2e territory.
38
45
  * What it DOES cover: client hooks that read NavigationProvider /
39
46
  * OutletContext — useParams, useReverse, useHref, useMount, useNavigation,
40
47
  * useRouter, usePathname, useSearchParams, Outlet/useOutlet nesting and seeded
41
48
  * descendant pending state, useLoader/useFetchLoader (seeded data),
42
- * useLocationState (seeded), and useHandle (seeded).
49
+ * useLocationState (seeded, or written by a navigation), and useHandle
50
+ * (seeded).
43
51
  * Basename-mounted apps: pass the `basename` option so useRouter().basename,
44
52
  * <Link> prefixing, and useMount/useHref resolve against the mount prefix
45
53
  * (without it they resolve at the root "/"). For an include("/shop", ...)
@@ -48,7 +56,12 @@
48
56
  */
49
57
 
50
58
  import { decideTransitionGatedOff } from "../browser/transition-when.js";
51
- import { useEffect, type ReactNode, type ComponentType } from "react";
59
+ import {
60
+ StrictMode,
61
+ useEffect,
62
+ type ReactNode,
63
+ type ComponentType,
64
+ } from "react";
52
65
  import type { RenderResult } from "@testing-library/react";
53
66
  import { renderSegments } from "../segment-system.js";
54
67
  import {
@@ -62,6 +75,7 @@ import {
62
75
  shouldStartViewTransition,
63
76
  } from "../browser/partial-update.js";
64
77
  import type {
78
+ NavigateOptions,
65
79
  NavigateOptionsInternal,
66
80
  NavigationStore,
67
81
  NavigationBridge,
@@ -80,10 +94,13 @@ import type {
80
94
  TransitionConfig,
81
95
  TransitionWhenKind,
82
96
  } from "../types.js";
83
- import type { LocationStateDefinition } from "../browser/react/location-state-shared.js";
97
+ import {
98
+ setLocationStateVersion,
99
+ type LocationStateDefinition,
100
+ } from "../browser/react/location-state-shared.js";
84
101
  import {
85
102
  buildHistoryState,
86
- hasLocationState,
103
+ clearLocationStateOnDocumentLoad,
87
104
  pushHistoryWithIdx,
88
105
  resolveNavigationState,
89
106
  } from "../browser/history-state.js";
@@ -93,9 +110,9 @@ import type { ThemeConfig } from "../theme/types.js";
93
110
  import { resolveThemeConfig } from "../theme/constants.js";
94
111
  import { isUnderTestRunner } from "../runtime-env.js";
95
112
  import { setupNavigationBridgeDelegatedPrefetch } from "../browser/navigation-bridge.js";
96
- import { resetAdaptiveStrategyForTesting } from "../browser/prefetch/default-strategy.js";
97
113
  import { resetPrefetchObserverForTesting } from "../browser/prefetch/observer.js";
98
114
  import type { PrefetchStrategy } from "../router/prefetch-default.js";
115
+ import { auditTreeCause } from "../suspense-audit.js";
99
116
 
100
117
  const TEST_ORIGIN = "http://localhost";
101
118
  let activePrefetchRegistrations = 0;
@@ -230,6 +247,8 @@ export interface RenderRouteOptions {
230
247
  * `useLocationState(StateDef)`. Like loaders, a real `createLocationState()`
231
248
  * handle has an empty injected key in a bare test, so pass `[def, value]`
232
249
  * pairs; renderRoute assigns a synthetic key and writes it to `history.state`.
250
+ * With `hydrate: true` a `clearOnReload` definition's seed is removed before
251
+ * hydration, as on a document load.
233
252
  *
234
253
  * @example
235
254
  * renderRoute([{ path: "/", Component: FlashBanner }], {
@@ -265,6 +284,24 @@ export interface RenderRouteOptions {
265
284
  * computes the segment id for you). Merged with `handles`.
266
285
  */
267
286
  handle?: HandleDataSeed;
287
+ /**
288
+ * Handle values a document delivers after hydration, as a loader's push
289
+ * that is not in the document's handle snapshot is (a push made after the
290
+ * handler output settled, or any loader push on a PPR shell HIT). Each
291
+ * entry is `[handle, values]`: the handle's values after the update, which
292
+ * replace its `handles` seed once the root has hydrated (or mounted). With
293
+ * `hydrate: true` the server HTML is rendered from `handles`, and a reader
294
+ * inside a `<Suspense>` boundary hydrates after the update was applied, as
295
+ * a boundary that hydrates after the root does in production.
296
+ *
297
+ * @example
298
+ * const { serverHtml, recoverableErrors } = await renderRoute(routes, {
299
+ * hydrate: true,
300
+ * handles: [[Notes, ["from-handler"]]],
301
+ * lateHandles: [[Notes, ["from-handler", "from-loader"]]],
302
+ * });
303
+ */
304
+ lateHandles?: ReadonlyArray<readonly [Handle<any, any>, unknown[]]>;
268
305
  /**
269
306
  * Route name -> pattern map. Informational for parity with the server test
270
307
  * context; client useReverse takes its map directly as an argument, so this
@@ -326,6 +363,25 @@ export interface RenderRouteOptions {
326
363
  * not override a `"none"` default.
327
364
  */
328
365
  defaultPrefetch?: PrefetchStrategy;
366
+ /**
367
+ * Hydrate instead of mounting fresh: the same element is rendered to HTML
368
+ * (react-dom/server `renderToString`), placed in the container and hydrated.
369
+ * The result gains `serverHtml` and `recoverableErrors`
370
+ * (RenderRouteHydrateResult). `data-hydrated` is set on `<html>` after
371
+ * hydration, as production's root does, and removed on unmount.
372
+ *
373
+ * Limits:
374
+ * - The server pass runs without `window` and `document`, and only those. A
375
+ * hook that reads a global that stays defined there (`history`,
376
+ * `localStorage`, `navigator`) outside a server snapshot renders the same
377
+ * in both passes here and can still mismatch in real SSR.
378
+ * - Content that suspends in the server pass (`lazy`, a pending or untracked
379
+ * `use()` promise) is emitted as its fallback and client-rendered, not
380
+ * hydrated; React reports the boundary in `recoverableErrors`. Outside any
381
+ * `<Suspense>` it rejects.
382
+ * - Needs @testing-library/react >= 16.2.0; throws on older versions.
383
+ */
384
+ hydrate?: boolean;
329
385
  }
330
386
 
331
387
  /**
@@ -356,10 +412,20 @@ export interface TestRouterHandle {
356
412
  * `options.transition: false` is the per-navigation opt-out
357
413
  * (`router.push(url, { transition: false })`, `<Link transition={false}>`):
358
414
  * the commit is urgent and no transition({ when }) predicate is called.
415
+ *
416
+ * `options.state` and `options.replace: true` write the history entry the
417
+ * way `useRouter().push(url, { state })` and `.replace(url, { state })` do,
418
+ * so a test can carry location state on a navigation whose loader it
419
+ * holds: `state` pushes an entry with it, `replace: true` replaces the
420
+ * current entry (with `state`, or with none, which drops the entry's
421
+ * location state as a production replace does). With neither, history is
422
+ * left alone and readers keep the state they have: `navigate(url)` and
423
+ * `navigate(url, { replace: false })` are the same call.
359
424
  */
360
425
  navigate(
361
426
  url: string,
362
- options?: Pick<RenderRouteOptions, "loaders"> & { transition?: boolean },
427
+ options?: Pick<RenderRouteOptions, "loaders"> &
428
+ Pick<NavigateOptions, "state" | "replace" | "transition">,
363
429
  ): Promise<void>;
364
430
  /**
365
431
  * Re-render the current location, as router.refresh() does: a spec's
@@ -379,6 +445,21 @@ export interface TestRouterHandle {
379
445
  /** Result of renderRoute: RTL's render result plus the router handle. */
380
446
  export type RenderRouteResult = RenderResult & { router: TestRouterHandle };
381
447
 
448
+ /** Result of renderRoute with `hydrate: true`. */
449
+ export type RenderRouteHydrateResult = RenderRouteResult & {
450
+ /** The HTML the server pass produced, as placed in the container. */
451
+ serverHtml: string;
452
+ /**
453
+ * Messages of the errors React recovered from (`onRecoverableError`): a
454
+ * hydration mismatch that made it re-render on the client, or a boundary the
455
+ * server pass could not finish. Empty means the tree hydrated as rendered.
456
+ * Live: React appends for as long as the root is mounted. An attribute-only
457
+ * mismatch is NOT here — React keeps the server attribute and only logs it
458
+ * with `console.error` in development.
459
+ */
460
+ recoverableErrors: string[];
461
+ };
462
+
382
463
  interface ResolvedMatch {
383
464
  params: Record<string, string>;
384
465
  pathname: string;
@@ -410,7 +491,6 @@ function DelegatedPrefetchRegistration({
410
491
  }
411
492
  pendingPrefetchReset = undefined;
412
493
  resetPrefetchObserverForTesting();
413
- resetAdaptiveStrategyForTesting();
414
494
  });
415
495
  }
416
496
  }
@@ -419,6 +499,46 @@ function DelegatedPrefetchRegistration({
419
499
  return null;
420
500
  }
421
501
 
502
+ /**
503
+ * initBrowserApp calls the bridge's registerLinkInterception, which is where
504
+ * production listens for `popstate` (navigation-bridge.ts); a rendered tree
505
+ * has no initBrowserApp.
506
+ */
507
+ function LinkInterceptionRegistration({
508
+ bridge,
509
+ }: {
510
+ bridge: NavigationBridge;
511
+ }): null {
512
+ useEffect(() => bridge.registerLinkInterception(), [bridge]);
513
+ return null;
514
+ }
515
+
516
+ /**
517
+ * The marker production's root sets from its effect after hydration
518
+ * (browser/rsc-router.tsx Rango), so a boundary that hydrates in a later pass
519
+ * finds it set, as in production. Removed on unmount: a test unmounts its
520
+ * root, production never does.
521
+ */
522
+ function HydratedMarker(): null {
523
+ useEffect(() => {
524
+ document.documentElement.dataset.hydrated = "";
525
+ return () => document.documentElement.removeAttribute("data-hydrated");
526
+ }, []);
527
+ return null;
528
+ }
529
+
530
+ /**
531
+ * Runs `run` from an effect after the tree's. That is where production's late
532
+ * handle channel writes a push that arrives after the root hydrated (the store
533
+ * holds it while the hydration window is open, EventController.lockHydration),
534
+ * and where the document has been revealed (browser/rsc-router.tsx
535
+ * documentRevealed).
536
+ */
537
+ function AfterTree({ run }: { run: () => void }): null {
538
+ useEffect(run, [run]);
539
+ return null;
540
+ }
541
+
422
542
  function matchLeaf(
423
543
  pattern: string,
424
544
  pathname: string,
@@ -504,6 +624,14 @@ function buildSegments(
504
624
  return segments;
505
625
  }
506
626
 
627
+ export function renderRoute(
628
+ routes: RenderRouteSpec[],
629
+ options: RenderRouteOptions & { hydrate: true },
630
+ ): Promise<RenderRouteHydrateResult>;
631
+ export function renderRoute(
632
+ routes: RenderRouteSpec[],
633
+ options?: RenderRouteOptions,
634
+ ): Promise<RenderRouteResult>;
507
635
  export async function renderRoute(
508
636
  routes: RenderRouteSpec[],
509
637
  options: RenderRouteOptions = {},
@@ -539,11 +667,17 @@ export async function renderRoute(
539
667
  const loaderData = seedLoaders(options.loaderData ?? {}, options.loaders);
540
668
 
541
669
  if (typeof window !== "undefined") {
670
+ // The tree has no app version (its bridge reports none), so a seed needs
671
+ // none either. Reset what an initBrowserApp in the same file may have left.
672
+ setLocationStateVersion(undefined);
542
673
  const stateObj: Record<string, unknown> = {};
543
674
  for (const [def, value] of options.locationState ?? []) {
544
675
  stateObj[withLocationStateKey(def).__rsc_ls_key] = value;
545
676
  }
546
677
  window.history.replaceState(stateObj, "");
678
+ // Hydrate mode is a document load of the seeded entry: run start-up's
679
+ // step (initBrowserApp), not a copy of it.
680
+ if (options.hydrate) clearLocationStateOnDocumentLoad();
547
681
  }
548
682
 
549
683
  const resolve = (pathname: string): ResolvedMatch => {
@@ -609,14 +743,40 @@ export async function renderRoute(
609
743
 
610
744
  const eventController = createEventController({ initialLocation: url });
611
745
  eventController.setParams(initialMatch.params);
746
+ // The seeded entry, as initBrowserApp commits the document's.
747
+ eventController.commitLocationState(window.history.state);
612
748
  // Resolve-by-default: resolve any deferred (Promise) seeded handle values
613
749
  // before applying, so the seeded handles reach collect/useHandle resolved —
614
750
  // matching what the server/client do in a real app.
615
751
  const resolvedSeed = await resolveDeferredHandleValues(handleSeed);
616
- eventController.setHandleData(
617
- resolvedSeed,
618
- initialSegments.map((s) => s.id),
619
- );
752
+ const initialSegmentIds = initialSegments.map((s) => s.id);
753
+ eventController.setHandleData(resolvedSeed, initialSegmentIds);
754
+ // As initBrowserApp before hydrateRoot: until the provider's barrier commit,
755
+ // handle reads return this state and later writes wait.
756
+ if (options.hydrate) eventController.lockHydration();
757
+ let resolveDocumentRevealed: (() => void) | undefined;
758
+ const documentRevealed = options.hydrate
759
+ ? new Promise<void>((resolve) => {
760
+ resolveDocumentRevealed = resolve;
761
+ })
762
+ : undefined;
763
+ // A late update carries the full state: the seed with these handles' values
764
+ // replaced, resolved as the browser resolves a late yield before applying.
765
+ let applyLateHandles: (() => void) | undefined;
766
+ if (options.lateHandles && leafRouteSegmentId !== undefined) {
767
+ const lateSeed = cloneHandleSeed(resolvedSeed);
768
+ for (const [handle, values] of options.lateHandles) {
769
+ const id = (handle as unknown as { $$id: string }).$$id;
770
+ (lateSeed[id] ??= {})[leafRouteSegmentId] = values;
771
+ }
772
+ const lateState = await resolveDeferredHandleValues(lateSeed);
773
+ // Flushed, not left to the notify timer: renderRoute's act() has
774
+ // returned by then.
775
+ applyLateHandles = () => {
776
+ eventController.setHandleData(lateState, initialSegmentIds);
777
+ eventController.flushRouteState();
778
+ };
779
+ }
620
780
 
621
781
  let warnedNavLifecycle = false;
622
782
  const navigate = async (
@@ -633,10 +793,11 @@ export async function renderRoute(
633
793
  // A useRouter().push/replace or <Link> navigation (`history`) writes its
634
794
  // entry the way production does (navigation-bridge.ts navigate ->
635
795
  // navigation-transaction.ts commit): the dev state check, typed entries
636
- // spread onto history.state, the idx stamp, and the __rsc_locationstate
637
- // event when the old or new entry carries location state. The URL stays
638
- // put: renderRoute tracks location on the event controller, not
639
- // window.location. router.navigate() and refresh() leave history alone.
796
+ // spread onto history.state, the idx stamp, and the entry state committed
797
+ // on the event controller, which the provider takes with the payload.
798
+ // The URL stays put: renderRoute tracks location on the event
799
+ // controller, not window.location. router.navigate() without `state` or
800
+ // `replace: true`, and refresh(), leave history alone.
640
801
  const history = navOptions?.history;
641
802
  const historyState = history
642
803
  ? buildHistoryState(
@@ -702,30 +863,28 @@ export async function renderRoute(
702
863
  eventController.setParams(match.params);
703
864
  store.setCurrentUrl(nextUrl.href);
704
865
  store.setSegmentIds(segments.map((s) => s.id));
705
- let notifyLocationState = false;
706
866
  if (history) {
707
- notifyLocationState =
708
- hasLocationState(window.history.state) ||
709
- hasLocationState(historyState);
710
867
  pushHistoryWithIdx(
711
868
  historyState ?? null,
712
869
  window.location.href,
713
870
  history.replace ?? false,
714
871
  );
872
+ eventController.commitLocationState(historyState);
715
873
  }
716
874
  const emit: UpdateSubscriber = (update) => store.emitUpdate(update);
717
875
  await act(async () => {
876
+ // As production's emitters name their cause (suspense-audit.ts I6).
877
+ auditTreeCause("navigation");
718
878
  // Production's transition() lane (browser/partial-update.ts). Its
719
879
  // same-structure / fully-prefetched / optimistic hold lanes are not
720
880
  // modeled: without transition() the commit stays urgent.
721
881
  if (shouldStartViewTransition(segments, gatedOff)) {
722
- commitInTransition(emit, segments, { root, metadata }, ["navigation"]);
882
+ commitInTransition(store.loaders, emit, segments, { root, metadata }, [
883
+ "navigation",
884
+ ]);
723
885
  } else {
724
886
  emit({ root, metadata });
725
887
  }
726
- if (notifyLocationState) {
727
- window.dispatchEvent(new Event("__rsc_locationstate"));
728
- }
729
888
  });
730
889
  };
731
890
 
@@ -739,8 +898,20 @@ export async function renderRoute(
739
898
  navigate: (target, navigateOptions) =>
740
899
  navigate(target, { history: navigateOptions ?? {} }),
741
900
  refresh: () => refresh(),
742
- handlePopstate: async () => {},
743
- registerLinkInterception: () => () => {},
901
+ // Back/forward onto the entry history.state holds. The tree has one
902
+ // location, so nothing is restored: a treeless commit of that entry, as
903
+ // production's traversal that keeps every segment
904
+ // (navigation-transaction.ts commit). Flushed, not left to the notify
905
+ // timer: the dispatching test's act() has returned by then.
906
+ handlePopstate: async () => {
907
+ eventController.commitLocationState(window.history.state, true);
908
+ eventController.flushRouteState();
909
+ },
910
+ registerLinkInterception: () => {
911
+ const onPopstate = (): void => void bridge.handlePopstate();
912
+ window.addEventListener("popstate", onPopstate);
913
+ return () => window.removeEventListener("popstate", onPopstate);
914
+ },
744
915
  registerDelegatedPrefetch: () =>
745
916
  setupNavigationBridgeDelegatedPrefetch(
746
917
  store,
@@ -764,6 +935,54 @@ export async function renderRoute(
764
935
  outletPending: options.outletPending,
765
936
  });
766
937
 
938
+ // The marker sits after the provider so its effect runs after the tree's,
939
+ // like the effect of production's root component.
940
+ const ui = (
941
+ <>
942
+ <NavigationProvider
943
+ store={store}
944
+ eventController={eventController}
945
+ initialPayload={{ root: initialTree, metadata: initialMetadata }}
946
+ bridge={bridge}
947
+ basename={basename}
948
+ themeConfig={
949
+ options.theme === undefined ? null : resolveThemeConfig(options.theme)
950
+ }
951
+ nonce={options.nonce}
952
+ hydration={documentRevealed ? { settled: documentRevealed } : undefined}
953
+ />
954
+ <DelegatedPrefetchRegistration bridge={bridge} />
955
+ <LinkInterceptionRegistration bridge={bridge} />
956
+ {options.hydrate && <HydratedMarker />}
957
+ {applyLateHandles && <AfterTree run={applyLateHandles} />}
958
+ {resolveDocumentRevealed && <AfterTree run={resolveDocumentRevealed} />}
959
+ </>
960
+ );
961
+
962
+ let serverHtml: string | undefined;
963
+ const recoverableErrors: string[] = [];
964
+ if (options.hydrate) {
965
+ // RTL forwards onRecoverableError to hydrateRoot from 16.2.0. 16.0-16.1
966
+ // are inside the peer range and drop it (their render() never names the
967
+ // option), which would leave `recoverableErrors` empty on a real mismatch.
968
+ if (!String(render).includes("onRecoverableError")) {
969
+ throw new Error(
970
+ "renderRoute: `hydrate` needs @testing-library/react >= 16.2.0. Older " +
971
+ "versions do not pass onRecoverableError to hydrateRoot, so a " +
972
+ "hydration mismatch would go unreported.",
973
+ );
974
+ }
975
+ const { getConfig } = await import("@testing-library/react");
976
+ const { renderToString } = await import("react-dom/server");
977
+ // RTL wraps the hydrated element the same way.
978
+ const serverUi = getConfig().reactStrictMode ? (
979
+ <StrictMode>{ui}</StrictMode>
980
+ ) : (
981
+ ui
982
+ );
983
+ serverHtml = withoutBrowserGlobals(() => renderToString(serverUi));
984
+ }
985
+
767
986
  // Wrap render in an awaited async act so a tree that suspends (async loaders,
768
987
  // loading states, deferred handle entries that arrive as a Promise) settles its
769
988
  // Suspense within act — otherwise React orphans the resolution ("a component
@@ -772,30 +991,32 @@ export async function renderRoute(
772
991
  let result!: Awaited<ReturnType<typeof render>>;
773
992
  const container = document.body.appendChild(document.createElement("div"));
774
993
  prefetchRoot = container;
994
+ if (serverHtml !== undefined) container.innerHTML = serverHtml;
775
995
  await act(async () => {
776
- result = render(
777
- <>
778
- <NavigationProvider
779
- store={store}
780
- eventController={eventController}
781
- initialPayload={{ root: initialTree, metadata: initialMetadata }}
782
- bridge={bridge}
783
- basename={basename}
784
- themeConfig={
785
- options.theme === undefined
786
- ? null
787
- : resolveThemeConfig(options.theme)
788
- }
789
- nonce={options.nonce}
790
- />
791
- <DelegatedPrefetchRegistration bridge={bridge} />
792
- </>,
793
- { baseElement: document.body, container },
794
- );
996
+ result = render(ui, {
997
+ baseElement: document.body,
998
+ container,
999
+ ...(serverHtml !== undefined && {
1000
+ hydrate: true,
1001
+ onRecoverableError: (error: unknown) => {
1002
+ recoverableErrors.push(
1003
+ error instanceof Error ? error.message : String(error),
1004
+ );
1005
+ },
1006
+ }),
1007
+ });
795
1008
  });
796
1009
 
797
1010
  const router: TestRouterHandle = {
798
- navigate,
1011
+ navigate: (target, navOptions) =>
1012
+ navigate(target, {
1013
+ loaders: navOptions?.loaders,
1014
+ transition: navOptions?.transition,
1015
+ history:
1016
+ navOptions?.state !== undefined || navOptions?.replace
1017
+ ? navOptions
1018
+ : undefined,
1019
+ }),
799
1020
  refresh,
800
1021
  pathname: () => new URL(eventController.getLocation().href).pathname,
801
1022
  params: () => eventController.getParams(),
@@ -803,7 +1024,33 @@ export async function renderRoute(
803
1024
  eventController,
804
1025
  };
805
1026
 
806
- return Object.assign(result, { router });
1027
+ if (serverHtml === undefined) return Object.assign(result, { router });
1028
+ return Object.assign(result, { router, serverHtml, recoverableErrors });
1029
+ }
1030
+
1031
+ /**
1032
+ * Runs the server pass without `window` and `document`, the two globals
1033
+ * SSR-aware code probes. A DOM test environment defines both, so with them in
1034
+ * place a `typeof window` branch takes its client side, renders the client
1035
+ * HTML, and the mismatch it causes in production hydrates clean. `fn` must be
1036
+ * synchronous (renderToString, not a streaming entry): nothing else may run
1037
+ * while they are gone.
1038
+ */
1039
+ function withoutBrowserGlobals<T>(fn: () => T): T {
1040
+ const scope = globalThis as { window?: unknown; document?: unknown };
1041
+ const windowDescriptor = Object.getOwnPropertyDescriptor(scope, "window")!;
1042
+ const documentDescriptor = Object.getOwnPropertyDescriptor(
1043
+ scope,
1044
+ "document",
1045
+ )!;
1046
+ try {
1047
+ delete scope.window;
1048
+ delete scope.document;
1049
+ return fn();
1050
+ } finally {
1051
+ Object.defineProperty(scope, "window", windowDescriptor);
1052
+ Object.defineProperty(scope, "document", documentDescriptor);
1053
+ }
807
1054
  }
808
1055
 
809
1056
  function makeMetadata(