@rangojs/router 0.0.0-experimental.a769fbe7 → 0.0.0-experimental.ac99d918

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 (374) hide show
  1. package/README.md +211 -43
  2. package/dist/bin/rango.js +279 -102
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +3313 -1160
  5. package/dist/vite/index.js.bak +5448 -0
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +62 -11
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +63 -1
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +222 -30
  12. package/skills/caching/SKILL.md +263 -8
  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 +250 -30
  18. package/skills/host-router/SKILL.md +124 -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 +223 -9
  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 +77 -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 +66 -5
  35. package/skills/router-setup/SKILL.md +38 -3
  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 +319 -27
  55. package/skills/use-cache/SKILL.md +36 -5
  56. package/skills/vercel/SKILL.md +107 -0
  57. package/skills/view-transitions/SKILL.md +294 -0
  58. package/src/__augment-tests__/augment.ts +81 -0
  59. package/src/__augment-tests__/augmented.check.ts +116 -0
  60. package/src/__internal.ts +1 -66
  61. package/src/browser/action-coordinator.ts +53 -36
  62. package/src/browser/action-fence.ts +47 -0
  63. package/src/browser/app-shell.ts +39 -0
  64. package/src/browser/app-version.ts +14 -0
  65. package/src/browser/cookie-name.ts +140 -0
  66. package/src/browser/event-controller.ts +81 -147
  67. package/src/browser/history-state.ts +21 -0
  68. package/src/browser/index.ts +3 -3
  69. package/src/browser/invalidate-client-cache.ts +52 -0
  70. package/src/browser/navigation-bridge.ts +66 -14
  71. package/src/browser/navigation-client.ts +172 -109
  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 +9 -59
  75. package/src/browser/partial-update.ts +79 -93
  76. package/src/browser/prefetch/cache.ts +180 -62
  77. package/src/browser/prefetch/fetch.ts +252 -39
  78. package/src/browser/prefetch/queue.ts +42 -8
  79. package/src/browser/rango-state.ts +158 -76
  80. package/src/browser/react/Link.tsx +72 -10
  81. package/src/browser/react/NavigationProvider.tsx +83 -31
  82. package/src/browser/react/ScrollRestoration.tsx +10 -6
  83. package/src/browser/react/context.ts +7 -2
  84. package/src/browser/react/filter-segment-order.ts +49 -7
  85. package/src/browser/react/index.ts +0 -48
  86. package/src/browser/react/location-state-shared.ts +166 -8
  87. package/src/browser/react/location-state.ts +39 -14
  88. package/src/browser/react/use-action.ts +6 -15
  89. package/src/browser/react/use-handle.ts +23 -69
  90. package/src/browser/react/use-link-status.ts +0 -4
  91. package/src/browser/react/use-navigation.ts +22 -5
  92. package/src/browser/react/use-params.ts +20 -10
  93. package/src/browser/react/use-reverse.ts +106 -0
  94. package/src/browser/react/use-router.ts +46 -11
  95. package/src/browser/react/use-search-params.ts +0 -5
  96. package/src/browser/react/use-segments.ts +11 -21
  97. package/src/browser/response-adapter.ts +52 -1
  98. package/src/browser/rsc-router.tsx +111 -24
  99. package/src/browser/scroll-restoration.ts +29 -19
  100. package/src/browser/segment-reconciler.ts +36 -14
  101. package/src/browser/segment-structure-assert.ts +2 -2
  102. package/src/browser/server-action-bridge.ts +176 -62
  103. package/src/browser/types.ts +60 -11
  104. package/src/browser/validate-redirect-origin.ts +43 -16
  105. package/src/build/collect-fallback-refs.ts +107 -0
  106. package/src/build/generate-manifest.ts +65 -40
  107. package/src/build/generate-route-types.ts +6 -0
  108. package/src/build/index.ts +8 -2
  109. package/src/build/prefix-tree-utils.ts +123 -0
  110. package/src/build/route-trie.ts +137 -32
  111. package/src/build/route-types/codegen.ts +4 -4
  112. package/src/build/route-types/include-resolution.ts +9 -2
  113. package/src/build/route-types/param-extraction.ts +6 -3
  114. package/src/build/route-types/per-module-writer.ts +7 -4
  115. package/src/build/route-types/router-processing.ts +333 -94
  116. package/src/build/route-types/scan-filter.ts +9 -2
  117. package/src/build/route-types/source-scan.ts +118 -0
  118. package/src/build/runtime-discovery.ts +9 -20
  119. package/src/cache/cache-error.ts +104 -0
  120. package/src/cache/cache-policy.ts +68 -28
  121. package/src/cache/cache-runtime.ts +134 -32
  122. package/src/cache/cache-scope.ts +100 -74
  123. package/src/cache/cache-tag.ts +98 -0
  124. package/src/cache/cf/cf-cache-store.ts +2256 -241
  125. package/src/cache/cf/index.ts +6 -16
  126. package/src/cache/document-cache.ts +61 -20
  127. package/src/cache/handle-snapshot.ts +63 -0
  128. package/src/cache/index.ts +22 -20
  129. package/src/cache/memory-segment-store.ts +136 -37
  130. package/src/cache/profile-registry.ts +6 -30
  131. package/src/cache/read-through-swr.ts +41 -11
  132. package/src/cache/segment-codec.ts +0 -16
  133. package/src/cache/tag-invalidation.ts +230 -0
  134. package/src/cache/types.ts +33 -100
  135. package/src/cache/vercel/index.ts +11 -0
  136. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  137. package/src/client.rsc.tsx +6 -21
  138. package/src/client.tsx +108 -290
  139. package/src/component-utils.ts +19 -0
  140. package/src/context-var.ts +17 -5
  141. package/src/decode-loader-results.ts +36 -0
  142. package/src/defer.ts +196 -0
  143. package/src/deps/browser.ts +0 -1
  144. package/src/deps/ssr.ts +0 -1
  145. package/src/errors.ts +30 -4
  146. package/src/handle.ts +70 -22
  147. package/src/handles/MetaTags.tsx +0 -14
  148. package/src/handles/breadcrumbs.ts +16 -5
  149. package/src/handles/meta.ts +0 -39
  150. package/src/host/cookie-handler.ts +0 -36
  151. package/src/host/errors.ts +0 -24
  152. package/src/host/index.ts +8 -2
  153. package/src/host/pattern-matcher.ts +7 -50
  154. package/src/host/router.ts +107 -99
  155. package/src/host/testing.ts +40 -27
  156. package/src/host/types.ts +37 -4
  157. package/src/host/utils.ts +1 -1
  158. package/src/href-client.ts +137 -22
  159. package/src/index.rsc.ts +69 -10
  160. package/src/index.ts +112 -14
  161. package/src/internal-debug.ts +2 -4
  162. package/src/loader-store.ts +500 -0
  163. package/src/loader.rsc.ts +20 -13
  164. package/src/loader.ts +12 -11
  165. package/src/missing-id-error.ts +68 -0
  166. package/src/network-error-thrower.tsx +1 -6
  167. package/src/outlet-context.ts +1 -1
  168. package/src/outlet-provider.tsx +1 -5
  169. package/src/prerender/param-hash.ts +10 -11
  170. package/src/prerender/store.ts +37 -41
  171. package/src/prerender.ts +198 -82
  172. package/src/redirect-origin.ts +100 -0
  173. package/src/response-utils.ts +37 -0
  174. package/src/reverse.ts +65 -15
  175. package/src/root-error-boundary.tsx +1 -19
  176. package/src/route-content-wrapper.tsx +7 -72
  177. package/src/route-definition/dsl-helpers.ts +413 -275
  178. package/src/route-definition/helper-factories.ts +29 -139
  179. package/src/route-definition/helpers-types.ts +107 -32
  180. package/src/route-definition/index.ts +3 -0
  181. package/src/route-definition/redirect.ts +50 -8
  182. package/src/route-definition/resolve-handler-use.ts +161 -0
  183. package/src/route-definition/use-item-types.ts +32 -0
  184. package/src/route-map-builder.ts +0 -16
  185. package/src/route-types.ts +37 -41
  186. package/src/router/basename.ts +14 -0
  187. package/src/router/content-negotiation.ts +108 -9
  188. package/src/router/error-handling.ts +13 -17
  189. package/src/router/find-match.ts +44 -23
  190. package/src/router/handler-context.ts +46 -30
  191. package/src/router/intercept-resolution.ts +23 -23
  192. package/src/router/lazy-includes.ts +15 -52
  193. package/src/router/loader-resolution.ts +207 -30
  194. package/src/router/logging.ts +0 -6
  195. package/src/router/manifest.ts +40 -42
  196. package/src/router/match-api.ts +120 -204
  197. package/src/router/match-context.ts +0 -22
  198. package/src/router/match-handlers.ts +58 -58
  199. package/src/router/match-middleware/background-revalidation.ts +0 -7
  200. package/src/router/match-middleware/cache-lookup.ts +161 -262
  201. package/src/router/match-middleware/cache-store.ts +3 -33
  202. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  203. package/src/router/match-middleware/segment-resolution.ts +45 -14
  204. package/src/router/match-pipelines.ts +1 -42
  205. package/src/router/match-result.ts +87 -39
  206. package/src/router/metrics.ts +0 -34
  207. package/src/router/middleware-types.ts +7 -140
  208. package/src/router/middleware.ts +169 -140
  209. package/src/router/navigation-snapshot.ts +131 -0
  210. package/src/router/params-util.ts +23 -0
  211. package/src/router/pattern-matching.ts +109 -63
  212. package/src/router/prerender-match.ts +190 -54
  213. package/src/router/preview-match.ts +32 -102
  214. package/src/router/request-classification.ts +276 -0
  215. package/src/router/revalidation.ts +63 -55
  216. package/src/router/route-snapshot.ts +244 -0
  217. package/src/router/router-context.ts +0 -27
  218. package/src/router/router-interfaces.ts +100 -35
  219. package/src/router/router-options.ts +91 -11
  220. package/src/router/router-registry.ts +2 -5
  221. package/src/router/segment-resolution/fresh.ts +119 -65
  222. package/src/router/segment-resolution/helpers.ts +34 -0
  223. package/src/router/segment-resolution/loader-cache.ts +40 -37
  224. package/src/router/segment-resolution/revalidation.ts +329 -305
  225. package/src/router/segment-resolution/static-store.ts +19 -5
  226. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  227. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  228. package/src/router/segment-resolution.ts +4 -1
  229. package/src/router/segment-wrappers.ts +0 -3
  230. package/src/router/state-cookie-name.ts +33 -0
  231. package/src/router/substitute-pattern-params.ts +56 -0
  232. package/src/router/telemetry-otel.ts +0 -20
  233. package/src/router/telemetry.ts +96 -19
  234. package/src/router/timeout.ts +0 -20
  235. package/src/router/trie-matching.ts +91 -46
  236. package/src/router/types.ts +9 -63
  237. package/src/router/url-params.ts +44 -0
  238. package/src/router.ts +128 -42
  239. package/src/rsc/handler-context.ts +3 -2
  240. package/src/rsc/handler.ts +492 -409
  241. package/src/rsc/helpers.ts +162 -46
  242. package/src/rsc/index.ts +1 -1
  243. package/src/rsc/json-route-result.ts +38 -0
  244. package/src/rsc/loader-fetch.ts +18 -3
  245. package/src/rsc/manifest-init.ts +33 -42
  246. package/src/rsc/origin-guard.ts +39 -25
  247. package/src/rsc/progressive-enhancement.ts +28 -4
  248. package/src/rsc/redirect-guard.ts +99 -0
  249. package/src/rsc/response-error.ts +79 -12
  250. package/src/rsc/response-route-handler.ts +90 -63
  251. package/src/rsc/rsc-rendering.ts +53 -56
  252. package/src/rsc/runtime-warnings.ts +23 -10
  253. package/src/rsc/server-action.ts +74 -69
  254. package/src/rsc/ssr-setup.ts +18 -2
  255. package/src/rsc/types.ts +22 -9
  256. package/src/runtime-env.ts +18 -0
  257. package/src/search-params.ts +4 -20
  258. package/src/segment-content-promise.ts +67 -0
  259. package/src/segment-loader-promise.ts +134 -0
  260. package/src/segment-system.tsx +208 -201
  261. package/src/serialize.ts +243 -0
  262. package/src/server/context.ts +211 -52
  263. package/src/server/cookie-store.ts +80 -5
  264. package/src/server/handle-store.ts +26 -24
  265. package/src/server/loader-registry.ts +10 -28
  266. package/src/server/request-context.ts +289 -124
  267. package/src/ssr/index.tsx +22 -15
  268. package/src/static-handler.ts +27 -18
  269. package/src/testing/cache-status.ts +162 -0
  270. package/src/testing/collect-handle.ts +40 -0
  271. package/src/testing/dispatch.ts +618 -0
  272. package/src/testing/dom.entry.ts +22 -0
  273. package/src/testing/e2e/fixture.ts +188 -0
  274. package/src/testing/e2e/index.ts +128 -0
  275. package/src/testing/e2e/matchers.ts +35 -0
  276. package/src/testing/e2e/page-helpers.ts +272 -0
  277. package/src/testing/e2e/parity.ts +387 -0
  278. package/src/testing/e2e/server.ts +195 -0
  279. package/src/testing/flight-matchers.ts +97 -0
  280. package/src/testing/flight-normalize.ts +11 -0
  281. package/src/testing/flight-runtime.d.ts +57 -0
  282. package/src/testing/flight-tree.ts +682 -0
  283. package/src/testing/flight.entry.ts +52 -0
  284. package/src/testing/flight.ts +232 -0
  285. package/src/testing/generated-routes.ts +183 -0
  286. package/src/testing/index.ts +99 -0
  287. package/src/testing/internal/context.ts +348 -0
  288. package/src/testing/internal/flight-client-globals.ts +30 -0
  289. package/src/testing/internal/seed-vars.ts +54 -0
  290. package/src/testing/render-handler.ts +330 -0
  291. package/src/testing/render-route.tsx +566 -0
  292. package/src/testing/run-loader.ts +378 -0
  293. package/src/testing/run-middleware.ts +205 -0
  294. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  295. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  296. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  297. package/src/testing/vitest-stubs/version.ts +5 -0
  298. package/src/testing/vitest.ts +305 -0
  299. package/src/theme/ThemeProvider.tsx +0 -52
  300. package/src/theme/ThemeScript.tsx +0 -6
  301. package/src/theme/constants.ts +0 -12
  302. package/src/theme/index.ts +0 -7
  303. package/src/theme/theme-context.ts +1 -5
  304. package/src/theme/theme-script.ts +0 -14
  305. package/src/theme/use-theme.ts +0 -3
  306. package/src/types/boundaries.ts +0 -35
  307. package/src/types/cache-types.ts +17 -8
  308. package/src/types/error-types.ts +30 -90
  309. package/src/types/global-namespace.ts +54 -41
  310. package/src/types/handler-context.ts +124 -70
  311. package/src/types/index.ts +1 -10
  312. package/src/types/loader-types.ts +40 -11
  313. package/src/types/request-scope.ts +107 -0
  314. package/src/types/route-config.ts +6 -50
  315. package/src/types/route-entry.ts +12 -7
  316. package/src/types/segments.ts +36 -15
  317. package/src/urls/include-helper.ts +33 -70
  318. package/src/urls/index.ts +1 -11
  319. package/src/urls/path-helper-types.ts +58 -11
  320. package/src/urls/path-helper.ts +57 -111
  321. package/src/urls/pattern-types.ts +48 -19
  322. package/src/urls/response-types.ts +25 -22
  323. package/src/urls/type-extraction.ts +58 -139
  324. package/src/urls/urls-function.ts +1 -18
  325. package/src/use-loader.tsx +346 -89
  326. package/src/vite/debug.ts +185 -0
  327. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  328. package/src/vite/discovery/discover-routers.ts +130 -85
  329. package/src/vite/discovery/discovery-errors.ts +194 -0
  330. package/src/vite/discovery/gate-state.ts +171 -0
  331. package/src/vite/discovery/prerender-collection.ts +192 -99
  332. package/src/vite/discovery/route-types-writer.ts +40 -84
  333. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  334. package/src/vite/discovery/state.ts +51 -4
  335. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  336. package/src/vite/index.ts +8 -0
  337. package/src/vite/plugin-types.ts +236 -6
  338. package/src/vite/plugins/cjs-to-esm.ts +8 -18
  339. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  340. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  341. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  342. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  343. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  344. package/src/vite/plugins/expose-action-id.ts +48 -95
  345. package/src/vite/plugins/expose-id-utils.ts +11 -50
  346. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  347. package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
  348. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  349. package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
  350. package/src/vite/plugins/expose-internal-ids.ts +554 -317
  351. package/src/vite/plugins/performance-tracks.ts +65 -207
  352. package/src/vite/plugins/refresh-cmd.ts +89 -27
  353. package/src/vite/plugins/use-cache-transform.ts +73 -83
  354. package/src/vite/plugins/vercel-output.ts +258 -0
  355. package/src/vite/plugins/version-injector.ts +21 -25
  356. package/src/vite/plugins/version-plugin.ts +32 -23
  357. package/src/vite/plugins/virtual-entries.ts +46 -17
  358. package/src/vite/rango.ts +207 -125
  359. package/src/vite/router-discovery.ts +931 -133
  360. package/src/vite/utils/ast-handler-extract.ts +15 -31
  361. package/src/vite/utils/banner.ts +1 -1
  362. package/src/vite/utils/bundle-analysis.ts +10 -15
  363. package/src/vite/utils/client-chunks.ts +184 -0
  364. package/src/vite/utils/forward-user-plugins.ts +171 -0
  365. package/src/vite/utils/manifest-utils.ts +4 -59
  366. package/src/vite/utils/package-resolution.ts +20 -52
  367. package/src/vite/utils/prerender-utils.ts +27 -29
  368. package/src/vite/utils/shared-utils.ts +97 -44
  369. package/src/browser/action-response-classifier.ts +0 -99
  370. package/src/browser/debug-channel.ts +0 -93
  371. package/src/browser/react/use-client-cache.ts +0 -58
  372. package/src/browser/shallow.ts +0 -40
  373. package/src/handles/index.ts +0 -7
  374. 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,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)
