@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
@@ -8,8 +8,9 @@ import type { ReactNode } from "react";
8
8
  import * as React from "react";
9
9
  import { startTransition } from "react";
10
10
 
11
- // addTransitionType is only available in React experimental
12
- const addTransitionType: ((type: string) => void) | undefined =
11
+ // React 19.3+ and experimental builds. Call it only inside startTransition:
12
+ // outside one, React starts a transition of its own.
13
+ export const addTransitionType: ((type: string) => void) | undefined =
13
14
  "addTransitionType" in React ? (React as any).addTransitionType : undefined;
14
15
 
15
16
  import type { RenderSegmentsOptions } from "../segment-system.js";
@@ -32,7 +33,12 @@ import {
32
33
  IS_BROWSER_DEBUG,
33
34
  } from "./logging.js";
34
35
  import { validatePayloadRedirect } from "./validate-redirect-origin.js";
35
- import type { HandleData, NavigationUpdate, RscMetadata } from "./types.js";
36
+ import type {
37
+ HandleData,
38
+ NavigationUpdate,
39
+ RscMetadata,
40
+ ShownEntry,
41
+ } from "./types.js";
36
42
  import { OPTIMISTIC_COMMIT_TRANSITION_TYPE } from "./optimistic-commit.js";
37
43
  import type { LoaderStore } from "../loader-store.js";
38
44
  import { setActiveInterceptTargets } from "../client-urls/navigation.js";
@@ -45,6 +51,7 @@ import { buildHistoryState } from "./history-state.js";
45
51
  import { addLocationState } from "./react/location-state-shared.js";
46
52
  import { isPlaceholder, type TransitionWhenKind } from "../types/segments.js";
47
53
  import { auditTreeCause, type TreeUpdateCause } from "../suspense-audit.js";
54
+ import { revalidationErrorOf } from "../decode-loader-results.js";
48
55
 
49
56
  /**
50
57
  * Hold/animate decision for a commit: a non-intercept segment carries a
@@ -79,19 +86,24 @@ function treeCause(mode: UpdateMode, traversal?: boolean): TreeUpdateCause {
79
86
  * settled/cached segments make it a no-op. Urgent commits call onUpdate
80
87
  * directly: nothing is held there, a reader of a pending stream suspends to
81
88
  * its fallback. Also used by renderRoute's navigate() (testing/render-route.tsx).
89
+ * Every commit is tagged with its transition({ when }) `kind` (#1165), then
90
+ * `transitionTypes`.
82
91
  */
83
92
  export function commitInTransition(
84
93
  loaders: LoaderStore,
85
94
  onUpdate: UpdateSubscriber,
86
95
  segments: readonly ResolvedSegment[],
87
96
  update: NavigationUpdate,
97
+ kind: TransitionWhenKind,
88
98
  transitionTypes: readonly string[],
89
99
  ): void {
90
100
  startTransition(() => {
91
101
  loaders.announcePendingStreams(segments);
92
102
  if (addTransitionType) {
103
+ addTransitionType(kind);
93
104
  for (const type of transitionTypes) addTransitionType(type);
94
105
  }
106
+ update.inTransition = true;
95
107
  onUpdate(update);
96
108
  });
97
109
  }
@@ -124,13 +136,15 @@ export interface PartialUpdateConfig {
124
136
  /** What the response's handlers pushed, to the handle store. */
125
137
  handles(metadata: RscMetadata): void;
126
138
  /**
127
- * Deliver the navigation state to its readers now. A page built with
128
- * holes commits in the task of its click, before the notification of the
129
- * click has gone out (event-controller.ts makeDebouncedNotifier), and
130
- * React holds that commit where a hole is read with no boundary: without
131
- * this useNavigation() reads idle, on the page being left, for the whole
132
- * wait. Delivered first, `loading` is on screen when the commit is
133
- * handed over, and stays while React holds it, as for a plain click.
139
+ * Deliver the navigation state to its readers now, and commit it. A page
140
+ * built with holes commits in the task of its click, before the
141
+ * notification of the click has gone out (event-controller.ts
142
+ * makeDebouncedNotifier), and React holds that commit where a hole is
143
+ * read with no boundary: without this useNavigation() reads idle, on the
144
+ * page being left, for the whole wait. Committed first, `loading` is on
145
+ * screen when the commit is handed over, and stays while React holds it,
146
+ * as for a plain click. Delivered at default priority it joins the
147
+ * commit's urgent batch, which brings idle with the URL, and never shows.
134
148
  */
135
149
  flush(): void;
136
150
  };
