@rangojs/router 0.0.0-experimental.1b930379 → 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 (400) hide show
  1. package/AGENTS.md +12 -0
  2. package/README.md +245 -49
  3. package/dist/bin/rango.js +441 -134
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3453 -1240
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +76 -21
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +64 -2
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +247 -23
  12. package/skills/caching/SKILL.md +318 -15
  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 +250 -30
  19. package/skills/host-router/SKILL.md +83 -23
  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 +279 -53
  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 +153 -109
  32. package/skills/rango/SKILL.md +251 -22
  33. package/skills/react-compiler/SKILL.md +168 -0
  34. package/skills/response-routes/SKILL.md +123 -48
  35. package/skills/route/SKILL.md +101 -5
  36. package/skills/router-setup/SKILL.md +116 -8
  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 +129 -0
  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 +332 -29
  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 +1 -66
  62. package/src/browser/action-coordinator.ts +53 -36
  63. package/src/browser/action-fence.ts +47 -0
  64. package/src/browser/app-shell.ts +39 -0
  65. package/src/browser/app-version.ts +14 -0
  66. package/src/browser/connection-warmup.ts +134 -0
  67. package/src/browser/cookie-name.ts +140 -0
  68. package/src/browser/event-controller.ts +197 -150
  69. package/src/browser/history-state.ts +21 -0
  70. package/src/browser/index.ts +3 -3
  71. package/src/browser/invalidate-client-cache.ts +52 -0
  72. package/src/browser/navigation-bridge.ts +111 -31
  73. package/src/browser/navigation-client.ts +201 -67
  74. package/src/browser/navigation-store-handle.ts +38 -0
  75. package/src/browser/navigation-store.ts +76 -67
  76. package/src/browser/navigation-transaction.ts +18 -66
  77. package/src/browser/network-error-handler.ts +34 -7
  78. package/src/browser/partial-update.ts +187 -112
  79. package/src/browser/prefetch/cache.ts +230 -35
  80. package/src/browser/prefetch/fetch.ts +338 -39
  81. package/src/browser/prefetch/queue.ts +126 -20
  82. package/src/browser/prefetch/resource-ready.ts +77 -0
  83. package/src/browser/rango-state.ts +158 -76
  84. package/src/browser/react/Link.tsx +111 -16
  85. package/src/browser/react/NavigationProvider.tsx +135 -120
  86. package/src/browser/react/ScrollRestoration.tsx +10 -6
  87. package/src/browser/react/context.ts +7 -2
  88. package/src/browser/react/filter-segment-order.ts +66 -7
  89. package/src/browser/react/index.ts +0 -48
  90. package/src/browser/react/location-state-shared.ts +178 -8
  91. package/src/browser/react/location-state.ts +39 -14
  92. package/src/browser/react/use-action.ts +6 -15
  93. package/src/browser/react/use-handle.ts +23 -69
  94. package/src/browser/react/use-href.tsx +8 -1
  95. package/src/browser/react/use-link-status.ts +33 -8
  96. package/src/browser/react/use-navigation.ts +32 -7
  97. package/src/browser/react/use-params.ts +20 -10
  98. package/src/browser/react/use-reverse.ts +106 -0
  99. package/src/browser/react/use-router.ts +46 -11
  100. package/src/browser/react/use-search-params.ts +0 -5
  101. package/src/browser/react/use-segments.ts +11 -21
  102. package/src/browser/response-adapter.ts +80 -5
  103. package/src/browser/rsc-router.tsx +218 -76
  104. package/src/browser/scroll-restoration.ts +54 -42
  105. package/src/browser/segment-reconciler.ts +36 -9
  106. package/src/browser/segment-structure-assert.ts +2 -2
  107. package/src/browser/server-action-bridge.ts +222 -61
  108. package/src/browser/types.ts +91 -11
  109. package/src/browser/validate-redirect-origin.ts +43 -16
  110. package/src/build/collect-fallback-refs.ts +107 -0
  111. package/src/build/generate-manifest.ts +65 -40
  112. package/src/build/generate-route-types.ts +5 -1
  113. package/src/build/index.ts +8 -2
  114. package/src/build/prefix-tree-utils.ts +123 -0
  115. package/src/build/route-trie.ts +165 -36
  116. package/src/build/route-types/ast-route-extraction.ts +15 -8
  117. package/src/build/route-types/codegen.ts +16 -5
  118. package/src/build/route-types/include-resolution.ts +125 -24
  119. package/src/build/route-types/param-extraction.ts +6 -3
  120. package/src/build/route-types/per-module-writer.ts +22 -6
  121. package/src/build/route-types/router-processing.ts +272 -96
  122. package/src/build/route-types/scan-filter.ts +9 -2
  123. package/src/build/route-types/source-scan.ts +216 -0
  124. package/src/build/runtime-discovery.ts +9 -20
  125. package/src/cache/cache-error.ts +104 -0
  126. package/src/cache/cache-key-utils.ts +29 -13
  127. package/src/cache/cache-policy.ts +108 -34
  128. package/src/cache/cache-runtime.ts +214 -48
  129. package/src/cache/cache-scope.ts +236 -89
  130. package/src/cache/cache-tag.ts +103 -0
  131. package/src/cache/cf/cf-base64.ts +33 -0
  132. package/src/cache/cf/cf-cache-constants.ts +127 -0
  133. package/src/cache/cf/cf-cache-store.ts +2224 -171
  134. package/src/cache/cf/cf-cache-types.ts +349 -0
  135. package/src/cache/cf/cf-kv-utils.ts +46 -0
  136. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  137. package/src/cache/cf/index.ts +11 -17
  138. package/src/cache/document-cache.ts +89 -27
  139. package/src/cache/handle-snapshot.ts +70 -0
  140. package/src/cache/index.ts +11 -20
  141. package/src/cache/memory-segment-store.ts +136 -37
  142. package/src/cache/profile-registry.ts +31 -31
  143. package/src/cache/read-through-swr.ts +41 -11
  144. package/src/cache/segment-codec.ts +9 -17
  145. package/src/cache/tag-invalidation.ts +230 -0
  146. package/src/cache/taint.ts +55 -0
  147. package/src/cache/types.ts +37 -100
  148. package/src/client.rsc.tsx +44 -21
  149. package/src/client.tsx +119 -290
  150. package/src/cloudflare/index.ts +11 -0
  151. package/src/cloudflare/tracing.ts +109 -0
  152. package/src/component-utils.ts +19 -0
  153. package/src/components/DefaultDocument.tsx +8 -2
  154. package/src/context-var.ts +84 -2
  155. package/src/debug.ts +2 -2
  156. package/src/decode-loader-results.ts +52 -0
  157. package/src/defer.ts +196 -0
  158. package/src/deps/ssr.ts +0 -1
  159. package/src/encode-kv.ts +49 -0
  160. package/src/errors.ts +30 -4
  161. package/src/escape-script.ts +52 -0
  162. package/src/handle.ts +70 -22
  163. package/src/handles/MetaTags.tsx +56 -19
  164. package/src/handles/Scripts.tsx +183 -0
  165. package/src/handles/breadcrumbs.ts +37 -8
  166. package/src/handles/is-thenable.ts +19 -0
  167. package/src/handles/meta.ts +51 -40
  168. package/src/handles/script.ts +244 -0
  169. package/src/host/cookie-handler.ts +9 -60
  170. package/src/host/errors.ts +0 -24
  171. package/src/host/index.ts +8 -2
  172. package/src/host/pattern-matcher.ts +23 -52
  173. package/src/host/router.ts +107 -99
  174. package/src/host/testing.ts +40 -27
  175. package/src/host/types.ts +37 -4
  176. package/src/host/utils.ts +1 -1
  177. package/src/href-client.ts +137 -22
  178. package/src/index.rsc.ts +93 -12
  179. package/src/index.ts +133 -15
  180. package/src/internal-debug.ts +11 -10
  181. package/src/loader-store.ts +500 -0
  182. package/src/loader.rsc.ts +20 -13
  183. package/src/loader.ts +12 -11
  184. package/src/missing-id-error.ts +68 -0
  185. package/src/outlet-context.ts +1 -1
  186. package/src/outlet-provider.tsx +1 -5
  187. package/src/prerender/param-hash.ts +16 -16
  188. package/src/prerender/store.ts +37 -41
  189. package/src/prerender.ts +198 -82
  190. package/src/redirect-origin.ts +100 -0
  191. package/src/regex-escape.ts +8 -0
  192. package/src/render-error-thrower.tsx +20 -0
  193. package/src/response-utils.ts +62 -0
  194. package/src/reverse.ts +65 -15
  195. package/src/root-error-boundary.tsx +1 -19
  196. package/src/route-content-wrapper.tsx +7 -72
  197. package/src/route-definition/dsl-helpers.ts +469 -276
  198. package/src/route-definition/helper-factories.ts +29 -139
  199. package/src/route-definition/helpers-types.ts +113 -37
  200. package/src/route-definition/index.ts +3 -0
  201. package/src/route-definition/redirect.ts +53 -12
  202. package/src/route-definition/resolve-handler-use.ts +161 -0
  203. package/src/route-definition/use-item-types.ts +32 -0
  204. package/src/route-map-builder.ts +7 -17
  205. package/src/route-types.ts +37 -41
  206. package/src/router/basename.ts +14 -0
  207. package/src/router/content-negotiation.ts +164 -17
  208. package/src/router/error-handling.ts +45 -18
  209. package/src/router/find-match.ts +45 -22
  210. package/src/router/handler-context.ts +83 -39
  211. package/src/router/instrument.ts +350 -0
  212. package/src/router/intercept-resolution.ts +50 -24
  213. package/src/router/lazy-includes.ts +19 -53
  214. package/src/router/loader-resolution.ts +274 -56
  215. package/src/router/logging.ts +5 -8
  216. package/src/router/manifest.ts +49 -45
  217. package/src/router/match-api.ts +120 -204
  218. package/src/router/match-context.ts +0 -22
  219. package/src/router/match-handlers.ts +58 -58
  220. package/src/router/match-middleware/background-revalidation.ts +33 -6
  221. package/src/router/match-middleware/cache-lookup.ts +214 -263
  222. package/src/router/match-middleware/cache-store.ts +73 -33
  223. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  224. package/src/router/match-middleware/segment-resolution.ts +52 -18
  225. package/src/router/match-pipelines.ts +1 -42
  226. package/src/router/match-result.ts +104 -40
  227. package/src/router/metrics.ts +5 -34
  228. package/src/router/middleware-types.ts +13 -142
  229. package/src/router/middleware.ts +270 -172
  230. package/src/router/navigation-snapshot.ts +131 -0
  231. package/src/router/params-util.ts +23 -0
  232. package/src/router/pattern-matching.ts +132 -90
  233. package/src/router/prefetch-cache-ttl.ts +51 -0
  234. package/src/router/prerender-match.ts +195 -56
  235. package/src/router/preview-match.ts +32 -102
  236. package/src/router/request-classification.ts +276 -0
  237. package/src/router/revalidation.ts +123 -73
  238. package/src/router/route-snapshot.ts +244 -0
  239. package/src/router/router-context.ts +8 -28
  240. package/src/router/router-interfaces.ts +115 -35
  241. package/src/router/router-options.ts +172 -15
  242. package/src/router/router-registry.ts +2 -5
  243. package/src/router/segment-resolution/fresh.ts +264 -77
  244. package/src/router/segment-resolution/helpers.ts +115 -30
  245. package/src/router/segment-resolution/loader-cache.ts +63 -37
  246. package/src/router/segment-resolution/revalidation.ts +474 -385
  247. package/src/router/segment-resolution/static-store.ts +19 -5
  248. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  249. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  250. package/src/router/segment-resolution.ts +5 -1
  251. package/src/router/segment-wrappers.ts +8 -5
  252. package/src/router/state-cookie-name.ts +33 -0
  253. package/src/router/substitute-pattern-params.ts +56 -0
  254. package/src/router/telemetry-otel.ts +161 -199
  255. package/src/router/telemetry.ts +96 -19
  256. package/src/router/timeout.ts +0 -20
  257. package/src/router/tracing.ts +206 -0
  258. package/src/router/trie-matching.ts +163 -59
  259. package/src/router/types.ts +10 -63
  260. package/src/router/url-params.ts +44 -0
  261. package/src/router.ts +163 -55
  262. package/src/rsc/handler-context.ts +3 -2
  263. package/src/rsc/handler.ts +658 -511
  264. package/src/rsc/helpers.ts +168 -46
  265. package/src/rsc/index.ts +2 -5
  266. package/src/rsc/json-route-result.ts +38 -0
  267. package/src/rsc/loader-fetch.ts +127 -31
  268. package/src/rsc/manifest-init.ts +33 -42
  269. package/src/rsc/origin-guard.ts +39 -25
  270. package/src/rsc/progressive-enhancement.ts +77 -11
  271. package/src/rsc/redirect-guard.ts +99 -0
  272. package/src/rsc/response-cache-serve.ts +238 -0
  273. package/src/rsc/response-error.ts +79 -12
  274. package/src/rsc/response-route-handler.ts +99 -189
  275. package/src/rsc/rsc-rendering.ts +105 -72
  276. package/src/rsc/runtime-warnings.ts +23 -10
  277. package/src/rsc/server-action.ts +263 -112
  278. package/src/rsc/ssr-setup.ts +18 -2
  279. package/src/rsc/types.ts +32 -6
  280. package/src/runtime-env.ts +18 -0
  281. package/src/search-params.ts +35 -30
  282. package/src/segment-content-promise.ts +67 -0
  283. package/src/segment-loader-promise.ts +149 -0
  284. package/src/segment-system.tsx +281 -129
  285. package/src/serialize.ts +243 -0
  286. package/src/server/context.ts +309 -61
  287. package/src/server/cookie-parse.ts +32 -0
  288. package/src/server/cookie-store.ts +80 -5
  289. package/src/server/handle-store.ts +40 -38
  290. package/src/server/loader-registry.ts +26 -46
  291. package/src/server/request-context.ts +398 -172
  292. package/src/ssr/index.tsx +25 -16
  293. package/src/static-handler.ts +27 -18
  294. package/src/testing/cache-status.ts +162 -0
  295. package/src/testing/collect-handle.ts +40 -0
  296. package/src/testing/dispatch.ts +701 -0
  297. package/src/testing/dom.entry.ts +22 -0
  298. package/src/testing/e2e/fixture.ts +188 -0
  299. package/src/testing/e2e/index.ts +128 -0
  300. package/src/testing/e2e/matchers.ts +35 -0
  301. package/src/testing/e2e/page-helpers.ts +272 -0
  302. package/src/testing/e2e/parity.ts +387 -0
  303. package/src/testing/e2e/server.ts +195 -0
  304. package/src/testing/flight-matchers.ts +97 -0
  305. package/src/testing/flight-normalize.ts +11 -0
  306. package/src/testing/flight-runtime.d.ts +57 -0
  307. package/src/testing/flight-tree.ts +682 -0
  308. package/src/testing/flight.entry.ts +52 -0
  309. package/src/testing/flight.ts +257 -0
  310. package/src/testing/generated-routes.ts +183 -0
  311. package/src/testing/index.ts +99 -0
  312. package/src/testing/internal/context.ts +371 -0
  313. package/src/testing/internal/flight-client-globals.ts +30 -0
  314. package/src/testing/internal/seed-vars.ts +54 -0
  315. package/src/testing/render-handler.ts +343 -0
  316. package/src/testing/render-route.tsx +581 -0
  317. package/src/testing/run-loader.ts +385 -0
  318. package/src/testing/run-middleware.ts +205 -0
  319. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  320. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  321. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  322. package/src/testing/vitest-stubs/version.ts +5 -0
  323. package/src/testing/vitest.ts +305 -0
  324. package/src/theme/ThemeProvider.tsx +20 -58
  325. package/src/theme/ThemeScript.tsx +7 -9
  326. package/src/theme/constants.ts +52 -13
  327. package/src/theme/index.ts +0 -7
  328. package/src/theme/theme-context.ts +1 -5
  329. package/src/theme/theme-script.ts +22 -21
  330. package/src/theme/use-theme.ts +0 -3
  331. package/src/types/boundaries.ts +0 -35
  332. package/src/types/cache-types.ts +17 -8
  333. package/src/types/error-types.ts +30 -90
  334. package/src/types/global-namespace.ts +54 -41
  335. package/src/types/handler-context.ts +233 -81
  336. package/src/types/index.ts +1 -10
  337. package/src/types/loader-types.ts +44 -15
  338. package/src/types/request-scope.ts +112 -0
  339. package/src/types/route-config.ts +6 -50
  340. package/src/types/route-entry.ts +19 -7
  341. package/src/types/segments.ts +37 -14
  342. package/src/urls/include-helper.ts +33 -70
  343. package/src/urls/index.ts +1 -11
  344. package/src/urls/path-helper-types.ts +58 -11
  345. package/src/urls/path-helper.ts +57 -111
  346. package/src/urls/pattern-types.ts +48 -19
  347. package/src/urls/response-types.ts +25 -22
  348. package/src/urls/type-extraction.ts +58 -139
  349. package/src/urls/urls-function.ts +1 -18
  350. package/src/use-loader.tsx +346 -89
  351. package/src/vite/debug.ts +185 -0
  352. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  353. package/src/vite/discovery/discover-routers.ts +130 -85
  354. package/src/vite/discovery/discovery-errors.ts +194 -0
  355. package/src/vite/discovery/gate-state.ts +171 -0
  356. package/src/vite/discovery/prerender-collection.ts +214 -132
  357. package/src/vite/discovery/route-types-writer.ts +40 -84
  358. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  359. package/src/vite/discovery/state.ts +57 -6
  360. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  361. package/src/vite/index.ts +6 -0
  362. package/src/vite/inject-client-debug.ts +36 -0
  363. package/src/vite/plugin-types.ts +155 -65
  364. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  365. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  366. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  367. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  368. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  369. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  370. package/src/vite/plugins/expose-action-id.ts +49 -98
  371. package/src/vite/plugins/expose-id-utils.ts +96 -51
  372. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  373. package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
  374. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  375. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  376. package/src/vite/plugins/expose-internal-ids.ts +553 -317
  377. package/src/vite/plugins/performance-tracks.ts +89 -0
  378. package/src/vite/plugins/refresh-cmd.ts +89 -27
  379. package/src/vite/plugins/use-cache-transform.ts +73 -83
  380. package/src/vite/plugins/version-injector.ts +21 -25
  381. package/src/vite/plugins/version-plugin.ts +46 -37
  382. package/src/vite/plugins/virtual-entries.ts +13 -18
  383. package/src/vite/rango.ts +238 -295
  384. package/src/vite/router-discovery.ts +940 -149
  385. package/src/vite/utils/ast-handler-extract.ts +26 -35
  386. package/src/vite/utils/banner.ts +4 -4
  387. package/src/vite/utils/bundle-analysis.ts +10 -15
  388. package/src/vite/utils/client-chunks.ts +184 -0
  389. package/src/vite/utils/directive-prologue.ts +40 -0
  390. package/src/vite/utils/forward-user-plugins.ts +171 -0
  391. package/src/vite/utils/manifest-utils.ts +4 -59
  392. package/src/vite/utils/package-resolution.ts +20 -52
  393. package/src/vite/utils/prerender-utils.ts +81 -34
  394. package/src/vite/utils/shared-utils.ts +92 -42
  395. package/src/browser/action-response-classifier.ts +0 -99
  396. package/src/browser/react/use-client-cache.ts +0 -58
  397. package/src/browser/shallow.ts +0 -40
  398. package/src/handles/index.ts +0 -7
  399. package/src/network-error-thrower.tsx +0 -23
  400. package/src/router/middleware-cookies.ts +0 -55
