@rangojs/router 0.0.0-experimental.32 → 0.0.0-experimental.3232cd17

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 (376) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +198 -44
  3. package/dist/bin/rango.js +287 -105
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3248 -1117
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +73 -21
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +107 -1
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +245 -21
  12. package/skills/caching/SKILL.md +302 -6
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +364 -0
  17. package/skills/hooks/SKILL.md +270 -30
  18. package/skills/host-router/SKILL.md +82 -22
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +49 -5
  21. package/skills/layout/SKILL.md +35 -9
  22. package/skills/links/SKILL.md +249 -17
  23. package/skills/loader/SKILL.md +294 -30
  24. package/skills/middleware/SKILL.md +52 -13
  25. package/skills/migrate-nextjs/SKILL.md +584 -0
  26. package/skills/migrate-react-router/SKILL.md +769 -0
  27. package/skills/mime-routes/SKILL.md +27 -0
  28. package/skills/observability/SKILL.md +137 -0
  29. package/skills/parallel/SKILL.md +203 -7
  30. package/skills/prerender/SKILL.md +123 -100
  31. package/skills/rango/SKILL.md +250 -22
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +122 -47
  34. package/skills/route/SKILL.md +97 -5
  35. package/skills/router-setup/SKILL.md +90 -5
  36. package/skills/server-actions/SKILL.md +775 -0
  37. package/skills/streams-and-websockets/SKILL.md +283 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +129 -0
  40. package/skills/testing/bindings.md +89 -0
  41. package/skills/testing/cache-prerender.md +124 -0
  42. package/skills/testing/client-components.md +122 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +92 -0
  45. package/skills/testing/handles.md +129 -0
  46. package/skills/testing/loader.md +128 -0
  47. package/skills/testing/middleware.md +99 -0
  48. package/skills/testing/render-handler.md +121 -0
  49. package/skills/testing/response-routes.md +95 -0
  50. package/skills/testing/reverse-and-types.md +84 -0
  51. package/skills/testing/server-actions.md +107 -0
  52. package/skills/testing/server-tree.md +128 -0
  53. package/skills/testing/setup.md +120 -0
  54. package/skills/typesafety/SKILL.md +329 -27
  55. package/skills/use-cache/SKILL.md +36 -5
  56. package/skills/view-transitions/SKILL.md +294 -0
  57. package/src/__augment-tests__/augment.ts +81 -0
  58. package/src/__augment-tests__/augmented.check.ts +116 -0
  59. package/src/__internal.ts +67 -40
  60. package/src/browser/action-coordinator.ts +53 -36
  61. package/src/browser/action-fence.ts +47 -0
  62. package/src/browser/app-shell.ts +39 -0
  63. package/src/browser/app-version.ts +14 -0
  64. package/src/browser/cookie-name.ts +140 -0
  65. package/src/browser/event-controller.ts +86 -147
  66. package/src/browser/history-state.ts +21 -0
  67. package/src/browser/index.ts +3 -3
  68. package/src/browser/invalidate-client-cache.ts +52 -0
  69. package/src/browser/link-interceptor.ts +4 -0
  70. package/src/browser/navigation-bridge.ts +148 -19
  71. package/src/browser/navigation-client.ts +187 -67
  72. package/src/browser/navigation-store-handle.ts +38 -0
  73. package/src/browser/navigation-store.ts +76 -67
  74. package/src/browser/navigation-transaction.ts +18 -66
  75. package/src/browser/partial-update.ts +123 -94
  76. package/src/browser/prefetch/cache.ts +214 -36
  77. package/src/browser/prefetch/fetch.ts +260 -38
  78. package/src/browser/prefetch/policy.ts +6 -0
  79. package/src/browser/prefetch/queue.ts +126 -20
  80. package/src/browser/prefetch/resource-ready.ts +77 -0
  81. package/src/browser/rango-state.ts +158 -76
  82. package/src/browser/react/Link.tsx +93 -11
  83. package/src/browser/react/NavigationProvider.tsx +115 -34
  84. package/src/browser/react/ScrollRestoration.tsx +10 -6
  85. package/src/browser/react/context.ts +7 -2
  86. package/src/browser/react/filter-segment-order.ts +49 -7
  87. package/src/browser/react/index.ts +0 -48
  88. package/src/browser/react/location-state-shared.ts +166 -8
  89. package/src/browser/react/location-state.ts +39 -14
  90. package/src/browser/react/use-action.ts +6 -15
  91. package/src/browser/react/use-handle.ts +23 -69
  92. package/src/browser/react/use-link-status.ts +0 -4
  93. package/src/browser/react/use-navigation.ts +22 -5
  94. package/src/browser/react/use-params.ts +20 -10
  95. package/src/browser/react/use-reverse.ts +106 -0
  96. package/src/browser/react/use-router.ts +46 -11
  97. package/src/browser/react/use-search-params.ts +0 -5
  98. package/src/browser/react/use-segments.ts +11 -21
  99. package/src/browser/response-adapter.ts +52 -1
  100. package/src/browser/rsc-router.tsx +215 -76
  101. package/src/browser/scroll-restoration.ts +46 -39
  102. package/src/browser/segment-reconciler.ts +36 -9
  103. package/src/browser/segment-structure-assert.ts +2 -2
  104. package/src/browser/server-action-bridge.ts +176 -50
  105. package/src/browser/types.ts +95 -11
  106. package/src/browser/validate-redirect-origin.ts +43 -16
  107. package/src/build/collect-fallback-refs.ts +107 -0
  108. package/src/build/generate-manifest.ts +65 -40
  109. package/src/build/generate-route-types.ts +5 -0
  110. package/src/build/index.ts +8 -2
  111. package/src/build/prefix-tree-utils.ts +123 -0
  112. package/src/build/route-trie.ts +137 -32
  113. package/src/build/route-types/codegen.ts +4 -4
  114. package/src/build/route-types/include-resolution.ts +9 -2
  115. package/src/build/route-types/param-extraction.ts +6 -3
  116. package/src/build/route-types/per-module-writer.ts +7 -4
  117. package/src/build/route-types/router-processing.ts +278 -96
  118. package/src/build/route-types/scan-filter.ts +9 -2
  119. package/src/build/route-types/source-scan.ts +118 -0
  120. package/src/build/runtime-discovery.ts +9 -20
  121. package/src/cache/cache-error.ts +104 -0
  122. package/src/cache/cache-policy.ts +68 -28
  123. package/src/cache/cache-runtime.ts +149 -43
  124. package/src/cache/cache-scope.ts +148 -81
  125. package/src/cache/cache-tag.ts +98 -0
  126. package/src/cache/cf/cf-cache-store.ts +2550 -93
  127. package/src/cache/cf/index.ts +11 -17
  128. package/src/cache/document-cache.ts +78 -27
  129. package/src/cache/handle-snapshot.ts +63 -0
  130. package/src/cache/index.ts +23 -20
  131. package/src/cache/memory-segment-store.ts +136 -37
  132. package/src/cache/profile-registry.ts +6 -30
  133. package/src/cache/read-through-swr.ts +41 -11
  134. package/src/cache/segment-codec.ts +0 -16
  135. package/src/cache/tag-invalidation.ts +230 -0
  136. package/src/cache/taint.ts +55 -0
  137. package/src/cache/types.ts +33 -100
  138. package/src/cache/vercel/index.ts +11 -0
  139. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  140. package/src/client.rsc.tsx +6 -21
  141. package/src/client.tsx +108 -290
  142. package/src/component-utils.ts +19 -0
  143. package/src/context-var.ts +84 -2
  144. package/src/debug.ts +2 -2
  145. package/src/decode-loader-results.ts +36 -0
  146. package/src/defer.ts +196 -0
  147. package/src/deps/ssr.ts +0 -1
  148. package/src/errors.ts +30 -4
  149. package/src/handle.ts +70 -22
  150. package/src/handles/MetaTags.tsx +0 -14
  151. package/src/handles/breadcrumbs.ts +16 -5
  152. package/src/handles/meta.ts +0 -39
  153. package/src/host/cookie-handler.ts +0 -36
  154. package/src/host/errors.ts +0 -24
  155. package/src/host/index.ts +8 -2
  156. package/src/host/pattern-matcher.ts +7 -50
  157. package/src/host/router.ts +107 -99
  158. package/src/host/testing.ts +40 -27
  159. package/src/host/types.ts +37 -4
  160. package/src/host/utils.ts +1 -1
  161. package/src/href-client.ts +137 -22
  162. package/src/index.rsc.ts +52 -26
  163. package/src/index.ts +100 -38
  164. package/src/internal-debug.ts +2 -4
  165. package/src/loader-store.ts +500 -0
  166. package/src/loader.rsc.ts +20 -13
  167. package/src/loader.ts +12 -11
  168. package/src/missing-id-error.ts +68 -0
  169. package/src/network-error-thrower.tsx +1 -6
  170. package/src/outlet-context.ts +1 -1
  171. package/src/outlet-provider.tsx +1 -5
  172. package/src/prerender/param-hash.ts +10 -11
  173. package/src/prerender/store.ts +37 -41
  174. package/src/prerender.ts +198 -82
  175. package/src/redirect-origin.ts +100 -0
  176. package/src/response-utils.ts +37 -0
  177. package/src/reverse.ts +65 -15
  178. package/src/root-error-boundary.tsx +1 -19
  179. package/src/route-content-wrapper.tsx +7 -72
  180. package/src/route-definition/dsl-helpers.ts +437 -274
  181. package/src/route-definition/helper-factories.ts +29 -139
  182. package/src/route-definition/helpers-types.ts +113 -37
  183. package/src/route-definition/index.ts +3 -0
  184. package/src/route-definition/redirect.ts +52 -10
  185. package/src/route-definition/resolve-handler-use.ts +161 -0
  186. package/src/route-definition/use-item-types.ts +32 -0
  187. package/src/route-map-builder.ts +7 -17
  188. package/src/route-types.ts +37 -41
  189. package/src/router/basename.ts +14 -0
  190. package/src/router/content-negotiation.ts +108 -9
  191. package/src/router/error-handling.ts +13 -17
  192. package/src/router/find-match.ts +45 -22
  193. package/src/router/handler-context.ts +83 -41
  194. package/src/router/intercept-resolution.ts +25 -23
  195. package/src/router/lazy-includes.ts +19 -53
  196. package/src/router/loader-resolution.ts +213 -30
  197. package/src/router/logging.ts +5 -8
  198. package/src/router/manifest.ts +49 -45
  199. package/src/router/match-api.ts +120 -204
  200. package/src/router/match-context.ts +0 -22
  201. package/src/router/match-handlers.ts +58 -58
  202. package/src/router/match-middleware/background-revalidation.ts +27 -6
  203. package/src/router/match-middleware/cache-lookup.ts +205 -249
  204. package/src/router/match-middleware/cache-store.ts +45 -32
  205. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  206. package/src/router/match-middleware/segment-resolution.ts +52 -18
  207. package/src/router/match-pipelines.ts +1 -42
  208. package/src/router/match-result.ts +104 -40
  209. package/src/router/metrics.ts +5 -34
  210. package/src/router/middleware-types.ts +13 -142
  211. package/src/router/middleware.ts +173 -143
  212. package/src/router/navigation-snapshot.ts +131 -0
  213. package/src/router/params-util.ts +23 -0
  214. package/src/router/pattern-matching.ts +109 -63
  215. package/src/router/prerender-match.ts +190 -54
  216. package/src/router/preview-match.ts +32 -102
  217. package/src/router/request-classification.ts +276 -0
  218. package/src/router/revalidation.ts +63 -55
  219. package/src/router/route-snapshot.ts +244 -0
  220. package/src/router/router-context.ts +6 -28
  221. package/src/router/router-interfaces.ts +100 -35
  222. package/src/router/router-options.ts +91 -11
  223. package/src/router/router-registry.ts +2 -5
  224. package/src/router/segment-resolution/fresh.ts +242 -75
  225. package/src/router/segment-resolution/helpers.ts +63 -24
  226. package/src/router/segment-resolution/loader-cache.ts +41 -37
  227. package/src/router/segment-resolution/revalidation.ts +456 -372
  228. package/src/router/segment-resolution/static-store.ts +19 -5
  229. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  230. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  231. package/src/router/segment-resolution.ts +4 -1
  232. package/src/router/segment-wrappers.ts +2 -3
  233. package/src/router/state-cookie-name.ts +33 -0
  234. package/src/router/substitute-pattern-params.ts +56 -0
  235. package/src/router/telemetry-otel.ts +0 -20
  236. package/src/router/telemetry.ts +96 -19
  237. package/src/router/timeout.ts +0 -20
  238. package/src/router/trie-matching.ts +91 -46
  239. package/src/router/types.ts +10 -63
  240. package/src/router/url-params.ts +44 -0
  241. package/src/router.ts +134 -43
  242. package/src/rsc/handler-context.ts +3 -2
  243. package/src/rsc/handler.ts +492 -383
  244. package/src/rsc/helpers.ts +162 -46
  245. package/src/rsc/index.ts +1 -1
  246. package/src/rsc/json-route-result.ts +38 -0
  247. package/src/rsc/loader-fetch.ts +23 -3
  248. package/src/rsc/manifest-init.ts +33 -42
  249. package/src/rsc/origin-guard.ts +39 -25
  250. package/src/rsc/progressive-enhancement.ts +30 -3
  251. package/src/rsc/redirect-guard.ts +99 -0
  252. package/src/rsc/response-error.ts +79 -12
  253. package/src/rsc/response-route-handler.ts +90 -63
  254. package/src/rsc/rsc-rendering.ts +56 -54
  255. package/src/rsc/runtime-warnings.ts +23 -10
  256. package/src/rsc/server-action.ts +74 -67
  257. package/src/rsc/ssr-setup.ts +18 -2
  258. package/src/rsc/types.ts +25 -6
  259. package/src/runtime-env.ts +18 -0
  260. package/src/search-params.ts +4 -20
  261. package/src/segment-content-promise.ts +67 -0
  262. package/src/segment-loader-promise.ts +134 -0
  263. package/src/segment-system.tsx +272 -129
  264. package/src/serialize.ts +243 -0
  265. package/src/server/context.ts +309 -61
  266. package/src/server/cookie-store.ts +80 -5
  267. package/src/server/handle-store.ts +26 -24
  268. package/src/server/loader-registry.ts +10 -28
  269. package/src/server/request-context.ts +338 -126
  270. package/src/ssr/index.tsx +23 -15
  271. package/src/static-handler.ts +27 -18
  272. package/src/testing/cache-status.ts +162 -0
  273. package/src/testing/collect-handle.ts +40 -0
  274. package/src/testing/dispatch.ts +618 -0
  275. package/src/testing/dom.entry.ts +22 -0
  276. package/src/testing/e2e/fixture.ts +188 -0
  277. package/src/testing/e2e/index.ts +128 -0
  278. package/src/testing/e2e/matchers.ts +35 -0
  279. package/src/testing/e2e/page-helpers.ts +272 -0
  280. package/src/testing/e2e/parity.ts +387 -0
  281. package/src/testing/e2e/server.ts +195 -0
  282. package/src/testing/flight-matchers.ts +97 -0
  283. package/src/testing/flight-normalize.ts +11 -0
  284. package/src/testing/flight-runtime.d.ts +57 -0
  285. package/src/testing/flight-tree.ts +682 -0
  286. package/src/testing/flight.entry.ts +52 -0
  287. package/src/testing/flight.ts +232 -0
  288. package/src/testing/generated-routes.ts +183 -0
  289. package/src/testing/index.ts +99 -0
  290. package/src/testing/internal/context.ts +348 -0
  291. package/src/testing/internal/flight-client-globals.ts +30 -0
  292. package/src/testing/internal/seed-vars.ts +54 -0
  293. package/src/testing/render-handler.ts +330 -0
  294. package/src/testing/render-route.tsx +566 -0
  295. package/src/testing/run-loader.ts +378 -0
  296. package/src/testing/run-middleware.ts +205 -0
  297. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  298. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  299. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  300. package/src/testing/vitest-stubs/version.ts +5 -0
  301. package/src/testing/vitest.ts +305 -0
  302. package/src/theme/ThemeProvider.tsx +0 -52
  303. package/src/theme/ThemeScript.tsx +0 -6
  304. package/src/theme/constants.ts +0 -12
  305. package/src/theme/index.ts +0 -7
  306. package/src/theme/theme-context.ts +1 -5
  307. package/src/theme/theme-script.ts +0 -14
  308. package/src/theme/use-theme.ts +0 -3
  309. package/src/types/boundaries.ts +0 -35
  310. package/src/types/cache-types.ts +17 -8
  311. package/src/types/error-types.ts +30 -90
  312. package/src/types/global-namespace.ts +54 -41
  313. package/src/types/handler-context.ts +233 -81
  314. package/src/types/index.ts +1 -10
  315. package/src/types/loader-types.ts +44 -15
  316. package/src/types/request-scope.ts +107 -0
  317. package/src/types/route-config.ts +6 -50
  318. package/src/types/route-entry.ts +19 -7
  319. package/src/types/segments.ts +37 -14
  320. package/src/urls/include-helper.ts +33 -70
  321. package/src/urls/index.ts +1 -11
  322. package/src/urls/path-helper-types.ts +58 -11
  323. package/src/urls/path-helper.ts +57 -111
  324. package/src/urls/pattern-types.ts +48 -19
  325. package/src/urls/response-types.ts +25 -22
  326. package/src/urls/type-extraction.ts +58 -139
  327. package/src/urls/urls-function.ts +1 -18
  328. package/src/use-loader.tsx +346 -89
  329. package/src/vite/debug.ts +185 -0
  330. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  331. package/src/vite/discovery/discover-routers.ts +130 -85
  332. package/src/vite/discovery/discovery-errors.ts +194 -0
  333. package/src/vite/discovery/gate-state.ts +171 -0
  334. package/src/vite/discovery/prerender-collection.ts +192 -99
  335. package/src/vite/discovery/route-types-writer.ts +40 -84
  336. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  337. package/src/vite/discovery/state.ts +51 -6
  338. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  339. package/src/vite/index.ts +8 -0
  340. package/src/vite/plugin-types.ts +187 -69
  341. package/src/vite/plugins/cjs-to-esm.ts +8 -18
  342. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  343. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  344. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  345. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  346. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  347. package/src/vite/plugins/expose-action-id.ts +49 -98
  348. package/src/vite/plugins/expose-id-utils.ts +11 -50
  349. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  350. package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
  351. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  352. package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
  353. package/src/vite/plugins/expose-internal-ids.ts +554 -317
  354. package/src/vite/plugins/performance-tracks.ts +89 -0
  355. package/src/vite/plugins/refresh-cmd.ts +89 -27
  356. package/src/vite/plugins/use-cache-transform.ts +73 -83
  357. package/src/vite/plugins/vercel-output.ts +258 -0
  358. package/src/vite/plugins/version-injector.ts +21 -25
  359. package/src/vite/plugins/version-plugin.ts +41 -20
  360. package/src/vite/plugins/virtual-entries.ts +2 -17
  361. package/src/vite/rango.ts +257 -289
  362. package/src/vite/router-discovery.ts +930 -140
  363. package/src/vite/utils/ast-handler-extract.ts +15 -31
  364. package/src/vite/utils/banner.ts +4 -4
  365. package/src/vite/utils/bundle-analysis.ts +10 -15
  366. package/src/vite/utils/client-chunks.ts +184 -0
  367. package/src/vite/utils/forward-user-plugins.ts +171 -0
  368. package/src/vite/utils/manifest-utils.ts +4 -59
  369. package/src/vite/utils/package-resolution.ts +20 -52
  370. package/src/vite/utils/prerender-utils.ts +27 -29
  371. package/src/vite/utils/shared-utils.ts +92 -42
  372. package/src/browser/action-response-classifier.ts +0 -99
  373. package/src/browser/react/use-client-cache.ts +0 -58
  374. package/src/browser/shallow.ts +0 -40
  375. package/src/handles/index.ts +0 -7
  376. package/src/router/middleware-cookies.ts +0 -55
