@rangojs/router 0.24.4 → 0.25.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 (267) hide show
  1. package/README.md +1 -0
  2. package/dist/bin/rango.js +363 -39
  3. package/dist/testing/vitest.js +37 -4
  4. package/dist/types/browser/event-controller.d.ts +24 -5
  5. package/dist/types/browser/intercept-utils.d.ts +5 -2
  6. package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
  7. package/dist/types/browser/navigation-transaction.d.ts +5 -5
  8. package/dist/types/browser/partial-update.d.ts +21 -13
  9. package/dist/types/browser/prefetch/cache.d.ts +8 -0
  10. package/dist/types/browser/rango-marker.d.ts +7 -0
  11. package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
  12. package/dist/types/browser/react/use-handle.d.ts +20 -0
  13. package/dist/types/browser/react/use-href.d.ts +6 -0
  14. package/dist/types/browser/react/use-router.d.ts +6 -1
  15. package/dist/types/browser/response-adapter.d.ts +5 -3
  16. package/dist/types/browser/scroll-restoration.d.ts +9 -3
  17. package/dist/types/browser/segment-reconciler.d.ts +10 -13
  18. package/dist/types/browser/types.d.ts +38 -8
  19. package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
  20. package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
  21. package/dist/types/cache/types.d.ts +8 -0
  22. package/dist/types/client-urls/client-urls.d.ts +6 -2
  23. package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
  24. package/dist/types/client-urls/server-projection.d.ts +9 -5
  25. package/dist/types/client-urls/types.d.ts +29 -16
  26. package/dist/types/client.d.ts +2 -2
  27. package/dist/types/client.rsc.d.ts +2 -2
  28. package/dist/types/decode-loader-results.d.ts +28 -7
  29. package/dist/types/href-client.d.ts +3 -1
  30. package/dist/types/index.rsc.d.ts +1 -1
  31. package/dist/types/loader-redirect.d.ts +2 -3
  32. package/dist/types/outlet-context.d.ts +7 -6
  33. package/dist/types/prefetch-markers.d.ts +14 -5
  34. package/dist/types/route-content-wrapper.d.ts +17 -14
  35. package/dist/types/route-definition/helpers-types.d.ts +7 -2
  36. package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
  37. package/dist/types/route-definition/rewrite.d.ts +48 -5
  38. package/dist/types/route-types.d.ts +2 -2
  39. package/dist/types/router/handler-context.d.ts +14 -2
  40. package/dist/types/router/intercept-resolution.d.ts +19 -46
  41. package/dist/types/router/lazy-includes.d.ts +7 -0
  42. package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
  43. package/dist/types/router/middleware.d.ts +0 -16
  44. package/dist/types/router/navigation-snapshot.d.ts +2 -0
  45. package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
  46. package/dist/types/router/revalidation.d.ts +8 -1
  47. package/dist/types/router/router-context.d.ts +4 -16
  48. package/dist/types/router/router-interfaces.d.ts +0 -6
  49. package/dist/types/router/router-options.d.ts +4 -7
  50. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
  51. package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
  52. package/dist/types/router/segment-resolution.d.ts +1 -1
  53. package/dist/types/router/segment-wrappers.d.ts +3 -5
  54. package/dist/types/router/substitute-pattern-params.d.ts +3 -2
  55. package/dist/types/router/tracing.d.ts +2 -2
  56. package/dist/types/rsc/full-payload.d.ts +14 -2
  57. package/dist/types/rsc/helpers.d.ts +8 -0
  58. package/dist/types/rsc/manifest-init.d.ts +20 -0
  59. package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
  60. package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
  61. package/dist/types/rsc/rewrite-passes.d.ts +12 -2
  62. package/dist/types/rsc/server-action.d.ts +6 -0
  63. package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
  64. package/dist/types/rsc/shell-capture.d.ts +7 -0
  65. package/dist/types/rsc/types.d.ts +5 -0
  66. package/dist/types/segment-system.d.ts +15 -0
  67. package/dist/types/server/context.d.ts +64 -4
  68. package/dist/types/server/request-context.d.ts +41 -4
  69. package/dist/types/server/request-headers.d.ts +12 -0
  70. package/dist/types/server/rewrite-trail.d.ts +8 -1
  71. package/dist/types/testing/e2e/index.d.ts +1 -1
  72. package/dist/types/testing/flight.entry.d.ts +1 -1
  73. package/dist/types/testing/index.d.ts +1 -1
  74. package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
  75. package/dist/types/testing/render-route.d.ts +21 -11
  76. package/dist/types/testing/run-loader.d.ts +7 -0
  77. package/dist/types/testing/run-middleware.d.ts +7 -0
  78. package/dist/types/testing/run-revalidate.d.ts +5 -4
  79. package/dist/types/testing/serve-shell-request.d.ts +41 -4
  80. package/dist/types/testing/shell-status.d.ts +17 -0
  81. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  82. package/dist/types/testing/vitest.d.ts +10 -1
  83. package/dist/types/thenable-status.d.ts +0 -6
  84. package/dist/types/types/handler-context.d.ts +12 -1
  85. package/dist/types/types/loader-types.d.ts +4 -3
  86. package/dist/types/types/route-entry.d.ts +2 -1
  87. package/dist/types/urls/path-helper-types.d.ts +4 -2
  88. package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
  89. package/dist/types/vite/discovery/state.d.ts +20 -0
  90. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
  91. package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
  92. package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
  93. package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
  94. package/dist/vite/index.js +6816 -6547
  95. package/package.json +1 -1
  96. package/skills/app-structure/SKILL.md +255 -0
  97. package/skills/app-structure/content-site.md +333 -0
  98. package/skills/app-structure/e-commerce.md +435 -0
  99. package/skills/app-structure/large-app.md +885 -0
  100. package/skills/app-structure/multi-app.md +280 -0
  101. package/skills/breadcrumbs/SKILL.md +6 -1
  102. package/skills/caching/SKILL.md +3 -1
  103. package/skills/catalog.json +12 -0
  104. package/skills/client-urls/SKILL.md +185 -32
  105. package/skills/composability/SKILL.md +10 -8
  106. package/skills/handler-use/SKILL.md +48 -9
  107. package/skills/hooks/handle-and-actions.md +104 -0
  108. package/skills/hooks/navigation.md +24 -10
  109. package/skills/hooks/urls.md +1 -1
  110. package/skills/i18n/SKILL.md +2 -1
  111. package/skills/intercept/SKILL.md +60 -29
  112. package/skills/layout/SKILL.md +6 -3
  113. package/skills/links/SKILL.md +151 -17
  114. package/skills/loader/SKILL.md +32 -10
  115. package/skills/middleware/SKILL.md +111 -15
  116. package/skills/observability/SKILL.md +11 -0
  117. package/skills/parallel/SKILL.md +5 -5
  118. package/skills/ppr/SKILL.md +43 -8
  119. package/skills/prerender/SKILL.md +1 -2
  120. package/skills/rango/SKILL.md +3 -0
  121. package/skills/response-routes/SKILL.md +4 -1
  122. package/skills/scripts/SKILL.md +12 -1
  123. package/skills/server-actions/SKILL.md +60 -20
  124. package/skills/testing/SKILL.md +1 -0
  125. package/skills/testing/client-components.md +13 -10
  126. package/skills/testing/loader.md +25 -24
  127. package/skills/testing/middleware.md +11 -11
  128. package/skills/testing/response-routes.md +4 -1
  129. package/skills/testing/serve-shell-request.md +85 -21
  130. package/skills/ux-architecture/SKILL.md +980 -0
  131. package/skills/view-transitions/SKILL.md +43 -10
  132. package/src/__augment-tests__/augmented.check.ts +28 -0
  133. package/src/browser/event-controller.ts +44 -18
  134. package/src/browser/intercept-utils.ts +3 -2
  135. package/src/browser/merge-segment-loaders.ts +0 -85
  136. package/src/browser/navigation-bridge.ts +112 -84
  137. package/src/browser/navigation-client.ts +20 -15
  138. package/src/browser/navigation-store.ts +4 -2
  139. package/src/browser/navigation-transaction.ts +9 -8
  140. package/src/browser/partial-update.ts +90 -32
  141. package/src/browser/prefetch/cache.ts +8 -0
  142. package/src/browser/prefetch/fetch.ts +13 -3
  143. package/src/browser/rango-marker.ts +13 -0
  144. package/src/browser/react/NavigationProvider.tsx +25 -7
  145. package/src/browser/react/filter-segment-order.ts +13 -0
  146. package/src/browser/react/use-action.ts +9 -15
  147. package/src/browser/react/use-handle.ts +63 -71
  148. package/src/browser/react/use-href.tsx +24 -5
  149. package/src/browser/react/use-link-status.ts +1 -1
  150. package/src/browser/react/use-navigation.ts +30 -40
  151. package/src/browser/react/use-router.ts +37 -6
  152. package/src/browser/react/use-segments.ts +8 -12
  153. package/src/browser/response-adapter.ts +11 -5
  154. package/src/browser/rsc-router.tsx +3 -1
  155. package/src/browser/scroll-restoration.ts +34 -20
  156. package/src/browser/segment-reconciler.ts +11 -29
  157. package/src/browser/server-action-bridge.ts +52 -16
  158. package/src/browser/types.ts +40 -8
  159. package/src/build/generate-manifest.ts +12 -1
  160. package/src/build/route-types/codegen.ts +6 -2
  161. package/src/build/route-types/per-module-writer.ts +94 -61
  162. package/src/cache/cache-runtime.ts +9 -0
  163. package/src/cache/cache-scope.ts +2 -1
  164. package/src/cache/cf/cf-cache-store.ts +2 -0
  165. package/src/cache/cf/cf-shell-frame.ts +3 -0
  166. package/src/cache/types.ts +8 -0
  167. package/src/cache/vercel/vercel-cache-store.ts +6 -1
  168. package/src/client-urls/client-urls.ts +202 -153
  169. package/src/client-urls/revalidate-registry.ts +12 -3
  170. package/src/client-urls/server-projection.ts +137 -66
  171. package/src/client-urls/types.ts +30 -15
  172. package/src/client.rsc.tsx +11 -1
  173. package/src/client.tsx +9 -1
  174. package/src/decode-loader-results.ts +64 -31
  175. package/src/href-client.ts +3 -1
  176. package/src/index.rsc.ts +1 -0
  177. package/src/index.ts +12 -4
  178. package/src/loader-redirect.tsx +2 -3
  179. package/src/outlet-context.ts +7 -6
  180. package/src/prefetch-markers.ts +15 -5
  181. package/src/reverse.ts +3 -3
  182. package/src/route-content-wrapper.tsx +19 -17
  183. package/src/route-definition/dsl-helpers.ts +27 -7
  184. package/src/route-definition/helpers-types.ts +7 -2
  185. package/src/route-definition/resolve-handler-use.ts +23 -10
  186. package/src/route-definition/rewrite.ts +86 -19
  187. package/src/route-types.ts +1 -2
  188. package/src/router/find-match.ts +37 -1
  189. package/src/router/handler-context.ts +85 -40
  190. package/src/router/intercept-resolution.ts +161 -307
  191. package/src/router/lazy-includes.ts +43 -27
  192. package/src/router/loader-resolution.ts +21 -1
  193. package/src/router/manifest.ts +6 -8
  194. package/src/router/match-api.ts +11 -1
  195. package/src/router/match-middleware/background-revalidation.ts +2 -4
  196. package/src/router/match-middleware/cache-lookup.ts +16 -20
  197. package/src/router/match-middleware/intercept-resolution.ts +41 -60
  198. package/src/router/match-result.ts +31 -2
  199. package/src/router/middleware.ts +6 -147
  200. package/src/router/navigation-snapshot.ts +38 -2
  201. package/src/router/prefetch-cache-ttl.ts +10 -18
  202. package/src/router/prerender-match.ts +2 -1
  203. package/src/router/revalidation.ts +24 -3
  204. package/src/router/router-context.ts +5 -18
  205. package/src/router/router-interfaces.ts +0 -7
  206. package/src/router/router-options.ts +4 -7
  207. package/src/router/segment-resolution/fresh.ts +6 -2
  208. package/src/router/segment-resolution/helpers.ts +7 -1
  209. package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
  210. package/src/router/segment-resolution/revalidation.ts +75 -35
  211. package/src/router/segment-resolution.ts +0 -1
  212. package/src/router/segment-wrappers.ts +8 -13
  213. package/src/router/substitute-pattern-params.ts +3 -2
  214. package/src/router/tracing.ts +2 -2
  215. package/src/router.ts +2 -9
  216. package/src/rsc/full-payload.ts +26 -4
  217. package/src/rsc/handler.ts +83 -41
  218. package/src/rsc/helpers.ts +13 -0
  219. package/src/rsc/manifest-init.ts +56 -3
  220. package/src/rsc/prefetch-render-error.ts +38 -0
  221. package/src/rsc/progressive-enhancement.ts +161 -211
  222. package/src/rsc/rewrite-passes.ts +56 -9
  223. package/src/rsc/rsc-rendering.ts +79 -23
  224. package/src/rsc/server-action.ts +25 -2
  225. package/src/rsc/shell-capture-constants.ts +22 -0
  226. package/src/rsc/shell-capture.ts +11 -0
  227. package/src/rsc/types.ts +5 -0
  228. package/src/segment-system.tsx +125 -119
  229. package/src/server/context.ts +96 -5
  230. package/src/server/handle-store.ts +3 -2
  231. package/src/server/request-context.ts +42 -3
  232. package/src/server/request-headers.ts +32 -0
  233. package/src/server/rewrite-trail.ts +55 -9
  234. package/src/ssr/ssr-root.tsx +6 -10
  235. package/src/testing/dispatch.ts +4 -5
  236. package/src/testing/e2e/index.ts +1 -0
  237. package/src/testing/flight.entry.ts +1 -0
  238. package/src/testing/index.ts +1 -0
  239. package/src/testing/internal/client-urls-projection.ts +31 -11
  240. package/src/testing/render-route.tsx +83 -40
  241. package/src/testing/run-loader.ts +26 -2
  242. package/src/testing/run-middleware.ts +10 -0
  243. package/src/testing/run-revalidate.ts +13 -6
  244. package/src/testing/serve-shell-request.ts +77 -4
  245. package/src/testing/shell-status.ts +40 -0
  246. package/src/testing/vitest-stubs/version.ts +1 -0
  247. package/src/testing/vitest.ts +74 -7
  248. package/src/thenable-status.ts +0 -11
  249. package/src/types/handler-context.ts +12 -1
  250. package/src/types/loader-types.ts +4 -3
  251. package/src/types/route-entry.ts +2 -1
  252. package/src/urls/include-helper.ts +38 -11
  253. package/src/urls/path-helper-types.ts +4 -2
  254. package/src/vite/discovery/client-urls-projection.ts +205 -16
  255. package/src/vite/discovery/discover-routers.ts +2 -2
  256. package/src/vite/discovery/prerender-collection.ts +2 -6
  257. package/src/vite/discovery/state.ts +26 -0
  258. package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
  259. package/src/vite/plugins/expose-internal-ids.ts +46 -40
  260. package/src/vite/plugins/hoist/source.ts +8 -7
  261. package/src/vite/plugins/transition-when-hoist.ts +1 -0
  262. package/src/vite/plugins/version-plugin.ts +7 -0
  263. package/src/vite/plugins/version.d.ts +7 -0
  264. package/src/vite/plugins/virtual-entries.ts +20 -0
  265. package/src/vite/rango.ts +4 -1
  266. package/src/vite/router-discovery.ts +39 -7
  267. package/src/vite/utils/prerender-utils.ts +17 -0