@@ -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
  /**
@@ -1,11 +1,70 @@
1
1
  /**
2
- * Filter segment IDs to only include routes and layouts.
3
- * Excludes parallels (contain .@) and loaders (contain D followed by digit).
2
+ * Build the handle-collection segment order from a raw `matched` list.
3
+ *
4
+ * Two responsibilities:
5
+ *
6
+ * 1. Drop loader sub-ids ("D" followed by a digit, e.g. "M0L0D1.user") —
7
+ * loaders never push handles.
8
+ *
9
+ * 2. Place each parallel slot id (contains ".@") immediately after its
10
+ * parent layout/route id. Raw segment-resolution emission order does NOT
11
+ * guarantee this: route-mounted parallels are resolved/pushed BEFORE the
12
+ * route handler's segment is appended (see fresh.ts:resolveSegment for
13
+ * routes, and revalidation.ts ~915-919), so matched can read
14
+ * `[..., R0.@panel, R0]`. collectHandleData consumes segmentOrder verbatim
15
+ * with later-wins semantics, so without normalization the route handler's
16
+ * Meta would override the slot's more-specific Meta — backwards.
17
+ *
18
+ * Slot-id format is `<parentShortCode>.@<slotName>`; `parentShortCode` never
19
+ * contains ".@", so splitting at the first ".@" reliably yields the parent.
4
20
  */
