@rangojs/router 0.26.6 → 0.28.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 (234) hide show
  1. package/dist/bin/rango.js +1 -1
  2. package/dist/types/__internal.d.ts +7 -1
  3. package/dist/types/browser/action-coordinator.d.ts +0 -4
  4. package/dist/types/browser/action-fence.d.ts +11 -3
  5. package/dist/types/browser/event-controller.d.ts +20 -1
  6. package/dist/types/browser/history-state.d.ts +7 -9
  7. package/dist/types/browser/intercept-utils.d.ts +23 -0
  8. package/dist/types/browser/navigation-store.d.ts +8 -6
  9. package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
  10. package/dist/types/browser/rsc-router.d.ts +1 -1
  11. package/dist/types/browser/segment-reconciler.d.ts +6 -4
  12. package/dist/types/browser/types.d.ts +2 -1
  13. package/dist/types/browser/wire-protocol.d.ts +25 -0
  14. package/dist/types/cache/cache-key-utils.d.ts +11 -5
  15. package/dist/types/cache/cache-scope.d.ts +34 -22
  16. package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
  17. package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
  18. package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
  19. package/dist/types/cache/handle-snapshot.d.ts +9 -4
  20. package/dist/types/cache/segment-codec.d.ts +14 -0
  21. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  22. package/dist/types/cache/tag-invalidation.d.ts +10 -0
  23. package/dist/types/cache/taint.d.ts +5 -3
  24. package/dist/types/cache/types.d.ts +7 -3
  25. package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
  26. package/dist/types/prefetch-markers.d.ts +4 -6
  27. package/dist/types/prerender/store.d.ts +31 -0
  28. package/dist/types/prerender/warm-request.d.ts +3 -3
  29. package/dist/types/prerender.d.ts +2 -2
  30. package/dist/types/route-name.d.ts +5 -0
  31. package/dist/types/router/error-handling.d.ts +4 -4
  32. package/dist/types/router/instrument.d.ts +4 -3
  33. package/dist/types/router/intercept-resolution.d.ts +25 -9
  34. package/dist/types/router/match-context.d.ts +12 -11
  35. package/dist/types/router/match-handlers.d.ts +2 -6
  36. package/dist/types/router/match-middleware/background-revalidation.d.ts +10 -20
  37. package/dist/types/router/match-middleware/cache-lookup.d.ts +35 -87
  38. package/dist/types/router/match-middleware/cache-store.d.ts +2 -2
  39. package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
  40. package/dist/types/router/prerender-match.d.ts +21 -6
  41. package/dist/types/router/preview-match.d.ts +2 -7
  42. package/dist/types/router/request-classification.d.ts +5 -0
  43. package/dist/types/router/revalidation.d.ts +6 -1
  44. package/dist/types/router/router-context.d.ts +9 -63
  45. package/dist/types/router/router-interfaces.d.ts +8 -14
  46. package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
  47. package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
  48. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
  49. package/dist/types/router/segment-resolution/resolution-pass.d.ts +93 -0
  50. package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
  51. package/dist/types/router/segment-resolution.d.ts +1 -2
  52. package/dist/types/router/segment-wrappers.d.ts +11 -48
  53. package/dist/types/router/telemetry.d.ts +2 -0
  54. package/dist/types/router/types.d.ts +10 -0
  55. package/dist/types/rsc/full-payload.d.ts +2 -2
  56. package/dist/types/rsc/handler-context.d.ts +6 -6
  57. package/dist/types/rsc/json-route-result.d.ts +5 -9
  58. package/dist/types/rsc/request-stages.d.ts +203 -0
  59. package/dist/types/rsc/response-cache-serve.d.ts +7 -12
  60. package/dist/types/rsc/response-route-handler.d.ts +2 -2
  61. package/dist/types/rsc/routine-plan.d.ts +1 -1
  62. package/dist/types/rsc/server-action.d.ts +2 -2
  63. package/dist/types/rsc/shell-capture-constants.d.ts +2 -2
  64. package/dist/types/rsc/shell-capture.d.ts +132 -15
  65. package/dist/types/rsc/shell-serve.d.ts +37 -12
  66. package/dist/types/server/context.d.ts +9 -8
  67. package/dist/types/server/handle-store.d.ts +16 -0
  68. package/dist/types/server/render-pass.d.ts +72 -0
  69. package/dist/types/server/request-context.d.ts +24 -104
  70. package/dist/types/server/rewrite-trail.d.ts +2 -2
  71. package/dist/types/testing/dispatch.d.ts +35 -18
  72. package/dist/types/testing/serve-shell-request.d.ts +7 -0
  73. package/dist/types/types/cache-types.d.ts +6 -3
  74. package/dist/types/types/error-types.d.ts +9 -1
  75. package/dist/types/types/handler-context.d.ts +18 -7
  76. package/dist/types/types/segments.d.ts +1 -1
  77. package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
  78. package/dist/types/vite/discovery/build-env.d.ts +22 -0
  79. package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
  80. package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
  81. package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
  82. package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
  83. package/dist/types/vite/discovery/state.d.ts +12 -0
  84. package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
  85. package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
  86. package/dist/types/vite/router-discovery.d.ts +8 -47
  87. package/dist/types/vite/temp-rsc-server.d.ts +39 -0
  88. package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
  89. package/dist/types/wire-params.d.ts +34 -0
  90. package/dist/vite/index.js +3809 -3710
  91. package/package.json +1 -1
  92. package/skills/cache-guide/SKILL.md +8 -5
  93. package/skills/caching/SKILL.md +37 -18
  94. package/skills/client-urls/SKILL.md +2 -1
  95. package/skills/document-cache/SKILL.md +18 -1
  96. package/skills/handler-use/SKILL.md +1 -1
  97. package/skills/intercept/SKILL.md +53 -13
  98. package/skills/layout/SKILL.md +11 -7
  99. package/skills/loader/SKILL.md +30 -13
  100. package/skills/middleware/SKILL.md +19 -1
  101. package/skills/migrate-nextjs/SKILL.md +5 -4
  102. package/skills/parallel/SKILL.md +5 -4
  103. package/skills/ppr/SKILL.md +19 -3
  104. package/skills/prerender/SKILL.md +39 -15
  105. package/skills/rango/SKILL.md +23 -11
  106. package/skills/server-actions/SKILL.md +51 -6
  107. package/skills/testing/predicates.md +1 -1
  108. package/skills/testing/response-routes.md +6 -4
  109. package/skills/testing/serve-shell-request.md +7 -7
  110. package/skills/use-cache/SKILL.md +46 -8
  111. package/skills/ux-architecture/SKILL.md +3 -2
  112. package/src/__internal.ts +7 -1
  113. package/src/browser/action-coordinator.ts +2 -13
  114. package/src/browser/action-fence.ts +11 -3
  115. package/src/browser/connection-warmup.ts +3 -1
  116. package/src/browser/event-controller.ts +42 -3
  117. package/src/browser/history-state.ts +15 -13
  118. package/src/browser/intercept-utils.ts +51 -0
  119. package/src/browser/navigation-bridge.ts +34 -30
  120. package/src/browser/navigation-client.ts +21 -33
  121. package/src/browser/navigation-store.ts +25 -27
  122. package/src/browser/navigation-transaction.ts +7 -11
  123. package/src/browser/partial-update.ts +8 -0
  124. package/src/browser/prefetch/cache.ts +2 -1
  125. package/src/browser/prefetch/fetch.ts +8 -16
  126. package/src/browser/prefetch/queue.ts +4 -4
  127. package/src/browser/react/NavigationProvider.tsx +2 -1
  128. package/src/browser/react/filter-segment-order.ts +2 -2
  129. package/src/browser/rsc-router.tsx +17 -2
  130. package/src/browser/segment-reconciler.ts +66 -53
  131. package/src/browser/server-action-bridge.ts +265 -127
  132. package/src/browser/types.ts +2 -1
  133. package/src/browser/wire-protocol.ts +41 -0
  134. package/src/cache/cache-key-utils.ts +11 -5
  135. package/src/cache/cache-runtime.ts +110 -50
  136. package/src/cache/cache-scope.ts +110 -64
  137. package/src/cache/cf/cf-cache-constants.ts +2 -2
  138. package/src/cache/cf/cf-cache-store.ts +486 -874
  139. package/src/cache/cf/cf-kv-utils.ts +1 -1
  140. package/src/cache/document-cache.ts +42 -15
  141. package/src/cache/handle-snapshot.ts +19 -7
  142. package/src/cache/segment-codec.ts +55 -0
  143. package/src/cache/shell-snapshot.ts +2 -2
  144. package/src/cache/tag-invalidation.ts +21 -2
  145. package/src/cache/taint.ts +5 -3
  146. package/src/cache/types.ts +7 -3
  147. package/src/cache/vercel/vercel-cache-store.ts +243 -310
  148. package/src/handles/pushed-by.ts +1 -1
  149. package/src/prefetch-markers.ts +4 -7
  150. package/src/prerender/store.ts +76 -0
  151. package/src/prerender/warm-request.ts +8 -4
  152. package/src/prerender.ts +2 -2
  153. package/src/route-name.ts +8 -0
  154. package/src/router/error-handling.ts +4 -4
  155. package/src/router/instrument.ts +4 -3
  156. package/src/router/intercept-resolution.ts +77 -75
  157. package/src/router/loader-resolution.ts +5 -4
  158. package/src/router/match-api.ts +4 -5
  159. package/src/router/match-context.ts +33 -12
  160. package/src/router/match-handlers.ts +8 -20
  161. package/src/router/match-middleware/background-revalidation.ts +41 -53
  162. package/src/router/match-middleware/cache-lookup.ts +680 -720
  163. package/src/router/match-middleware/cache-store.ts +14 -12
  164. package/src/router/match-middleware/intercept-resolution.ts +3 -23
  165. package/src/router/match-middleware/segment-resolution.ts +34 -61
  166. package/src/router/match-result.ts +1 -20
  167. package/src/router/middleware.ts +69 -11
  168. package/src/router/navigation-snapshot.ts +4 -3
  169. package/src/router/prerender-match.ts +100 -44
  170. package/src/router/preview-match.ts +6 -41
  171. package/src/router/request-classification.ts +13 -7
  172. package/src/router/revalidation.ts +33 -7
  173. package/src/router/route-snapshot.ts +2 -5
  174. package/src/router/router-context.ts +14 -117
  175. package/src/router/router-interfaces.ts +8 -14
  176. package/src/router/segment-resolution/loader-cache.ts +14 -9
  177. package/src/router/segment-resolution/loader-mask.ts +5 -5
  178. package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
  179. package/src/router/segment-resolution/resolution-pass.ts +137 -0
  180. package/src/router/segment-resolution/revalidation.ts +897 -776
  181. package/src/router/segment-resolution.ts +4 -13
  182. package/src/router/segment-wrappers.ts +51 -269
  183. package/src/router/telemetry.ts +5 -0
  184. package/src/router/types.ts +10 -0
  185. package/src/router.ts +12 -13
  186. package/src/rsc/full-payload.ts +6 -5
  187. package/src/rsc/handler-context.ts +8 -14
  188. package/src/rsc/handler.ts +126 -507
  189. package/src/rsc/json-route-result.ts +5 -9
  190. package/src/rsc/loader-fetch.ts +6 -5
  191. package/src/rsc/progressive-enhancement.ts +5 -0
  192. package/src/rsc/request-stages.ts +757 -0
  193. package/src/rsc/response-cache-serve.ts +10 -14
  194. package/src/rsc/response-route-handler.ts +13 -5
  195. package/src/rsc/rewrite-passes.ts +2 -1
  196. package/src/rsc/routine-plan.ts +1 -1
  197. package/src/rsc/rsc-rendering.ts +172 -349
  198. package/src/rsc/server-action.ts +87 -113
  199. package/src/rsc/shell-capture-constants.ts +3 -1
  200. package/src/rsc/shell-capture.ts +212 -169
  201. package/src/rsc/shell-serve.ts +121 -18
  202. package/src/rsc/ssr-setup.ts +5 -4
  203. package/src/segment-system.tsx +3 -3
  204. package/src/server/context.ts +22 -25
  205. package/src/server/handle-store.ts +55 -0
  206. package/src/server/render-pass.ts +291 -0
  207. package/src/server/request-context.ts +26 -115
  208. package/src/server/rewrite-trail.ts +6 -4
  209. package/src/ssr/index.tsx +1 -1
  210. package/src/ssr/ssr-root.tsx +1 -0
  211. package/src/testing/dispatch.ts +223 -710
  212. package/src/testing/e2e/page-helpers.ts +2 -2
  213. package/src/testing/serve-shell-request.ts +37 -32
  214. package/src/types/cache-types.ts +6 -3
  215. package/src/types/error-types.ts +9 -1
  216. package/src/types/handler-context.ts +21 -7
  217. package/src/types/segments.ts +1 -1
  218. package/src/vite/discovery/build-discovery-phase.ts +204 -0
  219. package/src/vite/discovery/build-env.ts +148 -0
  220. package/src/vite/discovery/bundle-scan-phase.ts +128 -0
  221. package/src/vite/discovery/dev-prerender-cache.ts +8 -3
  222. package/src/vite/discovery/dev-server-phase.ts +1776 -0
  223. package/src/vite/discovery/discover-routers.ts +1 -1
  224. package/src/vite/discovery/discovery-errors.ts +1 -1
  225. package/src/vite/discovery/prerender-collection.ts +24 -39
  226. package/src/vite/discovery/state.ts +15 -0
  227. package/src/vite/plugins/version-plugin.ts +1 -1
  228. package/src/vite/plugins/virtual-entries.ts +1 -1
  229. package/src/vite/router-discovery.ts +29 -2428
  230. package/src/vite/temp-rsc-server.ts +223 -0
  231. package/src/vite/utils/prerender-utils.ts +47 -0
  232. package/src/wire-params.ts +35 -0
  233. package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
  234. package/src/router/segment-resolution/fresh.ts +0 -945
