@rangojs/router 0.27.0 → 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 (212) hide show
  1. package/dist/bin/rango.js +1 -1
  2. package/dist/types/__internal.d.ts +7 -1
  3. package/dist/types/browser/event-controller.d.ts +2 -1
  4. package/dist/types/browser/history-state.d.ts +3 -9
  5. package/dist/types/browser/navigation-store.d.ts +8 -6
  6. package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
  7. package/dist/types/browser/segment-reconciler.d.ts +6 -4
  8. package/dist/types/browser/types.d.ts +2 -1
  9. package/dist/types/browser/wire-protocol.d.ts +25 -0
  10. package/dist/types/cache/cache-key-utils.d.ts +11 -5
  11. package/dist/types/cache/cache-scope.d.ts +34 -22
  12. package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
  13. package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
  14. package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
  15. package/dist/types/cache/handle-snapshot.d.ts +9 -4
  16. package/dist/types/cache/segment-codec.d.ts +14 -0
  17. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  18. package/dist/types/cache/tag-invalidation.d.ts +10 -0
  19. package/dist/types/cache/taint.d.ts +5 -3
  20. package/dist/types/cache/types.d.ts +7 -3
  21. package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
  22. package/dist/types/prefetch-markers.d.ts +5 -14
  23. package/dist/types/prerender/store.d.ts +31 -0
  24. package/dist/types/prerender/warm-request.d.ts +3 -3
  25. package/dist/types/prerender.d.ts +2 -2
  26. package/dist/types/route-name.d.ts +5 -0
  27. package/dist/types/router/error-handling.d.ts +4 -4
  28. package/dist/types/router/instrument.d.ts +4 -3
  29. package/dist/types/router/intercept-resolution.d.ts +25 -9
  30. package/dist/types/router/match-context.d.ts +4 -11
  31. package/dist/types/router/match-handlers.d.ts +2 -6
  32. package/dist/types/router/match-middleware/background-revalidation.d.ts +10 -20
  33. package/dist/types/router/match-middleware/cache-lookup.d.ts +34 -86
  34. package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
  35. package/dist/types/router/prerender-match.d.ts +21 -6
  36. package/dist/types/router/preview-match.d.ts +2 -7
  37. package/dist/types/router/request-classification.d.ts +5 -0
  38. package/dist/types/router/router-context.d.ts +7 -46
  39. package/dist/types/router/router-interfaces.d.ts +8 -14
  40. package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
  41. package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
  42. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
  43. package/dist/types/router/segment-resolution/resolution-pass.d.ts +93 -0
  44. package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
  45. package/dist/types/router/segment-resolution.d.ts +1 -2
  46. package/dist/types/router/segment-wrappers.d.ts +11 -48
  47. package/dist/types/router/telemetry.d.ts +2 -0
  48. package/dist/types/router/types.d.ts +10 -0
  49. package/dist/types/rsc/full-payload.d.ts +2 -2
  50. package/dist/types/rsc/handler-context.d.ts +6 -6
  51. package/dist/types/rsc/json-route-result.d.ts +5 -9
  52. package/dist/types/rsc/request-stages.d.ts +203 -0
  53. package/dist/types/rsc/response-cache-serve.d.ts +7 -12
  54. package/dist/types/rsc/response-route-handler.d.ts +2 -2
  55. package/dist/types/rsc/routine-plan.d.ts +1 -1
  56. package/dist/types/rsc/server-action.d.ts +2 -2
  57. package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
  58. package/dist/types/rsc/shell-capture.d.ts +132 -15
  59. package/dist/types/rsc/shell-serve.d.ts +37 -12
  60. package/dist/types/server/context.d.ts +9 -8
  61. package/dist/types/server/handle-store.d.ts +16 -0
  62. package/dist/types/server/render-pass.d.ts +72 -0
  63. package/dist/types/server/request-context.d.ts +24 -104
  64. package/dist/types/server/rewrite-trail.d.ts +2 -2
  65. package/dist/types/testing/dispatch.d.ts +35 -18
  66. package/dist/types/types/cache-types.d.ts +6 -3
  67. package/dist/types/types/error-types.d.ts +9 -1
  68. package/dist/types/types/handler-context.d.ts +9 -1
  69. package/dist/types/types/segments.d.ts +1 -1
  70. package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
  71. package/dist/types/vite/discovery/build-env.d.ts +22 -0
  72. package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
  73. package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
  74. package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
  75. package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
  76. package/dist/types/vite/discovery/state.d.ts +12 -0
  77. package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
  78. package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
  79. package/dist/types/vite/router-discovery.d.ts +8 -47
  80. package/dist/types/vite/temp-rsc-server.d.ts +39 -0
  81. package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
  82. package/dist/types/wire-params.d.ts +34 -0
  83. package/dist/vite/index.js +3809 -3710
  84. package/package.json +1 -1
  85. package/skills/cache-guide/SKILL.md +8 -5
  86. package/skills/caching/SKILL.md +37 -18
  87. package/skills/document-cache/SKILL.md +13 -1
  88. package/skills/handler-use/SKILL.md +1 -1
  89. package/skills/intercept/SKILL.md +17 -7
  90. package/skills/middleware/SKILL.md +18 -0
  91. package/skills/ppr/SKILL.md +18 -3
  92. package/skills/prerender/SKILL.md +39 -15
  93. package/skills/rango/SKILL.md +5 -1
  94. package/skills/server-actions/SKILL.md +8 -1
  95. package/skills/testing/response-routes.md +6 -4
  96. package/skills/use-cache/SKILL.md +46 -8
  97. package/src/__internal.ts +7 -1
  98. package/src/browser/connection-warmup.ts +3 -1
  99. package/src/browser/event-controller.ts +5 -3
  100. package/src/browser/history-state.ts +6 -12
  101. package/src/browser/navigation-bridge.ts +14 -13
  102. package/src/browser/navigation-client.ts +18 -25
  103. package/src/browser/navigation-store.ts +25 -27
  104. package/src/browser/navigation-transaction.ts +4 -1
  105. package/src/browser/prefetch/cache.ts +2 -1
  106. package/src/browser/prefetch/fetch.ts +8 -16
  107. package/src/browser/prefetch/queue.ts +4 -4
  108. package/src/browser/react/NavigationProvider.tsx +2 -1
  109. package/src/browser/react/filter-segment-order.ts +2 -2
  110. package/src/browser/rsc-router.tsx +1 -0
  111. package/src/browser/segment-reconciler.ts +66 -53
  112. package/src/browser/server-action-bridge.ts +50 -57
  113. package/src/browser/types.ts +2 -1
  114. package/src/browser/wire-protocol.ts +41 -0
  115. package/src/cache/cache-key-utils.ts +11 -5
  116. package/src/cache/cache-runtime.ts +110 -50
  117. package/src/cache/cache-scope.ts +110 -64
  118. package/src/cache/cf/cf-cache-constants.ts +2 -2
  119. package/src/cache/cf/cf-cache-store.ts +486 -874
  120. package/src/cache/cf/cf-kv-utils.ts +1 -1
  121. package/src/cache/document-cache.ts +34 -13
  122. package/src/cache/handle-snapshot.ts +19 -7
  123. package/src/cache/segment-codec.ts +55 -0
  124. package/src/cache/shell-snapshot.ts +2 -2
  125. package/src/cache/tag-invalidation.ts +21 -2
  126. package/src/cache/taint.ts +5 -3
  127. package/src/cache/types.ts +7 -3
  128. package/src/cache/vercel/vercel-cache-store.ts +243 -310
  129. package/src/handles/pushed-by.ts +1 -1
  130. package/src/prefetch-markers.ts +5 -16
  131. package/src/prerender/store.ts +76 -0
  132. package/src/prerender/warm-request.ts +8 -4
  133. package/src/prerender.ts +2 -2
  134. package/src/route-name.ts +8 -0
  135. package/src/router/error-handling.ts +4 -4
  136. package/src/router/instrument.ts +4 -3
  137. package/src/router/intercept-resolution.ts +77 -75
  138. package/src/router/loader-resolution.ts +5 -4
  139. package/src/router/match-api.ts +4 -5
  140. package/src/router/match-context.ts +20 -12
  141. package/src/router/match-handlers.ts +8 -20
  142. package/src/router/match-middleware/background-revalidation.ts +41 -53
  143. package/src/router/match-middleware/cache-lookup.ts +675 -726
  144. package/src/router/match-middleware/cache-store.ts +4 -8
  145. package/src/router/match-middleware/intercept-resolution.ts +3 -23
  146. package/src/router/match-middleware/segment-resolution.ts +34 -61
  147. package/src/router/middleware.ts +69 -11
  148. package/src/router/navigation-snapshot.ts +4 -4
  149. package/src/router/prerender-match.ts +100 -44
  150. package/src/router/preview-match.ts +6 -41
  151. package/src/router/request-classification.ts +13 -7
  152. package/src/router/route-snapshot.ts +2 -5
  153. package/src/router/router-context.ts +10 -95
  154. package/src/router/router-interfaces.ts +8 -14
  155. package/src/router/segment-resolution/loader-cache.ts +14 -9
  156. package/src/router/segment-resolution/loader-mask.ts +5 -5
  157. package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
  158. package/src/router/segment-resolution/resolution-pass.ts +137 -0
  159. package/src/router/segment-resolution/revalidation.ts +897 -776
  160. package/src/router/segment-resolution.ts +4 -13
  161. package/src/router/segment-wrappers.ts +51 -269
  162. package/src/router/telemetry.ts +5 -0
  163. package/src/router/types.ts +10 -0
  164. package/src/router.ts +12 -13
  165. package/src/rsc/full-payload.ts +6 -5
  166. package/src/rsc/handler-context.ts +8 -14
  167. package/src/rsc/handler.ts +126 -507
  168. package/src/rsc/json-route-result.ts +5 -9
  169. package/src/rsc/loader-fetch.ts +6 -5
  170. package/src/rsc/progressive-enhancement.ts +5 -0
  171. package/src/rsc/request-stages.ts +757 -0
  172. package/src/rsc/response-cache-serve.ts +10 -14
  173. package/src/rsc/response-route-handler.ts +13 -5
  174. package/src/rsc/rewrite-passes.ts +2 -1
  175. package/src/rsc/routine-plan.ts +1 -1
  176. package/src/rsc/rsc-rendering.ts +164 -349
  177. package/src/rsc/server-action.ts +87 -113
  178. package/src/rsc/shell-capture-constants.ts +1 -1
  179. package/src/rsc/shell-capture.ts +212 -169
  180. package/src/rsc/shell-serve.ts +121 -18
  181. package/src/rsc/ssr-setup.ts +5 -4
  182. package/src/segment-system.tsx +3 -3
  183. package/src/server/context.ts +22 -25
  184. package/src/server/handle-store.ts +55 -0
  185. package/src/server/render-pass.ts +291 -0
  186. package/src/server/request-context.ts +26 -115
  187. package/src/server/rewrite-trail.ts +6 -4
  188. package/src/ssr/index.tsx +1 -1
  189. package/src/testing/dispatch.ts +223 -710
  190. package/src/testing/e2e/page-helpers.ts +2 -2
  191. package/src/testing/serve-shell-request.ts +23 -36
  192. package/src/types/cache-types.ts +6 -3
  193. package/src/types/error-types.ts +9 -1
  194. package/src/types/handler-context.ts +12 -1
  195. package/src/types/segments.ts +1 -1
  196. package/src/vite/discovery/build-discovery-phase.ts +204 -0
  197. package/src/vite/discovery/build-env.ts +148 -0
  198. package/src/vite/discovery/bundle-scan-phase.ts +128 -0
  199. package/src/vite/discovery/dev-prerender-cache.ts +8 -3
  200. package/src/vite/discovery/dev-server-phase.ts +1776 -0
  201. package/src/vite/discovery/discover-routers.ts +1 -1
  202. package/src/vite/discovery/discovery-errors.ts +1 -1
  203. package/src/vite/discovery/prerender-collection.ts +24 -39
  204. package/src/vite/discovery/state.ts +15 -0
  205. package/src/vite/plugins/version-plugin.ts +1 -1
  206. package/src/vite/plugins/virtual-entries.ts +1 -1
  207. package/src/vite/router-discovery.ts +29 -2428
  208. package/src/vite/temp-rsc-server.ts +223 -0
  209. package/src/vite/utils/prerender-utils.ts +47 -0
  210. package/src/wire-params.ts +35 -0
  211. package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
  212. 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 &&
