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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (406) hide show
  1. package/AGENTS.md +17 -0
  2. package/README.md +291 -61
  3. package/dist/bin/rango.js +544 -143
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3744 -1329
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +67 -13
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +312 -0
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +247 -23
  12. package/skills/caching/SKILL.md +322 -19
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/debug-manifest/SKILL.md +4 -2
  16. package/skills/document-cache/SKILL.md +78 -55
  17. package/skills/handler-use/SKILL.md +364 -0
  18. package/skills/hooks/SKILL.md +282 -60
  19. package/skills/host-router/SKILL.md +278 -0
  20. package/skills/i18n/SKILL.md +276 -0
  21. package/skills/intercept/SKILL.md +50 -6
  22. package/skills/layout/SKILL.md +35 -9
  23. package/skills/links/SKILL.md +249 -17
  24. package/skills/loader/SKILL.md +297 -31
  25. package/skills/middleware/SKILL.md +52 -13
  26. package/skills/migrate-nextjs/SKILL.md +584 -0
  27. package/skills/migrate-react-router/SKILL.md +771 -0
  28. package/skills/mime-routes/SKILL.md +28 -1
  29. package/skills/observability/SKILL.md +172 -0
  30. package/skills/parallel/SKILL.md +203 -7
  31. package/skills/prerender/SKILL.md +155 -111
  32. package/skills/rango/SKILL.md +251 -23
  33. package/skills/react-compiler/SKILL.md +168 -0
  34. package/skills/response-routes/SKILL.md +123 -48
  35. package/skills/route/SKILL.md +104 -9
  36. package/skills/router-setup/SKILL.md +124 -11
  37. package/skills/scripts/SKILL.md +179 -0
  38. package/skills/server-actions/SKILL.md +775 -0
  39. package/skills/streams-and-websockets/SKILL.md +283 -0
  40. package/skills/tailwind/SKILL.md +27 -3
  41. package/skills/testing/SKILL.md +125 -222
  42. package/skills/testing/bindings.md +103 -0
  43. package/skills/testing/cache-prerender.md +127 -0
  44. package/skills/testing/client-components.md +124 -0
  45. package/skills/testing/e2e-parity.md +125 -0
  46. package/skills/testing/flight.md +91 -0
  47. package/skills/testing/handles.md +129 -0
  48. package/skills/testing/loader.md +128 -0
  49. package/skills/testing/middleware.md +99 -0
  50. package/skills/testing/render-handler.md +121 -0
  51. package/skills/testing/response-routes.md +95 -0
  52. package/skills/testing/reverse-and-types.md +84 -0
  53. package/skills/testing/server-actions.md +107 -0
  54. package/skills/testing/server-tree.md +128 -0
  55. package/skills/testing/setup.md +123 -0
  56. package/skills/typesafety/SKILL.md +357 -52
  57. package/skills/use-cache/SKILL.md +46 -14
  58. package/skills/view-transitions/SKILL.md +294 -0
  59. package/src/__augment-tests__/augment.ts +81 -0
  60. package/src/__augment-tests__/augmented.check.ts +116 -0
  61. package/src/__internal.ts +67 -40
  62. package/src/bin/rango.ts +18 -0
  63. package/src/browser/action-coordinator.ts +53 -36
  64. package/src/browser/action-fence.ts +47 -0
  65. package/src/browser/app-shell.ts +39 -0
  66. package/src/browser/app-version.ts +14 -0
  67. package/src/browser/connection-warmup.ts +134 -0
  68. package/src/browser/cookie-name.ts +140 -0
  69. package/src/browser/event-controller.ts +197 -150
  70. package/src/browser/history-state.ts +21 -0
  71. package/src/browser/index.ts +3 -3
  72. package/src/browser/invalidate-client-cache.ts +52 -0
  73. package/src/browser/link-interceptor.ts +4 -0
  74. package/src/browser/navigation-bridge.ts +200 -30
  75. package/src/browser/navigation-client.ts +217 -58
  76. package/src/browser/navigation-store-handle.ts +38 -0
  77. package/src/browser/navigation-store.ts +76 -67
  78. package/src/browser/navigation-transaction.ts +18 -66
  79. package/src/browser/network-error-handler.ts +34 -7
  80. package/src/browser/partial-update.ts +187 -112
  81. package/src/browser/prefetch/cache.ts +312 -30
  82. package/src/browser/prefetch/fetch.ts +344 -47
  83. package/src/browser/prefetch/policy.ts +6 -0
  84. package/src/browser/prefetch/queue.ts +126 -20
  85. package/src/browser/prefetch/resource-ready.ts +77 -0
  86. package/src/browser/rango-state.ts +158 -76
  87. package/src/browser/react/Link.tsx +125 -18
  88. package/src/browser/react/NavigationProvider.tsx +135 -120
  89. package/src/browser/react/ScrollRestoration.tsx +10 -6
  90. package/src/browser/react/context.ts +7 -2
  91. package/src/browser/react/filter-segment-order.ts +66 -7
  92. package/src/browser/react/index.ts +0 -48
  93. package/src/browser/react/location-state-shared.ts +178 -8
  94. package/src/browser/react/location-state.ts +39 -14
  95. package/src/browser/react/use-action.ts +6 -15
  96. package/src/browser/react/use-handle.ts +23 -69
  97. package/src/browser/react/use-href.tsx +8 -1
  98. package/src/browser/react/use-link-status.ts +33 -8
  99. package/src/browser/react/use-navigation.ts +32 -7
  100. package/src/browser/react/use-params.ts +20 -10
  101. package/src/browser/react/use-reverse.ts +106 -0
  102. package/src/browser/react/use-router.ts +46 -11
  103. package/src/browser/react/use-search-params.ts +0 -5
  104. package/src/browser/react/use-segments.ts +11 -21
  105. package/src/browser/response-adapter.ts +80 -5
  106. package/src/browser/rsc-router.tsx +226 -75
  107. package/src/browser/scroll-restoration.ts +54 -42
  108. package/src/browser/segment-reconciler.ts +36 -9
  109. package/src/browser/segment-structure-assert.ts +2 -2
  110. package/src/browser/server-action-bridge.ts +619 -442
  111. package/src/browser/types.ts +115 -11
  112. package/src/browser/validate-redirect-origin.ts +43 -16
  113. package/src/build/collect-fallback-refs.ts +107 -0
  114. package/src/build/generate-manifest.ts +65 -40
  115. package/src/build/generate-route-types.ts +7 -1
  116. package/src/build/index.ts +8 -2
  117. package/src/build/prefix-tree-utils.ts +123 -0
  118. package/src/build/route-trie.ts +182 -37
  119. package/src/build/route-types/ast-route-extraction.ts +15 -8
  120. package/src/build/route-types/codegen.ts +16 -5
  121. package/src/build/route-types/include-resolution.ts +125 -24
  122. package/src/build/route-types/param-extraction.ts +6 -3
  123. package/src/build/route-types/per-module-writer.ts +22 -6
  124. package/src/build/route-types/router-processing.ts +392 -106
  125. package/src/build/route-types/scan-filter.ts +9 -2
  126. package/src/build/route-types/source-scan.ts +216 -0
  127. package/src/build/runtime-discovery.ts +9 -20
  128. package/src/cache/cache-error.ts +104 -0
  129. package/src/cache/cache-key-utils.ts +29 -13
  130. package/src/cache/cache-policy.ts +108 -34
  131. package/src/cache/cache-runtime.ts +214 -48
  132. package/src/cache/cache-scope.ts +236 -89
  133. package/src/cache/cache-tag.ts +103 -0
  134. package/src/cache/cf/cf-base64.ts +33 -0
  135. package/src/cache/cf/cf-cache-constants.ts +127 -0
  136. package/src/cache/cf/cf-cache-store.ts +2224 -171
  137. package/src/cache/cf/cf-cache-types.ts +349 -0
  138. package/src/cache/cf/cf-kv-utils.ts +46 -0
  139. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  140. package/src/cache/cf/index.ts +11 -17
  141. package/src/cache/document-cache.ts +89 -27
  142. package/src/cache/handle-snapshot.ts +70 -0
  143. package/src/cache/index.ts +11 -20
  144. package/src/cache/memory-segment-store.ts +136 -37
  145. package/src/cache/profile-registry.ts +31 -31
  146. package/src/cache/read-through-swr.ts +41 -11
  147. package/src/cache/segment-codec.ts +9 -17
  148. package/src/cache/tag-invalidation.ts +230 -0
  149. package/src/cache/taint.ts +55 -0
  150. package/src/cache/types.ts +37 -100
  151. package/src/client.rsc.tsx +45 -21
  152. package/src/client.tsx +120 -336
  153. package/src/cloudflare/index.ts +11 -0
  154. package/src/cloudflare/tracing.ts +109 -0
  155. package/src/component-utils.ts +19 -0
  156. package/src/components/DefaultDocument.tsx +8 -2
  157. package/src/context-var.ts +84 -2
  158. package/src/debug.ts +2 -2
  159. package/src/decode-loader-results.ts +52 -0
  160. package/src/defer.ts +196 -0
  161. package/src/deps/ssr.ts +0 -1
  162. package/src/encode-kv.ts +49 -0
  163. package/src/errors.ts +30 -4
  164. package/src/escape-script.ts +52 -0
  165. package/src/handle.ts +70 -22
  166. package/src/handles/MetaTags.tsx +56 -19
  167. package/src/handles/Scripts.tsx +183 -0
  168. package/src/handles/breadcrumbs.ts +95 -0
  169. package/src/handles/is-thenable.ts +19 -0
  170. package/src/handles/meta.ts +51 -40
  171. package/src/handles/script.ts +244 -0
  172. package/src/host/cookie-handler.ts +9 -60
  173. package/src/host/errors.ts +0 -24
  174. package/src/host/index.ts +8 -5
  175. package/src/host/pattern-matcher.ts +23 -52
  176. package/src/host/router.ts +107 -99
  177. package/src/host/testing.ts +40 -27
  178. package/src/host/types.ts +37 -4
  179. package/src/host/utils.ts +1 -1
  180. package/src/href-client.ts +137 -22
  181. package/src/index.rsc.ts +79 -29
  182. package/src/index.ts +149 -65
  183. package/src/internal-debug.ts +11 -10
  184. package/src/loader-store.ts +500 -0
  185. package/src/loader.rsc.ts +20 -13
  186. package/src/loader.ts +12 -11
  187. package/src/missing-id-error.ts +68 -0
  188. package/src/outlet-context.ts +1 -1
  189. package/src/outlet-provider.tsx +1 -5
  190. package/src/prerender/param-hash.ts +16 -16
  191. package/src/prerender/store.ts +63 -26
  192. package/src/prerender.ts +198 -82
  193. package/src/redirect-origin.ts +100 -0
  194. package/src/regex-escape.ts +8 -0
  195. package/src/render-error-thrower.tsx +20 -0
  196. package/src/response-utils.ts +62 -0
  197. package/src/reverse.ts +65 -15
  198. package/src/root-error-boundary.tsx +1 -19
  199. package/src/route-content-wrapper.tsx +7 -72
  200. package/src/route-definition/dsl-helpers.ts +469 -276
  201. package/src/route-definition/helper-factories.ts +29 -139
  202. package/src/route-definition/helpers-types.ts +113 -37
  203. package/src/route-definition/index.ts +3 -3
  204. package/src/route-definition/redirect.ts +53 -12
  205. package/src/route-definition/resolve-handler-use.ts +161 -0
  206. package/src/route-definition/use-item-types.ts +32 -0
  207. package/src/route-map-builder.ts +7 -17
  208. package/src/route-types.ts +37 -41
  209. package/src/router/basename.ts +14 -0
  210. package/src/router/content-negotiation.ts +164 -17
  211. package/src/router/error-handling.ts +45 -18
  212. package/src/router/find-match.ts +45 -22
  213. package/src/router/handler-context.ts +110 -39
  214. package/src/router/instrument.ts +350 -0
  215. package/src/router/intercept-resolution.ts +50 -24
  216. package/src/router/lazy-includes.ts +19 -53
  217. package/src/router/loader-resolution.ts +274 -56
  218. package/src/router/logging.ts +5 -8
  219. package/src/router/manifest.ts +49 -45
  220. package/src/router/match-api.ts +121 -205
  221. package/src/router/match-context.ts +0 -22
  222. package/src/router/match-handlers.ts +58 -58
  223. package/src/router/match-middleware/background-revalidation.ts +33 -6
  224. package/src/router/match-middleware/cache-lookup.ts +214 -263
  225. package/src/router/match-middleware/cache-store.ts +73 -33
  226. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  227. package/src/router/match-middleware/segment-resolution.ts +52 -18
  228. package/src/router/match-pipelines.ts +1 -42
  229. package/src/router/match-result.ts +104 -49
  230. package/src/router/metrics.ts +217 -26
  231. package/src/router/middleware-types.ts +24 -110
  232. package/src/router/middleware.ts +384 -197
  233. package/src/router/navigation-snapshot.ts +131 -0
  234. package/src/router/params-util.ts +23 -0
  235. package/src/router/pattern-matching.ts +148 -91
  236. package/src/router/prefetch-cache-ttl.ts +51 -0
  237. package/src/router/prerender-match.ts +199 -56
  238. package/src/router/preview-match.ts +32 -102
  239. package/src/router/request-classification.ts +276 -0
  240. package/src/router/revalidation.ts +144 -74
  241. package/src/router/route-snapshot.ts +244 -0
  242. package/src/router/router-context.ts +8 -28
  243. package/src/router/router-interfaces.ts +129 -36
  244. package/src/router/router-options.ts +185 -23
  245. package/src/router/router-registry.ts +2 -5
  246. package/src/router/segment-resolution/fresh.ts +281 -76
  247. package/src/router/segment-resolution/helpers.ts +116 -31
  248. package/src/router/segment-resolution/loader-cache.ts +63 -37
  249. package/src/router/segment-resolution/revalidation.ts +493 -391
  250. package/src/router/segment-resolution/static-store.ts +19 -5
  251. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  252. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  253. package/src/router/segment-resolution.ts +5 -1
  254. package/src/router/segment-wrappers.ts +8 -5
  255. package/src/router/state-cookie-name.ts +33 -0
  256. package/src/router/substitute-pattern-params.ts +56 -0
  257. package/src/router/telemetry-otel.ts +161 -199
  258. package/src/router/telemetry.ts +96 -19
  259. package/src/router/timeout.ts +0 -20
  260. package/src/router/tracing.ts +206 -0
  261. package/src/router/trie-matching.ts +180 -58
  262. package/src/router/types.ts +10 -63
  263. package/src/router/url-params.ts +44 -0
  264. package/src/router.ts +182 -54
  265. package/src/rsc/handler-context.ts +3 -2
  266. package/src/rsc/handler.ts +702 -460
  267. package/src/rsc/helpers.ts +168 -46
  268. package/src/rsc/index.ts +2 -25
  269. package/src/rsc/json-route-result.ts +38 -0
  270. package/src/rsc/loader-fetch.ts +127 -31
  271. package/src/rsc/manifest-init.ts +33 -42
  272. package/src/rsc/origin-guard.ts +39 -25
  273. package/src/rsc/progressive-enhancement.ts +98 -19
  274. package/src/rsc/redirect-guard.ts +99 -0
  275. package/src/rsc/response-cache-serve.ts +238 -0
  276. package/src/rsc/response-error.ts +79 -12
  277. package/src/rsc/response-route-handler.ts +99 -189
  278. package/src/rsc/rsc-rendering.ts +126 -106
  279. package/src/rsc/runtime-warnings.ts +23 -10
  280. package/src/rsc/server-action.ts +269 -114
  281. package/src/rsc/ssr-setup.ts +144 -0
  282. package/src/rsc/types.ts +34 -6
  283. package/src/runtime-env.ts +18 -0
  284. package/src/search-params.ts +49 -41
  285. package/src/segment-content-promise.ts +67 -0
  286. package/src/segment-loader-promise.ts +149 -0
  287. package/src/segment-system.tsx +281 -129
  288. package/src/serialize.ts +243 -0
  289. package/src/server/context.ts +317 -63
  290. package/src/server/cookie-parse.ts +32 -0
  291. package/src/server/cookie-store.ts +80 -5
  292. package/src/server/handle-store.ts +40 -38
  293. package/src/server/loader-registry.ts +26 -46
  294. package/src/server/request-context.ts +425 -177
  295. package/src/server.ts +6 -0
  296. package/src/ssr/index.tsx +25 -16
  297. package/src/static-handler.ts +27 -18
  298. package/src/testing/cache-status.ts +162 -0
  299. package/src/testing/collect-handle.ts +40 -0
  300. package/src/testing/dispatch.ts +701 -0
  301. package/src/testing/dom.entry.ts +22 -0
  302. package/src/testing/e2e/fixture.ts +188 -0
  303. package/src/testing/e2e/index.ts +128 -0
  304. package/src/testing/e2e/matchers.ts +35 -0
  305. package/src/testing/e2e/page-helpers.ts +272 -0
  306. package/src/testing/e2e/parity.ts +387 -0
  307. package/src/testing/e2e/server.ts +195 -0
  308. package/src/testing/flight-matchers.ts +97 -0
  309. package/src/testing/flight-normalize.ts +11 -0
  310. package/src/testing/flight-runtime.d.ts +57 -0
  311. package/src/testing/flight-tree.ts +682 -0
  312. package/src/testing/flight.entry.ts +52 -0
  313. package/src/testing/flight.ts +257 -0
  314. package/src/testing/generated-routes.ts +183 -0
  315. package/src/testing/index.ts +99 -0
  316. package/src/testing/internal/context.ts +371 -0
  317. package/src/testing/internal/flight-client-globals.ts +30 -0
  318. package/src/testing/internal/seed-vars.ts +54 -0
  319. package/src/testing/render-handler.ts +343 -0
  320. package/src/testing/render-route.tsx +581 -0
  321. package/src/testing/run-loader.ts +385 -0
  322. package/src/testing/run-middleware.ts +205 -0
  323. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  324. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  325. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  326. package/src/testing/vitest-stubs/version.ts +5 -0
  327. package/src/testing/vitest.ts +305 -0
  328. package/src/theme/ThemeProvider.tsx +20 -58
  329. package/src/theme/ThemeScript.tsx +7 -9
  330. package/src/theme/constants.ts +52 -13
  331. package/src/theme/index.ts +3 -19
  332. package/src/theme/theme-context.ts +1 -5
  333. package/src/theme/theme-script.ts +22 -21
  334. package/src/theme/use-theme.ts +0 -3
  335. package/src/types/boundaries.ts +0 -35
  336. package/src/types/cache-types.ts +17 -8
  337. package/src/types/error-types.ts +30 -90
  338. package/src/types/global-namespace.ts +54 -41
  339. package/src/types/handler-context.ts +236 -88
  340. package/src/types/index.ts +1 -10
  341. package/src/types/loader-types.ts +44 -15
  342. package/src/types/request-scope.ts +112 -0
  343. package/src/types/route-config.ts +10 -45
  344. package/src/types/route-entry.ts +19 -7
  345. package/src/types/segments.ts +37 -19
  346. package/src/urls/include-helper.ts +33 -70
  347. package/src/urls/index.ts +1 -11
  348. package/src/urls/path-helper-types.ts +58 -11
  349. package/src/urls/path-helper.ts +57 -111
  350. package/src/urls/pattern-types.ts +48 -19
  351. package/src/urls/response-types.ts +25 -22
  352. package/src/urls/type-extraction.ts +58 -139
  353. package/src/urls/urls-function.ts +1 -18
  354. package/src/use-loader.tsx +346 -89
  355. package/src/vite/debug.ts +185 -0
  356. package/src/vite/discovery/bundle-postprocess.ts +64 -91
  357. package/src/vite/discovery/discover-routers.ts +147 -88
  358. package/src/vite/discovery/discovery-errors.ts +194 -0
  359. package/src/vite/discovery/gate-state.ts +171 -0
  360. package/src/vite/discovery/prerender-collection.ts +247 -145
  361. package/src/vite/discovery/route-types-writer.ts +40 -84
  362. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  363. package/src/vite/discovery/state.ts +61 -13
  364. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  365. package/src/vite/index.ts +10 -3
  366. package/src/vite/inject-client-debug.ts +36 -0
  367. package/src/vite/plugin-types.ts +155 -65
  368. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  369. package/src/vite/plugins/client-ref-dedup.ts +120 -0
  370. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  371. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  372. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  373. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  374. package/src/vite/plugins/expose-action-id.ts +49 -98
  375. package/src/vite/plugins/expose-id-utils.ts +96 -51
  376. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  377. package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
  378. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  379. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  380. package/src/vite/plugins/expose-internal-ids.ts +553 -317
  381. package/src/vite/plugins/performance-tracks.ts +89 -0
  382. package/src/vite/plugins/refresh-cmd.ts +127 -0
  383. package/src/vite/plugins/use-cache-transform.ts +73 -83
  384. package/src/vite/plugins/version-injector.ts +21 -25
  385. package/src/vite/plugins/version-plugin.ts +46 -37
  386. package/src/vite/plugins/virtual-entries.ts +13 -18
  387. package/src/vite/rango.ts +241 -287
  388. package/src/vite/router-discovery.ts +956 -149
  389. package/src/vite/utils/ast-handler-extract.ts +26 -35
  390. package/src/vite/utils/banner.ts +4 -4
  391. package/src/vite/utils/bundle-analysis.ts +10 -15
  392. package/src/vite/utils/client-chunks.ts +184 -0
  393. package/src/vite/utils/directive-prologue.ts +40 -0
  394. package/src/vite/utils/forward-user-plugins.ts +171 -0
  395. package/src/vite/utils/manifest-utils.ts +4 -59
  396. package/src/vite/utils/package-resolution.ts +20 -52
  397. package/src/vite/utils/prerender-utils.ts +141 -34
  398. package/src/vite/utils/shared-utils.ts +92 -42
  399. package/CLAUDE.md +0 -5
  400. package/src/browser/action-response-classifier.ts +0 -99
  401. package/src/browser/react/use-client-cache.ts +0 -58
  402. package/src/browser/shallow.ts +0 -40
  403. package/src/handles/index.ts +0 -6
  404. package/src/network-error-thrower.tsx +0 -23
  405. package/src/route-definition/route-function.ts +0 -119
  406. package/src/router/middleware-cookies.ts +0 -55