@@ -101,7 +101,7 @@ Navigating between `/`, `/about`, and `/contact` cross-fades the `<Outlet />` co
101
101
 
102
102
  ## Direction-aware transitions
103
103
 
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"`):
104
+ `ViewTransitionClass` accepts an object form keyed by transition type. Every commit the router runs in a transition carries its kind (`"push"`, `"replace"`, `"pop"`, `"action"`, `"revalidate"`, see [Transition kinds](#transition-kinds)). Under `transition()`, Rango also 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"`). The restore runs a view transition: it commits on the next task after the `popstate` event, with its saved scroll applied before the new snapshot is taken (see [Back and Forward](#back-and-forward)):
105
105
 
106
106
  ```tsx
107
107
  layout(<ShopShell />, () => [
@@ -132,7 +132,40 @@ layout(<ShopShell />, () => [
132
132
  /* @keyframes slide-out-to-left, slide-in-from-right, ... defined as usual */
133
133
  ```
134
134
 
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.
135
+ ### Transition kinds
136
+
137
+ Every commit the router runs in a transition adds its `transition({ when })` kind as a transition type, first, before the types above. A map keyed by kind tells a link navigation from a server action's revalidation, with or without `transition()` on the route:
138
+
139
+ | Kind | Commits |
140
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
141
+ | `"push"` | A link, `router.push()`, `navigate()`: the same-route content hold (search or catch-all change), a fully prefetched click, an intercept, the commit after a `clientUrls()` optimistic swap |
142
+ | `"replace"` | `router.replace()`, `<Link replace>` |
143
+ | `"pop"` | A back/forward: one that refetches, and under `transition()` one restored from the history cache |
144
+ | `"action"` | A server action's revalidation and its error-boundary commit |
145
+ | `"revalidate"` | `router.refresh()`, a stale entry's background revalidation |
146
+
147
+ ```tsx
148
+ // Animate navigations; keep action revalidations and background refreshes still.
149
+ <ViewTransition update={{ push: "page", replace: "page", default: "none" }}>
150
+ <ProductGrid />
151
+ </ViewTransition>
152
+ ```
153
+
154
+ - Tagging alone animates nothing: React starts a view transition only where a `<ViewTransition>` resolves a class other than `"none"` for the commit's types.
155
+ - A commit outside a transition carries no type: a cold click that mounts new segments, a gated-off commit (`when` returned `false`), and a back/forward restored from the history cache on a route without `transition()` (it commits inside the `popstate` event). Under `transition()` that restore carries `"pop"` and `"navigation-back"`.
156
+ - Under `transition()`, a stale entry's revalidation carries `"revalidate"` and `"action"`. An `action`-keyed map that must skip it also keys `revalidate: "none"` (`"none"` wins over any other match).
157
+ - The types reach the commit that brings the page. React keeps one queue of transition types per root, and the first transition commit takes the whole queue. No router hook update takes them first: `useHandle()` and `useSegments()` commit with the page, and a `useNavigation()` value that moves while neither the state nor the URL does (`pendingUrl` as the response starts streaming, `isStreaming` as it ends) commits with the page while the reader has a held value, urgently otherwise, so it starts no view transition of its own. A transition your own code starts while React holds a navigation's page can still take them. Handle values on a held page land in the commit right after the page, by design; see [hooks](../hooks/handle-and-actions.md#when-handle-values-change-by-design). Use a kind map to choose an animation, never for correctness.
158
+
159
+ ### Back and Forward
160
+
161
+ A back/forward the router restores from its history cache animates like a link under `transition()`:
162
+
163
+ - It commits on the next task after the `popstate` event, in a view transition whose types are `"pop"` and `"navigation-back"`. React commits a transition started inside the event synchronously, with no view transition, so the router waits that one task; the page being left stays on screen until then, and the location hooks change with the restored page.
164
+ - The saved scroll position is applied in the commit's layout effect, before the view transition takes its new snapshot, so the restored page animates in at the position it was left.
165
+ - A traversal the browser animated itself (`PopStateEvent.hasUAVisualTransition`: the iOS Safari and Chrome Android back swipe) commits at once and is not animated again.
166
+ - A second Back or Forward before the first one's restore commits wins; the first restore is dropped.
167
+ - Without `transition()` the restore commits inside the event, with no view transition and no type.
168
+ - So does an entry whose scroll the browser restores itself (`history.scrollRestoration` is `"auto"`: the document renders no `<Html.ScrollRestoration>`, which Rango's default document does, or the entry predates it). The browser restores that scroll right after the event, on whatever page is then on screen.
136
169
 
137
170
  ## Wrapper form: applying transition to a group of routes
138
171
 
@@ -234,7 +267,7 @@ interface TransitionConfig {
234
267
  ```
235
268
 
236
269
  - `default` is the catch-all if a phase-specific prop is unset.
237
- - The object form keys are React transition types tagged by rango: `"navigation"` (forward navigations), `"navigation-back"` (popstate cache restores), and `"action"` (partial-update action/refetch paths only — see the caveat in "Direction-aware transitions").
270
+ - The object form keys are React transition types tagged by rango: the commit's kind (`"push"`, `"replace"`, `"pop"`, `"action"`, `"revalidate"`, see [Transition kinds](#transition-kinds)) and, under `transition()`, `"navigation"` (forward navigations), `"navigation-back"` (popstate cache restores) and `"action"` (action and stale revalidations).
238
271
  - `name` lets you participate in cross-page morphs by name (advanced; you usually don't need this on a layout/route-level wrap).
239
272
  - `viewTransition` toggles whether rango places its own `<ViewTransition>` boundary. `"auto"` (default) wraps as described above; `false` opts out — see the next section.
240
273
 
@@ -380,13 +413,13 @@ interface TransitionWhenContext {
380
413
  - In a `clientUrls()` group, `false` also opts a same-route navigation out of
381
414
  the group's default hold.
382
415
 
383
- `kind` lines up with the class keys in "Direction-aware transitions": a
384
- `"pop"` restored from the history cache animates with `"navigation-back"`
385
- (a pop that refetches uses `"navigation"`); `"push"`, `"replace"` and a
386
- `router.refresh()` (`"revalidate"`) use `"navigation"`; a stale entry's
387
- revalidation (`"revalidate"`) and an action's refetch (`"action"`) use
388
- `"action"`. An action's own commit (`useActionState`, a form action) carries
389
- no transition type (see the note under "Direction-aware transitions").
416
+ `kind` is also the transition type the commit carries (see "Transition
417
+ kinds"): `{ push: "page", default: "none" }` animates the commits `when` sees
418
+ as `"push"`. Under `transition()` the older keys stay: a `"pop"` restored from
419
+ the history cache adds `"navigation-back"` (a pop that refetches adds
420
+ `"navigation"`); `"push"`, `"replace"` and a `router.refresh()`
421
+ (`"revalidate"`) add `"navigation"`; a stale entry's revalidation adds
422
+ `"action"`.
390
423
 
391
424
  ### Opting out per navigation
392
425
 
@@ -8,6 +8,7 @@
8
8
  import "./augment.js";
9
9
  import type {
10
10
  Handler,
11
+ LoaderContext,
11
12
  MiddlewareContext,
12
13
  ResponseHandlerContext,
13
14
  RouteLocation,
@@ -17,6 +18,7 @@ import type {
17
18
  TransitionWhenFn,
18
19
  } from "../index.js";
19
20
  import type { DefaultRouteName } from "../types/global-namespace.js";
21
+ import type { RequestContext } from "../server/request-context.js";
20
22
  import type { InterceptWhenFn } from "../server/context.js";
21
23
  import { href } from "../href-client.js";
22
24
  import { rewrite, type Rewrite } from "../route-definition/rewrite.js";
@@ -62,6 +64,32 @@ function responseReverse(ctx: ResponseHandlerContext): void {
62
64
  }
63
65
  void responseReverse;
64
66
 
67
+ // Loader ctx.reverse and getRequestContext().reverse take the same optional
68
+ // params as handlers: after the route match the runtime fills them from the
69
+ // matched route's params.
70
+ function loaderReverse(ctx: LoaderContext): void {
71
+ ctx.reverse("home");
72
+ ctx.reverse("blog.post");
73
+ ctx.reverse("blog.post", { slug: "hello" });
74
+ ctx.reverse("search", {}, { q: "hats" });
75
+ // @ts-expect-error - unknown param name is rejected
76
+ ctx.reverse("blog.post", { slugg: "hello" });
77
+ // @ts-expect-error - unknown global name is rejected
78
+ ctx.reverse("nope");
79
+ }
80
+ void loaderReverse;
81
+
82
+ function requestContextReverse(ctx: RequestContext): void {
83
+ ctx.reverse("home");
84
+ ctx.reverse("blog.post");
85
+ ctx.reverse("blog.post", { slug: "hello" });
86
+ // @ts-expect-error - unknown param name is rejected
87
+ ctx.reverse("blog.post", { slugg: "hello" });
88
+ // @ts-expect-error - unknown global name is rejected
89
+ ctx.reverse("nope");
90
+ }
91
+ void requestContextReverse;
92
+
65
93
  function middlewareReverse(ctx: MiddlewareContext): void {
66
94
  ctx.reverse("blog.post", { slug: "hello" });
67
95
  // @ts-expect-error - dot-local name; no include() scope to resolve it
@@ -72,6 +72,8 @@ export interface ActionEntry {
72
72
  result?: unknown;
73
73
  /** Error from action (set on failure) */
74
74
  error?: unknown;
75
+ /** A loader that failed while it re-ran after the action (set on success) */
76
+ revalidationError?: unknown;
75
77
  /** Segment IDs that were revalidated by this action */
76
78
  revalidatedSegments: string[];
77
79
  /** Timestamp when action started */
@@ -117,9 +119,15 @@ export type StateListener = () => void;
117
119
  export type ActionStateListener = (state: TrackedActionState) => void;
118
120
 
119
121
  /**
120
- * Handle state listener
122
+ * Handle state listener. `signal`, from a payload update: "anchor" when it
123
+ * hands React a page in a transition (route state on screen until it
124
+ * commits), "release" when it hands one urgently; "release" again once the
125
+ * page committed (useCommitAnchoredState, react/use-handle.ts).
121
126
  */
122
- export type HandleListener = () => void;
127
+ export type HandleListener = (signal?: RouteStateSignal) => void;
128
+
129
+ /** See HandleListener. */
130
+ export type RouteStateSignal = "anchor" | "release";
123
131
 
124
132
  /**
125
133
  * Internal handle state stored in controller.
@@ -200,8 +208,8 @@ export interface ActionHandle extends Disposable {
200
208
  * later-initiated winner's response lands and overwrites it.
201
209
  */
202
210
  claimLocationState(state: Record<string, unknown>): Record<string, unknown>;
203
- /** Complete the action with result */
204
- complete(result?: unknown): void;
211
+ /** Complete the action with result, and a loader rerun's error, if any */
212
+ complete(result?: unknown, revalidationError?: unknown): void;
205
213
  /** Fail the action with error */
206
214
  fail(error: unknown): void;
207
215
  /** Whether action was completed (success or failure) */
@@ -260,8 +268,18 @@ export interface EventController {
260
268
  * Deliver queued location, params, navigation, and handle notifications now.
261
269
  * NavigationProvider calls this inside the payload update so React assigns
262
270
  * every route-state hook update to the same normal or transition lane.
271
+ * `signal`: handle readers learn how the page is handed over. After
272
+ * "anchor" (a page in a transition) values that arrive before
273
+ * releaseRouteState() wait for it (useCommitAnchoredState,
274
+ * react/use-handle.ts).
275
+ */
276
+ flushRouteState(signal?: RouteStateSignal): void;
277
+ /**
278
+ * The page the last flushRouteState("anchor") announced has committed:
279
+ * NavigationProvider calls this from its layout effect, and handle readers
280
+ * apply what waited, in that task.
263
281
  */
264
- flushRouteState(): void;
282
+ releaseRouteState(): void;
265
283
 
266
284
  // Handle operations
267
285
  setHandleData(
@@ -403,12 +421,14 @@ export function subscribeToLocationChange(
403
421
  };
404
422
  }
405
423
 
406
- const DEFAULT_ACTION_STATE: TrackedActionState = {
424
+ /** An action with no tracked entry: idle, nothing reported. */
425
+ export const DEFAULT_ACTION_STATE: TrackedActionState = {
407
426
  state: "idle",
408
427
  actionId: null,
409
428
  payload: null,
410
429
  error: null,
411
430
  result: null,
431
+ revalidationError: null,
412
432
  };
413
433
 
414
434
  // Shared empty inflight-actions list. getState() hands back this exact reference
@@ -437,20 +457,23 @@ function matchesActionId(
437
457
 
438
458
  interface DebouncedNotifier {
439
459
  schedule(): void;
440
- flush(): void;
460
+ flush(signal?: RouteStateSignal): void;
441
461
  }
442
462
 
443
463
  // Batch rapid notifications into one task to prevent render storms. The
444
- // explicit flush lets a React update owner preserve its scheduling lane.
445
- function makeDebouncedNotifier(listeners: Set<() => void>): DebouncedNotifier {
464
+ // explicit flush lets a React update owner preserve its scheduling lane; a
465
+ // signal is delivered even with nothing queued.
466
+ function makeDebouncedNotifier(
467
+ listeners: Set<(signal?: RouteStateSignal) => void>,
468
+ ): DebouncedNotifier {
446
469
  let timeout: ReturnType<typeof setTimeout> | null = null;
447
- const flush = () => {
448
- if (timeout === null) return;
449
- clearTimeout(timeout);
470
+ const flush = (signal?: RouteStateSignal) => {
471
+ if (timeout === null && !signal) return;
472
+ clearTimeout(timeout ?? undefined);
450
473
  timeout = null;
451
474
  notifyListeners(
452
475
  [...listeners],
453
- (listener) => listener(),
476
+ (listener) => listener(signal),
454
477
  (listener) => listeners.has(listener),
455
478
  );
456
479
  };
@@ -613,9 +636,9 @@ export function createEventController(
613
636
 
614
637
  const notifyHandles = makeDebouncedNotifier(handleListeners);
615
638
 
616
- function flushRouteState(): void {
639
+ function flushRouteState(signal?: RouteStateSignal): void {
617
640
  notifyStateListeners.flush();
618
- notifyHandles.flush();
641
+ notifyHandles.flush(signal);
619
642
  }
620
643
 
621
644
  function getState(): DerivedNavigationState {
@@ -700,6 +723,7 @@ export function createEventController(
700
723
  payload: entry.payload,
701
724
  error: entry.error ?? null,
702
725
  result: entry.result ?? null,
726
+ revalidationError: entry.revalidationError ?? null,
703
727
  };
704
728
  }
705
729
 
@@ -871,7 +895,7 @@ export function createEventController(
871
895
  let actionCompleted = false;
872
896
  let cohortReleased = false;
873
897
  let pendingResult:
874
- | { type: "success"; value?: unknown }
898
+ | { type: "success"; value?: unknown; revalidationError?: unknown }
875
899
  | { type: "error"; value: unknown }
876
900
  | null = null;
877
901
 
@@ -922,6 +946,7 @@ export function createEventController(
922
946
  entry.error = pendingResult.value;
923
947
  } else if (pendingResult?.type === "success") {
924
948
  entry.result = pendingResult.value;
949
+ entry.revalidationError = pendingResult.revalidationError;
925
950
  }
926
951
  entry.phase = "settling";
927
952
  notify();
@@ -997,8 +1022,8 @@ export function createEventController(
997
1022
  return winning;
998
1023
  },
999
1024
 
1000
- complete(result?: unknown) {
1001
- settleWith({ type: "success", value: result });
1025
+ complete(result?: unknown, revalidationError?: unknown) {
1026
+ settleWith({ type: "success", value: result, revalidationError });
1002
1027
  },
1003
1028
 
1004
1029
  fail(error: unknown) {
@@ -1258,6 +1283,7 @@ export function createEventController(
1258
1283
  subscribeToAction,
1259
1284
  subscribeToHandles,
1260
1285
  flushRouteState,
1286
+ releaseRouteState: () => notifyHandles.flush("release"),
1261
1287
 
1262
1288
  // Direct access
1263
1289
  getCurrentNavigation: () => currentNavigation,
@@ -4,10 +4,11 @@ import type { SlotState } from "../types.js";
4
4
  /**
5
5
  * Check if a segment is an intercept segment.
6
6
  * Intercept segments have namespace starting with "intercept:" — both the
7
- * parallel container (@modal) and its content children receive this namespace
7
+ * parallel container (@modal) and its loader segments receive this namespace
8
8
  * from intercept-resolution.ts. Regular parallel segments like @sidebar do not.
9
+ * Server-safe: the cache and match pipeline share it.
9
10
  */
10
- export function isInterceptSegment(s: ResolvedSegment): boolean {
11
+ export function isInterceptSegment(s: { namespace?: string }): boolean {
11
12
  return s.namespace?.startsWith("intercept:") === true;
12
13
  }
13
14
 
@@ -1,90 +1,5 @@
1
1
  import type { ResolvedSegment } from "./types.js";
2
2
  import { debugLog, IS_BROWSER_DEBUG } from "./logging.js";
3
- import { settledValue } from "../thenable-status.js";
4
-
5
- /**
6
- * Merge partial loader data from server with cached loader data.
7
- *
8
- * During partial revalidation (stale or action), the server may return only
9
- * some loaders that pass the revalidation check, none when every one is held.
10
- * The component still needs all loader data, so we merge fresh data with
11
- * cached data. Two settled aggregates merge into the array itself: a fresh
12
- * Promise.all over settled parts is a promise React has not read, and content
13
- * on screen handed one suspends (docs/internal/suspense-contract.md, rule 2).
14
- *
15
- * @param fromServer - Segment returned from server with partial loaders
16
- * @param fromCache - Cached segment with full loader data
17
- * @returns Merged segment with complete loader data
18
- */
19
- export function mergeSegmentLoaders(
20
- fromServer: ResolvedSegment,
21
- fromCache: ResolvedSegment,
22
- ): ResolvedSegment {
23
- const serverLoaderIds = fromServer.loaderIds || [];
24
- const cachedLoaderIds = fromCache.loaderIds || [];
25
-
26
- if (IS_BROWSER_DEBUG) {
27
- debugLog(
28
- `[Browser] Merging partial loaders: server has ${serverLoaderIds.join(", ")}, cache has ${cachedLoaderIds.join(", ")}`,
29
- );
30
- }
31
-
32
- // Fresh data for the loaders the server re-ran, cached data for the rest.
33
- const merge = (newData: any[], cachedData: any[]): any[] =>
34
- cachedLoaderIds.map((id: string, i: number) => {
35
- const newIndex = serverLoaderIds.indexOf(id);
36
- return newIndex !== -1 ? newData[newIndex] : cachedData[i];
37
- });
38
- const settledNew = settledValue(fromServer.loaderDataPromise!);
39
- const settledCached = settledValue(fromCache.loaderDataPromise!);
40
-
41
- return {
42
- ...fromCache,
43
- // The server re-ran the slot's handler and layout: their render applies,
44
- // as it does when every loader re-runs. Only the loaders it held keep
45
- // their cached data.
46
- component: fromServer.component,
47
- layout: fromServer.layout,
48
- // Every loader held: the cached aggregate itself, so a boundary still
49
- // waiting on it keeps its one thenable (suspense-contract.md, rule 1).
50
- loaderDataPromise:
51
- serverLoaderIds.length === 0
52
- ? fromCache.loaderDataPromise
53
- : settledNew && settledCached
54
- ? merge(settledNew, settledCached)
55
- : Promise.all([
56
- fromServer.loaderDataPromise!,
57
- fromCache.loaderDataPromise!,
58
- ]).then(([newData, cachedData]) => merge(newData, cachedData)),
59
- // Keep all loader IDs from cache
60
- loaderIds: fromCache.loaderIds,
61
- // The rebuilt aggregate is the source of truth. Cached per-loader
62
- // streams would make LoaderResolver ignore loaderDataPromise.
63
- loaderStreams: undefined,
64
- awaitedLoaderIds: undefined,
65
- };
66
- }
67
-
68
- /**
69
- * Check if segments need loader merging during partial revalidation.
70
- *
71
- * Returns true when:
72
- * - Server returned fewer loaders than cached (partial revalidation)
73
- * - Both segments have loader data promises
74
- */
75
- export function needsLoaderMerge(
76
- fromServer: ResolvedSegment,
77
- fromCache: ResolvedSegment | undefined,
78
- ): fromCache is ResolvedSegment {
79
- return !!(
80
- fromCache &&
81
- fromServer.loaderIds &&
82
- fromCache.loaderIds &&
83
- fromServer.loaderIds.length < fromCache.loaderIds.length &&
84
- fromServer.loaderDataPromise &&
85
- fromCache.loaderDataPromise
86
- );
87
- }
88
3
 
89
4
  /**
90
5
  * Insert diff segments that aren't in the matched array into allSegments.