@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
@@ -64,12 +64,15 @@ import {
64
64
  } from "./segment-resolution/prefetch-deferral.js";
65
65
  import {
66
66
  assertNonCacheableReadAllowed,
67
+ getLoaderReverseScope,
67
68
  isInsideLoaderScope,
69
+ scopeReverse,
68
70
  runInsideLoaderBodyScope,
69
71
  runInsideLoaderRun,
70
72
  isInsidePushCallbackScope,
71
73
  runInsidePushCallbackScope,
72
74
  type LoaderRunIdentity,
75
+ type ScopableReverse,
73
76
  } from "../server/context.js";
74
77
  import { debugLog } from "./logging.js";
75
78
 
@@ -179,6 +182,8 @@ export function wrapLoaderWithErrorHandling<T>(
179
182
  // The token is the loader's $$id (hash#export in prod, pathfrag#export in dev),
180
183
  // not a clean display name.
181
184
  const loaderName = segmentId.split(".").pop() || "unknown";
185
+ // Captured now: the rejection can settle outside the request's ALS frame.
186
+ const renderErrors = _getRequestContext()?._renderErrors;
182
187
 
183
188
  return Promise.resolve(promise)
184
189
  .then(
@@ -275,6 +280,11 @@ export function wrapLoaderWithErrorHandling<T>(
275
280
  };
276
281
  }
277
282
 
283
+ // The error renders in place inside a 200 as a value, which neither
284
+ // Flight nor Fizz reports: record it like their errors, so a writer
285
+ // that reads _renderErrors (the document cache) does not store it.
286
+ renderErrors?.push(error);
287
+
278
288
  // Find nearest error boundary
279
289
  const fallback = findNearestErrorBoundary(entry);
280
290
 
@@ -543,6 +553,11 @@ function createLoaderExecutor<TEnv>(
543
553
  // (runInsideLoaderScope in fresh.ts). Handler-invoked loaders are NOT
544
554
  // inside loader scope. This determines whether rendered() is allowed.
545
555
  const isDslLoader = isInsideLoaderScope();
556
+ // The body resolves ctx.reverse(".name") in its DSL declaration's module
557
+ // (a layout's / parallel's / cache()'s loader). A route's loader, and one
558
+ // a handler starts with ctx.use(), resolve from the route. One run per
559
+ // request: the first declaration to start it wins.
560
+ const reverseScope = getLoaderReverseScope();
546
561
 
547
562
  // Owning segment for ctx.handle() attribution, captured SYNCHRONOUSLY at
548
563
  // kickoff: segment resolution sets _currentSegmentId while resolving the
@@ -568,6 +583,7 @@ function createLoaderExecutor<TEnv>(
568
583
  InternalHandlerContext<any, TEnv>,
569
584
  | "_routeName"
570
585
  | "_responseType"
586
+ | "_reverseScope"
571
587
  | "_currentSegmentId"
572
588
  | "_claimLoaderPushes"
573
589
  | "_runLoaderIsolated"
@@ -648,9 +664,13 @@ function createLoaderExecutor<TEnv>(
648
664
  }) as LoaderContext["use"],
649
665
  method: "GET",
650
666
  body: undefined,
651
- reverse: ctx.reverse as LoaderContext["reverse"],
667
+ reverse: scopeReverse(
668
+ ctx.reverse as ScopableReverse,
669
+ reverseScope,
670
+ ) as LoaderContext["reverse"],
652
671
  _routeName: internal._routeName,
653
672
  _responseType: internal._responseType,
673
+ _reverseScope: reverseScope,
654
674
  _currentSegmentId: owningSegmentId,
655
675
  _claimLoaderPushes: internal._claimLoaderPushes,
656
676
  _runLoaderIsolated: internal._runLoaderIsolated,
@@ -11,10 +11,8 @@ import MapRootLayout from "../server/root-layout";
11
11
  import { joinPrefix } from "./pattern-matching.js";
12
12
  import type { RouteEntry } from "../types";
13
13
  import type { UrlPatterns } from "../urls";
14
- import {
15
- isIncludeProvider,
16
- resolveIncludeModule,
17
- } from "../urls/include-provider.js";
14
+ import { isIncludeProvider } from "../urls/include-provider.js";
15
+ import { resolveEntryPatterns } from "./lazy-includes.js";
18
16
  import { VERSION } from "@rangojs/router:version";
19
17
 
20
18
  // Tags an error thrown while dynamically importing an async include's module
@@ -167,6 +165,9 @@ export async function loadManifest(
167
165
  // undefined) so a prior lazy build's scope cannot leak into a later
168
166
  // non-lazy build on the same ALS-backed Store.
169
167
  Store.includeScope = lazyContext?.includeScope;
168
+ // Same rule for the include's tree position (hidden include names).
169
+ Store.scopePath = lazyContext?.scopePath;
170
+ Store.hiddenScopePath = lazyContext?.hiddenScopePath;
170
171
 
171
172
  const handlerExecStart = performance.now();
172
173
  const useItems = await getContext().runWithStore(
@@ -190,10 +191,7 @@ export async function loadManifest(
190
191
  // cache the resolved patterns on the entry so later renders reuse them.
191
192
  if (isIncludeProvider(entry.lazyPatterns)) {
192
193
  try {
193
- entry.lazyPatterns = resolveIncludeModule(
194
- await entry.lazyPatterns(),
195
- entry.staticPrefix,
196
- ) as unknown as UrlPatterns<any>;
194
+ await resolveEntryPatterns(entry);
197
195
  } catch (err) {
198
196
  // A failed dynamic import of a REAL, matched route's module is a
199
197
  // server error, not a missing route. Tag it so the outer catch
@@ -54,10 +54,12 @@ import {
54
54
  type RouteSnapshot,
55
55
  } from "./route-snapshot.js";
56
56
  import { resolveNavigation } from "./navigation-snapshot.js";
57
+ import { pathKey, visitorUrl } from "../server/rewrite-trail.js";
57
58
  import { withLayoutLoaderCopies } from "../client-urls/layout-loader-copies.js";
58
59
  import {
59
60
  partialDeferralMode,
60
61
  planPrefetchDeferral,
62
+ resolveInterceptDeferralScope,
61
63
  requestKind,
62
64
  } from "./segment-resolution/prefetch-deferral.js";
63
65
 
@@ -375,6 +377,12 @@ export async function createMatchContextForPartial<TEnv>(
375
377
  }
376
378
 
377
379
  setRequestContextPrevRouteKey(nav.effectiveFromMatch?.routeKey);
380
+ // Two visitor URLs can share a rewrite target, and the render may read
381
+ // ctx.rewrittenFrom: a move of the visitor's URL counts as a change.
382
+ if (reqCtx && nav.prevVisitorUrl) {
383
+ reqCtx._visitorMoved =
384
+ pathKey(visitorUrl(reqCtx, cleanUrl)) !== pathKey(nav.prevVisitorUrl);
385
+ }
378
386
 
379
387
  const interceptSelectorContext: InterceptSelectorContext = {
380
388
  // The source's params come from matching its URL (the intercept source
@@ -484,7 +492,6 @@ export async function createMatchContextForPartial<TEnv>(
484
492
  prefetch: kind === "prefetch",
485
493
  method: request.method,
486
494
  isAction,
487
- isIntercept,
488
495
  isShellCapture: reqCtx?._shellCaptureRun === true,
489
496
  }),
490
497
  matched,
@@ -492,6 +499,9 @@ export async function createMatchContextForPartial<TEnv>(
492
499
  // What the page holds, not the copy forced above: see
493
500
  // prefetch-deferral.ts.
494
501
  held,
502
+ interceptResult
503
+ ? resolveInterceptDeferralScope(interceptResult.intercept)
504
+ : undefined,
495
505
  );
496
506
  (handlerContext as InternalHandlerContext<any, TEnv>)._prefetchDeferral =
497
507
  deferral;
@@ -230,10 +230,8 @@ export async function rerenderAndCacheRoute<TEnv>(
230
230
  handlerContext,
231
231
  true, // belongsToRoute
232
232
  undefined, // no revalidationContext: render fresh
233
- // Skip intercept middleware: the foreground already ran it.
234
- // Re-running it here would double its side effects, and a
235
- // short-circuit Response would abort the write.
236
- { skipMiddleware: true },
233
+ // A handler error rejects, so the failure is never cached.
234
+ { background: true },
237
235
  ),
238
236
  )),
239
237
  );
@@ -101,6 +101,7 @@ import { treeHasStreaming } from "./segment-resolution.js";
101
101
  import { loaderPins } from "../segment-resolution/loader-cache.js";
102
102
  import {
103
103
  fillSends,
104
+ interceptFillSkips,
104
105
  recordUse,
105
106
  } from "../segment-resolution/prefetch-deferral.js";
106
107
  import type { PrerenderStore, PrerenderEntry } from "../../prerender/store.js";
@@ -127,6 +128,7 @@ import { ShellRecordUnavailableError } from "../../cache/shell-snapshot.js";
127
128
  import { prerenderStoreShortCircuits } from "../navigation-snapshot.js";
128
129
  import { paramsEqual } from "../params-util.js";
129
130
  import { requestHeaders } from "../../server/request-headers.js";
131
+ import { isInterceptSegment } from "../../browser/intercept-utils.js";
130
132
 
131
133
  // Lazily initialized prerender store singleton and dynamically imported deps.
132
134
  // Dynamic imports prevent pulling in @vitejs/plugin-rsc/rsc virtual module at
@@ -207,6 +209,7 @@ async function* resolveFreshLoadersAndYield<TEnv>(
207
209
  resolveLoadersOnlyWithRevalidation: RouterContext<TEnv>["resolveLoadersOnlyWithRevalidation"],
208
210
  ): AsyncGenerator<ResolvedSegment> {
209
211
  const loaderStart = performance.now();
212
+ entries = entries.filter((e) => !interceptFillSkips(ctx, e.shortCode));
210
213
 
211
214
  if (ctx.isFullMatch) {
212
215
  if (resolveLoadersOnly) {
@@ -347,14 +350,8 @@ async function* yieldFromStore<TEnv>(
347
350
  !ctx.isFullMatch && !paramsEqual(ctx.matched.params, ctx.prevParams);
348
351
  const resendRouteSegments = replaceable === true && ctx.isAction;
349
352
  for (const segment of segments) {
350
- if (
351
- !ctx.isFullMatch &&
352
- ctx.clientSegmentSet.has(segment.id) &&
353
- // A stored intercept segment is re-sent, as on the runtime cache path:
354
- // its loaders run (handleCacheHitIntercept), and a kept (null) one would
355
- // be left out of the response with its loaders' new data.
356
- !segment.namespace?.startsWith("intercept:")
357
- ) {
353
+ if (interceptFillSkips(ctx, segment)) continue;
354
+ if (!ctx.isFullMatch && ctx.clientSegmentSet.has(segment.id)) {
358
355
  // A fill keeps it (fillSends); otherwise it is sent again when the
359
356
  // params changed or an action re-sends the route's own segments.
360
357
  const sends =
@@ -912,10 +909,15 @@ export function withCacheLookup<TEnv>(
912
909
  const entryRevalidateMap = canCheckSegmentRevalidation
913
910
  ? buildEntryRevalidateMap(ctx.entries)
914
911
  : undefined;
912
+ const searchChanged =
913
+ ctx.prevUrl.search !== ctx.url.search ||
914
+ pipelineReqCtx?._visitorMoved === true;
915
+ const routeParamsChanged = !paramsEqual(ctx.matched.params, ctx.prevParams);
915
916
 
916
917
  yield* liveSegments;
917
918
 
918
919
  for (const segment of cacheResult.segments) {
920
+ if (interceptFillSkips(ctx, segment)) continue;
919
921
  if (!ctx.clientSegmentSet.has(segment.id)) {
920
922
  if (isTraceActive()) {
921
923
  pushRevalidationTraceEntry({
@@ -932,12 +934,8 @@ export function withCacheLookup<TEnv>(
932
934
  continue;
933
935
  }
934
936
 
935
- if (segment.namespace?.startsWith("intercept:")) {
936
- yield segment;
937
- continue;
938
- }
939
-
940
- // A fill keeps a held segment outright (fillSends).
937
+ // A fill keeps a held segment outright (fillSends), an intercept's
938
+ // included.
941
939
  if (
942
940
  fillSends(ctx.handlerContext, segment.id, ctx.clientSegmentSet) ===
943
941
  false
@@ -946,14 +944,12 @@ export function withCacheLookup<TEnv>(
946
944
  yield segment;
947
945
  continue;
948
946
  }
947
+ if (isInterceptSegment(segment)) {
948
+ yield segment;
949
+ continue;
950
+ }
949
951
 
950
952
  const entryInfo = entryRevalidateMap?.get(segment.id);
951
-
952
- const searchChanged = ctx.prevUrl.search !== ctx.url.search;
953
- const routeParamsChanged = !paramsEqual(
954
- ctx.matched.params,
955
- ctx.prevParams,
956
- );
957
953
  const shouldDefaultRevalidate =
958
954
  (searchChanged || routeParamsChanged) &&
959
955
  (segment.type === "route" ||
@@ -29,7 +29,7 @@
29
29
  * v
30
30
  * +----------------------+ +----------------------------+
31
31
  * | Fresh intercept? |yes>| resolveInterceptEntry() |
32
- * | (!cacheHit or | | - middleware, loaders, UI |
32
+ * | (!cacheHit or | | - loaders, UI |
33
33
  * | no intercept segs) | +----------------------------+
34
34
  * +----------------------+ |
35
35
  * | no v
@@ -59,7 +59,7 @@
59
59
  *
60
60
  * 1. Fresh intercept (no cache):
61
61
  * - Full resolution of intercept entry
62
- * - Resolves middleware, loaders, and component
62
+ * - Resolves loaders and component
63
63
  * - Yields all intercept segments
64
64
  *
65
65
  * 2. Cache hit with intercept:
@@ -106,6 +106,7 @@ import type { MatchContext, MatchPipelineState } from "../match-context.js";
106
106
  import { getRouterContext } from "../router-context.js";
107
107
  import type { GeneratorMiddleware } from "./cache-lookup.js";
108
108
  import { debugLog } from "../logging.js";
109
+ import { isInterceptSegment } from "../../browser/intercept-utils.js";
109
110
 
110
111
  /**
111
112
  * Creates intercept resolution middleware
@@ -151,7 +152,7 @@ export function withInterceptResolution<TEnv>(
151
152
 
152
153
  if (skipInterceptResolution) {
153
154
  if (ctx.interceptResult && state.cacheHit && ctx.isIntercept) {
154
- await handleCacheHitIntercept(ctx, state, segments);
155
+ yield* await handleCacheHitIntercept(ctx, state, segments);
155
156
  }
156
157
  if (ms) {
157
158
  ms.metrics.push({
@@ -212,77 +213,57 @@ export function withInterceptResolution<TEnv>(
212
213
  };
213
214
  }
214
215
 
216
+ /**
217
+ * A cache or prerender hit that carries the intercept's slot: the slot keeps
218
+ * its stored component and layout, and its loaders, never stored, resolve as
219
+ * loader segments of it. Returns those segments for the caller to yield.
220
+ */
215
221
  async function handleCacheHitIntercept<TEnv>(
216
222
  ctx: MatchContext<TEnv>,
217
223
  state: MatchPipelineState,
218
224
  segments: ResolvedSegment[],
219
- ): Promise<void> {
220
- if (!ctx.interceptResult) return;
225
+ ): Promise<ResolvedSegment[]> {
226
+ if (!ctx.interceptResult) return [];
221
227
 
222
228
  const { resolveInterceptLoadersOnly } = getRouterContext<TEnv>();
223
229
 
224
230
  const slotName = ctx.interceptResult.intercept.slotName;
225
231
 
226
- const interceptSegments = segments.filter((s) =>
227
- s.namespace?.startsWith("intercept:"),
228
- );
229
- state.interceptSegments = interceptSegments;
230
-
231
- if (resolveInterceptLoadersOnly) {
232
- const Store = ctx.Store;
233
- const freshLoaderResult = await Store.run(() =>
234
- resolveInterceptLoadersOnly(
235
- ctx.interceptResult!.intercept,
236
- ctx.interceptResult!.entry,
237
- ctx.matched.params,
238
- ctx.handlerContext,
239
- true, // belongsToRoute
240
- {
241
- clientSegmentIds: ctx.clientSegmentSet,
242
- prevParams: ctx.prevParams,
243
- request: ctx.request,
244
- prevUrl: ctx.prevUrl,
245
- nextUrl: ctx.url,
246
- routeKey: ctx.routeKey,
247
- actionContext: ctx.actionContext,
248
- stale: ctx.stale,
249
- },
250
- ),
251
- );
232
+ const cachedSlot = segments.filter(isInterceptSegment);
233
+ // A record stored before 0.25.0 carries the loaders' aggregate on the slot;
234
+ // the loader segments below replace it. Build versions hash the router's
235
+ // code, so only a `version` pinned across the upgrade (createRouter's or
236
+ // the store's) still reads one.
237
+ for (const slot of cachedSlot) {
238
+ delete slot.loaderDataPromise;
239
+ delete slot.loaderIds;
240
+ }
252
241
 
253
- if (freshLoaderResult) {
254
- const interceptMainSegment = interceptSegments.find(
255
- (s) => s.type === "parallel" && s.slot,
256
- );
257
- if (interceptMainSegment) {
258
- interceptMainSegment.loaderDataPromise =
259
- freshLoaderResult.loaderDataPromise;
260
- interceptMainSegment.loaderIds = freshLoaderResult.loaderIds;
261
- debugLog(
262
- "matchPartial.intercept",
263
- freshLoaderResult.loaderIds.length > 0
264
- ? "cache hit with fresh intercept loaders"
265
- : "cache hit, every intercept loader held",
242
+ const loaderSegments = resolveInterceptLoadersOnly
243
+ ? await ctx.Store.run(() =>
244
+ resolveInterceptLoadersOnly(
245
+ ctx.interceptResult!.intercept,
246
+ ctx.interceptResult!.entry,
247
+ ctx.handlerContext,
248
+ true, // belongsToRoute
266
249
  {
267
- routeName: ctx.localRouteName,
268
- slotName,
250
+ clientSegmentIds: ctx.clientSegmentSet,
251
+ prevParams: ctx.prevParams,
252
+ request: ctx.request,
253
+ prevUrl: ctx.prevUrl,
254
+ nextUrl: ctx.url,
255
+ routeKey: ctx.routeKey,
256
+ actionContext: ctx.actionContext,
257
+ stale: ctx.stale,
269
258
  },
270
- );
271
- }
272
- } else {
273
- debugLog(
274
- "matchPartial.intercept",
275
- "cache hit, intercept has no loaders",
276
- {
277
- routeName: ctx.localRouteName,
278
- slotName,
279
- },
280
- );
281
- }
282
- }
283
-
259
+ ),
260
+ )
261
+ : [];
262
+ const interceptSegments = [...cachedSlot, ...loaderSegments];
263
+ state.interceptSegments = interceptSegments;
284
264
  state.slots[slotName] = {
285
265
  active: true,
286
266
  segments: interceptSegments,
287
267
  };
268
+ return loaderSegments;
288
269
  }
@@ -109,10 +109,14 @@
109
109
  */
110
110
  import type { MatchResult, ResolvedSegment } from "../types.js";
111
111
  import { isPlaceholder } from "../types/segments.js";
112
+ import { loaderParentId } from "../browser/react/filter-segment-order.js";
112
113
  import type { MatchContext, MatchPipelineState } from "./match-context.js";
113
114
  import { debugLog } from "./logging.js";
114
115
  import { appendMetric } from "./metrics.js";
115
- import { collectInterceptTargetNames } from "./intercept-resolution.js";
116
+ import {
117
+ collectInterceptTargetNames,
118
+ interceptSegmentIds,
119
+ } from "./intercept-resolution.js";
116
120
 
117
121
  export async function collectSegments(
118
122
  generator: AsyncGenerator<ResolvedSegment>,
@@ -192,6 +196,25 @@ function deduplicateLoaderSegments(
192
196
  return { segments: result, removedIds };
193
197
  }
194
198
 
199
+ /**
200
+ * An intercept's `matched`: the client's ids without any of the slot's (its
201
+ * id and every loader segment on it, another intercept's into the same slot
202
+ * included), then the slot's (interceptSegmentIds). One order for the
203
+ * prefetch, its fill, an action and a revisit: a fill whose `matched` differs
204
+ * from its tree's is rendered as a new tree (browser/partial-update.ts
205
+ * fillHoles).
206
+ */
207
+ function interceptMatched(
208
+ clientIds: readonly string[],
209
+ slotIds: readonly string[],
210
+ ): string[] {
211
+ // loaderParentId leaves a non-loader id as it is: the slot's own id goes too.
212
+ return [
213
+ ...clientIds.filter((id) => loaderParentId(id) !== slotIds[0]),
214
+ ...slotIds,
215
+ ];
216
+ }
217
+
195
218
  export function buildMatchResult<TEnv>(
196
219
  allSegments: ResolvedSegment[],
197
220
  ctx: MatchContext<TEnv>,
@@ -223,7 +246,13 @@ export function buildMatchResult<TEnv>(
223
246
  } else {
224
247
  allIds = ctx.interceptResult
225
248
  ? ctx.clientSegmentIds.length > 0
226
- ? [...ctx.clientSegmentIds, ...state.interceptSegments.map((s) => s.id)]
249
+ ? interceptMatched(
250
+ ctx.clientSegmentIds,
251
+ interceptSegmentIds(
252
+ ctx.interceptResult.entry,
253
+ ctx.interceptResult.intercept,
254
+ ),
255
+ )
227
256
  : allSegments.map((s) => s.id)
228
257
  : [...state.matchedIds, ...state.interceptSegments.map((s) => s.id)];
229
258
 
@@ -34,11 +34,7 @@ import { assertNonCacheableReadAllowed } from "../server/context.js";
34
34
  import { readGuardedTheme } from "../server/cookie-store.js";
35
35
  import { defineSlot, slotAccessor } from "../server/slot-accessors.js";
36
36
  import type { Theme } from "../theme/types.js";
37
- import {
38
- isRewrite,
39
- rewriteNotSupported,
40
- type Rewrite,
41
- } from "../route-definition/rewrite.js";
37
+ import { isRewrite, type Rewrite } from "../route-definition/rewrite.js";
42
38
  import { defineRewrittenFrom } from "../server/rewrite-trail.js";
43
39
 
44
40
  // Re-export types consumed through this module's path.
@@ -324,8 +320,8 @@ export function createMiddlewareContext<TEnv>(
324
320
  waitUntil: reqCtx ? reqCtx.waitUntil.bind(reqCtx) : fireAndForgetWaitUntil,
325
321
 
326
322
  get: ((keyOrVar: any) => {
327
- // "use cache" only: intercept middleware runs inside a cache()
328
- // subtree's render scope, and a middleware read is never cached there.
323
+ // "use cache" only: middleware runs before any cache() subtree
324
+ // renders, so the cache() read guard never applies to it.
329
325
  if (
330
326
  isNonCacheable(variables, keyOrVar) &&
331
327
  getCacheExecScope() !== undefined
@@ -517,13 +513,11 @@ function mergeReqCtxStub(
517
513
  // Clone `base` with stub headers merged into a fresh Headers (the clone keeps
518
514
  // the body mutable for post-next() modifications). Set-Cookie is always
519
515
  // appended; other headers obey stubOverridesNonCookie (see mergeStubHeaders).
520
- // mergeReqCtx folds in RequestContext stub cookies/headers; the intercept
521
- // short-circuit path passes false (its reqCtx headers are not merged here),
522
- // which is the one deliberate divergence between the call sites.
516
+ // RequestContext stub cookies/headers are folded in last.
523
517
  function mergeResponse(
524
518
  base: Response,
525
519
  stub: Headers,
526
- opts: { stubOverridesNonCookie: boolean; mergeReqCtx: boolean },
520
+ opts: { stubOverridesNonCookie: boolean },
527
521
  ): Response {
528
522
  const mergedHeaders = new Headers(base.headers);
529
523
  // The reserved external-redirect marker is never a trust signal and must never
@@ -532,9 +526,7 @@ function mergeResponse(
532
526
  // guard would not touch) to the client. The opt-in is the out-of-band brand.
533
527
  mergedHeaders.delete(EXTERNAL_REDIRECT_MARKER);
534
528
  mergeStubHeaders(mergedHeaders, stub, opts.stubOverridesNonCookie);
535
- if (opts.mergeReqCtx) {
536
- mergeReqCtxStub(mergedHeaders, _getRequestContext());
537
- }
529
+ mergeReqCtxStub(mergedHeaders, _getRequestContext());
538
530
  const merged = new Response(base.body, {
539
531
  status: base.status,
540
532
  statusText: base.statusText,
@@ -597,7 +589,6 @@ export async function executeMiddleware<TEnv>(
597
589
  // and reqCtx stub headers are merged in.
598
590
  responseHolder.response = mergeResponse(response, stubResponse.headers, {
599
591
  stubOverridesNonCookie: true,
600
- mergeReqCtx: true,
601
592
  });
602
593
 
603
594
  return responseHolder.response;
@@ -740,7 +731,6 @@ export async function executeMiddleware<TEnv>(
740
731
  // (stubOverridesNonCookie=false); reqCtx stub headers still merge in.
741
732
  const merged = mergeResponse(result, stubResponse.headers, {
742
733
  stubOverridesNonCookie: false,
743
- mergeReqCtx: true,
744
734
  });
745
735
  responseHolder.response = merged;
746
736
  return merged;
@@ -796,137 +786,6 @@ export async function executeMiddleware<TEnv>(
796
786
  return finalResponse;
797
787
  }
798
788
 
799
- /**
800
- * Execute middleware for intercepts (simplified execution)
801
- *
802
- * Intercepts use a shared stubResponse from the request context. This function:
803
- * - Runs middleware in sequence with a simple next() chain
804
- * - Returns Response if any middleware short-circuits (returns OR throws a Response, or redirects, BEFORE next())
805
- * - Returns null if all middleware calls next() - headers set after next() remain on stubResponse
806
- *
807
- * @param middlewares - Array of middleware functions
808
- * @param request - Original request
809
- * @param env - Environment bindings
810
- * @param params - Route params
811
- * @param variables - Shared variables object
812
- * @param stubResponse - Response from request context for collecting headers/cookies
813
- */
814
- export async function executeInterceptMiddleware<TEnv>(
815
- middlewares: MiddlewareFn<TEnv>[],
816
- request: Request,
817
- env: TEnv,
818
- params: Record<string, string>,
819
- variables: Record<string, any>,
820
- stubResponse: Response,
821
- reverse?: (
822
- name: string,
823
- params?: Record<string, string>,
824
- search?: Record<string, unknown>,
825
- ) => string,
826
- ): Promise<Response | null> {
827
- if (middlewares.length === 0) {
828
- return null;
829
- }
830
-
831
- let index = 0;
832
- let earlyResponse: Response | null = null;
833
-
834
- // Use provided stubResponse - headers/cookies set here will be merged by the caller
835
- const responseHolder: ResponseHolder = { response: stubResponse };
836
-
837
- const next = async (): Promise<Response> => {
838
- if (index >= middlewares.length || earlyResponse) {
839
- return stubResponse;
840
- }
841
-
842
- const ordinal = index;
843
- const middleware = middlewares[index++];
844
- const ctx = createMiddlewareContext(
845
- request,
846
- env,
847
- params,
848
- variables,
849
- responseHolder,
850
- reverse,
851
- );
852
-
853
- let nextCalled = false;
854
- const guardedNext = (): Promise<Response> => {
855
- if (nextCalled) {
856
- throw new Error(
857
- `[@rangojs/router] Intercept middleware called next() more than once.`,
858
- );
859
- }
860
- nextCalled = true;
861
- return next();
862
- };
863
-
864
- // Span-wrap each intercept middleware as rango.middleware (metric:false —
865
- // intercept runs inside the render phase already metered by render:total, so
866
- // it contributes a span but no separate perf metric). Bare MiddlewareFns
867
- // have no pattern, so the label is scoped to "*" like a pattern-less entry.
868
- const label = getMiddlewareMetricLabel(
869
- { handler: middleware, pattern: null } as MiddlewareEntry<TEnv>,
870
- ordinal,
871
- );
872
-
873
- const result: Response | void = await observePhase(
874
- PHASES.middleware(label),
875
- async () => {
876
- try {
877
- return await middleware(ctx, guardedNext);
878
- } catch (error) {
879
- // Thrown Response is short-circuit control flow, parity with the
880
- // explicit-return path below. Absorb it INSIDE the span so the tracing
881
- // runner settles rango.middleware as success, not STATUS_ERROR (same
882
- // reasoning as executeMiddleware's main chain). Real errors propagate.
883
- if (error instanceof Response) return error;
884
- if (isRewrite(error)) throw rewriteNotSupported(error, "intercept");
885
- throw error;
886
- }
887
- },
888
- );
889
- if (isRewrite(result)) throw returnedRewriteError(middleware);
890
-
891
- if (result instanceof Response) {
892
- earlyResponse = result;
893
- return result;
894
- }
895
-
896
- return stubResponse;
897
- };
898
-
899
- await next();
900
-
901
- // Return early response if middleware short-circuited (returned Response BEFORE next())
902
- if (earlyResponse) {
903
- // Capture in const for TypeScript narrowing (earlyResponse is `let` which loses narrowing in callbacks)
904
- const response: Response = earlyResponse;
905
-
906
- // Merge any headers/cookies set on stub into the early response
907
- let hasStubHeaders = false;
908
- stubResponse.headers.forEach(() => {
909
- hasStubHeaders = true;
910
- });
911
-
912
- if (hasStubHeaders) {
913
- // Only fill in missing headers — the returned Response's explicit headers
914
- // take precedence (stubOverridesNonCookie=false), matching executeMiddleware.
915
- // mergeReqCtx=false: the intercept path deliberately does NOT merge reqCtx
916
- // stub headers here (pinned by intercept-middleware-headers.test.ts).
917
- return mergeResponse(response, stubResponse.headers, {
918
- stubOverridesNonCookie: false,
919
- mergeReqCtx: false,
920
- });
921
- }
922
- return response;
923
- }
924
-
925
- // All middleware completed without short-circuit
926
- // Headers/cookies set on stubResponse will be merged into the final response by the caller
927
- return null;
928
- }
929
-
930
789
  /**
931
790
  * Execute middleware chain for loaders (simpler signature)
932
791
  *