@@ -294,11 +347,11 @@ export async function initBrowserApp(
294
347
  // full lifecycle (fetching + streaming, before commit) without
295
348
  // blocking on server actions.
296
349
  if (eventController.getState().isNavigating) {
297
- console.log("[RSCRouter] HMR: Skipping — navigation in progress");
350
+ console.log("[Rango] HMR: Skipping — navigation in progress");
298
351
  return;
299
352
  }
300
353
 
301
- console.log("[RSCRouter] HMR: Server update, refetching RSC");
354
+ console.log("[Rango] HMR: Server update, refetching RSC");
302
355
 
303
356
  const abort = new AbortController();
304
357
  hmrAbort = abort;
@@ -316,6 +369,7 @@ export async function initBrowserApp(
316
369
  segmentIds: [],
317
370
  previousUrl: store.getSegmentState().currentUrl,
318
371
  interceptSourceUrl: interceptSourceUrl || undefined,
372
+ routerId: store.getRouterId?.(),
319
373
  hmr: true,
320
374
  signal: abort.signal,
321
375
  });
@@ -329,6 +383,35 @@ export async function initBrowserApp(
329
383
  throw new Error("HMR refetch returned invalid payload");
330
384
  }
331
385
 
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);
406
+ }
407
+
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.
332
415
  if (payload.metadata?.isPartial) {
333
416
  const segments = payload.metadata.segments || [];
334
417
  const matched = payload.metadata.matched || [];
@@ -368,10 +451,10 @@ export async function initBrowserApp(
368
451
 
369
452
  await streamComplete;
370
453
  handle.complete(new URL(window.location.href));
371
- console.log("[RSCRouter] HMR: RSC stream complete");
454
+ console.log("[Rango] HMR: RSC stream complete");
372
455
  } catch (err) {
373
456
  if (abort.signal.aborted) return;
374
- console.warn("[RSCRouter] HMR: Refetch failed, reloading page", err);
457
+ console.warn("[Rango] HMR: Refetch failed, reloading page", err);
375
458
  window.location.reload();
376
459
  return;
377
460
  } finally {
@@ -383,7 +466,7 @@ export async function initBrowserApp(
383
466
  });
384
467
  }