@@ -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,
@@ -19,27 +19,9 @@ import type {
19
19
  } from "../types.js";
20
20
  import type { RouteMatchResult } from "./pattern-matching.js";
21
21
  import type { TelemetrySink } from "./telemetry.js";
22
- import type { ResolveSegmentOptions } from "./segment-resolution.js";
23
22
  import type { EvaluateRevalidationOptions } from "./revalidation.js";
24
-
25
- /**
26
- * Revalidation context passed to segment resolution
27
- */
28
- export interface RevalidationContext {
29
- clientSegmentIds: ReadonlySet<string>;
30
- prevParams: Record<string, string>;
31
- request: Request;
32
- prevUrl: URL;
33
- nextUrl: URL;
34
- routeKey: string;
35
- actionContext?: {
36
- actionId?: string;
37
- actionUrl?: URL;
38
- actionResult?: any;
39
- formData?: FormData;
40
- };
41
- stale?: boolean;
42
- }
23
+ import type { ResolutionPassInput } from "./segment-resolution/resolution-pass.js";
24
+ import type { RevalidatingWalkOptions } from "./segment-resolution/revalidation.js";
43
25
 
44
26
  /**
45
27
  * Result from intercept lookup
@@ -118,55 +100,22 @@ export interface RouterContext<TEnv = any> {
118
100
  isAction: boolean,
119
101
  ) => InterceptResult | null;
120
102
 
121
- resolveAllSegmentsWithRevalidation: (
103
+ /** The segment walker; a full match passes freshPassInput (FRESH). */
104
+ resolveAllSegments: (
105
+ pass: ResolutionPassInput<TEnv>,
122
106
  entries: EntryData[],
123
- routeKey: string,
124
- params: Record<string, string>,
125
- handlerContext: HandlerContext<any, TEnv>,
126
- clientSegmentSet: Set<string>,
127
- prevParams: Record<string, string>,
128
- request: Request,
129
- prevUrl: URL,
130
- nextUrl: URL,
131
- actionContext: any | undefined,
132
- interceptResult: InterceptResult | null,
133
- localRouteName: string,
134
- pathname: string,
135
- stale?: boolean,
136
- /** The matched route's full chain when `entries` is part of it (ResolveSegmentOptions.chain). */
137
- chain?: readonly EntryData[],
107
+ walk: RevalidatingWalkOptions,
138
108
  ) => Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }>;