@@ -185,8 +199,8 @@ export interface CommitOverrides {
185
199
  interceptSourceUrl?: string;
186
200
  /** Server-set location state to merge into history.pushState */
187
201
  serverState?: Record<string, unknown>;
188
- /** The committed route's name (payload metadata), remembered per history entry. */
189
- routeName?: string;
202
+ /** The payload's metadata: its route name and rewrite target, remembered per history entry. */
203
+ metadata?: ShownEntry;
190
204
  }
191
205
 
192
206
  /**
@@ -233,7 +247,9 @@ export type UpdateMode =
233
247
  };
234
248
 
235
249
  /**
236
- * Type for the fetchPartialUpdate function
250
+ * Type for the fetchPartialUpdate function. In `action` mode it resolves with
251
+ * the error of a loader that threw while it re-ran (revalidationErrorOf);
252
+ * the loader throws at its read.
237
253
  */
238
254
  export type PartialUpdater = (
239
255
  targetUrl: string,
@@ -242,7 +258,7 @@ export type PartialUpdater = (
242
258
  signal: AbortSignal | undefined,
243
259
  tx: BoundTransaction,
244
260
  mode?: UpdateMode,
245
- ) => Promise<void>;
261
+ ) => Promise<Error | undefined>;
246
262
 
247
263
  export function createPartialUpdater(
248
264
  config: PartialUpdateConfig,
@@ -545,7 +561,7 @@ export function createPartialUpdater(
545
561
  signal: AbortSignal | undefined,
546
562
  tx: BoundTransaction,
547
563
  mode: UpdateMode = { type: "navigate" },
548
- ): Promise<void> {
564
+ ): Promise<Error | undefined> {
549
565
  const segmentState = store.getSegmentState();
550
566
  const url = targetUrl || window.location.href;
551
567
 
@@ -666,6 +682,11 @@ export function createPartialUpdater(
666
682
  targetUrl: url,
667
683
  segmentIds: segments,
668
684
  previousUrl,
685
+ // The entry being left (a popstate has not moved it yet), unless
686
+ // previousUrl names an intercept's source.
687
+ rewriteTarget: interceptSourceUrl
688
+ ? undefined
689
+ : store.getHistoryEntryMemory()?.rewriteTarget,
669
690
  staleRevalidation:
670
691
  mode.type === "stale-revalidation" || segments.length === 0,
671
692
  version: getVersion(),
@@ -755,7 +776,7 @@ export function createPartialUpdater(
755
776
  const { scroll: commitScroll } = tx.commit(
756
777
  matchedIds,
757
778
  existingSegments,
758
- { routeName: payload.metadata.routeName },
779
+ { metadata: payload.metadata },
759
780
  );
760
781
  if (cachedHandleData) {
761
782
  store.updateCacheHandleData(
@@ -781,6 +802,7 @@ export function createPartialUpdater(
781
802
  onUpdate,
782
803
  existingSegments,
783
804
  cachedUpdate,
805
+ whenKind,
784
806
  ["navigation"],
785
807
  );
786
808
  } else {
@@ -804,7 +826,7 @@ export function createPartialUpdater(
804
826
  const { scroll: leaveScroll } = tx.commit(
805
827
  matchedIds,
806
828
  existingSegments,
807
- { routeName: payload.metadata.routeName },
829
+ { metadata: payload.metadata },
808
830
  );
809
831
 
810
832
  if (process.env.NODE_ENV !== "production") {
@@ -824,7 +846,7 @@ export function createPartialUpdater(
824
846
  "[Browser] No changes - all revalidations returned false, keeping existing UI",
825
847
  );
826
848
  tx.commit(matchedIds, existingSegments, {
827
- routeName: payload.metadata.routeName,
849
+ metadata: payload.metadata,
828
850
  treeless: true,
829
851
  });
830
852
  debugLog("[Browser] Navigation complete (no re-render)");
@@ -910,6 +932,13 @@ export function createPartialUpdater(
910
932
  // prefetch is decided against the real source.
911
933
  const gatedOff = decideGatedOff(reconciled.segments, payload.metadata);
912
934
 
935
+ const isInterceptResponse = hasActiveInterceptSlots(
936
+ payload.metadata?.slots,
937
+ );
938
+ // The source the commit records for an intercept, and its fill's.
939
+ const effectiveInterceptSource =
940
+ interceptSourceUrl || segmentState.currentUrl;
941
+
913
942
  // The fill: one request for what the prefetch deferred, listing what
914
943
  // the client holds once this payload commits. It leaves now, before
915
944
  // the tree is rendered, with this navigation's signal.
@@ -921,8 +950,15 @@ export function createPartialUpdater(
921
950
  answer: client.fetchPartial({
922
951
  targetUrl: url,
923
952
  segmentIds: heldIds,
924
- // The page the fill completes is the page it is on.
925
- previousUrl: url,
953
+ // The page the fill completes is the page it is on: from the
954
+ // target itself nothing intercepts, so an intercept's fill names
955
+ // its source, as a request from the open modal does.
956
+ previousUrl: isInterceptResponse ? effectiveInterceptSource : url,
957
+ ...(isInterceptResponse && {
958
+ interceptSourceUrl: effectiveInterceptSource,
959
+ }),
960
+ // The server ignores it beside an intercept source.
961
+ rewriteTarget: payload.metadata?.rewriteTarget,
926
962
  fill: true,
927
963
  signal,
928
964
  version: getVersion(),
@@ -1029,15 +1065,19 @@ export function createPartialUpdater(
1029
1065
  newTree = await renderSegments(reconciled.mainSegments, renderOptions);
1030
1066
  }
1031
1067
 
1068
+ // A background refresh keeps the stale page on screen when a loader it
1069
+ // re-ran threw (its entries settled in the build): nothing is committed,
1070
+ // and navigation-bridge logs the rejection (dc-t65). Every other lane
1071
+ // throws it at the read. Before the abort check: the await yields.
1072
+ if (mode.type === "stale-revalidation") {
1073
+ const failed = await revalidationErrorOf(serverSegments);
1074
+ if (failed) throw failed;
1075
+ }
1032
1076
  if (signal?.aborted) {
1033
1077
  debugLog("[Browser] Ignoring stale navigation (aborted before commit)");
1034
1078
  return;
1035
1079
  }
1036
1080
 
1037
- const isInterceptResponse = hasActiveInterceptSlots(
1038
- payload.metadata?.slots,
1039
- );
1040
-
1041
1081
  // Same-structure revalidation: every segment this commit will render
1042
1082
  // already exists on screen (by id) — the navigation mounts nothing new,
1043
1083
  // it only refreshes what the user is looking at. The hold this enables
@@ -1053,8 +1093,6 @@ export function createPartialUpdater(
1053
1093
  const isSameStructureNav =
1054
1094
  mode.type === "navigate" && holdsAll(reconciled.segments);
1055
1095
 
1056
- const effectiveInterceptSource =
1057
- interceptSourceUrl || segmentState.currentUrl;
1058
1096
  if (mode.type !== "action" && mode.type !== "stale-revalidation") {
1059
1097
  if (isInterceptResponse) {
1060
1098
  store.setInterceptSourceUrl(effectiveInterceptSource);
@@ -1065,7 +1103,7 @@ export function createPartialUpdater(
1065
1103
 
1066
1104
  const serverLocationState = payload.metadata?.locationState;
1067
1105
  const overrides: CommitOverrides = {
1068
- routeName: payload.metadata?.routeName,
1106
+ metadata: payload.metadata,
1069
1107
  ...(isInterceptResponse && {
1070
1108
  scroll: false,
1071
1109
  intercept: true,
@@ -1148,6 +1186,7 @@ export function createPartialUpdater(
1148
1186
  onUpdate,
1149
1187
  reconciled.mainSegments,
1150
1188
  update,
1189
+ whenKind,
1151
1190
  hasTransition ? ["action"] : [],
1152
1191
  );
1153
1192
  } else if (hasTransition) {
@@ -1156,6 +1195,7 @@ export function createPartialUpdater(
1156
1195
  onUpdate,
1157
1196
  reconciled.mainSegments,
1158
1197
  update,
1198
+ whenKind,
1159
1199
  optimisticPresented
1160
1200
  ? ["navigation", OPTIMISTIC_COMMIT_TRANSITION_TYPE]
1161
1201
  : ["navigation"],
@@ -1181,7 +1221,7 @@ export function createPartialUpdater(
1181
1221
  // flash). Inside a transition React holds the current UI until the
1182
1222
  // suspended data lands, the action treatment; nothing new mounts (by
1183
1223
  // the isSameStructureNav definition), so no fallback is being withheld
1184
- // from a first paint. No addTransitionType: this is the React
1224
+ // from a first paint. Its kind is its only type (#1165): a React
1185
1225
  // content-hold, not a view transition. Deliberate trade-off (#622
1186
1226
  // introduced, #624 reverted, then reinstated): a client component that
1187
1227
  // suspends during its FIRST render (use() of a promise created at
@@ -1199,6 +1239,7 @@ export function createPartialUpdater(
1199
1239
  onUpdate,
1200
1240
  reconciled.mainSegments,
1201
1241
  update,
1242
+ whenKind,
1202
1243
  optimisticPresented ? [OPTIMISTIC_COMMIT_TRANSITION_TYPE] : [],
1203
1244
  );
1204
1245
  } else {
@@ -1223,6 +1264,7 @@ export function createPartialUpdater(
1223
1264
  if (holes) await fillHoles(holes, signal, url, gatedOff);
1224
1265
 
1225
1266
  debugLog("[Browser] Navigation complete");
1267
+ if (mode.type === "action") return revalidationErrorOf(serverSegments);
1226
1268
  return;
1227
1269
  } else {
1228
1270
  console.warn(`[Browser] Full update (fallback)`);
@@ -1235,6 +1277,12 @@ export function createPartialUpdater(
1235
1277
  transitionGatedOff: fullGatedOff,
1236
1278
  });
1237
1279
 
1280
+ // A background refresh keeps the page, as in the partial branch: its
1281
+ // loaders streamed, so this waits for them before anything commits.
1282
+ if (mode.type === "stale-revalidation") {
1283
+ const failed = await revalidationErrorOf(segments);
1284
+ if (failed) throw failed;
1285
+ }
1238
1286
  if (signal?.aborted) {
1239
1287
  debugLog("[Browser] Ignoring stale navigation (aborted before commit)");
1240
1288
  return;
@@ -1242,7 +1290,7 @@ export function createPartialUpdater(
1242
1290
 
1243
1291
  const fullUpdateServerState = payload.metadata?.locationState;
1244
1292
  const { scroll: fullScroll } = tx.commit(segmentIds, segments, {
1245
- routeName: payload.metadata?.routeName,
1293
+ metadata: payload.metadata,
1246
1294
  ...(fullUpdateServerState && { serverState: fullUpdateServerState }),
1247
1295
  });
1248
1296
 
@@ -1283,6 +1331,7 @@ export function createPartialUpdater(
1283
1331
  onUpdate,
1284
1332
  segments,
1285
1333
  fullUpdate,
1334
+ whenKind,
1286
1335
  fullHasTransition ? ["action"] : [],
1287
1336
  );
1288
1337
  }
@@ -1292,16 +1341,25 @@ export function createPartialUpdater(
1292
1341
  onUpdate,
1293
1342
  segments,
1294
1343
  fullUpdate,
1344
+ whenKind,
1295
1345
  fullHasTransition ? ["action"] : [],
1296
1346
  );
1297
1347
  } else if (fullHasTransition) {
1298
- commitInTransition(store.loaders, onUpdate, segments, fullUpdate, [
1299
- "navigation",
1300
- ]);
1348
+ commitInTransition(
1349
+ store.loaders,
1350
+ onUpdate,
1351
+ segments,
1352
+ fullUpdate,
1353
+ whenKind,
1354
+ ["navigation"],
1355
+ );
1301
1356
  } else {
1302
1357
  onUpdate(fullUpdate);
1303
1358
  }
1304
1359
 
1360
+ // This render streamed its loaders (no isAction); the pending action
1361
+ // call waits for them only to report, as the partial lane's commit does.
1362
+ if (mode.type === "action") return revalidationErrorOf(segments);
1305
1363
  return;
1306
1364
  }
1307
1365
  }
@@ -91,6 +91,14 @@ export interface DecodedPrefetch {
91
91
  * one prefetch serves unlimited adoptions within TTL/state validity.
92
92
  */
93
93
  respawn?: () => DecodedPrefetch;
94
+ /**
95
+ * The prefetch's stall timer (executePrefetchFetch). Navigation clears it on
96
+ * the entry it serves: from then on the click owns the stream, which runs to
97
+ * its end (a Flight response that arrived is never aborted mid-stream). A
98
+ * prefetch nobody adopted keeps it, so a stalled one is still aborted.
99
+ * Absent on a respawned entry, which has no timer.
100
+ */
101
+ stallTimer?: ReturnType<typeof setTimeout>;
94
102
  /**
95
103
  * Release the buffered reserve bytes. Called when the entry is evicted
96
104
  * without being consumed (expiry, FIFO, sweep, cache clear) so the tee
@@ -37,6 +37,7 @@ import { teeWithCompletion, isForeignRouterId } from "../response-adapter.js";
37
37
  import { SEGMENT_FRAGMENT_CAPABILITY_HEADER } from "../../segment-fragments.js";
38
38
  import { PREFETCH_HEADER } from "../../prefetch-markers.js";
39
39
  import type { RscPayload } from "../types.js";
40
+ import { getRegisteredStore } from "../navigation-store-handle.js";
40
41
 
41
42
  /**
42
43
  * Decoder injected at app startup (see setPrefetchDecoder). This is
@@ -162,6 +163,13 @@ function buildPrefetchUrl(
162
163
  if (routerId) {
163
164
  targetUrl.searchParams.set("_rsc_rid", routerId);
164
165
  }
166
+ // The page's rewrite target, so the server diffs against the route that
167
+ // rendered it, as for the navigation.
168
+ const rewriteTarget =
169
+ getRegisteredStore()?.getHistoryEntryMemory()?.rewriteTarget;
170
+ if (rewriteTarget) {
171
+ targetUrl.searchParams.set("_rsc_rt", rewriteTarget);
172
+ }
165
173
  return targetUrl;
166
174
  }
167
175
 
@@ -218,8 +226,9 @@ function executePrefetchFetch(
218
226
  // hover/direct path (no caller signal) and the queue-driven path (whose caller
219
227
  // signal only aborts on navigation, never on a stall). On fire it aborts the
220
228
  // fetch/stream and evicts the published entry if one exists; it is NOT cleared
221
- // when headers arrive (see below) — it is cleared when the stream completes, or
222
- // in `.finally()` for paths that publish no streaming entry.
229
+ // when headers arrive (see below) — it is cleared when the stream completes,
230
+ // when a navigation serves the entry (DecodedPrefetch.stallTimer), or in
231
+ // `.finally()` for paths that publish no streaming entry.
223
232
  let publishedKey: string | undefined;
224
233
  let publishedEntry: DecodedPrefetch | undefined;
225
234
  const timeoutController = new AbortController();
@@ -341,6 +350,7 @@ function executePrefetchFetch(
341
350
  streamComplete,
342
351
  scope,
343
352
  complete: false,
353
+ stallTimer: timeoutId,
344
354
  ...(reserve && {
345
355
  dispose: () => {
346
356
  reserve.cancel().catch(() => {});
@@ -369,6 +379,7 @@ function executePrefetchFetch(
369
379
  // reserve would replay broken bytes, so it stays one-shot (and its
370
380
  // buffered reserve is released via the removePrefetch dispose above).
371
381
  Promise.allSettled([payload, streamComplete]).then(([decode]) => {
382
+ clearTimeout(timeoutId);
372
383
  if (decode.status === "fulfilled" && endedCleanly) {
373
384
  entry.complete = true;
374
385
  if (reserve) {
@@ -388,7 +399,6 @@ function executePrefetchFetch(
388
399
  }
389
400
  }
390
401
  }
391
- clearTimeout(timeoutId);
392
402
  });
393
403
  return entry;
394
404
  })
@@ -0,0 +1,13 @@
1
+ import { PACKAGE_VERSION } from "@rangojs/router:version";
2
+
3
+ /**
4
+ * Publishes `window.__rango = { version }` so outside tools can tell a page is
5
+ * a Rango app and which router release serves it. Write-only and informational:
6
+ * nothing in the router reads it (rango-marker.test.ts greps src/ for reads),
7
+ * and only the npm semver is exposed, no build hash, router id or config.
8
+ */
9
+ export function publishRangoMarker(): void {
10
+ (window as { __rango?: unknown }).__rango = Object.freeze({
11
+ version: PACKAGE_VERSION,
12
+ });
13
+ }
@@ -505,9 +505,16 @@ export function NavigationProvider({
505
505
 
506
506
  // The last navigation's scroll decision, until React commits its update.
507
507
  const pendingScrollRef = useRef<NavigationUpdate["scroll"]>(undefined);
508
+ // The last payload handed to React, until it commits: route state that
509
+ // arrived behind it waits for that commit (flushRouteState("anchor")).
510
+ const committingRef = useRef<typeof payload | undefined>(undefined);
508
511
 
509
512
  // Apply scroll after React commits the new content to the DOM
510
513
  useLayoutEffect(() => {
514
+ if (payload === committingRef.current) {
515
+ committingRef.current = undefined;
516
+ eventController.releaseRouteState();
517
+ }
511
518
  const scrollAction = pendingScrollRef.current;
512
519
  if (!scrollAction) return;
513
520
  pendingScrollRef.current = undefined;
@@ -518,6 +525,7 @@ export function NavigationProvider({
518
525
  restore: scrollAction.restore,
519
526
  scroll: scrollAction.enabled,
520
527
  isStreaming: scrollAction.isStreaming,
528
+ committed: true,
521
529
  });
522
530
  });
523
531
 
@@ -544,7 +552,9 @@ export function NavigationProvider({
544
552
  eventController.releaseHydrationLock();
545
553
  }
546
554
 
547
- setPayload({ root: update.root, metadata: update.metadata });
555
+ const next = { root: update.root, metadata: update.metadata };
556
+ committingRef.current = next;
557
+ setPayload(next);
548
558
  // The state of the entry this payload renders, as its commit recorded
549
559
  // it: one update, so one lane, so one React commit with the tree.
550
560
  setLocationState(eventController.getLocationState());
@@ -556,9 +566,23 @@ export function NavigationProvider({
556
566
  eventController.setParams(update.metadata.params);
557
567
  }
558
568
 
569
+ // tx.commit() and the metadata updates above mutate the controller
570
+ // synchronously, but its ordinary notifications are task-debounced. Flush
571
+ // them here so hook setState calls inherit this payload update's lane. In
572
+ // a transition that suspends, the source tree therefore keeps its source
573
+ // pathname/params until the destination payload commits with them.
574
+ // A page in a transition: route state that arrives from here waits for
575
+ // this payload's commit (useCommitAnchoredState). An urgent page: it is
576
+ // set at default priority, in the page's lane, and commits with it.
577
+ eventController.flushRouteState(
578
+ update.inTransition ? "anchor" : "release",
579
+ );
580
+
559
581
  // A page rendered from a history-cache entry (back/forward, or a link
560
582
  // to it) starts from the entry's pushes, and its response's handles
561
583
  // merge onto them: the page on screen's pushes are not its segments'.
584
+ // After the flush: a crumb whose content is still pending would hold
585
+ // the page in its transition; it waits and lands in the page's task.
562
586
  const { matched, cachedHandleData } = update.metadata;
563
587
  if (matched && cachedHandleData) {
564
588
  eventController.setHandleData(cachedHandleData, matched);
@@ -575,12 +599,6 @@ export function NavigationProvider({
575
599
  update.metadata.resolvedIds,
576
600
  );
577
601
  }
578
-
579
- // tx.commit() and the metadata updates above mutate the controller
580
- // synchronously, but its ordinary notifications are task-debounced. Flush
581
- // them here so hook setState calls inherit this payload update's lane. In
582
- // a transition that suspends, the source tree therefore keeps its source
583
- // pathname/params until the destination payload commits with them.
584
602
  eventController.flushRouteState();
585
603
  });
586
604
 
@@ -9,6 +9,19 @@ export function isLoaderSegmentId(id: string): boolean {
9
9
  return LOADER_SEGMENT_ID.test(id);
10
10
  }
11
11
 
12
+ /**
13
+ * The parent of a loader sub-id (`${parentId}D${index}.${loaderId}`); any
14
+ * other id unchanged. parentId is a shortCode (never "D"), or
15
+ * `${shortCode}.${slotName}` for an intercept slot's loaders, where the
16
+ * user-chosen name may hold an uppercase "D" ("@Detail"). Strip from the first
17
+ * `D<index>.`: splitting on a bare "D" mis-cut "@Detail" to "@" and silently
18
+ * dropped the loader's data. Correct only because slot names cannot contain
19
+ * "." (assertValidSlotName, route-definition/dsl-helpers.ts).
20
+ */
21
+ export function loaderParentId(id: string): string {
22
+ return id.replace(/D\d+\..*$/, "");
23
+ }
24
+
12
25
  /**
13
26
  * Build the handle-collection segment order from a raw `matched` list.
14
27
  *
@@ -11,19 +11,9 @@ import {
11
11
  import { NavigationStoreContext } from "./context.js";
12
12
  import { shallowEqual } from "./shallow-equal.js";
13
13
  import type { TrackedActionState } from "../types.js";
14
+ import { DEFAULT_ACTION_STATE } from "../event-controller.js";
14
15
  import { invariant } from "../../errors.js";
15
16
 
16
- /**
17
- * Default action state (idle with no payload)
18
- */
19
- const DEFAULT_ACTION_STATE: TrackedActionState = {
20
- state: "idle",
21
- actionId: null,
22
- payload: null,
23
- error: null,
24
- result: null,
25
- };
26
-
27
17
  /**
28
18
  * Extract action ID from a server action function or string.
29
19
  *
@@ -49,10 +39,13 @@ function getActionId(action: ServerActionFunction | string): string {
49
39
  return action;
50
40
  }
51
41
 
52
- // If we get here, this is likely an action passed as prop from a server component
53
- // These lose their metadata during RSC serialization
42
+ // If we get here, this is likely an action passed as prop from a server
43
+ // component: these lose their metadata during RSC serialization. The
44
+ // guidance is dev-only: it was about 225 B gzip of every client build that
45
+ // calls useAction.
54
46
  throw new Error(
55
- `useAction: Cannot extract action ID from function reference.
47
+ process.env.NODE_ENV !== "production"
48
+ ? `useAction: Cannot extract action ID from function reference.
56
49
 
57
50
  This typically happens when an action is passed as a prop from a server component.
58
51
  Actions passed through RSC lose their metadata during serialization.
@@ -65,7 +58,8 @@ Solutions:
65
58
  2. Use the action name as a string:
66
59
  const state = useAction("myAction");
67
60
 
68
- The string must match the exported function name from your "use server" file.`,
61
+ The string must match the exported function name from your "use server" file.`
62
+ : "useAction: Cannot extract action ID from function reference.",
69
63
  );
70
64
  }
71
65