@rangojs/router 0.0.0-experimental.135c6902 → 0.0.0-experimental.136

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 (404) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.md +245 -49
  3. package/dist/bin/rango.js +440 -133
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3386 -1111
  6. package/dist/vite/index.js.bak +5448 -0
  7. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  8. package/package.json +78 -25
  9. package/skills/api-client/SKILL.md +211 -0
  10. package/skills/breadcrumbs/SKILL.md +64 -2
  11. package/skills/bundle-analysis/SKILL.md +159 -0
  12. package/skills/cache-guide/SKILL.md +247 -23
  13. package/skills/caching/SKILL.md +281 -11
  14. package/skills/composability/SKILL.md +27 -3
  15. package/skills/css/SKILL.md +76 -0
  16. package/skills/debug-manifest/SKILL.md +4 -2
  17. package/skills/document-cache/SKILL.md +78 -55
  18. package/skills/handler-use/SKILL.md +364 -0
  19. package/skills/hooks/SKILL.md +250 -30
  20. package/skills/host-router/SKILL.md +83 -23
  21. package/skills/i18n/SKILL.md +276 -0
  22. package/skills/intercept/SKILL.md +87 -18
  23. package/skills/layout/SKILL.md +35 -9
  24. package/skills/links/SKILL.md +249 -17
  25. package/skills/loader/SKILL.md +289 -53
  26. package/skills/middleware/SKILL.md +52 -13
  27. package/skills/migrate-nextjs/SKILL.md +584 -0
  28. package/skills/migrate-react-router/SKILL.md +914 -0
  29. package/skills/mime-routes/SKILL.md +28 -1
  30. package/skills/observability/SKILL.md +172 -0
  31. package/skills/parallel/SKILL.md +144 -7
  32. package/skills/prerender/SKILL.md +172 -125
  33. package/skills/rango/SKILL.md +251 -22
  34. package/skills/react-compiler/SKILL.md +168 -0
  35. package/skills/response-routes/SKILL.md +123 -48
  36. package/skills/route/SKILL.md +101 -5
  37. package/skills/router-setup/SKILL.md +117 -10
  38. package/skills/scripts/SKILL.md +179 -0
  39. package/skills/server-actions/SKILL.md +775 -0
  40. package/skills/streams-and-websockets/SKILL.md +283 -0
  41. package/skills/tailwind/SKILL.md +27 -3
  42. package/skills/testing/SKILL.md +130 -0
  43. package/skills/testing/bindings.md +103 -0
  44. package/skills/testing/cache-prerender.md +127 -0
  45. package/skills/testing/client-components.md +124 -0
  46. package/skills/testing/e2e-parity.md +125 -0
  47. package/skills/testing/flight.md +91 -0
  48. package/skills/testing/handles.md +129 -0
  49. package/skills/testing/loader.md +128 -0
  50. package/skills/testing/middleware.md +99 -0
  51. package/skills/testing/render-handler.md +122 -0
  52. package/skills/testing/response-routes.md +95 -0
  53. package/skills/testing/reverse-and-types.md +84 -0
  54. package/skills/testing/server-actions.md +107 -0
  55. package/skills/testing/server-tree.md +128 -0
  56. package/skills/testing/setup.md +123 -0
  57. package/skills/typesafety/SKILL.md +332 -29
  58. package/skills/use-cache/SKILL.md +57 -14
  59. package/skills/view-transitions/SKILL.md +337 -0
  60. package/src/__augment-tests__/augment.ts +81 -0
  61. package/src/__augment-tests__/augmented.check.ts +116 -0
  62. package/src/__internal.ts +1 -66
  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 +192 -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/navigation-bridge.ts +131 -30
  74. package/src/browser/navigation-client.ts +199 -86
  75. package/src/browser/navigation-store-handle.ts +38 -0
  76. package/src/browser/navigation-store.ts +157 -74
  77. package/src/browser/navigation-transaction.ts +9 -59
  78. package/src/browser/network-error-handler.ts +34 -7
  79. package/src/browser/partial-update.ts +174 -109
  80. package/src/browser/prefetch/cache.ts +222 -69
  81. package/src/browser/prefetch/fetch.ts +347 -39
  82. package/src/browser/prefetch/queue.ts +113 -32
  83. package/src/browser/prefetch/resource-ready.ts +77 -0
  84. package/src/browser/rango-state.ts +158 -76
  85. package/src/browser/react/Link.tsx +102 -15
  86. package/src/browser/react/NavigationProvider.tsx +300 -122
  87. package/src/browser/react/ScrollRestoration.tsx +10 -6
  88. package/src/browser/react/context.ts +7 -2
  89. package/src/browser/react/deferred-handle-resolution.ts +75 -0
  90. package/src/browser/react/filter-segment-order.ts +66 -7
  91. package/src/browser/react/index.ts +0 -48
  92. package/src/browser/react/location-state-shared.ts +178 -8
  93. package/src/browser/react/location-state.ts +39 -14
  94. package/src/browser/react/use-action.ts +6 -15
  95. package/src/browser/react/use-handle.ts +23 -69
  96. package/src/browser/react/use-href.tsx +8 -1
  97. package/src/browser/react/use-link-status.ts +33 -8
  98. package/src/browser/react/use-navigation.ts +32 -7
  99. package/src/browser/react/use-params.ts +20 -10
  100. package/src/browser/react/use-reverse.ts +106 -0
  101. package/src/browser/react/use-router.ts +46 -11
  102. package/src/browser/react/use-search-params.ts +0 -5
  103. package/src/browser/react/use-segments.ts +11 -21
  104. package/src/browser/response-adapter.ts +99 -8
  105. package/src/browser/rsc-router.tsx +125 -28
  106. package/src/browser/scroll-restoration.ts +37 -22
  107. package/src/browser/segment-reconciler.ts +36 -14
  108. package/src/browser/segment-structure-assert.ts +2 -2
  109. package/src/browser/server-action-bridge.ts +222 -61
  110. package/src/browser/types.ts +115 -12
  111. package/src/browser/validate-redirect-origin.ts +43 -16
  112. package/src/build/collect-fallback-refs.ts +107 -0
  113. package/src/build/generate-manifest.ts +65 -40
  114. package/src/build/generate-route-types.ts +5 -1
  115. package/src/build/index.ts +8 -2
  116. package/src/build/prefix-tree-utils.ts +123 -0
  117. package/src/build/route-trie.ts +165 -36
  118. package/src/build/route-types/ast-route-extraction.ts +15 -8
  119. package/src/build/route-types/codegen.ts +16 -5
  120. package/src/build/route-types/include-resolution.ts +125 -24
  121. package/src/build/route-types/param-extraction.ts +6 -3
  122. package/src/build/route-types/per-module-writer.ts +22 -6
  123. package/src/build/route-types/router-processing.ts +260 -94
  124. package/src/build/route-types/scan-filter.ts +9 -2
  125. package/src/build/route-types/source-scan.ts +216 -0
  126. package/src/build/runtime-discovery.ts +9 -20
  127. package/src/cache/cache-error.ts +104 -0
  128. package/src/cache/cache-key-utils.ts +29 -13
  129. package/src/cache/cache-policy.ts +108 -34
  130. package/src/cache/cache-runtime.ts +239 -52
  131. package/src/cache/cache-scope.ts +234 -87
  132. package/src/cache/cache-tag.ts +103 -0
  133. package/src/cache/cf/cf-base64.ts +33 -0
  134. package/src/cache/cf/cf-cache-constants.ts +127 -0
  135. package/src/cache/cf/cf-cache-store.ts +1989 -378
  136. package/src/cache/cf/cf-cache-types.ts +349 -0
  137. package/src/cache/cf/cf-kv-utils.ts +46 -0
  138. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  139. package/src/cache/cf/index.ts +6 -16
  140. package/src/cache/document-cache.ts +89 -21
  141. package/src/cache/handle-snapshot.ts +70 -0
  142. package/src/cache/index.ts +10 -20
  143. package/src/cache/memory-segment-store.ts +136 -37
  144. package/src/cache/profile-registry.ts +46 -31
  145. package/src/cache/read-through-swr.ts +56 -12
  146. package/src/cache/segment-codec.ts +9 -17
  147. package/src/cache/tag-invalidation.ts +230 -0
  148. package/src/cache/taint.ts +55 -0
  149. package/src/cache/types.ts +37 -100
  150. package/src/client.rsc.tsx +44 -21
  151. package/src/client.tsx +119 -290
  152. package/src/cloudflare/index.ts +11 -0
  153. package/src/cloudflare/tracing.ts +109 -0
  154. package/src/component-utils.ts +19 -0
  155. package/src/components/DefaultDocument.tsx +8 -2
  156. package/src/context-var.ts +84 -2
  157. package/src/decode-loader-results.ts +52 -0
  158. package/src/defer.ts +196 -0
  159. package/src/deps/ssr.ts +0 -1
  160. package/src/encode-kv.ts +49 -0
  161. package/src/errors.ts +30 -4
  162. package/src/escape-script.ts +52 -0
  163. package/src/handle.ts +70 -22
  164. package/src/handles/MetaTags.tsx +62 -19
  165. package/src/handles/Scripts.tsx +183 -0
  166. package/src/handles/breadcrumbs.ts +37 -8
  167. package/src/handles/is-thenable.ts +19 -0
  168. package/src/handles/meta.ts +51 -40
  169. package/src/handles/script.ts +244 -0
  170. package/src/host/cookie-handler.ts +9 -60
  171. package/src/host/errors.ts +0 -24
  172. package/src/host/index.ts +8 -2
  173. package/src/host/pattern-matcher.ts +23 -52
  174. package/src/host/router.ts +107 -99
  175. package/src/host/testing.ts +40 -27
  176. package/src/host/types.ts +37 -4
  177. package/src/host/utils.ts +1 -1
  178. package/src/href-client.ts +137 -22
  179. package/src/index.rsc.ts +99 -13
  180. package/src/index.ts +139 -19
  181. package/src/internal-debug.ts +11 -10
  182. package/src/loader-store.ts +500 -0
  183. package/src/loader.rsc.ts +20 -13
  184. package/src/loader.ts +12 -11
  185. package/src/missing-id-error.ts +68 -0
  186. package/src/outlet-context.ts +1 -1
  187. package/src/outlet-provider.tsx +1 -5
  188. package/src/prerender/param-hash.ts +16 -16
  189. package/src/prerender/store.ts +37 -41
  190. package/src/prerender.ts +198 -82
  191. package/src/redirect-origin.ts +100 -0
  192. package/src/regex-escape.ts +8 -0
  193. package/src/render-error-thrower.tsx +20 -0
  194. package/src/response-utils.ts +62 -0
  195. package/src/reverse.ts +65 -15
  196. package/src/root-error-boundary.tsx +1 -19
  197. package/src/route-content-wrapper.tsx +19 -77
  198. package/src/route-definition/dsl-helpers.ts +461 -304
  199. package/src/route-definition/helper-factories.ts +28 -140
  200. package/src/route-definition/helpers-types.ts +149 -74
  201. package/src/route-definition/index.ts +4 -2
  202. package/src/route-definition/redirect.ts +51 -10
  203. package/src/route-definition/resolve-handler-use.ts +160 -0
  204. package/src/route-definition/use-item-types.ts +29 -0
  205. package/src/route-map-builder.ts +0 -16
  206. package/src/route-types.ts +37 -46
  207. package/src/router/basename.ts +14 -0
  208. package/src/router/content-negotiation.ts +164 -17
  209. package/src/router/error-handling.ts +45 -18
  210. package/src/router/find-match.ts +44 -23
  211. package/src/router/handler-context.ts +83 -39
  212. package/src/router/instrument.ts +350 -0
  213. package/src/router/intercept-resolution.ts +48 -24
  214. package/src/router/lazy-includes.ts +15 -52
  215. package/src/router/loader-resolution.ts +274 -56
  216. package/src/router/logging.ts +0 -6
  217. package/src/router/manifest.ts +40 -42
  218. package/src/router/match-api.ts +124 -204
  219. package/src/router/match-context.ts +0 -22
  220. package/src/router/match-handlers.ts +58 -58
  221. package/src/router/match-middleware/background-revalidation.ts +54 -25
  222. package/src/router/match-middleware/cache-lookup.ts +205 -271
  223. package/src/router/match-middleware/cache-store.ts +81 -50
  224. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  225. package/src/router/match-middleware/segment-resolution.ts +45 -14
  226. package/src/router/match-pipelines.ts +1 -42
  227. package/src/router/match-result.ts +93 -39
  228. package/src/router/metrics.ts +5 -34
  229. package/src/router/middleware-types.ts +13 -142
  230. package/src/router/middleware.ts +268 -171
  231. package/src/router/navigation-snapshot.ts +131 -0
  232. package/src/router/params-util.ts +23 -0
  233. package/src/router/pattern-matching.ts +132 -90
  234. package/src/router/prefetch-cache-ttl.ts +51 -0
  235. package/src/router/prefetch-limits.ts +37 -0
  236. package/src/router/prerender-match.ts +195 -56
  237. package/src/router/preview-match.ts +32 -102
  238. package/src/router/request-classification.ts +276 -0
  239. package/src/router/revalidation.ts +123 -73
  240. package/src/router/route-snapshot.ts +244 -0
  241. package/src/router/router-context.ts +3 -27
  242. package/src/router/router-interfaces.ts +129 -35
  243. package/src/router/router-options.ts +202 -15
  244. package/src/router/router-registry.ts +2 -5
  245. package/src/router/segment-resolution/fresh.ts +197 -92
  246. package/src/router/segment-resolution/helpers.ts +115 -30
  247. package/src/router/segment-resolution/loader-cache.ts +76 -39
  248. package/src/router/segment-resolution/revalidation.ts +392 -338
  249. package/src/router/segment-resolution/static-store.ts +19 -5
  250. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  251. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  252. package/src/router/segment-resolution.ts +5 -1
  253. package/src/router/segment-wrappers.ts +6 -5
  254. package/src/router/state-cookie-name.ts +33 -0
  255. package/src/router/substitute-pattern-params.ts +56 -0
  256. package/src/router/telemetry-otel.ts +161 -199
  257. package/src/router/telemetry.ts +96 -19
  258. package/src/router/timeout.ts +0 -20
  259. package/src/router/tracing.ts +206 -0
  260. package/src/router/trie-matching.ts +163 -59
  261. package/src/router/types.ts +10 -63
  262. package/src/router/url-params.ts +44 -0
  263. package/src/router.ts +174 -54
  264. package/src/rsc/handler-context.ts +3 -2
  265. package/src/rsc/handler.ts +660 -518
  266. package/src/rsc/helpers.ts +168 -46
  267. package/src/rsc/index.ts +2 -5
  268. package/src/rsc/json-route-result.ts +38 -0
  269. package/src/rsc/loader-fetch.ts +127 -31
  270. package/src/rsc/manifest-init.ts +33 -42
  271. package/src/rsc/origin-guard.ts +39 -25
  272. package/src/rsc/progressive-enhancement.ts +133 -13
  273. package/src/rsc/redirect-guard.ts +99 -0
  274. package/src/rsc/response-cache-serve.ts +238 -0
  275. package/src/rsc/response-error.ts +79 -12
  276. package/src/rsc/response-route-handler.ts +99 -189
  277. package/src/rsc/rsc-rendering.ts +115 -73
  278. package/src/rsc/runtime-warnings.ts +23 -10
  279. package/src/rsc/server-action.ts +287 -113
  280. package/src/rsc/ssr-setup.ts +18 -2
  281. package/src/rsc/transition-gate.ts +89 -0
  282. package/src/rsc/types.ts +36 -6
  283. package/src/runtime-env.ts +18 -0
  284. package/src/search-params.ts +35 -30
  285. package/src/segment-content-promise.ts +67 -0
  286. package/src/segment-loader-promise.ts +149 -0
  287. package/src/segment-system.tsx +269 -202
  288. package/src/serialize.ts +243 -0
  289. package/src/server/context.ts +235 -51
  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 +38 -46
  294. package/src/server/request-context.ts +442 -173
  295. package/src/ssr/index.tsx +24 -16
  296. package/src/static-handler.ts +27 -18
  297. package/src/testing/cache-status.ts +162 -0
  298. package/src/testing/collect-handle.ts +40 -0
  299. package/src/testing/dispatch.ts +701 -0
  300. package/src/testing/dom.entry.ts +22 -0
  301. package/src/testing/e2e/fixture.ts +188 -0
  302. package/src/testing/e2e/index.ts +128 -0
  303. package/src/testing/e2e/matchers.ts +35 -0
  304. package/src/testing/e2e/page-helpers.ts +272 -0
  305. package/src/testing/e2e/parity.ts +387 -0
  306. package/src/testing/e2e/server.ts +195 -0
  307. package/src/testing/flight-matchers.ts +97 -0
  308. package/src/testing/flight-normalize.ts +11 -0
  309. package/src/testing/flight-runtime.d.ts +57 -0
  310. package/src/testing/flight-tree.ts +682 -0
  311. package/src/testing/flight.entry.ts +52 -0
  312. package/src/testing/flight.ts +257 -0
  313. package/src/testing/generated-routes.ts +183 -0
  314. package/src/testing/index.ts +105 -0
  315. package/src/testing/internal/context.ts +371 -0
  316. package/src/testing/internal/flight-client-globals.ts +30 -0
  317. package/src/testing/internal/seed-vars.ts +54 -0
  318. package/src/testing/render-handler.ts +357 -0
  319. package/src/testing/render-route.tsx +581 -0
  320. package/src/testing/run-loader.ts +385 -0
  321. package/src/testing/run-middleware.ts +205 -0
  322. package/src/testing/run-transition-when.ts +164 -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 +0 -7
  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 +234 -82
  340. package/src/types/index.ts +3 -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 +6 -50
  344. package/src/types/route-entry.ts +12 -7
  345. package/src/types/segments.ts +136 -15
  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 +68 -18
  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 -19
  354. package/src/use-loader.tsx +346 -89
  355. package/src/vite/debug.ts +185 -0
  356. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  357. package/src/vite/discovery/discover-routers.ts +130 -85
  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 +214 -132
  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 +57 -4
  364. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  365. package/src/vite/index.ts +6 -0
  366. package/src/vite/inject-client-debug.ts +36 -0
  367. package/src/vite/plugin-types.ts +178 -5
  368. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  369. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  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 +48 -95
  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 +89 -27
  383. package/src/vite/plugins/use-cache-transform.ts +73 -83
  384. package/src/vite/plugins/version-injector.ts +40 -29
  385. package/src/vite/plugins/version-plugin.ts +37 -40
  386. package/src/vite/plugins/virtual-entries.ts +39 -25
  387. package/src/vite/rango.ts +119 -111
  388. package/src/vite/router-discovery.ts +941 -142
  389. package/src/vite/utils/ast-handler-extract.ts +26 -35
  390. package/src/vite/utils/banner.ts +1 -1
  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 +81 -34
  398. package/src/vite/utils/shared-utils.ts +92 -42
  399. package/src/browser/action-response-classifier.ts +0 -99
  400. package/src/browser/react/use-client-cache.ts +0 -58
  401. package/src/browser/shallow.ts +0 -40
  402. package/src/handles/index.ts +0 -7
  403. package/src/network-error-thrower.tsx +0 -23
  404. package/src/router/middleware-cookies.ts +0 -55