139
109
 
140
- // Generator-based segment resolution (for pipeline)
141
- resolveAllSegmentsWithRevalidationGenerator?: (
142
- entries: EntryData[],
143
- routeKey: string,
144
- params: Record<string, string>,
145
- handlerContext: HandlerContext<any, TEnv>,
146
- clientSegmentSet: Set<string>,
147
- prevParams: Record<string, string>,
148
- request: Request,
149
- prevUrl: URL,
150
- nextUrl: URL,
151
- loaderPromises: Map<string, Promise<any>>,
152
- actionContext?: any,
153
- ) => AsyncGenerator<ResolvedSegment | { __type: "id"; id: string }>;
154
-
155
110
  // Intercept resolution
156
111
  resolveInterceptEntry: (
112
+ pass: ResolutionPassInput<TEnv>,
157
113
  intercept: InterceptEntry,
158
114
  entry: EntryData,
159
- params: Record<string, string>,
160
- handlerContext: HandlerContext<any, TEnv>,
161
115
  belongsToRoute: boolean,
162
- revalidationContext?: RevalidationContext,
163
116
  options?: { background?: boolean },
164
117
  ) => Promise<ResolvedSegment[]>;
165
118
 
166
- collectWithMarkers?: <T>(
167
- gen: AsyncGenerator<T | { __type: "id"; id: string }>,
168
- ) => Promise<{ items: T[]; matchedIds: string[] }>;
169
-
170
119
  evaluateRevalidation: (
171
120
  params: EvaluateRevalidationOptions<TEnv>,
172
121
  ) => Promise<boolean>;