@@ -25,15 +25,18 @@ function parsePathname(pathname: string): string[] {
25
25
  }
26
26
 
27
27
  /**
28
- * Build segments state from event controller
28
+ * Build segments state from event controller. `segmentIds` is the
29
+ * route-only list (parallels and loaders stripped) — distinct from the
30
+ * controller's `segmentOrder` which drives handle collection and includes
31
+ * parallel slot ids.
29
32
  */
30
33
  function buildSegmentsState(
31
34
  location: URL,
32
- segmentOrder: string[],
35
+ routeSegmentIds: string[],
33
36
  ): SegmentsState {
34
37
  return {
35
38
  path: parsePathname(location.pathname),
36
- segmentIds: segmentOrder,
39
+ segmentIds: routeSegmentIds,
37
40
  location,
38
41
  };
39
42
  }
@@ -74,7 +77,7 @@ export function useSegments<T>(
74
77
  const handleState = ctx.eventController.getHandleState();
75
78
  const segmentsState = buildSegmentsState(
76
79
  location as URL,
77
- handleState.segmentOrder,
80
+ handleState.routeSegmentIds,
78
81
  );
79
82
  return selector ? selector(segmentsState) : segmentsState;
80
83
  });
@@ -83,47 +86,36 @@ export function useSegments<T>(
83
86
  const selectorRef = useRef(selector);
84
87
  selectorRef.current = selector;
85
88
 
86
- // Track selector identity to detect when the selector function changes.
87
- // Only then do we eagerly recompute during render to avoid staleness.
88
- // Without this guard, no-selector mode causes infinite re-renders because
89
- // buildSegmentsState creates fresh arrays that fail Object.is checks.
90
89
  const prevSelectorIdentity = useRef(selector);
91
90
 
92
- // Cache SegmentsState to stabilize nested references (path, segmentIds
93
- // arrays) so selectors returning composite values don't cause spurious
94
- // render-time setState calls.
95
91
  const segmentsCache = useRef<{
96
92
  location: URL;
97
- segmentOrder: string[];
93
+ routeSegmentIds: string[];
98
94
  state: SegmentsState;
99
95
  } | null>(null);
100
96
 
101
- // Recompute selected value from current store state and apply selector.
102
- // Shared by the render-time eager check and the subscription callback.
103
97
  function recompute(
104
98
  sel: ((state: SegmentsState) => T) | undefined,
105
99
  ): T | SegmentsState {
106
100
  const location = ctx!.eventController.getLocation();
107
101
  const handleState = ctx!.eventController.getHandleState();
108
102
 
109
- // Reuse cached state when inputs haven't changed by reference,
110
- // keeping array/object references stable for composite selectors.
111
103
  const cache = segmentsCache.current;
112
104
  let segmentsState: SegmentsState;
113
105
  if (
114
106
  cache &&
115
107
  cache.location === location &&
116
- cache.segmentOrder === handleState.segmentOrder
108
+ cache.routeSegmentIds === handleState.routeSegmentIds
117
109
  ) {
118
110
  segmentsState = cache.state;
119
111
  } else {
120
112
  segmentsState = buildSegmentsState(
121
113
  location as URL,
122
- handleState.segmentOrder,
114
+ handleState.routeSegmentIds,
123
115
  );
124
116
  segmentsCache.current = {
125
117
  location: location as URL,
126
- segmentOrder: handleState.segmentOrder,
118
+ routeSegmentIds: handleState.routeSegmentIds,
127
119
  state: segmentsState,
128
120
  };
129
121
  }
@@ -162,8 +154,6 @@ export function useSegments<T>(
162
154
  unsubscribeNav();
163
155
  unsubscribeHandles();
164
156
  };
165
- // Stable subscription: selector changes are handled via selectorRef,
166
- // state comparison uses prevState ref. No re-subscribe needed.
167
157
  // eslint-disable-next-line react-hooks/exhaustive-deps
168
158
  }, []);