@@ -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,16 @@ 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 { setPrefetchConcurrency } from "./prefetch/queue.js";
29
+ import { setPrefetchDecoder } from "./prefetch/fetch.js";
30
+ import { setAppVersion } from "./app-version.js";
26
31
  import {
27
32
  isInterceptSegment,
28
33
  splitInterceptSegments,
29
34
  } from "./intercept-utils.js";
35
+ import { createAppShellRef } from "./app-shell.js";
30
36
 
31
37
  // Vite HMR types are provided by vite/client
32
38
 
@@ -111,15 +117,26 @@ export interface BrowserAppContext {
111
117
  initialTheme?: Theme;
112
118
  /** Whether connection warmup is enabled */
113
119
  warmupEnabled?: boolean;
120
+ /** Whether the hydrated tree should be wrapped in React.StrictMode */
121
+ strictMode?: boolean;
114
122
  /** App version for prefetch version mismatch detection */
115
123
  version?: string;
124
+ /**
125
+ * App-shell ref, read through on each render so renderSegments and the
126
+ * NavigationProvider see rootLayout/basename/version without closing over a
127
+ * stale snapshot. Set once from the initial payload and not swapped within a
128
+ * session: a cross-app navigation is a full document load (X-RSC-Reload), so
129
+ * the target app establishes its own shell on load. Theme, warmup, and
130
+ * prefetch TTL are document-lifetime too (see AppShell).
131
+ */
132
+ appShellRef?: import("./app-shell.js").AppShellRef;
116
133
  }
