@rangojs/router 0.15.1 → 0.17.0

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 (249) hide show
  1. package/README.md +254 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  4. package/dist/types/browser/partial-update.d.ts +11 -0
  5. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  6. package/dist/types/browser/rsc-router.d.ts +1 -1
  7. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  8. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  9. package/dist/types/cache/cache-scope.d.ts +34 -5
  10. package/dist/types/cache/handle-capture.d.ts +41 -3
  11. package/dist/types/cache/handle-snapshot.d.ts +53 -13
  12. package/dist/types/cache/segment-codec.d.ts +10 -3
  13. package/dist/types/cache/taint.d.ts +1 -1
  14. package/dist/types/cache/types.d.ts +15 -3
  15. package/dist/types/client.d.ts +4 -4
  16. package/dist/types/client.rsc.d.ts +4 -5
  17. package/dist/types/components/DefaultDocument.d.ts +5 -2
  18. package/dist/types/context-var.d.ts +6 -5
  19. package/dist/types/escape-script.d.ts +1 -1
  20. package/dist/types/handles/MetaTags.d.ts +5 -4
  21. package/dist/types/handles/Scripts.d.ts +8 -7
  22. package/dist/types/handles/meta.d.ts +2 -2
  23. package/dist/types/handles/script.d.ts +17 -16
  24. package/dist/types/href-client.d.ts +1 -1
  25. package/dist/types/html.d.ts +12 -0
  26. package/dist/types/index.d.ts +1 -1
  27. package/dist/types/index.rsc.d.ts +3 -2
  28. package/dist/types/loader-store.d.ts +52 -0
  29. package/dist/types/reverse.d.ts +36 -4
  30. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  31. package/dist/types/route-definition/helpers-types.d.ts +36 -10
  32. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  33. package/dist/types/route-types.d.ts +3 -1
  34. package/dist/types/router/debug-manifest.d.ts +3 -2
  35. package/dist/types/router/error-handling.d.ts +20 -0
  36. package/dist/types/router/handler-context.d.ts +6 -0
  37. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  38. package/dist/types/router/middleware-types.d.ts +3 -2
  39. package/dist/types/router/router-context.d.ts +3 -1
  40. package/dist/types/router/router-interfaces.d.ts +24 -2
  41. package/dist/types/router/router-options.d.ts +8 -5
  42. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  43. package/dist/types/router/segment-resolution/loader-cache.d.ts +37 -1
  44. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  45. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
  46. package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
  47. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  48. package/dist/types/router/segment-wrappers.d.ts +1 -1
  49. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  50. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  51. package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
  52. package/dist/types/rsc/shell-capture.d.ts +5 -4
  53. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  54. package/dist/types/rsc/types.d.ts +10 -0
  55. package/dist/types/server/context.d.ts +33 -3
  56. package/dist/types/server/handle-store.d.ts +26 -2
  57. package/dist/types/server/request-context.d.ts +24 -21
  58. package/dist/types/ssr/index.d.ts +15 -0
  59. package/dist/types/static-handler.d.ts +4 -1
  60. package/dist/types/testing/generated-routes.d.ts +2 -2
  61. package/dist/types/testing/internal/context.d.ts +1 -1
  62. package/dist/types/testing/render-route.d.ts +32 -6
  63. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  64. package/dist/types/testing/vitest.d.ts +35 -1
  65. package/dist/types/theme/ThemeScript.d.ts +3 -3
  66. package/dist/types/thenable-status.d.ts +9 -0
  67. package/dist/types/types/cache-types.d.ts +14 -10
  68. package/dist/types/types/handler-context.d.ts +14 -0
  69. package/dist/types/types/loader-types.d.ts +8 -6
  70. package/dist/types/urls/response-types.d.ts +3 -12
  71. package/dist/types/use-loader.d.ts +12 -3
  72. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  73. package/dist/types/vite/rango.d.ts +1 -1
  74. package/dist/vite/index.js +884 -768
  75. package/package.json +4 -4
  76. package/skills/api-client/SKILL.md +22 -10
  77. package/skills/breadcrumbs/SKILL.md +65 -22
  78. package/skills/bundle-analysis/SKILL.md +22 -22
  79. package/skills/cache-guide/SKILL.md +207 -97
  80. package/skills/caching/SKILL.md +295 -93
  81. package/skills/catalog.json +16 -16
  82. package/skills/client-urls/SKILL.md +53 -31
  83. package/skills/cloudflare/SKILL.md +72 -20
  84. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  85. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  86. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  87. package/skills/comparison/SKILL.md +5 -0
  88. package/skills/comparison/references/framework-comparison.md +39 -33
  89. package/skills/composability/SKILL.md +38 -29
  90. package/skills/css/SKILL.md +10 -13
  91. package/skills/debug-manifest/SKILL.md +74 -31
  92. package/skills/defer-hydration/SKILL.md +2 -1
  93. package/skills/deployment-caching/SKILL.md +6 -1
  94. package/skills/document-cache/SKILL.md +92 -28
  95. package/skills/fonts/SKILL.md +16 -8
  96. package/skills/handler-use/SKILL.md +53 -17
  97. package/skills/hooks/SKILL.md +28 -20
  98. package/skills/hooks/data.md +43 -1
  99. package/skills/hooks/handle-and-actions.md +31 -16
  100. package/skills/hooks/navigation.md +39 -10
  101. package/skills/hooks/outlets.md +13 -6
  102. package/skills/hooks/state.md +37 -15
  103. package/skills/hooks/urls.md +7 -5
  104. package/skills/host-router/SKILL.md +64 -12
  105. package/skills/i18n/SKILL.md +60 -34
  106. package/skills/intercept/SKILL.md +161 -26
  107. package/skills/layout/SKILL.md +106 -50
  108. package/skills/links/SKILL.md +51 -19
  109. package/skills/loader/SKILL.md +242 -106
  110. package/skills/middleware/SKILL.md +119 -23
  111. package/skills/migrate-nextjs/SKILL.md +100 -45
  112. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  113. package/skills/migrate-react-router/SKILL.md +49 -38
  114. package/skills/migrate-react-router/component-migration.md +25 -6
  115. package/skills/migrate-react-router/data-and-actions.md +18 -5
  116. package/skills/migrate-react-router/route-mapping.md +12 -6
  117. package/skills/mime-routes/SKILL.md +12 -7
  118. package/skills/observability/SKILL.md +28 -14
  119. package/skills/parallel/SKILL.md +75 -39
  120. package/skills/ppr/SKILL.md +345 -150
  121. package/skills/prerender/SKILL.md +189 -100
  122. package/skills/rango/SKILL.md +65 -34
  123. package/skills/react-compiler/SKILL.md +6 -5
  124. package/skills/response-routes/SKILL.md +133 -25
  125. package/skills/route/SKILL.md +165 -60
  126. package/skills/router-setup/SKILL.md +418 -240
  127. package/skills/scripts/SKILL.md +31 -20
  128. package/skills/server-actions/SKILL.md +220 -59
  129. package/skills/shell-manifest/SKILL.md +12 -7
  130. package/skills/streams-and-websockets/SKILL.md +32 -14
  131. package/skills/tailwind/SKILL.md +37 -33
  132. package/skills/testing/SKILL.md +68 -32
  133. package/skills/testing/cache-prerender.md +55 -29
  134. package/skills/testing/client-components.md +59 -6
  135. package/skills/testing/e2e-parity.md +12 -2
  136. package/skills/testing/flight.md +19 -11
  137. package/skills/testing/handles.md +8 -5
  138. package/skills/testing/loader.md +8 -5
  139. package/skills/testing/middleware.md +8 -3
  140. package/skills/testing/predicates.md +120 -0
  141. package/skills/testing/render-handler.md +70 -22
  142. package/skills/testing/response-routes.md +11 -4
  143. package/skills/testing/reverse-and-types.md +20 -14
  144. package/skills/testing/server-actions.md +12 -3
  145. package/skills/testing/server-tree.md +9 -0
  146. package/skills/testing/setup.md +9 -5
  147. package/skills/theme/SKILL.md +106 -29
  148. package/skills/typesafety/SKILL.md +20 -9
  149. package/skills/typesafety/env-and-bindings.md +126 -47
  150. package/skills/typesafety/generated-files-and-cli.md +26 -14
  151. package/skills/typesafety/params-and-search.md +44 -19
  152. package/skills/typesafety/route-types.md +50 -14
  153. package/skills/use-cache/SKILL.md +200 -82
  154. package/skills/vercel/SKILL.md +21 -11
  155. package/skills/view-transitions/SKILL.md +41 -20
  156. package/src/__augment-tests__/augmented.check.ts +26 -1
  157. package/src/browser/index.ts +2 -2
  158. package/src/browser/navigation-bridge.ts +3 -4
  159. package/src/browser/navigation-transaction.ts +22 -2
  160. package/src/browser/partial-update.ts +75 -80
  161. package/src/browser/react/ScrollRestoration.tsx +6 -6
  162. package/src/browser/rsc-router.tsx +1 -1
  163. package/src/browser/scroll-restoration.ts +53 -6
  164. package/src/cache/cache-exec-scope.ts +37 -4
  165. package/src/cache/cache-key-utils.ts +1 -6
  166. package/src/cache/cache-runtime.ts +230 -61
  167. package/src/cache/cache-scope.ts +76 -12
  168. package/src/cache/document-cache.ts +15 -0
  169. package/src/cache/handle-capture.ts +90 -10
  170. package/src/cache/handle-snapshot.ts +132 -28
  171. package/src/cache/segment-codec.ts +26 -7
  172. package/src/cache/tag-invalidation.ts +4 -3
  173. package/src/cache/taint.ts +1 -1
  174. package/src/cache/types.ts +16 -3
  175. package/src/client.rsc.tsx +10 -9
  176. package/src/client.tsx +5 -4
  177. package/src/components/DefaultDocument.tsx +7 -2
  178. package/src/context-var.ts +6 -5
  179. package/src/escape-script.ts +1 -1
  180. package/src/handles/MetaTags.tsx +13 -11
  181. package/src/handles/Scripts.tsx +11 -10
  182. package/src/handles/meta.ts +2 -2
  183. package/src/handles/script.ts +17 -16
  184. package/src/href-client.ts +1 -1
  185. package/src/html.ts +12 -0
  186. package/src/index.rsc.ts +5 -0
  187. package/src/index.ts +1 -0
  188. package/src/loader-store.ts +106 -0
  189. package/src/prerender/build-shell-capture.ts +8 -11
  190. package/src/reverse.ts +44 -4
  191. package/src/route-definition/dsl-helpers.ts +134 -42
  192. package/src/route-definition/helpers-types.ts +36 -10
  193. package/src/route-definition/resolve-handler-use.ts +121 -6
  194. package/src/route-types.ts +2 -3
  195. package/src/router/debug-manifest.ts +10 -2
  196. package/src/router/error-handling.ts +51 -15
  197. package/src/router/handler-context.ts +34 -25
  198. package/src/router/intercept-resolution.ts +125 -39
  199. package/src/router/loader-resolution.ts +56 -11
  200. package/src/router/match-api.ts +12 -17
  201. package/src/router/match-middleware/background-revalidation.ts +161 -96
  202. package/src/router/match-middleware/cache-lookup.ts +93 -10
  203. package/src/router/match-middleware/cache-store.ts +17 -90
  204. package/src/router/middleware-types.ts +3 -5
  205. package/src/router/middleware.ts +52 -9
  206. package/src/router/prerender-match.ts +53 -19
  207. package/src/router/route-snapshot.ts +11 -1
  208. package/src/router/router-context.ts +2 -0
  209. package/src/router/router-interfaces.ts +25 -2
  210. package/src/router/router-options.ts +8 -5
  211. package/src/router/segment-resolution/fresh.ts +44 -12
  212. package/src/router/segment-resolution/loader-cache.ts +205 -45
  213. package/src/router/segment-resolution/loader-mask.ts +12 -22
  214. package/src/router/segment-resolution/loader-snapshot.ts +6 -6
  215. package/src/router/segment-resolution/mask-nested.ts +7 -7
  216. package/src/router/segment-resolution/revalidation.ts +34 -6
  217. package/src/router/segment-wrappers.ts +4 -0
  218. package/src/rsc/render-pipeline.ts +3 -0
  219. package/src/rsc/response-route-handler.ts +11 -3
  220. package/src/rsc/rsc-rendering.ts +8 -45
  221. package/src/rsc/shell-capture-constants.ts +13 -0
  222. package/src/rsc/shell-capture.ts +111 -90
  223. package/src/rsc/ssr-setup.ts +14 -8
  224. package/src/rsc/types.ts +11 -0
  225. package/src/server/context.ts +88 -11
  226. package/src/server/cookie-store.ts +5 -4
  227. package/src/server/handle-store.ts +232 -7
  228. package/src/server/request-context.ts +35 -30
  229. package/src/ssr/index.tsx +40 -7
  230. package/src/ssr-suspension-warning.ts +2 -13
  231. package/src/static-handler.ts +4 -1
  232. package/src/testing/flight-runtime.d.ts +21 -0
  233. package/src/testing/generated-routes.ts +2 -2
  234. package/src/testing/internal/context.ts +1 -1
  235. package/src/testing/internal/flight-client-globals.ts +7 -5
  236. package/src/testing/render-route.tsx +85 -24
  237. package/src/testing/run-loader.ts +5 -1
  238. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  239. package/src/testing/vitest.ts +62 -1
  240. package/src/theme/ThemeProvider.tsx +14 -12
  241. package/src/theme/ThemeScript.tsx +3 -3
  242. package/src/thenable-status.ts +16 -0
  243. package/src/types/cache-types.ts +14 -10
  244. package/src/types/handler-context.ts +14 -0
  245. package/src/types/loader-types.ts +8 -6
  246. package/src/urls/response-types.ts +3 -23
  247. package/src/use-loader.tsx +24 -4
  248. package/src/vite/plugins/use-cache-transform.ts +118 -74
  249. package/src/vite/rango.ts +7 -3
