@rangojs/router 0.24.3 → 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 (274) 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-root.d.ts +4 -1
  23. package/dist/types/client-urls/client-urls.d.ts +28 -2
  24. package/dist/types/client-urls/navigation.d.ts +6 -8
  25. package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
  26. package/dist/types/client-urls/server-projection.d.ts +20 -5
  27. package/dist/types/client-urls/types.d.ts +35 -17
  28. package/dist/types/client.d.ts +2 -2
  29. package/dist/types/client.rsc.d.ts +2 -2
  30. package/dist/types/decode-loader-results.d.ts +28 -7
  31. package/dist/types/href-client.d.ts +3 -1
  32. package/dist/types/index.rsc.d.ts +1 -1
  33. package/dist/types/loader-redirect.d.ts +2 -3
  34. package/dist/types/outlet-context.d.ts +7 -6
  35. package/dist/types/prefetch-markers.d.ts +14 -5
  36. package/dist/types/route-content-wrapper.d.ts +17 -14
  37. package/dist/types/route-definition/helpers-types.d.ts +7 -2
  38. package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
  39. package/dist/types/route-definition/rewrite.d.ts +48 -5
  40. package/dist/types/route-types.d.ts +2 -2
  41. package/dist/types/router/handler-context.d.ts +14 -2
  42. package/dist/types/router/intercept-resolution.d.ts +19 -46
  43. package/dist/types/router/lazy-includes.d.ts +7 -0
  44. package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
  45. package/dist/types/router/middleware.d.ts +0 -16
  46. package/dist/types/router/navigation-snapshot.d.ts +2 -0
  47. package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
  48. package/dist/types/router/revalidation.d.ts +8 -1
  49. package/dist/types/router/router-context.d.ts +4 -16
  50. package/dist/types/router/router-interfaces.d.ts +0 -6
  51. package/dist/types/router/router-options.d.ts +4 -7
  52. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
  53. package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
  54. package/dist/types/router/segment-resolution.d.ts +1 -1
  55. package/dist/types/router/segment-wrappers.d.ts +3 -5
  56. package/dist/types/router/substitute-pattern-params.d.ts +19 -2
  57. package/dist/types/router/tracing.d.ts +2 -2
  58. package/dist/types/rsc/full-payload.d.ts +14 -2
  59. package/dist/types/rsc/helpers.d.ts +8 -0
  60. package/dist/types/rsc/manifest-init.d.ts +20 -0
  61. package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
  62. package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
  63. package/dist/types/rsc/rewrite-passes.d.ts +12 -2
  64. package/dist/types/rsc/server-action.d.ts +6 -0
  65. package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
  66. package/dist/types/rsc/shell-capture.d.ts +7 -0
  67. package/dist/types/rsc/types.d.ts +5 -0
  68. package/dist/types/segment-system.d.ts +15 -0
  69. package/dist/types/server/context.d.ts +64 -4
  70. package/dist/types/server/request-context.d.ts +41 -4
  71. package/dist/types/server/request-headers.d.ts +12 -0
  72. package/dist/types/server/rewrite-trail.d.ts +8 -1
  73. package/dist/types/testing/e2e/index.d.ts +1 -1
  74. package/dist/types/testing/flight.entry.d.ts +1 -1
  75. package/dist/types/testing/index.d.ts +1 -1
  76. package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
  77. package/dist/types/testing/render-route.d.ts +21 -11
  78. package/dist/types/testing/run-loader.d.ts +7 -0
  79. package/dist/types/testing/run-middleware.d.ts +7 -0
  80. package/dist/types/testing/run-revalidate.d.ts +5 -4
  81. package/dist/types/testing/serve-shell-request.d.ts +41 -4
  82. package/dist/types/testing/shell-status.d.ts +17 -0
  83. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  84. package/dist/types/testing/vitest.d.ts +10 -1
  85. package/dist/types/thenable-status.d.ts +0 -6
  86. package/dist/types/types/handler-context.d.ts +12 -1
  87. package/dist/types/types/loader-types.d.ts +4 -3
  88. package/dist/types/types/route-entry.d.ts +2 -1
  89. package/dist/types/urls/path-helper-types.d.ts +4 -2
  90. package/dist/types/urls/path-helper.d.ts +5 -0
  91. package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
  92. package/dist/types/vite/discovery/state.d.ts +20 -0
  93. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
  94. package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
  95. package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
  96. package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
  97. package/dist/vite/index.js +6816 -6547
  98. package/package.json +1 -1
  99. package/skills/app-structure/SKILL.md +255 -0
  100. package/skills/app-structure/content-site.md +333 -0
  101. package/skills/app-structure/e-commerce.md +435 -0
  102. package/skills/app-structure/large-app.md +885 -0
  103. package/skills/app-structure/multi-app.md +280 -0
  104. package/skills/breadcrumbs/SKILL.md +6 -1
  105. package/skills/caching/SKILL.md +3 -1
  106. package/skills/catalog.json +12 -0
  107. package/skills/client-urls/SKILL.md +195 -32
  108. package/skills/composability/SKILL.md +10 -8
  109. package/skills/handler-use/SKILL.md +48 -9
  110. package/skills/hooks/handle-and-actions.md +104 -0
  111. package/skills/hooks/navigation.md +24 -10
  112. package/skills/hooks/urls.md +1 -1
  113. package/skills/i18n/SKILL.md +2 -1
  114. package/skills/intercept/SKILL.md +60 -29
  115. package/skills/layout/SKILL.md +6 -3
  116. package/skills/links/SKILL.md +151 -17
  117. package/skills/loader/SKILL.md +32 -10
  118. package/skills/middleware/SKILL.md +111 -15
  119. package/skills/observability/SKILL.md +11 -0
  120. package/skills/parallel/SKILL.md +5 -5
  121. package/skills/ppr/SKILL.md +43 -8
  122. package/skills/prerender/SKILL.md +1 -2
  123. package/skills/rango/SKILL.md +3 -0
  124. package/skills/response-routes/SKILL.md +4 -1
  125. package/skills/scripts/SKILL.md +12 -1
  126. package/skills/server-actions/SKILL.md +60 -20
  127. package/skills/testing/SKILL.md +1 -0
  128. package/skills/testing/client-components.md +13 -10
  129. package/skills/testing/loader.md +25 -24
  130. package/skills/testing/middleware.md +11 -11
  131. package/skills/testing/response-routes.md +4 -1
  132. package/skills/testing/serve-shell-request.md +85 -21
  133. package/skills/ux-architecture/SKILL.md +980 -0
  134. package/skills/view-transitions/SKILL.md +43 -10
  135. package/src/__augment-tests__/augmented.check.ts +28 -0
  136. package/src/browser/event-controller.ts +44 -18
  137. package/src/browser/intercept-utils.ts +3 -2
  138. package/src/browser/merge-segment-loaders.ts +0 -85
  139. package/src/browser/navigation-bridge.ts +119 -98
  140. package/src/browser/navigation-client.ts +20 -15
  141. package/src/browser/navigation-store.ts +4 -2
  142. package/src/browser/navigation-transaction.ts +9 -8
  143. package/src/browser/partial-update.ts +90 -32
  144. package/src/browser/prefetch/cache.ts +8 -0
  145. package/src/browser/prefetch/fetch.ts +13 -3
  146. package/src/browser/rango-marker.ts +13 -0
  147. package/src/browser/react/NavigationProvider.tsx +25 -7
  148. package/src/browser/react/filter-segment-order.ts +13 -0
  149. package/src/browser/react/use-action.ts +9 -15
  150. package/src/browser/react/use-handle.ts +63 -71
  151. package/src/browser/react/use-href.tsx +24 -5
  152. package/src/browser/react/use-link-status.ts +1 -1
  153. package/src/browser/react/use-navigation.ts +30 -40
  154. package/src/browser/react/use-reverse.ts +4 -17
  155. package/src/browser/react/use-router.ts +37 -6
  156. package/src/browser/react/use-segments.ts +8 -12
  157. package/src/browser/response-adapter.ts +11 -5
  158. package/src/browser/rsc-router.tsx +3 -1
  159. package/src/browser/scroll-restoration.ts +34 -20
  160. package/src/browser/segment-reconciler.ts +11 -29
  161. package/src/browser/server-action-bridge.ts +52 -16
  162. package/src/browser/types.ts +40 -8
  163. package/src/build/generate-manifest.ts +12 -1
  164. package/src/build/route-types/codegen.ts +6 -2
  165. package/src/build/route-types/per-module-writer.ts +94 -61
  166. package/src/cache/cache-runtime.ts +9 -0
  167. package/src/cache/cache-scope.ts +2 -1
  168. package/src/cache/cf/cf-cache-store.ts +2 -0
  169. package/src/cache/cf/cf-shell-frame.ts +3 -0
  170. package/src/cache/types.ts +8 -0
  171. package/src/cache/vercel/vercel-cache-store.ts +6 -1
  172. package/src/client-urls/client-root.tsx +10 -5
  173. package/src/client-urls/client-urls.ts +246 -166
  174. package/src/client-urls/navigation.ts +45 -38
  175. package/src/client-urls/revalidate-registry.ts +12 -3
  176. package/src/client-urls/server-projection.ts +286 -75
  177. package/src/client-urls/types.ts +40 -16
  178. package/src/client.rsc.tsx +11 -1
  179. package/src/client.tsx +9 -1
  180. package/src/decode-loader-results.ts +64 -31
  181. package/src/href-client.ts +3 -1
  182. package/src/index.rsc.ts +1 -0
  183. package/src/index.ts +12 -4
  184. package/src/loader-redirect.tsx +2 -3
  185. package/src/outlet-context.ts +7 -6
  186. package/src/prefetch-markers.ts +15 -5
  187. package/src/reverse.ts +3 -3
  188. package/src/route-content-wrapper.tsx +19 -17
  189. package/src/route-definition/dsl-helpers.ts +27 -7
  190. package/src/route-definition/helpers-types.ts +7 -2
  191. package/src/route-definition/resolve-handler-use.ts +23 -10
  192. package/src/route-definition/rewrite.ts +86 -19
  193. package/src/route-types.ts +1 -2
  194. package/src/router/find-match.ts +37 -1
  195. package/src/router/handler-context.ts +85 -40
  196. package/src/router/intercept-resolution.ts +161 -307
  197. package/src/router/lazy-includes.ts +43 -27
  198. package/src/router/loader-resolution.ts +21 -1
  199. package/src/router/manifest.ts +6 -8
  200. package/src/router/match-api.ts +11 -1
  201. package/src/router/match-middleware/background-revalidation.ts +2 -4
  202. package/src/router/match-middleware/cache-lookup.ts +16 -20
  203. package/src/router/match-middleware/intercept-resolution.ts +41 -60
  204. package/src/router/match-result.ts +31 -2
  205. package/src/router/middleware.ts +6 -147
  206. package/src/router/navigation-snapshot.ts +38 -2
  207. package/src/router/prefetch-cache-ttl.ts +10 -18
  208. package/src/router/prerender-match.ts +2 -1
  209. package/src/router/revalidation.ts +24 -3
  210. package/src/router/router-context.ts +5 -18
  211. package/src/router/router-interfaces.ts +0 -7
  212. package/src/router/router-options.ts +4 -7
  213. package/src/router/segment-resolution/fresh.ts +6 -2
  214. package/src/router/segment-resolution/helpers.ts +7 -1
  215. package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
  216. package/src/router/segment-resolution/revalidation.ts +75 -35
  217. package/src/router/segment-resolution.ts +0 -1
  218. package/src/router/segment-wrappers.ts +8 -13
  219. package/src/router/substitute-pattern-params.ts +37 -2
  220. package/src/router/tracing.ts +2 -2
  221. package/src/router.ts +2 -9
  222. package/src/rsc/full-payload.ts +26 -4
  223. package/src/rsc/handler.ts +83 -41
  224. package/src/rsc/helpers.ts +13 -0
  225. package/src/rsc/manifest-init.ts +56 -3
  226. package/src/rsc/prefetch-render-error.ts +38 -0
  227. package/src/rsc/progressive-enhancement.ts +161 -211
  228. package/src/rsc/rewrite-passes.ts +56 -9
  229. package/src/rsc/rsc-rendering.ts +79 -23
  230. package/src/rsc/server-action.ts +25 -2
  231. package/src/rsc/shell-capture-constants.ts +22 -0
  232. package/src/rsc/shell-capture.ts +11 -0
  233. package/src/rsc/types.ts +5 -0
  234. package/src/segment-system.tsx +125 -119
  235. package/src/server/context.ts +96 -5
  236. package/src/server/handle-store.ts +3 -2
  237. package/src/server/request-context.ts +42 -3
  238. package/src/server/request-headers.ts +32 -0
  239. package/src/server/rewrite-trail.ts +55 -9
  240. package/src/ssr/ssr-root.tsx +6 -10
  241. package/src/testing/dispatch.ts +4 -5
  242. package/src/testing/e2e/index.ts +1 -0
  243. package/src/testing/flight.entry.ts +1 -0
  244. package/src/testing/index.ts +1 -0
  245. package/src/testing/internal/client-urls-projection.ts +31 -11
  246. package/src/testing/render-route.tsx +83 -40
  247. package/src/testing/run-loader.ts +26 -2
  248. package/src/testing/run-middleware.ts +10 -0
  249. package/src/testing/run-revalidate.ts +13 -6
  250. package/src/testing/serve-shell-request.ts +77 -4
  251. package/src/testing/shell-status.ts +40 -0
  252. package/src/testing/vitest-stubs/version.ts +1 -0
  253. package/src/testing/vitest.ts +74 -7
  254. package/src/thenable-status.ts +0 -11
  255. package/src/types/handler-context.ts +12 -1
  256. package/src/types/loader-types.ts +4 -3
  257. package/src/types/route-entry.ts +2 -1
  258. package/src/urls/include-helper.ts +38 -11
  259. package/src/urls/path-helper-types.ts +4 -2
  260. package/src/urls/path-helper.ts +16 -3
  261. package/src/vite/discovery/client-urls-projection.ts +205 -16
  262. package/src/vite/discovery/discover-routers.ts +2 -2
  263. package/src/vite/discovery/prerender-collection.ts +2 -6
  264. package/src/vite/discovery/state.ts +26 -0
  265. package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
  266. package/src/vite/plugins/expose-internal-ids.ts +46 -40
  267. package/src/vite/plugins/hoist/source.ts +8 -7
  268. package/src/vite/plugins/transition-when-hoist.ts +1 -0
  269. package/src/vite/plugins/version-plugin.ts +7 -0
  270. package/src/vite/plugins/version.d.ts +7 -0
  271. package/src/vite/plugins/virtual-entries.ts +20 -0
  272. package/src/vite/rango.ts +4 -1
  273. package/src/vite/router-discovery.ts +39 -7
  274. package/src/vite/utils/prerender-utils.ts +17 -0
