@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
@@ -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.
@@ -553,15 +553,12 @@ export interface RangoOptions<TEnv = any> {
553
553
  version?: string;
554
554
 
555
555
  /**
556
- * TTL (in seconds) for the in-memory prefetch cache and the
557
- * Cache-Control header on prefetch responses.
556
+ * TTL (in seconds) for the client-side in-memory prefetch cache.
558
557
  *
559
558
  * 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.
559
+ * in-memory cache. Prefetch responses carry no `Cache-Control`, so the
560
+ * browser's HTTP cache does not keep them: a prefetch whose render failed
561
+ * after its 200 went out would otherwise answer later requests.
565
562
  *
566
563
  * The cache is automatically invalidated on server actions regardless
567
564
  * 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
 
@@ -30,12 +30,14 @@ import {
30
30
  getParallelSlotEntries,
31
31
  isPprEntry,
32
32
  type EntryData,
33
+ type InterceptEntry,
33
34
  type LoaderEntry,
34
35
  } from "../../server/context.js";
35
36
  import { requestHeaders } from "../../server/request-headers.js";
36
37
  import type { HandlerContext, InternalHandlerContext } from "../../types.js";
37
38
  import type { CacheScope } from "../../cache/cache-scope.js";
38
39
  import { FILL_PARAM, PREFETCH_HEADER } from "../../prefetch-markers.js";
40
+ import { isInterceptSegment } from "../../browser/intercept-utils.js";
39
41
 
40
42
  /**
41
43
  * What a request says it is, read once when its context is created
@@ -271,40 +273,64 @@ export function resolveDeferralScope(
271
273
  return scope;
272
274
  }
273
275
 
276
+ const interceptScopes = new WeakMap<InterceptEntry, DeferralScope>();
277
+
278
+ /**
279
+ * The deferral scope of a request that resolves `intercept`: its own flagged
280
+ * loaders, nothing of the target chain. The chain's layouts below the
281
+ * declaring entry render in that request and its `matched` leaves them out
282
+ * (match-result.ts), so a hole there would stand for a segment outside the
283
+ * page. A flagged loading() inside intercept() defers nothing: the slot is
284
+ * never a unit. Memoized on the entry (replaced with its manifest on HMR).
285
+ */
286
+ export function resolveInterceptDeferralScope(
287
+ intercept: InterceptEntry,
288
+ ): DeferralScope {
289
+ const cached = interceptScopes.get(intercept);
290
+ if (cached) return cached;
291
+ const flagged = intercept.loader.filter((l) => l.prefetch === false);
292
+ const scope: DeferralScope =
293
+ flagged.length === 0
294
+ ? EMPTY_SCOPE
295
+ : {
296
+ loaders: new Map(flagged.map((l) => [l, true as const])),
297
+ loaderIds: new Set(flagged.map((l) => l.loader.$$id)),
298
+ units: new Map(),
299
+ };
300
+ interceptScopes.set(intercept, scope);
301
+ return scope;
302
+ }
303
+
274
304
  /**
275
305
  * Which half of the feature a partial request is, if either (`fill` and
276
306
  * `prefetch` are requestKind's answer). Both halves are GETs: a request of
277
307
  * another method that carries either marker is a normal request. A prefetch
278
308
  * defers only on a plain GET navigation: an action re-renders what it
279
- * changed, an intercept resolves its loaders outside the shared funnel, and a
280
- * shell capture is never a prefetch whatever the request that scheduled it
281
- * sent. The four conditions overlap today (an action is a POST, a capture
282
- * runs the full match): each one states its own reason.
309
+ * changed, and a shell capture is never a prefetch whatever the request that
310
+ * scheduled it sent. The conditions overlap today (an action is a POST, a
311
+ * capture runs the full match): each one states its own reason.
283
312
  */
284
313
  export function partialDeferralMode(request: {
285
314
  fill: boolean;
286
315
  prefetch: boolean;
287
316
  method: string;
288
317
  isAction: boolean;
289
- isIntercept: boolean;
290
318
  isShellCapture: boolean;
291
319
  }): PrefetchDeferral["mode"] {
292
320
  if (request.method !== "GET") return undefined;
293
321
  if (request.fill) return "fill";
294
- return request.prefetch &&
295
- !request.isAction &&
296
- !request.isIntercept &&
297
- !request.isShellCapture
322
+ return request.prefetch && !request.isAction && !request.isShellCapture
298
323
  ? "prefetch"
299
324
  : undefined;
300
325
  }
301
326
 
302
- const NOTHING_HELD: ReadonlySet<string> = new Set();
327
+ export const NOTHING_HELD: ReadonlySet<string> = new Set();
303
328
 
304
329
  /**
305
330
  * The plan for one match, or undefined when the tree declares no flag and the
306
331
  * request is not a fill (the common case: nothing to carry). `held` is the
307
- * client's segment ids as the request listed them.
332
+ * client's segment ids as the request listed them. `scope` is the chain's
333
+ * unless the request resolves an intercept (resolveInterceptDeferralScope).
308
334
  */
309
335
  export function planPrefetchDeferral(
310
336
  entries: readonly EntryData[],
@@ -312,8 +338,8 @@ export function planPrefetchDeferral(
312
338
  matched: { pr?: boolean; od?: boolean },
313
339
  cacheScope: CacheScope | null | undefined,
314
340
  held: ReadonlySet<string> = NOTHING_HELD,
341
+ scope: DeferralScope = resolveDeferralScope(entries),
315
342
  ): PrefetchDeferral | undefined {
316
- const scope = resolveDeferralScope(entries);
317
343
  if (scope === EMPTY_SCOPE && mode !== "fill") return undefined;
318
344
 
319
345
  const leaf = entries[entries.length - 1];
@@ -386,6 +412,30 @@ export function fillSends(
386
412
  return isFillRequest(ctx) ? !held.has(segmentId) : undefined;
387
413
  }
388
414
 
415
+ /**
416
+ * A fill that resolves an intercept sends only what the client lacks of the
417
+ * intercept: a target-chain entry (by id) or stored segment it does not hold
418
+ * lies outside the intercept's `matched` (match-result.ts), so the fill skips
419
+ * it. The intercept's own segments are never skipped.
420
+ */
421
+ export function interceptFillSkips(
422
+ match: {
423
+ handlerContext: HandlerContext<any, any>;
424
+ isIntercept: boolean;
425
+ clientSegmentSet: ReadonlySet<string>;
426
+ },
427
+ segment: string | { id: string; namespace?: string },
428
+ ): boolean {
429
+ if (!match.isIntercept) return false;
430
+ if (typeof segment !== "string") {
431
+ if (isInterceptSegment(segment)) return false;
432
+ segment = segment.id;
433
+ }
434
+ return (
435
+ fillSends(match.handlerContext, segment, match.clientSegmentSet) === true
436
+ );
437
+ }
438
+
389
439
  const RECORD_OPEN: RecordUse = { read: true, write: true, refresh: true };
390
440
 
391
441
  /** PrefetchDeferral.record, or all of it open when the match has no plan. */