@rangojs/router 0.21.0 → 0.22.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/bin/rango.js +13 -1
  2. package/dist/types/browser/event-controller.d.ts +30 -13
  3. package/dist/types/browser/navigation-store.d.ts +9 -0
  4. package/dist/types/browser/partial-update.d.ts +2 -1
  5. package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
  6. package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
  7. package/dist/types/browser/react/context.d.ts +14 -0
  8. package/dist/types/browser/rsc-router.d.ts +13 -2
  9. package/dist/types/browser/types.d.ts +10 -0
  10. package/dist/types/build/generate-manifest.d.ts +2 -0
  11. package/dist/types/cache/background-task.d.ts +22 -0
  12. package/dist/types/cache/cache-key-utils.d.ts +50 -5
  13. package/dist/types/cache/cache-policy.d.ts +17 -0
  14. package/dist/types/cache/cache-scope.d.ts +1 -1
  15. package/dist/types/cache/cf/cf-cache-store.d.ts +7 -1
  16. package/dist/types/cache/document-cache.d.ts +8 -5
  17. package/dist/types/cache/handle-snapshot.d.ts +23 -0
  18. package/dist/types/cache/index.d.ts +1 -1
  19. package/dist/types/cache/invalidation-order.d.ts +14 -0
  20. package/dist/types/cache/memory-segment-store.d.ts +33 -1
  21. package/dist/types/cache/shell-snapshot.d.ts +10 -1
  22. package/dist/types/cache/store-scope.d.ts +22 -0
  23. package/dist/types/cache/tag-invalidation.d.ts +7 -6
  24. package/dist/types/cache/types.d.ts +63 -14
  25. package/dist/types/cache/vercel/vercel-cache-store.d.ts +7 -1
  26. package/dist/types/errors.d.ts +6 -0
  27. package/dist/types/handles/deferred-resolution.d.ts +7 -0
  28. package/dist/types/internal-suspense-audit.d.ts +1 -0
  29. package/dist/types/loader-store.d.ts +4 -8
  30. package/dist/types/outlet-provider.d.ts +2 -1
  31. package/dist/types/prerender/cloudflare.d.ts +49 -0
  32. package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
  33. package/dist/types/prerender/index.d.ts +12 -0
  34. package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
  35. package/dist/types/prerender/on-demand.d.ts +272 -0
  36. package/dist/types/prerender/producer-guard.d.ts +33 -0
  37. package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
  38. package/dist/types/prerender/warm-request.d.ts +65 -0
  39. package/dist/types/prerender/warm.d.ts +65 -0
  40. package/dist/types/prerender/writable-store.d.ts +170 -0
  41. package/dist/types/prerender.d.ts +27 -0
  42. package/dist/types/reverse.d.ts +1 -1
  43. package/dist/types/route-content-wrapper.d.ts +3 -1
  44. package/dist/types/router/error-handling.d.ts +2 -0
  45. package/dist/types/router/handler-context.d.ts +1 -1
  46. package/dist/types/router/match-middleware/cache-lookup.d.ts +14 -0
  47. package/dist/types/router/pattern-matching.d.ts +2 -0
  48. package/dist/types/router/prefetch-default.d.ts +4 -4
  49. package/dist/types/router/prerender-match.d.ts +23 -1
  50. package/dist/types/router/route-trie-builder.d.ts +8 -1
  51. package/dist/types/router/router-context.d.ts +1 -0
  52. package/dist/types/router/router-interfaces.d.ts +30 -0
  53. package/dist/types/router/router-options.d.ts +24 -0
  54. package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
  55. package/dist/types/router/segment-resolution/loader-cache.d.ts +2 -1
  56. package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
  57. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
  58. package/dist/types/router/trie-matching.d.ts +2 -0
  59. package/dist/types/rsc/shell-build-manifest.d.ts +6 -4
  60. package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
  61. package/dist/types/rsc/shell-capture.d.ts +14 -1
  62. package/dist/types/rsc/shell-serve.d.ts +2 -2
  63. package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
  64. package/dist/types/rsc/types.d.ts +8 -0
  65. package/dist/types/segment-boundary-content.d.ts +17 -0
  66. package/dist/types/segment-loader-promise.d.ts +2 -6
  67. package/dist/types/server/build-version-table.d.ts +9 -0
  68. package/dist/types/server/context.d.ts +12 -4
  69. package/dist/types/server/handle-store.d.ts +2 -0
  70. package/dist/types/server/request-context.d.ts +63 -2
  71. package/dist/types/suspense-audit-react.d.ts +26 -0
  72. package/dist/types/suspense-audit.d.ts +157 -0
  73. package/dist/types/testing/action-form.d.ts +34 -0
  74. package/dist/types/testing/flight.entry.d.ts +2 -0
  75. package/dist/types/testing/index.d.ts +4 -0
  76. package/dist/types/testing/internal/server-modules.d.ts +8 -0
  77. package/dist/types/testing/serve-shell-request.d.ts +34 -4
  78. package/dist/types/testing/shell-status.d.ts +14 -6
  79. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
  80. package/dist/types/thenable-status.d.ts +6 -0
  81. package/dist/types/types/handler-context.d.ts +7 -0
  82. package/dist/types/types/route-entry.d.ts +5 -0
  83. package/dist/types/urls/path-helper.d.ts +18 -0
  84. package/dist/types/vite/discovery/state.d.ts +11 -0
  85. package/dist/types/vite/inject-client-debug.d.ts +6 -25
  86. package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
  87. package/dist/vite/index.js +180 -497
  88. package/package.json +13 -3
  89. package/skills/cache-guide/SKILL.md +18 -17
  90. package/skills/caching/SKILL.md +40 -4
  91. package/skills/catalog.json +1 -1
  92. package/skills/cloudflare/SKILL.md +131 -0
  93. package/skills/comparison/references/framework-comparison.md +1 -1
  94. package/skills/deployment-caching/SKILL.md +33 -12
  95. package/skills/document-cache/SKILL.md +16 -14
  96. package/skills/hooks/SKILL.md +22 -0
  97. package/skills/hooks/handle-and-actions.md +3 -4
  98. package/skills/hooks/navigation.md +10 -0
  99. package/skills/hooks/state.md +5 -4
  100. package/skills/host-router/SKILL.md +18 -0
  101. package/skills/loader/SKILL.md +6 -4
  102. package/skills/middleware/SKILL.md +1 -1
  103. package/skills/migrate-nextjs/SKILL.md +82 -9
  104. package/skills/ppr/SKILL.md +31 -8
  105. package/skills/prerender/SKILL.md +645 -8
  106. package/skills/rango/SKILL.md +1 -0
  107. package/skills/response-routes/SKILL.md +5 -5
  108. package/skills/route/SKILL.md +29 -0
  109. package/skills/testing/SKILL.md +2 -1
  110. package/skills/testing/cache-prerender.md +18 -17
  111. package/skills/testing/serve-shell-request.md +104 -8
  112. package/skills/use-cache/SKILL.md +9 -5
  113. package/skills/vercel/SKILL.md +2 -0
  114. package/skills/view-transitions/SKILL.md +9 -4
  115. package/src/browser/event-controller.ts +69 -25
  116. package/src/browser/link-interceptor.ts +2 -2
  117. package/src/browser/navigation-bridge.ts +75 -35
  118. package/src/browser/navigation-store.ts +26 -0
  119. package/src/browser/navigation-transaction.ts +9 -2
  120. package/src/browser/network-error-handler.ts +2 -0
  121. package/src/browser/partial-update.ts +48 -12
  122. package/src/browser/prefetch/default-strategy.ts +22 -42
  123. package/src/browser/react/Link.tsx +37 -42
  124. package/src/browser/react/NavigationProvider.tsx +63 -3
  125. package/src/browser/react/context.ts +17 -0
  126. package/src/browser/react/location-state.ts +5 -19
  127. package/src/browser/react/use-action.ts +4 -1
  128. package/src/browser/react/use-handle.ts +4 -20
  129. package/src/browser/react/use-link-status.ts +4 -3
  130. package/src/browser/react/use-navigation.ts +4 -1
  131. package/src/browser/react/use-params.ts +4 -1
  132. package/src/browser/react/use-pathname.ts +4 -1
  133. package/src/browser/react/use-search-params.ts +5 -3
  134. package/src/browser/react/use-segments.ts +36 -54
  135. package/src/browser/rsc-router.tsx +79 -39
  136. package/src/browser/segment-reconciler.ts +3 -3
  137. package/src/browser/server-action-bridge.ts +26 -6
  138. package/src/browser/types.ts +11 -0
  139. package/src/build/generate-manifest.ts +17 -0
  140. package/src/build/merge-full-manifests.ts +5 -0
  141. package/src/build/route-trie.ts +1 -0
  142. package/src/cache/background-task.ts +61 -0
  143. package/src/cache/cache-key-utils.ts +91 -12
  144. package/src/cache/cache-policy.ts +11 -2
  145. package/src/cache/cache-runtime.ts +23 -23
  146. package/src/cache/cache-scope.ts +38 -36
  147. package/src/cache/cf/cf-cache-store.ts +16 -11
  148. package/src/cache/document-cache.ts +35 -14
  149. package/src/cache/handle-snapshot.ts +50 -0
  150. package/src/cache/index.ts +1 -0
  151. package/src/cache/invalidation-order.ts +53 -0
  152. package/src/cache/memory-segment-store.ts +52 -1
  153. package/src/cache/shell-snapshot.ts +17 -0
  154. package/src/cache/store-scope.ts +36 -0
  155. package/src/cache/tag-invalidation.ts +7 -5
  156. package/src/cache/types.ts +69 -14
  157. package/src/cache/vercel/vercel-cache-store.ts +20 -6
  158. package/src/client.tsx +40 -3
  159. package/src/errors.ts +13 -6
  160. package/src/handles/deferred-resolution.ts +7 -2
  161. package/src/internal-suspense-audit.ts +12 -0
  162. package/src/loader-store.ts +4 -9
  163. package/src/outlet-provider.tsx +25 -1
  164. package/src/prerender/cloudflare.ts +130 -0
  165. package/src/prerender/create-prerender-trigger.ts +780 -0
  166. package/src/prerender/index.ts +39 -0
  167. package/src/prerender/memory-prerender-store.ts +115 -0
  168. package/src/prerender/on-demand.ts +339 -0
  169. package/src/prerender/producer-guard.ts +66 -0
  170. package/src/prerender/shell-manifest-key.ts +12 -17
  171. package/src/prerender/warm-request.ts +154 -0
  172. package/src/prerender/warm.ts +193 -0
  173. package/src/prerender/writable-store.ts +278 -0
  174. package/src/prerender.ts +29 -0
  175. package/src/reverse.ts +1 -1
  176. package/src/route-content-wrapper.tsx +107 -13
  177. package/src/router/error-handling.ts +12 -12
  178. package/src/router/find-match.ts +1 -0
  179. package/src/router/handler-context.ts +2 -0
  180. package/src/router/intercept-resolution.ts +15 -8
  181. package/src/router/loader-resolution.ts +2 -10
  182. package/src/router/match-middleware/background-revalidation.ts +2 -0
  183. package/src/router/match-middleware/cache-lookup.ts +230 -38
  184. package/src/router/pattern-matching.ts +14 -0
  185. package/src/router/prefetch-default.ts +4 -4
  186. package/src/router/prerender-match.ts +460 -352
  187. package/src/router/route-trie-builder.ts +9 -0
  188. package/src/router/router-context.ts +1 -0
  189. package/src/router/router-interfaces.ts +33 -0
  190. package/src/router/router-options.ts +30 -0
  191. package/src/router/segment-resolution/fresh.ts +20 -19
  192. package/src/router/segment-resolution/loader-cache.ts +15 -17
  193. package/src/router/segment-resolution/revalidation.ts +34 -24
  194. package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
  195. package/src/router/trie-matching.ts +3 -0
  196. package/src/router.ts +107 -1
  197. package/src/rsc/handler.ts +51 -4
  198. package/src/rsc/progressive-enhancement.ts +5 -1
  199. package/src/rsc/response-cache-serve.ts +10 -4
  200. package/src/rsc/rsc-rendering.ts +53 -11
  201. package/src/rsc/shell-build-manifest.ts +10 -8
  202. package/src/rsc/shell-capture-constants.ts +39 -7
  203. package/src/rsc/shell-capture.ts +108 -19
  204. package/src/rsc/shell-serve.ts +12 -6
  205. package/src/rsc/ssr-module-loader.ts +26 -0
  206. package/src/rsc/types.ts +8 -0
  207. package/src/segment-boundary-content.ts +25 -0
  208. package/src/segment-loader-promise.ts +28 -38
  209. package/src/segment-system.tsx +117 -57
  210. package/src/server/build-version-table.ts +15 -0
  211. package/src/server/context.ts +40 -17
  212. package/src/server/cookie-store.ts +5 -0
  213. package/src/server/handle-store.ts +13 -0
  214. package/src/server/loader-registry.ts +6 -1
  215. package/src/server/request-context.ts +87 -1
  216. package/src/ssr/ssr-root.tsx +20 -13
  217. package/src/suspense-audit-react.tsx +98 -0
  218. package/src/suspense-audit.ts +1030 -0
  219. package/src/testing/action-form.ts +66 -0
  220. package/src/testing/flight-runtime.d.ts +27 -0
  221. package/src/testing/flight.entry.ts +3 -0
  222. package/src/testing/index.ts +19 -0
  223. package/src/testing/internal/server-modules.ts +32 -0
  224. package/src/testing/render-route.tsx +25 -12
  225. package/src/testing/serve-shell-request.ts +190 -34
  226. package/src/testing/shell-status.ts +17 -6
  227. package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
  228. package/src/thenable-status.ts +10 -0
  229. package/src/types/handler-context.ts +7 -0
  230. package/src/types/route-entry.ts +6 -0
  231. package/src/urls/path-helper.ts +69 -9
  232. package/src/use-loader.tsx +57 -30
  233. package/src/vite/discovery/bundle-postprocess.ts +4 -0
  234. package/src/vite/discovery/discover-routers.ts +15 -0
  235. package/src/vite/discovery/shell-prerender-phase.ts +5 -5
  236. package/src/vite/discovery/state.ts +17 -1
  237. package/src/vite/inject-client-debug.ts +42 -20
  238. package/src/vite/plugins/virtual-entries.ts +8 -1
  239. package/src/vite/router-discovery.ts +14 -0
  240. package/src/vite/utils/bundle-analysis.ts +39 -7
  241. package/dist/types/browser/react/subscribe-to-nothing.d.ts +0 -7
  242. package/dist/types/segment-content-promise.d.ts +0 -13
  243. package/src/browser/react/subscribe-to-nothing.ts +0 -7
  244. package/src/segment-content-promise.ts +0 -67