@@ -17,18 +17,15 @@ import { isRouteRootScoped } from "../route-map-builder.js";
17
17
  import { setupBuildUse } from "./loader-resolution.js";
18
18
  import { findInOrphans } from "./error-handling.js";
19
19
  import { interceptSlotId } from "./intercept-resolution.js";
20
+ import { localRouteName } from "../route-name.js";
20
21
  import { loadManifest } from "./manifest.js";
21
22
  import { traverseBack } from "./pattern-matching.js";
22
23
  import type { RouterContext } from "./router-context.js";
23
- import type { ResolveSegmentOptions } from "./segment-resolution.js";
24
+ import { freshPassInput } from "./segment-resolution/resolution-pass.js";
24
25
  import { runWithRouterContext } from "./router-context.js";
25
26
  import type { EntryData, InterceptEntry } from "../server/context";
26
27
  import type { PartialPrerenderProps } from "../urls/pattern-types.js";
27
- import type {
28
- HandlerContext,
29
- InternalHandlerContext,
30
- ResolvedSegment,
31
- } from "../types";
28
+ import type { InternalHandlerContext, ResolvedSegment } from "../types";
32
29
  import type {
33
30
  SerializedSegmentData,
34
31
  SegmentHandleData,
@@ -39,6 +36,21 @@ import type { OnDemandRouteConfig } from "../prerender/on-demand.js";
39
36
  import { isPrerenderPersonalizationError } from "../prerender/producer-guard.js";
40
37
  import { isDataNotFoundError } from "../errors.js";
41
38
 
39
+ /**
40
+ * An intercept a navigation can select over the route that the build does
41
+ * not bake: its HIT misses (selectInterceptVariant) and it renders live,
42
+ * with the request instead of BuildContext.
43
+ * - `same-slot`: another intercept on its layout fills the same slot.
44
+ * - `handler-response` / `layout-response`: its handler or layout returned a
45
+ * Response for this URL.
46
+ * - `local-name`: it targets the route's localRouteName, which the build
47
+ * never bakes.
48
+ */
49
+ export interface LiveIntercept {
50
+ slot: string;
51
+ reason: "same-slot" | "handler-response" | "layout-response" | "local-name";
52
+ }
53
+
42
54
  export interface PrerenderMatchDeps<TEnv = any> {
43
55
  /** Owning router id; scopes bake-time root-scope/search-schema lookups the
44
56
  * same way reqCtx._routerId does at runtime (issue #762). */
@@ -55,14 +67,7 @@ export interface PrerenderMatchDeps<TEnv = any> {
55
67
  ) => RouteMatchResult<TEnv> | null | Promise<RouteMatchResult<TEnv> | null>;
56
68
  buildRouterContext: () => RouterContext<TEnv>;
57
69
  mergedRouteMap: Record<string, string>;
58
- resolveAllSegments: (
59
- entries: EntryData[],
60
- routeKey: string,
61
- params: Record<string, string>,
62
- context: HandlerContext<any, TEnv>,
63
- loaderPromises: Map<string, Promise<any>>,
64
- options?: ResolveSegmentOptions,
65
- ) => Promise<ResolvedSegment[]>;
70
+ resolveAllSegments: RouterContext<TEnv>["resolveAllSegments"];
66
71
  }
67
72
 
68
73
  /**
@@ -95,9 +100,13 @@ export async function matchForPrerender<TEnv = any>(
95
100
  routeName: string;
96
101
  params: Record<string, string>;
97
102
  interceptSegments?: SerializedSegmentData[];
98
- /** RSC-encoded MERGED (main + intercept) handle map for the intercept artifact;
99
- * the sinks store it as-is (no longer merge raw records). */
103
+ /** RSC-encoded handle map for the intercept artifact: the intercept's
104
+ * pushes and those of the main segments the intercepted route does not
105
+ * own; the sinks store it as-is. */
100
106
  interceptHandles?: string;
107
+ /** The intercepts the intercept variant does not hold, for the build's
108
+ * warning (warnLiveIntercepts). */
109
+ liveIntercepts?: LiveIntercept[];
101
110
  passthrough?: true;
102
111
  /** On-demand only: the render hit `notFound()` and nothing else failed. */
103
112
  notFound?: true;
@@ -340,16 +349,16 @@ export async function matchForPrerender<TEnv = any>(
340
349
  setupBuildUse(buildCtx);
341
350
 
342
351
  // 8. Resolve all segments with skipLoaders
343
- const loaderPromises = new Map<string, Promise<any>>();
344
- const allSegments = await deps.resolveAllSegments(
352
+ const { segments: allSegments } = await deps.resolveAllSegments(
353
+ freshPassInput(buildCtx, matched.routeKey, matchedParams, {
354
+ skipLoaders: true,
355
+ // A render failure (or `throw new Skip()`) must reach the
356
+ // build/dev caller, not be baked into a frozen error page
357
+ // (issue #587).
358
+ throwOnError: true,
359
+ }),
345
360
  entries,
346
- matched.routeKey,
347
- matchedParams,
348
- buildCtx,
349
- loaderPromises,
350
- // throwOnError: a render failure (or `throw new Skip()`) must reach the
351
- // build/dev caller, not be baked into a frozen error page (issue #587).
352
- { skipLoaders: true, throwOnError: true },
361
+ { pathname: buildCtx.pathname },
353
362
  );
354
363
 
355
364
  // 9. Detect passthrough sentinel: handler returned ctx.passthrough().
@@ -375,7 +384,7 @@ export async function matchForPrerender<TEnv = any>(
375
384
  // the tree) as an error row and completes normally. Rethrown below like
376
385
  // a handler throw, so the caller's policy decides instead of baking the
377
386
  // error (#914). On-demand refresh: a deep async child calling
378
- // cookies()/headers() throws here, not in resolveAllSegments; a
387
+ // cookies()/headers() throws here, not in segment resolution; a
379
388
  // personalization error is thrown in preference to any other so the
380
389
  // trigger maps it to skipped-personalized and keeps the old entry.
381
390
  const { serializeSegments } = await import("../cache/segment-codec.js");
@@ -453,18 +462,25 @@ export async function matchForPrerender<TEnv = any>(
453
462
  intercept: InterceptEntry;
454
463
  entry: EntryData;
455
464
  }[] = [];
456
- // The entry, then its orphans (nested ones included): the same traversal
457
- // findInterceptForRoute uses; the build keeps ALL matches (not just the
458
- // innermost) and skips when().
465
+ const localNamed: InterceptEntry[] = [];
466
+ const localName = localRouteName(matched.routeKey);
467
+ // From the route's parent up, each entry then its orphans (nested ones
468
+ // included): the walk findInterceptForRoute makes at runtime, so an
469
+ // intercept in the route's own layouts, which it never selects, is not
470
+ // baked. The build keeps ALL matches (not just the innermost) and skips
471
+ // when(); a HIT serves the one the match selected
472
+ // (selectInterceptVariant, prerender/store.ts).
459
473
  const collect = (source: EntryData): undefined => {
460
474
  for (const ic of source.intercept) {
461
475
  if (ic.routeName === matched.routeKey) {
462
476
  foundIntercepts.push({ intercept: ic, entry: source });
477
+ } else if (ic.routeName === localName) {
478
+ localNamed.push(ic);
463
479
  }
464
480
  }
465
481
  return undefined;
466
482
  };
467
- let current: EntryData | null = manifestEntry;
483
+ let current: EntryData | null = manifestEntry.parent;
468
484
  let prev: EntryData | null = null;
469
485
  while (current) {
470
486
  collect(current);
@@ -472,22 +488,50 @@ export async function matchForPrerender<TEnv = any>(
472
488
  prev = current;
473
489
  current = current.parent;
474
490
  }
491
+ // Two intercepts that fill one slot share its segment id, so a HIT
492
+ // could not tell which one the variant holds: neither is baked, and
493
+ // both render live.
494
+ const slotIds = foundIntercepts.map(({ intercept, entry }) =>
495
+ interceptSlotId(entry, intercept),
496
+ );
497
+ const bakedIntercepts = foundIntercepts.filter(
498
+ (_, i) =>
499
+ slotIds.indexOf(slotIds[i]!) === slotIds.lastIndexOf(slotIds[i]!),
500
+ );
501
+
502
+ const liveIntercepts: LiveIntercept[] = [];
503
+ const live = (
504
+ intercept: InterceptEntry,
505
+ reason: LiveIntercept["reason"],
506
+ ): void => {
507
+ liveIntercepts.push({ slot: intercept.slotName, reason });
508
+ };
509
+ for (const found of foundIntercepts) {
510
+ if (!bakedIntercepts.includes(found)) {
511
+ live(found.intercept, "same-slot");
512
+ }
513
+ }
514
+ // The match tries the local name only when no intercept for the full
515
+ // name applies, and one without when() always applies.
516
+ if (foundIntercepts.every(({ intercept }) => intercept.when?.length)) {
517
+ for (const intercept of localNamed) live(intercept, "local-name");
518
+ }
475
519
 
476
520
  // V1 writes only the main variant. Do not execute intercept producers
477
521
  // whose output would be discarded (and whose personalization/errors
478
522
  // must not block an otherwise valid main refresh).
479
- if (!onDemand && foundIntercepts.length > 0) {
523
+ if (!onDemand && bakedIntercepts.length > 0) {
480
524
  const interceptResolvedSegments: typeof nonLoaderSegments = [];
481
525
 
482
- for (const { intercept, entry: parentEntry } of foundIntercepts) {
526
+ for (const { intercept, entry: parentEntry } of bakedIntercepts) {
483
527
  // setupBuildUse keys handle pushes by ctx._currentSegmentId. The main
484
- // resolveAllSegments pass left it on the route's segment id, so pin it
528
+ // segment pass left it on the route's segment id, so pin it
485
529
  // to THIS intercept's slot id before resolving the handler/layout --
486
530
  // otherwise the intercept's ctx.use() handle pushes land in the wrong
487
531
  // bucket and getDataForSegment(seg.id) below drops them from the baked
488
532
  // artifact. Re-pinned per iteration so multiple intercepts targeting
489
- // the same route each get their own id (mirrors fresh.ts parallel-slot
490
- // pinning).
533
+ // the same route each get their own id (mirrors the walker's
534
+ // parallel-slot pinning, revalidation.ts).
491
535
  const interceptSegmentId = interceptSlotId(parentEntry, intercept);
492
536
  (buildCtx as InternalHandlerContext<any, TEnv>)._currentSegmentId =
493
537
  interceptSegmentId;
@@ -500,7 +544,7 @@ export async function matchForPrerender<TEnv = any>(
500
544
  const handlerResolved =
501
545
  handlerRaw instanceof Promise ? await handlerRaw : handlerRaw;
502
546
  if (handlerResolved instanceof Response) {
503
- // Handler returned a redirect/response -- skip this intercept
547
+ live(intercept, "handler-response");
504
548
  continue;
505
549
  }
506
550
  const component: ReactNode = handlerResolved;
@@ -510,7 +554,10 @@ export async function matchForPrerender<TEnv = any>(
510
554
  if (intercept.layout) {
511
555
  if (typeof intercept.layout === "function") {
512
556
  const layoutResult = await intercept.layout(buildCtx);
513
- if (layoutResult instanceof Response) continue;
557
+ if (layoutResult instanceof Response) {
558
+ live(intercept, "layout-response");
559
+ continue;
560
+ }
514
561
  layoutElement = layoutResult;
515
562
  } else {
516
563
  layoutElement = intercept.layout;
@@ -548,14 +595,22 @@ export async function matchForPrerender<TEnv = any>(
548
595
  await resolveSegmentHandleValues(segHandles);
549
596
  }
550
597
  }
551
- // The intercept artifact serves main + intercept segments together, so
552
- // encode the MERGED handle map here (the sinks no longer merge raw
553
- // records — they store this pre-encoded string as-is).
598
+ // The handle map of exactly the segments the sinks store for the
599
+ // intercept artifact; they store this string as-is.
600
+ const { interceptVariantSegments } =
601
+ await import("../prerender/store.js");
602
+ const variantHandlesRecord: Record<string, SegmentHandleData> = {};
603
+ for (const { metadata } of interceptVariantSegments(
604
+ serializedSegments,
605
+ interceptSegments,
606
+ )) {
607
+ const data =
608
+ handlesRecord[metadata.id] ??
609
+ interceptHandlesRecord[metadata.id];
610
+ if (data) variantHandlesRecord[metadata.id] = data;
611
+ }
554
612
  interceptHandles = await encodeHandles(
555
- {
556
- ...handlesRecord,
557
- ...interceptHandlesRecord,
558
- },
613
+ variantHandlesRecord,
559
614
  onFlightError,
560
615
  );
561
616
  }
@@ -570,6 +625,7 @@ export async function matchForPrerender<TEnv = any>(
570
625
  params: matchedParams,
571
626
  interceptSegments,
572
627
  interceptHandles,
628
+ liveIntercepts,
573
629
  onDemandConfig,
574
630
  ppr: routePpr,
575
631
  };
@@ -1,6 +1,5 @@
1
1
  import { negotiateRoute } from "./content-negotiation.js";
2
2
  import { runWithRouterLogContext, withRouterLogScope } from "./logging.js";
3
- import type { EntryData } from "../server/context";
4
3
  import type { RouteMatchResult } from "./pattern-matching.js";
5
4
  import type { MiddlewareFn } from "./middleware.js";
6
5
  import { resolveRoute } from "./route-snapshot.js";
@@ -12,8 +11,8 @@ export interface PreviewMatchDeps<TEnv = any> {
12
11
  }
13
12
 
14
13
  /**
15
- * Preview match - returns route middleware without segment resolution.
16
- * Also returns responseType and handler for response routes (non-RSC short-circuit).
14
+ * Preview match - returns the route, its params and route middleware without
15
+ * segment resolution (a content-negotiated variant's, when one wins).
17
16
  */
18
17
  export async function previewMatch<TEnv = any>(
19
18
  request: Request,
@@ -24,11 +23,7 @@ export async function previewMatch<TEnv = any>(
24
23
  handler: MiddlewareFn;
25
24
  params: Record<string, string>;
26
25
  }>;
27
- responseType?: string;
28
- handler?: Function;
29
26
  params?: Record<string, string>;
30
- negotiated?: boolean;
31
- manifestEntry?: EntryData;
32
27
  routeKey?: string;
33
28
  } | null> {
34
29
  return runWithRouterLogContext(
@@ -56,46 +51,16 @@ export async function previewMatch<TEnv = any>(
56
51
  }
57
52
 
58
53
  const snapshot = result.snapshot;
59
- const { matched, manifestEntry, routeMiddleware, responseType } =
60
- snapshot;
61
-
54
+ const { matched } = snapshot;
55
+ // A winning response variant brings its own route middleware.
62
56
  const negotiation = await negotiateRoute(request, pathname, snapshot);
63
- if (negotiation) {
64
- return {
65
- routeMiddleware:
66
- negotiation.routeMiddleware.length > 0
67
- ? negotiation.routeMiddleware
68
- : undefined,
69
- responseType: negotiation.responseType,
70
- handler: negotiation.handler,
71
- params: matched.params,
72
- manifestEntry: negotiation.manifestEntry,
73
- routeKey: matched.routeKey,
74
- // omitted unless a variant negotiated, preserving the prior public
75
- // shape (absent for plain response routes, not negotiated:false)
76
- ...(negotiation.negotiated ? { negotiated: true } : {}),
77
- };
78
- }
79
-
80
- // No negotiation or RSC won — return default route info
81
- const hasVariants =
82
- matched.negotiateVariants && matched.negotiateVariants.length > 0;
57
+ const routeMiddleware =
58
+ negotiation?.routeMiddleware ?? snapshot.routeMiddleware;
83
59
  return {
84
60
  routeMiddleware:
85
61
  routeMiddleware.length > 0 ? routeMiddleware : undefined,
86
62
  params: matched.params,
87
63
  routeKey: matched.routeKey,
88
- ...(responseType
89
- ? {
90
- responseType,
91
- handler:
92
- manifestEntry.type === "route"
93
- ? manifestEntry.handler
94
- : undefined,
95
- manifestEntry,
96
- }
97
- : {}),
98
- ...(hasVariants ? { negotiated: true } : {}),
99
64
  };
100
65
  }),
101
66
  );
@@ -22,6 +22,7 @@ import type { CollectedMiddleware } from "./middleware-types.js";
22
22
  import type { RouteMatchResult } from "./pattern-matching.js";
23
23
  import { negotiateRoute } from "./content-negotiation.js";
24
24
  import { stripInternalParams } from "./handler-context.js";
25
+ import { WIRE } from "../wire-params.js";
25
26
  import { resolveRoute, type RouteSnapshot } from "./route-snapshot.js";
26
27
 
27
28
  interface RedirectPlan<TEnv = any> {
@@ -74,6 +75,11 @@ interface FullRenderPlan<TEnv = any> {
74
75
  mode: "full-render";
75
76
  route: RouteSnapshot<TEnv>;
76
77
  negotiated: boolean;
78
+ /**
79
+ * No route matched: the render path renders the 404 (match() re-throws
80
+ * RouteNotFoundError). Set by rsc/request-stages.ts classify.
81
+ */
82
+ unmatched?: true;
77
83
  }
78
84
 
79
85
  interface PartialRenderPlan<TEnv = any> {
@@ -151,9 +157,9 @@ export async function classifyRequest<TEnv = any>(
151
157
  const pathname = url.pathname;
152
158
  const isAction =
153
159
  requestHeaders(request).has("rsc-action") ||
154
- url.searchParams.has("_rsc_action");
155
- const isLoaderFetch = url.searchParams.has("_rsc_loader");
156
- const isPartialReq = url.searchParams.has("_rsc_partial");
160
+ url.searchParams.has(WIRE.action);
161
+ const isLoaderFetch = url.searchParams.has(WIRE.loader);
162
+ const isPartialReq = url.searchParams.has(WIRE.partial);
157
163
  const contentType = requestHeaders(request).get("content-type") || "";
158
164
  const isFormSubmission =
159
165
  contentType.includes("multipart/form-data") ||
@@ -173,11 +179,11 @@ export async function classifyRequest<TEnv = any>(
173
179
  // Before the version check: each router has its own version, so a tab of
174
180
  // another app always carries one this router does not have. That request is
175
181
  // an app switch, not a stale client.
176
- const clientRouterId = url.searchParams.get("_rsc_rid");
182
+ const clientRouterId = url.searchParams.get(WIRE.routerId);
177
183
  if (
178
184
  clientRouterId &&
179
185
  clientRouterId !== deps.routerId &&
180
- url.searchParams.has("_rsc_partial")
186
+ url.searchParams.has(WIRE.partial)
181
187
  ) {
182
188
  return {
183
189
  mode: "app-switch",
@@ -185,7 +191,7 @@ export async function classifyRequest<TEnv = any>(
185
191
  };
186
192
  }
187
193
 
188
- const clientVersion = url.searchParams.get("_rsc_v");
194
+ const clientVersion = url.searchParams.get(WIRE.version);
189
195
  if (
190
196
  deps.routerVersion &&
191
197
  clientVersion &&
@@ -254,7 +260,7 @@ export async function classifyRequest<TEnv = any>(
254
260
 
255
261
  const actionId =
256
262
  requestHeaders(request).get("rsc-action") ||
257
- url.searchParams.get("_rsc_action");
263
+ url.searchParams.get(WIRE.action);
258
264
 
259
265
  const hasVariants =
260
266
  snapshot.matched.negotiateVariants &&
@@ -24,6 +24,22 @@ import { makeIsAction } from "./is-action.js";
24
24
  const REVALIDATE_ALWAYS: ShouldRevalidateFn<any, any> = () => true;
25
25
  const REVALIDATE_NEVER: ShouldRevalidateFn<any, any> = () => false;
26
26
 
27
+ /** Trace reasons of the action defaults, per lane: an action, a stale request. */
28
+ const ACTION_DEFAULT_REASONS = {
29
+ action: {
30
+ route: "action:route-segment",
31
+ loader: "action:loader-segment",
32
+ routeChild: "action:belongs-to-route",
33
+ parentChain: "action:parent-chain-skip",
34
+ },
35
+ stale: {
36
+ route: "stale:route-segment",
37
+ loader: "stale:loader-segment",
38
+ routeChild: "stale:belongs-to-route",
39
+ parentChain: "stale:parent-chain-skip",
40
+ },
41
+ } as const;
42
+
27
43
  /**
28
44
  * The predicate `revalidate(true)` / `revalidate(false)` stands for: the
29
45
  * constant hard decision of `revalidate(() => true)` / `() => false`.
@@ -61,8 +77,13 @@ export interface EvaluateRevalidationOptions<TEnv> {
61
77
  context: HandlerContext<any, TEnv>;
62
78
  /** Action context if triggered by action */
63
79
  actionContext?: ActionContext;
64
- /** If true, this is a stale cache revalidation request */
80
+ /** The browser's `_rsc_stale` signal: seeds the action defaults. */
65
81
  stale?: boolean;
82
+ /**
83
+ * The route-cache entry is past its TTL (SWR). Predicates see it as `stale`;
84
+ * it never seeds the action defaults.
85
+ */
86
+ cacheStale?: boolean;
66
87
  /** Trace source hint for the revalidation trace */
67
88
  traceSource?: RevalidationTraceEntry["source"];
68
89
  /**
@@ -100,6 +121,7 @@ export async function evaluateRevalidation<TEnv>(
100
121
  context,
101
122
  actionContext,
102
123
  stale,
124
+ cacheStale,
103
125
  traceSource,
104
126
  defaultOverride,
105
127
  } = options;
@@ -147,19 +169,23 @@ export async function evaluateRevalidation<TEnv>(
147
169
  if (defaultOverride) {
148
170
  defaultShouldRevalidate = defaultOverride.value;
149
171
  defaultReason = defaultOverride.reason;
150
- } else if (request.method === "POST") {
172
+ } else if (request.method === "POST" || stale) {
173
+ // A stale request re-renders a page an action may have changed after the
174
+ // client cached it. Not an action: no actionContext, isAction() is false.
175
+ const reasons =
176
+ ACTION_DEFAULT_REASONS[request.method === "POST" ? "action" : "stale"];
151
177
  if (segment.type === "route") {
152
178
  defaultShouldRevalidate = true;
153
- defaultReason = "action:route-segment";
179
+ defaultReason = reasons.route;
154
180
  } else if (segment.type === "loader") {
155
181
  defaultShouldRevalidate = true;
156
- defaultReason = "action:loader-segment";
182
+ defaultReason = reasons.loader;
157
183
  } else if (segment.belongsToRoute) {
158
184
  defaultShouldRevalidate = true;
159
- defaultReason = "action:belongs-to-route";
185
+ defaultReason = reasons.routeChild;
160
186
  } else {
161
187
  defaultShouldRevalidate = false;
162
- defaultReason = "action:parent-chain-skip";
188
+ defaultReason = reasons.parentChain;
163
189
  }
164
190
  } else {
165
191
  if (segment.type === "route") {
@@ -251,7 +277,7 @@ export async function evaluateRevalidation<TEnv>(
251
277
  routeName: toRouteName,
252
278
  fromRouteName,
253
279
  toRouteName,
254
- stale,
280
+ stale: cacheStale ? true : stale,
255
281
  });
256
282
  } catch (error) {
257
283
  // A thrown Response is control flow (e.g. `throw redirect(...)`), not a
@@ -20,6 +20,7 @@ import { loadManifest } from "./manifest.js";
20
20
  import { collectRouteMiddleware } from "./middleware.js";
21
21
  import type { CollectedMiddleware } from "./middleware-types.js";
22
22
  import { traverseBack } from "./pattern-matching.js";
23
+ import { localRouteName } from "../route-name.js";
23
24
  import type { RouteMatchResult } from "./pattern-matching.js";
24
25
 
25
26
  /**
@@ -170,10 +171,6 @@ export async function resolveRoute<TEnv = any>(
170
171
  matched.responseType ||
171
172
  (manifestEntry.type === "route" ? manifestEntry.responseType : undefined);
172
173
 
173
- const localRouteName = matched.routeKey.includes(".")
174
- ? matched.routeKey.split(".").pop()!
175
- : matched.routeKey;
176
-
177
174
  return {
178
175
  type: "match",
179
176
  snapshot: {
@@ -181,7 +178,7 @@ export async function resolveRoute<TEnv = any>(
181
178
  manifestEntry,
182
179
  entries,
183
180
  routeKey: matched.routeKey,
184
- localRouteName,
181
+ localRouteName: localRouteName(matched.routeKey),
185
182
  params: matched.params,
186
183
  routeMiddleware,
187
184
  cacheScope,