@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
@@ -16,11 +16,6 @@ import type {
16
16
  InternalHandlerContext,
17
17
  ResolvedSegment,
18
18
  } from "../types";
19
- import { evaluateRevalidation } from "./revalidation.js";
20
- import { getRequestContext } from "../server/request-context.js";
21
- import { executeInterceptMiddleware } from "./middleware.js";
22
- import { createReverseFunction } from "./handler-context.js";
23
- import { getGlobalRouteMap } from "../route-map-builder.js";
24
19
  import {
25
20
  handleHandlerResult,
26
21
  warnOnStreamedResponse,
@@ -29,15 +24,21 @@ import {
29
24
  import { catchSegmentError } from "./segment-resolution/helpers.js";
30
25
  import { trackStreamedHandler } from "./segment-resolution/streamed-handler-boundary.js";
31
26
  import { findInOrphans } from "./error-handling.js";
32
- import { resolveLoaderData } from "./segment-resolution/loader-cache.js";
27
+ import {
28
+ deliverStartedLoaders,
29
+ resolveOwnerLoaders,
30
+ } from "./segment-resolution/revalidation.js";
31
+ import {
32
+ fillSends,
33
+ NOTHING_HELD,
34
+ } from "./segment-resolution/prefetch-deferral.js";
33
35
  import type { SegmentResolutionDeps } from "./types.js";
34
- import { debugLog } from "./logging.js";
35
36
  import { getRouterContext } from "./router-context.js";
37
+ import type { RevalidationContext } from "./router-context.js";
36
38
  import { runInSegmentTagScope } from "../cache/cache-tag.js";
37
39
  import {
38
40
  RangoContext,
39
41
  latchPprHeaderScopeForEntries,
40
- runInsideLoaderScope,
41
42
  } from "../server/context.js";
42
43
 
43
44
  /**
@@ -48,7 +49,6 @@ import {
48
49
  * intercept over a ppr-declared target route, in its own store scope where
49
50
  * the main funnel's latch does not apply. Latch by declaration, off the
50
51
  * target route's manifest entry (same leaf input as the segment funnels).
51
- * Called AFTER intercept middleware runs — middleware stays the live lane.
52
52
  */
53
53
  function latchPprHeaderScopeForInterceptTarget(
54
54
  interceptEntry: InterceptEntry,
@@ -172,7 +172,7 @@ export function findInterceptForRoute(
172
172
  * An intercept handler's throw or notFound() renders the declaring entry's
173
173
  * boundary as the slot component (catchSegmentError, as for route handlers on
174
174
  * partial navigations: 500/404 status, onError "handler"). Responses and every
175
- * error on the background re-render (skipMiddleware) rethrow, so a failed
175
+ * error on the background re-render (`background`) rethrow, so a failed
176
176
  * re-render aborts its cache write instead of storing the fallback.
177
177
  */
178
178
  function renderInterceptHandlerError<TEnv>(
@@ -181,9 +181,9 @@ function renderInterceptHandlerError<TEnv>(
181
181
  params: Record<string, string>,
182
182
  context: HandlerContext<any, TEnv>,
183
183
  deps: SegmentResolutionDeps<TEnv>,
184
- skipMiddleware: boolean | undefined,
184
+ background: boolean | undefined,
185
185
  ): ReactNode {
186
- if (error instanceof Response || skipMiddleware) throw error;
186
+ if (error instanceof Response || background) throw error;
187
187
  return catchSegmentError(
188
188
  error,
189
189
  parentEntry,
@@ -198,8 +198,80 @@ function renderInterceptHandlerError<TEnv>(
198
198
  ).component;
199
199
  }
200
200
 
201
+ /** The segment id of `intercept`'s slot on `parentEntry`. */
202
+ export function interceptSlotId(
203
+ parentEntry: EntryData,
204
+ intercept: InterceptEntry,
205
+ ): string {
206
+ return `${parentEntry.shortCode}.${intercept.slotName}`;
207
+ }
208
+
209
+ /**
210
+ * Every segment id an intercept's slot owns, in order: the slot, then one
211
+ * loader segment per loader in declaration order, run or not. A request that
212
+ * resolves the intercept lists them in `matched` (match-result.ts).
213
+ */
214
+ export function interceptSegmentIds(
215
+ parentEntry: EntryData,
216
+ intercept: InterceptEntry,
217
+ ): string[] {
218
+ const slotId = interceptSlotId(parentEntry, intercept);
219
+ return [
220
+ slotId,
221
+ ...intercept.loader.map((l, i) => `${slotId}D${i}.${l.loader.$$id}`),
222
+ ];
223
+ }
224
+
225
+ /**
226
+ * An intercept's loaders are loader segments of its slot, resolved through
227
+ * the partial lane's funnel (revalidation.ts resolveOwnerLoaders): each one
228
+ * revalidated on its own, kept in a fill, deferred by a prefetch when flagged.
229
+ */
230
+ function resolveInterceptLoaders<TEnv>(
231
+ interceptEntry: InterceptEntry,
232
+ parentEntry: EntryData,
233
+ context: HandlerContext<any, TEnv>,
234
+ belongsToRoute: boolean,
235
+ deps: SegmentResolutionDeps<TEnv>,
236
+ rc: RevalidationContext,
237
+ ): Promise<{ segments: ResolvedSegment[] }> {
238
+ return resolveOwnerLoaders(
239
+ {
240
+ loaders: interceptEntry.loader,
241
+ namespace: `intercept:${interceptEntry.routeName}`,
242
+ segmentPrefix: interceptSlotId(parentEntry, interceptEntry),
243
+ boundary: parentEntry,
244
+ bakeLane: false,
245
+ traceSource: "intercept-loader",
246
+ },
247
+ context,
248
+ belongsToRoute,
249
+ rc.clientSegmentIds,
250
+ rc.prevParams,
251
+ rc.request,
252
+ rc.prevUrl,
253
+ rc.nextUrl,
254
+ rc.routeKey,
255
+ deps,
256
+ rc.actionContext,
257
+ rc.stale,
258
+ );
259
+ }
260
+
201
261
  /**
202
- * Resolve an intercept entry and emit segment with the slot name.
262
+ * An intercept slot with no loading() waits for its loaders that are not
263
+ * deferred; the browser's side is foldInterceptLoaders (segment-system.tsx).
264
+ */
265
+ async function settleLoaderSegments(
266
+ segments: readonly ResolvedSegment[],
267
+ ): Promise<void> {
268
+ await Promise.all(
269
+ segments.filter((s) => !s.deferred).map((s) => s.loaderData),
270
+ );
271
+ }
272
+
273
+ /**
274
+ * Resolve an intercept entry: its slot segment, then its loader segments.
203
275
  */
204
276
  export async function resolveInterceptEntry<TEnv>(
205
277
  interceptEntry: InterceptEntry,
@@ -208,150 +280,68 @@ export async function resolveInterceptEntry<TEnv>(
208
280
  context: HandlerContext<any, TEnv>,
209
281
  belongsToRoute: boolean,
210
282
  deps: SegmentResolutionDeps<TEnv>,
211
- revalidationContext?: {
212
- clientSegmentIds: Set<string>;
213
- prevParams: Record<string, string>;
214
- request: Request;
215
- prevUrl: URL;
216
- nextUrl: URL;
217
- routeKey: string;
218
- actionContext?: {
219
- actionId?: string;
220
- actionUrl?: URL;
221
- actionResult?: any;
222
- formData?: FormData;
223
- };
224
- stale?: boolean;
283
+ // The background re-render passes none: nothing held, rendered fresh.
284
+ revalidationContext: RevalidationContext = {
285
+ clientSegmentIds: NOTHING_HELD,
286
+ prevParams: {},
287
+ request: context.request,
288
+ prevUrl: context.url,
289
+ nextUrl: context.url,
290
+ routeKey: interceptEntry.routeName,
225
291
  },
226
292
  options?: {
227
293
  /**
228
- * Skip the intercept's middleware execution. Set ONLY by the post-response
229
- * background re-render paths (proactive caching, stale background
294
+ * Set ONLY by the post-response background re-render (stale background
230
295
  * revalidation), whose sole purpose is to re-render the segment tree to
231
- * populate the cache. The foreground request already ran the intercept
232
- * middleware before the response was sent — it validated auth, set cookies,
233
- * and wrote context vars into the request context's shared `_variables`,
234
- * which the background render reuses. Re-running middleware here would fire
235
- * its side effects a SECOND time, and a middleware that short-circuits with
236
- * a Response would `throw` and silently abort the cache write. Never set on
237
- * the foreground path.
296
+ * populate the cache: a handler error rejects instead of rendering the
297
+ * error boundary, so a failure is never cached. Never set on the
298
+ * foreground path.
238
299
  */
239
- skipMiddleware?: boolean;
300
+ background?: boolean;
240
301
  },
241
302
  ): Promise<ResolvedSegment[]> {
242
- const segments: ResolvedSegment[] = [];
243
-
244
- if (!options?.skipMiddleware && interceptEntry.middleware.length > 0) {
245
- const requestCtx = getRequestContext();
246
- if (!requestCtx?.res) {
247
- throw new Error(
248
- "Request context with stubResponse is required for intercept middleware",
249
- );
250
- }
251
- const middlewareResponse = await executeInterceptMiddleware(
252
- interceptEntry.middleware,
253
- context.request,
254
- context.env,
255
- params,
256
- (context as InternalHandlerContext<any, TEnv>)._variables,
257
- requestCtx.res,
258
- createReverseFunction(getGlobalRouteMap()),
259
- );
260
- if (middlewareResponse) throw middlewareResponse;
261
- }
262
-
263
303
  latchPprHeaderScopeForInterceptTarget(interceptEntry);
264
304
 
265
- // The intercept segment's id: the tag scope its handler and layout record in
266
- // (#957, cache-tag.ts runInSegmentTagScope) and the owner of its handler's
267
- // and loaders' handle pushes, pinned before the loader kickoffs as
268
- // fresh.ts/revalidation.ts pin theirs (createLoaderExecutor captures it at
269
- // kickoff; a handler's ctx.use(Handle) reads it when called).
270
- const slotSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
305
+ const slotSegmentId = interceptSlotId(parentEntry, interceptEntry);
306
+ const { segments: loaderSegments } = await resolveInterceptLoaders(
307
+ interceptEntry,
308
+ parentEntry,
309
+ context,
310
+ belongsToRoute,
311
+ deps,
312
+ revalidationContext,
313
+ );
314
+ // The slot's tag scope (#957, cache-tag.ts runInSegmentTagScope) and the
315
+ // owner of its handler's handle pushes (a handler's ctx.use(Handle) reads
316
+ // it when called).
271
317
  (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
272
318
  slotSegmentId;
273
- const hasLoaders = interceptEntry.loader.length > 0;
274
-
275
- const loaderPromises: Promise<any>[] = [];
276
- const loaderIds: string[] = [];
277
-
278
- for (let i = 0; i < interceptEntry.loader.length; i++) {
279
- const loaderEntry = interceptEntry.loader[i];
280
- const { loader, revalidate: loaderRevalidateFns } = loaderEntry;
281
- const segmentId = `${slotSegmentId}D${i}.${loader.$$id}`;
282
-
283
- if (revalidationContext) {
284
- const {
285
- clientSegmentIds,
286
- prevParams,
287
- request,
288
- prevUrl,
289
- nextUrl,
290
- routeKey,
291
- actionContext,
292
- stale,
293
- } = revalidationContext;
294
-
295
- if (clientSegmentIds.has(slotSegmentId)) {
296
- const dummySegment: ResolvedSegment = {
297
- id: segmentId,
298
- namespace: `intercept:${interceptEntry.routeName}`,
299
- type: "loader",
300
- index: i,
301
- component: null,
302
- params,
303
- loaderId: loader.$$id,
304
- belongsToRoute,
305
- };
306
-
307
- const shouldRevalidate = await evaluateRevalidation({
308
- segment: dummySegment,
309
- prevParams,
310
- getPrevSegment: null,
311
- request,
312
- prevUrl,
313
- nextUrl,
314
- revalidations: loaderRevalidateFns.map((fn, j) => ({
315
- name: `intercept-loader-revalidate${j}`,
316
- fn,
317
- })),
318
- routeKey,
319
- context,
320
- actionContext,
321
- stale,
322
- traceSource: "intercept-loader",
323
- });
324
-
325
- if (!shouldRevalidate) {
326
- debugLog("intercept.loader", "skipped revalidation", {
327
- loaderId: loader.$$id,
328
- });
329
- continue;
330
- }
331
- debugLog("intercept.loader", "revalidating", {
332
- loaderId: loader.$$id,
333
- stale,
334
- });
335
- }
336
- }
337
319
 
338
- loaderIds.push(loader.$$id);
339
- loaderPromises.push(
340
- deps.wrapLoaderPromise(
341
- // The DSL binding's funnel, as for route loaders: a loader's own
342
- // cache() applies here too.
343
- runInsideLoaderScope(
344
- () => resolveLoaderData(loaderEntry, context, context.pathname),
345
- loader.$$id,
346
- ),
347
- parentEntry,
348
- segmentId,
349
- context.pathname,
350
- // Report a throwing intercept loader to onError + loader.error telemetry,
351
- // matching the fresh/revalidation paths.
352
- buildLoaderErrorContext(context),
353
- ),
354
- );
320
+ const slotSegment = (
321
+ component: ReactNode,
322
+ layout: ReactNode | undefined,
323
+ ): ResolvedSegment => ({
324
+ id: slotSegmentId,
325
+ namespace: `intercept:${interceptEntry.routeName}`,
326
+ type: "parallel",
327
+ index: 0,
328
+ component,
329
+ loading: interceptEntry.loading === false ? null : interceptEntry.loading,
330
+ layout,
331
+ params,
332
+ slot: interceptEntry.slotName,
333
+ belongsToRoute,
334
+ parallelName: `intercept:${interceptEntry.routeName}.${interceptEntry.slotName}`,
335
+ });
336
+
337
+ // A fill keeps a slot the client holds: neither its handler nor its layout
338
+ // runs, only the loaders the client lacks (match-result.ts leaves the null
339
+ // slot out of the response).
340
+ if (
341
+ fillSends(context, slotSegmentId, revalidationContext.clientSegmentIds) ===
342
+ false
343
+ ) {
344
+ return [slotSegment(null, undefined), ...loaderSegments];
355
345
  }
356
346
 
357
347
  let handlerResult: ReactNode;
@@ -370,7 +360,7 @@ export async function resolveInterceptEntry<TEnv>(
370
360
  params,
371
361
  context,
372
362
  deps,
373
- options?.skipMiddleware,
363
+ options?.background,
374
364
  );
375
365
  }
376
366
  // Consumed only after the layout and loader awaits below; a rejection in that
@@ -397,11 +387,10 @@ export async function resolveInterceptEntry<TEnv>(
397
387
  }
398
388
 
399
389
  let component: ReactNode;
400
- let loaderDataPromise: Promise<any[]> | any[] | undefined;
401
390
  // A streamed (loading()) handler resolves after the 200 has started: tracked
402
391
  // like route handlers (onError report, handle-store completion) and
403
- // recovered into the declared boundary, except on the skipMiddleware
404
- // background re-render, which must reject so a failure is never cached.
392
+ // recovered into the declared boundary, except on the background
393
+ // re-render, which must reject so a failure is never cached.
405
394
  const streamedComponent = (promise: Promise<unknown>): ReactNode =>
406
395
  trackStreamedHandler(deps, promise, {
407
396
  boundaryEntry: parentEntry,
@@ -410,10 +399,10 @@ export async function resolveInterceptEntry<TEnv>(
410
399
  context,
411
400
  routeKey: interceptEntry.routeName,
412
401
  params,
413
- recover: !options?.skipMiddleware,
402
+ recover: !options?.background,
414
403
  });
415
404
 
416
- if (interceptEntry.loading && loaderPromises.length > 0) {
405
+ if (interceptEntry.loading && loaderSegments.length > 0) {
417
406
  if (handlerResult instanceof Promise) {
418
407
  warnOnStreamedResponse(
419
408
  handlerResult,
@@ -424,13 +413,10 @@ export async function resolveInterceptEntry<TEnv>(
424
413
  handlerResult instanceof Promise
425
414
  ? streamedComponent(handlerResult)
426
415
  : (Promise.resolve(handlerResult) as ReactNode);
427
- loaderDataPromise = Promise.all(loaderPromises);
428
416
  } else if (interceptEntry.loading && handlerResult instanceof Promise) {
429
417
  component = streamedComponent(handlerResult);
430
418
  } else {
431
- if (loaderPromises.length > 0) {
432
- loaderDataPromise = await Promise.all(loaderPromises);
433
- }
419
+ if (!interceptEntry.loading) await settleLoaderSegments(loaderSegments);
434
420
  component = handlerResult;
435
421
  if (handlerResult instanceof Promise) {
436
422
  try {
@@ -442,172 +428,40 @@ export async function resolveInterceptEntry<TEnv>(
442
428
  params,
443
429
  context,
444
430
  deps,
445
- options?.skipMiddleware,
431
+ options?.background,
446
432
  );
447
433
  }
448
434
  }
449
435
  }
436
+ // After the handler is awaited, or at once when it streams: the loaders it
437
+ // started with ctx.use() by now are delivered from this response.
438
+ deliverStartedLoaders(context, loaderSegments);
450
439
 
451
- // The aggregate lists the loaders that ran; empty, not absent, when the
452
- // client holds the slot and all skipped. Absent replaces the client's
453
- // aggregate, empty merges into it (merge-segment-loaders.ts).
454
- if (hasLoaders && loaderPromises.length === 0) {
455
- loaderDataPromise = [];
456
- }
457
-
458
- const interceptSegment = {
459
- id: slotSegmentId,
460
- namespace: `intercept:${interceptEntry.routeName}`,
461
- type: "parallel" as const,
462
- index: 0,
463
- component,
464
- loading: interceptEntry.loading === false ? null : interceptEntry.loading,
465
- layout: layoutElement,
466
- params,
467
- slot: interceptEntry.slotName,
468
- belongsToRoute,
469
- parallelName: `intercept:${interceptEntry.routeName}.${interceptEntry.slotName}`,
470
- loaderDataPromise,
471
- loaderIds: hasLoaders ? loaderIds : undefined,
472
- };
473
- segments.push(interceptSegment);
474
-
475
- return segments;
440
+ return [slotSegment(component, layoutElement), ...loaderSegments];
476
441
  }
477
442
 
478
443
  /**
479
- * Resolve only the loaders for a cached intercept segment.
480
- * Used on intercept cache hit to get fresh loader data while keeping cached component/layout.
444
+ * The loader segments of a cached intercept slot, on a cache hit: the slot
445
+ * keeps its cached component and layout, its loaders are never cached.
481
446
  */
482
447
  export async function resolveInterceptLoadersOnly<TEnv>(
483
448
  interceptEntry: InterceptEntry,
484
449
  parentEntry: EntryData,
485
- params: Record<string, string>,
486
450
  context: HandlerContext<any, TEnv>,
487
451
  belongsToRoute: boolean,
488
452
  deps: SegmentResolutionDeps<TEnv>,
489
- revalidationContext: {
490
- clientSegmentIds: Set<string>;
491
- prevParams: Record<string, string>;
492
- request: Request;
493
- prevUrl: URL;
494
- nextUrl: URL;
495
- routeKey: string;
496
- actionContext?: {
497
- actionId?: string;
498
- actionUrl?: URL;
499
- actionResult?: any;
500
- formData?: FormData;
501
- };
502
- stale?: boolean;
503
- },
504
- ): Promise<{
505
- loaderDataPromise: Promise<any[]> | any[];
506
- loaderIds: string[];
507
- } | null> {
508
- if (interceptEntry.loader.length === 0) {
509
- return null;
510
- }
511
-
453
+ revalidationContext: RevalidationContext,
454
+ ): Promise<ResolvedSegment[]> {
512
455
  latchPprHeaderScopeForInterceptTarget(interceptEntry);
513
-
514
- // The loaders' handle push owner, as in resolveInterceptEntry.
515
- const slotSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
516
- (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
517
- slotSegmentId;
518
- const loaderPromises: Promise<any>[] = [];
519
- const loaderIds: string[] = [];
520
-
521
- const {
522
- clientSegmentIds,
523
- prevParams,
524
- request,
525
- prevUrl,
526
- nextUrl,
527
- routeKey,
528
- actionContext,
529
- stale,
530
- } = revalidationContext;
531
-
532
- for (let i = 0; i < interceptEntry.loader.length; i++) {
533
- const loaderEntry = interceptEntry.loader[i];
534
- const { loader, revalidate: loaderRevalidateFns } = loaderEntry;
535
- const segmentId = `${slotSegmentId}D${i}.${loader.$$id}`;
536
-
537
- if (clientSegmentIds.has(slotSegmentId)) {
538
- const dummySegment: ResolvedSegment = {
539
- id: segmentId,
540
- namespace: `intercept:${interceptEntry.routeName}`,
541
- type: "loader",
542
- index: i,
543
- component: null,
544
- params,
545
- loaderId: loader.$$id,
546
- belongsToRoute,
547
- };
548
-
549
- const shouldRevalidate = await evaluateRevalidation({
550
- segment: dummySegment,
551
- prevParams,
552
- getPrevSegment: null,
553
- request,
554
- prevUrl,
555
- nextUrl,
556
- revalidations: loaderRevalidateFns.map((fn, j) => ({
557
- name: `intercept-loader-revalidate${j}`,
558
- fn,
559
- })),
560
- routeKey,
561
- context,
562
- actionContext,
563
- stale,
564
- traceSource: "intercept-loader",
565
- });
566
-
567
- if (!shouldRevalidate) {
568
- debugLog("intercept.loader", "skipped on cache hit", {
569
- loaderId: loader.$$id,
570
- });
571
- continue;
572
- }
573
- debugLog("intercept.loader", "revalidating on cache hit", {
574
- loaderId: loader.$$id,
575
- stale,
576
- });
577
- }
578
-
579
- loaderIds.push(loader.$$id);
580
- loaderPromises.push(
581
- deps.wrapLoaderPromise(
582
- // The DSL binding's funnel, as for route loaders: a loader's own
583
- // cache() applies here too.
584
- runInsideLoaderScope(
585
- () => resolveLoaderData(loaderEntry, context, context.pathname),
586
- loader.$$id,
587
- ),
588
- parentEntry,
589
- segmentId,
590
- context.pathname,
591
- // Report a throwing intercept loader to onError + loader.error telemetry,
592
- // matching the fresh/revalidation paths. resolveInterceptLoadersOnly is
593
- // only called on the cache-hit partial-update path (handleCacheHitIntercept),
594
- // so flag isPartial:true exactly like revalidation.ts's partial path.
595
- { ...buildLoaderErrorContext(context), isPartial: true },
596
- ),
597
- );
598
- }
599
-
600
- // Every loader held: the empty aggregate (see resolveInterceptEntry).
601
- if (loaderPromises.length === 0) {
602
- return { loaderDataPromise: [], loaderIds };
603
- }
604
-
605
- // Match fresh-path semantics: only defer (no await) when loading is truthy.
606
- // `loading: false` means "no loading UI, await loaders before render" —
607
- // same as the fresh path's `if (interceptEntry.loading && ...)` check.
608
- const loaderDataPromise = interceptEntry.loading
609
- ? Promise.all(loaderPromises)
610
- : await Promise.all(loaderPromises);
611
-
612
- return { loaderDataPromise, loaderIds };
456
+ const { segments } = await resolveInterceptLoaders(
457
+ interceptEntry,
458
+ parentEntry,
459
+ context,
460
+ belongsToRoute,
461
+ deps,
462
+ revalidationContext,
463
+ );
464
+ if (!interceptEntry.loading) await settleLoaderSegments(segments);
465
+ deliverStartedLoaders(context, segments);
466
+ return segments;
613
467
  }
@@ -102,36 +102,51 @@ export function evaluateLazyEntry<TEnv = any>(
102
102
  }
103
103
 
104
104
  // Async provider (`() => import("./routes")`): the route module is evaluated
105
- // off the startup path, on the first request reaching this prefix. Concurrent
106
- // first-hits share one in-flight promise so the import + expansion run exactly
107
- // once. The eager path below stays fully synchronous (no Promise), so the
108
- // per-entry match loop pays no microtask for normal includes.
105
+ // off the startup path, on the first request reaching this prefix. The eager
106
+ // path below stays fully synchronous (no Promise), so the per-entry match
107
+ // loop pays no microtask for normal includes.
109
108
  const lazyPatterns = entry.lazyPatterns;
110
109
  if (isIncludeProvider(lazyPatterns)) {
111
- const inflight = (entry as { _lazyInflight?: Promise<void> })._lazyInflight;
112
- if (inflight) return inflight;
113
- const work = (async () => {
114
- const resolved = resolveIncludeModule(
115
- await lazyPatterns(),
116
- entry.staticPrefix,
117
- );
118
- // Cache the resolved patterns: any later re-entry expands synchronously.
119
- entry.lazyPatterns = resolved as unknown as UrlPatterns<TEnv>;
120
- runExpansion(entry, deps, resolved as UrlPatterns<TEnv>);
121
- })();
122
- (entry as { _lazyInflight?: Promise<void> })._lazyInflight = work;
123
- const clear = () => {
124
- (entry as { _lazyInflight?: Promise<void> })._lazyInflight = undefined;
125
- };
126
- // On failure, clear the flag (lazyEvaluated stays false) so a later request
127
- // can retry the import rather than wedging the route permanently.
128
- work.then(clear, clear);
129
- return work;
110
+ // Concurrent first-hits share the import; the first to resume expands.
111
+ return resolveEntryPatterns(entry).then((resolved) => {
112
+ if (!entry.lazyEvaluated) runExpansion(entry, deps, resolved);
113
+ });
130
114
  }
131
115
 
132
116
  runExpansion(entry, deps, lazyPatterns as UrlPatterns<TEnv>);
133
117
  }
134
118
 
119
+ type ProviderEntry<TEnv> = RouteEntry<TEnv> & {
120
+ _providerInflight?: Promise<UrlPatterns<TEnv>>;
121
+ };
122
+
123
+ /**
124
+ * Resolve an async include entry's provider once and cache the patterns on
125
+ * `entry.lazyPatterns`. Concurrent callers (match-time expansion and
126
+ * render-time loadManifest) share one in-flight import; it clears when it
127
+ * settles, so a rejected import is retried by the next request.
128
+ */
129
+ export function resolveEntryPatterns<TEnv = any>(
130
+ entry: RouteEntry<TEnv>,
131
+ ): Promise<UrlPatterns<TEnv>> {
132
+ const target = entry as ProviderEntry<TEnv>;
133
+ const lazyPatterns = target.lazyPatterns;
134
+ if (!isIncludeProvider(lazyPatterns)) {
135
+ return Promise.resolve(lazyPatterns as UrlPatterns<TEnv>);
136
+ }
137
+ target._providerInflight ??= (async () => {
138
+ const resolved = resolveIncludeModule(
139
+ await lazyPatterns(),
140
+ target.staticPrefix,
141
+ ) as UrlPatterns<TEnv>;
142
+ target.lazyPatterns = resolved;
143
+ return resolved;
144
+ })().finally(() => {
145
+ target._providerInflight = undefined;
146
+ });
147
+ return target._providerInflight;
148
+ }
149
+
135
150
  /**
136
151
  * Synchronously expand a lazy entry's (already-resolved) patterns into routes
137
152
  * and splice any nested lazy includes as new entries. Runs once per entry.
@@ -143,10 +158,9 @@ function runExpansion<TEnv = any>(
143
158
  ): void {
144
159
  // lazyEvaluated is set at the END, only after the handler ran and the routes
145
160
  // (and any nested includes) were spliced. Setting it up-front would mark the
146
- // entry done even if the handler throws mid-expansion: the async provider
147
- // path clears _lazyInflight on rejection so a later request can retry, but a
148
- // premature lazyEvaluated=true would make evaluateLazyEntry short-circuit
149
- // (line ~77) forever, wedging the route at 404 until the isolate restarts.
161
+ // entry done even if the handler throws mid-expansion, so a later request
162
+ // could not retry: evaluateLazyEntry would short-circuit forever, wedging the
163
+ // route at 404 until the isolate restarts.
150
164
  const lazyContext = entry.lazyContext;
151
165
 
152
166
  const manifest = new Map<string, EntryData>();
@@ -173,6 +187,8 @@ function runExpansion<TEnv = any>(
173
187
  rootScoped: lazyContext?.rootScoped,
174
188
  routerId: lazyContext?.routerId,
175
189
  includeScope: lazyContext?.includeScope,
190
+ scopePath: lazyContext?.scopePath,
191
+ hiddenScopePath: lazyContext?.hiddenScopePath,
176
192
  },
177
193
  () => {
178
194
  const includePrefix = (entry as any)._lazyPrefix || "";