@@ -8,7 +8,7 @@ argument-hint: [layout|route|parallel|intercept]
8
8
 
9
9
  `transition()` opts a route (or group of routes) into transition-driven navigation. It does two things, and you choose how far to go:
10
10
 
11
- 1. **`startTransition` (the foundation).** The navigation commit is driven through React's `startTransition`. That holds the previous content across a same-route navigation (stale-while-revalidate — no loading-skeleton flash) and is the **precondition** for any view-transition animation. Works on **all** React versions.
11
+ 1. **`startTransition` (the foundation).** The navigation commit is driven through React's `startTransition`. That holds the previous content across a same-route navigation (stale-while-revalidate — no loading-skeleton flash) and is the **precondition** for any view-transition animation. Works on **all** React versions. While the hold lasts, `useLoader` readers of the loaders being re-run report `isLoading: true` — keyed by loader family (`$$id`), not by segment; see the hooks skill, `data.md`.
12
12
  2. **`<ViewTransition>` (the animation, layered on top).** On React 19.3+ (or an experimental build), rango also wraps the segment content in React's `<ViewTransition>` so the swap cross-fades/morphs. This is the only part that needs 19.3+; pass `viewTransition: false` to keep #1 without it (and place your own `<ViewTransition>` where you want it).
13
13
 
