@rangojs/router 0.24.4 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/README.md +1 -0
  2. package/dist/bin/rango.js +363 -39
  3. package/dist/testing/vitest.js +37 -4
  4. package/dist/types/browser/event-controller.d.ts +24 -5
  5. package/dist/types/browser/intercept-utils.d.ts +5 -2
  6. package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
  7. package/dist/types/browser/navigation-transaction.d.ts +5 -5
  8. package/dist/types/browser/partial-update.d.ts +21 -13
  9. package/dist/types/browser/prefetch/cache.d.ts +8 -0
  10. package/dist/types/browser/rango-marker.d.ts +7 -0
  11. package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
  12. package/dist/types/browser/react/use-handle.d.ts +20 -0
  13. package/dist/types/browser/react/use-href.d.ts +6 -0
  14. package/dist/types/browser/react/use-router.d.ts +6 -1
  15. package/dist/types/browser/response-adapter.d.ts +5 -3
  16. package/dist/types/browser/scroll-restoration.d.ts +9 -3
  17. package/dist/types/browser/segment-reconciler.d.ts +10 -13
  18. package/dist/types/browser/types.d.ts +38 -8
  19. package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
  20. package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
  21. package/dist/types/cache/types.d.ts +8 -0
  22. package/dist/types/client-urls/client-urls.d.ts +6 -2
  23. package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
  24. package/dist/types/client-urls/server-projection.d.ts +9 -5
  25. package/dist/types/client-urls/types.d.ts +29 -16
  26. package/dist/types/client.d.ts +2 -2
  27. package/dist/types/client.rsc.d.ts +2 -2
  28. package/dist/types/decode-loader-results.d.ts +28 -7
  29. package/dist/types/href-client.d.ts +3 -1
  30. package/dist/types/index.rsc.d.ts +1 -1
  31. package/dist/types/loader-redirect.d.ts +2 -3
  32. package/dist/types/outlet-context.d.ts +7 -6
  33. package/dist/types/prefetch-markers.d.ts +14 -5
  34. package/dist/types/route-content-wrapper.d.ts +17 -14
  35. package/dist/types/route-definition/helpers-types.d.ts +7 -2
  36. package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
  37. package/dist/types/route-definition/rewrite.d.ts +48 -5
  38. package/dist/types/route-types.d.ts +2 -2
  39. package/dist/types/router/handler-context.d.ts +14 -2
  40. package/dist/types/router/intercept-resolution.d.ts +19 -46
  41. package/dist/types/router/lazy-includes.d.ts +7 -0
  42. package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
  43. package/dist/types/router/middleware.d.ts +0 -16
  44. package/dist/types/router/navigation-snapshot.d.ts +2 -0
  45. package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
  46. package/dist/types/router/revalidation.d.ts +8 -1
  47. package/dist/types/router/router-context.d.ts +4 -16
  48. package/dist/types/router/router-interfaces.d.ts +0 -6
  49. package/dist/types/router/router-options.d.ts +4 -7
  50. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
  51. package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
  52. package/dist/types/router/segment-resolution.d.ts +1 -1
  53. package/dist/types/router/segment-wrappers.d.ts +3 -5
  54. package/dist/types/router/substitute-pattern-params.d.ts +3 -2
  55. package/dist/types/router/tracing.d.ts +2 -2
  56. package/dist/types/rsc/full-payload.d.ts +14 -2
  57. package/dist/types/rsc/helpers.d.ts +8 -0
  58. package/dist/types/rsc/manifest-init.d.ts +20 -0
  59. package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
  60. package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
  61. package/dist/types/rsc/rewrite-passes.d.ts +12 -2
  62. package/dist/types/rsc/server-action.d.ts +6 -0
  63. package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
  64. package/dist/types/rsc/shell-capture.d.ts +7 -0
  65. package/dist/types/rsc/types.d.ts +5 -0
  66. package/dist/types/segment-system.d.ts +15 -0
  67. package/dist/types/server/context.d.ts +64 -4
  68. package/dist/types/server/request-context.d.ts +41 -4
  69. package/dist/types/server/request-headers.d.ts +12 -0
  70. package/dist/types/server/rewrite-trail.d.ts +8 -1
  71. package/dist/types/testing/e2e/index.d.ts +1 -1
  72. package/dist/types/testing/flight.entry.d.ts +1 -1
  73. package/dist/types/testing/index.d.ts +1 -1
  74. package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
  75. package/dist/types/testing/render-route.d.ts +21 -11
  76. package/dist/types/testing/run-loader.d.ts +7 -0
  77. package/dist/types/testing/run-middleware.d.ts +7 -0
  78. package/dist/types/testing/run-revalidate.d.ts +5 -4
  79. package/dist/types/testing/serve-shell-request.d.ts +41 -4
  80. package/dist/types/testing/shell-status.d.ts +17 -0
  81. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  82. package/dist/types/testing/vitest.d.ts +10 -1
  83. package/dist/types/thenable-status.d.ts +0 -6
  84. package/dist/types/types/handler-context.d.ts +12 -1
  85. package/dist/types/types/loader-types.d.ts +4 -3
  86. package/dist/types/types/route-entry.d.ts +2 -1
  87. package/dist/types/urls/path-helper-types.d.ts +4 -2
  88. package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
  89. package/dist/types/vite/discovery/state.d.ts +20 -0
  90. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
  91. package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
  92. package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
  93. package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
  94. package/dist/vite/index.js +6816 -6547
  95. package/package.json +1 -1
  96. package/skills/app-structure/SKILL.md +255 -0
  97. package/skills/app-structure/content-site.md +333 -0
  98. package/skills/app-structure/e-commerce.md +435 -0
  99. package/skills/app-structure/large-app.md +885 -0
  100. package/skills/app-structure/multi-app.md +280 -0
  101. package/skills/breadcrumbs/SKILL.md +6 -1
  102. package/skills/caching/SKILL.md +3 -1
  103. package/skills/catalog.json +12 -0
  104. package/skills/client-urls/SKILL.md +185 -32
  105. package/skills/composability/SKILL.md +10 -8
  106. package/skills/handler-use/SKILL.md +48 -9
  107. package/skills/hooks/handle-and-actions.md +104 -0
  108. package/skills/hooks/navigation.md +24 -10
  109. package/skills/hooks/urls.md +1 -1
  110. package/skills/i18n/SKILL.md +2 -1
  111. package/skills/intercept/SKILL.md +60 -29
  112. package/skills/layout/SKILL.md +6 -3
  113. package/skills/links/SKILL.md +151 -17
  114. package/skills/loader/SKILL.md +32 -10
  115. package/skills/middleware/SKILL.md +111 -15
  116. package/skills/observability/SKILL.md +11 -0
  117. package/skills/parallel/SKILL.md +5 -5
  118. package/skills/ppr/SKILL.md +43 -8
  119. package/skills/prerender/SKILL.md +1 -2
  120. package/skills/rango/SKILL.md +3 -0
  121. package/skills/response-routes/SKILL.md +4 -1
  122. package/skills/scripts/SKILL.md +12 -1
  123. package/skills/server-actions/SKILL.md +60 -20
  124. package/skills/testing/SKILL.md +1 -0
  125. package/skills/testing/client-components.md +13 -10
  126. package/skills/testing/loader.md +25 -24
  127. package/skills/testing/middleware.md +11 -11
  128. package/skills/testing/response-routes.md +4 -1
  129. package/skills/testing/serve-shell-request.md +85 -21
  130. package/skills/ux-architecture/SKILL.md +980 -0
  131. package/skills/view-transitions/SKILL.md +43 -10
  132. package/src/__augment-tests__/augmented.check.ts +28 -0
  133. package/src/browser/event-controller.ts +44 -18
  134. package/src/browser/intercept-utils.ts +3 -2
  135. package/src/browser/merge-segment-loaders.ts +0 -85
  136. package/src/browser/navigation-bridge.ts +112 -84
  137. package/src/browser/navigation-client.ts +20 -15
  138. package/src/browser/navigation-store.ts +4 -2
  139. package/src/browser/navigation-transaction.ts +9 -8
  140. package/src/browser/partial-update.ts +90 -32
  141. package/src/browser/prefetch/cache.ts +8 -0
  142. package/src/browser/prefetch/fetch.ts +13 -3
  143. package/src/browser/rango-marker.ts +13 -0
  144. package/src/browser/react/NavigationProvider.tsx +25 -7
  145. package/src/browser/react/filter-segment-order.ts +13 -0
  146. package/src/browser/react/use-action.ts +9 -15
  147. package/src/browser/react/use-handle.ts +63 -71
  148. package/src/browser/react/use-href.tsx +24 -5
  149. package/src/browser/react/use-link-status.ts +1 -1
  150. package/src/browser/react/use-navigation.ts +30 -40
  151. package/src/browser/react/use-router.ts +37 -6
  152. package/src/browser/react/use-segments.ts +8 -12
  153. package/src/browser/response-adapter.ts +11 -5
  154. package/src/browser/rsc-router.tsx +3 -1
  155. package/src/browser/scroll-restoration.ts +34 -20
  156. package/src/browser/segment-reconciler.ts +11 -29
  157. package/src/browser/server-action-bridge.ts +52 -16
  158. package/src/browser/types.ts +40 -8
  159. package/src/build/generate-manifest.ts +12 -1
  160. package/src/build/route-types/codegen.ts +6 -2
  161. package/src/build/route-types/per-module-writer.ts +94 -61
  162. package/src/cache/cache-runtime.ts +9 -0
  163. package/src/cache/cache-scope.ts +2 -1
  164. package/src/cache/cf/cf-cache-store.ts +2 -0
  165. package/src/cache/cf/cf-shell-frame.ts +3 -0
  166. package/src/cache/types.ts +8 -0
  167. package/src/cache/vercel/vercel-cache-store.ts +6 -1
  168. package/src/client-urls/client-urls.ts +202 -153
  169. package/src/client-urls/revalidate-registry.ts +12 -3
  170. package/src/client-urls/server-projection.ts +137 -66
  171. package/src/client-urls/types.ts +30 -15
  172. package/src/client.rsc.tsx +11 -1
  173. package/src/client.tsx +9 -1
  174. package/src/decode-loader-results.ts +64 -31
  175. package/src/href-client.ts +3 -1
  176. package/src/index.rsc.ts +1 -0
  177. package/src/index.ts +12 -4
  178. package/src/loader-redirect.tsx +2 -3
  179. package/src/outlet-context.ts +7 -6
  180. package/src/prefetch-markers.ts +15 -5
  181. package/src/reverse.ts +3 -3
  182. package/src/route-content-wrapper.tsx +19 -17
  183. package/src/route-definition/dsl-helpers.ts +27 -7
  184. package/src/route-definition/helpers-types.ts +7 -2
  185. package/src/route-definition/resolve-handler-use.ts +23 -10
  186. package/src/route-definition/rewrite.ts +86 -19
  187. package/src/route-types.ts +1 -2
  188. package/src/router/find-match.ts +37 -1
  189. package/src/router/handler-context.ts +85 -40
  190. package/src/router/intercept-resolution.ts +161 -307
  191. package/src/router/lazy-includes.ts +43 -27
  192. package/src/router/loader-resolution.ts +21 -1
  193. package/src/router/manifest.ts +6 -8
  194. package/src/router/match-api.ts +11 -1
  195. package/src/router/match-middleware/background-revalidation.ts +2 -4
  196. package/src/router/match-middleware/cache-lookup.ts +16 -20
  197. package/src/router/match-middleware/intercept-resolution.ts +41 -60
  198. package/src/router/match-result.ts +31 -2
  199. package/src/router/middleware.ts +6 -147
  200. package/src/router/navigation-snapshot.ts +38 -2
  201. package/src/router/prefetch-cache-ttl.ts +10 -18
  202. package/src/router/prerender-match.ts +2 -1
  203. package/src/router/revalidation.ts +24 -3
  204. package/src/router/router-context.ts +5 -18
  205. package/src/router/router-interfaces.ts +0 -7
  206. package/src/router/router-options.ts +4 -7
  207. package/src/router/segment-resolution/fresh.ts +6 -2
  208. package/src/router/segment-resolution/helpers.ts +7 -1
  209. package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
  210. package/src/router/segment-resolution/revalidation.ts +75 -35
  211. package/src/router/segment-resolution.ts +0 -1
  212. package/src/router/segment-wrappers.ts +8 -13
  213. package/src/router/substitute-pattern-params.ts +3 -2
  214. package/src/router/tracing.ts +2 -2
  215. package/src/router.ts +2 -9
  216. package/src/rsc/full-payload.ts +26 -4
  217. package/src/rsc/handler.ts +83 -41
  218. package/src/rsc/helpers.ts +13 -0
  219. package/src/rsc/manifest-init.ts +56 -3
  220. package/src/rsc/prefetch-render-error.ts +38 -0
  221. package/src/rsc/progressive-enhancement.ts +161 -211
  222. package/src/rsc/rewrite-passes.ts +56 -9
  223. package/src/rsc/rsc-rendering.ts +79 -23
  224. package/src/rsc/server-action.ts +25 -2
  225. package/src/rsc/shell-capture-constants.ts +22 -0
  226. package/src/rsc/shell-capture.ts +11 -0
  227. package/src/rsc/types.ts +5 -0
  228. package/src/segment-system.tsx +125 -119
  229. package/src/server/context.ts +96 -5
  230. package/src/server/handle-store.ts +3 -2
  231. package/src/server/request-context.ts +42 -3
  232. package/src/server/request-headers.ts +32 -0
  233. package/src/server/rewrite-trail.ts +55 -9
  234. package/src/ssr/ssr-root.tsx +6 -10
  235. package/src/testing/dispatch.ts +4 -5
  236. package/src/testing/e2e/index.ts +1 -0
  237. package/src/testing/flight.entry.ts +1 -0
  238. package/src/testing/index.ts +1 -0
  239. package/src/testing/internal/client-urls-projection.ts +31 -11
  240. package/src/testing/render-route.tsx +83 -40
  241. package/src/testing/run-loader.ts +26 -2
  242. package/src/testing/run-middleware.ts +10 -0
  243. package/src/testing/run-revalidate.ts +13 -6
  244. package/src/testing/serve-shell-request.ts +77 -4
  245. package/src/testing/shell-status.ts +40 -0
  246. package/src/testing/vitest-stubs/version.ts +1 -0
  247. package/src/testing/vitest.ts +74 -7
  248. package/src/thenable-status.ts +0 -11
  249. package/src/types/handler-context.ts +12 -1
  250. package/src/types/loader-types.ts +4 -3
  251. package/src/types/route-entry.ts +2 -1
  252. package/src/urls/include-helper.ts +38 -11
  253. package/src/urls/path-helper-types.ts +4 -2
  254. package/src/vite/discovery/client-urls-projection.ts +205 -16
  255. package/src/vite/discovery/discover-routers.ts +2 -2
  256. package/src/vite/discovery/prerender-collection.ts +2 -6
  257. package/src/vite/discovery/state.ts +26 -0
  258. package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
  259. package/src/vite/plugins/expose-internal-ids.ts +46 -40
  260. package/src/vite/plugins/hoist/source.ts +8 -7
  261. package/src/vite/plugins/transition-when-hoist.ts +1 -0
  262. package/src/vite/plugins/version-plugin.ts +7 -0
  263. package/src/vite/plugins/version.d.ts +7 -0
  264. package/src/vite/plugins/virtual-entries.ts +20 -0
  265. package/src/vite/rango.ts +4 -1
  266. package/src/vite/router-discovery.ts +39 -7
  267. package/src/vite/utils/prerender-utils.ts +17 -0