@@ -1,18 +1,12 @@
1
1
  "use client";
2
2
 
3
- import {
4
- useContext,
5
- useState,
6
- useEffect,
7
- useRef,
8
- useOptimistic,
9
- startTransition,
10
- } from "react";
3
+ import { useContext, useState, useEffect, useRef } from "react";
11
4
  import type { Handle } from "../../handle.js";
12
5
  import { collectHandleData } from "../../handle.js";
13
6
  import type { HandleData } from "../types.js";
14
7
  import { NavigationStoreContext } from "./context.js";
15
8
  import { shallowEqual } from "./shallow-equal.js";
9
+ import type { RouteStateSignal } from "../event-controller.js";
16
10
 
17
11
  /**
18
12
  * Hook to access collected handle data.
@@ -43,77 +37,75 @@ export function useHandle<T, A, S>(
43
37
  ): Rango.FlightSerialize<A> | S {
44
38
  const ctx = useContext(NavigationStoreContext);
45
39
 
46
- const [value, setValue] = useState<Rango.FlightSerialize<A> | S>(() => {
47
- if (!ctx) {
48
- const collected = collectHandleData(
49
- handle,
50
- {},
51
- [],
52
- ) as Rango.FlightSerialize<A>;
53
- return selector ? selector(collected) : collected;
54
- }
40
+ const selectorRef = useRef(selector);
41
+ selectorRef.current = selector;
55
42
 
56
- // Until every boundary has hydrated the store returns the state the
57
- // document's HTML was rendered with (EventController.lockHydration), so a
58
- // reader hydrating late reads what SSR rendered (issue #1035).
59
- const state = ctx.eventController.getHandleState();
43
+ // Until every boundary has hydrated the store returns the state the
44
+ // document's HTML was rendered with (EventController.lockHydration), so a
45
+ // reader hydrating late reads what SSR rendered (issue #1035).
46
+ const collect = (): Rango.FlightSerialize<A> | S => {
47
+ const state = ctx?.eventController.getHandleState();
60
48
  const collected = collectHandleData(
61
49
  handle,
62
- state.data,
63
- state.segmentOrder,
50
+ state?.data ?? {},
51
+ state?.segmentOrder ?? [],
64
52
  ) as Rango.FlightSerialize<A>;
65
- return selector ? selector(collected) : collected;
66
- });
67
- const [optimisticValue, setOptimisticValue] = useOptimistic(value);
68
-
69
- const prevValueRef = useRef(value);
70
- prevValueRef.current = value;
53
+ return selectorRef.current ? selectorRef.current(collected) : collected;
54
+ };
71
55
 
72
- const selectorRef = useRef(selector);
73
- selectorRef.current = selector;
56
+ const [value, setValue] = useCommitAnchoredState(collect);
74
57
 
75
58
  useEffect(() => {
76
59
  if (!ctx) return;
77
-
78
- // Sync current state for the (possibly new) handle so that switching
79
- // handles on an idle page doesn't leave stale data from the old handle.
80
- const currentHandleState = ctx.eventController.getHandleState();
81
- const currentCollected = collectHandleData(
82
- handle,
83
- currentHandleState.data,
84
- currentHandleState.segmentOrder,
85
- ) as Rango.FlightSerialize<A>;
86
- const currentValue = selectorRef.current
87
- ? selectorRef.current(currentCollected)
88
- : currentCollected;
89
- if (!shallowEqual(currentValue, prevValueRef.current)) {
90
- prevValueRef.current = currentValue;
91
- setValue(currentValue);
92
- }
93
-
94
- return ctx.eventController.subscribeToHandles(() => {
95
- const state = ctx.eventController.getHandleState();
96
- const isAction =
97
- ctx.eventController.getState().inflightActions.length > 0;
98
- const collected = collectHandleData(
99
- handle,
100
- state.data,
101
- state.segmentOrder,
102
- ) as Rango.FlightSerialize<A>;
103
- const nextValue = selectorRef.current
104
- ? selectorRef.current(collected)
105
- : collected;
106
-
107
- if (!shallowEqual(nextValue, prevValueRef.current)) {
108
- prevValueRef.current = nextValue;
109
- startTransition(() => {
110
- // Skip optimistic update during actions to prevent Suspense fallback
111
- if (!isAction) setOptimisticValue(nextValue);
112
- setValue(nextValue);
113
- });
114
- }
115
- });
60
+ const update = (signal?: RouteStateSignal): void =>
61
+ setValue(collect(), signal);
62
+ // A (possibly new) handle on an idle page reads the current state.
63
+ update();
64
+ return ctx.eventController.subscribeToHandles(update);
116
65
  }, [handle]);
117
66
 
118
- return optimisticValue;
67
+ return value;
68
+ }
69
+
70
+ /**
71
+ * Route state the controller's handle notifications drive (useHandle,
72
+ * useSegments) commits with the page it belongs to. NavigationProvider
73
+ * flushes the controller inside every payload update, so a value known then
74
+ * is set in the page's lane. A page handed over urgently (signal "release")
75
+ * takes a value that arrives before it commits the same way: set at default
76
+ * priority, it renders in the page's lane, and the page's effects read it.
77
+ * A page in a transition (signal "anchor") does not: until it commits
78
+ * (releaseRouteState() from the provider's layout effect, signal "release"),
79
+ * a new value waits, and is applied in the commit's task, right after the
80
+ * page's own commit. In the page's lane, a value that suspends in a revealed
81
+ * <Suspense> (a crumb's content) would hold the page; in a lane of its own,
82
+ * it commits over the page being left and takes the page's transition types
83
+ * (#1165); and a transition entangled with the page misses its commit when it
84
+ * is set after React rendered the page, before its view transition's
85
+ * mutation, and starts a view transition of its own. A value shallow-equal to
86
+ * the last one sets nothing, so a reader renders only when its value moves.
87
+ */
88
+ export function useCommitAnchoredState<T>(
89
+ init: () => T,
90
+ ): [T, (value: T, signal?: RouteStateSignal) => void] {
91
+ // An app that enables the React Compiler compiles the router too; its memo
92
+ // cache gains nothing here and cost 107 B gzip in the router chunk.
93
+ "use no memo";
94
+ const [value, setValue] = useState(init);
95
+ const ref = useRef<{ last: T; held?: boolean; waiting?: boolean }>({
96
+ last: value,
97
+ });
98
+ const [set] = useState(() => (next: T, signal?: RouteStateSignal): void => {
99
+ const r = ref.current;
100
+ if (signal) r.waiting = signal === "anchor";
101
+ if (!shallowEqual(next, r.last)) {
102
+ r.last = next;
103
+ r.held = true;
104
+ }
105
+ // The anchor's own change goes in the page's lane, with the page.
106
+ if (!r.held || (r.waiting && signal !== "anchor")) return;
107
+ r.held = false;
108
+ setValue(() => r.last);
109
+ });
110
+ return [value, set];
119
111
  }
