@rangojs/router 0.18.0 → 0.19.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 (244) hide show
  1. package/dist/types/browser/event-controller.d.ts +3 -3
  2. package/dist/types/browser/history-state.d.ts +2 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +20 -2
  4. package/dist/types/browser/partial-update.d.ts +16 -1
  5. package/dist/types/browser/prefetch/cache.d.ts +6 -6
  6. package/dist/types/browser/react/Link.d.ts +19 -9
  7. package/dist/types/browser/react/context.d.ts +2 -2
  8. package/dist/types/browser/react/location-state-shared.d.ts +95 -56
  9. package/dist/types/browser/react/location-state.d.ts +1 -1
  10. package/dist/types/browser/transition-when.d.ts +80 -0
  11. package/dist/types/browser/types.d.ts +104 -9
  12. package/dist/types/build/generate-manifest.d.ts +8 -0
  13. package/dist/types/cache/background-task.d.ts +24 -2
  14. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  15. package/dist/types/cache/cache-key-utils.d.ts +44 -13
  16. package/dist/types/cache/cache-policy.d.ts +1 -1
  17. package/dist/types/cache/cache-runtime.d.ts +7 -0
  18. package/dist/types/cache/cache-scope.d.ts +117 -35
  19. package/dist/types/cache/cache-tag.d.ts +24 -4
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +55 -44
  21. package/dist/types/cache/cf/cf-shell-frame.d.ts +1 -1
  22. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +0 -15
  23. package/dist/types/cache/document-cache.d.ts +10 -0
  24. package/dist/types/cache/handle-snapshot.d.ts +40 -8
  25. package/dist/types/cache/invalidation-order.d.ts +23 -0
  26. package/dist/types/cache/request-tag-mask.d.ts +54 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +88 -141
  28. package/dist/types/cache/tag-invalidation.d.ts +38 -14
  29. package/dist/types/cache/types.d.ts +68 -61
  30. package/dist/types/cache/vercel/vercel-cache-store.d.ts +23 -16
  31. package/dist/types/client-urls/navigation.d.ts +28 -2
  32. package/dist/types/client-urls/server-projection.d.ts +5 -2
  33. package/dist/types/client-urls/types.d.ts +5 -5
  34. package/dist/types/client.d.ts +1 -1
  35. package/dist/types/index.d.ts +1 -1
  36. package/dist/types/index.rsc.d.ts +1 -1
  37. package/dist/types/route-definition/helpers-types.d.ts +11 -11
  38. package/dist/types/route-name.d.ts +7 -0
  39. package/dist/types/router/error-handling.d.ts +1 -1
  40. package/dist/types/router/intercept-resolution.d.ts +2 -0
  41. package/dist/types/router/match-context.d.ts +5 -0
  42. package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
  43. package/dist/types/router/router-options.d.ts +4 -1
  44. package/dist/types/router/segment-resolution/loader-cache.d.ts +18 -1
  45. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
  46. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +14 -40
  47. package/dist/types/router/segment-resolution/mask-nested.d.ts +11 -3
  48. package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
  49. package/dist/types/router/transition-when.d.ts +24 -10
  50. package/dist/types/rsc/attach-transition-when.d.ts +18 -0
  51. package/dist/types/rsc/full-payload.d.ts +14 -7
  52. package/dist/types/rsc/response-cache-serve.d.ts +15 -12
  53. package/dist/types/rsc/shell-build-manifest.d.ts +4 -3
  54. package/dist/types/rsc/shell-capture-constants.d.ts +77 -20
  55. package/dist/types/rsc/shell-capture.d.ts +78 -81
  56. package/dist/types/rsc/shell-serve.d.ts +19 -48
  57. package/dist/types/rsc/types.d.ts +2 -0
  58. package/dist/types/segment-system.d.ts +7 -0
  59. package/dist/types/server/context.d.ts +89 -61
  60. package/dist/types/server/cookie-store.d.ts +30 -18
  61. package/dist/types/server/handle-store.d.ts +52 -1
  62. package/dist/types/server/request-context.d.ts +66 -45
  63. package/dist/types/server/request-headers.d.ts +44 -0
  64. package/dist/types/server.d.ts +1 -0
  65. package/dist/types/testing/e2e/index.d.ts +1 -1
  66. package/dist/types/testing/index.d.ts +3 -2
  67. package/dist/types/testing/internal/action-id.d.ts +7 -0
  68. package/dist/types/testing/location-state-key.d.ts +24 -0
  69. package/dist/types/testing/render-route.d.ts +17 -1
  70. package/dist/types/testing/run-transition-when.d.ts +44 -69
  71. package/dist/types/testing/serve-shell-request.d.ts +7 -6
  72. package/dist/types/testing/shell-status.d.ts +34 -22
  73. package/dist/types/transition-when-ref.d.ts +79 -0
  74. package/dist/types/types/cache-types.d.ts +15 -3
  75. package/dist/types/types/index.d.ts +1 -1
  76. package/dist/types/types/segments.d.ts +86 -71
  77. package/dist/types/urls/path-helper-types.d.ts +3 -4
  78. package/dist/types/urls/pattern-types.d.ts +3 -4
  79. package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
  80. package/dist/vite/index.js +960 -97
  81. package/package.json +1 -1
  82. package/skills/cache-guide/SKILL.md +76 -49
  83. package/skills/caching/SKILL.md +167 -69
  84. package/skills/client-urls/SKILL.md +1 -1
  85. package/skills/comparison/references/framework-comparison.md +8 -5
  86. package/skills/document-cache/SKILL.md +24 -0
  87. package/skills/hooks/SKILL.md +3 -1
  88. package/skills/hooks/navigation.md +23 -2
  89. package/skills/hooks/state.md +98 -6
  90. package/skills/intercept/SKILL.md +29 -12
  91. package/skills/loader/SKILL.md +52 -21
  92. package/skills/middleware/SKILL.md +18 -0
  93. package/skills/migrate-nextjs/SKILL.md +3 -3
  94. package/skills/observability/SKILL.md +6 -5
  95. package/skills/parallel/SKILL.md +1 -1
  96. package/skills/ppr/SKILL.md +129 -77
  97. package/skills/prerender/SKILL.md +3 -3
  98. package/skills/rango/SKILL.md +25 -6
  99. package/skills/route/SKILL.md +2 -0
  100. package/skills/testing/SKILL.md +27 -25
  101. package/skills/testing/cache-prerender.md +9 -9
  102. package/skills/testing/client-components.md +45 -3
  103. package/skills/testing/loader.md +3 -2
  104. package/skills/testing/middleware.md +1 -0
  105. package/skills/testing/predicates.md +49 -49
  106. package/skills/testing/render-handler.md +1 -0
  107. package/skills/testing/serve-shell-request.md +14 -0
  108. package/skills/testing/server-actions.md +9 -3
  109. package/skills/theme/SKILL.md +8 -4
  110. package/skills/typesafety/env-and-bindings.md +8 -0
  111. package/skills/use-cache/SKILL.md +64 -19
  112. package/skills/view-transitions/SKILL.md +150 -29
  113. package/src/__augment-tests__/augmented.check.ts +23 -0
  114. package/src/browser/event-controller.ts +4 -4
  115. package/src/browser/history-state.ts +52 -1
  116. package/src/browser/link-interceptor.ts +4 -0
  117. package/src/browser/navigation-bridge.ts +77 -18
  118. package/src/browser/navigation-client.ts +4 -3
  119. package/src/browser/navigation-store.ts +43 -1
  120. package/src/browser/navigation-transaction.ts +31 -11
  121. package/src/browser/partial-update.ts +146 -35
  122. package/src/browser/prefetch/cache.ts +6 -6
  123. package/src/browser/prefetch/fetch.ts +2 -2
  124. package/src/browser/react/Link.tsx +38 -33
  125. package/src/browser/react/NavigationProvider.tsx +2 -2
  126. package/src/browser/react/context.ts +2 -2
  127. package/src/browser/react/location-state-shared.ts +241 -49
  128. package/src/browser/react/location-state.ts +5 -2
  129. package/src/browser/rsc-router.tsx +4 -0
  130. package/src/browser/server-action-bridge.ts +84 -15
  131. package/src/browser/transition-when.ts +205 -0
  132. package/src/browser/types.ts +124 -10
  133. package/src/build/generate-manifest.ts +77 -1
  134. package/src/cache/background-task.ts +55 -2
  135. package/src/cache/cache-exec-scope.ts +48 -1
  136. package/src/cache/cache-key-utils.ts +56 -16
  137. package/src/cache/cache-policy.ts +9 -4
  138. package/src/cache/cache-runtime.ts +197 -144
  139. package/src/cache/cache-scope.ts +304 -112
  140. package/src/cache/cache-tag.ts +105 -13
  141. package/src/cache/cf/cf-cache-store.ts +152 -76
  142. package/src/cache/cf/cf-shell-frame.ts +2 -2
  143. package/src/cache/cf/cf-tag-marker-memo.ts +11 -100
  144. package/src/cache/document-cache.ts +53 -7
  145. package/src/cache/handle-snapshot.ts +52 -9
  146. package/src/cache/invalidation-order.ts +84 -0
  147. package/src/cache/request-tag-mask.ts +147 -0
  148. package/src/cache/shell-snapshot.ts +130 -348
  149. package/src/cache/tag-invalidation.ts +57 -51
  150. package/src/cache/types.ts +71 -67
  151. package/src/cache/vercel/vercel-cache-store.ts +61 -58
  152. package/src/client-urls/client-root.tsx +8 -4
  153. package/src/client-urls/client-urls.ts +8 -6
  154. package/src/client-urls/navigation.ts +55 -4
  155. package/src/client-urls/server-projection.ts +31 -10
  156. package/src/client-urls/types.ts +12 -5
  157. package/src/client.tsx +2 -0
  158. package/src/host/cookie-handler.ts +2 -1
  159. package/src/index.rsc.ts +3 -0
  160. package/src/index.ts +3 -0
  161. package/src/loader-redirect.tsx +7 -3
  162. package/src/prerender/build-shell-capture.ts +155 -146
  163. package/src/route-definition/dsl-helpers.ts +2 -0
  164. package/src/route-definition/helpers-types.ts +11 -11
  165. package/src/route-name.ts +12 -0
  166. package/src/router/content-negotiation.ts +4 -1
  167. package/src/router/error-handling.ts +5 -2
  168. package/src/router/handler-context.ts +10 -7
  169. package/src/router/intercept-resolution.ts +11 -1
  170. package/src/router/loader-resolution.ts +28 -3
  171. package/src/router/logging.ts +5 -3
  172. package/src/router/match-api.ts +62 -34
  173. package/src/router/match-context.ts +5 -0
  174. package/src/router/match-handlers.ts +9 -29
  175. package/src/router/match-middleware/background-revalidation.ts +3 -5
  176. package/src/router/match-middleware/cache-lookup.ts +35 -19
  177. package/src/router/match-middleware/cache-store.ts +15 -9
  178. package/src/router/match-result.ts +1 -0
  179. package/src/router/middleware.ts +2 -5
  180. package/src/router/navigation-snapshot.ts +6 -5
  181. package/src/router/prerender-match.ts +4 -0
  182. package/src/router/request-classification.ts +7 -4
  183. package/src/router/router-options.ts +4 -1
  184. package/src/router/segment-resolution/fresh.ts +0 -4
  185. package/src/router/segment-resolution/loader-cache.ts +150 -25
  186. package/src/router/segment-resolution/loader-mask.ts +3 -4
  187. package/src/router/segment-resolution/loader-snapshot.ts +15 -78
  188. package/src/router/segment-resolution/mask-nested.ts +47 -22
  189. package/src/router/segment-resolution/revalidation.ts +0 -3
  190. package/src/router/segment-resolution/view-transition-default.ts +6 -21
  191. package/src/router/telemetry.ts +6 -3
  192. package/src/router/transition-when.ts +34 -52
  193. package/src/router.ts +2 -1
  194. package/src/rsc/attach-transition-when.ts +47 -0
  195. package/src/rsc/full-payload.ts +30 -10
  196. package/src/rsc/handler.ts +12 -10
  197. package/src/rsc/loader-fetch.ts +2 -1
  198. package/src/rsc/manifest-init.ts +4 -0
  199. package/src/rsc/origin-guard.ts +8 -5
  200. package/src/rsc/progressive-enhancement.ts +7 -30
  201. package/src/rsc/response-cache-serve.ts +68 -80
  202. package/src/rsc/response-route-handler.ts +2 -2
  203. package/src/rsc/rsc-rendering.ts +206 -252
  204. package/src/rsc/server-action.ts +8 -20
  205. package/src/rsc/shell-build-manifest.ts +22 -20
  206. package/src/rsc/shell-capture-constants.ts +136 -20
  207. package/src/rsc/shell-capture.ts +352 -589
  208. package/src/rsc/shell-serve.ts +73 -109
  209. package/src/rsc/ssr-setup.ts +3 -2
  210. package/src/rsc/types.ts +2 -0
  211. package/src/segment-system.tsx +40 -7
  212. package/src/server/context.ts +189 -124
  213. package/src/server/cookie-store.ts +153 -127
  214. package/src/server/handle-store.ts +245 -27
  215. package/src/server/request-context.ts +123 -84
  216. package/src/server/request-headers.ts +134 -0
  217. package/src/server.ts +4 -0
  218. package/src/testing/dispatch.ts +4 -7
  219. package/src/testing/e2e/index.ts +1 -0
  220. package/src/testing/index.ts +5 -0
  221. package/src/testing/internal/action-id.ts +25 -0
  222. package/src/testing/internal/context.ts +1 -1
  223. package/src/testing/location-state-key.ts +45 -0
  224. package/src/testing/render-handler.ts +1 -1
  225. package/src/testing/render-route.tsx +109 -16
  226. package/src/testing/run-client-revalidate.ts +3 -19
  227. package/src/testing/run-loader.ts +5 -1
  228. package/src/testing/run-transition-when.ts +109 -172
  229. package/src/testing/serve-shell-request.ts +10 -7
  230. package/src/testing/shell-status.ts +60 -56
  231. package/src/transition-when-ref.ts +220 -0
  232. package/src/types/cache-types.ts +15 -3
  233. package/src/types/index.ts +3 -0
  234. package/src/types/segments.ts +94 -91
  235. package/src/urls/path-helper-types.ts +3 -4
  236. package/src/urls/pattern-types.ts +3 -4
  237. package/src/vite/discovery/discover-routers.ts +3 -0
  238. package/src/vite/discovery/shell-prerender-phase.ts +0 -2
  239. package/src/vite/discovery/virtual-module-codegen.ts +16 -6
  240. package/src/vite/plugins/transition-when-hoist.ts +1064 -0
  241. package/src/vite/rango.ts +2 -0
  242. package/src/vite/router-discovery.ts +75 -5
  243. package/dist/types/rsc/transition-gate.d.ts +0 -23
  244. package/src/rsc/transition-gate.ts +0 -86