@@ -178,45 +127,12 @@ export interface RouterContext<TEnv = any> {
178
127
  }
179
128
  | undefined;
180
129
 
181
- resolveAllSegments: (
182
- entries: EntryData[],
183
- routeKey: string,
184
- params: Record<string, string>,
185
- handlerContext: HandlerContext<any, TEnv>,
186
- loaderPromises: Map<string, Promise<any>>,
187
- options?: ResolveSegmentOptions,
188
- ) => Promise<ResolvedSegment[]>;
189
-
190
- resolveAllSegmentsGenerator?: (
191
- entries: EntryData[],
192
- routeKey: string,
193
- params: Record<string, string>,
194
- handlerContext: HandlerContext<any, TEnv>,
195
- loaderPromises: Map<string, Promise<any>>,
196
- ) => AsyncGenerator<ResolvedSegment | { __type: "id"; id: string }>;
197
-
198
- collectSegmentsFromGenerator?: <T>(
199
- gen: AsyncGenerator<T | { __type: "id"; id: string }>,
200
- ) => Promise<T[]>;
201
-
202
130
  createHandleStore: () => any;
203
131
 
204
132
  resolveLoadersOnly?: (
133
+ pass: ResolutionPassInput<TEnv>,
205
134
  entries: EntryData[],
206
- handlerContext: HandlerContext<any, TEnv>,
207
- ) => Promise<ResolvedSegment[]>;
208
-
209
- resolveLoadersOnlyWithRevalidation?: (
210
- entries: EntryData[],
211
- handlerContext: HandlerContext<any, TEnv>,
212
- clientSegmentSet: Set<string>,
213
- prevParams: Record<string, string>,
214
- request: Request,
215
- prevUrl: URL,
216
- nextUrl: URL,
217
- routeKey: string,
218
- actionContext?: any,
219
- stale?: boolean,
135
+ walk?: Pick<RevalidatingWalkOptions, "interceptResult">,
220
136
  ) => Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }>;