5
21
  export function filterSegmentOrder(matched: string[]): string[] {
6
- return matched.filter((id) => {
7
- if (id.includes(".@")) return false;
8
- if (/D\d+\./.test(id)) return false;
9
- return true;
10
- });
22
+ const slotsByParent = new Map<string, string[]>();
23
+ const nonSlots: string[] = [];
24
+ const nonSlotSet = new Set<string>();
25
+
26
+ for (const id of matched) {
27
+ if (/D\d+\./.test(id)) continue;
28
+ const slotIdx = id.indexOf(".@");
29
+ if (slotIdx >= 0) {
30
+ const parent = id.slice(0, slotIdx);
31
+ const list = slotsByParent.get(parent);
32
+ if (list) {
33
+ list.push(id);
34
+ } else {
35
+ slotsByParent.set(parent, [id]);
36
+ }
37
+ } else {
38
+ nonSlots.push(id);
39
+ nonSlotSet.add(id);
40
+ }
41
+ }
42
+
43
+ const result: string[] = [];
44
+ for (const id of nonSlots) {
45
+ result.push(id);
46
+ const slots = slotsByParent.get(id);
47
+ if (slots) result.push(...slots);
48
+ }
49
+ for (const [parent, slots] of slotsByParent) {
50
+ if (!nonSlotSet.has(parent)) result.push(...slots);
51
+ }
52
+ return result;
53
+ }
54
+
55
+ /**
56
+ * Build the "layouts and routes only" id list for useSegments().segmentIds.
57
+ *
58
+ * Strips parallel slot ids (contain ".@") and loader sub-ids ("D" followed by
59
+ * a digit, e.g. "M0L0D1.user") without reordering. Distinct from
60
+ * filterSegmentOrder, which also reorders slots after their parent for handle
61
+ * collection.
62
+ *
63
+ * Shared by SSR (ssr/index.tsx) and the client event controller
64
+ * (event-controller.ts) so both produce identical output; if they diverge,
65
+ * useSegments().segmentIds rendered during SSR and after hydration disagree
66
+ * and React reports a hydration mismatch.
67
+ */
68
+ export function filterRouteSegmentIds(matched: string[]): string[] {
69
+ return matched.filter((id) => !id.includes(".@") && !/D\d+\./.test(id));
11
70
  }
