@rangojs/router 0.24.4 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. package/README.md +1 -0
  2. package/dist/bin/rango.js +363 -39
  3. package/dist/testing/vitest.js +37 -4
  4. package/dist/types/browser/event-controller.d.ts +24 -5
  5. package/dist/types/browser/intercept-utils.d.ts +5 -2
  6. package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
  7. package/dist/types/browser/navigation-transaction.d.ts +5 -5
  8. package/dist/types/browser/partial-update.d.ts +21 -13
  9. package/dist/types/browser/prefetch/cache.d.ts +8 -0
  10. package/dist/types/browser/rango-marker.d.ts +7 -0
  11. package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
  12. package/dist/types/browser/react/use-handle.d.ts +20 -0
  13. package/dist/types/browser/react/use-href.d.ts +6 -0
  14. package/dist/types/browser/react/use-router.d.ts +6 -1
  15. package/dist/types/browser/response-adapter.d.ts +5 -3
  16. package/dist/types/browser/scroll-restoration.d.ts +9 -3
  17. package/dist/types/browser/segment-reconciler.d.ts +10 -13
  18. package/dist/types/browser/types.d.ts +38 -8
  19. package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +0 -2
  21. package/dist/types/cache/cf/cf-cache-types.d.ts +0 -16
  22. package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
  23. package/dist/types/cache/handle-snapshot.d.ts +2 -2
  24. package/dist/types/cache/types.d.ts +16 -6
  25. package/dist/types/cache/vercel/vercel-cache-store.d.ts +1 -15
  26. package/dist/types/client-urls/client-urls.d.ts +6 -2
  27. package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
  28. package/dist/types/client-urls/server-projection.d.ts +9 -5
  29. package/dist/types/client-urls/types.d.ts +29 -16
  30. package/dist/types/client.d.ts +2 -2
  31. package/dist/types/client.rsc.d.ts +2 -2
  32. package/dist/types/decode-loader-results.d.ts +28 -7
  33. package/dist/types/handles/deferred-resolution.d.ts +3 -2
  34. package/dist/types/handles/meta.d.ts +5 -0
  35. package/dist/types/href-client.d.ts +3 -1
  36. package/dist/types/index.rsc.d.ts +1 -1
  37. package/dist/types/loader-redirect.d.ts +2 -3
  38. package/dist/types/outlet-context.d.ts +7 -6
  39. package/dist/types/prefetch-markers.d.ts +14 -5
  40. package/dist/types/prerender/build-shell-capture.d.ts +1 -2
  41. package/dist/types/prerender/writable-store.d.ts +2 -3
  42. package/dist/types/route-content-wrapper.d.ts +17 -14
  43. package/dist/types/route-definition/helpers-types.d.ts +7 -2
  44. package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
  45. package/dist/types/route-definition/rewrite.d.ts +48 -5
  46. package/dist/types/route-types.d.ts +2 -2
  47. package/dist/types/router/handler-context.d.ts +14 -2
  48. package/dist/types/router/intercept-resolution.d.ts +19 -46
  49. package/dist/types/router/lazy-includes.d.ts +7 -0
  50. package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
  51. package/dist/types/router/middleware.d.ts +0 -16
  52. package/dist/types/router/navigation-snapshot.d.ts +2 -0
  53. package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
  54. package/dist/types/router/revalidation.d.ts +8 -1
  55. package/dist/types/router/router-context.d.ts +4 -16
  56. package/dist/types/router/router-interfaces.d.ts +0 -11
  57. package/dist/types/router/router-options.d.ts +4 -27
  58. package/dist/types/router/segment-resolution/mask-nested.d.ts +5 -3
  59. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
  60. package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
  61. package/dist/types/router/segment-resolution.d.ts +1 -1
  62. package/dist/types/router/segment-wrappers.d.ts +3 -5
  63. package/dist/types/router/substitute-pattern-params.d.ts +3 -2
  64. package/dist/types/router/tracing.d.ts +2 -2
  65. package/dist/types/router-versions.d.ts +8 -6
  66. package/dist/types/rsc/full-payload.d.ts +14 -2
  67. package/dist/types/rsc/helpers.d.ts +8 -0
  68. package/dist/types/rsc/manifest-init.d.ts +20 -0
  69. package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
  70. package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
  71. package/dist/types/rsc/rewrite-passes.d.ts +12 -2
  72. package/dist/types/rsc/server-action.d.ts +6 -0
  73. package/dist/types/rsc/shell-capture-constants.d.ts +14 -4
  74. package/dist/types/rsc/shell-capture.d.ts +23 -2
  75. package/dist/types/rsc/types.d.ts +5 -8
  76. package/dist/types/segment-system.d.ts +15 -0
  77. package/dist/types/server/build-version-table.d.ts +10 -13
  78. package/dist/types/server/context.d.ts +90 -6
  79. package/dist/types/server/handle-store.d.ts +17 -6
  80. package/dist/types/server/request-context.d.ts +45 -7
  81. package/dist/types/server/request-headers.d.ts +12 -0
  82. package/dist/types/server/rewrite-trail.d.ts +8 -1
  83. package/dist/types/testing/build-versions.d.ts +0 -3
  84. package/dist/types/testing/e2e/index.d.ts +1 -1
  85. package/dist/types/testing/flight.entry.d.ts +1 -1
  86. package/dist/types/testing/index.d.ts +1 -1
  87. package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
  88. package/dist/types/testing/internal/seed-vars.d.ts +4 -4
  89. package/dist/types/testing/render-route.d.ts +21 -11
  90. package/dist/types/testing/run-loader.d.ts +7 -0
  91. package/dist/types/testing/run-middleware.d.ts +7 -0
  92. package/dist/types/testing/run-revalidate.d.ts +5 -4
  93. package/dist/types/testing/serve-shell-request.d.ts +42 -5
  94. package/dist/types/testing/shell-status.d.ts +17 -0
  95. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  96. package/dist/types/testing/vitest.d.ts +10 -1
  97. package/dist/types/thenable-status.d.ts +0 -6
  98. package/dist/types/types/handler-context.d.ts +12 -1
  99. package/dist/types/types/loader-types.d.ts +10 -4
  100. package/dist/types/types/route-entry.d.ts +2 -1
  101. package/dist/types/urls/path-helper-types.d.ts +4 -2
  102. package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
  103. package/dist/types/vite/discovery/state.d.ts +20 -0
  104. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
  105. package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
  106. package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
  107. package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
  108. package/dist/vite/index.js +6819 -6555
  109. package/package.json +1 -1
  110. package/skills/app-structure/SKILL.md +255 -0
  111. package/skills/app-structure/content-site.md +333 -0
  112. package/skills/app-structure/e-commerce.md +435 -0
  113. package/skills/app-structure/large-app.md +885 -0
  114. package/skills/app-structure/multi-app.md +280 -0
  115. package/skills/breadcrumbs/SKILL.md +6 -1
  116. package/skills/cache-guide/SKILL.md +4 -8
  117. package/skills/caching/SKILL.md +6 -6
  118. package/skills/catalog.json +12 -0
  119. package/skills/client-urls/SKILL.md +185 -32
  120. package/skills/comparison/references/framework-comparison.md +1 -1
  121. package/skills/composability/SKILL.md +10 -8
  122. package/skills/handler-use/SKILL.md +48 -9
  123. package/skills/hooks/handle-and-actions.md +104 -0
  124. package/skills/hooks/navigation.md +24 -10
  125. package/skills/hooks/urls.md +1 -1
  126. package/skills/host-router/SKILL.md +1 -1
  127. package/skills/i18n/SKILL.md +2 -1
  128. package/skills/intercept/SKILL.md +60 -29
  129. package/skills/layout/SKILL.md +6 -3
  130. package/skills/links/SKILL.md +151 -17
  131. package/skills/loader/SKILL.md +42 -19
  132. package/skills/middleware/SKILL.md +111 -15
  133. package/skills/observability/SKILL.md +11 -0
  134. package/skills/parallel/SKILL.md +5 -5
  135. package/skills/ppr/SKILL.md +103 -52
  136. package/skills/prerender/SKILL.md +4 -5
  137. package/skills/rango/SKILL.md +3 -0
  138. package/skills/response-routes/SKILL.md +4 -1
  139. package/skills/router-setup/SKILL.md +0 -2
  140. package/skills/scripts/SKILL.md +12 -1
  141. package/skills/server-actions/SKILL.md +60 -20
  142. package/skills/testing/SKILL.md +1 -0
  143. package/skills/testing/client-components.md +13 -10
  144. package/skills/testing/loader.md +25 -24
  145. package/skills/testing/middleware.md +11 -11
  146. package/skills/testing/render-handler.md +17 -17
  147. package/skills/testing/response-routes.md +4 -1
  148. package/skills/testing/serve-shell-request.md +86 -22
  149. package/skills/testing/server-actions.md +16 -16
  150. package/skills/ux-architecture/SKILL.md +980 -0
  151. package/skills/vercel/SKILL.md +1 -1
  152. package/skills/view-transitions/SKILL.md +43 -10
  153. package/src/__augment-tests__/augmented.check.ts +28 -0
  154. package/src/browser/event-controller.ts +44 -18
  155. package/src/browser/intercept-utils.ts +3 -2
  156. package/src/browser/merge-segment-loaders.ts +0 -85
  157. package/src/browser/navigation-bridge.ts +112 -84
  158. package/src/browser/navigation-client.ts +20 -15
  159. package/src/browser/navigation-store.ts +4 -2
  160. package/src/browser/navigation-transaction.ts +9 -8
  161. package/src/browser/partial-update.ts +90 -32
  162. package/src/browser/prefetch/cache.ts +8 -0
  163. package/src/browser/prefetch/fetch.ts +13 -3
  164. package/src/browser/rango-marker.ts +13 -0
  165. package/src/browser/react/NavigationProvider.tsx +25 -7
  166. package/src/browser/react/filter-segment-order.ts +13 -0
  167. package/src/browser/react/use-action.ts +9 -15
  168. package/src/browser/react/use-handle.ts +63 -71
  169. package/src/browser/react/use-href.tsx +24 -5
  170. package/src/browser/react/use-link-status.ts +1 -1
  171. package/src/browser/react/use-navigation.ts +30 -40
  172. package/src/browser/react/use-router.ts +37 -6
  173. package/src/browser/react/use-segments.ts +8 -12
  174. package/src/browser/response-adapter.ts +11 -5
  175. package/src/browser/rsc-router.tsx +3 -1
  176. package/src/browser/scroll-restoration.ts +34 -20
  177. package/src/browser/segment-reconciler.ts +11 -29
  178. package/src/browser/server-action-bridge.ts +52 -16
  179. package/src/browser/types.ts +40 -8
  180. package/src/build/generate-manifest.ts +12 -1
  181. package/src/build/route-types/codegen.ts +6 -2
  182. package/src/build/route-types/per-module-writer.ts +94 -61
  183. package/src/cache/cache-runtime.ts +9 -0
  184. package/src/cache/cache-scope.ts +2 -1
  185. package/src/cache/cf/cf-cache-store.ts +6 -7
  186. package/src/cache/cf/cf-cache-types.ts +0 -17
  187. package/src/cache/cf/cf-shell-frame.ts +3 -0
  188. package/src/cache/handle-snapshot.ts +2 -2
  189. package/src/cache/types.ts +16 -6
  190. package/src/cache/vercel/vercel-cache-store.ts +8 -19
  191. package/src/client-urls/client-urls.ts +202 -153
  192. package/src/client-urls/revalidate-registry.ts +12 -3
  193. package/src/client-urls/server-projection.ts +137 -66
  194. package/src/client-urls/types.ts +30 -15
  195. package/src/client.rsc.tsx +11 -1
  196. package/src/client.tsx +9 -1
  197. package/src/decode-loader-results.ts +64 -31
  198. package/src/handles/deferred-resolution.ts +3 -2
  199. package/src/handles/meta.ts +5 -0
  200. package/src/href-client.ts +3 -1
  201. package/src/index.rsc.ts +1 -0
  202. package/src/index.ts +12 -4
  203. package/src/loader-redirect.tsx +2 -3
  204. package/src/outlet-context.ts +7 -6
  205. package/src/prefetch-markers.ts +15 -5
  206. package/src/prerender/build-shell-capture.ts +1 -2
  207. package/src/prerender/writable-store.ts +2 -3
  208. package/src/reverse.ts +3 -3
  209. package/src/route-content-wrapper.tsx +19 -17
  210. package/src/route-definition/dsl-helpers.ts +41 -21
  211. package/src/route-definition/helpers-types.ts +7 -2
  212. package/src/route-definition/resolve-handler-use.ts +23 -10
  213. package/src/route-definition/rewrite.ts +86 -19
  214. package/src/route-types.ts +1 -2
  215. package/src/router/find-match.ts +37 -1
  216. package/src/router/handler-context.ts +85 -40
  217. package/src/router/intercept-resolution.ts +161 -307
  218. package/src/router/lazy-includes.ts +43 -27
  219. package/src/router/loader-resolution.ts +21 -1
  220. package/src/router/manifest.ts +15 -10
  221. package/src/router/match-api.ts +11 -1
  222. package/src/router/match-middleware/background-revalidation.ts +2 -4
  223. package/src/router/match-middleware/cache-lookup.ts +16 -20
  224. package/src/router/match-middleware/intercept-resolution.ts +41 -60
  225. package/src/router/match-result.ts +31 -2
  226. package/src/router/middleware.ts +6 -147
  227. package/src/router/navigation-snapshot.ts +38 -2
  228. package/src/router/prefetch-cache-ttl.ts +10 -18
  229. package/src/router/prerender-match.ts +2 -1
  230. package/src/router/revalidation.ts +24 -3
  231. package/src/router/router-context.ts +5 -18
  232. package/src/router/router-interfaces.ts +0 -13
  233. package/src/router/router-options.ts +4 -28
  234. package/src/router/segment-resolution/fresh.ts +6 -2
  235. package/src/router/segment-resolution/helpers.ts +7 -1
  236. package/src/router/segment-resolution/loader-cache.ts +4 -3
  237. package/src/router/segment-resolution/mask-nested.ts +5 -3
  238. package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
  239. package/src/router/segment-resolution/revalidation.ts +75 -35
  240. package/src/router/segment-resolution.ts +0 -1
  241. package/src/router/segment-wrappers.ts +8 -13
  242. package/src/router/substitute-pattern-params.ts +3 -2
  243. package/src/router/tracing.ts +2 -2
  244. package/src/router-versions.ts +7 -7
  245. package/src/router.ts +5 -17
  246. package/src/rsc/full-payload.ts +26 -4
  247. package/src/rsc/handler.ts +86 -48
  248. package/src/rsc/helpers.ts +13 -0
  249. package/src/rsc/manifest-init.ts +56 -3
  250. package/src/rsc/prefetch-render-error.ts +38 -0
  251. package/src/rsc/progressive-enhancement.ts +161 -211
  252. package/src/rsc/rewrite-passes.ts +56 -9
  253. package/src/rsc/rsc-rendering.ts +79 -23
  254. package/src/rsc/server-action.ts +25 -2
  255. package/src/rsc/shell-capture-constants.ts +26 -3
  256. package/src/rsc/shell-capture.ts +137 -51
  257. package/src/rsc/types.ts +5 -9
  258. package/src/segment-system.tsx +125 -119
  259. package/src/server/build-version-table.ts +12 -24
  260. package/src/server/context.ts +160 -7
  261. package/src/server/handle-store.ts +80 -12
  262. package/src/server/request-context.ts +46 -6
  263. package/src/server/request-headers.ts +32 -0
  264. package/src/server/rewrite-trail.ts +55 -9
  265. package/src/ssr/ssr-root.tsx +6 -10
  266. package/src/testing/build-versions.ts +0 -3
  267. package/src/testing/dispatch.ts +5 -8
  268. package/src/testing/e2e/index.ts +1 -0
  269. package/src/testing/flight.entry.ts +1 -0
  270. package/src/testing/index.ts +1 -0
  271. package/src/testing/internal/client-urls-projection.ts +31 -11
  272. package/src/testing/internal/seed-vars.ts +4 -4
  273. package/src/testing/render-route.tsx +83 -40
  274. package/src/testing/run-loader.ts +26 -2
  275. package/src/testing/run-middleware.ts +10 -0
  276. package/src/testing/run-revalidate.ts +13 -6
  277. package/src/testing/serve-shell-request.ts +79 -9
  278. package/src/testing/shell-status.ts +40 -0
  279. package/src/testing/vitest-stubs/version.ts +1 -0
  280. package/src/testing/vitest.ts +74 -7
  281. package/src/thenable-status.ts +0 -11
  282. package/src/types/handler-context.ts +12 -1
  283. package/src/types/loader-types.ts +10 -4
  284. package/src/types/route-entry.ts +2 -1
  285. package/src/urls/include-helper.ts +38 -11
  286. package/src/urls/path-helper-types.ts +4 -2
  287. package/src/vite/discovery/client-urls-projection.ts +205 -16
  288. package/src/vite/discovery/discover-routers.ts +2 -2
  289. package/src/vite/discovery/prerender-collection.ts +2 -6
  290. package/src/vite/discovery/shell-prerender-phase.ts +1 -5
  291. package/src/vite/discovery/state.ts +26 -0
  292. package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
  293. package/src/vite/plugins/expose-internal-ids.ts +46 -40
  294. package/src/vite/plugins/hoist/source.ts +8 -7
  295. package/src/vite/plugins/transition-when-hoist.ts +1 -0
  296. package/src/vite/plugins/version-plugin.ts +7 -0
  297. package/src/vite/plugins/version.d.ts +7 -0
  298. package/src/vite/plugins/virtual-entries.ts +20 -0
  299. package/src/vite/rango.ts +4 -1
  300. package/src/vite/router-discovery.ts +39 -7
  301. package/src/vite/utils/prerender-utils.ts +17 -0