221
137
 
222
138
  // Entry revalidation map
@@ -230,11 +146,10 @@ export interface RouterContext<TEnv = any> {
230
146
  requestId?: string;
231
147
 
232
148
  resolveInterceptLoadersOnly?: (
149
+ pass: ResolutionPassInput<TEnv>,
233
150
  intercept: InterceptEntry,
234
151
  entry: EntryData,
235
- handlerContext: HandlerContext<any, TEnv>,
236
152
  belongsToRoute: boolean,
237
- revalidationContext: RevalidationContext,
238
153
  ) => Promise<ResolvedSegment[]>;
239
154
  }
240
155
 
@@ -4,7 +4,6 @@ import type { ReverseFunction } from "../reverse.js";
4
4
  import type { UrlPatterns } from "../urls.js";
5
5
  import type { ClientUrlPatterns } from "../client-urls/types.js";
6
6
  import type { UrlBuilder, EnvCompatible } from "../urls/pattern-types.js";
7
- import type { EntryData } from "../server/context";
8
7
  import type { ErrorInfo, MatchResult } from "../types";
9
8
  import type { NonceProvider } from "../rsc/types.js";
10
9
  import type { ShellCaptureDebug } from "../rsc/shell-capture.js";
@@ -12,6 +11,7 @@ import type { ExecutionContext } from "../server/request-context.js";
12
11
  import type { SerializedSegmentData } from "../cache/types.js";
13
12
  import type { MiddlewareEntry, MiddlewareFn } from "./middleware.js";
14
13
  import type { RouteMatchResult } from "./pattern-matching.js";
14
+ import type { LiveIntercept } from "./prerender-match.js";
15
15
  import type { ExtractParams } from "../types/route-config.js";
16
16
  import { RSC_ROUTER_BRAND } from "./router-registry.js";
17
17
  import type { RangoOptions, RootLayoutProps } from "./router-options.js";
@@ -19,6 +19,7 @@ import type { DefaultVars } from "../types/global-namespace.js";
19
19
  import type { ResolvedTimeouts, OnTimeoutCallback } from "./timeout.js";
20
20
  import type { ResolvedTracing } from "./tracing.js";
21
21
  import type { TelemetrySink } from "./telemetry.js";
22
+ import type { ActionContext } from "./types.js";
22
23
 
23
24
  /**
24
25
  * Options passed to router.fetch(), router.match(), and other request entrypoints.
@@ -544,8 +545,10 @@ export interface RangoInternal<
544
545
  routeName: string;
545
546
  params: Record<string, string>;
546
547
  interceptSegments?: SerializedSegmentData[];
547
- /** RSC-encoded MERGED (main + intercept) handle map for the intercept artifact. */
548
+ /** RSC-encoded handle map for the intercept artifact (see matchForPrerender). */
548
549
  interceptHandles?: string;
550
+ /** The intercepts the intercept artifact does not hold (see matchForPrerender). */
551
+ liveIntercepts?: LiveIntercept[];
549
552
  passthrough?: true;
550
553
  } | null>;
551
554
 
@@ -563,8 +566,8 @@ export interface RangoInternal<
563
566
  ): Promise<{ encoded: string; handles: string } | null>;
564
567
 
565
568
  /**
566
- * Preview match - returns route middleware without segment resolution.
567
- * Also returns responseType and handler for response routes (non-RSC short-circuit).
569
+ * Preview match - returns the route, its params and route middleware
570
+ * without segment resolution.
568
571
  */
569
572
  previewMatch(
570
573
  request: Request,
@@ -574,23 +577,14 @@ export interface RangoInternal<
574
577
  handler: import("./middleware.js").MiddlewareFn;
575
578
  params: Record<string, string>;
576
579
  }>;
577
- responseType?: string;
578
- handler?: Function;
579
580
  params?: Record<string, string>;
580
- negotiated?: boolean;
581
- manifestEntry?: EntryData;
582
581
  routeKey?: string;
583
582
  } | null>;
584
583
 
585
584
  matchPartial(
586
585
  request: Request,
587
586
  input?: RouterRequestInput<TEnv>,
588
- actionContext?: {
589
- actionId?: string;
590
- actionUrl?: URL;
591
- actionResult?: any;
592
- formData?: FormData;
593
- },
587
+ actionContext?: ActionContext,
594
588
  ): Promise<MatchResult | null>;
595
589
 
596
590
  /**