117
134
 
118
135
  // Module-level state for the initialized app
119
136
  let browserAppContext: BrowserAppContext | null = null;
120
137
 
121
138
  /**
122
- * Initialize the browser app. Must be called before rendering RSCRouter.
139
+ * Initialize the browser app. Must be called before rendering Rango.
123
140
  *
124
141
  * This function:
125
142
  * - Loads the initial RSC payload from the stream
@@ -139,7 +156,6 @@ export async function initBrowserApp(
139
156
  initialTheme,
140
157
  } = options;
141
158
 
142
- // Load initial payload from SSR-injected __FLIGHT_DATA__
143
159
  const initialPayload =
144
160
  await deps.createFromReadableStream<RscPayload>(rscStream);
145
161
 
@@ -164,6 +180,18 @@ export async function initBrowserApp(
164
180
  ...(storeOptions?.cacheSize && { cacheSize: storeOptions.cacheSize }),
165
181
  });
166
182
 
183
+ // Register the active store on the module-level handle and wire the
184
+ // jar-divergence observer before any getRangoState() read can detect a
185
+ // cross-tab/server rotation. There is no global store singleton, so this
186
+ // handle is the live reference.
187
+ registerNavigationStore(store);
188
+
189
+ // Seed router identity from the initial SSR payload so the first
190
+ // cross-app SPA navigation can detect the app switch.
191
+ if (initialPayload.metadata?.routerId) {
192
+ store.setRouterId?.(initialPayload.metadata.routerId);
193
+ }
194
+
167
195
  // Create event controller for reactive state management
168
196
  const eventController = createEventController({
169
197
  initialLocation: new URL(window.location.href),
@@ -198,26 +226,55 @@ export async function initBrowserApp(
198
226
  // Create composable utilities
199
227
  const client = createNavigationClient(deps);
200
228
 
201
- // Extract rootLayout and version from metadata for browser-side re-renders
202
- const rootLayout = initialPayload.metadata?.rootLayout;
229
+ // Capture the per-router app-shell. rootLayout, basename, and version live
230
+ // here and are read through the ref at call time rather than closed over.
231
+ // It is set once from the initial payload and not swapped within a session:
232
+ // a cross-app navigation is a full document load (X-RSC-Reload), so the
233
+ // target app establishes its own shell on load.
203
234
  const version = initialPayload.metadata?.version;
235
+ const appShellRef = createAppShellRef({
236
+ routerId: initialPayload.metadata?.routerId,
237
+ rootLayout: initialPayload.metadata?.rootLayout,
238
+ basename: initialPayload.metadata?.basename,
239
+ version,
240
+ });
204
241
 
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");
242
+ // Initialize the rango state cookie for cache invalidation. The build version
243
+ // busts cached prefetches on deploy; the server-resolved cookie name
244
+ // namespaces the cookie so sibling apps on the same origin don't collide
245
+ // (falls back to the bare default prefix if metadata lacks the name).
246
+ initRangoState(version ?? "0", initialPayload.metadata?.stateCookieName);
247
+ setAppVersion(version);
208
248
 
209
- // Initialize the in-memory prefetch cache TTL from server config.
210
- // A value of 0 disables the cache; undefined falls back to the module default.
249
+ // Initialize the in-memory prefetch cache (TTL + max size) and the prefetch
250
+ // queue concurrency from server config. A TTL of 0 disables the cache;
251
+ // undefined values fall back to the module defaults.
211
252
  const prefetchCacheTTL = initialPayload.metadata?.prefetchCacheTTL;
212
- if (prefetchCacheTTL !== undefined) {
213
- initPrefetchCache(prefetchCacheTTL);
253
+ const prefetchCacheSize = initialPayload.metadata?.prefetchCacheSize;
254
+ if (prefetchCacheTTL !== undefined || prefetchCacheSize !== undefined) {
255
+ initPrefetchCache(prefetchCacheTTL, prefetchCacheSize);
256
+ }
257
+ const prefetchConcurrency = initialPayload.metadata?.prefetchConcurrency;
258
+ if (prefetchConcurrency !== undefined) {
259
+ setPrefetchConcurrency(prefetchConcurrency);
214
260
  }
215
261
 
216
- // Create a bound renderSegments that includes rootLayout
262
+ // Wire the RSC decoder so prefetches decode eagerly and warm the route's
263
+ // client chunks (same createFromFetch the navigation client uses).
264
+ setPrefetchDecoder((response) => deps.createFromFetch<RscPayload>(response));
265
+
266
+ // Create a bound renderSegments that reads rootLayout through the shell ref.
267
+ // The shell is set once at init and not swapped within a session (a cross-app
268
+ // navigation is a full document load), so this always renders this app's
269
+ // Document; reading through the ref just avoids closing over a stale value.
217
270
  const renderSegments = (
218
271
  segments: ResolvedSegment[],
219
272
  options?: RenderSegmentsOptions,
220
- ) => baseRenderSegments(segments, { ...options, rootLayout });
273
+ ) =>
274
+ baseRenderSegments(segments, {
275
+ ...options,
276
+ rootLayout: appShellRef.get().rootLayout,
277
+ });
221
278
 
222
279
  // Lazy reference for navigation bridge — the action bridge is created first
223
280
  // but may need to trigger SPA navigation for action redirects.
@@ -231,10 +288,15 @@ export async function initBrowserApp(
231
288
  deps,
232
289
  onUpdate: (update) => store.emitUpdate(update),
233
290
  renderSegments,
234
- version,
235
291
  onNavigate: (url, options) => {
236
292
  if (!navigateFn) {
237
- window.location.href = url;
293
+ // Navigation bridge not wired yet: hard-navigate, but re-validate
294
+ // same-origin defensively so this init-window fallback cannot become an
295
+ // open redirect (the normal path validates inside the navigation bridge).
296
+ const safe = validateRedirectOrigin(url, window.location.origin);
297
+ if (safe) {
298
+ window.location.href = safe;
299
+ }
238
300
  return Promise.resolve();
239
301
  }
240
302
  return navigateFn(url, options);
@@ -249,7 +311,7 @@ export async function initBrowserApp(
249
311
  client,
250
312
  onUpdate: (update) => store.emitUpdate(update),
251
313
  renderSegments,
252
- version,
314
+ version: version,
253
315
  });
254
316
 
255
317
  // Connect action redirect → navigation bridge (now that both are initialized)
@@ -294,11 +356,11 @@ export async function initBrowserApp(
294
356
  // full lifecycle (fetching + streaming, before commit) without
295
357
  // blocking on server actions.
296
358
  if (eventController.getState().isNavigating) {
297
- console.log("[RSCRouter] HMR: Skipping — navigation in progress");
359
+ console.log("[Rango] HMR: Skipping — navigation in progress");
298
360
  return;
299
361
  }
300
362
 
301
- console.log("[RSCRouter] HMR: Server update, refetching RSC");
363
+ console.log("[Rango] HMR: Server update, refetching RSC");
302
364
 
303
365
  const abort = new AbortController();
304
366
  hmrAbort = abort;
@@ -316,6 +378,7 @@ export async function initBrowserApp(
316
378
  segmentIds: [],
317
379
  previousUrl: store.getSegmentState().currentUrl,
318
380
  interceptSourceUrl: interceptSourceUrl || undefined,
381
+ routerId: store.getRouterId?.(),
319
382
  hmr: true,
320
383
  signal: abort.signal,
321
384
  });
@@ -329,6 +392,35 @@ export async function initBrowserApp(
329
392
  throw new Error("HMR refetch returned invalid payload");
330
393
  }
331
394
 
395
+ // Update version BEFORE rebuilding state so that
396
+ // clearHistoryCache() runs first, then the fresh segment
397
+ // cache entry we create below survives.
398
+ //
399
+ // Compare against the bridge's live version, not the init-time
400
+ // `version` const: after the first HMR bump the const is stale, so a
401
+ // later update with an unchanged version would otherwise re-clear the
402
+ // cache and re-broadcast across tabs/apps. The live read fires only
403
+ // on a genuine version change.
404
+ const newVersion = payload.metadata.version;
405
+ const currentVersion = navigationBridge.getVersion();
406
+ if (newVersion && newVersion !== currentVersion) {
407
+ console.log(
408
+ "[Rango] HMR: version changed",
409
+ currentVersion,
410
+ "→",
411
+ newVersion,
412
+ "clearing caches",
413
+ );
414
+ navigationBridge.updateVersion(newVersion);
415
+ }
416
+
417
+ // Apply only partial segment updates. A non-partial payload during
418
+ // HMR is transient: the worker route table is still rebuilding after
419
+ // the edit, so the URL momentarily resolves to not-found/catch-all.
420
+ // Skip it -- the debounced follow-up refetch returns the settled
421
+ // route's partial payload and renders it below. We never reload here:
422
+ // a paramless document GET would run the SSR path and surface the
423
+ // not-found page during that same transient.
332
424
  if (payload.metadata?.isPartial) {
333
425
  const segments = payload.metadata.segments || [];
334
426
  const matched = payload.metadata.matched || [];
@@ -368,10 +460,10 @@ export async function initBrowserApp(
368
460
 
369
461
  await streamComplete;
370
462
  handle.complete(new URL(window.location.href));
371
- console.log("[RSCRouter] HMR: RSC stream complete");
463
+ console.log("[Rango] HMR: RSC stream complete");
372
464
  } catch (err) {
373
465
  if (abort.signal.aborted) return;
374
- console.warn("[RSCRouter] HMR: Refetch failed, reloading page", err);
466
+ console.warn("[Rango] HMR: Refetch failed, reloading page", err);
375
467
  window.location.reload();
376
468
  return;
377
469
  } finally {
@@ -383,7 +475,7 @@ export async function initBrowserApp(
383
475
  });
384
476
  }
385
477
 
386
- // Store context for RSCRouter component
478
+ // Store context for Rango component
387
479
  const context: BrowserAppContext = {
388
480
  store,
389
481
  eventController,
@@ -393,7 +485,9 @@ export async function initBrowserApp(
393
485
  themeConfig: effectiveThemeConfig,
394
486
  initialTheme: effectiveInitialTheme,
395
487
  warmupEnabled: initialPayload.metadata?.warmupEnabled ?? true,
488
+ strictMode: initialPayload.metadata?.strictMode ?? true,
396
489
  version,
490
+ appShellRef,
397
491
  };
398
492
  browserAppContext = context;
399
493
 
@@ -406,7 +500,7 @@ export async function initBrowserApp(
406
500
  export function getBrowserAppContext(): BrowserAppContext {
407
501
  if (!browserAppContext) {
408
502
  throw new Error(
409
- "RSCRouter: initBrowserApp() must be called before rendering RSCRouter",
503
+ "Rango: initBrowserApp() must be called before rendering Rango",
410
504
  );
411
505
  }
412
506
  return browserAppContext;
@@ -420,18 +514,18 @@ export function resetBrowserAppContext(): void {
420
514
  }
421
515
 
422
516
  /**
423
- * Props for the RSCRouter component
517
+ * Props for the Rango component
424
518
  */
