@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1c0bdfad

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 (406) hide show
  1. package/AGENTS.md +17 -0
  2. package/README.md +291 -61
  3. package/dist/bin/rango.js +544 -143
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3744 -1329
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +67 -13
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +312 -0
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +247 -23
  12. package/skills/caching/SKILL.md +322 -19
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/debug-manifest/SKILL.md +4 -2
  16. package/skills/document-cache/SKILL.md +78 -55
  17. package/skills/handler-use/SKILL.md +364 -0
  18. package/skills/hooks/SKILL.md +282 -60
  19. package/skills/host-router/SKILL.md +278 -0
  20. package/skills/i18n/SKILL.md +276 -0
  21. package/skills/intercept/SKILL.md +50 -6
  22. package/skills/layout/SKILL.md +35 -9
  23. package/skills/links/SKILL.md +249 -17
  24. package/skills/loader/SKILL.md +297 -31
  25. package/skills/middleware/SKILL.md +52 -13
  26. package/skills/migrate-nextjs/SKILL.md +584 -0
  27. package/skills/migrate-react-router/SKILL.md +771 -0
  28. package/skills/mime-routes/SKILL.md +28 -1
  29. package/skills/observability/SKILL.md +172 -0
  30. package/skills/parallel/SKILL.md +203 -7
  31. package/skills/prerender/SKILL.md +155 -111
  32. package/skills/rango/SKILL.md +251 -23
  33. package/skills/react-compiler/SKILL.md +168 -0
  34. package/skills/response-routes/SKILL.md +123 -48
  35. package/skills/route/SKILL.md +104 -9
  36. package/skills/router-setup/SKILL.md +124 -11
  37. package/skills/scripts/SKILL.md +179 -0
  38. package/skills/server-actions/SKILL.md +775 -0
  39. package/skills/streams-and-websockets/SKILL.md +283 -0
  40. package/skills/tailwind/SKILL.md +27 -3
  41. package/skills/testing/SKILL.md +125 -222
  42. package/skills/testing/bindings.md +103 -0
  43. package/skills/testing/cache-prerender.md +127 -0
  44. package/skills/testing/client-components.md +124 -0
  45. package/skills/testing/e2e-parity.md +125 -0
  46. package/skills/testing/flight.md +91 -0
  47. package/skills/testing/handles.md +129 -0
  48. package/skills/testing/loader.md +128 -0
  49. package/skills/testing/middleware.md +99 -0
  50. package/skills/testing/render-handler.md +121 -0
  51. package/skills/testing/response-routes.md +95 -0
  52. package/skills/testing/reverse-and-types.md +84 -0
  53. package/skills/testing/server-actions.md +107 -0
  54. package/skills/testing/server-tree.md +128 -0
  55. package/skills/testing/setup.md +123 -0
  56. package/skills/typesafety/SKILL.md +357 -52
  57. package/skills/use-cache/SKILL.md +46 -14
  58. package/skills/view-transitions/SKILL.md +294 -0
  59. package/src/__augment-tests__/augment.ts +81 -0
  60. package/src/__augment-tests__/augmented.check.ts +116 -0
  61. package/src/__internal.ts +67 -40
  62. package/src/bin/rango.ts +18 -0
  63. package/src/browser/action-coordinator.ts +53 -36
  64. package/src/browser/action-fence.ts +47 -0
  65. package/src/browser/app-shell.ts +39 -0
  66. package/src/browser/app-version.ts +14 -0
  67. package/src/browser/connection-warmup.ts +134 -0
  68. package/src/browser/cookie-name.ts +140 -0
  69. package/src/browser/event-controller.ts +197 -150
  70. package/src/browser/history-state.ts +21 -0
  71. package/src/browser/index.ts +3 -3
  72. package/src/browser/invalidate-client-cache.ts +52 -0
  73. package/src/browser/link-interceptor.ts +4 -0
  74. package/src/browser/navigation-bridge.ts +200 -30
  75. package/src/browser/navigation-client.ts +217 -58
  76. package/src/browser/navigation-store-handle.ts +38 -0
  77. package/src/browser/navigation-store.ts +76 -67
  78. package/src/browser/navigation-transaction.ts +18 -66
  79. package/src/browser/network-error-handler.ts +34 -7
  80. package/src/browser/partial-update.ts +187 -112
  81. package/src/browser/prefetch/cache.ts +312 -30
  82. package/src/browser/prefetch/fetch.ts +344 -47
  83. package/src/browser/prefetch/policy.ts +6 -0
  84. package/src/browser/prefetch/queue.ts +126 -20
  85. package/src/browser/prefetch/resource-ready.ts +77 -0
  86. package/src/browser/rango-state.ts +158 -76
  87. package/src/browser/react/Link.tsx +125 -18
  88. package/src/browser/react/NavigationProvider.tsx +135 -120
  89. package/src/browser/react/ScrollRestoration.tsx +10 -6
  90. package/src/browser/react/context.ts +7 -2
  91. package/src/browser/react/filter-segment-order.ts +66 -7
  92. package/src/browser/react/index.ts +0 -48
  93. package/src/browser/react/location-state-shared.ts +178 -8
  94. package/src/browser/react/location-state.ts +39 -14
  95. package/src/browser/react/use-action.ts +6 -15
  96. package/src/browser/react/use-handle.ts +23 -69
  97. package/src/browser/react/use-href.tsx +8 -1
  98. package/src/browser/react/use-link-status.ts +33 -8
  99. package/src/browser/react/use-navigation.ts +32 -7
  100. package/src/browser/react/use-params.ts +20 -10
  101. package/src/browser/react/use-reverse.ts +106 -0
  102. package/src/browser/react/use-router.ts +46 -11
  103. package/src/browser/react/use-search-params.ts +0 -5
  104. package/src/browser/react/use-segments.ts +11 -21
  105. package/src/browser/response-adapter.ts +80 -5
  106. package/src/browser/rsc-router.tsx +226 -75
  107. package/src/browser/scroll-restoration.ts +54 -42
  108. package/src/browser/segment-reconciler.ts +36 -9
  109. package/src/browser/segment-structure-assert.ts +2 -2
  110. package/src/browser/server-action-bridge.ts +619 -442
  111. package/src/browser/types.ts +115 -11
  112. package/src/browser/validate-redirect-origin.ts +43 -16
  113. package/src/build/collect-fallback-refs.ts +107 -0
  114. package/src/build/generate-manifest.ts +65 -40
  115. package/src/build/generate-route-types.ts +7 -1
  116. package/src/build/index.ts +8 -2
  117. package/src/build/prefix-tree-utils.ts +123 -0
  118. package/src/build/route-trie.ts +182 -37
  119. package/src/build/route-types/ast-route-extraction.ts +15 -8
  120. package/src/build/route-types/codegen.ts +16 -5
  121. package/src/build/route-types/include-resolution.ts +125 -24
  122. package/src/build/route-types/param-extraction.ts +6 -3
  123. package/src/build/route-types/per-module-writer.ts +22 -6
  124. package/src/build/route-types/router-processing.ts +392 -106
  125. package/src/build/route-types/scan-filter.ts +9 -2
  126. package/src/build/route-types/source-scan.ts +216 -0
  127. package/src/build/runtime-discovery.ts +9 -20
  128. package/src/cache/cache-error.ts +104 -0
  129. package/src/cache/cache-key-utils.ts +29 -13
  130. package/src/cache/cache-policy.ts +108 -34
  131. package/src/cache/cache-runtime.ts +214 -48
  132. package/src/cache/cache-scope.ts +236 -89
  133. package/src/cache/cache-tag.ts +103 -0
  134. package/src/cache/cf/cf-base64.ts +33 -0
  135. package/src/cache/cf/cf-cache-constants.ts +127 -0
  136. package/src/cache/cf/cf-cache-store.ts +2224 -171
  137. package/src/cache/cf/cf-cache-types.ts +349 -0
  138. package/src/cache/cf/cf-kv-utils.ts +46 -0
  139. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  140. package/src/cache/cf/index.ts +11 -17
  141. package/src/cache/document-cache.ts +89 -27
  142. package/src/cache/handle-snapshot.ts +70 -0
  143. package/src/cache/index.ts +11 -20
  144. package/src/cache/memory-segment-store.ts +136 -37
  145. package/src/cache/profile-registry.ts +31 -31
  146. package/src/cache/read-through-swr.ts +41 -11
  147. package/src/cache/segment-codec.ts +9 -17
  148. package/src/cache/tag-invalidation.ts +230 -0
  149. package/src/cache/taint.ts +55 -0
  150. package/src/cache/types.ts +37 -100
  151. package/src/client.rsc.tsx +45 -21
  152. package/src/client.tsx +120 -336
  153. package/src/cloudflare/index.ts +11 -0
  154. package/src/cloudflare/tracing.ts +109 -0
  155. package/src/component-utils.ts +19 -0
  156. package/src/components/DefaultDocument.tsx +8 -2
  157. package/src/context-var.ts +84 -2
  158. package/src/debug.ts +2 -2
  159. package/src/decode-loader-results.ts +52 -0
  160. package/src/defer.ts +196 -0
  161. package/src/deps/ssr.ts +0 -1
  162. package/src/encode-kv.ts +49 -0
  163. package/src/errors.ts +30 -4
  164. package/src/escape-script.ts +52 -0
  165. package/src/handle.ts +70 -22
  166. package/src/handles/MetaTags.tsx +56 -19
  167. package/src/handles/Scripts.tsx +183 -0
  168. package/src/handles/breadcrumbs.ts +95 -0
  169. package/src/handles/is-thenable.ts +19 -0
  170. package/src/handles/meta.ts +51 -40
  171. package/src/handles/script.ts +244 -0
  172. package/src/host/cookie-handler.ts +9 -60
  173. package/src/host/errors.ts +0 -24
  174. package/src/host/index.ts +8 -5
  175. package/src/host/pattern-matcher.ts +23 -52
  176. package/src/host/router.ts +107 -99
  177. package/src/host/testing.ts +40 -27
  178. package/src/host/types.ts +37 -4
  179. package/src/host/utils.ts +1 -1
  180. package/src/href-client.ts +137 -22
  181. package/src/index.rsc.ts +79 -29
  182. package/src/index.ts +149 -65
  183. package/src/internal-debug.ts +11 -10
  184. package/src/loader-store.ts +500 -0
  185. package/src/loader.rsc.ts +20 -13
  186. package/src/loader.ts +12 -11
  187. package/src/missing-id-error.ts +68 -0
  188. package/src/outlet-context.ts +1 -1
  189. package/src/outlet-provider.tsx +1 -5
  190. package/src/prerender/param-hash.ts +16 -16
  191. package/src/prerender/store.ts +63 -26
  192. package/src/prerender.ts +198 -82
  193. package/src/redirect-origin.ts +100 -0
  194. package/src/regex-escape.ts +8 -0
  195. package/src/render-error-thrower.tsx +20 -0
  196. package/src/response-utils.ts +62 -0
  197. package/src/reverse.ts +65 -15
  198. package/src/root-error-boundary.tsx +1 -19
  199. package/src/route-content-wrapper.tsx +7 -72
  200. package/src/route-definition/dsl-helpers.ts +469 -276
  201. package/src/route-definition/helper-factories.ts +29 -139
  202. package/src/route-definition/helpers-types.ts +113 -37
  203. package/src/route-definition/index.ts +3 -3
  204. package/src/route-definition/redirect.ts +53 -12
  205. package/src/route-definition/resolve-handler-use.ts +161 -0
  206. package/src/route-definition/use-item-types.ts +32 -0
  207. package/src/route-map-builder.ts +7 -17
  208. package/src/route-types.ts +37 -41
  209. package/src/router/basename.ts +14 -0
  210. package/src/router/content-negotiation.ts +164 -17
  211. package/src/router/error-handling.ts +45 -18
  212. package/src/router/find-match.ts +45 -22
  213. package/src/router/handler-context.ts +110 -39
  214. package/src/router/instrument.ts +350 -0
  215. package/src/router/intercept-resolution.ts +50 -24
  216. package/src/router/lazy-includes.ts +19 -53
  217. package/src/router/loader-resolution.ts +274 -56
  218. package/src/router/logging.ts +5 -8
  219. package/src/router/manifest.ts +49 -45
  220. package/src/router/match-api.ts +121 -205
  221. package/src/router/match-context.ts +0 -22
  222. package/src/router/match-handlers.ts +58 -58
  223. package/src/router/match-middleware/background-revalidation.ts +33 -6
  224. package/src/router/match-middleware/cache-lookup.ts +214 -263
  225. package/src/router/match-middleware/cache-store.ts +73 -33
  226. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  227. package/src/router/match-middleware/segment-resolution.ts +52 -18
  228. package/src/router/match-pipelines.ts +1 -42
  229. package/src/router/match-result.ts +104 -49
  230. package/src/router/metrics.ts +217 -26
  231. package/src/router/middleware-types.ts +24 -110
  232. package/src/router/middleware.ts +384 -197
  233. package/src/router/navigation-snapshot.ts +131 -0
  234. package/src/router/params-util.ts +23 -0
  235. package/src/router/pattern-matching.ts +148 -91
  236. package/src/router/prefetch-cache-ttl.ts +51 -0
  237. package/src/router/prerender-match.ts +199 -56
  238. package/src/router/preview-match.ts +32 -102
  239. package/src/router/request-classification.ts +276 -0
  240. package/src/router/revalidation.ts +144 -74
  241. package/src/router/route-snapshot.ts +244 -0
  242. package/src/router/router-context.ts +8 -28
  243. package/src/router/router-interfaces.ts +129 -36
  244. package/src/router/router-options.ts +185 -23
  245. package/src/router/router-registry.ts +2 -5
  246. package/src/router/segment-resolution/fresh.ts +281 -76
  247. package/src/router/segment-resolution/helpers.ts +116 -31
  248. package/src/router/segment-resolution/loader-cache.ts +63 -37
  249. package/src/router/segment-resolution/revalidation.ts +493 -391
  250. package/src/router/segment-resolution/static-store.ts +19 -5
  251. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  252. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  253. package/src/router/segment-resolution.ts +5 -1
  254. package/src/router/segment-wrappers.ts +8 -5
  255. package/src/router/state-cookie-name.ts +33 -0
  256. package/src/router/substitute-pattern-params.ts +56 -0
  257. package/src/router/telemetry-otel.ts +161 -199
  258. package/src/router/telemetry.ts +96 -19
  259. package/src/router/timeout.ts +0 -20
  260. package/src/router/tracing.ts +206 -0
  261. package/src/router/trie-matching.ts +180 -58
  262. package/src/router/types.ts +10 -63
  263. package/src/router/url-params.ts +44 -0
  264. package/src/router.ts +182 -54
  265. package/src/rsc/handler-context.ts +3 -2
  266. package/src/rsc/handler.ts +702 -460
  267. package/src/rsc/helpers.ts +168 -46
  268. package/src/rsc/index.ts +2 -25
  269. package/src/rsc/json-route-result.ts +38 -0
  270. package/src/rsc/loader-fetch.ts +127 -31
  271. package/src/rsc/manifest-init.ts +33 -42
  272. package/src/rsc/origin-guard.ts +39 -25
  273. package/src/rsc/progressive-enhancement.ts +98 -19
  274. package/src/rsc/redirect-guard.ts +99 -0
  275. package/src/rsc/response-cache-serve.ts +238 -0
  276. package/src/rsc/response-error.ts +79 -12
  277. package/src/rsc/response-route-handler.ts +99 -189
  278. package/src/rsc/rsc-rendering.ts +126 -106
  279. package/src/rsc/runtime-warnings.ts +23 -10
  280. package/src/rsc/server-action.ts +269 -114
  281. package/src/rsc/ssr-setup.ts +144 -0
  282. package/src/rsc/types.ts +34 -6
  283. package/src/runtime-env.ts +18 -0
  284. package/src/search-params.ts +49 -41
  285. package/src/segment-content-promise.ts +67 -0
  286. package/src/segment-loader-promise.ts +149 -0
  287. package/src/segment-system.tsx +281 -129
  288. package/src/serialize.ts +243 -0
  289. package/src/server/context.ts +317 -63
  290. package/src/server/cookie-parse.ts +32 -0
  291. package/src/server/cookie-store.ts +80 -5
  292. package/src/server/handle-store.ts +40 -38
  293. package/src/server/loader-registry.ts +26 -46
  294. package/src/server/request-context.ts +425 -177
  295. package/src/server.ts +6 -0
  296. package/src/ssr/index.tsx +25 -16
  297. package/src/static-handler.ts +27 -18
  298. package/src/testing/cache-status.ts +162 -0
  299. package/src/testing/collect-handle.ts +40 -0
  300. package/src/testing/dispatch.ts +701 -0
  301. package/src/testing/dom.entry.ts +22 -0
  302. package/src/testing/e2e/fixture.ts +188 -0
  303. package/src/testing/e2e/index.ts +128 -0
  304. package/src/testing/e2e/matchers.ts +35 -0
  305. package/src/testing/e2e/page-helpers.ts +272 -0
  306. package/src/testing/e2e/parity.ts +387 -0
  307. package/src/testing/e2e/server.ts +195 -0
  308. package/src/testing/flight-matchers.ts +97 -0
  309. package/src/testing/flight-normalize.ts +11 -0
  310. package/src/testing/flight-runtime.d.ts +57 -0
  311. package/src/testing/flight-tree.ts +682 -0
  312. package/src/testing/flight.entry.ts +52 -0
  313. package/src/testing/flight.ts +257 -0
  314. package/src/testing/generated-routes.ts +183 -0
  315. package/src/testing/index.ts +99 -0
  316. package/src/testing/internal/context.ts +371 -0
  317. package/src/testing/internal/flight-client-globals.ts +30 -0
  318. package/src/testing/internal/seed-vars.ts +54 -0
  319. package/src/testing/render-handler.ts +343 -0
  320. package/src/testing/render-route.tsx +581 -0
  321. package/src/testing/run-loader.ts +385 -0
  322. package/src/testing/run-middleware.ts +205 -0
  323. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  324. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  325. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  326. package/src/testing/vitest-stubs/version.ts +5 -0
  327. package/src/testing/vitest.ts +305 -0
  328. package/src/theme/ThemeProvider.tsx +20 -58
  329. package/src/theme/ThemeScript.tsx +7 -9
  330. package/src/theme/constants.ts +52 -13
  331. package/src/theme/index.ts +3 -19
  332. package/src/theme/theme-context.ts +1 -5
  333. package/src/theme/theme-script.ts +22 -21
  334. package/src/theme/use-theme.ts +0 -3
  335. package/src/types/boundaries.ts +0 -35
  336. package/src/types/cache-types.ts +17 -8
  337. package/src/types/error-types.ts +30 -90
  338. package/src/types/global-namespace.ts +54 -41
  339. package/src/types/handler-context.ts +236 -88
  340. package/src/types/index.ts +1 -10
  341. package/src/types/loader-types.ts +44 -15
  342. package/src/types/request-scope.ts +112 -0
  343. package/src/types/route-config.ts +10 -45
  344. package/src/types/route-entry.ts +19 -7
  345. package/src/types/segments.ts +37 -19
  346. package/src/urls/include-helper.ts +33 -70
  347. package/src/urls/index.ts +1 -11
  348. package/src/urls/path-helper-types.ts +58 -11
  349. package/src/urls/path-helper.ts +57 -111
  350. package/src/urls/pattern-types.ts +48 -19
  351. package/src/urls/response-types.ts +25 -22
  352. package/src/urls/type-extraction.ts +58 -139
  353. package/src/urls/urls-function.ts +1 -18
  354. package/src/use-loader.tsx +346 -89
  355. package/src/vite/debug.ts +185 -0
  356. package/src/vite/discovery/bundle-postprocess.ts +64 -91
  357. package/src/vite/discovery/discover-routers.ts +147 -88
  358. package/src/vite/discovery/discovery-errors.ts +194 -0
  359. package/src/vite/discovery/gate-state.ts +171 -0
  360. package/src/vite/discovery/prerender-collection.ts +247 -145
  361. package/src/vite/discovery/route-types-writer.ts +40 -84
  362. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  363. package/src/vite/discovery/state.ts +61 -13
  364. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  365. package/src/vite/index.ts +10 -3
  366. package/src/vite/inject-client-debug.ts +36 -0
  367. package/src/vite/plugin-types.ts +155 -65
  368. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  369. package/src/vite/plugins/client-ref-dedup.ts +120 -0
  370. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  371. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  372. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  373. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  374. package/src/vite/plugins/expose-action-id.ts +49 -98
  375. package/src/vite/plugins/expose-id-utils.ts +96 -51
  376. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  377. package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
  378. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  379. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  380. package/src/vite/plugins/expose-internal-ids.ts +553 -317
  381. package/src/vite/plugins/performance-tracks.ts +89 -0
  382. package/src/vite/plugins/refresh-cmd.ts +127 -0
  383. package/src/vite/plugins/use-cache-transform.ts +73 -83
  384. package/src/vite/plugins/version-injector.ts +21 -25
  385. package/src/vite/plugins/version-plugin.ts +46 -37
  386. package/src/vite/plugins/virtual-entries.ts +13 -18
  387. package/src/vite/rango.ts +241 -287
  388. package/src/vite/router-discovery.ts +956 -149
  389. package/src/vite/utils/ast-handler-extract.ts +26 -35
  390. package/src/vite/utils/banner.ts +4 -4
  391. package/src/vite/utils/bundle-analysis.ts +10 -15
  392. package/src/vite/utils/client-chunks.ts +184 -0
  393. package/src/vite/utils/directive-prologue.ts +40 -0
  394. package/src/vite/utils/forward-user-plugins.ts +171 -0
  395. package/src/vite/utils/manifest-utils.ts +4 -59
  396. package/src/vite/utils/package-resolution.ts +20 -52
  397. package/src/vite/utils/prerender-utils.ts +141 -34
  398. package/src/vite/utils/shared-utils.ts +92 -42
  399. package/CLAUDE.md +0 -5
  400. package/src/browser/action-response-classifier.ts +0 -99
  401. package/src/browser/react/use-client-cache.ts +0 -58
  402. package/src/browser/shallow.ts +0 -40
  403. package/src/handles/index.ts +0 -6
  404. package/src/network-error-thrower.tsx +0 -23
  405. package/src/route-definition/route-function.ts +0 -119
  406. package/src/router/middleware-cookies.ts +0 -55