@@ -1,52 +1,4 @@
1
- // React exports for browser navigation
2
-
3
- // Hook with Zustand-style selectors
4
- export { useNavigation } from "./use-navigation.js";
5
-
6
- // Router actions hook (stable reference, no re-renders)
7
- export { useRouter } from "./use-router.js";
8
-
9
- // URL hooks
10
- export { usePathname } from "./use-pathname.js";
11
- export { useSearchParams } from "./use-search-params.js";
12
- export { useParams } from "./use-params.js";
13
-
14
- // Action state tracking hook
15
- export { useAction, type TrackedActionState } from "./use-action.js";
16
-
17
- // Segments state hook
18
- export { useSegments, type SegmentsState } from "./use-segments.js";
19
-
20
- // Handle data hook
21
- export { useHandle } from "./use-handle.js";
22
-
23
- // Client cache controls hook
24
- export {
25
- useClientCache,
26
- type ClientCacheControls,
27
- } from "./use-client-cache.js";
28
-
29
- // Provider
30
1
  export {
31
2
  NavigationProvider,
32
3
  type NavigationProviderProps,
33
4
  } from "./NavigationProvider.js";
34
-
35
- // Context (for advanced usage)
36
- export {
37
- NavigationStoreContext,
38
- type NavigationStoreContextValue,
39
- } from "./context.js";
40
-
41
- // Link component
42
- export { Link, type LinkProps, type PrefetchStrategy } from "./Link.js";
43
-
44
- // Link status hook
45
- export { useLinkStatus, type LinkStatus } from "./use-link-status.js";
46
-
47
- // Scroll restoration
48
- export {
49
- ScrollRestoration,
50
- useScrollRestoration,
51
- type ScrollRestorationProps,
52
- } from "./ScrollRestoration.js";