@@ -2,7 +2,9 @@
2
2
 
3
3
  import { useCallback } from "react";
4
4
  import { href, type ValidPaths } from "../../href-client.js";
5
+ import { substitutePatternParams } from "../../router/substitute-pattern-params.js";
5
6
  import { useMount } from "./use-mount.js";
7
+ import { useParams } from "./use-params.js";
6
8
 
7
9
  /**
8
10
  * Client-side hook for mount-aware URL resolution.
@@ -11,6 +13,12 @@ import { useMount } from "./use-mount.js";
11
13
  * include() mount prefix. Inside an include("/shop", ...) scope,
12
14
  * the returned function resolves local paths relative to /shop.
13
15
  *
16
+ * Under a parameterized mount (`include("/:accountId", ...)`) the mount's
17
+ * params are filled from the current route params, as `useReverse` does:
18
+ * `href("/issues")` at `/acme/board` returns `/acme/issues`. A mount param
19
+ * missing from the current match throws `Missing param` when the returned
20
+ * function is called.
21
+ *
14
22
  * For absolute paths (outside the current mount), use the bare
15
23
  * href() function directly instead.
16
24
  *
@@ -37,11 +45,22 @@ import { useMount } from "./use-mount.js";
37
45
  */
38
46
  export function useHref(): (path: `/${string}`) => string {
39
47
  const mount = useMount();
40
- // Memoize on `mount` (stable within a route) so the returned function is
41
- // referentially stable across re-renders — a consumer can safely pass it as a
42
- // dependency or a prop to a memoized child without forcing re-renders.
48
+ // The filled mount (it starts with "/"), or the Missing param message. The
49
+ // selector also runs in the params subscription, so it must not throw, and
50
+ // a string lets useParams skip the re-render, and keeps the returned
51
+ // function stable, unless the mount's own values change.
52
+ const base = useParams((p) => {
53
+ try {
54
+ return substitutePatternParams(mount, p, mount);
55
+ } catch (e) {
56
+ return (e as Error).message;
57
+ }
58
+ });
43
59
  return useCallback(
44
- (path: `/${string}`) => href(path as ValidPaths, mount),
45
- [mount],
60
+ (path: `/${string}`) => {
61
+ if (!base.startsWith("/")) throw new Error(base);
62
+ return href(path as ValidPaths, base);
63
+ },
64
+ [base],
46
65
  );
47
66
  }