@@ -5,6 +5,7 @@ import React, {
5
5
  useCallback,
6
6
  useContext,
7
7
  useEffect,
8
+ useMemo,
8
9
  useRef,
9
10
  type ForwardRefExoticComponent,
10
11
  type RefAttributes,
@@ -32,31 +33,54 @@ export type LinkState =
32
33
  | StateOrGetter<Record<string, unknown>>;
33
34
 
34
35
  import { prefetchDirect, prefetchQueued } from "../prefetch/fetch.js";
36
+ import { getAppVersion } from "../app-version.js";
35
37
  import {
36
38
  observeForPrefetch,
37
39
  unobserveForPrefetch,
38
40
  } from "../prefetch/observer.js";
39
41
 
40
- // Touch device detection for hybrid strategy.
41
- // Checked once at module load (Link.tsx is "use client", runs only in browser).
42
- const isTouchDevice =
43
- typeof window !== "undefined" && window.matchMedia("(hover: none)").matches;
42
+ /**
43
+ * Read current touch/no-hover capability. Evaluated at the point of use (per
44
+ * render) rather than once at module load, so `prefetch="adaptive"` reacts to
45
+ * input-capability changes on hybrid devices (touch laptops, tablets gaining or
46
+ * losing a pointer) and after SSR -> hydrate. The SSR guard returns a stable
47
+ * `false` (pointer/hover default) so the resolved strategy doesn't drift on the
48
+ * server vs the first client render.
49
+ */
50
+ function isTouchDevice(): boolean {
51
+ if (typeof window === "undefined") return false;
52
+ return window.matchMedia("(hover: none)").matches;
53
+ }
44
54
 
45
55
  /**
46
56
  * Prefetch strategy for the Link component
47
57
  * - "hover": Prefetch on mouse enter (direct, no queue)
48
58
  * - "viewport": Prefetch when link enters viewport (queued, waits for idle)
49
59
  * - "render": Prefetch on component mount regardless of visibility (queued, waits for idle)
50
- * - "hybrid": Hover on pointer devices, viewport on touch devices
60
+ * - "adaptive": Hover on pointer devices, viewport on touch devices
51
61
  * - "none": No prefetching (default)
52
62
  */
53
63
  export type PrefetchStrategy =
54
64
  | "hover"
55
65
  | "viewport"
56
66
  | "render"
57
- | "hybrid"
67
+ | "adaptive"
58
68
  | "none";
59
69
 
70
+ /**
71
+ * Resolve a prefetch strategy, expanding "adaptive" to the concrete strategy
72
+ * for the CURRENT input capability: "viewport" on touch (no-hover) devices,
73
+ * "hover" on pointer devices. Non-adaptive strategies pass through unchanged.
74
+ * Reads touch capability live (not a module-load snapshot) so the result
75
+ * tracks input-capability changes.
76
+ */
77
+ export function resolveAdaptiveStrategy(
78
+ prefetch: PrefetchStrategy,
79
+ ): PrefetchStrategy {
80
+ if (prefetch !== "adaptive") return prefetch;
81
+ return isTouchDevice() ? "viewport" : "hover";
82
+ }
83
+
60
84
  /**
61
85
  * Link component props
62
86
  */
@@ -80,11 +104,46 @@ export interface LinkProps extends Omit<
80
104
  * Force full document navigation instead of SPA
81
105
  */
82
106
  reloadDocument?: boolean;
107
+ /**
108
+ * Whether to revalidate server data on navigation.
109
+ * Set to `false` to skip the RSC server fetch and only update the URL.
110
+ *
111
+ * Only takes effect when the pathname stays the same (search param / hash changes).
112
+ * If the pathname changes, this option is ignored and a full navigation occurs.
113
+ *
114
+ * @default true
115
+ */
116
+ revalidate?: boolean;
83
117
  /**
84
118
  * Prefetch strategy for the link destination
85
119
  * @default "none"
86
120
  */
87
121
  prefetch?: PrefetchStrategy;
122
+ /**
123
+ * Opt-in override for the prefetch cache scope.
124
+ *
125
+ * The default cache is source-agnostic: one shared entry per target,
126
+ * keyed on Rango state + target URL. This is correct for routes whose
127
+ * response shape doesn't depend on where the user navigates from.
128
+ *
129
+ * Set `":source"` when this Link's response would legitimately differ
130
+ * based on the source page — typically when the target route (or one
131
+ * of its layouts) uses a custom `revalidate()` handler that reads
132
+ * `currentUrl` / `currentParams`, and the wildcard entry would
133
+ * therefore serve the wrong diff to a navigation from a different
134
+ * source.
135
+ *
136
+ * Intercept responses are auto-scoped to the source via a server-side
137
+ * tag, so `":source"` is only needed for custom revalidation logic.
138
+ *
139
+ * @example
140
+ * ```tsx
141
+ * // Route uses a `revalidate()` that branches on currentUrl — opt in
142
+ * // so prefetches don't bleed across source pages.
143
+ * <Link to="/dashboard" prefetch="hover" prefetchKey=":source" />
144
+ * ```
145
+ */
146
+ prefetchKey?: ":source";
88
147
  /**
89
148
  * State to pass to history.pushState/replaceState.
90
149
  * Accessible via useLocationState() hook.
@@ -170,7 +229,9 @@ export const Link: ForwardRefExoticComponent<
170
229
  replace = false,
171
230
  scroll = true,
172
231
  reloadDocument = false,
232
+ revalidate,
173
233
  prefetch = "none",
234
+ prefetchKey,
174
235
  state,
175
236
  children,
176
237
  onClick,
@@ -181,9 +242,20 @@ export const Link: ForwardRefExoticComponent<
181
242
  const ctx = useContext(NavigationStoreContext);
182
243
  const isExternal = isExternalUrl(to);
183
244
 
184
- // Resolve hybrid: viewport on touch devices, hover on pointer devices
185
- const resolvedStrategy =
186
- prefetch === "hybrid" ? (isTouchDevice ? "viewport" : "hover") : prefetch;
245
+ // Auto-prefix with basename for app-local paths.
246
+ // Skip if external, already prefixed, or not a root-relative path.
247
+ const resolvedTo = useMemo(() => {
248
+ if (isExternal) return to;
249
+ const bn = ctx?.basename;
250
+ if (!bn || !to.startsWith("/") || to.startsWith(bn + "/") || to === bn)
251
+ return to;
252
+ return to === "/" ? bn : bn + to;
253
+ }, [to, isExternal, ctx?.basename]);
254
+
255
+ // Resolve adaptive: viewport on touch devices, hover on pointer devices.
256
+ // isTouchDevice() is read here (per render), not from a module-load snapshot,
257
+ // so a device whose input capability changes resolves to the current value.
258
+ const resolvedStrategy = resolveAdaptiveStrategy(prefetch);
187
259
 
188
260
  // Internal ref for viewport observation; merge with forwarded ref
189
261
  const internalRef = useRef<HTMLAnchorElement | null>(null);
@@ -262,17 +334,45 @@ export const Link: ForwardRefExoticComponent<
262
334
  resolvedState = currentState;
263
335
  }
264
336
 
265
- ctx.navigate(to, { replace, scroll, state: resolvedState });
337
+ ctx.navigate(resolvedTo, {
338
+ replace,
339
+ scroll,
340
+ state: resolvedState,
341
+ revalidate,
342
+ });
266
343
  },
267
- [to, isExternal, reloadDocument, replace, scroll, ctx, onClick],
344
+ [
345
+ resolvedTo,
346
+ isExternal,
347
+ reloadDocument,
348
+ replace,
349
+ scroll,
350
+ revalidate,
351
+ ctx,
352
+ onClick,
353
+ ],
268
354
  );
269
355
 
270
356
  const handleMouseEnter = useCallback(() => {
271
- if (resolvedStrategy === "hover" && !isExternal && ctx?.store) {
357
+ if (
358
+ (resolvedStrategy === "hover" || resolvedStrategy === "viewport") &&
359
+ !isExternal &&
360
+ ctx?.store
361
+ ) {
362
+ // For "hover", this is the primary prefetch trigger.
363
+ // For "viewport", this upgrades/prioritizes a potentially queued
364
+ // prefetch — prefetchDirect bypasses the queue, and hasPrefetch
365
+ // deduplicates if the viewport prefetch already completed.
272
366
  const segmentState = ctx.store.getSegmentState();
273
- prefetchDirect(to, segmentState.currentSegmentIds, ctx.version);
367
+ prefetchDirect(
368
+ resolvedTo,
369
+ segmentState.currentSegmentIds,
370
+ getAppVersion(),
371
+ ctx.store.getRouterId?.(),
372
+ prefetchKey,
373
+ );
274
374
  }
275
- }, [resolvedStrategy, to, isExternal, ctx]);
375
+ }, [resolvedStrategy, resolvedTo, isExternal, ctx, prefetchKey]);
276
376
 
277
377
  // Viewport/render prefetch: waits for idle before starting,
278
378
  // uses concurrency-limited queue to avoid flooding.
@@ -289,7 +389,13 @@ export const Link: ForwardRefExoticComponent<
289
389
  const triggerPrefetch = () => {
290
390
  if (cancelled) return;
291
391
  const segmentState = ctx.store.getSegmentState();
292
- prefetchQueued(to, segmentState.currentSegmentIds, ctx.version);
392
+ prefetchQueued(
393
+ resolvedTo,
394
+ segmentState.currentSegmentIds,
395
+ getAppVersion(),
396
+ ctx.store.getRouterId?.(),
397
+ prefetchKey,
398
+ );
293
399
  };
294
400
 
295
401
  // Schedule prefetch only when the app is idle (no navigation/streaming).
@@ -328,21 +434,22 @@ export const Link: ForwardRefExoticComponent<
328
434
  unobserveForPrefetch(observedElement);
329
435
  }
330
436
  };
331
- }, [resolvedStrategy, to, isExternal, ctx]);
437
+ }, [resolvedStrategy, resolvedTo, isExternal, ctx, prefetchKey]);
332
438
 
333
439
  return (
334
440
  <a
335
441
  ref={setRef}
336
- href={to}
442
+ href={resolvedTo}
337
443
  onClick={handleClick}
338
444
  onMouseEnter={handleMouseEnter}
339
445
  data-link-component
340
446
  data-external={isExternal ? "" : undefined}
341
447
  data-scroll={scroll === false ? "false" : undefined}
342
448
  data-replace={replace ? "true" : undefined}
449
+ data-revalidate={revalidate === false ? "false" : undefined}
343
450
  {...props}
344
451
  >
345
- <LinkContext.Provider value={to}>{children}</LinkContext.Provider>
452
+ <LinkContext.Provider value={resolvedTo}>{children}</LinkContext.Provider>
346
453
  </a>
347
454
  );
348
455
  });
@@ -3,8 +3,10 @@
3
3
  import React, {
4
4
  useState,
5
5
  useEffect,
6
+ useLayoutEffect,
6
7
  useCallback,
7
8
  useMemo,
9
+ useRef,
8
10
  use,
9
11
  type ReactNode,
10
12
  } from "react";
@@ -25,6 +27,10 @@ import { ThemeProvider } from "../../theme/ThemeProvider.js";
25
27
  import { NonceContext } from "./nonce-context.js";
26
28
  import type { ResolvedThemeConfig, Theme } from "../../theme/types.js";
27
29
  import { cancelAllPrefetches } from "../prefetch/queue.js";
30
+ import { handleNavigationEnd } from "../scroll-restoration.js";
31
+ import { createAppShellRef, type AppShellRef } from "../app-shell.js";
32
+ import { startConnectionWarmup } from "../connection-warmup.js";
33
+ import { debugLog } from "../logging.js";
28
34
 
29
35
  /**
30
36
  * Process handles from an async generator, updating the event controller
@@ -43,10 +49,22 @@ async function processHandles(
43
49
  store: NavigationStore;
44
50
  matched?: string[];
45
51
  isPartial?: boolean;
52
+ /** Server's `resolvedIds`: every segment re-resolved this request,
53
+ * including null-component ones excluded from `diff`/`segments`.
54
+ * Drives cleanup of stale handle buckets when a re-resolved segment
55
+ * pushed nothing. */
56
+ resolvedIds?: string[];
46
57
  historyKey: string;
47
58
  },
