@rangojs/router 0.18.0 → 0.19.1

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 +37 -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 +48 -5
  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 +126 -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 +962 -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 +67 -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 +54 -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 +202 -144
  139. package/src/cache/cache-scope.ts +304 -112
  140. package/src/cache/cache-tag.ts +209 -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 +42 -5
  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 +152 -26
  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 +263 -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 +134 -87
  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
@@ -14,9 +14,13 @@ import {
14
14
  linkRecordedTags,
15
15
  linkValueTags,
16
16
  loaderTagSetsArmed,
17
+ markTagSetStart,
18
+ readStartedLoaderValue,
17
19
  readValueTags,
18
20
  tagLoaderValue,
21
+ trackLoaderRun,
19
22
  } from "../cache/cache-tag.js";
23
+ import { executionStart } from "../cache/tag-invalidation.js";
20
24
  import type {
21
25
  ResolvedSegment,
22
26
  HandlerContext,
@@ -55,8 +59,10 @@ import {
55
59
  assertNonCacheableReadAllowed,
56
60
  isInsideLoaderScope,
57
61
  runInsideLoaderBodyScope,
62
+ runInsideLoaderRun,
58
63
  isInsidePushCallbackScope,
59
64
  runInsidePushCallbackScope,
65
+ type LoaderRunIdentity,
60
66
  } from "../server/context.js";
61
67
  import { debugLog } from "./logging.js";
62
68
 
@@ -396,6 +402,12 @@ function createLoaderExecutor<TEnv>(
396
402
  * payload's handle-stream lifetime.
397
403
  */
398
404
  silentHandles?: boolean;
405
+ /**
406
+ * A stale refresh's executor (setupLoaderAccess _runLoaderIsolated): its
407
+ * runs' pushes are diverted from the page's store, so a run's end says
408
+ * nothing about the page's replayed values (HandleStore.settleLoaderRun).
409
+ */
410
+ detached?: boolean;
399
411
  /**
400
412
  * loaderId -> the cache() tags of that loader's binding in this request
401
413
  * (setupLoaderAccess _bindLoaderCacheTags): an execution of it outside
@@ -410,6 +422,8 @@ function createLoaderExecutor<TEnv>(
410
422
  // Capture RequestContext eagerly for cookie access (ALS protection on Cloudflare)
411
423
  const reqCtxRef = _getRequestContext();
412
424
  const silentHandles = executorOptions?.silentHandles === true;
425
+ // A run's end reports to the page's store only when its pushes reach it.
426
+ const reportsRunEnd = !silentHandles && executorOptions?.detached !== true;
413
427
  const internal = ctx as InternalHandlerContext<any, TEnv>;
414
428
 
415
429
  // Dependency graph: loaderId -> set of loader IDs it directly depends on.
@@ -469,7 +483,9 @@ function createLoaderExecutor<TEnv>(
469
483
  const memo = bound || loaderPromises.get(loader.$$id);
470
484
  if (memo) {
471
485
  if (recordTags) readValueTags(memo);
472
- return memo;
486
+ // A "use cache" body reading it refuses an identity read the run made
487
+ // (#1011): the body ran outside the cached function, unguarded.
488
+ return readStartedLoaderValue(memo, loader.$$id);
473
489
  }
474
490
 
475
491
  // Get loader function - either from loader object or fetchable registry
@@ -712,21 +728,37 @@ function createLoaderExecutor<TEnv>(
712
728
  //
713
729
  // The CONSUMPTION-LANE RULE: a handler-consumed loader's value is a
714
730
  // BAKED copy in every shared artifact (cache(), "use cache", the PPR
715
- // shell). The cache-purity guards permit its identity reads; the PPR
716
- // capture guard does not (server/context.ts assertNotInsideShellCapture).
731
+ // shell). The cache() guard permits its identity reads; a "use cache"
732
+ // body it was entered in and a PPR capture do not (server/context.ts
733
+ // guardIdentityRead), and a "use cache" body that reads the value after
734
+ // another reader started it refuses what `identity` recorded (#1011).
717
735
 
736
+ const identity: LoaderRunIdentity = { loaderId: loader.$$id };
718
737
  const recordedTags = recordTags ? new Set<string>() : undefined;
738
+ // A loader-cache write that stores or reads this value gates on the
739
+ // earliest start behind it (#977, earliestRecordedStart).
740
+ if (recordedTags) markTagSetStart(recordedTags, executionStart());
741
+ const runEndStore = reportsRunEnd
742
+ ? (reqCtxRef ?? _getRequestContext())?._handleStore
743
+ : undefined;
719
744
  const promise = observePhase(PHASES.loader(loader.$$id), () =>
720
745
  Promise.resolve(
721
746
  runInsideLoaderBodyScope(
722
- () => loaderFn(loaderCtx as LoaderContext<any, TEnv>),
747
+ () =>
748
+ runInsideLoaderRun(identity, () =>
749
+ loaderFn(loaderCtx as LoaderContext<any, TEnv>),
750
+ ),
723
751
  loader.$$id,
724
752
  recordedTags,
725
753
  ),
726
754
  ).finally(() => {
727
755
  pendingLoaders.delete(loader.$$id);
756
+ // Its pushes are made: values replayed for it that none replaced
757
+ // go (a hole that throws or skips its push shows none).
758
+ runEndStore?.settleLoaderRun(loader.$$id);
728
759
  }),
729
760
  );
761
+ trackLoaderRun(promise, identity);
730
762
  if (recordedTags) {
731
763
  tagLoaderValue(promise, recordedTags);
732
764
  readValueTags(promise);
@@ -809,7 +841,12 @@ export function setupLoaderAccess<TEnv>(
809
841
  _loaderCacheOverrides: { value: undefined },
810
842
  }),
811
843
  new Map(),
812
- executor,
844
+ {
845
+ get bindingTags() {
846
+ return executor.bindingTags;
847
+ },
848
+ detached: true,
849
+ },
813
850
  )(loader, null);
814
851
 
815
852
  ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
@@ -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;