@@ -100,7 +100,7 @@ export function useLinkStatus(): LinkStatus {
100
100
  // to a non-idle (loading) state. Used to decide whether to emit a release
101
101
  // update when returning to idle, so the optimistic store doesn't stay pinned
102
102
  // to `true` if a parent transition (e.g. the <Link> click / view transition
103
- // commit) is still pending. Mirrors useNavigation's optimisticPinnedRef.
103
+ // commit) is still pending.
104
104
  const optimisticPinnedRef = useRef(false);
105
105
 
106
106
  const [pending, setOptimisticPending] = useOptimistic(basePending);
@@ -4,7 +4,7 @@ import {
4
4
  useContext,
5
5
  useState,
6
6
  useEffect,
7
- useOptimistic,
7
+ useLayoutEffect,
8
8
  startTransition,
9
9
  useRef,
10
10
  } from "react";
@@ -46,22 +46,19 @@ export function useNavigation<T>(
46
46
  throw new Error("useNavigation must be used within NavigationProvider");
47
47
  }
48
48
 
49
- const [baseValue, setBaseValue] = useState<T | PublicNavigationState>(() => {
49
+ const [value, setValue] = useState<T | PublicNavigationState>(() => {
50
50
  const publicState = toPublicState(
51
51
  ctx.eventController.getHydrationSnapshot()?.state ??
52
52
  ctx.eventController.getState(),
53
53
  );
54
54
  return selector ? selector(publicState) : publicState;
55
55
  });
56
- const prevState = useRef(baseValue);
57
-
58
- // Tracks whether the most recent setOptimisticValue call pinned the value
59
- // to a non-idle state. Used to decide whether to emit a release update when
60
- // returning to idle, so the optimistic store doesn't stay pinned if a
61
- // parent transition (e.g. <Link> click) is still pending.
62
- const optimisticPinnedRef = useRef(false);
63
-
64
- const [value, setOptimisticValue] = useOptimistic(baseValue);
56
+ const prevState = useRef(value);
57
+ // The value on screen: a set value that differs has not committed yet.
58
+ const committedRef = useRef(value);
59
+ useLayoutEffect(() => {
60
+ committedRef.current = value;
61
+ });
65
62
 
66
63
  // Store selector in a ref so the subscription callback always uses the
67
64
  // latest selector without re-subscribing on every render (inline functions
@@ -71,10 +68,28 @@ export function useNavigation<T>(
71
68
  const selectorRef = useRef(selector);
72
69
  selectorRef.current = selector;
73
70
 
74
- // Subscribe to event controller state changes (only runs on client)
71
+ // Subscribe to event controller state changes (only runs on client).
72
+ //
73
+ // The value commits in the lane of the notification that carries it, as
74
+ // the location hooks' values do: NavigationProvider flushes a navigation's
75
+ // notification into the transition that commits its tree, so idle commits
76
+ // with the destination's location (useSearchParams, usePathname,
77
+ // useParams). An optimistic update renders in the sync lane, ahead of that
78
+ // transition, and put idle on screen with the location being left (#1164).
75
79
  useEffect(() => {
80
+ let last: DerivedNavigationState | undefined;
76
81
  const update = () => {
77
82
  const currentState = ctx.eventController.getState();
83
+ // Neither the state nor the location moved (the stream starting or
84
+ // ending): while a value of this hook's is held, a transition, which
85
+ // React entangles with it, so it never commits ahead of the location.
86
+ // Otherwise urgent: a transition of its own would start a view
87
+ // transition, and could commit ahead of a held page and take its
88
+ // transition types.
89
+ const quiet =
90
+ currentState.state === last?.state &&
91
+ currentState.location === last.location;
92
+ last = currentState;
78
93
  const publicState = toPublicState(currentState);
79
94
  const nextSelected = selectorRef.current
80
95
  ? selectorRef.current(publicState)
@@ -82,35 +97,10 @@ export function useNavigation<T>(
82
97
 
83
98
  // Check if selected value has changed
84
99
  if (!shallowEqual(nextSelected, prevState.current)) {
100
+ const held = prevState.current !== committedRef.current;
85
101
  prevState.current = nextSelected;
86
-
87
- // Check if any actions are in progress for optimistic updates
88
- const hasInflightActions =
89
- ctx.eventController.getInflightActions().size > 0;
90
-
91
- const shouldPin = hasInflightActions || publicState.state !== "idle";
92
-
93
- if (shouldPin) {
94
- // Pin the optimistic store so the loading value shows immediately
95
- // even if a parent transition (e.g. <Link> click) defers the
96
- // urgent setBaseValue commit.
97
- startTransition(() => {
98
- setOptimisticValue(nextSelected);
99
- });
100
- optimisticPinnedRef.current = true;
101
- } else if (optimisticPinnedRef.current) {
102
- // Release a previously-pinned optimistic value. Without this,
103
- // useOptimistic keeps returning the stale loading value while
104
- // any parent transition is still pending, even after baseValue
105
- // flipped to idle.
106
- startTransition(() => {
107
- setOptimisticValue(nextSelected);
108
- });
109
- optimisticPinnedRef.current = false;
110
- }
111
-
112
- // Always update base state so UI reflects current state
113
- setBaseValue(nextSelected);
102
+ if (quiet && held) startTransition(() => setValue(nextSelected));
103
+ else setValue(nextSelected);
114
104
  }
115
105
  };
116
106
 
@@ -2,7 +2,10 @@
2
2
 
3
3
  import { useCallback } from "react";
4
4
  import type { LocalReverseFunction } from "../../reverse.js";
5
- import { substitutePatternParams } from "../../router/substitute-pattern-params.js";
5
+ import {
6
+ joinMount,
7
+ substitutePatternParams,
8
+ } from "../../router/substitute-pattern-params.js";
6
9
  import { serializeSearchParams } from "../../search-params.js";
7
10
  import { useMount } from "./use-mount.js";
8
11
  import { useParams } from "./use-params.js";
@@ -15,22 +18,6 @@ function getPattern(entry: RouteEntry | undefined): string | undefined {
15
18
  return typeof entry === "string" ? entry : entry.path;
16
19
  }
17
20
 
18
- /**
19
- * Join an include mount prefix with a mount-relative pattern.
20
- *
21
- * `pattern === "/"` is the index of the local module — under a non-root
22
- * mount it must collapse so `/` under `/blog` becomes `/blog`, not
23
- * `/blog/`. This matches `ctx.reverse(".index")` on the server.
24
- */
25
- function joinMount(mount: string, pattern: string): string {
26
- if (pattern === "/") {
27
- if (mount === "" || mount === "/") return "/";
28
- return mount.endsWith("/") ? mount.slice(0, -1) : mount;
29
- }
30
- const normalizedMount = mount === "/" ? "" : mount.replace(/\/+$/, "");
31
- return normalizedMount + pattern;
32
- }
33
-
34
21
  /**
35
22
  * Mount-aware reverse function for a locally-imported `routes` map.
36
23
  *
@@ -6,6 +6,8 @@ import { useMount } from "./use-mount.js";
6
6
  import { prefetchDirect } from "../prefetch/loader.js";
7
7
  import { getAppVersion } from "../app-version.js";
8
8
  import { prefixBasename } from "../../router/basename.js";
9
+ import { substitutePatternParams } from "../../router/substitute-pattern-params.js";
10
+ import { href, type ValidPaths } from "../../href-client.js";
9
11
  import type { RouterInstance, RouterNavigateOptions } from "../types.js";
10
12
 
11
13
  /**
@@ -22,7 +24,12 @@ import type { RouterInstance, RouterNavigateOptions } from "../types.js";
22
24
  *
23
25
  * RELATIVE paths (no leading `/`, no scheme/query/hash) resolve against the
24
26
  * current include() mount: `router.push("cart")` inside `include("/shop")`
25
- * navigates to `/shop/cart`. Absolute paths stay APP-absolute — unlike
27
+ * navigates to `/shop/cart`. Under a parameterized mount
28
+ * (`include("/:accountId", ...)`) the mount's params are filled from the
29
+ * current route params: `router.push("issues")` at `/acme/board` navigates
30
+ * to `/acme/issues`, and a missing mount param rejects the push/replace
31
+ * promise with `Missing param` (a prefetch skips the target).
32
+ * Absolute paths stay APP-absolute — unlike
26
33
  * basename, the mount is scoped, and absolute pushes legitimately target
27
34
  * outside it, so they are never auto-prefixed.
28
35
  *
@@ -70,16 +77,32 @@ export function useRouter(): RouterInstance {
70
77
  ) {
71
78
  return withBasename(url);
72
79
  }
73
- const base = mount === "/" ? "" : mount.replace(/\/$/, "");
74
- return withBasename(`${base}/${relative}`);
80
+ // The mount is the include()'s URL pattern: fill its params from the
81
+ // committed params at call time (not render time, so this object stays
82
+ // stable across navigations), as useHref does.
83
+ return withBasename(
84
+ href(
85
+ `/${relative}` as ValidPaths,
86
+ substitutePatternParams(
87
+ mount,
88
+ ctx!.eventController.getParams(),
89
+ mount,
90
+ ),
91
+ ),
92
+ );
75
93
  }
76
94
 
95
+ // push/replace are async so a target that cannot resolve (a missing
96
+ // mount param) rejects the returned promise instead of throwing.
77
97
  return {
78
- push(url: string, options?: RouterNavigateOptions): Promise<void> {
98
+ async push(url: string, options?: RouterNavigateOptions): Promise<void> {
79
99
  return ctx.navigate(resolveTarget(url), { ...options, replace: false });
80
100
  },
81
101
 
82
- replace(url: string, options?: RouterNavigateOptions): Promise<void> {
102
+ async replace(
103
+ url: string,
104
+ options?: RouterNavigateOptions,
105
+ ): Promise<void> {
83
106
  return ctx.navigate(resolveTarget(url), { ...options, replace: true });
84
107
  },
85
108
 
@@ -90,8 +113,16 @@ export function useRouter(): RouterInstance {
90
113
  prefetch(url: string, options?: { key?: ":source" }): void {
91
114
  const segmentState = ctx.store?.getSegmentState();
92
115
  if (segmentState) {
116
+ let target: string;
117
+ try {
118
+ target = resolveTarget(url);
119
+ } catch {
120
+ // A prefetch is a hint: a target that cannot resolve (missing
121
+ // mount param) is skipped; push/replace report the error.
122
+ return;
123
+ }
93
124
  prefetchDirect(
94
- resolveTarget(url),
125
+ target,
95
126
  segmentState.currentSegmentIds,
96
127
  getAppVersion(),
97
128
  ctx.store?.getRouterId?.(),
@@ -1,8 +1,9 @@
1
1
  "use client";
2
2
 
3
- import { useContext, useState, useEffect, useRef } from "react";
3
+ import { useContext, useEffect, useRef } from "react";
4
4
  import { NavigationStoreContext } from "./context.js";
5
- import { shallowEqual } from "./shallow-equal.js";
5
+ import { useCommitAnchoredState } from "./use-handle.js";
6
+ import type { RouteStateSignal } from "../event-controller.js";
6
7
 
7
8
  /**
8
9
  * Segments state returned by useSegments hook
@@ -71,7 +72,8 @@ export function useSegments<T>(
71
72
  const selectorRef = useRef(selector);
72
73
  selectorRef.current = selector;
73
74
 
74
- const [state, setState] = useState<T | SegmentsState>(() => {
75
+ // Commits with the page it describes (useCommitAnchoredState).
76
+ const [state, setState] = useCommitAnchoredState<T | SegmentsState>(() => {
75
77
  if (!ctx) {
76
78
  const fallbackLocation = new URL("/", "http://localhost");
77
79
  const fallbackState = buildSegmentsState(fallbackLocation, []);
@@ -88,8 +90,6 @@ export function useSegments<T>(
88
90
  return selector ? selector(segmentsState) : segmentsState;
89
91
  });
90
92
 
91
- const prevState = useRef(state);
92
-
93
93
  useEffect(() => {
94
94
  if (!ctx) {
95
95
  return;
@@ -103,7 +103,7 @@ export function useSegments<T>(
103
103
  state: SegmentsState;
104
104
  } | null = null;
105
105
 
106
- const updateState = () => {
106
+ const updateState = (signal?: RouteStateSignal) => {
107
107
  const location = ctx.eventController.getLocation() as URL;
108
108
  const { routeSegmentIds } = ctx.eventController.getHandleState();
109
109
  if (
@@ -118,17 +118,13 @@ export function useSegments<T>(
118
118
  };
119
119
  }
120
120
  const sel = selectorRef.current;
121
- const nextSelected = sel ? sel(cache.state) : cache.state;
122
- if (!shallowEqual(nextSelected, prevState.current)) {
123
- prevState.current = nextSelected;
124
- setState(nextSelected);
125
- }
121
+ setState(sel ? sel(cache.state) : cache.state, signal);
126
122
  };
127
123
 
128
124
  // Catch up with a change between the seeding render and this effect.
129
125
  updateState();
130
126
 
131
- const unsubscribeNav = ctx.eventController.subscribe(updateState);
127
+ const unsubscribeNav = ctx.eventController.subscribe(() => updateState());
132
128
  const unsubscribeHandles =
133
129
  ctx.eventController.subscribeToHandles(updateState);
134
130
 
@@ -1,4 +1,5 @@
1
1
  import { validateRedirectOrigin } from "./validate-redirect-origin.js";
2
+ import { PREFETCH_FAILED_ROW } from "../prefetch-markers.js";
2
3
 
3
4
  type HeaderResult = { url: string } | "blocked" | null;
4
5
 
@@ -105,9 +106,11 @@ export function handleReloadHeader(
105
106
  * end-of-stream, calling onComplete when done.
106
107
  *
107
108
  * `onComplete` receives `endedCleanly`: true only on a normal EOF drain, false
108
- * on a read error or an abort (the signal cancelled the reader). Callers that
109
- * gate a "fully complete" fast path (e.g. prefetch's `entry.complete`) must
110
- * treat a non-clean end as incomplete — a broken stream is not complete data.
109
+ * on a read error, an abort (the signal cancelled the reader), or a body that
110
+ * ends with PREFETCH_FAILED_ROW (a prefetch whose render reported an error,
111
+ * rsc/prefetch-render-error.ts). Callers that gate a "fully complete" fast
112
+ * path (e.g. prefetch's `entry.complete`) must treat a non-clean end as
113
+ * incomplete — a broken stream is not complete data.
111
114
  *
112
115
  * If the response has no body, onComplete fires synchronously with `true`
113
116
  * (an empty body is a clean, complete stream).
@@ -156,13 +159,16 @@ export function teeWithCompletion(
156
159
  // loop breaks here normally, NOT in the .catch — re-check the signal to
157
160
  // catch that case).
158
161
  let cleanEof = false;
162
+ // The last five bytes read, across chunk boundaries.
163
+ let tail = "";
159
164
  try {
160
165
  while (true) {
161
- const { done } = await reader.read();
166
+ const { done, value } = await reader.read();
162
167
  if (done) {
163
- cleanEof = true;
168
+ cleanEof = tail !== PREFETCH_FAILED_ROW;
164
169
  break;
165
170
  }
171
+ tail = (tail + String.fromCharCode(...value.slice(-5))).slice(-5);
166
172
  }
167
173
  } finally {
168
174
  if (onAbort) signal!.removeEventListener("abort", onAbort);
@@ -37,6 +37,7 @@ import {
37
37
  setPrefetchConcurrency,
38
38
  setPrefetchDecoder,
39
39
  } from "./prefetch/loader.js";
40
+ import { publishRangoMarker } from "./rango-marker.js";
40
41
  import { setAppVersion } from "./app-version.js";
41
42
  import {
42
43
  isInterceptSegment,
@@ -185,6 +186,7 @@ export async function initBrowserApp(
185
186
  } = options;
186
187
 
187
188
  bootLog("initBrowserApp start");
189
+ publishRangoMarker();
188
190
  // Before any location read (the history key, the navigation store, the
189
191
  // event controller): the server rendered the marked request for the clean
190
192
  // URL, so the client starts from the same one and hydration agrees.
@@ -248,7 +250,7 @@ export async function initBrowserApp(
248
250
 
249
251
  // The document's entry: the transition({ when }) source of the first
250
252
  // navigation, and of a later back/forward that leaves it.
251
- store.rememberDisplayedEntry(initialPayload.metadata?.routeName);
253
+ store.rememberDisplayedEntry(initialPayload.metadata);
252
254
 
253
255
  // Register the active store on the module-level handle and wire the
254
256
  // jar-divergence observer before any getRangoState() read can detect a