@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
@@ -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
  }
@@ -92,6 +92,12 @@ export type EntryPropCommon = {
92
92
  cache?: EntryCacheConfig;
93
93
  /** URL prefix from include() scope, used for MountContext on client */
94
94
  mountPath?: string;
95
+ /**
96
+ * Include scope the entry's handler and loaders resolve `ctx.reverse(".name")`
97
+ * in. Structural entries only (layout, cache, parallel, wrapping
98
+ * middleware); a route resolves from its own name. See ReverseScope.
99
+ */
100
+ reverseScope?: ReverseScope;
95
101
  /** clientUrls() group key (PathOptions.clientGroup); route entries only. */
96
102
  clientGroup?: string;
97
103
  };
@@ -163,6 +169,13 @@ export type LoaderEntry = {
163
169
  * (router/segment-resolution/prefetch-deferral.ts).
164
170
  */
165
171
  prefetch?: false;
172
+ /**
173
+ * The declaring layout's / parallel's / cache()'s include scope, for the
174
+ * loader body's `ctx.reverse(".name")` (its DSL kickoff carries it,
175
+ * runInsideLoaderScope). Unset on a route's loaders: they resolve from the
176
+ * route's name.
177
+ */
178
+ reverseScope?: ReverseScope;
166
179
  };
167
180
 
168
181
  /**
@@ -233,7 +246,6 @@ export type InterceptEntry = {
233
246
  slotName: `@${string}`; // e.g., "@modal"
234
247
  routeName: string; // e.g., "card"
235
248
  handler: ReactNode | Handler<any, any, any>;
236
- middleware: MiddlewareFn<any, any>[];
237
249
  loader: LoaderEntry[];
238
250
  loading?: ReactNode | false;
239
251
  transition?: TransitionConfig;
@@ -327,10 +339,22 @@ export type EntryData =
327
339
  EntryPropSegments &
328
340
  EntryPropRender);
329
341
 
342
+ /**
343
+ * Where an include()'s handler runs in the route tree. Hidden include-scope
344
+ * names (`$prefix_*`) derive from it, never from counter state, so build
345
+ * discovery and runtime expansion name every include the same (#1104).
346
+ */
347
+ export interface IncludeScopePosition {
348
+ /** Sibling indexes of the enclosing includes, "_"-joined ("" at a mount root). */
349
+ scopePath?: string;
350
+ /** The scopePath tail since the nearest include that added a name segment. */
351
+ hiddenScopePath?: string;
352
+ }
353
+
330
354
  /**
331
355
  * Tracked include info for build-time manifest generation
332
356
  */