169
159
 
@@ -24,6 +24,51 @@ export function emptyResponse(): Response {
24
24
  return new Response(null, { status: 200 });
25
25
  }
26
26
 
27
+ /**
28
+ * Whether an RSC content response carries a server-stamped router identity
29
+ * (`X-RSC-Router-Id`) that DIFFERS from the id this client expects (its own
30
+ * routerId, also sent as `_rsc_rid`). Pre-decode integrity check: lets a caller
31
+ * refuse a foreign app's payload before `createFromFetch` imports its chunks.
32
+ *
33
+ * True ONLY when both the header and the expected id are present and differ. An
34
+ * absent header (control-only reload/redirect responses are not stamped) or an
35
+ * absent expected id (e.g. before the client is seeded) is a pass-through —
36
+ * never a false reject.
37
+ */
38
+ export function isForeignRouterId(
39
+ response: Response,
40
+ expectedId: string | undefined,
41
+ ): boolean {
42
+ const got = response.headers.get("X-RSC-Router-Id");
43
+ if (!got || !expectedId) return false;
44
+ return got !== expectedId;
45
+ }
46
+
47
+ /**
48
+ * Handle the X-RSC-Reload control header (server requests a full page reload on
49
+ * a version mismatch). Returns a short-circuit response when the header is
50
+ * present -- emptyResponse() if the URL was blocked by origin validation, or a
51
+ * never-resolving promise while the page reloads -- and null when absent, so
52
+ * the caller continues processing (e.g. the X-RSC-Redirect check). Scoped to
53
+ * X-RSC-Reload only; redirect handling differs between callers.
54
+ */
55
+ export function handleReloadHeader(
56
+ response: Response,
57
+ opts: { onBlocked: () => void; onReload: (url: string) => void },
58
+ ): Response | Promise<Response> | null {
59
+ const reload = extractRscHeaderUrl(response, "X-RSC-Reload");
60
+ if (reload === "blocked") {
61
+ opts.onBlocked();
62
+ return emptyResponse();
63
+ }
64
+ if (reload) {
65
+ opts.onReload(reload.url);
66
+ window.location.href = reload.url;
67
+ return new Promise<Response>(() => {});
68
+ }
69
+ return null;
70
+ }
71
+
27
72
  /**
28
73
  * Tee a response body for RSC parsing and stream completion tracking.
29
74
  * Returns a new Response with one branch; the other is consumed to detect
@@ -31,11 +76,17 @@ export function emptyResponse(): Response {
31
76
  *
32
77
  * If the response has no body, onComplete fires synchronously.
33
78
  * If signal is provided, an abort cancels the tracking reader.
79
+ *
80
+ * `silent` suppresses the stream-error log. Prefetch passes it: a speculative,
81
+ * low-priority prefetch that is aborted or never consumed can error its stream
82
+ * benignly, which is not worth surfacing. The fresh-navigation path keeps the
83
+ * log (default), where a stream error reflects a real failed navigation.
34
84
  */