425
- export interface RSCRouterProps {}
519
+ export interface RangoProps {}
426
520
 
427
521
  /**
428
- * RSCRouter component - renders the RSC router with all internal wiring.
522
+ * Rango component - renders the RSC router with all internal wiring.
429
523
  *
430
524
  * Must be called after initBrowserApp() has completed.
431
525
  *
432
526
  * @example
433
527
  * ```tsx
434
- * import { initBrowserApp, RSCRouter } from "rsc-router/browser";
528
+ * import { initBrowserApp, Rango } from "rsc-router/browser";
435
529
  * import { rscStream } from "rsc-html-stream/client";
436
530
  * import * as rscBrowser from "@vitejs/plugin-rsc/browser";
437
531
  *
@@ -441,14 +535,14 @@ export interface RSCRouterProps {}
441
535
  * hydrateRoot(
442
536
  * document,
443
537
  * <React.StrictMode>
444
- * <RSCRouter />
538
+ * <Rango />
445
539
  * </React.StrictMode>
446
540
  * );
447
541
  * }
448
542
  * main();
449
543
  * ```
450
544
  */
451
- export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
545
+ export function Rango(_props: RangoProps): React.ReactElement {
452
546
  const {
453
547
  store,
454
548
  eventController,
@@ -459,6 +553,7 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
459
553
  initialTheme,
460
554
  warmupEnabled,
461
555
  version,
556
+ appShellRef,
462
557
  } = getBrowserAppContext();
463
558
 
464
559
  // Signal that the React tree has hydrated. useEffect only fires after
@@ -478,6 +573,8 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
478
573
  initialTheme={initialTheme}
479
574
  warmupEnabled={warmupEnabled}
480
575
  version={version}
576
+ basename={initialPayload.metadata?.basename}
577
+ appShellRef={appShellRef}
481
578
  />
482
579
  );
