@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
@@ -75,7 +75,8 @@ let customGetKey: GetScrollKeyFunction | null = null;
75
75
  /**
76
76
  * Scroll key of the entry on screen. At popstate history.state already belongs
77
77
  * to the destination, so this is the only way to save the page being left
78
- * under its own key (see handleTraversalStart).
78
+ * under its own key (see handleTraversalStart). A traversal moves it when its
79
+ * restore commits (handleNavigationEnd), not at its event.
79
80
  */
80
81
  let displayedKey: string | null = null;
81
82
 
@@ -283,10 +284,12 @@ export function cancelScrollRestorationPolling(): void {
283
284
  *
284
285
  * @param options.retryIfStreaming - If true, poll while streaming until we can scroll to target
285
286
  * @param options.isStreaming - Function to check if streaming is in progress
287
+ * @param options.committed - React has committed the restored tree (a layout effect)
286
288
  */
287
289
  export function restoreScrollPosition(options?: {
288
290
  retryIfStreaming?: boolean;
289
291
  isStreaming?: () => boolean;
292
+ committed?: boolean;
290
293
  }): boolean {
291
294
  // Clear any pending polling
292
295
  cancelScrollRestorationPolling();
@@ -298,6 +301,11 @@ export function restoreScrollPosition(options?: {
298
301
  return false;
299
302
  }
300
303
 
304
+ // Once React committed the restored tree, scroll now, so a view
305
+ // transition's new snapshot is taken at the restored position, while the
306
+ // page still streams too.
307
+ if (options?.committed) window.scrollTo(0, savedY);
308
+
301
309
  // If streaming, poll until streaming ends then scroll to saved position
302
310
  if (options?.retryIfStreaming && options?.isStreaming?.()) {
303
311
  const startTime = Date.now();
@@ -319,9 +327,8 @@ export function restoreScrollPosition(options?: {
319
327
  return true;
320
328
  }
321
329
 
322
- // Not streaming — scroll after React commits and browser paints.
323
- // startTransition defers the DOM commit, so scrolling synchronously
324
- // would be overwritten when React replaces the content.
330
+ // Not streaming: scroll (again) on the next frame, for layout that settles
331
+ // after the commit and for a caller that ran before it.
325
332
  deferToNextPaint(() => {
326
333
  window.scrollTo(0, savedY);
327
334
  debugLog("[Scroll] Restored position:", savedY, "for key:", key);
@@ -374,18 +381,18 @@ export function handleNavigationStart(): void {
374
381
 
375
382
  /**
376
383
  * Handle scroll for a back/forward traversal, at popstate before anything
377
- * renders: save the page being left under ITS key (history has already moved,
378
- * so saveCurrentScrollPosition would use the destination's key) and track the
379
- * destination as the displayed entry.
384
+ * renders: save the page on screen under ITS key (history has already moved,
385
+ * so saveCurrentScrollPosition would use the destination's key). A second
386
+ * traversal before the first one's restore commits (a rapid double Back, a
387
+ * deferred animated restore) saves the page still on screen, never an entry
388
+ * it did not show.
380
389
  */
381
390
  export function handleTraversalStart(): void {
382
391
  if (!initialized) return;
383
- const leftKey = displayedKey;
384
- trackDisplayedEntry();
385
392
  // A key shared with the destination (a getKey that collapses entries, e.g.
386
393
  // by pathname) keeps the destination's saved position.
387
- if (leftKey !== null && leftKey !== displayedKey) {
388
- saveScrollPosition(leftKey, window.scrollY);
394
+ if (displayedKey !== null && displayedKey !== getScrollKey()) {
395
+ saveScrollPosition(displayedKey, window.scrollY);
389
396
  }
390
397
  }
391
398
 
@@ -394,13 +401,15 @@ export function handleTraversalStart(): void {
394
401
  * @param options.restore - If true, restore saved position (for popstate)
395
402
  * @param options.scroll - If false, don't scroll at all
396
403
  * @param options.isStreaming - Function to check if streaming is in progress (for retry logic)
404
+ * @param options.committed - Called after React committed the new tree
397
405
  */
398
406
  export function handleNavigationEnd(options: {
399
407
  restore?: boolean;
400
408
  scroll?: boolean;
401
409
  isStreaming?: () => boolean;
410
+ committed?: boolean;
402
411
  }): void {
403
- const { restore = false, scroll = true, isStreaming } = options;
412
+ const { restore = false, scroll = true, isStreaming, committed } = options;
404
413
 
405
414
  // Don't scroll if explicitly disabled or not in a browser
406
415
  if (scroll === false || typeof window === "undefined") {
@@ -412,11 +421,17 @@ export function handleNavigationEnd(options: {
412
421
  // matters during cross-app navigation where ScrollRestoration unmounts
413
422
  // and remounts, creating a brief window where initialized is false.
414
423
  if (restore) {
415
- if (
416
- initialized &&
417
- restoreScrollPosition({ retryIfStreaming: true, isStreaming })
418
- ) {
419
- return;
424
+ if (initialized) {
425
+ trackDisplayedEntry();
426
+ if (
427
+ restoreScrollPosition({
428
+ retryIfStreaming: true,
429
+ isStreaming,
430
+ committed,
431
+ })
432
+ ) {
433
+ return;
434
+ }
420
435
  }
421
436
  // An "auto" entry (no <Html.ScrollRestoration>, or created before it mounted)
422
437
  // is restored by the browser. A scrollTo here races that restore, and
@@ -438,9 +453,8 @@ export function handleNavigationEnd(options: {
438
453
  // against unmeasured layout, so sync scroll is correct here even
439
454
  // before the new tree's scrollHeight settles.
440
455
  //
441
- // (The restore branch above keeps deferToNextPaint because savedY
442
- // depends on the new tree's max scroll; sync scrollTo against an
443
- // unmeasured DOM would clamp savedY to whatever the old/zero max was.)
456
+ // (The restore branch above also scrolls again on the next frame: savedY
457
+ // depends on the new tree's max scroll, which can grow after the commit.)
444
458
  if (scrollToHash()) {
445
459
  return;
446
460
  }
@@ -1,9 +1,5 @@
1
1
  import type { ResolvedSegment } from "./types.js";
2
- import {
3
- mergeSegmentLoaders,
4
- needsLoaderMerge,
5
- insertMissingDiffSegments,
6
- } from "./merge-segment-loaders.js";
2
+ import { insertMissingDiffSegments } from "./merge-segment-loaders.js";
7
3
  import { assertSegmentStructure } from "./segment-structure-assert.js";
8
4
  import { splitInterceptSegments } from "./intercept-utils.js";
9
5
  import { debugLog, IS_BROWSER_DEBUG } from "./logging.js";
@@ -11,15 +7,17 @@ import { debugLog, IS_BROWSER_DEBUG } from "./logging.js";
11
7
  /**
12
8
  * Determines the merging behavior for segment reconciliation.
13
9
  *
14
- * - 'action': From server-action-bridge's own merge. Always merges loaders,
15
- * always preserves cached loading (even undefined), never clears cached
16
- * segment loading.
17
- * - 'navigation': From partial-update during normal navigation. Does NOT merge
18
- * loaders, preserves cached loading only when defined, clears truthy loading
19
- * on cached segments not in server diff.
10
+ * - 'action': From server-action-bridge's own merge. Always preserves cached
11
+ * loading (even undefined), never clears cached segment loading.
12
+ * - 'navigation': From partial-update during normal navigation. Preserves
13
+ * cached loading only when defined, clears truthy loading on cached
14
+ * segments not in server diff.
20
15
  * - 'stale-revalidation': From partial-update during stale revalidation or
21
- * action-triggered refetch. Merges loaders, always preserves cached loading
22
- * (same as action), clears truthy loading on cached segments not in server diff.
16
+ * action-triggered refetch. Always preserves cached loading (same as
17
+ * action), clears truthy loading on cached segments not in server diff.
18
+ *
19
+ * No actor merges loader data: a loader the server skipped, an intercept's
20
+ * included, is its own cached segment in `matched`.
23
21
  */
24
22
  export type ReconcileActor = "navigation" | "action" | "stale-revalidation";
25
23
 
@@ -64,11 +62,6 @@ export interface ReconcileResult {
64
62
  * (not undefined). When cached is undefined, lets server value through
65
63
  * because we're building a new tree.
66
64
  *
67
- * Loader merging:
68
- * - action/stale-revalidation: Merges partial loader data when server returns
69
- * fewer loaders than cached (revalidation only updated some loaders).
70
- * - navigation: Does not merge (full navigation fetches complete data).
71
- *
72
65
  * Cached segment handling (segments in matched but not in server response):
73
66
  * - action: Returns cached segment as-is (preserve tree structure).
74
67
  * - navigation/stale-revalidation: Clears truthy loading to undefined
@@ -77,7 +70,6 @@ export interface ReconcileResult {
77
70
  */
78
71
  export function reconcileSegments(input: ReconcileInput): ReconcileResult {
79
72
  const { actor, matched, diff, insertMissingDiff } = input;
80
- const shouldMergeLoaders = actor !== "navigation";
81
73
  const context = actor === "action" ? "action-bridge" : "partial-update";
82
74
 
83
75
  // Build lookup maps from arrays
@@ -119,16 +111,6 @@ export function reconcileSegments(input: ReconcileInput): ReconcileResult {
119
111
 
120
112
  if (fromServer) {
121
113
  const inDiff = diffSet.has(segId);
122
- // Merge partial loader data when server returns fewer loaders than cached
123
- if (shouldMergeLoaders && needsLoaderMerge(fromServer, fromCache)) {
124
- if (IS_BROWSER_DEBUG) {
125
- debugLog(
126
- `[reconcile] ${segId}: MERGE loaders (server partial, ${inDiff ? "in diff" : "not in diff"})`,
127
- );
128
- }
129
- return mergeSegmentLoaders(fromServer, fromCache);
130
- }
131
-
132
114
  // Preserve cached structural properties to maintain consistent React tree.
133
115
  // Changing these between renders alters the element nesting
134
116
  // (with/without RouteContentWrapper, MountContextProvider, etc.),
@@ -3,8 +3,10 @@ import type {
3
3
  ServerActionBridgeConfig,
4
4
  RscPayload,
5
5
  ResolvedSegment,
6
+ NavigationUpdate,
7
+ UpdateSubscriber,
6
8
  } from "./types.js";
7
- import { createPartialUpdater } from "./partial-update.js";
9
+ import { addTransitionType, createPartialUpdater } from "./partial-update.js";
8
10
  import { enterActionFence, exitActionFence } from "./action-fence.js";
9
11
  import { KEEP_CACHE_HEADER } from "./cookie-name.js";
10
12
  import { createNavigationTransaction } from "./navigation-transaction.js";
@@ -39,6 +41,7 @@ import {
39
41
  import { mergeLocationState } from "./history-state.js";
40
42
  import { stampLocationState } from "./react/location-state-shared.js";
41
43
  import { classifyActionOutcome } from "./action-coordinator.js";
44
+ import { revalidationErrorOf } from "../decode-loader-results.js";
42
45
  import { getAppVersion } from "./app-version.js";
43
46
  import {
44
47
  decideCommitGatedOff,
@@ -99,6 +102,24 @@ function applyActionLocationState(
99
102
  }
100
103
  }
101
104
 
105
+ /**
106
+ * An action's commit: urgent when transition({ when }) gated it off, like
107
+ * every gated-off commit, else a transition tagged `action` (#1165).
108
+ */
109
+ function commitAction(
110
+ onUpdate: UpdateSubscriber,
111
+ update: NavigationUpdate,
112
+ gatedOff: boolean,
113
+ ): void {
114
+ if (gatedOff) onUpdate(update);
115
+ else
116
+ startTransition(() => {
117
+ addTransitionType?.("action");
118
+ update.inTransition = true;
119
+ onUpdate(update);
120
+ });
121
+ }
122
+
102
123
  /**
103
124
  * Create a server action bridge for handling RSC server actions
104
125
  *
@@ -173,6 +194,7 @@ export function createServerActionBridge(
173
194
  * Refetch current route via a navigation transaction.
174
195
  * Encapsulates the repeated pattern of creating a navTx + fetchPartialUpdate
175
196
  * used by navigated-away, hmr-missing, and consolidation-needed scenarios.
197
+ * Resolves with the error of a loader that threw on the refetch.
176
198
  */
177
199
  async function refetchRoute(opts?: {
178
200
  segments?: string[];
@@ -180,7 +202,7 @@ export function createServerActionBridge(
180
202
  actionId?: string;
181
203
  /** transition({ when }) `action` fields for the refetch commit. */
182
204
  action?: TransitionWhenActionInput;
183
- }): Promise<void> {
205
+ }): Promise<Error | undefined> {
184
206
  const src = opts?.interceptSourceUrl ?? null;
185
207
  const navTx = createNavigationTransaction(
186
208
  store,
@@ -189,7 +211,7 @@ export function createServerActionBridge(
189
211
  { replace: true, skipLoadingState: true },
190
212
  );
191
213
  try {
192
- await fetchPartialUpdate(
214
+ return await fetchPartialUpdate(
193
215
  window.location.href,
194
216
  opts?.segments ?? [],
195
217
  false,
@@ -302,6 +324,11 @@ export function createServerActionBridge(
302
324
  if (rid) {
303
325
  url.searchParams.set("_rsc_rid", rid);
304
326
  }
327
+ // The page's rewrite target: the server diffs the re-render against it.
328
+ const rewriteTarget = store.getHistoryEntryMemory()?.rewriteTarget;
329
+ if (rewriteTarget) {
330
+ url.searchParams.set("_rsc_rt", rewriteTarget);
331
+ }
305
332
 
306
333
  // Encode arguments
307
334
  const encodedBody = await deps.encodeReply(args, { temporaryReferences });
@@ -618,12 +645,13 @@ export function createServerActionBridge(
618
645
  );
619
646
  store.rememberDisplayedEntry();
620
647
 
621
- // Update UI with error boundary: urgent when transition({ when })
622
- // gated it off, like every gated-off commit.
623
- const errorUpdate = { root: errorTree, metadata: metadata! };
648
+ // Update UI with error boundary.
624
649
  if (process.env.NODE_ENV !== "production") auditTreeCause("action");
625
- if (errorGatedOff) onUpdate(errorUpdate);
626
- else startTransition(() => onUpdate(errorUpdate));
650
+ commitAction(
651
+ onUpdate,
652
+ { root: errorTree, metadata: metadata! },
653
+ errorGatedOff,
654
+ );
627
655
 
628
656
  // Throw the error so the action promise rejects
629
657
  if (returnValue && !returnValue.ok) {
@@ -677,6 +705,8 @@ export function createServerActionBridge(
677
705
  const fullSegments = reconciled.segments;
678
706
 
679
707
  const returnData = returnValue?.data;
708
+ // A loader that threw while it re-ran: reported, not thrown.
709
+ let revalidationError: Error | undefined;
680
710
 
681
711
  if (returnValue && !returnValue.ok) {
682
712
  handle.fail(returnValue.data);
@@ -754,7 +784,7 @@ export function createServerActionBridge(
754
784
  // route. Invalidation is deferred to finalizeAction(); here we only
755
785
  // trigger the revalidation refetch (suppressed on keep).
756
786
  if (!keepCache) {
757
- await refetchRoute({
787
+ revalidationError = await refetchRoute({
758
788
  interceptSourceUrl: store.getInterceptSourceUrl(),
759
789
  actionId: id,
760
790
  action: whenAction,
@@ -771,7 +801,7 @@ export function createServerActionBridge(
771
801
  // resolving last must discharge a directive-free sibling's repair.
772
802
  // See the keep row in docs/design/rango-state-cookie.md (the all-keep
773
803
  // edge, and the benign re-mark-stale-after-refetch end-state delta).
774
- await refetchRoute({
804
+ revalidationError = await refetchRoute({
775
805
  interceptSourceUrl,
776
806
  actionId: id,
777
807
  action: whenAction,
@@ -794,7 +824,7 @@ export function createServerActionBridge(
794
824
  handle.clearConsolidation();
795
825
 
796
826
  // Ungated on keepCache, same as hmr-missing above (see the keep row).
797
- await refetchRoute({
827
+ revalidationError = await refetchRoute({
798
828
  segments: segmentsToSend,
799
829
  interceptSourceUrl,
800
830
  actionId: id,
@@ -824,6 +854,9 @@ export function createServerActionBridge(
824
854
  currentHandleData,
825
855
  );
826
856
  }
857
+ // Not rendered: the sibling's refetch reruns these loaders and
858
+ // reports its own. Resolve when the metadata lands, without waiting
859
+ // for this answer's loader chunks.
827
860
  break;
828
861
  }
829
862
 
@@ -854,6 +887,7 @@ export function createServerActionBridge(
854
887
  ? reconciled.interceptSegments
855
888
  : undefined,
856
889
  });
890
+ revalidationError = await revalidationErrorOf(segments);
857
891
 
858
892
  // Re-check if user navigated away (could happen during async renderSegments)
859
893
  if (window.location.pathname !== actionStartPathname) {
@@ -891,12 +925,14 @@ export function createServerActionBridge(
891
925
  fullSegments,
892
926
  currentHandleData,
893
927
  );
894
- store.rememberDisplayedEntry(metadata?.routeName);
928
+ store.rememberDisplayedEntry(metadata);
895
929
 
896
- const actionUpdate = { root: newTree, metadata: metadata! };
897
930
  if (process.env.NODE_ENV !== "production") auditTreeCause("action");
898
- if (gatedOff) onUpdate(actionUpdate);
899
- else startTransition(() => onUpdate(actionUpdate));
931
+ commitAction(
932
+ onUpdate,
933
+ { root: newTree, metadata: metadata! },
934
+ gatedOff,
935
+ );
900
936
  // Invalidation deferred to finalizeAction() (runs after this caches
901
937
  // the fresh segments), suppressed when the action called
902
938
  // keepClientCache().
@@ -904,7 +940,7 @@ export function createServerActionBridge(
904
940
  }
905
941
  }
906
942
 
907
- handle.complete(returnData);
943
+ handle.complete(returnData, revalidationError);
908
944
  return returnData;
909
945
  } catch (error) {
910
946
  // What the call rejects with (a revalidation render that threw, a
@@ -36,6 +36,11 @@ export type HandleData = Record<string, Record<string, unknown[]>>;
36
36
  */
37
37
  export interface RscMetadata {
38
38
  pathname: string;
39
+ /**
40
+ * Path and search of the URL the route rendered, when middleware rewrote
41
+ * the request there. Kept per history entry and sent back as `_rsc_rt`.
42
+ */
43
+ rewriteTarget?: string;
39
44
  segments: ResolvedSegment[];
40
45
  /** Router instance ID — the current app's identity. A mismatch with the
41
46
  * client's id (sent as _rsc_rid) is detected server-side and answered with
@@ -248,6 +253,13 @@ export interface TrackedActionState {
248
253
 
249
254
  /** Result data from the action (preserved after completion) */
250
255
  result: unknown | null;
256
+
257
+ /**
258
+ * The error of a loader that threw while it re-ran after this successful
259
+ * action (the first, when several did). The loader also throws at its
260
+ * useLoader read, where the nearest error boundary catches it.
261
+ */
262
+ revalidationError: unknown | null;
251
263
  }
252
264
 
253
265
  /**
@@ -292,13 +304,24 @@ export interface SegmentState {
292
304
  currentSegmentIds: string[];
293
305
  }
294
306
 
307
+ /**
308
+ * What a committed payload says about the page it shows, remembered per
309
+ * history entry (NavigationStore.rememberDisplayedEntry).
310
+ *
311
+ * @internal This type is an implementation detail and may change without notice.
312
+ */
313
+ export interface ShownEntry {
314
+ readonly routeName?: string;
315
+ readonly rewriteTarget?: string;
316
+ }
317
+
295
318
  /**
296
319
  * What the router remembers about one history entry for transition({ when })
297
- * (NavigationStore.rememberDisplayedEntry).
320
+ * and the requests made from it (NavigationStore.rememberDisplayedEntry).
298
321
  *
299
322
  * @internal This type is an implementation detail and may change without notice.
300
323
  */
301
- export interface HistoryEntryMemory {
324
+ export interface HistoryEntryMemory extends ShownEntry {
302
325
  readonly routeName: string | undefined;
303
326
  /** The entry's `history.state` as last recorded. */
304
327
  readonly state: unknown;
@@ -326,6 +349,12 @@ export interface NavigationUpdate {
326
349
  /** Function to check if streaming is in progress */
327
350
  isStreaming?: () => boolean;
328
351
  };
352
+ /**
353
+ * Handed to React inside startTransition (commitInTransition, an action's
354
+ * commit, a cached back/forward's): NavigationProvider joins the route
355
+ * state that arrives while React holds it to its commit.
356
+ */
357
+ inTransition?: true;
329
358
  }
330
359
 
331
360
  /**
@@ -587,12 +616,12 @@ export interface NavigationStore {
587
616
  /**
588
617
  * Per-history-entry memory for transition({ when }) sources: record the
589
618
  * entry on screen (keyed by its `history.state.key`) with its current
590
- * `history.state` and, when given, its route name (kept from the last
591
- * record otherwise; every push/replace creates a new key, so a stale name
592
- * never carries to another entry). Called after every commit, restore and
593
- * state merge.
619
+ * `history.state` and, when `shown` is given, its route name and rewrite
620
+ * target (both kept from the last record otherwise; every push/replace
621
+ * creates a new key, so a stale record never carries to another entry).
622
+ * Called after every commit, restore and state merge.
594
623
  */
595
- rememberDisplayedEntry(routeName?: string): void;
624
+ rememberDisplayedEntry(shown?: ShownEntry): void;
596
625
  /**
597
626
  * The memory of the entry on screen, or of `entryKey`. At popstate
598
627
  * history.state already belongs to the destination, so back/forward reads
@@ -683,6 +712,8 @@ export interface FetchPartialOptions {
683
712
  targetUrl: string;
684
713
  segmentIds: string[];
685
714
  previousUrl: string;
715
+ /** The page's RscMetadata.rewriteTarget, sent as `_rsc_rt`. */
716
+ rewriteTarget?: string;
686
717
  signal?: AbortSignal;
687
718
  /** If true, this is a stale cache revalidation request - server should force revalidators */
688
719
  staleRevalidation?: boolean;
@@ -772,7 +803,8 @@ export interface ServerActionBridgeConfig {
772
803
  export interface NavigationBridge {
773
804
  navigate(url: string, options?: NavigateOptionsInternal): Promise<void>;
774
805
  refresh(): Promise<void>;
775
- handlePopstate(): Promise<void>;
806
+ /** `event`: the popstate event; a traversal the browser animated itself (hasUAVisualTransition) is not animated again. */
807
+ handlePopstate(event?: PopStateEvent): Promise<void>;
776
808
  registerLinkInterception(): () => void;
777
809
  registerDelegatedPrefetch(): () => void;
778
810
  /** Current RSC version (live, reflects the latest updateVersion). */
@@ -16,7 +16,11 @@ import {
16
16
  RangoContext,
17
17
  runWithPrefixes,
18
18
  } from "../server/context.js";
19
- import type { EntryData, TrackedInclude } from "../server/context.js";
19
+ import type {
20
+ EntryData,
21
+ IncludeScopePosition,
22
+ TrackedInclude,
23
+ } from "../server/context.js";
20
24
  import type { TrailingSlashMode } from "../types.js";
21
25
  import { createRouteHelpers } from "../route-definition.js";
22
26
  import MapRootLayout from "../server/root-layout.js";
@@ -179,6 +183,7 @@ async function buildPrefixTreeNode(
179
183
  routeSearchSchemas?: Record<string, Record<string, string>>,
180
184
  routerId?: string,
181
185
  validateTransitionWhen?: boolean,
186
+ position?: IncludeScopePosition,
182
187
  ): Promise<PrefixTreeNode> {
183
188
  // Resolve an async include provider (`() => import("./routes")`) so its routes
184
189
  // are walked into the build-time manifest/types/href. Runtime matching still
@@ -222,6 +227,10 @@ async function buildPrefixTreeNode(
222
227
  mountIndex,
223
228
  ...(routerId ? { routerId } : {}),
224
229
  trackedIncludes, // Enable nested include tracking
230
+ // The same position runtime expansion runs this include at
231
+ // (lazy-includes.ts), so nested hidden names agree across realms.
232
+ scopePath: position?.scopePath,
233
+ hiddenScopePath: position?.hiddenScopePath,
225
234
  },
226
235
  () => {
227
236
  const helpers = createRouteHelpers();
@@ -304,6 +313,7 @@ async function buildPrefixTreeNode(
304
313
  routeSearchSchemas,
305
314
  routerId,
306
315
  validateTransitionWhen,
316
+ include,
307
317
  ),
308
318
  );
309
319
 
@@ -505,6 +515,7 @@ export async function generateManifestFull<TEnv>(
505
515
  routeSearchSchemas,
506
516
  options?.routerId,
507
517
  options?.validateTransitionWhen,
518
+ include,
508
519
  ),
509
520
  );
510
521
 
@@ -48,7 +48,11 @@ export function generatePerModuleTypesSource(
48
48
  }
49
49
  deduped.set(name, { pattern, params, search });
50
50
  }
51
- const sorted = [...deduped.entries()].sort(([a], [b]) => a.localeCompare(b));
51
+ // Locale pinned: the machine's default locale reorders names (Czech sorts
52
+ // "ch" after "h"), and generated files must be identical on every machine.
53
+ const sorted = [...deduped.entries()].sort(([a], [b]) =>
54
+ a.localeCompare(b, "en"),
55
+ );
52
56
  const body = sorted
53
57
  .map(([name, { pattern, params, search }]) => {
54
58
  const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `"${name}"`;
@@ -69,7 +73,7 @@ export function generateRouteTypesSource(
69
73
  ): string {
70
74
  const entries = Object.entries(routeManifest)
71
75
  .filter(([name]) => !isAutoGeneratedRouteName(name))
72
- .sort(([a], [b]) => a.localeCompare(b));
76
+ .sort(([a], [b]) => a.localeCompare(b, "en"));
73
77
 
74
78
  const filteredSearchSchemas = searchSchemas
75
79
  ? Object.fromEntries(