14
14
  > The `<ViewTransition>` layer requires a React that exports `<ViewTransition>` / `addTransitionType`: stable 19.3+ or an experimental build. Rango feature-detects it; on React 19.2 that layer is a no-op — but the `startTransition` driving (content hold) still applies.
@@ -52,7 +52,7 @@ A simple cross-fade between pages that share a layout:
52
52
  import { urls } from "@rangojs/router";
53
53
  import { Outlet } from "@rangojs/router/client";
54
54
 
55
- function ShopShell({ children }: { children: React.ReactNode }) {
55
+ function ShopShell() {
56
56
  return (
57
57
  <div className="shop">
58
58
  <NavBar />
@@ -74,23 +74,34 @@ export const urlpatterns = urls(({ layout, path, transition }) => [
74
74
  ]);
75
75
  ```
76
76
 
77
+ The config values are React `<ViewTransition>` classes: React applies them as
78
+ `view-transition-class`, so the CSS targets the view-transition
79
+ pseudo-elements by class:
80
+
77
81
  ```css
78
- ::view-transition-old(root) {
79
- animation: fade-out 200ms ease both;
82
+ ::view-transition-old(.page-fade) {
83
+ animation: 200ms ease both fade-out;
84
+ }
85
+ ::view-transition-new(.page-fade) {
86
+ animation: 200ms ease both fade-in;
80
87
  }
81
- ::view-transition-new(root) {
82
- animation: fade-in 200ms ease both;
88
+ @keyframes fade-out {
89
+ to {
90
+ opacity: 0;
91
+ }
83
92
  }
84
- .page-fade {
85
- /* class hooks per phase */
93
+ @keyframes fade-in {
94
+ from {
95
+ opacity: 0;
96
+ }
86
97
  }
87
98
  ```
88
99
 
89
- Navigating between `/`, `/about`, and `/contact` morphs the `<Outlet />` content with the `page-fade` class. The shell (NavBar, Footer) does not morph because the wrap sits inside the shell, not around it.
100
+ Navigating between `/`, `/about`, and `/contact` cross-fades the `<Outlet />` content with the `page-fade` class. The shell (NavBar, Footer) does not animate because the wrap sits inside the shell, not around it.
90
101
 
91
102
  ## Direction-aware transitions
92
103
 
93
- `ViewTransitionClass` accepts an object form keyed by transition type. Rango tags forward navigations as `"navigation"` and back/forward popstate as `"navigation-back"`:
104
+ `ViewTransitionClass` accepts an object form keyed by transition type. Rango tags forward navigations as `"navigation"` and back/forward navigations restored from the client cache as `"navigation-back"` (a back/forward that has to refetch from the server is tagged `"navigation"`):
94
105
 
95
106
  ```tsx
96
107
  layout(<ShopShell />, () => [
@@ -106,12 +117,19 @@ layout(<ShopShell />, () => [
106
117
  ```
107
118
 
108
119
  ```css
109
- .slide-left {
110
- animation-name: slide-from-right;
120
+ ::view-transition-old(.slide-left) {
121
+ animation: 250ms ease both slide-out-to-left;
122
+ }
123
+ ::view-transition-new(.slide-left) {
124
+ animation: 250ms ease both slide-in-from-right;
111
125
  }
112
- .slide-right {
113
- animation-name: slide-from-left;
126
+ ::view-transition-old(.slide-right) {
127
+ animation: 250ms ease both slide-out-to-right;
114
128
  }
129
+ ::view-transition-new(.slide-right) {
130
+ animation: 250ms ease both slide-in-from-left;
131
+ }
132
+ /* @keyframes slide-out-to-left, slide-in-from-right, ... defined as usual */
115
133
  ```
116
134
 
117
135
  > Note: `"action"` is only tagged on partial-update action/refetch paths today; ordinary `server-action-bridge` commits (`useAction` / `useActionState` revalidations) are not currently tagged. Don't rely on an `action`-keyed class to fire on every form action.
@@ -147,8 +165,8 @@ function GalleryShell() {
147
165
  <main>
148
166
  <Outlet /> {/* page transition lands here */}
149
167
  </main>
150
- <ParallelOutlet name="@modal" />{" "}
151
- {/* modal mounts here — sibling of the VT */}
168
+ {/* modal mounts here, as a sibling of the VT */}
169
+ <ParallelOutlet name="@modal" />
152
170
  </>
153
171
  );
154
172
  }
@@ -222,7 +240,7 @@ interface TransitionConfig {
222
240
 
223
241
  ## Conditional transitions (`when`)
224
242
 
225
- `transition({ when })` gates the hold per request. The predicate runs server-side and outside any cache scope; return `false` to drop this segment's transition for the request (the navigation streams its `loading()` fallback instead of holding).
243
+ `transition({ when })` gates the hold per request. The predicate runs server-side and outside any cache scope; return `false` to drop this segment's transition for the request. The hold is navigation-wide: the navigation streams its `loading()` fallback instead of holding only when no other matched segment still has a transition (the common case is a single `transition()` on the route).
226
244
 
227
245
  Timing follows the route's rendering contract:
228
246
 
@@ -280,7 +298,10 @@ urls(({ path, transition }) => [
280
298
  ]),
281
299
  ]);
282
300
 
283
- // ProductPage renders the boundary itself, exactly where it's wanted:
301
+ // ProductPage renders the boundary itself, exactly where it's wanted
302
+ // (ViewTransition is exported by React 19.3+ and experimental builds):
303
+ import { ViewTransition } from "react";
304
+
284
305
  function ProductPage() {
285
306
  return (
286
307
  <ViewTransition name="hero">
@@ -311,7 +332,7 @@ const router = createRouter<AppEnv>({ viewTransition: false });
311
332
  | `transition({ viewTransition: false })` | `"auto"` | no wrap (per-route wins) | **ST only** |
312
333
  | `transition({ viewTransition: false })` | `false` | no wrap | ST only |
313
334
 
314
- On stable React the "VT" column is always a no-op (there is no `<ViewTransition>`), so every row collapses to its `startTransition`-only behavior there.
335
+ On React 19.2 (no `<ViewTransition>` export) the "VT" column is a no-op, so every row collapses to its `startTransition`-only behavior there.
315
336
 
316
337
  | Config | Router boundary | startTransition driving (no skeleton flash) | Your own `<ViewTransition name>` |
317
338
  | ---------------------------------------------------- | ---------------- | ------------------------------------------- | ---------------------------------- |
@@ -320,7 +341,7 @@ On stable React the "VT" column is always a no-op (there is no `<ViewTransition>
320
341
  | `transition({ viewTransition: false })` | none | yes | fires alone |
321
342
  | global `viewTransition: false`, route `transition()` | none | yes | fires alone |
322
343
 
323
- > On **stable** React there is no `<ViewTransition>` at all, so `viewTransition: false` is visually a no-op there — but the startTransition driving and content-hold still apply, identical to `transition({})`.
344
+ > On React 19.2 there is no `<ViewTransition>` at all, so `viewTransition: false` is visually a no-op there — but the startTransition driving and content-hold still apply, identical to `transition({})`.
324
345
 
325
346
  ## Testing a transition() route
326
347
 
@@ -6,7 +6,13 @@
6
6
  * turns these into tsc errors. Run via tsconfig.augment-check.json.
7
7
  */
8
8
  import "./augment.js";
9
- import type { Handler, RouteParams, RouteSearchParams } from "../index.js";
9
+ import type {
10
+ Handler,
11
+ MiddlewareContext,
12
+ ResponseHandlerContext,
13
+ RouteParams,
14
+ RouteSearchParams,
15
+ } from "../index.js";
10
16
  import type { DefaultRouteName } from "../types/global-namespace.js";
11
17
  import { href } from "../href-client.js";
12
18
  import type { Money, TestBindings } from "./augment.js";
@@ -39,6 +45,25 @@ const varsHandler: Handler<"home"> = (ctx) => {
39
45
  };
40
46
  void varsHandler;
41
47
 
48
+ // Response-route and middleware ctx.reverse resolve global names only (no
49
+ // include() scope): generated names and params are validated, `.name` is not.
50
+ function responseReverse(ctx: ResponseHandlerContext): void {
51
+ ctx.reverse("home");
52
+ ctx.reverse("blog.post", { slug: "hello" });
53
+ // @ts-expect-error - dot-local name; no include() scope to resolve it
54
+ ctx.reverse(".home");
55
+ // @ts-expect-error - unknown global name is rejected
56
+ ctx.reverse("nope");
57
+ }
58
+ void responseReverse;
59
+
60
+ function middlewareReverse(ctx: MiddlewareContext): void {
61
+ ctx.reverse("blog.post", { slug: "hello" });
62
+ // @ts-expect-error - dot-local name; no include() scope to resolve it
63
+ ctx.reverse(".home");
64
+ }
65
+ void middlewareReverse;
66
+
42
67
  // routeName narrows to the generated route names.
43
68
  type _routeName = Expect<
44
69
  Equal<DefaultRouteName, "home" | "blog.post" | "search">
@@ -3,10 +3,10 @@
3
3
  // ============================================================================
4
4
  //
5
5
  // Usage:
6
- // import { initBrowserApp, Rango } from "rsc-router/browser";
6
+ // import { initBrowserApp, Rango } from "@rangojs/router/browser";
7
7
  //
8
8
  // For React components (Link, useNavigation, etc.):
9
- // import { Link, useNavigation, useAction, href } from "rsc-router/client";
9
+ // import { Link, useNavigation, useAction, href } from "@rangojs/router/client";
10
10
  //
11
11
  // ============================================================================
12
12
 
@@ -18,6 +18,7 @@ import { buildHistoryState, pushHistoryWithIdx } from "./history-state.js";
18
18
  import {
19
19
  handleNavigationStart,
20
20
  handleNavigationEnd,
21
+ handleTraversalStart,
21
22
  ensureHistoryKey,
22
23
  } from "./scroll-restoration.js";
23
24
 
@@ -496,6 +497,7 @@ export function createNavigationBridge(
496
497
  async handlePopstate(): Promise<void> {
497
498
  // Abort any pending navigation to prevent race conditions
498
499
  eventController.abortNavigation();
500
+ handleTraversalStart();
499
501
 
500
502
  const url = window.location.href;
501
503
 
@@ -698,7 +700,7 @@ export function createNavigationBridge(
698
700
  tx.handle.signal,
699
701
  tx.with({
700
702
  url,
701
- replace: true,
703
+ traversal: true,
702
704
  scroll: false,
703
705
  intercept: isIntercept,
704
706
  interceptSourceUrl,
@@ -771,9 +773,6 @@ export function createNavigationBridge(
771
773
  "[Browser] Page restored from bfcache, resetting navigation state",
772
774
  );
773
775
  eventController.abortNavigation();
774
- // pagehide flips scrollRestoration to "auto" for bfcache compat;
775
- // restore "manual" so the router controls scroll on SPA navigations.
776
- window.history.scrollRestoration = "manual";
777
776
  }
778
777
  };
779
778
 
@@ -11,7 +11,11 @@ import {
11
11
  } from "./scroll-restoration.js";
12
12
  import type { EventController, NavigationHandle } from "./event-controller.js";
13
13
  import { debugLog } from "./logging.js";
14
- import { buildHistoryState, pushHistoryWithIdx } from "./history-state.js";
14
+ import {
15
+ buildHistoryState,
16
+ mergeLocationState,
17
+ pushHistoryWithIdx,
18
+ } from "./history-state.js";
15
19
 
16
20
  export { resolveNavigationState } from "./history-state.js";
17
21
 
@@ -49,6 +53,12 @@ interface CommitOptions {
49
53
  cacheOnly?: boolean;
50
54
  /** Server-set location state to merge into history.pushState */
51
55
  serverState?: Record<string, unknown>;
56
+ /**
57
+ * Back/forward: history is already at this entry. Its state (scroll key,
58
+ * location state, idx) stays as the browser restored it; serverState is
59
+ * merged in. The page left was saved by handleTraversalStart.
60
+ */
61
+ traversal?: boolean;
52
62
  }
53
63
 
54
64
  /**
@@ -131,6 +141,7 @@ export function createNavigationTransaction(
131
141
  interceptSourceUrl,
132
142
  cacheOnly,
133
143
  serverState,
144
+ traversal,
134
145
  } = opts;
135
146
 
136
147
  const parsedUrl = new URL(url, window.location.origin);
@@ -145,7 +156,7 @@ export function createNavigationTransaction(
145
156
  return { scroll: false };
146
157
  }
147
158
 
148
- handleNavigationStart();
159
+ if (!traversal) handleNavigationStart();
149
160
 
150
161
  store.setSegmentIds(segmentIds);
151
162
  store.setCurrentUrl(url);
@@ -162,6 +173,15 @@ export function createNavigationTransaction(
162
173
  return { scroll: false };
163
174
  }
164
175
 
176
+ if (traversal) {
177
+ if (serverState && Object.keys(serverState).length > 0) {
178
+ mergeLocationState(serverState);
179
+ }
180
+ handle.complete(parsedUrl);
181
+ debugLog("[Browser] Traversal committed, historyKey:", historyKey);
182
+ return { scroll };
183
+ }
184
+
165
185
  const historyState = buildHistoryState(
166
186
  opts.state,
167
187
  { intercept, sourceUrl: interceptSourceUrl },
@@ -31,6 +31,7 @@ import {
31
31
  } from "./validate-redirect-origin.js";
32
32
  import type { NavigationUpdate } from "./types.js";
33
33
  import { OPTIMISTIC_COMMIT_TRANSITION_TYPE } from "./optimistic-commit.js";
34
+ import { loaderStore } from "../loader-store.js";
34
35
  import {
35
36
  collectClientRevalidationDecisions,
36
37
  setActiveInterceptTargets,
@@ -42,7 +43,9 @@ function toScrollPayload(
42
43
  return { enabled: scroll !== false ? scroll : false };
43
44
  }
44
45
 
45
- function shouldStartViewTransition(segments: ResolvedSegment[]): boolean {
46
+ export function shouldStartViewTransition(
47
+ segments: ResolvedSegment[],
48
+ ): boolean {
46
49
  let hasIntercept = false;
47
50
  let hasTransition = false;
48
51
  for (const s of segments) {
@@ -52,6 +55,29 @@ function shouldStartViewTransition(segments: ResolvedSegment[]): boolean {
52
55
  return !hasIntercept && hasTransition;
53
56
  }
54
57
 
58
+ /**
59
+ * Transition commit. Every held commit announces the loader streams the
60
+ * committed tree is still receiving (loader-store.ts announcePendingStreams)
61
+ * so held useLoader readers pin isLoading:true until this transition lands;
62
+ * settled/cached segments make it a no-op. Urgent commits call onUpdate
63
+ * directly: nothing is held there, a reader of a pending stream suspends to
64
+ * its fallback. Also used by renderRoute's navigate() (testing/render-route.tsx).
65
+ */
66
+ export function commitInTransition(
67
+ onUpdate: UpdateSubscriber,
68
+ segments: readonly ResolvedSegment[],
69
+ update: NavigationUpdate,
70
+ transitionTypes: readonly string[],
71
+ ): void {
72
+ startTransition(() => {
73
+ loaderStore.announcePendingStreams(segments);
74
+ if (addTransitionType) {
75
+ for (const type of transitionTypes) addTransitionType(type);
76
+ }
77
+ onUpdate(update);
78
+ });
79
+ }
80
+
55
81
  /**
56
82
  * Configuration for creating a partial updater
57
83
  */
@@ -351,12 +377,9 @@ export function createPartialUpdater(
351
377
  };
352
378
 
353
379
  if (shouldStartViewTransition(existingSegments)) {
354
- startTransition(() => {
355
- if (addTransitionType) {
356
- addTransitionType("navigation");
357
- }
358
- onUpdate(cachedUpdate);
359
- });
380
+ commitInTransition(onUpdate, existingSegments, cachedUpdate, [
381
+ "navigation",
382
+ ]);
360
383
  } else {
361
384
  onUpdate(cachedUpdate);
362
385
  }
@@ -589,31 +612,27 @@ export function createPartialUpdater(
589
612
 
590
613
  const scrollPayload = toScrollPayload(navScroll);
591
614
 
615
+ const update: NavigationUpdate = {
616
+ root: newTree,
617
+ metadata: payload.metadata!,
618
+ scroll: scrollPayload,
619
+ };
592
620
  if (mode.type === "action" || mode.type === "stale-revalidation") {
593
- startTransition(() => {
594
- if (hasTransition && addTransitionType) {
595
- addTransitionType("action");
596
- }
597
- onUpdate({
598
- root: newTree,
599
- metadata: payload.metadata!,
600
- scroll: scrollPayload,
601
- });
602
- });
621
+ commitInTransition(
622
+ onUpdate,
623
+ reconciled.mainSegments,
624
+ update,
625
+ hasTransition ? ["action"] : [],
626
+ );
603
627
  } else if (hasTransition) {
604
- startTransition(() => {
605
- if (addTransitionType) {
606
- addTransitionType("navigation");
607
- if (optimisticPresented) {
608
- addTransitionType(OPTIMISTIC_COMMIT_TRANSITION_TYPE);
609
- }
610
- }
611
- onUpdate({
612
- root: newTree,
613
- metadata: payload.metadata!,
614
- scroll: scrollPayload,
615
- });
616
- });
628
+ commitInTransition(
629
+ onUpdate,
630
+ reconciled.mainSegments,
631
+ update,
632
+ optimisticPresented
633
+ ? ["navigation", OPTIMISTIC_COMMIT_TRANSITION_TYPE]
634
+ : ["navigation"],
635
+ );
617
636
  } else if (fullyPrefetched || isSameStructureNav || optimisticPresented) {
618
637
  // Content-hold commit, two triggers. Fully-prefetched nav: the payload
619
638
  // is fully resolved (forceAwait above), so the transition commits
@@ -637,26 +656,18 @@ export function createPartialUpdater(
637
656
  // optimistic clientUrls() presentation commits here too: the group
638
657
  // segment reconciles in place (clientGroup key), so a read that still
639
658
  // suspends must hold the presented content, not flash a fallback.
640
- startTransition(() => {
641
- if (optimisticPresented && addTransitionType) {
642
- addTransitionType(OPTIMISTIC_COMMIT_TRANSITION_TYPE);
643
- }
644
- onUpdate({
645
- root: newTree,
646
- metadata: payload.metadata!,
647
- scroll: scrollPayload,
648
- });
649
- });
659
+ commitInTransition(
660
+ onUpdate,
661
+ reconciled.mainSegments,
662
+ update,
663
+ optimisticPresented ? [OPTIMISTIC_COMMIT_TRANSITION_TYPE] : [],
664
+ );
650
665
  } else {
651
666
  // Cold/partially-prefetched nav that mounts NEW segments: normal
652
667
  // commit so fallbacks stream like a first load and the click has
653
668
  // visible feedback. Explicit transition() routes keep the
654
669
  // content-hold via the hasTransition branch above (the opt-in).
655
- onUpdate({
656
- root: newTree,
657
- metadata: payload.metadata!,
658
- scroll: scrollPayload,
659
- });
670
+ onUpdate(update);
660
671
  }
661
672
 
662
673
  debugLog("[Browser] Navigation complete");
@@ -689,6 +700,11 @@ export function createPartialUpdater(
689
700
 
690
701
  const fullHasTransition = shouldStartViewTransition(segments);
691
702
  const fullScrollPayload = toScrollPayload(fullScroll);
703
+ const fullUpdate: NavigationUpdate = {
704
+ root: newTree,
705
+ metadata: payload.metadata!,
706
+ scroll: fullScrollPayload,
707
+ };
692
708
 
693
709
  if (mode.type === "stale-revalidation") {
694
710
  await rawStreamComplete;
@@ -704,44 +720,23 @@ export function createPartialUpdater(
704
720
  );
705
721
  return;
706
722
  }
707
- startTransition(() => {
708
- if (fullHasTransition && addTransitionType) {
709
- addTransitionType("action");
710
- }
711
- onUpdate({
712
- root: newTree,
713
- metadata: payload.metadata!,
714
- scroll: fullScrollPayload,
715
- });
716
- });
723
+ commitInTransition(
724
+ onUpdate,
725
+ segments,
726
+ fullUpdate,
727
+ fullHasTransition ? ["action"] : [],
728
+ );
717
729
  } else if (mode.type === "action") {
718
- startTransition(() => {
719
- if (fullHasTransition && addTransitionType) {
720
- addTransitionType("action");
721
- }
722
- onUpdate({
723
- root: newTree,
724
- metadata: payload.metadata!,
725
- scroll: fullScrollPayload,
726
- });
727
- });
730
+ commitInTransition(
731
+ onUpdate,
732
+ segments,
733
+ fullUpdate,
734
+ fullHasTransition ? ["action"] : [],
735
+ );
728
736
  } else if (fullHasTransition) {
729
- startTransition(() => {
730
- if (addTransitionType) {
731
- addTransitionType("navigation");
732
- }
733
- onUpdate({
734
- root: newTree,
735
- metadata: payload.metadata!,
736
- scroll: fullScrollPayload,
737
- });
738
- });
737
+ commitInTransition(onUpdate, segments, fullUpdate, ["navigation"]);
739
738
  } else {
740
- onUpdate({
741
- root: newTree,
742
- metadata: payload.metadata!,
743
- scroll: fullScrollPayload,
744
- });
739
+ onUpdate(fullUpdate);
745
740
  }
746
741
 
747
742
  return;
@@ -4,7 +4,7 @@ import { useEffect } from "react";
4
4
  import { initScrollRestoration } from "../scroll-restoration.js";
5
5
 
6
6
  /**
7
- * Props for ScrollRestoration component
7
+ * Props for `<Html.ScrollRestoration />`.
8
8
  */
9
9
  export interface ScrollRestorationProps {
10
10
  /**
@@ -25,10 +25,10 @@ export interface ScrollRestorationProps {
25
25
  * const byPathname = (location) => location.pathname;
26
26
  *
27
27
  * // Restore based on pathname (same URL = same scroll)
28
- * <ScrollRestoration getKey={byPathname} />
28
+ * <Html.ScrollRestoration getKey={byPathname} />
29
29
  *
30
30
  * // Restore based on unique history entry (default)
31
- * // <ScrollRestoration /> — omit getKey to use location.key
31
+ * // <Html.ScrollRestoration /> — omit getKey to use location.key
32
32
  * ```
33
33
  */
34
34
  getKey?: (location: {
@@ -40,7 +40,7 @@ export interface ScrollRestorationProps {
40
40
  }
41
41
 
42
42
  /**
43
- * ScrollRestoration component
43
+ * Public as `Html.ScrollRestoration` (`@rangojs/router/client`).
44
44
  *
45
45
  * Enables scroll position restoration across navigations:
46
46
  * - Saves scroll positions to sessionStorage
@@ -57,7 +57,7 @@ export interface ScrollRestorationProps {
57
57
  * return (
58
58
  * <html>
59
59
  * <body>
60
- * <ScrollRestoration />
60
+ * <Html.ScrollRestoration />
61
61
  * {children}
62
62
  * </body>
63
63
  * </html>
@@ -78,7 +78,7 @@ export function ScrollRestoration({ getKey }: ScrollRestorationProps) {
78
78
  /**
79
79
  * Hook to initialize scroll restoration
80
80
  *
81
- * Alternative to the ScrollRestoration component for more control.
81
+ * Alternative to `<Html.ScrollRestoration />` for more control.
82
82
  *
83
83
  * @example
84
84
  * ```tsx
@@ -655,7 +655,7 @@ export interface RangoProps {}
655
655
  *
656
656
  * @example
657
657
  * ```tsx
658
- * import { initBrowserApp, Rango } from "rsc-router/browser";
658
+ * import { initBrowserApp, Rango } from "@rangojs/router/browser";
659
659
  * import { rscStream } from "rsc-html-stream/client";
660
660
  * import * as rscBrowser from "@vitejs/plugin-rsc/browser";
661
661
  *