@@ -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(
@@ -62,70 +62,107 @@ export function findUrlsVariableNames(code: string): string[] {
62
62
  return names;
63
63
  }
64
64
 
65
+ export interface PerModuleRouteTypesPlan {
66
+ genPath: string;
67
+ /** A urls()/clientUrls() variable resolved to zero routes; content is empty. */
68
+ isPlaceholder: boolean;
69
+ content: string;
70
+ }
71
+
65
72
  /**
66
- * Generate per-module route types for a single url module file.
67
- * Follows include() calls recursively to produce the full route tree.
68
- * No-ops if the file doesn't contain `urls(`/`clientUrls(` or has no named
69
- * routes.
73
+ * Pure half of the per-module writer: reads sources, never writes. Returns
74
+ * null when the file has no `urls(`/`clientUrls(` or no named routes and no
75
+ * urls() variable. Throws on failure.
70
76
  */
71
- export function writePerModuleRouteTypesForFile(filePath: string): void {
72
- try {
73
- const source = readFileSync(filePath, "utf-8");
74
- if (!source.includes("urls(") && !source.includes("clientUrls(")) return;
77
+ export function planPerModuleRouteTypesForFile(
78
+ filePath: string,
79
+ ): PerModuleRouteTypesPlan | null {
80
+ const source = readFileSync(filePath, "utf-8");
81
+ if (!source.includes("urls(") && !source.includes("clientUrls(")) return null;
75
82
 
76
- const varNames = findUrlsVariableNames(source);
83
+ const varNames = findUrlsVariableNames(source);
77
84
 
78
- type Route = {
79
- name: string;
80
- pattern: string;
81
- params?: Record<string, string>;
82
- search?: Record<string, string>;
83
- };
84
- let routes: Route[];
85
+ type Route = {
86
+ name: string;
87
+ pattern: string;
88
+ params?: Record<string, string>;
89
+ search?: Record<string, string>;
90
+ };
91
+ let routes: Route[];
85
92
 
86
- if (varNames.length > 0) {
87
- // Follow includes recursively via the combined route map builder.
88
- // The visited set in buildCombinedRouteMapWithSearch prevents infinite loops.
89
- // Share one per-file scan memo across all urls() variables so a shared
90
- // include target is read+parsed once for this file, not once per variable.
91
- routes = [];
92
- const memo = createScanMemo();
93
- for (const varName of varNames) {
94
- const { routes: routeMap, searchSchemas } =
95
- buildCombinedRouteMapWithSearch(
96
- filePath,
97
- varName,
98
- undefined,
99
- undefined,
100
- undefined,
101
- memo,
102
- );
103
- for (const [name, pattern] of Object.entries(routeMap)) {
104
- const params = extractParamsFromPattern(pattern);
105
- routes.push({
106
- name,
107
- pattern,
108
- ...(params ? { params } : {}),
109
- ...(searchSchemas[name] ? { search: searchSchemas[name] } : {}),
110
- });
111
- }
93
+ if (varNames.length > 0) {
94
+ // Follow includes recursively via the combined route map builder.
95
+ // The visited set in buildCombinedRouteMapWithSearch prevents infinite loops.
96
+ // Share one per-file scan memo across all urls() variables so a shared
97
+ // include target is read+parsed once for this file, not once per variable.
98
+ routes = [];
99
+ const memo = createScanMemo();
100
+ for (const varName of varNames) {
101
+ const { routes: routeMap, searchSchemas } =
102
+ buildCombinedRouteMapWithSearch(
103
+ filePath,
104
+ varName,
105
+ undefined,
106
+ undefined,
107
+ undefined,
108
+ memo,
109
+ );
110
+ for (const [name, pattern] of Object.entries(routeMap)) {
111
+ const params = extractParamsFromPattern(pattern);
112
+ routes.push({
113
+ name,
114
+ pattern,
115
+ ...(params ? { params } : {}),
116
+ ...(searchSchemas[name] ? { search: searchSchemas[name] } : {}),
117
+ });
112
118
  }
113
- } else {
114
- // Fallback: no urls() variable found, extract path() calls directly
115
- routes = extractRoutesFromSource(source);
116
119
  }
120
+ } else {
121
+ // Fallback: no urls() variable found, extract path() calls directly
122
+ routes = extractRoutesFromSource(source);
123
+ }
124
+
125
+ // Match .ts/.tsx/.js/.jsx (same as router-processing.ts / router-transform.ts).
126
+ // Without the jsx? branch a .jsx/.js source produced genPath === filePath,
127
+ // overwriting the source file instead of writing a sibling .gen.ts.
128
+ const genPath = filePath.replace(/\.(tsx?|jsx?)$/, ".gen.ts");
117
129
 
118
- // Match .ts/.tsx/.js/.jsx (same as router-processing.ts / router-transform.ts).
119
- // Without the jsx? branch a .jsx/.js source produced genPath === filePath,
120
- // overwriting the source file instead of writing a sibling .gen.ts.
121
- const genPath = filePath.replace(/\.(tsx?|jsx?)$/, ".gen.ts");
130
+ if (routes.length === 0) {
131
+ if (varNames.length === 0) return null;
132
+ return {
133
+ genPath,
134
+ isPlaceholder: true,
135
+ content: generatePerModuleTypesSource([]),
136
+ };
137
+ }
138
+ return {
139
+ genPath,
140
+ isPlaceholder: false,
141
+ content: generatePerModuleTypesSource(routes),
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Generate per-module route types for a single url module file.
147
+ * Follows include() calls recursively to produce the full route tree.
148
+ * No-ops if the file doesn't contain `urls(`/`clientUrls(` or has no named
149
+ * routes.
150
+ */
151
+ export function writePerModuleRouteTypesForFile(filePath: string): void {
152
+ try {
153
+ const plan = planPerModuleRouteTypesForFile(filePath);
154
+ if (!plan) return;
155
+ const { genPath, isPlaceholder, content } = plan;
156
+ const exists = existsSync(genPath);
122
157
 
123
- // When a urls() variable was found but static resolution yields zero
124
- // routes, write an empty placeholder so generated imports stay
125
- // resolvable until runtime discovery fills them in.
126
- if (routes.length === 0) {
127
- if (varNames.length > 0 && !existsSync(genPath)) {
128
- writeFileSync(genPath, generatePerModuleTypesSource([]));
158
+ // A urls() variable whose static resolution yields zero routes gets an
159
+ // empty placeholder so an import of the sibling .gen.ts resolves. Nothing
160
+ // fills it in later: runtime discovery writes only *.named-routes.gen.ts
161
+ // (runtime-discovery.ts), so it stays `routes = {}` until a static run can
162
+ // resolve the module's includes and overwrites it.
163
+ if (isPlaceholder) {
164
+ if (!exists) {
165
+ writeFileSync(genPath, content);
129
166
  console.log(
130
167
  `[rango] Generated route types (placeholder) -> ${genPath}`,
131
168
  );
@@ -133,12 +170,8 @@ export function writePerModuleRouteTypesForFile(filePath: string): void {
133
170
  return;
134
171
  }
135
172
 
136
- const genSource = generatePerModuleTypesSource(routes);
137
- const existing = existsSync(genPath)
138
- ? readFileSync(genPath, "utf-8")
139
- : null;
140
- if (existing !== genSource) {
141
- writeFileSync(genPath, genSource);
173
+ if (!exists || readFileSync(genPath, "utf-8") !== content) {
174
+ writeFileSync(genPath, content);
142
175
  console.log(`[rango] Generated route types -> ${genPath}`);
143
176
  }
144
177
  } catch (err) {
@@ -508,6 +508,15 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
508
508
  if (ctx._routeName) {
509
509
  keyArgs.push(ctx._routeName);
510
510
  }
511
+ // A layout's / parallel's / their loader's ctx resolves dot names
512
+ // in its own module (withReverseScope), so one route's segments
513
+ // can reverse the same name differently.
514
+ const scope = ctx._reverseScope;
515
+ if (scope) {
516
+ keyArgs.push(
517
+ `reverse-scope:${scope.prefix ?? ""}:${scope.rootScoped}`,
518
+ );
519
+ }
511
520
  keyArgs.push(ctx.pathname, ctx.params);
512
521
  if (ctx._responseType) {
513
522
  keyArgs.push(ctx._responseType);
@@ -24,6 +24,7 @@ import {
24
24
  runInSegmentTagScope,
25
25
  } from "./cache-tag.js";
26
26
  import { reportCacheError } from "./cache-error.js";
27
+ import { isInterceptSegment } from "../browser/intercept-utils.js";
27
28
  import {
28
29
  predatesInvalidation,
29
30
  type ExecutionStart,
@@ -373,7 +374,7 @@ export class CacheScope {
373
374
  const next = id.charCodeAt(boundary.length);
374
375
  return !(next >= 48 && next <= 57);
375
376
  }
376
- return namespace?.startsWith("intercept:") === true;
377
+ return isInterceptSegment({ namespace });
377
378
  }
378
379
 
379
380
  /**
@@ -422,6 +422,7 @@ function shellHeadToEntry(head: ShellFrameHead): ShellEntryHead {
422
422
  reactVersion: head.rv,
423
423
  buildVersion: head.bv,
424
424
  initialTheme: head.i,
425
+ pathname: head.lp,
425
426
  docKey: head.dk,
426
427
  prunedRecords: head.pr,
427
428
  navigationOnly: head.no,
@@ -2695,6 +2696,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
2695
2696
  t: tags,
2696
2697
  ta: taggedAt,
2697
2698
  i: entry.initialTheme,
2699
+ lp: entry.pathname,
2698
2700
  dk: entry.docKey,
2699
2701
  pr: entry.prunedRecords,
2700
2702
  no: entry.navigationOnly,