333
- export interface TrackedInclude {
357
+ export interface TrackedInclude extends IncludeScopePosition {
334
358
  prefix: string;
335
359
  fullPrefix: string;
336
360
  namePrefix?: string;
@@ -352,7 +376,7 @@ export type CachedHeaderScope = {
352
376
  /**
353
377
  * Context stored in AsyncLocalStorage
354
378
  */
355
- interface HelperContext {
379
+ interface HelperContext extends IncludeScopePosition {
356
380
  manifest: Map<string, EntryData>;
357
381
  namespace: string;
358
382
  parent: EntryData | null;
@@ -597,6 +621,8 @@ export const getContext = (): {
597
621
  trackedIncludes: store.trackedIncludes,
598
622
  cacheProfiles: store.cacheProfiles,
599
623
  includeScope: store.includeScope,
624
+ scopePath: store.scopePath,
625
+ hiddenScopePath: store.hiddenScopePath,
600
626
  // cachedHeaderScope (and insideCacheScope) deliberately NOT copied —
601
627
  // the header guard's middleware exemption depends on the latch dying
602
628
  // with the funnel scope (see assertCachedHeaderWriteAllowed).
@@ -641,6 +667,8 @@ export const getContext = (): {
641
667
  rootScoped: store?.rootScoped,
642
668
  trackedIncludes: store?.trackedIncludes,
643
669
  cacheProfiles: store?.cacheProfiles,
670
+ scopePath: store?.scopePath,
671
+ hiddenScopePath: store?.hiddenScopePath,
644
672
  },
645
673
  callback,
646
674
  );
@@ -763,6 +791,54 @@ export function getRootScoped(): boolean {
763
791
  return store?.rootScoped ?? true;
764
792
  }
765
793
 
794
+ /**
795
+ * The include scope a segment's dot-local `ctx.reverse(".name")` resolves in.
796
+ *
797
+ * One handler ctx serves every segment of a request and is built from the
798
+ * matched route's name (match-api.ts), so a parent module's layout resolved
799
+ * `.settings` from the matched CHILD module's prefix first: on
800
+ * `/acme/web/issues` the account layout's `.settings` hit the project
801
+ * module's route. Structural entries (layout, cache, parallel, wrapping
802
+ * middleware) and the loaders they declare capture the scope where the DSL
803
+ * defined them. Their handler gets a ctx whose `reverse` carries it
804
+ * (withReverseScope, router/handler-context.ts), and their loaders' DSL
805
+ * kickoff carries it in the loader scope (runInsideLoaderScope) for the
806
+ * loader ctx the executor builds. Global names and mount-param autofill do
807
+ * not change.
808
+ */
809
+ export interface ReverseScope {
810
+ /** Include name prefix ("account.project"); undefined outside named includes. */
811
+ readonly prefix: string | undefined;
812
+ /** getRootScoped() where the segment was defined: bare-name fallback allowed. */
813
+ readonly rootScoped: boolean;
814
+ }
815
+
816
+ /** The ReverseScope of the DSL position being evaluated. */
817
+ export function getReverseScope(): ReverseScope {
818
+ return { prefix: getNamePrefix(), rootScoped: getRootScoped() };
819
+ }
820
+
821
+ /**
822
+ * A createReverseFunction result (router/handler-context.ts). The internal
823
+ * 4th argument resolves dot names in a segment's ReverseScope instead of the
824
+ * route's name.
825
+ */
826
+ export type ScopableReverse = (
827
+ name: string,
828
+ hrefParams?: Record<string, string>,
829
+ search?: Record<string, unknown>,
830
+ segmentScope?: ReverseScope,
831
+ ) => string;
832
+
833
+ /** `reverse` with dot names resolved in `scope`, if any. */
834
+ export function scopeReverse(
835
+ reverse: ScopableReverse,
836
+ scope: ReverseScope | undefined,
837
+ ): ScopableReverse {
838
+ if (scope === undefined) return reverse;
839
+ return (name, hrefParams, search) => reverse(name, hrefParams, search, scope);
840
+ }
841
+
766
842
  /**
767
843
  * Stamp build-time scope identity onto a Static() definition at mount time.
768
844
  * The bake collector (prerender-collection.ts) renders defs OUTSIDE any
@@ -909,6 +985,8 @@ interface LoaderScope {
909
985
  active: true;
910
986
  /** $$id of the DSL loader this scope resolves (runInsideLoaderScope). */
911
987
  loaderId?: string;
988
+ /** The declaring entry's ReverseScope (LoaderEntry.reverseScope). */
989
+ reverseScope?: ReverseScope;
912
990
  }
913
991
  const loaderScopeALS: AsyncLocalStorage<LoaderScope> = ((globalThis as any)[
914
992
  LOADER_SCOPE_KEY
@@ -1472,8 +1550,21 @@ export function assertCachedHeaderWriteAllowed(
1472
1550
  * are bypassed because loaders are always fresh (never cached) and
1473
1551
  * their side effects (setCookie, header, etc.) are safe.
1474
1552
  */
1475
- export function runInsideLoaderScope<T>(fn: () => T, loaderId?: string): T {
1476
- return loaderScopeALS.run({ active: true, loaderId }, fn);
1553
+ export function runInsideLoaderScope<T>(
1554
+ fn: () => T,
1555
+ loaderId?: string,
1556
+ reverseScope?: ReverseScope,
1557
+ ): T {
1558
+ return loaderScopeALS.run({ active: true, loaderId, reverseScope }, fn);
1559
+ }
1560
+
1561
+ /**
1562
+ * The ReverseScope of the DSL loader kickoff around this call: a loader the
1563
+ * kickoff starts, and one its body starts with ctx.use(), resolve dot names
1564
+ * in it. A handler's ctx.use(Loader) runs outside any kickoff: none.
1565
+ */
1566
+ export function getLoaderReverseScope(): ReverseScope | undefined {
1567
+ return loaderScopeALS.getStore()?.reverseScope;
1477
1568
  }
1478
1569
 
1479
1570
  /**
@@ -47,8 +47,9 @@ function createLateHandlePushError(
47
47
  ): Error {
48
48
  const error = new Error(
49
49
  `Handle "${handleName}" for segment "${segmentId}" was pushed after handle collection completed. ` +
50
- `This usually means an async JSX subtree suspended and later tried to push a handle during streaming. ` +
51
- `Push handles from the route/layout handler or during the initial synchronous JSX render instead.`,
50
+ `This usually means an async component pushed it after an await. ` +
51
+ `Push from the route or layout handler, or reserve the slot there with ` +
52
+ `ctx.use(Handle).defer() and resolve it from the component.`,
52
53
  );
53
54
  error.name = "LateHandlePushError";
54
55
  return error;