@rangojs/router 0.24.4 → 0.26.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 (301) 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-cache-store.d.ts +0 -2
  21. package/dist/types/cache/cf/cf-cache-types.d.ts +0 -16
  22. package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
  23. package/dist/types/cache/handle-snapshot.d.ts +2 -2
  24. package/dist/types/cache/types.d.ts +16 -6
  25. package/dist/types/cache/vercel/vercel-cache-store.d.ts +1 -15
  26. package/dist/types/client-urls/client-urls.d.ts +6 -2
  27. package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
  28. package/dist/types/client-urls/server-projection.d.ts +9 -5
  29. package/dist/types/client-urls/types.d.ts +29 -16
  30. package/dist/types/client.d.ts +2 -2
  31. package/dist/types/client.rsc.d.ts +2 -2
  32. package/dist/types/decode-loader-results.d.ts +28 -7
  33. package/dist/types/handles/deferred-resolution.d.ts +3 -2
  34. package/dist/types/handles/meta.d.ts +5 -0
  35. package/dist/types/href-client.d.ts +3 -1
  36. package/dist/types/index.rsc.d.ts +1 -1
  37. package/dist/types/loader-redirect.d.ts +2 -3
  38. package/dist/types/outlet-context.d.ts +7 -6
  39. package/dist/types/prefetch-markers.d.ts +14 -5
  40. package/dist/types/prerender/build-shell-capture.d.ts +1 -2
  41. package/dist/types/prerender/writable-store.d.ts +2 -3
  42. package/dist/types/route-content-wrapper.d.ts +17 -14
  43. package/dist/types/route-definition/helpers-types.d.ts +7 -2
  44. package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
  45. package/dist/types/route-definition/rewrite.d.ts +48 -5
  46. package/dist/types/route-types.d.ts +2 -2
  47. package/dist/types/router/handler-context.d.ts +14 -2
  48. package/dist/types/router/intercept-resolution.d.ts +19 -46
  49. package/dist/types/router/lazy-includes.d.ts +7 -0
  50. package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
  51. package/dist/types/router/middleware.d.ts +0 -16
  52. package/dist/types/router/navigation-snapshot.d.ts +2 -0
  53. package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
  54. package/dist/types/router/revalidation.d.ts +8 -1
  55. package/dist/types/router/router-context.d.ts +4 -16
  56. package/dist/types/router/router-interfaces.d.ts +0 -11
  57. package/dist/types/router/router-options.d.ts +4 -27
  58. package/dist/types/router/segment-resolution/mask-nested.d.ts +5 -3
  59. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
  60. package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
  61. package/dist/types/router/segment-resolution.d.ts +1 -1
  62. package/dist/types/router/segment-wrappers.d.ts +3 -5
  63. package/dist/types/router/substitute-pattern-params.d.ts +3 -2
  64. package/dist/types/router/tracing.d.ts +2 -2
  65. package/dist/types/router-versions.d.ts +8 -6
  66. package/dist/types/rsc/full-payload.d.ts +14 -2
  67. package/dist/types/rsc/helpers.d.ts +8 -0
  68. package/dist/types/rsc/manifest-init.d.ts +20 -0
  69. package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
  70. package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
  71. package/dist/types/rsc/rewrite-passes.d.ts +12 -2
  72. package/dist/types/rsc/server-action.d.ts +6 -0
  73. package/dist/types/rsc/shell-capture-constants.d.ts +14 -4
  74. package/dist/types/rsc/shell-capture.d.ts +23 -2
  75. package/dist/types/rsc/types.d.ts +5 -8
  76. package/dist/types/segment-system.d.ts +15 -0
  77. package/dist/types/server/build-version-table.d.ts +10 -13
  78. package/dist/types/server/context.d.ts +90 -6
  79. package/dist/types/server/handle-store.d.ts +17 -6
  80. package/dist/types/server/request-context.d.ts +45 -7
  81. package/dist/types/server/request-headers.d.ts +12 -0
  82. package/dist/types/server/rewrite-trail.d.ts +8 -1
  83. package/dist/types/testing/build-versions.d.ts +0 -3
  84. package/dist/types/testing/e2e/index.d.ts +1 -1
  85. package/dist/types/testing/flight.entry.d.ts +1 -1
  86. package/dist/types/testing/index.d.ts +1 -1
  87. package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
  88. package/dist/types/testing/internal/seed-vars.d.ts +4 -4
  89. package/dist/types/testing/render-route.d.ts +21 -11
  90. package/dist/types/testing/run-loader.d.ts +7 -0
  91. package/dist/types/testing/run-middleware.d.ts +7 -0
  92. package/dist/types/testing/run-revalidate.d.ts +5 -4
  93. package/dist/types/testing/serve-shell-request.d.ts +42 -5
  94. package/dist/types/testing/shell-status.d.ts +17 -0
  95. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  96. package/dist/types/testing/vitest.d.ts +10 -1
  97. package/dist/types/thenable-status.d.ts +0 -6
  98. package/dist/types/types/handler-context.d.ts +12 -1
  99. package/dist/types/types/loader-types.d.ts +10 -4
  100. package/dist/types/types/route-entry.d.ts +2 -1
  101. package/dist/types/urls/path-helper-types.d.ts +4 -2
  102. package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
  103. package/dist/types/vite/discovery/state.d.ts +20 -0
  104. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
  105. package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
  106. package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
  107. package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
  108. package/dist/vite/index.js +6819 -6555
  109. package/package.json +1 -1
  110. package/skills/app-structure/SKILL.md +255 -0
  111. package/skills/app-structure/content-site.md +333 -0
  112. package/skills/app-structure/e-commerce.md +435 -0
  113. package/skills/app-structure/large-app.md +885 -0
  114. package/skills/app-structure/multi-app.md +280 -0
  115. package/skills/breadcrumbs/SKILL.md +6 -1
  116. package/skills/cache-guide/SKILL.md +4 -8
  117. package/skills/caching/SKILL.md +6 -6
  118. package/skills/catalog.json +12 -0
  119. package/skills/client-urls/SKILL.md +185 -32
  120. package/skills/comparison/references/framework-comparison.md +1 -1
  121. package/skills/composability/SKILL.md +10 -8
  122. package/skills/handler-use/SKILL.md +48 -9
  123. package/skills/hooks/handle-and-actions.md +104 -0
  124. package/skills/hooks/navigation.md +24 -10
  125. package/skills/hooks/urls.md +1 -1
  126. package/skills/host-router/SKILL.md +1 -1
  127. package/skills/i18n/SKILL.md +2 -1
  128. package/skills/intercept/SKILL.md +60 -29
  129. package/skills/layout/SKILL.md +6 -3
  130. package/skills/links/SKILL.md +151 -17
  131. package/skills/loader/SKILL.md +42 -19
  132. package/skills/middleware/SKILL.md +111 -15
  133. package/skills/observability/SKILL.md +11 -0
  134. package/skills/parallel/SKILL.md +5 -5
  135. package/skills/ppr/SKILL.md +103 -52
  136. package/skills/prerender/SKILL.md +4 -5
  137. package/skills/rango/SKILL.md +3 -0
  138. package/skills/response-routes/SKILL.md +4 -1
  139. package/skills/router-setup/SKILL.md +0 -2
  140. package/skills/scripts/SKILL.md +12 -1
  141. package/skills/server-actions/SKILL.md +60 -20
  142. package/skills/testing/SKILL.md +1 -0
  143. package/skills/testing/client-components.md +13 -10
  144. package/skills/testing/loader.md +25 -24
  145. package/skills/testing/middleware.md +11 -11
  146. package/skills/testing/render-handler.md +17 -17
  147. package/skills/testing/response-routes.md +4 -1
  148. package/skills/testing/serve-shell-request.md +86 -22
  149. package/skills/testing/server-actions.md +16 -16
  150. package/skills/ux-architecture/SKILL.md +980 -0
  151. package/skills/vercel/SKILL.md +1 -1
  152. package/skills/view-transitions/SKILL.md +43 -10
  153. package/src/__augment-tests__/augmented.check.ts +28 -0
  154. package/src/browser/event-controller.ts +44 -18
  155. package/src/browser/intercept-utils.ts +3 -2
  156. package/src/browser/merge-segment-loaders.ts +0 -85
  157. package/src/browser/navigation-bridge.ts +112 -84
  158. package/src/browser/navigation-client.ts +20 -15
  159. package/src/browser/navigation-store.ts +4 -2
  160. package/src/browser/navigation-transaction.ts +9 -8
  161. package/src/browser/partial-update.ts +90 -32
  162. package/src/browser/prefetch/cache.ts +8 -0
  163. package/src/browser/prefetch/fetch.ts +13 -3
  164. package/src/browser/rango-marker.ts +13 -0
  165. package/src/browser/react/NavigationProvider.tsx +25 -7
  166. package/src/browser/react/filter-segment-order.ts +13 -0
  167. package/src/browser/react/use-action.ts +9 -15
  168. package/src/browser/react/use-handle.ts +63 -71
  169. package/src/browser/react/use-href.tsx +24 -5
  170. package/src/browser/react/use-link-status.ts +1 -1
  171. package/src/browser/react/use-navigation.ts +30 -40
  172. package/src/browser/react/use-router.ts +37 -6
  173. package/src/browser/react/use-segments.ts +8 -12
  174. package/src/browser/response-adapter.ts +11 -5
  175. package/src/browser/rsc-router.tsx +3 -1
  176. package/src/browser/scroll-restoration.ts +34 -20
  177. package/src/browser/segment-reconciler.ts +11 -29
  178. package/src/browser/server-action-bridge.ts +52 -16
  179. package/src/browser/types.ts +40 -8
  180. package/src/build/generate-manifest.ts +12 -1
  181. package/src/build/route-types/codegen.ts +6 -2
  182. package/src/build/route-types/per-module-writer.ts +94 -61
  183. package/src/cache/cache-runtime.ts +9 -0
  184. package/src/cache/cache-scope.ts +2 -1
  185. package/src/cache/cf/cf-cache-store.ts +6 -7
  186. package/src/cache/cf/cf-cache-types.ts +0 -17
  187. package/src/cache/cf/cf-shell-frame.ts +3 -0
  188. package/src/cache/handle-snapshot.ts +2 -2
  189. package/src/cache/types.ts +16 -6
  190. package/src/cache/vercel/vercel-cache-store.ts +8 -19
  191. package/src/client-urls/client-urls.ts +202 -153
  192. package/src/client-urls/revalidate-registry.ts +12 -3
  193. package/src/client-urls/server-projection.ts +137 -66
  194. package/src/client-urls/types.ts +30 -15
  195. package/src/client.rsc.tsx +11 -1
  196. package/src/client.tsx +9 -1
  197. package/src/decode-loader-results.ts +64 -31
  198. package/src/handles/deferred-resolution.ts +3 -2
  199. package/src/handles/meta.ts +5 -0
  200. package/src/href-client.ts +3 -1
  201. package/src/index.rsc.ts +1 -0
  202. package/src/index.ts +12 -4
  203. package/src/loader-redirect.tsx +2 -3
  204. package/src/outlet-context.ts +7 -6
  205. package/src/prefetch-markers.ts +15 -5
  206. package/src/prerender/build-shell-capture.ts +1 -2
  207. package/src/prerender/writable-store.ts +2 -3
  208. package/src/reverse.ts +3 -3
  209. package/src/route-content-wrapper.tsx +19 -17
  210. package/src/route-definition/dsl-helpers.ts +41 -21
  211. package/src/route-definition/helpers-types.ts +7 -2
  212. package/src/route-definition/resolve-handler-use.ts +23 -10
  213. package/src/route-definition/rewrite.ts +86 -19
  214. package/src/route-types.ts +1 -2
  215. package/src/router/find-match.ts +37 -1
  216. package/src/router/handler-context.ts +85 -40
  217. package/src/router/intercept-resolution.ts +161 -307
  218. package/src/router/lazy-includes.ts +43 -27
  219. package/src/router/loader-resolution.ts +21 -1
  220. package/src/router/manifest.ts +15 -10
  221. package/src/router/match-api.ts +11 -1
  222. package/src/router/match-middleware/background-revalidation.ts +2 -4
  223. package/src/router/match-middleware/cache-lookup.ts +16 -20
  224. package/src/router/match-middleware/intercept-resolution.ts +41 -60
  225. package/src/router/match-result.ts +31 -2
  226. package/src/router/middleware.ts +6 -147
  227. package/src/router/navigation-snapshot.ts +38 -2
  228. package/src/router/prefetch-cache-ttl.ts +10 -18
  229. package/src/router/prerender-match.ts +2 -1
  230. package/src/router/revalidation.ts +24 -3
  231. package/src/router/router-context.ts +5 -18
  232. package/src/router/router-interfaces.ts +0 -13
  233. package/src/router/router-options.ts +4 -28
  234. package/src/router/segment-resolution/fresh.ts +6 -2
  235. package/src/router/segment-resolution/helpers.ts +7 -1
  236. package/src/router/segment-resolution/loader-cache.ts +4 -3
  237. package/src/router/segment-resolution/mask-nested.ts +5 -3
  238. package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
  239. package/src/router/segment-resolution/revalidation.ts +75 -35
  240. package/src/router/segment-resolution.ts +0 -1
  241. package/src/router/segment-wrappers.ts +8 -13
  242. package/src/router/substitute-pattern-params.ts +3 -2
  243. package/src/router/tracing.ts +2 -2
  244. package/src/router-versions.ts +7 -7
  245. package/src/router.ts +5 -17
  246. package/src/rsc/full-payload.ts +26 -4
  247. package/src/rsc/handler.ts +86 -48
  248. package/src/rsc/helpers.ts +13 -0
  249. package/src/rsc/manifest-init.ts +56 -3
  250. package/src/rsc/prefetch-render-error.ts +38 -0
  251. package/src/rsc/progressive-enhancement.ts +161 -211
  252. package/src/rsc/rewrite-passes.ts +56 -9
  253. package/src/rsc/rsc-rendering.ts +79 -23
  254. package/src/rsc/server-action.ts +25 -2
  255. package/src/rsc/shell-capture-constants.ts +26 -3
  256. package/src/rsc/shell-capture.ts +137 -51
  257. package/src/rsc/types.ts +5 -9
  258. package/src/segment-system.tsx +125 -119
  259. package/src/server/build-version-table.ts +12 -24
  260. package/src/server/context.ts +160 -7
  261. package/src/server/handle-store.ts +80 -12
  262. package/src/server/request-context.ts +46 -6
  263. package/src/server/request-headers.ts +32 -0
  264. package/src/server/rewrite-trail.ts +55 -9
  265. package/src/ssr/ssr-root.tsx +6 -10
  266. package/src/testing/build-versions.ts +0 -3
  267. package/src/testing/dispatch.ts +5 -8
  268. package/src/testing/e2e/index.ts +1 -0
  269. package/src/testing/flight.entry.ts +1 -0
  270. package/src/testing/index.ts +1 -0
  271. package/src/testing/internal/client-urls-projection.ts +31 -11
  272. package/src/testing/internal/seed-vars.ts +4 -4
  273. package/src/testing/render-route.tsx +83 -40
  274. package/src/testing/run-loader.ts +26 -2
  275. package/src/testing/run-middleware.ts +10 -0
  276. package/src/testing/run-revalidate.ts +13 -6
  277. package/src/testing/serve-shell-request.ts +79 -9
  278. package/src/testing/shell-status.ts +40 -0
  279. package/src/testing/vitest-stubs/version.ts +1 -0
  280. package/src/testing/vitest.ts +74 -7
  281. package/src/thenable-status.ts +0 -11
  282. package/src/types/handler-context.ts +12 -1
  283. package/src/types/loader-types.ts +10 -4
  284. package/src/types/route-entry.ts +2 -1
  285. package/src/urls/include-helper.ts +38 -11
  286. package/src/urls/path-helper-types.ts +4 -2
  287. package/src/vite/discovery/client-urls-projection.ts +205 -16
  288. package/src/vite/discovery/discover-routers.ts +2 -2
  289. package/src/vite/discovery/prerender-collection.ts +2 -6
  290. package/src/vite/discovery/shell-prerender-phase.ts +1 -5
  291. package/src/vite/discovery/state.ts +26 -0
  292. package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
  293. package/src/vite/plugins/expose-internal-ids.ts +46 -40
  294. package/src/vite/plugins/hoist/source.ts +8 -7
  295. package/src/vite/plugins/transition-when-hoist.ts +1 -0
  296. package/src/vite/plugins/version-plugin.ts +7 -0
  297. package/src/vite/plugins/version.d.ts +7 -0
  298. package/src/vite/plugins/virtual-entries.ts +20 -0
  299. package/src/vite/rango.ts +4 -1
  300. package/src/vite/router-discovery.ts +39 -7
  301. package/src/vite/utils/prerender-utils.ts +17 -0