35
85
  export function teeWithCompletion(
36
86
  response: Response,
37
87
  onComplete: () => void,
38
88
  signal?: AbortSignal,
89
+ silent = false,
39
90
  ): Response {
40
91
  if (!response.body) {
41
92
  onComplete();
@@ -59,7 +110,7 @@ export function teeWithCompletion(
59
110
  onComplete();
60
111
  }
61
112
  })().catch((error) => {
62
- if (!signal?.aborted) {
113
+ if (!silent && !signal?.aborted) {
63
114
  console.error("[Browser] Error reading tracking stream:", error);
64
115
  }
65
116
  onComplete();
@@ -8,6 +8,7 @@ import {
8
8
  generateHistoryKey,
9
9
  } from "./navigation-store.js";
10
10
  import { createEventController } from "./event-controller.js";
11
+ import { validateRedirectOrigin } from "./validate-redirect-origin.js";
11
12
  import { createNavigationClient } from "./navigation-client.js";
12
13
  import { createServerActionBridge } from "./server-action-bridge.js";
13
14
  import { createNavigationBridge } from "./navigation-bridge.js";
@@ -22,11 +23,15 @@ import type {
22
23
  import type { EventController } from "./event-controller.js";
23
24
  import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
24
25
  import { initRangoState } from "./rango-state.js";
26
+ import { registerNavigationStore } from "./navigation-store-handle.js";
25
27
  import { initPrefetchCache } from "./prefetch/cache.js";
28
+ import { setPrefetchDecoder } from "./prefetch/fetch.js";
29
+ import { setAppVersion } from "./app-version.js";
26
30
  import {
27
31
  isInterceptSegment,
28
32
  splitInterceptSegments,
29
33
  } from "./intercept-utils.js";
34
+ import { createAppShellRef } from "./app-shell.js";
30
35
 
31
36
  // Vite HMR types are provided by vite/client
32
37
 
@@ -113,13 +118,22 @@ export interface BrowserAppContext {
113
118
  warmupEnabled?: boolean;
114
119
  /** App version for prefetch version mismatch detection */
115
120
  version?: string;
121
+ /**
122
+ * App-shell ref, read through on each render so renderSegments and the
123
+ * NavigationProvider see rootLayout/basename/version without closing over a
124
+ * stale snapshot. Set once from the initial payload and not swapped within a
125
+ * session: a cross-app navigation is a full document load (X-RSC-Reload), so
126
+ * the target app establishes its own shell on load. Theme, warmup, and
127
+ * prefetch TTL are document-lifetime too (see AppShell).
128
+ */
129
+ appShellRef?: import("./app-shell.js").AppShellRef;
116
130
  }
117
131
 
118
132
  // Module-level state for the initialized app
119
133
  let browserAppContext: BrowserAppContext | null = null;
120
134
 
121
135
  /**
122
- * Initialize the browser app. Must be called before rendering RSCRouter.
136
+ * Initialize the browser app. Must be called before rendering Rango.
123
137
  *
124
138
  * This function:
125
139
  * - Loads the initial RSC payload from the stream
@@ -139,7 +153,6 @@ export async function initBrowserApp(
139
153
  initialTheme,
140
154
  } = options;
141
155
 
142
- // Load initial payload from SSR-injected __FLIGHT_DATA__
143
156
  const initialPayload =
144
157
  await deps.createFromReadableStream<RscPayload>(rscStream);
145
158
 
@@ -164,6 +177,18 @@ export async function initBrowserApp(
164
177
  ...(storeOptions?.cacheSize && { cacheSize: storeOptions.cacheSize }),
165
178
  });
166
179
 
180
+ // Register the active store on the module-level handle and wire the
181
+ // jar-divergence observer before any getRangoState() read can detect a
182
+ // cross-tab/server rotation. There is no global store singleton, so this
183
+ // handle is the live reference.
184
+ registerNavigationStore(store);
185
+
186
+ // Seed router identity from the initial SSR payload so the first
187
+ // cross-app SPA navigation can detect the app switch.
188
+ if (initialPayload.metadata?.routerId) {
189
+ store.setRouterId?.(initialPayload.metadata.routerId);
190
+ }
191
+
167
192
  // Create event controller for reactive state management
168
193
  const eventController = createEventController({
169
194
  initialLocation: new URL(window.location.href),
@@ -198,13 +223,25 @@ export async function initBrowserApp(
198
223
  // Create composable utilities
199
224
  const client = createNavigationClient(deps);
200
225
 
201
- // Extract rootLayout and version from metadata for browser-side re-renders
202
- const rootLayout = initialPayload.metadata?.rootLayout;
226
+ // Capture the per-router app-shell. rootLayout, basename, and version live
227
+ // here and are read through the ref at call time rather than closed over.
228
+ // It is set once from the initial payload and not swapped within a session:
229
+ // a cross-app navigation is a full document load (X-RSC-Reload), so the
230
+ // target app establishes its own shell on load.
203
231
  const version = initialPayload.metadata?.version;
232
+ const appShellRef = createAppShellRef({
233
+ routerId: initialPayload.metadata?.routerId,
234
+ rootLayout: initialPayload.metadata?.rootLayout,
235
+ basename: initialPayload.metadata?.basename,
236
+ version,
237
+ });
204
238
 
205
- // Initialize the localStorage state key for cache invalidation.
206
- // Uses the build version so a new deploy automatically busts all cached prefetches.
207
- initRangoState(version ?? "0");
239
+ // Initialize the rango state cookie for cache invalidation. The build version
240
+ // busts cached prefetches on deploy; the server-resolved cookie name
241
+ // namespaces the cookie so sibling apps on the same origin don't collide
242
+ // (falls back to the bare default prefix if metadata lacks the name).
243
+ initRangoState(version ?? "0", initialPayload.metadata?.stateCookieName);
244
+ setAppVersion(version);
208
245
 
209
246
  // Initialize the in-memory prefetch cache TTL from server config.
210
247
  // A value of 0 disables the cache; undefined falls back to the module default.
@@ -213,11 +250,22 @@ export async function initBrowserApp(
213
250
  initPrefetchCache(prefetchCacheTTL);
214
251
  }
215
252
 
216
- // Create a bound renderSegments that includes rootLayout
253
+ // Wire the RSC decoder so prefetches decode eagerly and warm the route's
254
+ // client chunks (same createFromFetch the navigation client uses).
255
+ setPrefetchDecoder((response) => deps.createFromFetch<RscPayload>(response));
256
+
257
+ // Create a bound renderSegments that reads rootLayout through the shell ref.
258
+ // The shell is set once at init and not swapped within a session (a cross-app
259
+ // navigation is a full document load), so this always renders this app's
260
+ // Document; reading through the ref just avoids closing over a stale value.
217
261
  const renderSegments = (
218
262
  segments: ResolvedSegment[],
219
263
  options?: RenderSegmentsOptions,
220
- ) => baseRenderSegments(segments, { ...options, rootLayout });
264
+ ) =>
265
+ baseRenderSegments(segments, {
266
+ ...options,
267
+ rootLayout: appShellRef.get().rootLayout,
268
+ });
221
269
 
222
270
  // Lazy reference for navigation bridge — the action bridge is created first
223
271
  // but may need to trigger SPA navigation for action redirects.
@@ -231,10 +279,15 @@ export async function initBrowserApp(
231
279
  deps,
232
280
  onUpdate: (update) => store.emitUpdate(update),
233
281
  renderSegments,
234
- version,
235
282
  onNavigate: (url, options) => {
236
283
  if (!navigateFn) {
237
- window.location.href = url;
284
+ // Navigation bridge not wired yet: hard-navigate, but re-validate
285
+ // same-origin defensively so this init-window fallback cannot become an
286
+ // open redirect (the normal path validates inside the navigation bridge).
287
+ const safe = validateRedirectOrigin(url, window.location.origin);
288
+ if (safe) {
289
+ window.location.href = safe;
290
+ }
238
291
  return Promise.resolve();
239
292
  }
240
293
  return navigateFn(url, options);
@@ -249,7 +302,7 @@ export async function initBrowserApp(
249
302
  client,
250
303
  onUpdate: (update) => store.emitUpdate(update),
251
304
  renderSegments,
252
- version,
305
+ version: version,
253
306
  });
254
307
 
255
308
  // Connect action redirect → navigation bridge (now that both are initialized)
@@ -263,75 +316,157 @@ export async function initBrowserApp(
263
316
  // Build initial tree with rootLayout
264
317
  const initialTree = renderSegments(initialPayload.metadata!.segments);
265
318
 
266
- // Setup HMR
319
+ // Setup HMR with debounce — burst saves (format-on-save, rapid edits)
320
+ // fire many rsc:update events in quick succession. Without debouncing,
321
+ // each event triggers a fetchPartial() which on slow routes can pile up
322
+ // and overwhelm the worker (cross-request promise issues, 500s).
267
323
  if (import.meta.hot) {
268
- import.meta.hot.on("rsc:update", async () => {
269
- console.log("[RSCRouter] HMR: Server update, refetching RSC");
270
-
271
- const handle = eventController.startNavigation(window.location.href, {
272
- replace: true,
273
- });
274
- const streamingToken = handle.startStreaming();
275
-
276
- const interceptSourceUrl = store.getInterceptSourceUrl();
277
-
278
- try {
279
- const { payload, streamComplete } = await client.fetchPartial({
280
- targetUrl: window.location.href,
281
- segmentIds: [],
282
- previousUrl: store.getSegmentState().currentUrl,
283
- interceptSourceUrl: interceptSourceUrl || undefined,
284
- hmr: true,
324
+ let hmrTimer: ReturnType<typeof setTimeout> | null = null;
325
+ let hmrAbort: AbortController | null = null;
326
+
327
+ import.meta.hot.on("rsc:update", () => {
328
+ // Cancel any pending debounce timer
329
+ if (hmrTimer !== null) {
330
+ clearTimeout(hmrTimer);
331
+ }
332
+
333
+ // Abort any in-flight HMR fetch so it doesn't race with the next one
334
+ if (hmrAbort) {
335
+ hmrAbort.abort();
336
+ hmrAbort = null;
337
+ }
338
+
339
+ // Debounce: wait 200ms of quiet before fetching
340
+ hmrTimer = setTimeout(async () => {
341
+ hmrTimer = null;
342
+
343
+ // Don't interrupt an active user navigation — startNavigation()
344
+ // would abort it and refetch the old URL (window.location.href
345
+ // hasn't updated yet). The user's navigation will pick up the
346
+ // new server code when it completes. isNavigating covers the
347
+ // full lifecycle (fetching + streaming, before commit) without
348
+ // blocking on server actions.
349
+ if (eventController.getState().isNavigating) {
350
+ console.log("[Rango] HMR: Skipping — navigation in progress");
351
+ return;
352
+ }
353
+
354
+ console.log("[Rango] HMR: Server update, refetching RSC");
355
+
356
+ const abort = new AbortController();
357
+ hmrAbort = abort;
358
+
359
+ const handle = eventController.startNavigation(window.location.href, {
360
+ replace: true,
285
361
  });
362
+ const streamingToken = handle.startStreaming();
363
+
364
+ const interceptSourceUrl = store.getInterceptSourceUrl();
365
+
366
+ try {
367
+ const { payload, streamComplete } = await client.fetchPartial({
368
+ targetUrl: window.location.href,
369
+ segmentIds: [],
370
+ previousUrl: store.getSegmentState().currentUrl,
371
+ interceptSourceUrl: interceptSourceUrl || undefined,
372
+ routerId: store.getRouterId?.(),
373
+ hmr: true,
374
+ signal: abort.signal,
375
+ });
286
376
 
287
- if (payload.metadata?.isPartial) {
288
- const segments = payload.metadata.segments || [];
289
- const matched = payload.metadata.matched || [];
377
+ if (abort.signal.aborted) return;
290
378
 
291
- // Derive intercept state from the returned payload, not the
292
- // pre-fetch store snapshot. If the HMR edit removed intercept
293
- // behavior, the response won't contain intercept segments.
294
- const responseIsIntercept = segments.some(isInterceptSegment);
379
+ // If the server returned a non-RSC response (404, 500 without
380
+ // error boundary), the payload won't have valid metadata.
381
+ // Reload to recover rather than leaving the page stale.
382
+ if (!payload.metadata) {
383
+ throw new Error("HMR refetch returned invalid payload");
384
+ }
295
385
 
296
- // Sync store intercept state with what the server returned
297
- if (!responseIsIntercept && interceptSourceUrl) {
298
- store.setInterceptSourceUrl(null);
386
+ // Update version BEFORE rebuilding state so that
387
+ // clearHistoryCache() runs first, then the fresh segment
388
+ // cache entry we create below survives.
389
+ //
390
+ // Compare against the bridge's live version, not the init-time
391
+ // `version` const: after the first HMR bump the const is stale, so a
392
+ // later update with an unchanged version would otherwise re-clear the
393
+ // cache and re-broadcast across tabs/apps. The live read fires only
394
+ // on a genuine version change.
395
+ const newVersion = payload.metadata.version;
396
+ const currentVersion = navigationBridge.getVersion();
397
+ if (newVersion && newVersion !== currentVersion) {
398
+ console.log(
399
+ "[Rango] HMR: version changed",
400
+ currentVersion,
401
+ "→",
402
+ newVersion,
403
+ "clearing caches",
404
+ );
405
+ navigationBridge.updateVersion(newVersion);
299
406
  }
300
407
 
301
- store.setSegmentIds(matched);
302
- store.setCurrentUrl(window.location.href);
408
+ // Apply only partial segment updates. A non-partial payload during
409
+ // HMR is transient: the worker route table is still rebuilding after
410
+ // the edit, so the URL momentarily resolves to not-found/catch-all.
411
+ // Skip it -- the debounced follow-up refetch returns the settled
412
+ // route's partial payload and renders it below. We never reload here:
413
+ // a paramless document GET would run the SSR path and surface the
414
+ // not-found page during that same transient.
415
+ if (payload.metadata?.isPartial) {
416
+ const segments = payload.metadata.segments || [];
417
+ const matched = payload.metadata.matched || [];
418
+
419
+ // Derive intercept state from the returned payload, not the
420
+ // pre-fetch store snapshot. If the HMR edit removed intercept
421
+ // behavior, the response won't contain intercept segments.
422
+ const responseIsIntercept = segments.some(isInterceptSegment);
423
+
424
+ // Sync store intercept state with what the server returned
425
+ if (!responseIsIntercept && interceptSourceUrl) {
426
+ store.setInterceptSourceUrl(null);
427
+ }
428
+
429
+ store.setSegmentIds(matched);
430
+ store.setCurrentUrl(window.location.href);
431
+
432
+ const historyKey = generateHistoryKey(window.location.href, {
433
+ intercept: responseIsIntercept,
434
+ });
435
+ store.setHistoryKey(historyKey);
436
+ const currentHandleData = eventController.getHandleState().data;
437
+ store.cacheSegmentsForHistory(
438
+ historyKey,
439
+ segments,
440
+ currentHandleData,
441
+ );
442
+
443
+ const { main, intercept } = splitInterceptSegments(segments);
444
+ store.emitUpdate({
445
+ root: renderSegments(main, {
446
+ interceptSegments: intercept.length > 0 ? intercept : undefined,
447
+ }),
448
+ metadata: payload.metadata,
449
+ });
450
+ }
303
451
 
304
- const historyKey = generateHistoryKey(window.location.href, {
305
- intercept: responseIsIntercept,
306
- });
307
- store.setHistoryKey(historyKey);
308
- const currentHandleData = eventController.getHandleState().data;
309
- store.cacheSegmentsForHistory(
310
- historyKey,
311
- segments,
312
- currentHandleData,
313
- );
314
-
315
- const { main, intercept } = splitInterceptSegments(segments);
316
- store.emitUpdate({
317
- root: renderSegments(main, {
318
- interceptSegments: intercept.length > 0 ? intercept : undefined,
319
- }),
320
- metadata: payload.metadata,
321
- });
452
+ await streamComplete;
453
+ handle.complete(new URL(window.location.href));
454
+ console.log("[Rango] HMR: RSC stream complete");
455
+ } catch (err) {
456
+ if (abort.signal.aborted) return;
457
+ console.warn("[Rango] HMR: Refetch failed, reloading page", err);
458
+ window.location.reload();
459
+ return;
460
+ } finally {
461
+ if (hmrAbort === abort) hmrAbort = null;
462
+ streamingToken.end();
463
+ handle[Symbol.dispose]();
322
464
  }
323
-
324
- await streamComplete;
325
- handle.complete(new URL(window.location.href));
326
- console.log("[RSCRouter] HMR: RSC stream complete");
327
- } finally {
328
- streamingToken.end();
329
- handle[Symbol.dispose]();
330
- }
465
+ }, 200);
331
466
  });
332
467
  }
333
468
 
334
- // Store context for RSCRouter component
469
+ // Store context for Rango component
335
470
  const context: BrowserAppContext = {
336
471
  store,
337
472
  eventController,
@@ -342,6 +477,7 @@ export async function initBrowserApp(
342
477
  initialTheme: effectiveInitialTheme,
343
478
  warmupEnabled: initialPayload.metadata?.warmupEnabled ?? true,
344
479
  version,
480
+ appShellRef,
345
481
  };
346
482
  browserAppContext = context;
347
483
 
@@ -354,7 +490,7 @@ export async function initBrowserApp(
354
490
  export function getBrowserAppContext(): BrowserAppContext {
355
491
  if (!browserAppContext) {
356
492
  throw new Error(
357
- "RSCRouter: initBrowserApp() must be called before rendering RSCRouter",
493
+ "Rango: initBrowserApp() must be called before rendering Rango",
358
494
  );
359
495
  }
360
496
  return browserAppContext;
@@ -368,18 +504,18 @@ export function resetBrowserAppContext(): void {
368
504
  }
369
505
 
370
506
  /**
371
- * Props for the RSCRouter component
507
+ * Props for the Rango component
372
508
  */
373
- export interface RSCRouterProps {}
509
+ export interface RangoProps {}
374
510
 
375
511
  /**
376
- * RSCRouter component - renders the RSC router with all internal wiring.
512
+ * Rango component - renders the RSC router with all internal wiring.
377
513
  *
378
514
  * Must be called after initBrowserApp() has completed.
379
515
  *
380
516
  * @example
381
517
  * ```tsx
382
- * import { initBrowserApp, RSCRouter } from "rsc-router/browser";
518
+ * import { initBrowserApp, Rango } from "rsc-router/browser";
383
519
  * import { rscStream } from "rsc-html-stream/client";
384
520
  * import * as rscBrowser from "@vitejs/plugin-rsc/browser";
385
521
  *
@@ -389,14 +525,14 @@ export interface RSCRouterProps {}
389
525
  * hydrateRoot(
390
526
  * document,
391
527
  * <React.StrictMode>
392
- * <RSCRouter />
528
+ * <Rango />
393
529
  * </React.StrictMode>
394
530
  * );
395
531
  * }
396
532
  * main();
397
533
  * ```
398
534
  */
399
- export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
535
+ export function Rango(_props: RangoProps): React.ReactElement {
400
536
  const {
401
537
  store,
402
538
  eventController,
@@ -407,6 +543,7 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
407
543
  initialTheme,
408
544
  warmupEnabled,
409
545
  version,
546
+ appShellRef,
410
547
  } = getBrowserAppContext();
411
548
 
412
549
  // Signal that the React tree has hydrated. useEffect only fires after
@@ -426,6 +563,8 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
426
563
  initialTheme={initialTheme}
427
564
  warmupEnabled={warmupEnabled}
428
565
  version={version}
566
+ basename={initialPayload.metadata?.basename}
567
+ appShellRef={appShellRef}
429
568
  />
430
569
  );
431
570
  }