@@ -1,3 +1,4 @@
1
+ import { requestHeaders } from "../server/request-headers.js";
1
2
  import { AsyncLocalStorage } from "node:async_hooks";
2
3
  import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
3
4
 
@@ -61,10 +62,11 @@ function nextId(prefix: string, counter: number): string {
61
62
  }
62
63
 
63
64
  function getHeaderRequestId(request: Request): string | null {
65
+ const headers = requestHeaders(request);
64
66
  const candidate =
65
- request.headers.get("x-rsc-router-request-id") ??
66
- request.headers.get("x-request-id") ??
67
- request.headers.get("cf-ray");
67
+ headers.get("x-rsc-router-request-id") ??
68
+ headers.get("x-request-id") ??
69
+ headers.get("cf-ray");
68
70
  if (!candidate) return null;
69
71
  const trimmed = candidate.trim();
70
72
  return trimmed.length > 0 ? trimmed : null;
@@ -1,10 +1,14 @@
1
+ import { requestHeaders } from "../server/request-headers.js";
1
2
  import {
2
3
  CacheScope,
3
4
  createCacheScope,
4
5
  resolveShellImplicitCacheScope,
5
6
  } from "../cache/cache-scope.js";
6
7
  import { armLoaderTagSets, armRecordTagOwners } from "../cache/cache-tag.js";
7
- import { bindsLoaderCache } from "./segment-resolution/loader-cache.js";
8
+ import {
9
+ bindsLoaderCache,
10
+ routeLoaderLanes,
11
+ } from "./segment-resolution/loader-cache.js";
8
12
  import { RouteNotFoundError } from "../errors";
9
13
  import {
10
14
  createErrorInfo,
@@ -37,8 +41,7 @@ import {
37
41
  _getRequestContext,
38
42
  setRequestContextPrevRouteKey,
39
43
  } from "../server/request-context.js";
40
- import { isAutoGeneratedRouteName } from "../route-name.js";
41
- import type { DefaultRouteName } from "../types/global-namespace.js";
44
+ import { publicRouteName } from "../route-name.js";
42
45
  import { debugLog, debugWarn } from "./logging.js";
43
46
  import {
44
47
  resolveRoute,
@@ -62,7 +65,7 @@ export async function createMatchContextForFull<TEnv>(
62
65
 
63
66
  const metricsStore = deps.getMetricsStore();
64
67
 
65
- const isHmr = !!request.headers.get("X-RSC-HMR");
68
+ const isHmr = !!requestHeaders(request).get("X-RSC-HMR");
66
69
  // HMR: clear the manifest cache so stale handler references are discarded.
67
70
  if (isHmr) {
68
71
  clearManifestCache();
@@ -165,6 +168,12 @@ export async function createMatchContextForFull<TEnv>(
165
168
  armRecordTagOwners();
166
169
  }
167
170
  if (bindsLoaderCache(snapshot.entries)) armLoaderTagSets();
171
+ // A shell capture credits a push under a live-lane loader's body to that
172
+ // loader (shell-capture.ts deriveShellCaptureContext). The lanes are set
173
+ // before any loader runs: a bake-lane loader can start a later one.
174
+ if (reqCtx?._shellCaptureRun) {
175
+ reqCtx._shellCaptureLoaderLanes = routeLoaderLanes(snapshot.entries);
176
+ }
168
177
 
169
178
  return {
170
179
  request,
@@ -188,18 +197,20 @@ export async function createMatchContextForFull<TEnv>(
188
197
  metricsStore,
189
198
  Store,
190
199
  interceptContextMatch: null,
191
- interceptSelectorContext: {
192
- from: cleanUrl,
193
- to: cleanUrl,
194
- params: matched.params,
195
- request,
196
- env,
197
- segments: { path: [], ids: [] },
198
- toRouteName:
199
- matched.routeKey && !isAutoGeneratedRouteName(matched.routeKey)
200
- ? (matched.routeKey as DefaultRouteName)
201
- : undefined,
202
- },
200
+ interceptSelectorContext: (() => {
201
+ const here = {
202
+ url: cleanUrl,
203
+ params: matched.params,
204
+ routeName: publicRouteName(matched.routeKey),
205
+ };
206
+ return {
207
+ from: here,
208
+ to: here,
209
+ request,
210
+ env,
211
+ segments: { path: [], ids: [] },
212
+ };
213
+ })(),
203
214
  isSameRouteNavigation: false,
204
215
  interceptResult: null,
205
216
  cacheScope,
@@ -231,7 +242,7 @@ export async function createMatchContextForPartial<TEnv>(
231
242
 
232
243
  const metricsStore = deps.getMetricsStore();
233
244
 
234
- const isHmr = !!request.headers.get("X-RSC-HMR");
245
+ const isHmr = !!requestHeaders(request).get("X-RSC-HMR");
235
246
 
236
247
  // HMR: clear manifest cache so stale handler references are discarded
237
248
  if (isHmr) {
@@ -349,31 +360,27 @@ export async function createMatchContextForPartial<TEnv>(
349
360
  });
350
361
  }
351
362
 
352
- setRequestContextPrevRouteKey(
353
- nav.effectiveFromMatch?.routeKey,
354
- nav.effectiveFromUrl,
355
- nav.effectiveFromMatch?.params ?? nav.prevParams,
356
- );
363
+ setRequestContextPrevRouteKey(nav.effectiveFromMatch?.routeKey);
357
364
 
358
365
  const interceptSelectorContext: InterceptSelectorContext = {
359
- from: nav.effectiveFromUrl,
360
- to: cleanUrl,
361
- params: matched.params,
366
+ // The source's params come from matching its URL (the intercept source
367
+ // while one is open), like its route name.
368
+ from: {
369
+ url: nav.effectiveFromUrl,
370
+ params: nav.effectiveFromMatch?.params ?? {},
371
+ routeName: publicRouteName(nav.effectiveFromMatch?.routeKey),
372
+ },
373
+ to: {
374
+ url: cleanUrl,
375
+ params: matched.params,
376
+ routeName: publicRouteName(matched.routeKey),
377
+ },
362
378
  request,
363
379
  env,
364
380
  segments: {
365
381
  path: nav.effectiveFromUrl.pathname.split("/").filter(Boolean),
366
382
  ids: nav.filteredSegmentIds,
367
383
  },
368
- fromRouteName:
369
- nav.effectiveFromMatch?.routeKey &&
370
- !isAutoGeneratedRouteName(nav.effectiveFromMatch.routeKey)
371
- ? (nav.effectiveFromMatch.routeKey as DefaultRouteName)
372
- : undefined,
373
- toRouteName:
374
- matched.routeKey && !isAutoGeneratedRouteName(matched.routeKey)
375
- ? (matched.routeKey as DefaultRouteName)
376
- : undefined,
377
384
  };
378
385
  const isAction = !!actionContext;
379
386
 
@@ -414,6 +421,26 @@ export async function createMatchContextForPartial<TEnv>(
414
421
  clientSegmentSet.delete(snapshot.manifestEntry.shortCode);
415
422
  }
416
423
 
424
+ // Whether an intercept targets this route at all: the same walk as above
425
+ // with isAction=true, which skips when(). If one does, the response depends
426
+ // on the source even when none applied (a when() that rejected it, a
427
+ // same-route navigation), so it must not share the source-agnostic prefetch
428
+ // slot (#1007). The walk is over the TARGET's chain, so a route no
429
+ // intercept targets never depends on the source this way.
430
+ const targetsIntercept = (routeKey: string): boolean =>
431
+ findInterceptForRoute(
432
+ routeKey,
433
+ snapshot.manifestEntry.parent,
434
+ null,
435
+ true,
436
+ ) !== null;
437
+ const interceptTargeted =
438
+ !!interceptResult ||
439
+ (!skipInterceptForAction &&
440
+ (targetsIntercept(matched.routeKey) ||
441
+ (snapshot.localRouteName !== matched.routeKey &&
442
+ targetsIntercept(snapshot.localRouteName))));
443
+
417
444
  const isIntercept = !!interceptResult;
418
445
  // A normal-route navigation may replay a PPR shell's canonical segment
419
446
  // snapshot. Intercepts remain source-dependent and always use their normal
@@ -451,6 +478,7 @@ export async function createMatchContextForPartial<TEnv>(
451
478
  interceptSelectorContext,
452
479
  isSameRouteNavigation: nav.isSameRouteNavigation,
453
480
  interceptResult,
481
+ interceptTargeted,
454
482
  cacheScope,
455
483
  isIntercept,
456
484
  actionContext,
@@ -176,6 +176,11 @@ export interface MatchContext<TEnv = any> {
176
176
  interceptSelectorContext: InterceptSelectorContext;
177
177
  isSameRouteNavigation: boolean;
178
178
  interceptResult: InterceptResult | null;
179
+ /**
180
+ * An intercept targets this route, matched or not (a same-route navigation
181
+ * skips it), so the response depends on the source. Partial matches only.
182
+ */
183
+ interceptTargeted?: boolean;
179
184
 
180
185
  // Cache
181
186
  cacheScope: CacheScope | null;
@@ -9,7 +9,6 @@ import type {
9
9
  NotFoundBoundaryHandler,
10
10
  } from "../types";
11
11
  import {
12
- isPprEntry,
13
12
  type EntryData,
14
13
  type InterceptEntry,
15
14
  type InterceptSelectorContext,
@@ -48,7 +47,7 @@ import {
48
47
  } from "./telemetry.js";
49
48
  import { _getRequestContext } from "../server/request-context.js";
50
49
  import { ShellRecordUnavailableError } from "../cache/shell-snapshot.js";
51
- import { evaluatePprTransitionWhen } from "./transition-when.js";
50
+ import { recordTransitionWhenRefs } from "./transition-when.js";
52
51
 
53
52
  /**
54
53
  * Per-call telemetry lifecycle emitter for match()/matchPartial(). Each method
@@ -235,33 +234,14 @@ export function createMatchHandlers<TEnv = any>(
235
234
  if (reqCtx) reqCtx._cacheSignal = segments;
236
235
  };
237
236
 
238
- const evaluatePprTransitionWhenForMatch = (
239
- ctx: MatchContext<TEnv>,
240
- isPartial: boolean,
241
- ): void => {
237
+ const recordTransitionWhenRefsForMatch = (ctx: MatchContext<TEnv>): void => {
242
238
  const reqCtx = _getRequestContext();
243
239
  if (!reqCtx) return;
244
- reqCtx._pprTransitionDecisions = undefined;
245
- if (!isPprEntry(ctx.manifestEntry)) return;
246
-
247
- evaluatePprTransitionWhen(
248
- ctx.entries,
249
- reqCtx,
250
- {
251
- params: ctx.matched.params,
252
- routeName: ctx.interceptSelectorContext.toRouteName,
253
- },
254
- (error, segmentId) =>
255
- callOnError(error, "rendering", {
256
- request: ctx.request,
257
- url: ctx.url,
258
- env: ctx.env,
259
- params: ctx.matched.params,
260
- segmentId,
261
- isPartial,
262
- handledByBoundary: false,
263
- }),
264
- );
240
+ const entry = ctx.manifestEntry;
241
+ recordTransitionWhenRefs(ctx.entries, reqCtx, {
242
+ routeName: ctx.routeKey,
243
+ pattern: entry?.type === "route" ? entry.pattern : undefined,
244
+ });
265
245
  };
266
246
 
267
247
  async function createMatchContextForFull(
@@ -350,7 +330,7 @@ export function createMatchHandlers<TEnv = any>(
350
330
  }
351
331
 
352
332
  const ctx = result as MatchContext<TEnv>;
353
- evaluatePprTransitionWhenForMatch(ctx, false);
333
+ recordTransitionWhenRefsForMatch(ctx);
354
334
 
355
335
  try {
356
336
  const state = createPipelineState();
@@ -466,7 +446,7 @@ export function createMatchHandlers<TEnv = any>(
466
446
  emitter.end(0, false);
467
447
  return null;
468
448
  }
469
- evaluatePprTransitionWhenForMatch(ctx, true);
449
+ recordTransitionWhenRefsForMatch(ctx);
470
450
 
471
451
  if (isRouterDebugEnabled()) {
472
452
  startRevalidationTrace({
@@ -107,6 +107,7 @@ import type { GeneratorMiddleware } from "./cache-lookup.js";
107
107
  import { debugLog, debugWarn, getOrCreateRequestId } from "../logging.js";
108
108
  import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
109
109
  import { getContext } from "../../server/context.js";
110
+ import { executionStart } from "../../cache/tag-invalidation.js";
110
111
  import {
111
112
  createRequestContext,
112
113
  runWithRequestContext,
@@ -134,10 +135,6 @@ import {
134
135
  * cache-scope.ts), so a swap sent live pushes into this render.
135
136
  * setupLoaderAccess binds the store at setup, so it runs inside the
136
137
  * derived context too.
137
- * - _transitionWhen: fresh resolution appends transition({ when })
138
- * predicates; the foreground's gateTransitions reads the array after the
139
- * match, and a stale HIT (which replays the stored transition) must not
140
- * evaluate this render's predicates.
141
138
  * - _metricsStore: undefined, and the DSL store below has `metrics` unset
142
139
  * (track() reads that one), so nothing lands on the foreground's perf
143
140
  * timeline. ctx.Store.run closes over ctx.Store, so the derived store
@@ -156,6 +153,7 @@ export async function rerenderAndCacheRoute<TEnv>(
156
153
  cacheScope: CacheScope,
157
154
  routerCtx: RouterContext<TEnv>,
158
155
  ): Promise<number> {
156
+ const start = executionStart();
159
157
  const handleStore = routerCtx.createHandleStore();
160
158
  // Response writes land in a throwaway context nothing merges or drains. Its
161
159
  // mutators are closures over its own stub, and they keep the same guards.
@@ -172,7 +170,6 @@ export async function rerenderAndCacheRoute<TEnv>(
172
170
  }),
173
171
  {
174
172
  _handleStore: handleStore,
175
- _transitionWhen: [],
176
173
  _metricsStore: undefined,
177
174
  _onResponseCallbacks: [],
178
175
  header: sink.header,
@@ -253,6 +250,7 @@ export async function rerenderAndCacheRoute<TEnv>(
253
250
  ctx.matched.params,
254
251
  segments,
255
252
  ctx.isIntercept,
253
+ start,
256
254
  );
257
255
  return segments.length;
258
256
  });
@@ -92,12 +92,14 @@
92
92
  * - Action context (if POST)
93
93
  */
94
94
  import type { InternalHandlerContext, ResolvedSegment } from "../../types.js";
95
+ import type { OwnedPushDelivery } from "../../cache/handle-snapshot.js";
95
96
  import type { EntryData } from "../../server/context.js";
96
97
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
97
98
  import { getRouterContext, type RouterContext } from "../router-context.js";
98
99
  import { observeEvent } from "../instrument.js";
99
100
  import { pushRevalidationTraceEntry, isTraceActive } from "../logging.js";
100
101
  import { treeHasStreaming } from "./segment-resolution.js";
102
+ import { liveLaneLoaderIds } from "../segment-resolution/loader-cache.js";
101
103
  import type { PrerenderStore, PrerenderEntry } from "../../prerender/store.js";
102
104
  import {
103
105
  _getRequestContext,
@@ -297,8 +299,7 @@ async function* yieldFromStore<TEnv>(
297
299
  !paramsChanged &&
298
300
  ctx.clientSegmentSet.has(segment.id)
299
301
  ) {
300
- segment.component = null;
301
- segment.loading = undefined;
302
+ keepClientSegment(segment);
302
303
  }
303
304
  yield segment;
304
305
  }
@@ -405,6 +406,21 @@ async function* tryPrerenderLookup<TEnv>(
405
406
  return true;
406
407
  }
407
408
 
409
+ /**
410
+ * Keep the client's copy of a stored (runtime cache, prerender, or shell
411
+ * replay) segment this navigation does not re-render. collectMatchResult
412
+ * (match-result.ts) omits a null-component segment the client holds, as the
413
+ * live partial path omits a segment whose revalidation said no
414
+ * (segment-resolution/revalidation.ts).
415
+ *
416
+ * A transition({ when }) is no exception: it is decided in the browser for
417
+ * kept segments too, so no segment is re-sent to carry a decision (#986).
418
+ */
419
+ function keepClientSegment(segment: ResolvedSegment): void {
420
+ segment.component = null;
421
+ segment.loading = undefined;
422
+ }
423
+
408
424
  /**
409
425
  * Async generator middleware type
410
426
  */
@@ -531,14 +547,24 @@ export function withCacheLookup<TEnv>(
531
547
  }
532
548
 
533
549
  // A record's loader-owned handle values claim their loader, so its own
534
- // cache() HIT does not replay them again (restoreHandles).
535
- const claimLoaderPushes = (ctx.handlerContext as InternalHandlerContext)
536
- ._claimLoaderPushes;
550
+ // cache() HIT does not replay them again (restoreHandles). A document
551
+ // HIT tail restores a bake-lane owner's values as the prelude's, unclaimed:
552
+ // they stand against any later push of that loader
553
+ // (HandleStore.pushRestored). An owner the route runs on the live lane is
554
+ // a hole, so its live run still replaces them, even inside a restored
555
+ // loader's body.
556
+ const ownedPushes: OwnedPushDelivery = {
557
+ claim: (ctx.handlerContext as InternalHandlerContext)._claimLoaderPushes,
558
+ };
559
+ if (tailMarker) {
560
+ ownedPushes.liveLane = liveLaneLoaderIds(ctx.entries);
561
+ ownedPushes.restore = tailMarker.docTail === true;
562
+ }
537
563
  const explicitLookup = await ctx.cacheScope.lookupRouteDetailed(
538
564
  ctx.pathname,
539
565
  ctx.matched.params,
540
566
  ctx.isIntercept,
541
- claimLoaderPushes,
567
+ ownedPushes,
542
568
  );
543
569
  let cacheResult =
544
570
  explicitLookup.status === "hit" ? explicitLookup.result : null;
@@ -582,7 +608,7 @@ export function withCacheLookup<TEnv>(
582
608
  ctx.pathname,
583
609
  ctx.matched.params,
584
610
  ctx.isIntercept,
585
- claimLoaderPushes,
611
+ ownedPushes,
586
612
  );
587
613
  } else if (explicitLookup.status === "bypass") {
588
614
  // condition() refused at lookup time (the gate only pre-decides the
@@ -675,7 +701,6 @@ export function withCacheLookup<TEnv>(
675
701
  ...liveMatchedIds,
676
702
  ...cacheResult.segments.map((s) => s.id),
677
703
  ];
678
- const pprTransitionDecisions = pipelineReqCtx?._pprTransitionDecisions;
679
704
 
680
705
  const canCheckSegmentRevalidation =
681
706
  !ctx.isFullMatch &&
@@ -751,13 +776,7 @@ export function withCacheLookup<TEnv>(
751
776
  reason: "cached-no-rules",
752
777
  });
753
778
  }
754
- // A PPR transition decision must reach the client even when this cached
755
- // segment otherwise needs no update. Keep its replayed component so the
756
- // partial result retains the segment for gateTransitions().
757
- if (!pprTransitionDecisions?.has(segment.id)) {
758
- segment.component = null;
759
- segment.loading = undefined;
760
- }
779
+ keepClientSegment(segment);
761
780
  yield segment;
762
781
  continue;
763
782
  }
@@ -789,10 +808,7 @@ export function withCacheLookup<TEnv>(
789
808
  shouldRevalidate,
790
809
  });
791
810
 
792
- if (!shouldRevalidate && !pprTransitionDecisions?.has(segment.id)) {
793
- segment.component = null;
794
- segment.loading = undefined;
795
- }
811
+ if (!shouldRevalidate) keepClientSegment(segment);
796
812
 
797
813
  yield segment;
798
814
  }
@@ -106,11 +106,9 @@ import {
106
106
  type RequestContext,
107
107
  } from "../../server/request-context.js";
108
108
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
109
- import {
110
- createShellImplicitDocScope,
111
- resolveCacheTags,
112
- } from "../../cache/cache-scope.js";
109
+ import { createShellImplicitDocScope } from "../../cache/cache-scope.js";
113
110
  import { recordSegmentTags } from "../../cache/cache-tag.js";
111
+ import { executionStart } from "../../cache/tag-invalidation.js";
114
112
  import { getRouterContext } from "../router-context.js";
115
113
  import { debugLog, debugWarn, getOrCreateRequestId } from "../logging.js";
116
114
  import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
@@ -131,6 +129,9 @@ export function withCacheStore<TEnv>(
131
129
  source: AsyncGenerator<ResolvedSegment>,
132
130
  ): AsyncGenerator<ResolvedSegment> {
133
131
  const ms = ctx.metricsStore;
132
+ // Before the lookup and the handlers below it run: the record's write
133
+ // gate (#977, CacheScope.cacheRoute).
134
+ const renderStart = executionStart();
134
135
 
135
136
  const allSegments: ResolvedSegment[] = [];
136
137
  for await (const segment of source) {
@@ -266,6 +267,10 @@ export function withCacheStore<TEnv>(
266
267
  ctx.matched.params,
267
268
  allSegmentsToCache,
268
269
  ctx.isIntercept,
270
+ // A capture's doc record lives only in the shell entry, which
271
+ // putShell gates by the capture start; skipping it here would
272
+ // read as a render that produced no record.
273
+ cacheScope.isShellImplicitDocScope ? undefined : renderStart,
269
274
  );
270
275
  if (INTERNAL_RANGO_DEBUG) {
271
276
  const dur = performance.now() - start;
@@ -341,13 +346,14 @@ function recordShellCaptureDocRecord<TEnv>(
341
346
  if (!scope.allowsCache("write")) return;
342
347
 
343
348
  const docScope = createShellImplicitDocScope(marker);
344
- // The route scope's cache({ tags }) describe these segments too: on its
345
- // HIT they arrive with the replayed record (recordSegmentTags in
346
- // lookupRouteDetailed); on a fresh render they are recorded here, so the
347
- // doc record, and the shell taking its tags, carries them either way.
349
+ // The route scope's cache({ tags }), and its enclosing scopes' (#974),
350
+ // describe these segments too: on its HIT they arrive with the replayed
351
+ // record (recordSegmentTags in lookupRouteDetailed); on a fresh render they
352
+ // are recorded here, so the doc record, and the shell taking its tags,
353
+ // carries them either way.
348
354
  recordSegmentTags(
349
355
  segments.map((s) => s.id),
350
- resolveCacheTags(scope.config, requestCtx),
356
+ scope.resolveTags(requestCtx),
351
357
  requestCtx,
352
358
  );
353
359
  // Through onResponse, like the implicit scope's own write: the capture
@@ -281,6 +281,7 @@ export function buildMatchResult<TEnv>(
281
281
  routeName: ctx.routeKey,
282
282
  slots: Object.keys(state.slots).length > 0 ? state.slots : undefined,
283
283
  ...(interceptTargets.length > 0 ? { interceptTargets } : {}),
284
+ ...(ctx.interceptTargeted ? { interceptTargeted: true } : {}),
284
285
  routeMiddleware:
285
286
  ctx.routeMiddleware.length > 0 ? ctx.routeMiddleware : undefined,
286
287
  };
@@ -31,7 +31,7 @@ import {
31
31
  } from "../cache/taint.js";
32
32
  import { getCacheExecScope } from "../cache/cache-exec-scope.js";
33
33
  import { assertNonCacheableReadAllowed } from "../server/context.js";
34
- import { assertThemeReadAllowed } from "../server/cookie-store.js";
34
+ import { readGuardedTheme } from "../server/cookie-store.js";
35
35
  import type { Theme } from "../theme/types.js";
36
36
 
37
37
  // Re-export types consumed through this module's path.
@@ -52,10 +52,7 @@ export type {
52
52
  */
53
53
  const MIDDLEWARE_THEME_DESCRIPTOR: PropertyDescriptor = {
54
54
  get(): Theme | undefined {
55
- const reqCtx = _getRequestContext();
56
- if (!reqCtx?._themeConfig) return undefined;
57
- assertThemeReadAllowed(reqCtx, "ctx.theme");
58
- return reqCtx._readTheme();
55
+ return readGuardedTheme(_getRequestContext(), "ctx.theme");
59
56
  },
60
57
  enumerable: false,
61
58
  configurable: true,
@@ -1,3 +1,4 @@
1
+ import { requestHeaders } from "../server/request-headers.js";
1
2
  import type { RouteMatchResult } from "./pattern-matching.js";
2
3
 
3
4
  export interface NavigationSnapshot {
@@ -40,8 +41,8 @@ export interface ResolveNavigationDeps {
40
41
  */
41
42
  export function getNavigationContextHeader(request: Request): string | null {
42
43
  return (
43
- request.headers.get("X-RSC-Router-Client-Path") ||
44
- request.headers.get("Referer")
44
+ requestHeaders(request).get("X-RSC-Router-Client-Path") ||
45
+ requestHeaders(request).get("Referer")
45
46
  );
46
47
  }
47
48
 
@@ -59,7 +60,7 @@ export function prerenderStoreShortCircuits(
59
60
  pr: true | undefined,
60
61
  request: Request,
61
62
  ): boolean {
62
- return pr === true && !request.headers.get("X-RSC-HMR");
63
+ return pr === true && !requestHeaders(request).get("X-RSC-HMR");
63
64
  }
64
65
 
65
66
  /**
@@ -71,7 +72,7 @@ export function prerenderStoreShortCircuits(
71
72
  * request would report `prerender-store` for an artifact that cannot serve.
72
73
  */
73
74
  export function getInterceptSourceHeader(request: Request): string | null {
74
- return request.headers.get("X-RSC-Router-Intercept-Source");
75
+ return requestHeaders(request).get("X-RSC-Router-Intercept-Source");
75
76
  }
76
77
 
77
78
  export async function resolveNavigation(
@@ -85,7 +86,7 @@ export async function resolveNavigation(
85
86
  const stale = url.searchParams.get("_rsc_stale") === "true";
86
87
  const previousUrl = getNavigationContextHeader(request);
87
88
  const interceptSourceUrl = getInterceptSourceHeader(request);
88
- const isHmr = !!request.headers.get("X-RSC-HMR");
89
+ const isHmr = !!requestHeaders(request).get("X-RSC-HMR");
89
90
 
90
91
  if (!previousUrl) {
91
92
  return null;
@@ -223,6 +223,8 @@ export async function matchForPrerender<TEnv = any>(
223
223
  res: stubRes,
224
224
  cookie: () => undefined,
225
225
  cookies: () => ({}),
226
+ _readCookie: () => undefined,
227
+ _readCookies: () => ({}),
226
228
  setCookie: () => {},
227
229
  deleteCookie: () => {},
228
230
  header: () => {},
@@ -526,6 +528,8 @@ export async function renderStaticSegment<TEnv = any>(
526
528
  res: stubRes,
527
529
  cookie: () => undefined,
528
530
  cookies: () => ({}),
531
+ _readCookie: () => undefined,
532
+ _readCookies: () => ({}),
529
533
  setCookie: () => {},
530
534
  deleteCookie: () => {},
531
535
  header: () => {},
@@ -15,6 +15,7 @@
15
15
  * Builds on RouteSnapshot from route-snapshot.ts.
16
16
  */
17
17
 
18
+ import { requestHeaders } from "../server/request-headers.js";
18
19
  import { RouteNotFoundError } from "../errors.js";
19
20
  import type { EntryData } from "../server/context.js";
20
21
  import type { CollectedMiddleware } from "./middleware-types.js";
@@ -149,10 +150,11 @@ export async function classifyRequest<TEnv = any>(
149
150
  ): Promise<RequestPlan<TEnv>> {
150
151
  const pathname = url.pathname;
151
152
  const isAction =
152
- request.headers.has("rsc-action") || url.searchParams.has("_rsc_action");
153
+ requestHeaders(request).has("rsc-action") ||
154
+ url.searchParams.has("_rsc_action");
153
155
  const isLoaderFetch = url.searchParams.has("_rsc_loader");
154
156
  const isPartialReq = url.searchParams.has("_rsc_partial");
155
- const contentType = request.headers.get("content-type") || "";
157
+ const contentType = requestHeaders(request).get("content-type") || "";
156
158
  const isFormSubmission =
157
159
  contentType.includes("multipart/form-data") ||
158
160
  contentType.includes("application/x-www-form-urlencoded");
@@ -176,7 +178,7 @@ export async function classifyRequest<TEnv = any>(
176
178
  ) {
177
179
  let reloadUrl = stripInternalParams(url).toString();
178
180
  if (isAction) {
179
- const referer = request.headers.get("referer");
181
+ const referer = requestHeaders(request).get("referer");
180
182
  if (referer) {
181
183
  try {
182
184
  const refererUrl = new URL(referer);
@@ -248,7 +250,8 @@ export async function classifyRequest<TEnv = any>(
248
250
  }
249
251
 
250
252
  const actionId =
251
- request.headers.get("rsc-action") || url.searchParams.get("_rsc_action");
253
+ requestHeaders(request).get("rsc-action") ||
254
+ url.searchParams.get("_rsc_action");
252
255
 
253
256
  const hasVariants =
254
257
  snapshot.matched.negotiateVariants &&
@@ -518,7 +518,10 @@ export interface RangoOptions<TEnv = any> {
518
518
  *
519
519
  * Controls how long prefetch responses are kept in the client-side
520
520
  * in-memory cache and sets `Cache-Control: private, max-age=<ttl>`
521
- * on server responses for CDN/edge caching.
521
+ * on prefetch responses, so the browser's HTTP cache may reuse them
522
+ * (`private`: shared caches such as a CDN do not store them). A route an
523
+ * intercept targets gets no prefetch `Cache-Control`: whether its
524
+ * response is the modal or the full page depends on the source page.
522
525
  *
523
526
  * The cache is automatically invalidated on server actions regardless
524
527
  * of TTL, so this is primarily a staleness safety net.
@@ -324,7 +324,6 @@ export async function resolveSegment<TEnv>(
324
324
  transition: applyViewTransitionDefault(
325
325
  entry.transition,
326
326
  deps.viewTransitionDefault,
327
- entry.shortCode,
328
327
  ),
329
328
  params,
330
329
  belongsToRoute: false,
@@ -474,7 +473,6 @@ export async function resolveSegment<TEnv>(
474
473
  transition: applyViewTransitionDefault(
475
474
  entry.transition,
476
475
  deps.viewTransitionDefault,
477
- entry.shortCode,
478
476
  ),
479
477
  params,
480
478
  belongsToRoute: true,
@@ -563,7 +561,6 @@ export async function resolveOrphanLayout<TEnv>(
563
561
  transition: applyViewTransitionDefault(
564
562
  orphan.transition,
565
563
  deps.viewTransitionDefault,
566
- orphan.shortCode,
567
564
  ),
568
565
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
569
566
  });
@@ -723,7 +720,6 @@ export async function resolveParallelEntry<TEnv>(
723
720
  transition: applyViewTransitionDefault(
724
721
  parallelEntry.transition,
725
722
  deps.viewTransitionDefault,
726
- `${parentShortCode}.${slot}`,
727
723
  ),
728
724
  params,
729
725
  slot,