@@ -11,7 +11,7 @@ import { MountContextProvider } from "./browser/react/mount-context.js";
11
11
  import type { ResolvedSegment, RootLayoutProps } from "./types.js";
12
12
  import { isRenderableLoading } from "./types/boundaries.js";
13
13
  import { isPlaceholder } from "./types/segments.js";
14
- import { decodeLoaderResults } from "./decode-loader-results.js";
14
+ import { decodeLoaderResults, isLoaderError } from "./decode-loader-results.js";
15
15
  import { LoaderRedirect } from "./loader-redirect.js";
16
16
  import { invariant } from "./errors.js";
17
17
  import {
@@ -26,10 +26,8 @@ import { INTERNAL_RANGO_DEBUG } from "./internal-debug.js";
26
26
  import { INTERNAL_RANGO_SUSPENSE_AUDIT } from "./internal-suspense-audit.js";
27
27
  import { getBoundaryContent } from "./segment-boundary-content.js";
28
28
  import { asStream } from "./browser/defer.js";
29
- import {
30
- buildLoaderPromise,
31
- getMemoizedLoaderPromise,
32
- } from "./segment-loader-promise.js";
29
+ import { getMemoizedLoaderPromise } from "./segment-loader-promise.js";
30
+ import { loaderParentId } from "./browser/react/filter-segment-order.js";
33
31
  import {
34
32
  auditSegmentElement,
35
33
  finishTreeAudit,
@@ -106,6 +104,60 @@ export function restoreParallelLoaderMarkers(
106
104
  return nextSegments ?? segments;
107
105
  }
108
106
 
107
+ /**
108
+ * An intercept's loaders arrive as loader segments of its slot
109
+ * (`${slot.id}D${i}.${$$id}`, router/intercept-resolution.ts). Folded back
110
+ * onto the slot before the tree walk, as one aggregate the slot's
111
+ * LoaderBoundary reads (client.tsx renderSlotContent): walked as loaders they
112
+ * would attach to the slot's parent, whose subtree (the page under the modal)
113
+ * would change shape and remount. Any other loader segment passes through; a
114
+ * list with nothing to fold is returned as it is, synchronously.
115
+ * The slot is a new object (rule 6, suspense-contract.md); its aggregate is
116
+ * memoized on the loaders' data (rule 1). Settled before the commit when
117
+ * nothing in it is deferred and the slot has no loading() or the lane settles
118
+ * loaders (forceAwait, action): a modal with no loading() commits whole, as
119
+ * when the server awaited its loaders.
120
+ */
121
+ export function foldInterceptLoaders(
122
+ segments: ResolvedSegment[],
123
+ settleLoaders: boolean,
124
+ ): ResolvedSegment[] | Promise<ResolvedSegment[]> {
125
+ const loadersBySlot = new Map<string, ResolvedSegment[]>();
126
+ const rest: ResolvedSegment[] = [];
127
+ for (const segment of segments) {
128
+ // Only a slot's loader has a slot id, `${shortCode}.@${name}`, as parent.
129
+ const parentId =
130
+ segment.type === "loader" && segment.id.includes(".@")
131
+ ? loaderParentId(segment.id)
132
+ : "";
133
+ if (parentId.includes(".@")) pushToGroup(loadersBySlot, parentId, segment);
134
+ else rest.push(segment);
135
+ }
136
+ if (loadersBySlot.size === 0) return segments;
137
+ const fold = async (
138
+ slot: ResolvedSegment,
139
+ loaders: ResolvedSegment[],
140
+ ): Promise<ResolvedSegment> => {
141
+ const aggregate = getMemoizedLoaderPromise(loaders);
142
+ return {
143
+ ...slot,
144
+ loaderIds: loaders.map((l) => l.loaderId!),
145
+ loaderDataPromise: settlesLoaders(
146
+ loaders,
147
+ settleLoaders || !isRenderableLoading(slot.loading),
148
+ )
149
+ ? await aggregate
150
+ : aggregate,
151
+ };
152
+ };
153
+ return Promise.all(
154
+ rest.map((slot) => {
155
+ const loaders = loadersBySlot.get(slot.id);
156
+ return loaders ? fold(slot, loaders) : slot;
157
+ }),
158
+ );
159
+ }
160
+
109
161
  /**
110
162
  * Options for renderSegments
111
163
  */
@@ -252,19 +304,21 @@ function wrapDefaultOutletContent(
252
304
  *
253
305
  * The settled results also go through decodeLoaderResults here: a settled
254
306
  * redirect()/notFound() (or an error with an errorBoundary() fallback) must
255
- * be resolved to its node while the tree is built, exactly as the
256
- * forceAwait/action lanes do, because the read-site throw has no boundary on
257
- * the document lane — flagged content renders without Suspense and a layout
258
- * reader sits above every boundary, so the throw is a Fizz shell error (500).
259
- * `settledFallback` is that node; the caller plants it and, for a redirect,
260
- * replaces the whole page (an ancestor reading the loader would still throw).
307
+ * be resolved to its node while the tree is built, because the read-site
308
+ * throw has no boundary on the document lane — flagged content renders
309
+ * without Suspense and a layout reader sits above every boundary, so the
310
+ * throw is a Fizz shell error (500). `settledFallback` is that node; the
311
+ * caller plants it and, for a redirect, replaces the whole page (an ancestor
312
+ * reading the loader would still throw).
261
313
  *
262
- * `settle` is the forceAwait/action lane of a loader set that holds a
263
- * deferred loader (`prefetch: false`, see hasDeferred): every other loader is
264
- * settled like a flagged one, so the commit stays whole for them, and the
265
- * deferred one keeps its hole for the read site to suspend on. A settled
266
- * error with no boundary is left to the read site there, where the aggregate
267
- * lane it replaces would have thrown it.
314
+ * `settle` is every client lane that commits whole (forceAwait: back/forward,
315
+ * a cached or fully-prefetched click, stale revalidation, leaving an
316
+ * intercept; and the action lane): every loader but a deferred one
317
+ * (`prefetch: false`, which keeps its hole for the read site to suspend on)
318
+ * is settled like a flagged one, so the commit shows no fallback. A loader
319
+ * error is left to its read site there, its errorBoundary() fallback riding
320
+ * the throw, as on a navigation: the nearest boundary to the read catches it
321
+ * (#1129, dc-t65). notFound()/redirect() keep their build-time node.
268
322
  */
269
323
  async function buildLoaderStreams(
270
324
  loaders: ResolvedSegment[],
@@ -283,34 +337,31 @@ async function buildLoaderStreams(
283
337
  const value = await l.loaderData;
284
338
  streams[l.loaderId!] = value;
285
339
  if (l.awaitBeforeFlush) (awaitedIds ??= []).push(l.loaderId!);
340
+ if (settle && isLoaderError(value)) continue;
286
341
  (settledIds ??= []).push(l.loaderId!);
287
342
  (settledValues ??= []).push(value);
288
343
  } else {
289
344
  streams[l.loaderId!] = l.loaderData;
290
345
  }
291
346
  }
292
- let settledFallback: ReactNode | undefined;
293
- if (settledIds) {
294
- try {
295
- settledFallback =
296
- decodeLoaderResults(settledValues!, settledIds).errorFallback ??
297
- undefined;
298
- } catch (error) {
299
- if (!settle) throw error;
300
- }
301
- }
347
+ // Only the document lane can throw here: a settle lane left its errors out.
348
+ const settledFallback = settledIds
349
+ ? (decodeLoaderResults(settledValues!, settledIds).errorFallback ??
350
+ undefined)
351
+ : undefined;
302
352
  return { streams, awaitedIds, settledFallback };
303
353
  }
304
354
 
305
355
  /**
306
- * A loader set that holds a deferred loader (`prefetch: false`): its
356
+ * Whether a loader set is settled before the commit: its lane asks for it
357
+ * and nothing in it is deferred. A deferred loader's (`prefetch: false`)
307
358
  * `loaderData` is a hole (browser/defer.ts) the browser resolves when the
308
359
  * fill request lands (browser/partial-update.ts fillHoles), so no lane may
309
360
  * await the set as a whole. The
310
361
  * tree build would wait for a request that is only sent once it returns.
311
362
  */
312
- function hasDeferred(loaders: ResolvedSegment[]): boolean {
313
- return loaders.some(isPlaceholder);
363
+ function settlesLoaders(loaders: ResolvedSegment[], lane: boolean): boolean {
364
+ return lane && !loaders.some(isPlaceholder);
314
365
  }
315
366
 
316
367
  function isLoaderRedirectNode(node: ReactNode): boolean {
@@ -378,10 +429,9 @@ export async function renderSegments(
378
429
  transitionGatedOff,
379
430
  } = options || {};
380
431
  // forceAwait and action lanes settle loader data before the commit, except
381
- // a set that holds a deferred loader (hasDeferred).
432
+ // a deferred loader (settlesLoaders), and hand each settled entry to its
433
+ // read (buildLoaderStreams). Only the document lane decodes in the build.
382
434
  const settleLoaders = !!(forceAwait || isAction);
383
- const settleAll = (loaders: ResolvedSegment[]): boolean =>
384
- settleLoaders && !hasDeferred(loaders);
385
435
 
386
436
  const segDebug = INTERNAL_RANGO_DEBUG;
387
437
  const segDebugStart = segDebug ? performance.now() : 0;
@@ -395,10 +445,16 @@ export async function renderSegments(
395
445
  }
396
446
 
397
447
  const temporalLazyRefs: Promise<any>[] = [];
398
- const normalizedSegments = restoreParallelLoaderMarkers(segments);
399
- const normalizedInterceptSegments = interceptSegments
400
- ? restoreParallelLoaderMarkers(interceptSegments)
401
- : undefined;
448
+ // Back/forward restores an intercept entry unsplit (navigation-bridge.ts).
449
+ // Awaited only when there is something to fold: no extra microtask.
450
+ let folded = foldInterceptLoaders(segments, settleLoaders);
451
+ if (folded instanceof Promise) folded = await folded;
452
+ const normalizedSegments = restoreParallelLoaderMarkers(folded);
453
+ let normalizedInterceptSegments =
454
+ interceptSegments && foldInterceptLoaders(interceptSegments, settleLoaders);
455
+ if (normalizedInterceptSegments instanceof Promise) {
456
+ normalizedInterceptSegments = await normalizedInterceptSegments;
457
+ }
402
458
 
403
459
  /**
404
460
  * Registers promises from lazy/async components for awaiting.
@@ -423,7 +479,9 @@ export async function renderSegments(
423
479
  );
424
480
  }
425
481
  // Separate segments by type, passing intercept segments for explicit injection
426
- const tree = segmentTreeWalk(normalizedSegments, normalizedInterceptSegments);
482
+ const tree = [
483
+ ...segmentTreeWalk(normalizedSegments, normalizedInterceptSegments),
484
+ ];
427
485
  // Dev only: the wrapper chain per segment, compared with the previous
428
486
  // tree's (suspense-audit.ts, I5).
429
487
  const treeAudit =
@@ -452,7 +510,8 @@ export async function renderSegments(
452
510
  );
453
511
  // Render content segments as siblings
454
512
  let content: ReactNode = null;
455
- for (const node of tree) {
513
+ for (let i = 0; i < tree.length; i++) {
514
+ const node = tree[i]!;
456
515
  invariant(
457
516
  node.segment.type === "layout" ||
458
517
  node.segment.type === "route" ||
@@ -504,7 +563,7 @@ export async function renderSegments(
504
563
  .join(",")
505
564
  : "";
506
565
  // Route identity the per-route remount used to provide; group routes key
507
- // by the group instead and pass it to the error boundary as its resetKey.
566
+ // by the group instead and pass it to the error boundary's reset.
508
567
  const idParamsKey = paramStr ? `${id}-${paramStr}` : id;
509
568
  const key = clientGroup ? `cg:${clientGroup}` : idParamsKey;
510
569
 
@@ -630,14 +689,19 @@ export async function renderSegments(
630
689
  // lane changes remount the subtree (docs/tree-structure.md).
631
690
  if (loaderEntries.length > 0 || clientGroup) {
632
691
  nodeContent = createElement(StreamedLoaderErrorBoundary, {
633
- resetKey: idParamsKey,
692
+ // Leaf first: tree.slice(i) is this segment and its ancestors. A
693
+ // read walks up the chain, so this boundary can catch an ancestor
694
+ // loader's marker and resets when that loader's data changes too.
695
+ reset: [
696
+ idParamsKey,
697
+ ...tree.slice(i).flatMap((n) => n.loaders.map((l) => l.loaderData)),
698
+ ],
634
699
  children: nodeContent,
635
700
  });
636
701
  }
637
702
 
638
703
  if (!clientGroup && loading !== undefined && loading !== null) {
639
704
  const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
640
- let boundaryLoaderData: Promise<any[]> | any[] = loaderDataPromise;
641
705
  // SPIKE (streaming useLoader): per-loader streams for the boundary's
642
706
  // readers. MUST come from the individual loader refs — splitting the
643
707
  // aggregate client-side is wrong (Promise.all resolves at the slowest
@@ -648,19 +712,7 @@ export async function renderSegments(
648
712
  // for content-suspending routes (segment-loader-promise.ts).
649
713
  let boundaryLoaderStreams: Record<string, unknown> | undefined;
650
714
  let boundaryAwaitedLoaderIds: string[] | undefined;
651
- if (settleAll(loaderEntries)) {
652
- const awaitStart = segDebug ? performance.now() : 0;
653
- boundaryLoaderData =
654
- loaderDataPromise instanceof Promise
655
- ? await loaderDataPromise
656
- : loaderDataPromise;
657
- if (segDebug) {
658
- segDebugLog(`segment ${id}: loaders awaited (forceAwait/action)`, {
659
- loaderIds,
660
- ms: Math.round(performance.now() - awaitStart),
661
- });
662
- }
663
- } else if (loaderEntries.length > 0) {
715
+ if (loaderEntries.length > 0) {
664
716
  let settledFallback: ReactNode | undefined;
665
717
  ({
666
718
  streams: boundaryLoaderStreams,
@@ -668,7 +720,9 @@ export async function renderSegments(
668
720
  settledFallback,
669
721
  } = await buildLoaderStreams(loaderEntries, settleLoaders));
670
722
  if (settledFallback !== undefined) {
671
- if (isLoaderRedirectNode(settledFallback)) {
723
+ // Only the document lane replaces the whole page; a client lane
724
+ // keeps its layouts and the redirect replaces this segment's content.
725
+ if (!settleLoaders && isLoaderRedirectNode(settledFallback)) {
672
726
  content = settledFallback;
673
727
  break;
674
728
  }
@@ -687,7 +741,7 @@ export async function renderSegments(
687
741
  : LoaderBoundary,
688
742
  {
689
743
  key: `loader-boundary-${key}`,
690
- loaderDataPromise: boundaryLoaderData,
744
+ loaderDataPromise,
691
745
  loaderIds,
692
746
  loaderStreams: boundaryLoaderStreams,
693
747
  awaitedLoaderIds: boundaryAwaitedLoaderIds,
@@ -726,35 +780,13 @@ export async function renderSegments(
726
780
  // per-loader UNDECODED results (possibly pending promises) through
727
781
  // OutletProvider.loaderStreams — useLoader suspends at the read site to
728
782
  // the nearest CONSUMER Suspense boundary. forceAwait (popstate, stale
729
- // revalidation, fully-prefetched) and action lanes keep the blocking
730
- // await so those commits stay whole with no fallback flash.
731
- let loaderData: Record<string, any> = {};
783
+ // revalidation, fully-prefetched) and the action lane await each loader
784
+ // in buildLoaderStreams so those commits stay whole with no fallback
785
+ // flash, and still hand each entry to its read.
732
786
  let errorFallback: ReactNode = null;
733
787
  let loaderStreams: Record<string, unknown> | undefined;
734
788
  let awaitedLoaderIds: string[] | undefined;
735
- if (settleAll(layoutLoaders)) {
736
- const layoutAwaitStart = segDebug ? performance.now() : 0;
737
- const resolvedData = await buildLoaderPromise(layoutLoaders);
738
- if (segDebug) {
739
- segDebugLog(`segment ${id}: layout loaders awaited (blocking)`, {
740
- loaderIds: layoutLoaderIds,
741
- ms: Math.round(performance.now() - layoutAwaitStart),
742
- });
743
- }
744
- const decodeStart = segDebug ? performance.now() : 0;
745
- ({ loaderData, errorFallback } = decodeLoaderResults(
746
- resolvedData,
747
- layoutLoaderIds,
748
- ));
749
- if (segDebug) {
750
- const decodeMs = Math.round(performance.now() - decodeStart);
751
- if (decodeMs > 0) {
752
- segDebugLog(`segment ${id}: loader results decoded`, {
753
- ms: decodeMs,
754
- });
755
- }
756
- }
757
- } else if (layoutLoaders.length > 0) {
789
+ if (layoutLoaders.length > 0) {
758
790
  let settledFallback: ReactNode | undefined;
759
791
  ({
760
792
  streams: loaderStreams,
@@ -762,7 +794,9 @@ export async function renderSegments(
762
794
  settledFallback,
763
795
  } = await buildLoaderStreams(layoutLoaders, settleLoaders));
764
796
  if (settledFallback !== undefined) {
765
- if (isLoaderRedirectNode(settledFallback)) {
797
+ // Only the document lane replaces the whole page; a client lane
798
+ // keeps its layouts and the redirect replaces this segment's content.
799
+ if (!settleLoaders && isLoaderRedirectNode(settledFallback)) {
766
800
  content = settledFallback;
767
801
  break;
768
802
  }
@@ -813,10 +847,11 @@ export async function renderSegments(
813
847
  // Flagged (ssr:false) delivery still holds: when every owned
814
848
  // loader paid the pre-flush await, settle the aggregate so
815
849
  // LoaderResolver decodes the array without a Flight-chunk use().
816
- const settleParallelAggregate =
817
- (settleLoaders ||
818
- ownedLoaders.every((l) => l.awaitBeforeFlush === true)) &&
819
- !hasDeferred(ownedLoaders);
850
+ const settleParallelAggregate = settlesLoaders(
851
+ ownedLoaders,
852
+ settleLoaders ||
853
+ ownedLoaders.every((l) => l.awaitBeforeFlush === true),
854
+ );
820
855
  if (settleParallelAggregate) {
821
856
  const parallelAwaitStart = segDebug ? performance.now() : 0;
822
857
  p.loaderDataPromise =
@@ -855,8 +890,6 @@ export async function renderSegments(
855
890
  content: outletContent,
856
891
  segment: node.segment,
857
892
  parallel: node.parallel,
858
- loaderData:
859
- Object.keys(loaderData).length > 0 ? loaderData : undefined,
860
893
  loaderStreams,
861
894
  awaitedLoaderIds,
862
895
  pending: outletPending,
@@ -974,20 +1007,6 @@ export async function renderSegments(
974
1007
  * @param segments - Main segments from the route tree
975
1008
  * @param interceptSegments - Optional intercept segments to inject
976
1009
  */
977
- // Loader segment ids have the grammar `${parentId}D${index}.${loaderId}`.
978
- // parentId is the parent shortCode (M/L/P/R/C + digits, never "D") for normal
979
- // loaders, or `${shortCode}.${slotName}` for intercept-slot loaders, where the
980
- // slot name is user-controlled (`@${string}`) and may contain an uppercase "D"
981
- // (e.g. "@Detail"). Strip from the first `D<index>.` separator so the slot name
982
- // is preserved; splitting on a bare "D" mis-cut "@Detail" to "@" and silently
983
- // dropped the loader's data. The first-`D<index>.` strip is only correct because
984
- // slot names cannot contain "." -- assertValidSlotName (route-definition/
985
- // dsl-helpers.ts) rejects a "." at definition time, so a name like "@D3.foo"
986
- // (which WOULD mis-cut here) can never reach this function.
987
- function loaderParentId(loaderSegmentId: string): string {
988
- return loaderSegmentId.replace(/D\d+\..*$/, "");
989
- }
990
-
991
1010
  // Append a value to the array stored under `key`, creating the array on first
992
1011
  // use. Single Map lookup (vs the has/get!().push double-lookup idiom).
993
1012
  function pushToGroup<K, V>(map: Map<K, V[]>, key: K, value: V): void {
@@ -1032,18 +1051,14 @@ function* segmentTreeWalk(
1032
1051
  }
1033
1052
  }
1034
1053
 
1035
- // INTERCEPT SEGMENTS: Explicitly inject into parent's parallel array
1036
- // Intercept segments are passed separately for explicit handling
1054
+ // INTERCEPT SEGMENTS: Explicitly inject into parent's parallel array.
1055
+ // Their loaders are on the slot already (foldInterceptLoaders).
1037
1056
  if (interceptSegments && interceptSegments.length > 0) {
1038
1057
  for (const intercept of interceptSegments) {
1039
1058
  if (intercept.type === "parallel" && intercept.slot) {
1040
1059
  // Extract parent ID from intercept ID (e.g., "M4L0L0L2.@modal" → "M4L0L0L2")
1041
1060
  const parentId = intercept.id.split(".")[0];
1042
1061
  pushToGroup(parallelsByParent, parentId, intercept);
1043
- } else if (intercept.type === "loader") {
1044
- // Intercept loaders - extract parent from loader ID (slot name preserved)
1045
- const parentId = loaderParentId(intercept.id);
1046
- pushToGroup(loadersByParent, parentId, intercept);
1047
1062
  }
1048
1063
  }
1049
1064
  }
@@ -1062,15 +1077,6 @@ function* segmentTreeWalk(
1062
1077
  const parallel = parallelsByParent.get(segment.id) || [];
1063
1078
  const loaders = loadersByParent.get(segment.id) || [];
1064
1079
 
1065
- // Also include loaders from parallel segments (e.g., intercept loaders)
1066
- // These have parent IDs like "M9L0L1.@modal" which match the parallel segment ID
1067
- for (const p of parallel) {
1068
- const parallelLoaders = loadersByParent.get(p.id);
1069
- if (parallelLoaders) {
1070
- loaders.push(...parallelLoaders);
1071
- }
1072
- }
1073
-
1074
1080
  yield { segment, parallel, loaders };
1075
1081
  }
1076
1082
  }
@@ -25,33 +25,24 @@ let table: RouterVersionsTable | undefined = ROUTER_VERSIONS;
25
25
  /**
26
26
  * The versions a router serves with, in order:
27
27
  *
28
- * 1. `override`, a consumer-set `version` (createRouter or createRSCHandler):
29
- * that exact value for both.
30
- * 2. The router's entry in the build table, else the whole-build entry.
31
- * 3. `VERSION` for both: the dev stamp, bumped on every RSC module edit.
28
+ * 1. The router's entry in the build table, else the whole-build entry.
29
+ * 2. `VERSION` for both: the dev stamp, bumped on every RSC module edit.
32
30
  */
33
31
  export function resolveRouterVersions(
34
32
  routerId: string | undefined,
35
- override?: string,
36
33
  ): RouterVersions {
37
- return (
38
- resolveVersionsFrom(table, routerId, override) ?? uniformVersions(VERSION)
39
- );
34
+ return resolveVersionsFrom(table, routerId) ?? uniformVersions(VERSION);
40
35
  }
41
36
 
42
37
  /**
43
38
  * The version an on-demand prerender overlay key carries: the router's data
44
39
  * version, because the overlay stores segment data, as the segment cache does
45
40
  * (a client-only deploy keeps both). The write side (`router.prerender`,
46
- * router.ts) and the read side (rsc/handler.ts) both call this with
47
- * `createRouter({ version })`, the only override the trigger can see, so they
48
- * cannot disagree; a `createRSCHandler({ version })` does not move it.
41
+ * router.ts) and the read side (rsc/handler.ts) both call this, so they
42
+ * cannot disagree.
49
43
  */
50
- export function resolvePrerenderVersion(
51
- routerId: string | undefined,
52
- routerVersion: string | undefined,
53
- ): string {
54
- return resolveRouterVersions(routerId, routerVersion).data;
44
+ export function resolvePrerenderVersion(routerId: string | undefined): string {
45
+ return resolveRouterVersions(routerId).data;
55
46
  }
56
47
 
57
48
  /** The table gives some router a pair of its own. */
@@ -104,16 +95,13 @@ export type KeyVersion = keyof RouterVersions | null;
104
95
 
105
96
  /**
106
97
  * The `v/{version}/` prefix of a persistent store key, or "" for an
107
- * unversioned one. A store-level `version` is used for every versioned kind;
108
- * otherwise the version is the serving router's, read per operation because
109
- * the cache factory builds the store before the request context exists.
98
+ * unversioned one. The version is the serving router's, read per operation
99
+ * because the cache factory builds the store before the request context
100
+ * exists.
110
101
  */
111
- export function versionKeyPrefix(
112
- storeVersion: string | undefined,
113
- kind: KeyVersion,
114
- ): string {
102
+ export function versionKeyPrefix(kind: KeyVersion): string {
115
103
  if (kind === null) return "";
116
- const version = storeVersion ?? getCacheVersions()[kind];
104
+ const version = getCacheVersions()[kind];
117
105
  return version ? `v/${version}/` : "";
118
106
  }
119
107