package/src/router.ts CHANGED
@@ -14,6 +14,14 @@ import {
14
14
  isValidDevDiscoveryEpoch,
15
15
  } from "./dev-discovery-protocol.js";
16
16
  import { createReverse, type ReverseFunction } from "./reverse.js";
17
+ import {
18
+ resolvePrerenderVersion,
19
+ resolveRouterVersions,
20
+ } from "./server/build-version-table.js";
21
+ import { isDevEnvironment } from "./errors.js";
22
+ import { isViteDevServer } from "./cache/store-scope.js";
23
+ import { createPrerenderTrigger } from "./prerender/create-prerender-trigger.js";
24
+ import { createMemoryPrerenderStore } from "./prerender/memory-prerender-store.js";
17
25
  import {
18
26
  registerRouteMap,
19
27
  getRouterManifest,
@@ -155,6 +163,7 @@ export function createRouter<TEnv = any>(
155
163
  notFound,
156
164
  onError,
157
165
  cache,
166
+ prerender: prerenderConfigOption,
158
167
  cacheProfiles: cacheProfilesOption,
159
168
  theme: themeOption,
160
169
  urls: urlsOption,
@@ -181,6 +190,16 @@ export function createRouter<TEnv = any>(
181
190
  strictMode: strictModeOption = true,
182
191
  } = options;
183
192
 
193
+ const defaultDevPrerenderStore = prerenderConfigOption
194
+ ? undefined
195
+ : createMemoryPrerenderStore();
196
+ const effectivePrerenderConfigOption =
197
+ prerenderConfigOption ??
198
+ ((() =>
199
+ isViteDevServer() && defaultDevPrerenderStore
200
+ ? { store: defaultDevPrerenderStore }
201
+ : undefined) as RangoOptions<TEnv>["prerender"]);
202
+
184
203
  // Debug cache signal gate (DEVELOPMENT/TEST ONLY). Enabled by the
185
204
  // debugCacheSignal option OR the RANGO_TEST_SIGNALS=1 env flag. When off,
186
205
  // no X-Rango-Cache header is emitted and output is byte-identical.
@@ -744,6 +763,77 @@ export function createRouter<TEnv = any>(
744
763
  );
745
764
  }
746
765
 
766
+ // Shared reverse function (also the value behind router.reverse).
767
+ const reverseFn = createReverse(mergedRouteMap);
768
+ const reverseLoose = reverseFn as unknown as (
769
+ name: string,
770
+ params?: Record<string, string>,
771
+ ) => string;
772
+
773
+ // On-demand prerender binder (router.prerender({ env, ctx }) -> runner with .many / .markStale).
774
+ // Requestless: runProducer runs matchForPrerender with onDemand=true (arming
775
+ // the personalization guard); the store/config resolve per call from {env,ctx}.
776
+ // The key version is the router's data version, resolved by the same call the
777
+ // handler's overlay read makes (resolvePrerenderVersion), so the keys agree.
778
+ const prerenderTrigger = createPrerenderTrigger<TEnv, {}>({
779
+ routerId,
780
+ resolveVersion: () => resolvePrerenderVersion(routerId, version),
781
+ isDev: isDevEnvironment,
782
+ isViteDevServer,
783
+ ensureManifest: () => ensureRouterManifest(routerId),
784
+ resolveConfig: (env, ctx) => {
785
+ const opt = effectivePrerenderConfigOption;
786
+ if (!opt) return undefined;
787
+ return typeof opt === "function" ? opt(env, ctx) : opt;
788
+ },
789
+ reverse: (route, params) => {
790
+ try {
791
+ return reverseLoose(route, params);
792
+ } catch {
793
+ return undefined;
794
+ }
795
+ },
796
+ matchRoute: async (pathname) => {
797
+ const m = await findMatch(pathname);
798
+ if (!m) return null;
799
+ return {
800
+ routeName: m.routeKey,
801
+ params: m.params,
802
+ isOnDemand: m.od === true,
803
+ isPassthrough: m.pt === true,
804
+ };
805
+ },
806
+ runProducer: ({ pathname, isPassthrough, env, dev }) =>
807
+ _matchForPrerender<TEnv>(
808
+ pathname,
809
+ {},
810
+ {
811
+ ...prerenderDeps,
812
+ versions: resolveRouterVersions(routerId, version),
813
+ },
814
+ undefined,
815
+ isPassthrough,
816
+ env,
817
+ dev,
818
+ true,
819
+ ),
820
+ // A warm goes through router.fetch's handler: the one createRouter's own
821
+ // cache, nonce and version configure. An entry that passes them to
822
+ // createRSCHandler itself is not what a warm runs.
823
+ resolveCacheConfig: (env, ctx) =>
824
+ typeof cache === "function" ? cache(env, ctx) : cache,
825
+ // Called from inside a request, the warm leaves that request's scopes:
826
+ // it runs in the context the request entered the handler with
827
+ // (RequestContext._runAtRequestEntry).
828
+ fetch: (request, input) => {
829
+ const atRequestEntry = _getRequestContext()?._runAtRequestEntry;
830
+ return atRequestEntry
831
+ ? atRequestEntry(() => router.fetch(request, input))
832
+ : router.fetch(request, input);
833
+ },
834
+ ambientOrigin: () => _getRequestContext()?.url.origin,
835
+ });
836
+
747
837
  // Create match handler functions bound to router state
748
838
  const matchHandlers = createMatchHandlers<TEnv>({
749
839
  buildRouterContext,
@@ -866,6 +956,7 @@ export function createRouter<TEnv = any>(
866
956
  // Collect route keys that have prerender handlers (for non-trie match path)
867
957
  let prerenderRouteKeys: Set<string> | undefined;
868
958
  let passthroughRouteKeys: Set<string> | undefined;
959
+ let onDemandRouteKeys: Set<string> | undefined;
869
960
  for (const [name, entry] of manifest.entries()) {
870
961
  if (entry.type === "route" && entry.isPrerender) {
871
962
  if (!prerenderRouteKeys) prerenderRouteKeys = new Set();
@@ -874,6 +965,10 @@ export function createRouter<TEnv = any>(
874
965
  if (!passthroughRouteKeys) passthroughRouteKeys = new Set();
875
966
  passthroughRouteKeys.add(name);
876
967
  }
968
+ if (entry.isOnDemand === true) {
969
+ if (!onDemandRouteKeys) onDemandRouteKeys = new Set();
970
+ onDemandRouteKeys.add(name);
971
+ }
877
972
  }
878
973
  }
879
974
 
@@ -900,6 +995,7 @@ export function createRouter<TEnv = any>(
900
995
  cacheProfiles: resolvedCacheProfiles,
901
996
  ...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
902
997
  ...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
998
+ ...(onDemandRouteKeys ? { onDemandRouteKeys } : {}),
903
999
  });
904
1000
  }
905
1001
  } else {
@@ -920,6 +1016,7 @@ export function createRouter<TEnv = any>(
920
1016
  cacheProfiles: resolvedCacheProfiles,
921
1017
  ...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
922
1018
  ...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
1019
+ ...(onDemandRouteKeys ? { onDemandRouteKeys } : {}),
923
1020
  });
924
1021
  }