@@ -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
  *
@@ -4,6 +4,8 @@ import type { RouteMatchResult } from "./pattern-matching.js";
4
4
 
5
5
  export interface NavigationSnapshot {
6
6
  prevUrl: URL;
7
+ /** The visitor's URL of the page left, when `prevUrl` is its rewrite target. */
8
+ prevVisitorUrl: URL | null;
7
9
  prevParams: Record<string, string>;
8
10
  prevMatch: RouteMatchResult | null;
9
11
 
@@ -95,6 +97,35 @@ export function getInterceptSourceHeader(request: Request): string | null {
95
97
  return requestHeaders(request).get("X-RSC-Router-Intercept-Source");
96
98
  }
97
99
 
100
+ /**
101
+ * The URL the page left rendered, when middleware rewrote it there (#1144):
102
+ * `_rsc_rt`, the payload's `rewriteTarget` the browser sends back while
103
+ * `X-RSC-Router-Client-Path` names the visitor's URL. Matching the visitor's
104
+ * URL would find the rewriting route. Same origin only. It names the page
105
+ * left, never what renders, so a forged value runs or skips no middleware.
106
+ */
107
+ function getShownRouteUrl(url: URL): URL | null {
108
+ const shown = url.searchParams.get("_rsc_rt");
109
+ if (!shown) return null;
110
+ try {
111
+ const parsed = new URL(shown, url.origin);
112
+ return parsed.origin === url.origin ? formSearch(parsed) : null;
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * `url` with its query as URLSearchParams writes it, as the request's own URL
120
+ * has it: setting and stripping `_rsc*` params rewrote that one (`?q=a%2Cb`),
121
+ * while a URL the browser names keeps the address bar's (`?q=a,b`). Compared
122
+ * as sent, every navigation from such a page was a search change.
123
+ */
124
+ function formSearch(url: URL): URL {
125
+ url.search = String(url.searchParams);
126
+ return url;
127
+ }
128
+
98
129
  export async function resolveNavigation(
99
130
  request: Request,
100
131
  url: URL,
@@ -112,12 +143,15 @@ export async function resolveNavigation(
112
143
  return null;
113
144
  }
114
145
 
115
- let prevUrl: URL;
146
+ let visitor: URL;
116
147
  try {
117
- prevUrl = new URL(previousUrl, url.origin);
148
+ visitor = formSearch(new URL(previousUrl, url.origin));
118
149
  } catch {
119
150
  return null;
120
151
  }
152
+ // An intercept's request diffs from its source, which _rsc_rt does not name.
153
+ const shown = interceptSourceUrl ? null : getShownRouteUrl(url);
154
+ const prevUrl = shown ?? visitor;
121
155
 
122
156
  let interceptContextUrl: URL;
123
157
  try {
@@ -154,6 +188,7 @@ export async function resolveNavigation(
154
188
 
155
189
  return {
156
190
  prevUrl,
191
+ prevVisitorUrl: shown && visitor,
157
192
  prevParams,
158
193
  prevMatch,
159
194
  interceptContextUrl,
@@ -176,6 +211,7 @@ export function createNavigationSnapshot(
176
211
  const defaultUrl = new URL("http://localhost/");
177
212
  return {
178
213
  prevUrl: defaultUrl,
214
+ prevVisitorUrl: null,
179
215
  prevParams: {},
180
216
  prevMatch: null,
181
217
  interceptContextUrl: defaultUrl,
@@ -1,15 +1,14 @@
1
1
  /**
2
- * Resolve the prefetch cache TTL once, at router init, into the three derived
3
- * values the rest of the router consumes: seconds (for the Cache-Control
4
- * max-age), milliseconds (for the client-side in-memory cache), and the
5
- * Cache-Control header string (or false when caching is disabled).
2
+ * Resolve the prefetch cache TTL once, at router init, into the values the
3
+ * rest of the router consumes: seconds, and milliseconds for the client-side
4
+ * in-memory prefetch cache (shipped in payload metadata). Prefetch responses
5
+ * carry no Cache-Control: the browser HTTP cache would keep a prefetch whose
6
+ * render failed after its 200 went out (#1170).
6
7
  *
7
8
  * `false` disables prefetch caching (seconds 0). A non-finite input
8
9
  * (NaN/Infinity, e.g. from an env-derived number that failed to parse) is
9
- * treated as the default rather than producing a malformed
10
- * `Cache-Control: max-age=NaN` header; CDNs/browsers ignore such a directive,
11
- * which would silently disable caching on EVERY prefetch response. Negative
12
- * finite values clamp to 0 (disabled).
10
+ * treated as the default rather than disabling the cache. Negative finite
11
+ * values clamp to 0 (disabled).
13
12
  *
14
13
  * Policy note: this is the prefetch-TTL policy specifically. Other finite-number
15
14
  * guards in the codebase deliberately differ — defer.ts treats Infinity as an
@@ -17,12 +16,10 @@
17
16
  * at config time. They are NOT the same guard, so don't unify them.
18
17
  */
19
18
  export interface ResolvedPrefetchCacheTTL {
20
- /** TTL in seconds for the Cache-Control max-age directive. */
19
+ /** TTL in seconds. */
21
20
  seconds: number;
22
21
  /** TTL in milliseconds for the client-side in-memory prefetch cache. */
23
22
  ms: number;
24
- /** Cache-Control header value, or false when caching is disabled. */
25
- cacheControl: string | false;
26
23
  }
27
24
 
28
25
  const DEFAULT_PREFETCH_CACHE_TTL_SECONDS = 300;
@@ -38,14 +35,9 @@ export function resolvePrefetchCacheTTL(
38
35
  } else if (typeof rawTTL === "number" && Number.isFinite(rawTTL)) {
39
36
  seconds = Math.max(0, Math.floor(rawTTL));
40
37
  } else {
41
- // Non-finite (NaN/Infinity): fall back to the default rather than emit a
42
- // malformed max-age=NaN/Infinity that CDNs and browsers reject.
38
+ // Non-finite (NaN/Infinity): fall back to the default.
43
39
  seconds = DEFAULT_PREFETCH_CACHE_TTL_SECONDS;
44
40
  }
45
41
 
46
- return {
47
- seconds,
48
- ms: seconds * 1000,
49
- cacheControl: seconds === 0 ? false : `private, max-age=${seconds}`,
50
- };
42
+ return { seconds, ms: seconds * 1000 };
51
43
  }
@@ -16,6 +16,7 @@ import { detectPrerenderPassthrough } from "../prerender.js";
16
16
  import { isRouteRootScoped } from "../route-map-builder.js";
17
17
  import { setupBuildUse } from "./loader-resolution.js";
18
18
  import { findInOrphans } from "./error-handling.js";
19
+ import { interceptSlotId } from "./intercept-resolution.js";
19
20
  import { loadManifest } from "./manifest.js";
20
21
  import { traverseBack } from "./pattern-matching.js";
21
22
  import type { RouterContext } from "./router-context.js";
@@ -487,7 +488,7 @@ export async function matchForPrerender<TEnv = any>(
487
488
  // artifact. Re-pinned per iteration so multiple intercepts targeting
488
489
  // the same route each get their own id (mirrors fresh.ts parallel-slot
489
490
  // pinning).
490
- const interceptSegmentId = `${parentEntry.shortCode}.${intercept.slotName}`;
491
+ const interceptSegmentId = interceptSlotId(parentEntry, intercept);
491
492
  (buildCtx as InternalHandlerContext<any, TEnv>)._currentSegmentId =
492
493
  interceptSegmentId;
493
494
 
@@ -4,7 +4,11 @@
4
4
  * Evaluates whether segments should revalidate based on params, actions, and custom functions.
5
5
  */
6
6
 
7
- import type { ResolvedSegment, HandlerContext } from "../types";
7
+ import type {
8
+ ResolvedSegment,
9
+ HandlerContext,
10
+ ShouldRevalidateFn,
11
+ } from "../types";
8
12
  import type { ActionContext } from "./types";
9
13
  import {
10
14
  debugLog,
@@ -17,6 +21,22 @@ import { isAutoGeneratedRouteName } from "../route-name.js";
17
21
  import { paramsEqual } from "./params-util.js";
18
22
  import { makeIsAction } from "./is-action.js";
19
23
 
24
+ const REVALIDATE_ALWAYS: ShouldRevalidateFn<any, any> = () => true;
25
+ const REVALIDATE_NEVER: ShouldRevalidateFn<any, any> = () => false;
26
+
27
+ /**
28
+ * The predicate `revalidate(true)` / `revalidate(false)` stands for: the
29
+ * constant hard decision of `revalidate(() => true)` / `() => false`.
30
+ * Booleans are converted where they enter (the urls() helper, clientUrls()
31
+ * materialization, runRevalidate), so evaluation only ever sees functions.
32
+ */
33
+ export function toShouldRevalidateFn(
34
+ fn: ShouldRevalidateFn<any, any> | boolean,
35
+ ): ShouldRevalidateFn<any, any> {
36
+ if (typeof fn !== "boolean") return fn;
37
+ return fn ? REVALIDATE_ALWAYS : REVALIDATE_NEVER;
38
+ }
39
+
20
40
  /**
21
41
  * Options for revalidation evaluation
22
42
  */
@@ -85,7 +105,9 @@ export async function evaluateRevalidation<TEnv>(
85
105
  } = options;
86
106
  const nextParams = segment.params || {};
87
107
  const paramsChanged = !paramsEqual(nextParams, prevParams);
88
- const searchChanged = prevUrl.search !== nextUrl.search;
108
+ const reqCtx = _getRequestContext();
109
+ const searchChanged =
110
+ prevUrl.search !== nextUrl.search || reqCtx?._visitorMoved === true;
89
111
 
90
112
  function pushTrace(
91
113
  defaultVal: boolean,
@@ -197,7 +219,6 @@ export async function evaluateRevalidation<TEnv>(
197
219
 
198
220
  const toRouteName =
199
221
  routeKey && !isAutoGeneratedRouteName(routeKey) ? routeKey : undefined;
200
- const reqCtx = _getRequestContext();
201
222
  const prevRouteKey = reqCtx?._prevRouteKey;
202
223
  const fromRouteName =
203
224
  prevRouteKey && !isAutoGeneratedRouteName(prevRouteKey)
@@ -25,7 +25,7 @@ import type { ResolveSegmentOptions } from "./segment-resolution.js";
25
25
  * Revalidation context passed to segment resolution
26
26
  */
27
27
  export interface RevalidationContext {
28
- clientSegmentIds: Set<string>;
28
+ clientSegmentIds: ReadonlySet<string>;
29
29
  prevParams: Record<string, string>;
30
30
  request: Request;
31
31
  prevUrl: URL;
@@ -37,7 +37,7 @@ export interface RevalidationContext {
37
37
  actionResult?: any;
38
38
  formData?: FormData;
39
39
  };
40
- stale: boolean;
40
+ stale?: boolean;
41
41
  }
42
42
 
43
43
  /**
@@ -159,7 +159,7 @@ export interface RouterContext<TEnv = any> {
159
159
  handlerContext: HandlerContext<any, TEnv>,
160
160
  belongsToRoute: boolean,
161
161
  revalidationContext?: RevalidationContext,
162
- options?: { skipMiddleware?: boolean },
162
+ options?: { background?: boolean },
163
163
  ) => Promise<ResolvedSegment[]>;
164
164
 
165
165
  collectWithMarkers?: <T>(
@@ -250,23 +250,10 @@ export interface RouterContext<TEnv = any> {
250
250
  resolveInterceptLoadersOnly?: (
251
251
  intercept: InterceptEntry,
252
252
  entry: EntryData,
253
- params: Record<string, string>,
254
253
  handlerContext: HandlerContext<any, TEnv>,
255
254
  belongsToRoute: boolean,
256
- revalidationContext: {
257
- clientSegmentIds: Set<string>;
258
- prevParams: Record<string, string>;
259
- request: Request;
260
- prevUrl: URL;
261
- nextUrl: URL;
262
- routeKey: string;
263
- actionContext?: any;
264
- stale?: boolean;
265
- },
266
- ) => Promise<{
267
- loaderDataPromise: Promise<any[]> | any[];
268
- loaderIds: string[];
269
- } | null>;
255
+ revalidationContext: RevalidationContext,
256
+ ) => Promise<ResolvedSegment[]>;
270
257
  }
271
258
 
272
259
  const routerContext = new AsyncLocalStorage<RouterContext<any>>();
@@ -382,13 +382,6 @@ export interface RangoInternal<
382
382
  import("../cache/profile-registry.js").CacheProfile
383
383
  >;
384
384
 
385
- /**
386
- * Cache-Control header value for prefetch responses.
387
- * False means no caching of prefetch responses.
388
- * Derived from prefetchCacheTTL.
389
- */
390
- readonly prefetchCacheControl: string | false;
391
-
392
385
  /**
393
386
  * TTL in milliseconds for the client-side in-memory prefetch cache.
394
387
  * 0 means caching is disabled.
@@ -486,12 +479,6 @@ export interface RangoInternal<
486
479
  */
487
480
  readonly nonce?: NonceProvider<TEnv>;
488
481
 
489
- /**
490
- * The consumer-set version (createRouter({ version })), used for both of the
491
- * router's versions. Undefined when the router uses its build versions.
492
- */
493
- readonly version?: string;
494
-
495
482
  /**
496
483
  * URL patterns reference for build-time manifest generation
497
484
  */
@@ -532,36 +532,12 @@ export interface RangoOptions<TEnv = any> {
532
532
  nonce?: NonceProvider<TEnv>;
533
533
 
534
534
  /**
535
- * Version override. When set, this exact value is used for both of the
536
- * router's versions: it keys the router's cached data (segments, `"use
537
- * cache"` values, loader data) and its stored HTML (PPR shells,
538
- * document-cache responses), is sent in payload metadata, and is what the
539
- * browser's `_rsc_v` is compared with (a mismatch reloads the tab).
540
- *
541
- * Leave it unset for the default: rango computes two versions per router at
542
- * build time, a data version from the router's server code and a document
543
- * version that also covers the client assets. The same code builds to the
544
- * same versions, so a deploy that does not change this router keeps its
545
- * cache. In dev both are a stamp bumped on every RSC module edit.
546
- *
547
- * Set it to clear this router's cache on demand (change the value), or to
548
- * tie the cache to your own release id (a per-deploy value clears the cache
549
- * on every deploy).
550
- *
551
- * @default the router's build versions
552
- */
553
- version?: string;
554
-
555
- /**
556
- * TTL (in seconds) for the in-memory prefetch cache and the
557
- * Cache-Control header on prefetch responses.
535
+ * TTL (in seconds) for the client-side in-memory prefetch cache.
558
536
  *
559
537
  * Controls how long prefetch responses are kept in the client-side
560
- * in-memory cache and sets `Cache-Control: private, max-age=<ttl>`
561
- * on prefetch responses, so the browser's HTTP cache may reuse them
562
- * (`private`: shared caches such as a CDN do not store them). A route an
563
- * intercept targets gets no prefetch `Cache-Control`: whether its
564
- * response is the modal or the full page depends on the source page.
538
+ * in-memory cache. Prefetch responses carry no `Cache-Control`, so the
539
+ * browser's HTTP cache does not keep them: a prefetch whose render failed
540
+ * after its 200 went out would otherwise answer later requests.
565
541
  *
566
542
  * The cache is automatically invalidated on server actions regardless
567
543
  * of TTL, so this is primarily a staleness safety net.
@@ -38,6 +38,7 @@ import { _getRequestContext } from "../../server/request-context.js";
38
38
  import { getRouterContext } from "../router-context.js";
39
39
  import { trackStreamedHandler } from "./streamed-handler-boundary.js";
40
40
  import { observeSegmentHandler } from "../instrument.js";
41
+ import { withReverseScope } from "../handler-context.js";
41
42
  import {
42
43
  track,
43
44
  RangoContext,
@@ -177,6 +178,7 @@ export async function resolveLoaders<TEnv>(
177
178
  bakeLane || loaderEntry.bake === true ? segmentId : null,
178
179
  ),
179
180
  loader.$$id,
181
+ loaderEntry.reverseScope,
180
182
  ),
181
183
  entry,
182
184
  segmentId,
@@ -227,6 +229,7 @@ export async function resolveLoaders<TEnv>(
227
229
  bakeLane || loaderEntry.bake === true ? segmentId : null,
228
230
  ),
229
231
  loader.$$id,
232
+ loaderEntry.reverseScope,
230
233
  ),
231
234
  entry,
232
235
  segmentId,
@@ -646,6 +649,7 @@ export async function resolveParallelEntry<TEnv>(
646
649
  >;
647
650
 
648
651
  const slotsToResolve = slotNames ?? (Object.keys(slots) as `@${string}`[]);
652
+ const slotContext = withReverseScope(context, parallelEntry.reverseScope);
649
653
 
650
654
  for (const slot of slotsToResolve) {
651
655
  // Try static lookup first — in production, handler bodies are evicted
@@ -683,7 +687,7 @@ export async function resolveParallelEntry<TEnv>(
683
687
  `${parentShortCode}.${slot}`,
684
688
  `${parallelEntry.id}.${slot}`,
685
689
  handler,
686
- context,
690
+ slotContext,
687
691
  )
688
692
  : handler;
689
693
  if (result instanceof Promise) {
@@ -708,7 +712,7 @@ export async function resolveParallelEntry<TEnv>(
708
712
  `${parentShortCode}.${slot}`,
709
713
  `${parallelEntry.id}.${slot}`,
710
714
  handler,
711
- context,
715
+ slotContext,
712
716
  )
713
717
  : handler;
714
718
  doneParallelHandler();
@@ -30,6 +30,7 @@ import type { SegmentResolutionDeps } from "../types.js";
30
30
  import { debugLog } from "../logging.js";
31
31
  import { tryStaticLookup } from "./static-store.js";
32
32
  import { observeSegmentHandler } from "../instrument.js";
33
+ import { withReverseScope } from "../handler-context.js";
33
34
  import type { TelemetrySink } from "../telemetry.js";
34
35
  import { resolveSink, safeEmit, getRequestId } from "../telemetry.js";
35
36
 
@@ -176,7 +177,12 @@ export async function resolveLayoutComponent<TEnv>(
176
177
  // the span so a handler that returns a Response (redirect control flow, which it
177
178
  // rethrows) is not recorded as a span error — mirrors the route-handler sites.
178
179
  return handleHandlerResult(
179
- await observeSegmentHandler(entry.shortCode, entry.id, handler, context),
180
+ await observeSegmentHandler(
181
+ entry.shortCode,
182
+ entry.id,
183
+ handler,
184
+ withReverseScope(context, entry.reverseScope),
185
+ ),
180
186
  );
181
187
  }
182
188
 
@@ -378,9 +378,10 @@ function getLoaderStore(
378
378
  * push funnel reads the pushReplayed owner, shell-capture.ts), so a replay
379
379
  * of that shell that serves this loader from its pin restores them owned
380
380
  * (HandleStore.pushRestored), on a document HIT and on a navigation alike.
381
- * This replay still runs there: the store drops its settled values, which
382
- * the record already restored, and keeps the thenable ones the record could
383
- * not keep (a deferred push, the reason the loader's pin carries `runs`).
381
+ * This replay still runs there: the store drops its values that settle
382
+ * thenable-free, which the record already restored, and keeps the ones
383
+ * holding a promise the record could not keep (the reason the loader's pin
384
+ * carries `runs`).
384
385
  */
385
386
  async function replayLoaderHandles(
386
387
  encoded: string | undefined,
@@ -125,9 +125,11 @@ function someNestedThenable(
125
125
  /**
126
126
  * True when `value` is a thenable or holds one where the capture's loader
127
127
  * push funnel masks it (maskNestedContainerThenables): through plain data
128
- * objects, arrays and React element props. Such a push stays out of a shell's
129
- * record and reaches a HIT only from a run of its loader there, so a HIT
130
- * keeps it; a push without one is the record's (HandleStore.pushRestored).
128
+ * objects, arrays and React element props. A settled push that holds one
129
+ * stays out of a shell's record and reaches a HIT only from a run of its
130
+ * loader there, so a HIT keeps it; a push without one is the record's
131
+ * (HandleStore.pushRestored). A top-level thenable is judged by the value it
132
+ * settles to, on both sides (#1182).
131
133
  */
132
134
  export function holdsThenable(value: unknown): boolean {
133
135
  return someNestedThenable(value, () => true);