385
468
 
386
- // Store context for RSCRouter component
469
+ // Store context for Rango component
387
470
  const context: BrowserAppContext = {
388
471
  store,
389
472
  eventController,
@@ -394,6 +477,7 @@ export async function initBrowserApp(
394
477
  initialTheme: effectiveInitialTheme,
395
478
  warmupEnabled: initialPayload.metadata?.warmupEnabled ?? true,
396
479
  version,
480
+ appShellRef,
397
481
  };
398
482
  browserAppContext = context;
399
483
 
@@ -406,7 +490,7 @@ export async function initBrowserApp(
406
490
  export function getBrowserAppContext(): BrowserAppContext {
407
491
  if (!browserAppContext) {
408
492
  throw new Error(
409
- "RSCRouter: initBrowserApp() must be called before rendering RSCRouter",
493
+ "Rango: initBrowserApp() must be called before rendering Rango",
410
494
  );
411
495
  }
412
496
  return browserAppContext;
@@ -420,18 +504,18 @@ export function resetBrowserAppContext(): void {
420
504
  }
421
505
 
422
506
  /**
423
- * Props for the RSCRouter component
507
+ * Props for the Rango component
424
508
  */
425
- export interface RSCRouterProps {}
509
+ export interface RangoProps {}
426
510
 
427
511
  /**
428
- * RSCRouter component - renders the RSC router with all internal wiring.
512
+ * Rango component - renders the RSC router with all internal wiring.
429
513
  *
430
514
  * Must be called after initBrowserApp() has completed.
431
515
  *
432
516
  * @example
433
517
  * ```tsx
434
- * import { initBrowserApp, RSCRouter } from "rsc-router/browser";
518
+ * import { initBrowserApp, Rango } from "rsc-router/browser";
435
519
  * import { rscStream } from "rsc-html-stream/client";
436
520
  * import * as rscBrowser from "@vitejs/plugin-rsc/browser";
437
521
  *
@@ -441,14 +525,14 @@ export interface RSCRouterProps {}
441
525
  * hydrateRoot(
442
526
  * document,
443
527
  * <React.StrictMode>
444
- * <RSCRouter />
528
+ * <Rango />
445
529
  * </React.StrictMode>
446
530
  * );
447
531
  * }
448
532
  * main();
449
533
  * ```
450
534
  */
451
- export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
535
+ export function Rango(_props: RangoProps): React.ReactElement {
452
536
  const {
453
537
  store,
454
538
  eventController,
@@ -459,6 +543,7 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
459
543
  initialTheme,
460
544
  warmupEnabled,
461
545
  version,
546
+ appShellRef,
462
547
  } = getBrowserAppContext();
463
548
 
464
549
  // Signal that the React tree has hydrated. useEffect only fires after
@@ -478,6 +563,8 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
478
563
  initialTheme={initialTheme}
479
564
  warmupEnabled={warmupEnabled}
480
565
  version={version}
566
+ basename={initialPayload.metadata?.basename}
567
+ appShellRef={appShellRef}
481
568
  />
482
569
  );
483
570
  }
@@ -332,6 +332,8 @@ export function scrollToHash(): boolean {
332
332
  * Scroll to top of page
333
333
  */
334
334
  export function scrollToTop(): void {
335
+ if (typeof window === "undefined") return;
336
+ if (typeof window.scrollTo !== "function") return;
335
337
  window.scrollTo(0, 0);
336
338
  }
337
339
 
@@ -356,36 +358,44 @@ export function handleNavigationEnd(options: {
356
358
  scroll?: boolean;
357
359
  isStreaming?: () => boolean;
358
360
  }): void {
359
- if (!initialized) {
360
- return;
361
- }
362
-
363
361
  const { restore = false, scroll = true, isStreaming } = options;
364
362
 
365
- // Don't scroll if explicitly disabled
366
- if (scroll === false) {
363
+ // Don't scroll if explicitly disabled or not in a browser
364
+ if (scroll === false || typeof window === "undefined") {
367
365
  return;
368
366
  }
369
367
 
370
- // For back/forward (restore), try to restore saved position
371
- if (restore) {
368
+ // Save/restore requires initialization (sessionStorage, history state).
369
+ // But basic scroll-to-top and hash scrolling work without it — this
370
+ // matters during cross-app navigation where ScrollRestoration unmounts
371
+ // and remounts, creating a brief window where initialized is false.
372
+ if (restore && initialized) {
372
373
  if (restoreScrollPosition({ retryIfStreaming: true, isStreaming })) {
373
374
  return;
374
375
  }
375
376
  // Fall through to hash or top if no saved position
376
377
  }
377
378
 
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
- });
379
+ // scrollToHash / scrollToTop run synchronously here.
380
+ // handleNavigationEnd is invoked from NavigationProvider's
381
+ // useLayoutEffect (post-commit, pre-paint), so a sync scrollTo is
382
+ // captured by the upcoming paint AND by startViewTransition's snapshot.
383
+ // Deferring via rAF here pushed the call past the snapshot capture,
384
+ // making forward navigations wrapped in a layout/route view transition
385
+ // skip scroll-to-top — the live DOM scrolled but the captured snapshot
386
+ // was at the previous scroll position, so the user-facing page stayed
387
+ // visually clamped at the source page's scrollY (often the new tree's
388
+ // max scroll for tall→short navs). Y=0 / a hash element are robust
389
+ // against unmeasured layout, so sync scroll is correct here even
390
+ // before the new tree's scrollHeight settles.
391
+ //
392
+ // (The restore branch above keeps deferToNextPaint because savedY
393
+ // depends on the new tree's max scroll; sync scrollTo against an
394
+ // unmeasured DOM would clamp savedY to whatever the old/zero max was.)
395
+ if (scrollToHash()) {
396
+ return;
397
+ }
398
+ scrollToTop();
389
399
  }
390
400
 
391
401
  /**
@@ -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"}. ` +