925
1022
 
@@ -1020,7 +1117,7 @@ export function createRouter<TEnv = any>(
1020
1117
  // Type-safe URL builder using merged route map
1021
1118
  // Types are tracked through the builder chain via TRoutes parameter
1022
1119
  // Seeded with static route names from the generated file (injected by Vite)
1023
- reverse: createReverse(mergedRouteMap),
1120
+ reverse: reverseFn,
1024
1121
 
1025
1122
  // Expose accumulated route map for typeof extraction
1026
1123
  // Returns {} initially, but builder chain accumulates specific route types
@@ -1037,6 +1134,15 @@ export function createRouter<TEnv = any>(
1037
1134
  // Expose cache configuration for RSC handler
1038
1135
  cache,
1039
1136
 
1137
+ // Expose the prerender store config (durable overlay) for the RSC handler to
1138
+ // resolve per request. Stored under _prerenderConfig because `prerender` on
1139
+ // the instance is the trigger method (router.prerender()).
1140
+ _prerenderConfig: effectivePrerenderConfigOption,
1141
+
1142
+ // On-demand prerender trigger: router.prerender({ env, ctx })(target),
1143
+ // plus .many() and .markStale() on the runner.
1144
+ prerender: prerenderTrigger,
1145
+
1040
1146
  // Expose notFound component for RSC handler
1041
1147
  notFound,
1042
1148
 
@@ -15,6 +15,7 @@ import {
15
15
  getRequestContext,
16
16
  _getRequestContext,
17
17
  createRequestContext,
18
+ captureRequestEntryContext,
18
19
  } from "../server/request-context.js";
19
20
  import * as rscDeps from "@vitejs/plugin-rsc/rsc/server";
20
21
  import type {
@@ -46,7 +47,10 @@ import {
46
47
  type ResponseRouteMatch,
47
48
  } from "./response-route-handler.js";
48
49
  import { nonce as nonceToken, resolveProviderNonce } from "./nonce.js";
49
- import { resolveRouterVersions } from "../server/build-version-table.js";
50
+ import {
51
+ resolvePrerenderVersion,
52
+ resolveRouterVersions,
53
+ } from "../server/build-version-table.js";
50
54
  import type { ErrorPhase } from "../types.js";
51
55
  import type { RouterRequestInput } from "../router/router-interfaces.js";
52
56
  import {
@@ -86,6 +90,8 @@ import {
86
90
  } from "./origin-guard.js";
87
91
  import { handleRscRendering } from "./rsc-rendering.js";
88
92
  import { withoutShellMissMarker } from "./shell-serve.js";
93
+ import { readWarmMark } from "../prerender/warm-request.js";
94
+ import { defaultSSRModuleLoaderForTests } from "./ssr-module-loader.js";
89
95
  import {
90
96
  withTimeout,
91
97
  isTimeoutEnabled,
@@ -205,7 +211,13 @@ export function createRSCHandler<
205
211
  // In dev mode Vite may hot-reload the module, so skip memoization.
206
212
  const rawLoadSSRModule: LoadSSRModule =
207
213
  options.loadSSRModule ??
208
- (() => import.meta.viteRsc.loadModule("ssr", "index"));
214
+ (() =>
215
+ // The test seam (ssr-module-loader.ts). Gated on the build mode, not
216
+ // NODE_ENV: the rsc Vitest project forces NODE_ENV=production, its mode
217
+ // is "test", and a production build folds the whole branch away.
218
+ ((import.meta as any).env?.MODE !== "production" &&
219
+ defaultSSRModuleLoaderForTests()?.()) ||
220
+ import.meta.viteRsc.loadModule("ssr", "index"));
209
221
  let _ssrModulePromise: Promise<SSRModule> | undefined;
210
222
  const loadSSRModule: LoadSSRModule =
211
223
  process.env.NODE_ENV === "production"
@@ -452,12 +464,17 @@ export function createRSCHandler<
452
464
  input: RouterRequestInput<TEnv> = {},
453
465
  ): Promise<Response> {
454
466
  const handlerStart = performance.now();
467
+ // Before any router scope: see captureRequestEntryContext.
468
+ const runAtRequestEntry = captureRequestEntryContext();
455
469
  // A degraded PPR HIT's reload carries the forced-MISS marker: keep only
456
470
  // the flag (set on the request context below) and drop the marker before
457
471
  // anything reads the request, so middleware, handlers, loaders, cache
458
472
  // keys and the SSR search seed see the URL the visitor asked for.
459
473
  const unmarkedRequest = withoutShellMissMarker(incomingRequest);
460
474
  const request = unmarkedRequest ?? incomingRequest;
475
+ // A router.prerender() warm: the mark is the incoming Request object
476
+ // itself (prerender/warm-request.ts), read before anything rebuilds it.
477
+ const warm = readWarmMark(incomingRequest);
461
478
  // Create the metrics store at handler start so handler:total has startTime=0
462
479
  // and all metrics are relative to the request entry point.
463
480
  const earlyMetricsStore = router.debugPerformance
@@ -509,10 +526,12 @@ export function createRSCHandler<
509
526
  let searchParamsFilter: SearchParamsFilter | undefined;
510
527
  const cacheOption = options.cache ?? router.cache;
511
528
  if (cacheOption && !url.searchParams.has("__no_cache")) {
529
+ // A warm writes to the store the trigger checked the scope of.
512
530
  const cacheConfig =
513
- typeof cacheOption === "function"
531
+ warm?.cacheConfig ??
532
+ (typeof cacheOption === "function"
514
533
  ? cacheOption(env, executionCtx)
515
- : cacheOption;
534
+ : cacheOption);
516
535
 
517
536
  if (cacheConfig.enabled !== false) {
518
537
  cacheStore = cacheConfig.store;
@@ -522,6 +541,28 @@ export function createRSCHandler<
522
541
  }
523
542
  }
524
543
 
544
+ // Resolve the writable prerender store (durable overlay) for the serve path.
545
+ // Same factory-or-object shape as cache, resolved per request from env/ctx
546
+ // (never memoized). The version resolves per request, as router.prerender()
547
+ // resolves it per refresh, so both sides key off the same table state.
548
+ let resolvedPrerender:
549
+ | import("../prerender/on-demand.js").ResolvedPrerender<any>
550
+ | undefined;
551
+ const prerenderOption = router._prerenderConfig;
552
+ if (prerenderOption) {
553
+ const prerenderConfig =
554
+ typeof prerenderOption === "function"
555
+ ? prerenderOption(env, executionCtx)
556
+ : prerenderOption;
557
+ if (prerenderConfig?.store) {
558
+ resolvedPrerender = {
559
+ config: prerenderConfig,
560
+ routerId: router.id,
561
+ version: resolvePrerenderVersion(router.id, router.version),
562
+ };
563
+ }
564
+ }
565
+
525
566
  // Route manifest is populated at startup via the virtual module
526
567
  // (virtual:rsc-router/routes-manifest). In build/production, it's inlined
527
568
  // into the bundle. In dev mode (Node), the discovery plugin populates it
@@ -576,6 +617,7 @@ export function createRSCHandler<
576
617
  url,
577
618
  variables,
578
619
  cacheStore,
620
+ prerender: resolvedPrerender,
579
621
  searchParamsFilter,
580
622
  explicitTaggedStores,
581
623
  cacheProfiles: router.cacheProfiles,
@@ -585,6 +627,11 @@ export function createRSCHandler<
585
627
  versions,
586
628
  });
587
629
  if (unmarkedRequest) requestContext._shellForcedMiss = true;
630
+ requestContext._runAtRequestEntry = runAtRequestEntry;
631
+ if (warm) {
632
+ requestContext._prerenderWarm = warm;
633
+ requestContext._renderErrors = warm.renderErrors;
634
+ }
588
635
  // Gate on the SAME enabled-semantics withTimeout uses (isTimeoutEnabled):
589
636
  // a `renderStartMs: 0` / negative opt-out disables the timeout, so the
590
637
  // driver's cursor bookkeeping (which only the timeout reads) must be off too.
@@ -438,6 +438,11 @@ function renderPeResponse<TEnv>(
438
438
  strictMode: ctx.router.strictMode,
439
439
  initialTheme: payloadInitialTheme(getRequestContext()),
440
440
  },
441
+ // The same decoded value the SSR render receives, never a second decode:
442
+ // hydrateRoot needs it on the client or useActionState resets. A promise,
443
+ // so a state Flight cannot serialize (a class instance) rejects only this
444
+ // slot instead of failing the payload root and the whole document.
445
+ ...(reactFormState ? { formState: Promise.resolve(reactFormState) } : {}),
441
446
  };
442
447
 
443
448
  const trace = getRequestContext()._activeRoutine;
@@ -470,7 +475,6 @@ function renderPeResponse<TEnv>(
470
475
  {
471
476
  // metricsStore=undefined is safe: the handler already stashed the early
472
477
  // SSR setup promise, so this reuses it instead of starting setup again.
473
- // reactFormState travels through the SSR option, not RscPayload.
474
478
  html: createSsrHtmlStage({
475
479
  ctx,
476
480
  request,
@@ -33,9 +33,11 @@ import {
33
33
  finalizeResponse,
34
34
  } from "./helpers.js";
35
35
  import { reportCacheError } from "../cache/cache-error.js";
36
- import { cacheKeyBase } from "../cache/cache-key-utils.js";
36
+ import { requestKeyBase } from "../cache/cache-key-utils.js";
37
+ import { isWarmReplace, noteWarmWrite } from "../prerender/warm-request.js";
37
38
  import {
38
39
  executionStart,
40
+ markResponseStart,
39
41
  predatesInvalidation,
40
42
  type ExecutionStart,
41
43
  } from "../cache/tag-invalidation.js";
@@ -104,7 +106,7 @@ export async function serveResponseRouteWithCache(
104
106
  // Default key: response:{type}: + host-namespaced base (sorted search, reserved
105
107
  // _rsc*/__* params excluded). Same composition as document/segment tiers so
106
108
  // the host-namespacing and search-normalization rules cannot drift.
107
- let cacheKey = `response:${responseType}:${cacheKeyBase(url.host, url.pathname, url.searchParams, undefined, reqCtx._searchParamsFilter)}`;
109
+ let cacheKey = `response:${responseType}:${requestKeyBase(reqCtx, url.host, url.pathname, url.searchParams, undefined, reqCtx._searchParamsFilter)}`;
108
110
 
109
111
  // Keyed like the segment record (CacheScope.resolveKeyFrom): with key()
110
112
  // results on the cache() chain (a full override, namespaced, #975) or an
@@ -180,15 +182,19 @@ export async function serveResponseRouteWithCache(
180
182
  }
181
183
  await store2.putResponse!(
182
184
  cacheKey,
183
- fresh,
185
+ markResponseStart(fresh, start),
184
186
  cacheScope!.ttl,
185
187
  cacheScope!.swr,
186
188
  responseTags,
187
189
  );
190
+ noteWarmWrite(reqCtx, "response");
188
191
  };
189
192
 
190
193
  try {
191
- const cached = await store.getResponse(cacheKey);
194
+ // A router.prerender() warm takes the miss branch and replaces the entry.
195
+ const cached = isWarmReplace(reqCtx)
196
+ ? null
197
+ : await store.getResponse(cacheKey);
192
198
  if (cached && canServeCached(cached.response)) {
193
199
  if (!cached.shouldRevalidate) {
194
200
  return applyPreHandlerCallbacks(cached.response);
@@ -18,6 +18,7 @@ import {
18
18
  buildShellLoaderSeed,
19
19
  countSnapshotFamilies,
20
20
  hasDocRecord,
21
+ shellPrerenderHandles,
21
22
  } from "../cache/shell-snapshot.js";
22
23
  import { appendMetric } from "../router/metrics.js";
23
24
  import type { MetricsStore } from "../server/context.js";
@@ -93,6 +94,10 @@ import {
93
94
  type DevShellLookup,
94
95
  } from "./shell-build-manifest.js";
95
96
  import { contextGet } from "../context-var.js";
97
+ import {
98
+ isWarmReplace,
99
+ noteWarmShellEvent,
100
+ } from "../prerender/warm-request.js";
96
101
  import {
97
102
  resolveSwrWindow,
98
103
  staleShellRecaptureDue,
@@ -465,6 +470,13 @@ function* shellServePlan<TEnv>(
465
470
  ): RoutinePlan<ShellServeOutcome> {
466
471
  const { ctx, request, env, url, isPartial, handleStore, nonce, reqCtx } =
467
472
  input;
473
+ const manifestEntry = reqCtx._classifiedRoute?.manifestEntry;
474
+
475
+ // A router.prerender() warm (prerender/warm-request.ts) reports the shell of
476
+ // a ppr route: "not-eligible" stands for every gate below that passes, until
477
+ // a HIT or a capture event overwrites it.
478
+ const warm = reqCtx._prerenderWarm;
479
+ if (warm && resolvePprConfig(manifestEntry)) warm.shell ??= "not-eligible";
468
480
 
469
481
  if (
470
482
  isPartial ||
@@ -474,9 +486,11 @@ function* shellServePlan<TEnv>(
474
486
  ) {
475
487
  return { kind: "pass" };
476
488
  }
477
- const manifestEntry = reqCtx._classifiedRoute?.manifestEntry;
478
489
  const pprConfig = resolvePprConfig(manifestEntry);
479
490
  if (!pprConfig) return { kind: "pass" };
491
+ // In replace mode the request is a MISS without reading a shell, and its
492
+ // capture is forced (ShellCaptureDescriptor.force).
493
+ const warmReplace = isWarmReplace(reqCtx);
480
494
  // A degraded HIT's reload (shellReloadScript) renders like a cache miss:
481
495
  // no shell read, no capture, so it can never degrade again. The handler
482
496
  // stripped the marker from the request and left this flag.
@@ -487,7 +501,7 @@ function* shellServePlan<TEnv>(
487
501
  // other visitor. Every step below, a HIT included, therefore runs without
488
502
  // one.
489
503
  const store = reqCtx._cacheStore;
490
- const baseKey = buildShellKey(url, reqCtx._searchParamsFilter);
504
+ const baseKey = buildShellKey(ctx.router.id, url, reqCtx._searchParamsFilter);
491
505
  if (activeRequestNonce(nonce, reqCtx) !== undefined) {
492
506
  // Declared intent that cannot be honored deserves a diagnostic (unlike an
493
507
  // undeclared route, which is silent): a ppr route gated off by an active
@@ -551,12 +565,16 @@ function* shellServePlan<TEnv>(
551
565
  // A MISS schedules its capture after rendering: taken before the read, this
552
566
  // lets that capture skip itself when another request's capture stored the
553
567
  // shell in between (ShellCaptureDescriptor.storedSeqAtRead).
554
- const storedSeqAtRead = lastStoredCaptureSeq(key);
568
+ // A warm's capture is its own: one another request stored meanwhile does
569
+ // not cancel it, so it takes no sequence.
570
+ const storedSeqAtRead = warmReplace ? undefined : lastStoredCaptureSeq(key);
555
571
  // SSR setup starts before route handling, so read the shell before joining it
556
572
  // to overlap the remaining setup work with cache I/O.
557
- const cached = yield* step("shell-read", () =>
558
- readShellEntry(store, key, reqCtx, pprConfig.tags),
559
- );
573
+ const cached = warmReplace
574
+ ? null
575
+ : yield* step("shell-read", () =>
576
+ readShellEntry(store, key, reqCtx, pprConfig.tags),
577
+ );
560
578
 
561
579
  // allReady (ssr.resolveStreaming) bypasses PPR entirely: buffering defeats
562
580
  // streaming, so bots/SEO crawlers get one complete axis-1 document. These
@@ -579,6 +597,15 @@ function* shellServePlan<TEnv>(
579
597
  pprConfig,
580
598
  store,
581
599
  );
600
+ if (warm) {
601
+ // The warm learns its capture's outcome from the capture's own events,
602
+ // ahead of the router's debugShellCapture sink.
603
+ const routerSink = descriptor.debugSink;
604
+ descriptor.debugSink = (event) => {
605
+ noteWarmShellEvent(warm, event);
606
+ routerSink?.(event);
607
+ };
608
+ }
582
609
  // MISS (no entry, invalid reactVersion, a tombstone, or store read
583
610
  // failure): axis 1 + a background capture scheduled once the response is
584
611
  // known servable.
@@ -587,6 +614,10 @@ function* shellServePlan<TEnv>(
587
614
  descriptor: { ...descriptor, storedSeqAtRead },
588
615
  ssrModule,
589
616
  };
617
+ // Neither the stored shell nor the build shell is read: a warm renders.
618
+ if (warmReplace) {
619
+ return { ...miss, descriptor: { ...descriptor, force: true } };
620
+ }
590
621
 
591
622
  // A HIT replays the handler layer from the entry's doc record (`docKey`),
592
623
  // or, for a Prerender route, from the prerender store. An entry that has
@@ -612,6 +643,9 @@ function* shellServePlan<TEnv>(
612
643
  "[ShellServe] getShell",
613
644
  );
614
645
  } else {
646
+ // An onlyIfStale warm found a servable shell; a recapture scheduled
647
+ // below overwrites this through the capture's events.
648
+ if (warm) warm.shell = "fresh";
615
649
  // Stale (SWR) hit: serve the stale shell now, recapture in the
616
650
  // background (stampede-guarded + backoff inside scheduleShellCapture),
617
651
  // at most once per SHELL_MIN_RECAPTURE_INTERVAL_MS of the shell's age.
@@ -680,17 +714,23 @@ function* shellServePlan<TEnv>(
680
714
  // route warned. A search-bearing URL never reads one: nothing to probe.
681
715
  const routeName = reqCtx._classifiedRoute?.routeKey;
682
716
  if (
683
- !partitionBuildShellCheckDone(url.pathname, routeName) &&
717
+ !partitionBuildShellCheckDone(ctx.router.id, url.pathname, routeName) &&
684
718
  shellSearchSeed(url, reqCtx._searchParamsFilter) === ""
685
719
  ) {
686
720
  const found = yield* step("build-shell-check", () =>
687
- hasBuildShell(url.pathname, devShellLookup),
721
+ hasBuildShell(ctx.router.id, url.pathname, devShellLookup),
722
+ );
723
+ notePartitionBuildShellCheck(
724
+ ctx.router.id,
725
+ url.pathname,
726
+ routeName,
727
+ found,
688
728
  );
689
- notePartitionBuildShellCheck(url.pathname, routeName, found);
690
729
  }
691
730
  } else {
692
731
  buildHit = yield* step("build-shell-lookup", () =>
693
732
  lookupBuildShell(
733
+ ctx.router.id,
694
734
  url,
695
735
  ctx.version,
696
736
  store,
@@ -705,6 +745,7 @@ function* shellServePlan<TEnv>(
705
745
  ? openShellDocumentMetered(reqCtx, { entry: buildHit.entry })
706
746
  : null;
707
747
  if (buildHit && buildDocument) {
748
+ if (warm) warm.shell = "fresh";
708
749
  // Past ppr.ttl: still serve the baked entry, recapture upgrades it.
709
750
  if (buildHit.stale) {
710
751
  yield* handoff("shell-recapture", () =>
@@ -1398,7 +1439,7 @@ async function matchPartialWithPprReplay<TEnv>(
1398
1439
  // The visitor's own partition (resolveShellPartition): replay never
1399
1440
  // crosses into another partition's shell.
1400
1441
  const key = await resolveRequestShellKey(
1401
- buildShellKey(url, reqCtx._searchParamsFilter),
1442
+ buildShellKey(ctx.router.id, url, reqCtx._searchParamsFilter),
1402
1443
  routeSnapshot,
1403
1444
  store,
1404
1445
  reqCtx,
@@ -1896,7 +1937,7 @@ function serveShellHit(
1896
1937
  // fresh; a promise-free bake-lane loader is served from its pin
1897
1938
  // (_shellLoaderSeed, loader-cache.ts). A Prerender route's tail takes
1898
1939
  // the handler layer from the prerender store before this scope is
1899
- // consulted.
1940
+ // consulted, and the loader pushes from `prerenderHandles`.
1900
1941
  seededCtx._shellImplicitCache = {
1901
1942
  ttl: descriptor.ttl,
1902
1943
  swr: descriptor.swr,
@@ -1904,6 +1945,7 @@ function serveShellHit(
1904
1945
  keyPrefix: "doc",
1905
1946
  docTail: true,
1906
1947
  fixedDocKey: entry.docKey,
1948
+ prerenderHandles: shellPrerenderHandles(records),
1907
1949
  };
1908
1950
  // Fragment splice (issue #700): store hits in THIS tail emit their stored
1909
1951
  // segment fragments verbatim (expanded by segment-fragments.ts). The flag
@@ -55,7 +55,7 @@ export interface BuildShellEntry {
55
55
  }
56
56
 
57
57
  interface ShellManifestModule {
58
- /** Manifest key (pathname — see shell-manifest-key.ts) -> asset specifier. */
58
+ /** Manifest key (router id + pathname, shell-manifest-key.ts) -> asset specifier. */
59
59
  default: Record<string, string>;
60
60
  loadShellAsset: (spec: string) => Promise<{ default: BuildShellEntry }>;
61
61
  }
@@ -92,9 +92,7 @@ function loadManifest(): Promise<ShellManifestModule | null> {
92
92
  * the document HIT decodes the prelude once per serve (openShellDocument),
93
93
  * partial replay never does. `null` memoizes a failed verdict —
94
94
  * deterministically invalid. Keyed by the asking router's document version as
95
- * well as the spec: the manifest key is pathname-only (shell-manifest-key.ts),
96
- * so under a host router two routers with different versions can ask about one
97
- * record.
95
+ * well as the spec: the verdict is that version's.
98
96
  */
99
97
  const validatedSpecs = new Map<string, ValidatedBuildShellEntry | null>();
100
98
 
@@ -270,9 +268,11 @@ async function fetchDevShellEntry(
270
268
  * request is search-less, like the entries lookupBuildShell serves): a
271
269
  * manifest entry for the path in production, a Prerender route in dev. No
272
270
  * asset load and no dev capture: it only decides whether a request that
273
- * cannot read the build shell deserves a warning.
271
+ * cannot read the build shell deserves a warning. The manifest entry is the
272
+ * one staged for the router `routerId` (shell-manifest-key.ts).
274
273
  */
275
274
  export async function hasBuildShell(
275
+ routerId: string,
276
276
  pathname: string,
277
277
  dev?: DevShellLookup,
278
278
  ): Promise<boolean> {
@@ -280,11 +280,12 @@ export async function hasBuildShell(
280
280
  return dev?.isPrerenderRoute === true;
281
281
  }
282
282
  const mod = await loadManifest();
283
- return mod?.default[buildShellManifestKey(pathname)] !== undefined;
283
+ return mod?.default[buildShellManifestKey(routerId, pathname)] !== undefined;
284
284
  }
285
285
 
286
286
  /**
287
287
  * Look up the baked shell entry for a request, applying every serve gate:
288
+ * the serving router's own entry only (`routerId`, shell-manifest-key.ts),
288
289
  * search-less requests only (the build captured the bare pathname; a
289
290
  * search-bearing URL has its own shell identity owned by runtime capture),
290
291
  * version validity, payload integrity, and tag-invalidation markers. Returns
@@ -297,6 +298,7 @@ export async function hasBuildShell(
297
298
  * traffic the shell was prerendered for.
298
299
  */
299
300
  export async function lookupBuildShell(
301
+ routerId: string,
300
302
  url: URL,
301
303
  buildVersion: string,
302
304
  store: SegmentCacheStore,
@@ -314,7 +316,7 @@ export async function lookupBuildShell(
314
316
  if (hasManifest) {
315
317
  const mod = await loadManifest();
316
318
  if (!mod) return null;
317
- const spec = mod.default[buildShellManifestKey(url.pathname)];
319
+ const spec = mod.default[buildShellManifestKey(routerId, url.pathname)];
318
320
  if (!spec) return null;
319
321
  record = await validatedManifestRecord(mod, spec, buildVersion);
320
322
  } else if (dev) {
@@ -341,7 +343,7 @@ export async function lookupBuildShell(
341
343
  // diagnostic; the route keeps runtime-capture semantics.
342
344
  warnOnce(
343
345
  "build-shell-tag-check",
344
- buildShellManifestKey(url.pathname),
346
+ buildShellManifestKey(routerId, url.pathname),
345
347
  () =>
346
348
  `[rango] Build-time shell for "${url.pathname}" carries cache tags, but ` +
347
349
  "the app cache store cannot answer tag-invalidation history durably " +
@@ -6,7 +6,10 @@
6
6
  * pulling the capture orchestration module or shell-serve.ts (React).
7
7
  */
8
8
 
9
- import { sortedSearchString } from "../cache/cache-key-utils.js";
9
+ import {
10
+ routerKeyPrefix,
11
+ sortedSearchString,
12
+ } from "../cache/cache-key-utils.js";
10
13
  import type { SearchParamsFilter } from "../cache/search-params-filter.js";
11
14
 
12
15
  /**
@@ -85,13 +88,14 @@ export function shellSearchSeed(url: URL, filter?: SearchParamsFilter): string {
85
88
  }
86
89
 
87
90
  /**
88
- * Shell cache key: host + pathname + sorted search + a `:shell` namespace suffix
89
- * (so it can never collide with a document-cache key; the store further isolates
90
- * the shell family internally).
91
+ * Shell cache key: router id + host + pathname + sorted search + a `:shell`
92
+ * namespace suffix (so it can never collide with a document-cache key; the
93
+ * store further isolates the shell family internally).
91
94
  *
92
95
  * The key includes the request HOST: in a multi-tenant host-router deployment
93
96
  * (one worker, one shared KV/runtime-cache store) a host-less key would serve
94
- * tenant A's captured shell to tenant B's users.
97
+ * tenant A's captured shell to tenant B's users. It starts with the serving
98
+ * ROUTER for the same reason (cache-key-utils.ts, the router rule).
95
99
  *
96
100
  * `filter` is the request's compiled `cache.searchParams` config
97
101
  * (ctx._searchParamsFilter): excluded params collapse onto one shell slot.
@@ -99,8 +103,12 @@ export function shellSearchSeed(url: URL, filter?: SearchParamsFilter): string {
99
103
  * and lookup makes every shell request a permanent miss. The testing helper
100
104
  * `shellCacheKey` (testing/shell-status.ts) builds its key with it too.
101
105
  */
102
- export function buildShellKey(url: URL, filter?: SearchParamsFilter): string {
103
- return `${url.host}${url.pathname}${shellSearchSeed(url, filter)}:shell`;
106
+ export function buildShellKey(
107
+ routerId: string,
108
+ url: URL,
109
+ filter?: SearchParamsFilter,
110
+ ): string {
111
+ return `${routerKeyPrefix(routerId)}${url.host}${url.pathname}${shellSearchSeed(url, filter)}:shell`;
104
112
  }
105
113
 
106
114
  /**
@@ -173,6 +181,30 @@ export const POST_QUIESCE_TASK_HOPS = 16;
173
181
  */
174
182
  export const SHELL_CAPTURE_TASK_HARD_CAP_MS = 25_000;
175
183
 
184
+ /**
185
+ * Why a capture attempt refused to store its shell
186
+ * (ShellCaptureDebugEvent.refusal, `caches.refusal` on a `router.prerender()`
187
+ * result): `identity` (a request-scoped read: cookies(), headers(), the
188
+ * theme, a `{ cache: false }` variable), `dynamic` (`ctx.dynamic()`),
189
+ * `loader` (a `{ ssr: false }` loader rejected, or settled with
190
+ * redirect()/notFound()), `no-record` (no doc record: the route's cache()
191
+ * refused the write, or the handler output failed), `handles` (a pushed
192
+ * handle value did not finish encoding), `size` (over the store's value
193
+ * limit), `record-expired` (the route cache() record the capture wrote ran
194
+ * out first), `invalidated` (a tag of the shell was invalidated after the
195
+ * capture started), `uncacheable` (the store cannot hold the entry).
196
+ */
197
+ export type ShellCaptureRefusal =
198
+ | "identity"
199
+ | "dynamic"
200
+ | "loader"
201
+ | "no-record"
202
+ | "handles"
203
+ | "size"
204
+ | "record-expired"
205
+ | "invalidated"
206
+ | "uncacheable";
207
+
176
208
  /**
177
209
  * Shared PPR loader-lane hint, appended to every capture warning or error
178
210
  * whose fix depends on the lane (shell-capture.ts warnings, the cookie-store.ts