@@ -117,6 +117,7 @@ export function setupLinkInterception(
117
117
  // Read navigation options from data attributes (set by Link component)
118
118
  const scrollAttr = link.getAttribute("data-scroll");
119
119
  const replaceAttr = link.getAttribute("data-replace");
120
+ const revalidateAttr = link.getAttribute("data-revalidate");
120
121
 
121
122
  const navigateOptions: NavigateOptions = {};
122
123
  if (scrollAttr === "false") {
@@ -125,6 +126,9 @@ export function setupLinkInterception(
125
126
  if (replaceAttr === "true") {
126
127
  navigateOptions.replace = true;
127
128
  }
129
+ if (revalidateAttr === "false") {
130
+ navigateOptions.revalidate = false;
131
+ }
128
132
 
129
133
  onNavigate(href, navigateOptions);
130
134
  };
@@ -4,12 +4,21 @@ import type {
4
4
  NavigateOptionsInternal,
5
5
  ResolvedSegment,
6
6
  } from "./types.js";
7
+ import { setAppVersion } from "./app-version.js";
8
+ import { isActionFenceActive } from "./action-fence.js";
9
+ import { getRangoState } from "./rango-state.js";
7
10
  import * as React from "react";
8
11
  import { startTransition } from "react";
9
12
  import {
10
13
  createNavigationTransaction,
11
14
  resolveNavigationState,
12
15
  } from "./navigation-transaction.js";
16
+ import { buildHistoryState, pushHistoryWithIdx } from "./history-state.js";
17
+ import {
18
+ handleNavigationStart,
19
+ handleNavigationEnd,
20
+ ensureHistoryKey,
21
+ } from "./scroll-restoration.js";
13
22
 
14
23
  // addTransitionType is only available in React experimental
15
24
  const addTransitionType: ((type: string) => void) | undefined =
@@ -18,12 +27,12 @@ const addTransitionType: ((type: string) => void) | undefined =
18
27
  import { setupLinkInterception } from "./link-interceptor.js";
19
28
  import { createPartialUpdater } from "./partial-update.js";
20
29
  import { generateHistoryKey } from "./navigation-store.js";
21
- import { handleNavigationEnd } from "./scroll-restoration.js";
22
30
  import type { EventController } from "./event-controller.js";
23
31
  import { isInterceptOnlyCache } from "./intercept-utils.js";
24
32
  import {
25
33
  toNetworkError,
26
34
  emitNetworkError,
35
+ emitNavigationError,
27
36
  isBackgroundSuppressible,
28
37
  } from "./network-error-handler.js";
29
38
  import { debugLog } from "./logging.js";
@@ -35,11 +44,6 @@ if (typeof Symbol.dispose === "undefined") {
35
44
  (Symbol as any).dispose = Symbol("Symbol.dispose");
36
45
  }
37
46
 
38
- /** Get IDs of non-loader segments (layouts, routes, parallels). */
39
- function getNonLoaderSegmentIds(segments: ResolvedSegment[]): string[] {
40
- return segments.filter((s) => s.type !== "loader").map((s) => s.id);
41
- }
42
-
43
47
  export { createNavigationTransaction };
44
48
 
45
49
  /**
@@ -47,7 +51,7 @@ export { createNavigationTransaction };
47
51
  */
48
52
  export interface NavigationBridgeConfigWithController extends NavigationBridgeConfig {
49
53
  eventController: EventController;
50
- /** RSC version from initial payload metadata */
54
+ /** RSC version from initial payload metadata. */
51
55
  version?: string;
52
56
  }
53
57
 
@@ -67,8 +71,8 @@ export interface NavigationBridgeConfigWithController extends NavigationBridgeCo
67
71
  export function createNavigationBridge(
68
72
  config: NavigationBridgeConfigWithController,
69
73
  ): NavigationBridge {
70
- const { store, client, eventController, onUpdate, renderSegments, version } =
71
- config;
74
+ const { store, client, eventController, onUpdate, renderSegments } = config;
75
+ let version = config.version;
72
76
 
73
77
  // Create shared partial updater
74
78
  const fetchPartialUpdate = createPartialUpdater({
@@ -76,7 +80,7 @@ export function createNavigationBridge(
76
80
  client,
77
81
  onUpdate,
78
82
  renderSegments,
79
- version,
83
+ getVersion: () => version,
80
84
  });
81
85
 
82
86
  return {
@@ -114,6 +118,81 @@ export function createNavigationBridge(
114
118
  return;
115
119
  }
116
120
 
121
+ // Shallow navigation: skip RSC fetch when revalidate is false
122
+ // and the pathname hasn't changed (search param / hash only change).
123
+ if (
124
+ options?.revalidate === false &&
125
+ targetUrl.pathname === new URL(window.location.href).pathname
126
+ ) {
127
+ // Preserve intercept context from the current history entry so that
128
+ // popstate uses the correct cache key (:intercept suffix) and restores
129
+ // the right full-page vs modal semantics.
130
+ const currentHistoryState = window.history.state;
131
+ const isIntercept = currentHistoryState?.intercept === true;
132
+ const interceptSourceUrl = isIntercept
133
+ ? currentHistoryState?.sourceUrl
134
+ : undefined;
135
+
136
+ const historyKey = generateHistoryKey(url, { intercept: isIntercept });
137
+
138
+ // Copy current segments to the new history key so back/forward restores instantly
139
+ const currentKey = store.getHistoryKey();
140
+ const currentCache = store.getCachedSegments(currentKey);
141
+ if (currentCache?.segments) {
142
+ const currentHandleData = eventController.getHandleState().data;
143
+ store.cacheSegmentsForHistory(
144
+ historyKey,
145
+ currentCache.segments,
146
+ currentHandleData,
147
+ );
148
+ }
149
+
150
+ // Save current scroll position before changing URL
151
+ handleNavigationStart();
152
+
153
+ // Snapshot old state before pushState/replaceState overwrites it
154
+ const oldState = window.history.state;
155
+
156
+ // Update browser URL (carry intercept context into history state)
157
+ const historyState = buildHistoryState(
158
+ resolvedState,
159
+ {
160
+ intercept: isIntercept || undefined,
161
+ sourceUrl: interceptSourceUrl,
162
+ },
163
+ {},
164
+ );
165
+ pushHistoryWithIdx(historyState, url, options?.replace ?? false);
166
+
167
+ // Ensure new history entry has a scroll restoration key
168
+ ensureHistoryKey();
169
+
170
+ // Notify useLocationState() hooks when state changes
171
+ const hasOldState =
172
+ oldState &&
173
+ typeof oldState === "object" &&
174
+ ("state" in oldState ||
175
+ Object.keys(oldState).some((k) => k.startsWith("__rsc_ls_")));
176
+ const hasNewState =
177
+ historyState &&
178
+ ("state" in historyState ||
179
+ Object.keys(historyState).some((k) => k.startsWith("__rsc_ls_")));
180
+ if (hasOldState || hasNewState) {
181
+ window.dispatchEvent(new Event("__rsc_locationstate"));
182
+ }
183
+
184
+ // Update store history key so future navigations reference the right cache
185
+ store.setHistoryKey(historyKey);
186
+ store.setCurrentUrl(url);
187
+
188
+ // Notify hooks — location updates, state stays idle
189
+ eventController.setLocation(targetUrl);
190
+
191
+ // Handle post-navigation scroll
192
+ handleNavigationEnd({ scroll: options.scroll });
193
+ return;
194
+ }
195
+
117
196
  // Only abort pending requests when navigating to a different route
118
197
  // Same-route navigation (e.g., /todos -> /todos) should not cancel in-flight actions
119
198
  const currentPath = new URL(window.location.href).pathname;
@@ -181,18 +260,24 @@ export function createNavigationBridge(
181
260
  // 2. routes that CAN be intercepted - we don't know if this navigation will intercept
182
261
  // 3. when leaving intercept - we need fresh non-intercept segments from server
183
262
  // 4. redirect-with-state - force re-render so hooks read fresh state
263
+ // 5. stale cache - server action invalidated it, need fresh data with loading state
184
264
  const hasUsableCache =
185
265
  cachedSegments &&
186
266
  cachedSegments.length > 0 &&
187
267
  !isInterceptOnlyCache(cachedSegments) &&
188
268
  !hasInterceptCache &&
189
269
  !isLeavingIntercept &&
270
+ !cached?.stale &&
190
271
  !options?._skipCache;
191
272
 
192
- using tx = createNavigationTransaction(store, eventController, url, {
273
+ // Forward navigations always await fetchPartialUpdate before rendering,
274
+ // so useNavigation should always report "loading". skipLoadingState is
275
+ // only used for popstate background revalidation (line ~526) where
276
+ // cached content renders instantly without a network wait.
277
+ const tx = createNavigationTransaction(store, eventController, url, {
193
278
  ...options,
194
279
  state: resolvedState,
195
- skipLoadingState: hasUsableCache,
280
+ skipLoadingState: false,
196
281
  });
197
282
 
198
283
  // REVALIDATE: Fetch fresh data from server
@@ -200,7 +285,7 @@ export function createNavigationBridge(
200
285
  await fetchPartialUpdate(
201
286
  url,
202
287
  hasUsableCache
203
- ? getNonLoaderSegmentIds(cachedSegments!)
288
+ ? cachedSegments!.map((s) => s.id)
204
289
  : options?._skipCache
205
290
  ? [] // Action redirect: send no segments so server renders everything fresh
206
291
  : undefined,
@@ -224,7 +309,7 @@ export function createNavigationBridge(
224
309
  );
225
310
  } catch (error) {
226
311
  // Server-side redirect with location state: the current transaction's
227
- // `using` cleanup resets loading state. Re-navigate to the redirect
312
+ // cleanup resets loading state. Re-navigate to the redirect
228
313
  // target carrying the server-set state into history.pushState.
229
314
  if (error instanceof ServerRedirect) {
230
315
  const redirectUrl = validateRedirectOrigin(
@@ -241,8 +326,15 @@ export function createNavigationBridge(
241
326
  } as NavigateOptionsInternal);
242
327
  }
243
328
 
244
- if (error instanceof DOMException && error.name === "AbortError") {
245
- debugLog("[Browser] Navigation aborted by newer navigation");
329
+ // Aborted, or superseded by a newer navigation. A superseded nav may
330
+ // reject with a non-AbortError (e.g. a Flight decode that fails after its
331
+ // signal was aborted), so check the signal too -- otherwise we would
332
+ // render a boundary that clobbers the newer navigation's content.
333
+ if (
334
+ (error instanceof DOMException && error.name === "AbortError") ||
335
+ tx.handle.signal.aborted
336
+ ) {
337
+ debugLog("[Browser] Navigation aborted or superseded");
246
338
  return;
247
339
  }
248
340
 
@@ -259,7 +351,15 @@ export function createNavigationBridge(
259
351
  return;
260
352
  }
261
353
 
262
- throw error;
354
+ // A response we could not process (undecodable Flight body, or an
355
+ // unanticipated failure building the response). Surface the route's
356
+ // error boundary rather than let the rejection abort the navigation
357
+ // silently. Prefetched responses funnel here too: a failed warm-prefetch
358
+ // payload rejects on consumption and propagates to this catch.
359
+ console.error("[Browser] Unprocessable navigation response:", error);
360
+ emitNavigationError(onUpdate, error, url);
361
+ } finally {
362
+ tx[Symbol.dispose]();
263
363
  }
264
364
  },
265
365
 
@@ -269,7 +369,7 @@ export function createNavigationBridge(
269
369
  async refresh(): Promise<void> {
270
370
  eventController.abortNavigation();
271
371
 
272
- using tx = createNavigationTransaction(
372
+ const tx = createNavigationTransaction(
273
373
  store,
274
374
  eventController,
275
375
  window.location.href,
@@ -286,6 +386,14 @@ export function createNavigationBridge(
286
386
  tx.with({ url: window.location.href, replace: true, scroll: false }),
287
387
  );
288
388
  } catch (error) {
389
+ // Aborted or superseded: bail without rendering a boundary (see navigate()).
390
+ if (
391
+ (error instanceof DOMException && error.name === "AbortError") ||
392
+ tx.handle.signal.aborted
393
+ ) {
394
+ return;
395
+ }
396
+
289
397
  const networkError = toNetworkError(error, {
290
398
  url: window.location.href,
291
399
  operation: "revalidation",
@@ -298,7 +406,14 @@ export function createNavigationBridge(
298
406
  emitNetworkError(onUpdate, networkError, window.location.href);
299
407
  return;
300
408
  }
301
- throw error;
409
+
410
+ // refresh() shares the fetchPartialUpdate chokepoint with navigate()/
411
+ // popstate, so an unprocessable response must surface the error boundary
412
+ // here too rather than become an uncaught rejection.
413
+ console.error("[Browser] Unprocessable refresh response:", error);
414
+ emitNavigationError(onUpdate, error, window.location.href);
415
+ } finally {
416
+ tx[Symbol.dispose]();
302
417
  }
303
418
  },
304
419
 
@@ -328,6 +443,15 @@ export function createNavigationBridge(
328
443
  eventController.abortAllActions();
329
444
  }
330
445
 
446
+ // Popstate that exits an intercept to a non-intercept destination. The
447
+ // fallback fetch path below needs `leave-intercept` mode so it filters
448
+ // the cached @modal segment from the request and forces a re-render —
449
+ // otherwise a cache-miss popstate whose server response has an empty
450
+ // diff hits the "no changes" branch in partial-update and the modal
451
+ // stays on screen.
452
+ const isLeavingIntercept =
453
+ !isIntercept && currentInterceptSource !== null;
454
+
331
455
  // Compute history key from URL (with intercept suffix if applicable)
332
456
  const historyKey = generateHistoryKey(url, { intercept: isIntercept });
333
457
 
@@ -351,11 +475,22 @@ export function createNavigationBridge(
351
475
  // Helper to check if streaming is in progress
352
476
  const isStreaming = () => eventController.getState().isStreaming;
353
477
 
478
+ // Surface any external rotation of the rango state cookie (a server
479
+ // Set-Cookie, a sibling tab, a cookie clear) BEFORE reading the stale bit.
480
+ // The divergence observer only runs inside getRangoState() — fetch-time —
481
+ // so a popstate-first interaction would otherwise serve a pre-mutation
482
+ // page as fresh and never fetch to trigger the observer. Reading here lets
483
+ // the observer mark the history cache stale so getCachedSegments sees it.
484
+ getRangoState();
485
+
354
486
  // Check if we can restore from history cache
355
487
  const cached = store.getCachedSegments(historyKey);
356
488
  const cachedSegments = cached?.segments;
357
489
  const cachedHandleData = cached?.handleData;
358
- const isStale = cached?.stale ?? false;
490
+ // While an action is in flight the fence persists no stale flag, so OR it
491
+ // in here: a popstate during the flight serves the cached entry AND
492
+ // revalidates (SWR) instead of serving it as fresh.
493
+ const isStale = (cached?.stale ?? false) || isActionFenceActive();
359
494
 
360
495
  if (cachedSegments && cachedSegments.length > 0) {
361
496
  // Update store to point to this history entry
@@ -364,6 +499,12 @@ export function createNavigationBridge(
364
499
  store.setCurrentUrl(url);
365
500
  store.setPath(new URL(url).pathname);
366
501
 
502
+ // Restore router identity from cache so subsequent navigations
503
+ // don't falsely detect an app switch.
504
+ if (cached?.routerId) {
505
+ store.setRouterId?.(cached.routerId);
506
+ }
507
+
367
508
  // Render from cache - force await to skip loading fallbacks
368
509
  try {
369
510
  const root = await renderSegments(cachedSegments, {
@@ -389,8 +530,16 @@ export function createNavigationBridge(
389
530
  cachedHandleData,
390
531
  params: cachedParams,
391
532
  },
533
+ scroll: { restore: true, isStreaming },
392
534
  };
393
- const hasTransition = cachedSegments.some((s) => s.transition);
535
+ // Intercept-driven popstate (entering OR leaving an intercept) only
536
+ // mutates the parallel slot; the main outlet shows the same content.
537
+ // Skip startViewTransition in those cases — same rationale as the
538
+ // intercept guard in partial-update.ts's hasTransition computation.
539
+ const hasTransition =
540
+ !isIntercept &&
541
+ !isLeavingIntercept &&
542
+ cachedSegments.some((s) => s.transition);
394
543
  if (hasTransition) {
395
544
  startTransition(() => {
396
545
  if (addTransitionType) {
@@ -402,14 +551,11 @@ export function createNavigationBridge(
402
551
  onUpdate(popstateUpdate);
403
552
  }
404
553
 
405
- // Restore scroll position for back/forward navigation
406
- handleNavigationEnd({ restore: true, isStreaming });
407
-
408
554
  // SWR: If stale, trigger background revalidation
409
555
  if (isStale) {
410
556
  debugLog("[Browser] Cache is stale, background revalidating...");
411
557
  // Background revalidation - don't await, just fire and forget
412
- const segmentIds = getNonLoaderSegmentIds(cachedSegments);
558
+ const segmentIds = cachedSegments.map((s) => s.id);
413
559
 
414
560
  const tx = createNavigationTransaction(
415
561
  store,
@@ -457,7 +603,7 @@ export function createNavigationBridge(
457
603
  }
458
604
 
459
605
  // Fetch if not cached
460
- using tx = createNavigationTransaction(store, eventController, url, {
606
+ const tx = createNavigationTransaction(store, eventController, url, {
461
607
  replace: true,
462
608
  });
463
609
 
@@ -474,13 +620,22 @@ export function createNavigationBridge(
474
620
  intercept: isIntercept,
475
621
  interceptSourceUrl,
476
622
  }),
477
- isIntercept ? { type: "navigate", interceptSourceUrl } : undefined,
623
+ isIntercept
624
+ ? { type: "navigate", interceptSourceUrl }
625
+ : isLeavingIntercept
626
+ ? { type: "leave-intercept" }
627
+ : undefined,
478
628
  );
479
629
  // Restore scroll position after fetch completes
480
630
  handleNavigationEnd({ restore: true, isStreaming });
481
631
  } catch (error) {
482
- if (error instanceof DOMException && error.name === "AbortError") {
483
- debugLog("[Browser] Popstate navigation aborted");
632
+ // Aborted or superseded by a newer navigation: bail without clobbering
633
+ // content with a boundary (see navigate()).
634
+ if (
635
+ (error instanceof DOMException && error.name === "AbortError") ||
636
+ tx.handle.signal.aborted
637
+ ) {
638
+ debugLog("[Browser] Popstate navigation aborted or superseded");
484
639
  return;
485
640
  }
486
641
 
@@ -497,7 +652,12 @@ export function createNavigationBridge(
497
652
  return;
498
653
  }
499
654
 
500
- throw error;
655
+ // Unprocessable response on a back/forward navigation: surface the
656
+ // error boundary instead of an uncaught rejection (see navigate()).
657
+ console.error("[Browser] Unprocessable popstate response:", error);
658
+ emitNavigationError(onUpdate, error, url);
659
+ } finally {
660
+ tx[Symbol.dispose]();
501
661
  }
502
662
  },
503
663
 
@@ -549,6 +709,16 @@ export function createNavigationBridge(
549
709
  window.removeEventListener("pageshow", handlePageShow);
550
710
  };
551
711
  },
712
+
713
+ getVersion(): string | undefined {
714
+ return version;
715
+ },
716
+
717
+ updateVersion(newVersion: string): void {
718
+ version = newVersion;
719
+ setAppVersion(newVersion);
720
+ store.clearHistoryCache();
721
+ },
552
722
  };
553
723
  }
554
724