48
59
  ): Promise<void> {
49
- const { eventController, store, matched, isPartial, historyKey } = opts;
60
+ const {
61
+ eventController,
62
+ store,
63
+ matched,
64
+ isPartial,
65
+ resolvedIds,
66
+ historyKey,
67
+ } = opts;
50
68
 
51
69
  let yieldCount = 0;
52
70
  for await (const handleData of handlesGenerator) {
@@ -54,14 +72,14 @@ async function processHandles(
54
72
  // This prevents handle data from cancelled navigations polluting
55
73
  // the current route's breadcrumbs (e.g., quick popstate after clicking a link).
56
74
  if (historyKey !== store.getHistoryKey()) {
57
- console.log(
75
+ debugLog(
58
76
  "[NavigationProvider] Stopping handle processing - user navigated away",
59
77
  );
60
78
  return;
61
79
  }
62
80
 
63
81
  yieldCount++;
64
- eventController.setHandleData(handleData, matched, isPartial);
82
+ eventController.setHandleData(handleData, matched, isPartial, resolvedIds);
65
83
  }
66
84
 
67
85
  // Check again before final updates
@@ -69,12 +87,11 @@ async function processHandles(
69
87
  return;
70
88
  }
71
89
 
72
- // For partial updates where the generator yielded nothing (cached handlers),
73
- // we still need to update the segment order to clean up stale handle data.
74
- // This happens when navigating away from a route - the handlers for the new
75
- // route might not push any breadcrumbs, but we still need to remove the old ones.
90
+ // For partial updates where the generator yielded nothing (every
91
+ // re-resolved handler pushed nothing), still call setHandleData so the
92
+ // cleanup pass can clear out stale buckets for those segments.
76
93
  if (yieldCount === 0 && matched) {
77
- eventController.setHandleData({}, matched, true);
94
+ eventController.setHandleData({}, matched, true, resolvedIds);
78
95
  }
79
96
 
80
97
  // After handles processing completes, update the cache's handleData.
@@ -130,10 +147,33 @@ export interface NavigationProviderProps {
130
147
  warmupEnabled?: boolean;
131
148
 
132
149
  /**
133
- * App version from server payload (stable, immutable).
134
- * Forwarded to prefetch requests for version mismatch detection.
150
+ * App version from server payload.
151
+ * Used only as a fallback when `appShellRef` is not supplied.
135
152
  */
136
153
  version?: string;
154
+
155
+ /**
156
+ * URL prefix for all routes (from createRouter({ basename })).
157
+ * Used only as a fallback when `appShellRef` is not supplied.
158
+ */
159
+ basename?: string;
160
+
161
+ /**
162
+ * App-shell ref. When provided, the context's `basename` and `version` are
163
+ * read through it (live getters) so they don't close over a stale snapshot or
164
+ * invalidate the memoized context value. The shell is set once at init and is
165
+ * not swapped within a session — a cross-app navigation is a full document
166
+ * load (X-RSC-Reload), so the target app establishes its own shell on load.
167
+ */
168
+ appShellRef?: AppShellRef;
169
+
170
+ /**
171
+ * CSP nonce to expose via NonceContext. Production leaves this undefined — the
172
+ * browser has no nonce (it is a server-side HTML concern), and SSR provides the
173
+ * nonce through its own NonceContext.Provider. Test harnesses (renderRoute) set
174
+ * it to seed a nonce so components calling useNonce() can be exercised.
175
+ */
176
+ nonce?: string;
137
177
  }
138
178
 
139
179
  /**
@@ -166,6 +206,9 @@ export function NavigationProvider({
166
206
  initialTheme,
167
207
  warmupEnabled,
168
208
  version,
209
+ basename,
210
+ appShellRef,
211
+ nonce,
169
212
  }: NavigationProviderProps): ReactNode {
170
213
  // Track current payload for rendering (this triggers re-renders)
171
214
  const [payload, setPayload] = useState(initialPayload);
@@ -187,130 +230,100 @@ export function NavigationProvider({
187
230
  await bridge.refresh();
188
231
  }, []);
189
232
 
190
- // Context value is stable (store, eventController, navigate, refresh never change)
191
- const contextValue = useMemo<NavigationStoreContextValue>(
192
- () => ({
233
+ // basename/version are always read through a shell ref so the context value
234
+ // has a single shape. Both are set once: a supplied appShellRef is seeded
235
+ // from the init payload (a cross-app navigation reloads, so it is not swapped
236
+ // in-session), and the standalone fallback wraps the mount-time props.
237
+ const fallbackShellRef = useRef<AppShellRef | null>(null);
238
+ if (!fallbackShellRef.current) {
239
+ fallbackShellRef.current = createAppShellRef({ basename, version });
240
+ }
241
+ const shellRef = appShellRef ?? fallbackShellRef.current;
242
+
243
+ const contextValue = useMemo<NavigationStoreContextValue>(() => {
244
+ const value = {
193
245
  store,
194
246
  eventController,
195
247
  navigate,
196
248
  refresh,
197
- version,
198
- }),
199
- [],
200
- );
249
+ } as NavigationStoreContextValue;
250
+ Object.defineProperty(value, "basename", {
251
+ configurable: true,
252
+ enumerable: true,
253
+ get: () => shellRef.get().basename,
254
+ });
255
+ Object.defineProperty(value, "version", {
256
+ configurable: true,
257
+ enumerable: true,
258
+ get: () => shellRef.get().version,
259
+ });
260
+ return value;
261
+ }, []);
201
262
 
202
- // Connection warmup: keep TLS alive after idle periods.
203
- // After 60s of no user interaction, marks connection as "cold".
204
- // On next interaction or visibility change, sends a HEAD request to warm TLS
205
- // before the user actually clicks a link.
263
+ // Connection warmup: keep TLS alive after idle periods. After 60s of no
264
+ // interaction the connection is marked cold; the next pointer/touch
265
+ // interaction or visibility change warms TLS via a HEAD request before the
266
+ // user clicks a link. State machine lives in connection-warmup.ts.
206
267
  useEffect(() => {
207
268
  if (!warmupEnabled) return;
208
-
209
- const IDLE_TIMEOUT = 60_000;
210
- const DEBOUNCE_DELAY = 150;
211
-
212
- let idleTimer: ReturnType<typeof setTimeout> | undefined;
213
- let debounceTimer: ReturnType<typeof setTimeout> | undefined;
214
- let isCold = false;
215
- let warmupListenersAttached = false;
216
-
217
- function sendWarmup() {
218
- isCold = false;
219
- fetch("/?_rsc_warmup", { method: "HEAD" }).catch(() => {});
220
- }
221
-
222
- function triggerWarmup() {
223
- if (!isCold) return;
224
- clearTimeout(debounceTimer);
225
- debounceTimer = setTimeout(() => {
226
- sendWarmup();
227
- detachWarmupListeners();
228
- resetIdleTimer();
229
- }, DEBOUNCE_DELAY);
230
- }
231
-
232
- function onVisibilityChange() {
233
- if (document.visibilityState === "visible" && isCold) {
234
- triggerWarmup();
235
- }
236
- }
237
-
238
- function attachWarmupListeners() {
239
- if (warmupListenersAttached) return;
240
- warmupListenersAttached = true;
241
- document.addEventListener("visibilitychange", onVisibilityChange);
242
- document.addEventListener("mousemove", triggerWarmup, { once: true });
243
- document.addEventListener("touchstart", triggerWarmup, { once: true });
244
- }
245
-
246
- function detachWarmupListeners() {
247
- warmupListenersAttached = false;
248
- document.removeEventListener("visibilitychange", onVisibilityChange);
249
- document.removeEventListener("mousemove", triggerWarmup);
250
- document.removeEventListener("touchstart", triggerWarmup);
251
- }
252
-
253
- function markCold() {
254
- isCold = true;
255
- attachWarmupListeners();
256
- }
257
-
258
- function resetIdleTimer() {
259
- clearTimeout(idleTimer);
260
- isCold = false;
261
- idleTimer = setTimeout(markCold, IDLE_TIMEOUT);
262
- }
263
-
264
- // Activity events that reset the idle timer
265
- const activityEvents = [
266
- "mousemove",
267
- "keydown",
268
- "touchstart",
269
- "scroll",
270
- ] as const;
271
- const activityOptions: AddEventListenerOptions = { passive: true };
272
-
273
- for (const event of activityEvents) {
274
- document.addEventListener(event, resetIdleTimer, activityOptions);
275
- }
276
-
277
- resetIdleTimer();
278
-
279
- return () => {
280
- clearTimeout(idleTimer);
281
- clearTimeout(debounceTimer);
282
- detachWarmupListeners();
283
- for (const event of activityEvents) {
284
- document.removeEventListener(event, resetIdleTimer);
285
- }
286
- };
269
+ return startConnectionWarmup();
287
270
  }, [warmupEnabled]);
288
271
 
289
- // Cancel speculative prefetches when navigation starts.
290
- // Viewport/render prefetches should not compete with navigation fetches.
272
+ // Cancel non-matching prefetches when navigation starts.
273
+ // Frees connections so the navigation fetch isn't competing with
274
+ // speculative prefetches. The prefetch matching the navigation target
275
+ // is kept alive so it can be reused via consumeInflightPrefetch.
291
276
  useEffect(() => {
292
277
  let wasIdle = true;
293
278
  const unsub = eventController.subscribe(() => {
294
279
  const state = eventController.getState();
295
280
  const isIdle = state.state === "idle" && !state.isStreaming;
296
281
  if (wasIdle && !isIdle) {
297
- cancelAllPrefetches();
282
+ cancelAllPrefetches(state.pendingUrl);
298
283
  }
299
284
  wasIdle = isIdle;
300
285
  });
301
286
  return unsub;
302
287
  }, [eventController]);
303
288
 
289
+ // Pending scroll action to apply after React commits
290
+ const pendingScrollRef = useRef<NavigationUpdate["scroll"]>(undefined);
291
+
292
+ // Apply scroll after React commits the new content to the DOM
293
+ useLayoutEffect(() => {
294
+ const scrollAction = pendingScrollRef.current;
295
+ if (!scrollAction) return;
296
+ pendingScrollRef.current = undefined;
297
+
298
+ if (scrollAction.enabled === false) return;
299
+
300
+ handleNavigationEnd({
301
+ restore: scrollAction.restore,
302
+ scroll: scrollAction.enabled,
303
+ isStreaming: scrollAction.isStreaming,
304
+ });
305
+ });
306
+
304
307
  // Subscribe to UI updates (for re-rendering the tree)
305
308
  useEffect(() => {
306
309
  const unsubscribe = store.onUpdate((update) => {
310
+ // Capture scroll intent — it will be applied in useLayoutEffect
311
+ // after React commits this state update to the DOM.
312
+ // Always assign (even undefined) to clear stale scroll from prior navigations,
313
+ // so server actions or error updates don't accidentally replay old scroll.
314
+ pendingScrollRef.current = update.scroll;
315
+
307
316
  setPayload({
308
317
  root: update.root,
309
318
  metadata: update.metadata,
310
319
  });
311
320
 
312
- // Update route params
313
- eventController.setParams(update.metadata.params ?? {});
321
+ // Update route params. Only reset when the server actually sends a params
322
+ // map — an absent `params` field means "no change" (e.g., legacy action
323
+ // responses that omitted params). Explicit `{}` still clears correctly.
324
+ if (update.metadata.params !== undefined) {
325
+ eventController.setParams(update.metadata.params);
326
+ }
314
327
 
315
328
  // Update handle data progressively as it streams in
316
329
  if (update.metadata.handles) {
@@ -323,24 +336,20 @@ export function NavigationProvider({
323
336
  store,
324
337
  matched: update.metadata.matched,
325
338
  isPartial: update.metadata.isPartial,
339
+ resolvedIds: update.metadata.resolvedIds,
326
340
  historyKey,
327
341
  }).catch((err) =>
328
342
  console.error("[NavigationProvider] Error consuming handles:", err),
329
343
  );
330
- } else if (update.metadata.cachedHandleData) {
331
- // For back/forward navigation from cache, restore the cached handleData
332
- // This restores breadcrumbs to the exact state they were when the page was cached
333
- eventController.setHandleData(
334
- update.metadata.cachedHandleData,
335
- update.metadata.matched,
336
- false, // full replace - restore entire cached state
337
- );
338
344
  } else if (update.metadata.matched) {
339
- // For cached navigations without handleData, update segmentOrder to clean up stale data
345
+ // cachedHandleData present -> full restore (back/forward); absent ->
346
+ // partial cleanup of segments no longer matched.
347
+ const cached = update.metadata.cachedHandleData;
340
348
  eventController.setHandleData(
341
- {}, // Empty data - all existing data not in matched will be cleaned up
349
+ cached ?? {},
342
350
  update.metadata.matched,
343
- true, // partial update - will clean up segments not in matched
351
+ cached === undefined,
352
+ cached === undefined ? update.metadata.resolvedIds : undefined,
344
353
  );
345
354
  }
346
355
  });
@@ -353,7 +362,8 @@ export function NavigationProvider({
353
362
  payload.root instanceof Promise ? use(payload.root) : payload.root;
354
363
 
355
364
  // Wrap content in RootErrorBoundary to catch:
356
- // 1. Errors from NetworkErrorThrower (rendered during network failures)
365
+ // 1. Errors from RenderErrorThrower (network failures and unprocessable
366
+ // navigation responses, routed here by the navigation bridge)
357
367
  // 2. Client component errors that occur before/outside the segment tree's error boundary
358
368
  // 3. Errors during promise resolution or navigation state updates
359
369
  // This acts as a safety net - the segment tree has its own RootErrorBoundary that
@@ -362,7 +372,11 @@ export function NavigationProvider({
362
372
  // Build the content tree
363
373
  let content = <RootErrorBoundary>{root}</RootErrorBoundary>;
364
374
 
365
- // Wrap with ThemeProvider when theme is enabled
375
+ // Wrap with ThemeProvider when theme is enabled. The ThemeProvider is
376
+ // document-lifetime: its config comes from the initial load and persists for
377
+ // the session. It sits above the segment tree and is not remounted in-session;
378
+ // a cross-app navigation is a full document load (X-RSC-Reload), so the target
379
+ // app's theme config takes effect on its own load.
366
380
  if (themeConfig) {
367
381
  content = (
368
382
  <ThemeProvider config={themeConfig} initialTheme={initialTheme}>
@@ -373,9 +387,10 @@ export function NavigationProvider({
373
387
 
374
388
  // Match SSR tree shape: NonceContext.Provider is always present so
375
389
  // hydration sees the same component tree. Value is undefined on the
376
- // client — CSP nonces are a server-side HTML concern.
390
+ // client — CSP nonces are a server-side HTML concern — unless a test
391
+ // harness seeded one via the `nonce` prop.
377
392
  content = (
378
- <NonceContext.Provider value={undefined}>{content}</NonceContext.Provider>
393
+ <NonceContext.Provider value={nonce}>{content}</NonceContext.Provider>
379
394
  );
380
395
 
381
396
  return (
@@ -14,17 +14,21 @@ export interface ScrollRestorationProps {
14
14
  * Return location.pathname to restore scroll based on path
15
15
  * (useful for keeping scroll position on the same page).
16
16
  *
17
+ * Provide a stable reference: a module-level function or one wrapped in
18
+ * useCallback. The init effect re-runs when getKey's identity changes, and
19
+ * teardown clears in-memory scroll positions — a fresh inline arrow on every
20
+ * parent render would discard unpersisted positions mid-session.
21
+ *
17
22
  * @example
18
23
  * ```tsx
24
+ * // Stable module-level getKey (recommended)
25
+ * const byPathname = (location) => location.pathname;
26
+ *
19
27
  * // Restore based on pathname (same URL = same scroll)
20
- * <ScrollRestoration
21
- * getKey={(location) => location.pathname}
22
- * />
28
+ * <ScrollRestoration getKey={byPathname} />
23
29
  *
24
30
  * // Restore based on unique history entry (default)
25
- * <ScrollRestoration
26
- * getKey={(location) => location.key}
27
- * />
31
+ * // <ScrollRestoration /> — omit getKey to use location.key
28
32
  * ```
29
33
  */
30
34
  getKey?: (location: {
@@ -43,10 +43,15 @@ export interface NavigationStoreContextValue {
43
43
  refresh: () => Promise<void>;
44
44
 
45
45
  /**
46
- * App version from server payload (stable, immutable).
47
- * Used in prefetch requests for version mismatch detection.
46
+ * App version from the initial server payload.
48
47
  */
49
48
  version: string | undefined;
49
+
50
+ /**
51
+ * URL prefix for all routes (from createRouter({ basename })).
52
+ * Used by Link and useRouter() to auto-prefix app-local paths.
53
+ */
54
+ basename: string | undefined;
50
55
  }
51
56
 
52
57
  /**