483
580
  }
@@ -191,10 +191,15 @@ export function saveCurrentScrollPosition(): void {
191
191
 
192
192
  /**
193
193
  * Persist scroll positions to sessionStorage.
194
- * If the write fails due to quota exceeded, progressively evict the oldest
195
- * entries and retry until it succeeds or the store is empty.
194
+ * If the write fails (typically QuotaExceededError), evict the oldest ~1/4 of
195
+ * entries ONCE and retry the write a single time; if that still fails, remove
196
+ * our storage key entirely so we don't block other sessionStorage consumers.
197
+ * This is a single evict-then-retry-then-clear ladder, not a loop.
198
+ *
199
+ * Exported so that single eviction/retry/clear ladder is unit-testable directly.
200
+ * The browser drives it from the `pagehide` handler.
196
201
  */
197
- function persistToSessionStorage(): void {
202
+ export function persistToSessionStorage(): void {
198
203
  try {
199
204
  sessionStorage.setItem(
200
205
  SCROLL_STORAGE_KEY,
@@ -332,6 +337,8 @@ export function scrollToHash(): boolean {
332
337
  * Scroll to top of page
333
338
  */
334
339
  export function scrollToTop(): void {
340
+ if (typeof window === "undefined") return;
341
+ if (typeof window.scrollTo !== "function") return;
335
342
  window.scrollTo(0, 0);
336
343
  }
337
344
 
@@ -356,36 +363,44 @@ export function handleNavigationEnd(options: {
356
363
  scroll?: boolean;
357
364
  isStreaming?: () => boolean;
358
365
  }): void {
359
- if (!initialized) {
360
- return;
361
- }
362
-
363
366
  const { restore = false, scroll = true, isStreaming } = options;
364
367
 
365
- // Don't scroll if explicitly disabled
366
- if (scroll === false) {
368
+ // Don't scroll if explicitly disabled or not in a browser
369
+ if (scroll === false || typeof window === "undefined") {
367
370
  return;
368
371
  }
369
372
 
370
- // For back/forward (restore), try to restore saved position
371
- if (restore) {
373
+ // Save/restore requires initialization (sessionStorage, history state).
374
+ // But basic scroll-to-top and hash scrolling work without it — this
375
+ // matters during cross-app navigation where ScrollRestoration unmounts
376
+ // and remounts, creating a brief window where initialized is false.
377
+ if (restore && initialized) {
372
378
  if (restoreScrollPosition({ retryIfStreaming: true, isStreaming })) {
373
379
  return;
374
380
  }
375
381
  // Fall through to hash or top if no saved position
376
382
  }
377
383
 
378
- // Defer hash and scroll-to-top to after React paints the new content,
379
- // so the user doesn't see the current page jump before the new route appears.
380
- deferToNextPaint(() => {
381
- // Try hash scrolling first
382
- if (scrollToHash()) {
383
- return;
384
- }
385
-
386
- // Default: scroll to top
387
- scrollToTop();
388
- });
384
+ // scrollToHash / scrollToTop run synchronously here.
385
+ // handleNavigationEnd is invoked from NavigationProvider's
386
+ // useLayoutEffect (post-commit, pre-paint), so a sync scrollTo is
387
+ // captured by the upcoming paint AND by startViewTransition's snapshot.
388
+ // Deferring via rAF here pushed the call past the snapshot capture,
389
+ // making forward navigations wrapped in a layout/route view transition
390
+ // skip scroll-to-top — the live DOM scrolled but the captured snapshot
391
+ // was at the previous scroll position, so the user-facing page stayed
392
+ // visually clamped at the source page's scrollY (often the new tree's
393
+ // max scroll for tall→short navs). Y=0 / a hash element are robust
394
+ // against unmeasured layout, so sync scroll is correct here even
395
+ // before the new tree's scrollHeight settles.
396
+ //
397
+ // (The restore branch above keeps deferToNextPaint because savedY
398
+ // depends on the new tree's max scroll; sync scrollTo against an
399
+ // unmeasured DOM would clamp savedY to whatever the old/zero max was.)
400
+ if (scrollToHash()) {
401
+ return;
402
+ }
403
+ scrollToTop();
389
404
  }
390
405
 
391
406
  /**
@@ -6,6 +6,7 @@ import {
6
6
  } from "./merge-segment-loaders.js";
7
7
  import { assertSegmentStructure } from "./segment-structure-assert.js";
8
8
  import { splitInterceptSegments } from "./intercept-utils.js";
9
+ import { debugLog } from "./logging.js";
9
10
 
10
11
  /**
11
12
  * Determines the merging behavior for segment reconciliation.
@@ -85,14 +86,29 @@ export function reconcileSegments(input: ReconcileInput): ReconcileResult {
85
86
  const cachedSegments = new Map<string, ResolvedSegment>();
86
87
  input.cachedSegments.forEach((s) => cachedSegments.set(s.id, s));
87
88
 
89
+ const diffSet = new Set(diff);
90
+ debugLog(
91
+ `[reconcile] actor=${actor}, matched=${matched.length}, diff=${diff.length}`,
92
+ );
93
+ debugLog(
94
+ `[reconcile] server segments: ${[...serverSegments.keys()].join(", ")}`,
95
+ );
96
+ debugLog(
97
+ `[reconcile] cached segments: ${[...cachedSegments.keys()].join(", ")}`,
98
+ );
99
+
88
100
  const segments = matched
89
101
  .map((segId: string) => {
90
102
  const fromServer = serverSegments.get(segId);
91
103
  const fromCache = cachedSegments.get(segId);
92
104
 
93
105
  if (fromServer) {
106
+ const inDiff = diffSet.has(segId);
94
107
  // Merge partial loader data when server returns fewer loaders than cached
95
108
  if (shouldMergeLoaders && needsLoaderMerge(fromServer, fromCache)) {
109
+ debugLog(
110
+ `[reconcile] ${segId}: MERGE loaders (server partial, ${inDiff ? "in diff" : "not in diff"})`,
111
+ );
96
112
  return mergeSegmentLoaders(fromServer, fromCache);
97
113
  }
98
114
 
@@ -143,8 +159,14 @@ export function reconcileSegments(input: ReconcileInput): ReconcileResult {
143
159
  // above fails to preserve a value it should have.
144
160
  assertSegmentStructure(fromCache, merged, context);
145
161
 
162
+ debugLog(
163
+ `[reconcile] ${segId}: SERVER+CACHE merge (${inDiff ? "in diff" : "not in diff"}, type=${fromServer.type}, component=${fromServer.component === null ? "null→cached" : "server"})`,
164
+ );
146
165
  return merged;
147
166
  }
167
+ debugLog(
168
+ `[reconcile] ${segId}: SERVER only (${inDiff ? "in diff" : "not in diff"}, type=${fromServer.type}, no cache entry)`,
169
+ );
148
170
  return fromServer;
149
171
  }
150
172
 
@@ -158,20 +180,20 @@ export function reconcileSegments(input: ReconcileInput): ReconcileResult {
158
180
  return fromCache;
159
181
  }
160
182
 
161
- // For non-action actors: cached segments the server decided not to re-render.
162
- // - Preserve loading=false (suppressed boundary) to maintain tree structure
163
- // - Preserve parallel segment loading so renderSegments can reconstruct
164
- // parallel-owned loader markers from the cached slot metadata
165
- // - Clear other truthy loading values to prevent suspense on cached content
166
- if (actor !== "action") {
167
- if (fromCache.type === "parallel" && fromCache.loading !== undefined) {
168
- return fromCache;
169
- }
170
- if (fromCache.loading !== undefined && fromCache.loading !== false) {
171
- return { ...fromCache, loading: undefined };
172
- }
173
- }
174
-
183
+ debugLog(
184
+ `[reconcile] ${segId}: CACHE only (not from server, type=${fromCache.type}, component=${fromCache.component != null ? "yes" : "null"})`,
185
+ );
186
+
187
+ // Return the cached segment as-is, regardless of actor. We used to clear
188
+ // truthy `loading` here to prevent a stale Suspense fallback from
189
+ // committing against cached content, but that swapped the render tree
190
+ // from the LoaderBoundary branch to the plain OutletProvider branch
191
+ // inside renderSegments, causing React to unmount the entire chain
192
+ // (LoaderBoundary > Suspense > LoaderResolver > RouteContentWrapper >
193
+ // Suspender) every time the user opened an intercept or navigated back
194
+ // to a cached page. The flicker is now prevented by renderSegments'
195
+ // promise memoization keeping React's use() in "known fulfilled" state,
196
+ // so preserving `loading` keeps the element tree stable.
175
197
  return fromCache;
176
198
  })
177
199
  .filter(Boolean) as ResolvedSegment[];
@@ -48,7 +48,7 @@ export function assertSegmentStructure(
48
48
 
49
49
  if (cachedCategory !== incomingCategory) {
50
50
  console.warn(
51
- `[RSC Router] Tree structure mismatch detected in ${context} ` +
51
+ `[Rango] Tree structure mismatch detected in ${context} ` +
52
52
  `for segment "${cached.id}": loading category changed from ` +
53
53
  `"${cachedCategory}" (${describeLoading(cached.loading)}) to ` +
54
54
  `"${incomingCategory}" (${describeLoading(incoming.loading)}). ` +
@@ -64,7 +64,7 @@ export function assertSegmentStructure(
64
64
  const incomingHasMount = !!incoming.mountPath;
65
65
  if (cachedHasMount !== incomingHasMount) {
66
66
  console.warn(
67
- `[RSC Router] MountContextProvider mismatch detected in ${context} ` +
67
+ `[Rango] MountContextProvider mismatch detected in ${context} ` +
68
68
  `for segment "${cached.id}": mountPath changed from ` +
69
69
  `${cachedHasMount ? `"${cached.mountPath}"` : "undefined"} to ` +
70
70
  `${incomingHasMount ? `"${incoming.mountPath}"` : "undefined"}. ` +