@rangojs/router 0.20.0 → 0.22.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 (331) hide show
  1. package/dist/bin/rango.js +39 -15
  2. package/dist/types/browser/event-controller.d.ts +49 -0
  3. package/dist/types/browser/history-state.d.ts +16 -4
  4. package/dist/types/browser/navigation-store.d.ts +9 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +7 -0
  6. package/dist/types/browser/partial-update.d.ts +2 -1
  7. package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
  8. package/dist/types/browser/rango-state.d.ts +3 -1
  9. package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
  10. package/dist/types/browser/react/context.d.ts +25 -0
  11. package/dist/types/browser/react/location-state-shared.d.ts +84 -10
  12. package/dist/types/browser/react/location-state.d.ts +7 -2
  13. package/dist/types/browser/rsc-router.d.ts +13 -2
  14. package/dist/types/browser/types.d.ts +10 -0
  15. package/dist/types/build/generate-manifest.d.ts +2 -0
  16. package/dist/types/cache/background-task.d.ts +22 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +50 -5
  18. package/dist/types/cache/cache-policy.d.ts +17 -0
  19. package/dist/types/cache/cache-scope.d.ts +5 -6
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +34 -7
  21. package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
  22. package/dist/types/cache/document-cache.d.ts +8 -5
  23. package/dist/types/cache/handle-snapshot.d.ts +59 -50
  24. package/dist/types/cache/index.d.ts +3 -1
  25. package/dist/types/cache/invalidation-order.d.ts +14 -0
  26. package/dist/types/cache/memory-segment-store.d.ts +33 -1
  27. package/dist/types/cache/shell-snapshot.d.ts +29 -4
  28. package/dist/types/cache/store-scope.d.ts +22 -0
  29. package/dist/types/cache/tag-invalidation.d.ts +7 -6
  30. package/dist/types/cache/types.d.ts +84 -24
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +35 -17
  32. package/dist/types/client-urls/navigation.d.ts +7 -1
  33. package/dist/types/client-urls/optimistic-location.d.ts +10 -4
  34. package/dist/types/errors.d.ts +6 -0
  35. package/dist/types/handles/deferred-resolution.d.ts +18 -1
  36. package/dist/types/internal-suspense-audit.d.ts +1 -0
  37. package/dist/types/loader-store.d.ts +4 -8
  38. package/dist/types/outlet-provider.d.ts +2 -1
  39. package/dist/types/prerender/build-shell-capture.d.ts +8 -6
  40. package/dist/types/prerender/cloudflare.d.ts +49 -0
  41. package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
  42. package/dist/types/prerender/index.d.ts +12 -0
  43. package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
  44. package/dist/types/prerender/on-demand.d.ts +272 -0
  45. package/dist/types/prerender/producer-guard.d.ts +33 -0
  46. package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
  47. package/dist/types/prerender/warm-request.d.ts +65 -0
  48. package/dist/types/prerender/warm.d.ts +65 -0
  49. package/dist/types/prerender/writable-store.d.ts +170 -0
  50. package/dist/types/prerender.d.ts +27 -0
  51. package/dist/types/reverse.d.ts +1 -1
  52. package/dist/types/route-content-wrapper.d.ts +3 -1
  53. package/dist/types/router/error-handling.d.ts +2 -0
  54. package/dist/types/router/handler-context.d.ts +1 -1
  55. package/dist/types/router/match-middleware/cache-lookup.d.ts +15 -1
  56. package/dist/types/router/navigation-snapshot.d.ts +8 -1
  57. package/dist/types/router/pattern-matching.d.ts +2 -0
  58. package/dist/types/router/prefetch-default.d.ts +4 -4
  59. package/dist/types/router/prerender-match.d.ts +23 -1
  60. package/dist/types/router/route-trie-builder.d.ts +8 -1
  61. package/dist/types/router/router-context.d.ts +1 -0
  62. package/dist/types/router/router-interfaces.d.ts +32 -1
  63. package/dist/types/router/router-options.d.ts +39 -5
  64. package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
  65. package/dist/types/router/segment-resolution/loader-cache.d.ts +23 -10
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
  67. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
  68. package/dist/types/router/trie-matching.d.ts +2 -0
  69. package/dist/types/router-versions.d.ts +39 -0
  70. package/dist/types/rsc/handler-context.d.ts +5 -0
  71. package/dist/types/rsc/shell-build-manifest.d.ts +9 -6
  72. package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
  73. package/dist/types/rsc/shell-capture.d.ts +14 -1
  74. package/dist/types/rsc/shell-serve.d.ts +2 -2
  75. package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
  76. package/dist/types/rsc/types.d.ts +12 -6
  77. package/dist/types/segment-boundary-content.d.ts +17 -0
  78. package/dist/types/segment-loader-promise.d.ts +2 -6
  79. package/dist/types/server/build-version-table.d.ts +53 -0
  80. package/dist/types/server/context.d.ts +12 -4
  81. package/dist/types/server/handle-store.d.ts +89 -46
  82. package/dist/types/server/request-context.d.ts +84 -9
  83. package/dist/types/suspense-audit-react.d.ts +26 -0
  84. package/dist/types/suspense-audit.d.ts +157 -0
  85. package/dist/types/testing/action-form.d.ts +34 -0
  86. package/dist/types/testing/build-versions.d.ts +32 -0
  87. package/dist/types/testing/dom.entry.d.ts +1 -1
  88. package/dist/types/testing/flight.entry.d.ts +3 -1
  89. package/dist/types/testing/index.d.ts +6 -0
  90. package/dist/types/testing/internal/seed-vars.d.ts +7 -2
  91. package/dist/types/testing/internal/server-modules.d.ts +8 -0
  92. package/dist/types/testing/location-state-key.d.ts +5 -1
  93. package/dist/types/testing/render-route.d.ts +77 -6
  94. package/dist/types/testing/serve-shell-request.d.ts +55 -4
  95. package/dist/types/testing/shell-status.d.ts +14 -6
  96. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
  97. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  98. package/dist/types/thenable-status.d.ts +6 -0
  99. package/dist/types/types/handler-context.d.ts +15 -5
  100. package/dist/types/types/route-entry.d.ts +5 -0
  101. package/dist/types/urls/path-helper.d.ts +18 -0
  102. package/dist/types/vite/discovery/build-versions.d.ts +393 -0
  103. package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
  104. package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
  105. package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
  106. package/dist/types/vite/discovery/state.d.ts +25 -0
  107. package/dist/types/vite/encryption-key.d.ts +18 -0
  108. package/dist/types/vite/inject-client-debug.d.ts +6 -25
  109. package/dist/types/vite/plugin-types.d.ts +27 -0
  110. package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
  111. package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
  112. package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
  113. package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
  114. package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
  115. package/dist/types/vite/plugins/virtual-entries.d.ts +18 -1
  116. package/dist/types/vite/router-discovery.d.ts +6 -0
  117. package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
  118. package/dist/types/vite/utils/client-chunks.d.ts +2 -0
  119. package/dist/types/vite/utils/compare-strings.d.ts +7 -0
  120. package/dist/types/vite/utils/package-resolution.d.ts +8 -0
  121. package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
  122. package/dist/vite/index.js +1707 -931
  123. package/package.json +12 -3
  124. package/skills/cache-guide/SKILL.md +102 -37
  125. package/skills/caching/SKILL.md +56 -9
  126. package/skills/catalog.json +1 -1
  127. package/skills/client-urls/SKILL.md +16 -0
  128. package/skills/cloudflare/SKILL.md +133 -1
  129. package/skills/comparison/references/framework-comparison.md +17 -13
  130. package/skills/deployment-caching/SKILL.md +33 -12
  131. package/skills/document-cache/SKILL.md +16 -14
  132. package/skills/hooks/SKILL.md +25 -0
  133. package/skills/hooks/handle-and-actions.md +5 -0
  134. package/skills/hooks/navigation.md +38 -0
  135. package/skills/hooks/state.md +213 -11
  136. package/skills/hooks/urls.md +40 -1
  137. package/skills/host-router/SKILL.md +31 -0
  138. package/skills/intercept/SKILL.md +6 -0
  139. package/skills/loader/SKILL.md +25 -5
  140. package/skills/middleware/SKILL.md +2 -0
  141. package/skills/migrate-nextjs/SKILL.md +82 -9
  142. package/skills/ppr/SKILL.md +150 -48
  143. package/skills/prerender/SKILL.md +651 -10
  144. package/skills/rango/SKILL.md +9 -7
  145. package/skills/response-routes/SKILL.md +5 -5
  146. package/skills/route/SKILL.md +29 -0
  147. package/skills/router-setup/SKILL.md +15 -1
  148. package/skills/testing/SKILL.md +32 -27
  149. package/skills/testing/cache-prerender.md +18 -17
  150. package/skills/testing/client-components.md +193 -8
  151. package/skills/testing/handles.md +2 -0
  152. package/skills/testing/loader.md +1 -1
  153. package/skills/testing/middleware.md +1 -1
  154. package/skills/testing/render-handler.md +18 -18
  155. package/skills/testing/serve-shell-request.md +167 -22
  156. package/skills/testing/server-actions.md +17 -17
  157. package/skills/testing/server-tree.md +1 -1
  158. package/skills/typesafety/generated-files-and-cli.md +1 -1
  159. package/skills/use-cache/SKILL.md +25 -10
  160. package/skills/vercel/SKILL.md +5 -3
  161. package/skills/view-transitions/SKILL.md +9 -4
  162. package/src/bin/rango.ts +1 -1
  163. package/src/browser/event-controller.ts +119 -2
  164. package/src/browser/history-state.ts +39 -17
  165. package/src/browser/link-interceptor.ts +2 -2
  166. package/src/browser/navigation-bridge.ts +104 -41
  167. package/src/browser/navigation-store.ts +26 -0
  168. package/src/browser/navigation-transaction.ts +24 -12
  169. package/src/browser/network-error-handler.ts +2 -0
  170. package/src/browser/partial-update.ts +58 -14
  171. package/src/browser/prefetch/default-strategy.ts +22 -42
  172. package/src/browser/rango-state.ts +3 -1
  173. package/src/browser/react/Link.tsx +37 -43
  174. package/src/browser/react/NavigationProvider.tsx +85 -1
  175. package/src/browser/react/context.ts +30 -0
  176. package/src/browser/react/location-state-shared.ts +200 -34
  177. package/src/browser/react/location-state.ts +28 -77
  178. package/src/browser/react/use-action.ts +4 -1
  179. package/src/browser/react/use-handle.ts +3 -0
  180. package/src/browser/react/use-link-status.ts +4 -3
  181. package/src/browser/react/use-navigation.ts +4 -1
  182. package/src/browser/react/use-params.ts +4 -1
  183. package/src/browser/react/use-pathname.ts +4 -1
  184. package/src/browser/react/use-search-params.ts +5 -3
  185. package/src/browser/react/use-segments.ts +36 -54
  186. package/src/browser/rsc-router.tsx +93 -31
  187. package/src/browser/segment-reconciler.ts +3 -3
  188. package/src/browser/server-action-bridge.ts +43 -7
  189. package/src/browser/types.ts +11 -0
  190. package/src/build/generate-manifest.ts +17 -0
  191. package/src/build/merge-full-manifests.ts +5 -0
  192. package/src/build/route-trie.ts +1 -0
  193. package/src/cache/background-task.ts +61 -0
  194. package/src/cache/cache-key-utils.ts +91 -12
  195. package/src/cache/cache-policy.ts +11 -2
  196. package/src/cache/cache-runtime.ts +23 -23
  197. package/src/cache/cache-scope.ts +48 -36
  198. package/src/cache/cf/cf-cache-store.ts +91 -66
  199. package/src/cache/cf/cf-cache-types.ts +12 -4
  200. package/src/cache/document-cache.ts +35 -14
  201. package/src/cache/handle-snapshot.ts +118 -86
  202. package/src/cache/index.ts +5 -0
  203. package/src/cache/invalidation-order.ts +53 -0
  204. package/src/cache/memory-segment-store.ts +52 -1
  205. package/src/cache/shell-snapshot.ts +52 -5
  206. package/src/cache/store-scope.ts +36 -0
  207. package/src/cache/tag-invalidation.ts +7 -5
  208. package/src/cache/types.ts +90 -24
  209. package/src/cache/vercel/vercel-cache-store.ts +68 -26
  210. package/src/client-urls/client-root.tsx +1 -0
  211. package/src/client-urls/navigation.ts +10 -0
  212. package/src/client-urls/optimistic-location.ts +10 -4
  213. package/src/client.tsx +40 -3
  214. package/src/errors.ts +13 -6
  215. package/src/handles/deferred-resolution.ts +36 -0
  216. package/src/internal-suspense-audit.ts +12 -0
  217. package/src/loader-store.ts +4 -9
  218. package/src/outlet-provider.tsx +25 -1
  219. package/src/prerender/build-shell-capture.ts +11 -9
  220. package/src/prerender/cloudflare.ts +130 -0
  221. package/src/prerender/create-prerender-trigger.ts +780 -0
  222. package/src/prerender/index.ts +39 -0
  223. package/src/prerender/memory-prerender-store.ts +115 -0
  224. package/src/prerender/on-demand.ts +339 -0
  225. package/src/prerender/producer-guard.ts +66 -0
  226. package/src/prerender/shell-manifest-key.ts +12 -17
  227. package/src/prerender/warm-request.ts +154 -0
  228. package/src/prerender/warm.ts +193 -0
  229. package/src/prerender/writable-store.ts +278 -0
  230. package/src/prerender.ts +29 -0
  231. package/src/reverse.ts +1 -1
  232. package/src/route-content-wrapper.tsx +107 -13
  233. package/src/router/error-handling.ts +12 -12
  234. package/src/router/find-match.ts +1 -0
  235. package/src/router/handler-context.ts +2 -0
  236. package/src/router/intercept-resolution.ts +15 -8
  237. package/src/router/loader-resolution.ts +2 -10
  238. package/src/router/manifest.ts +2 -0
  239. package/src/router/match-middleware/background-revalidation.ts +2 -0
  240. package/src/router/match-middleware/cache-lookup.ts +239 -54
  241. package/src/router/navigation-snapshot.ts +25 -6
  242. package/src/router/pattern-matching.ts +14 -0
  243. package/src/router/prefetch-default.ts +4 -4
  244. package/src/router/prerender-match.ts +460 -352
  245. package/src/router/request-classification.ts +15 -12
  246. package/src/router/route-trie-builder.ts +9 -0
  247. package/src/router/router-context.ts +1 -0
  248. package/src/router/router-interfaces.ts +35 -1
  249. package/src/router/router-options.ts +45 -5
  250. package/src/router/segment-resolution/fresh.ts +20 -19
  251. package/src/router/segment-resolution/loader-cache.ts +110 -77
  252. package/src/router/segment-resolution/revalidation.ts +34 -24
  253. package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
  254. package/src/router/trie-matching.ts +3 -0
  255. package/src/router-versions.ts +60 -0
  256. package/src/router.ts +107 -1
  257. package/src/rsc/full-payload.ts +6 -2
  258. package/src/rsc/handler-context.ts +5 -0
  259. package/src/rsc/handler.ts +64 -7
  260. package/src/rsc/progressive-enhancement.ts +5 -1
  261. package/src/rsc/response-cache-serve.ts +10 -4
  262. package/src/rsc/rsc-rendering.ts +65 -12
  263. package/src/rsc/shell-build-manifest.ts +17 -12
  264. package/src/rsc/shell-capture-constants.ts +39 -7
  265. package/src/rsc/shell-capture.ts +144 -45
  266. package/src/rsc/shell-serve.ts +12 -6
  267. package/src/rsc/ssr-module-loader.ts +26 -0
  268. package/src/rsc/types.ts +12 -6
  269. package/src/segment-boundary-content.ts +25 -0
  270. package/src/segment-loader-promise.ts +28 -38
  271. package/src/segment-system.tsx +117 -57
  272. package/src/server/build-version-table.ts +130 -0
  273. package/src/server/context.ts +40 -17
  274. package/src/server/cookie-store.ts +5 -0
  275. package/src/server/handle-store.ts +357 -127
  276. package/src/server/request-context.ts +149 -12
  277. package/src/ssr/ssr-root.tsx +32 -13
  278. package/src/suspense-audit-react.tsx +98 -0
  279. package/src/suspense-audit.ts +1030 -0
  280. package/src/testing/action-form.ts +66 -0
  281. package/src/testing/build-versions.ts +64 -0
  282. package/src/testing/dispatch.ts +6 -0
  283. package/src/testing/dom.entry.ts +1 -0
  284. package/src/testing/flight-runtime.d.ts +27 -0
  285. package/src/testing/flight.entry.ts +4 -0
  286. package/src/testing/index.ts +22 -0
  287. package/src/testing/internal/context.ts +2 -1
  288. package/src/testing/internal/seed-vars.ts +14 -2
  289. package/src/testing/internal/server-modules.ts +32 -0
  290. package/src/testing/location-state-key.ts +18 -8
  291. package/src/testing/render-handler.ts +2 -1
  292. package/src/testing/render-route.tsx +293 -46
  293. package/src/testing/serve-shell-request.ts +262 -37
  294. package/src/testing/shell-status.ts +17 -6
  295. package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
  296. package/src/testing/vitest-stubs/version.ts +4 -1
  297. package/src/thenable-status.ts +10 -0
  298. package/src/types/handler-context.ts +15 -5
  299. package/src/types/route-entry.ts +6 -0
  300. package/src/urls/path-helper.ts +69 -9
  301. package/src/use-loader.tsx +57 -30
  302. package/src/vite/discovery/build-versions.ts +1281 -0
  303. package/src/vite/discovery/bundle-postprocess.ts +4 -0
  304. package/src/vite/discovery/discover-routers.ts +15 -0
  305. package/src/vite/discovery/installed-packages.ts +159 -0
  306. package/src/vite/discovery/prerender-collection.ts +38 -1
  307. package/src/vite/discovery/router-versions-phase.ts +336 -0
  308. package/src/vite/discovery/shell-prerender-phase.ts +33 -43
  309. package/src/vite/discovery/state.ts +36 -1
  310. package/src/vite/discovery/virtual-module-codegen.ts +7 -2
  311. package/src/vite/encryption-key.ts +101 -9
  312. package/src/vite/inject-client-debug.ts +42 -20
  313. package/src/vite/plugin-types.ts +28 -0
  314. package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
  315. package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
  316. package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
  317. package/src/vite/plugins/expose-internal-ids.ts +12 -3
  318. package/src/vite/plugins/stable-rsc-output.ts +147 -0
  319. package/src/vite/plugins/version-plugin.ts +19 -24
  320. package/src/vite/plugins/version.d.ts +17 -2
  321. package/src/vite/plugins/virtual-entries.ts +39 -6
  322. package/src/vite/rango.ts +28 -3
  323. package/src/vite/router-discovery.ts +96 -13
  324. package/src/vite/utils/bundle-analysis.ts +39 -7
  325. package/src/vite/utils/client-chunks.ts +5 -1
  326. package/src/vite/utils/compare-strings.ts +9 -0
  327. package/src/vite/utils/package-resolution.ts +9 -0
  328. package/src/vite/utils/prerender-utils.ts +4 -1
  329. package/src/vite/utils/shared-utils.ts +1 -1
  330. package/dist/types/segment-content-promise.d.ts +0 -13
  331. package/src/segment-content-promise.ts +0 -67
@@ -41,7 +41,9 @@ import {
41
41
  SHELL_CAPTURE_TASK_HARD_CAP_MS,
42
42
  resetShellWarningsForTests,
43
43
  warnOnce,
44
+ type ShellCaptureRefusal,
44
45
  } from "./shell-capture-constants.js";
46
+ import { noteWarmWrite } from "../prerender/warm-request.js";
45
47
  import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
46
48
  import { observePhase, PHASES } from "../router/instrument.js";
47
49
  import type { TraceSpan } from "../router/tracing.js";
@@ -75,6 +77,7 @@ import type {
75
77
  CachedEntryData,
76
78
  ShellCacheEntry,
77
79
  SegmentCacheStore,
80
+ ShellSnapshotHandlesValue,
78
81
  ShellSnapshotRecord,
79
82
  } from "../cache/types.js";
80
83
  import {
@@ -83,6 +86,7 @@ import {
83
86
  } from "../router/segment-resolution/loader-snapshot.js";
84
87
  import {
85
88
  RecordingShellStore,
89
+ SHELL_HANDLES_RECORD_KEY,
86
90
  SnapshotOnlySegmentStore,
87
91
  countSnapshotFamilies,
88
92
  estimateShellEntryBytes,
@@ -90,10 +94,17 @@ import {
90
94
  hasDocRecord,
91
95
  pruneShellSnapshot,
92
96
  } from "../cache/shell-snapshot.js";
97
+ import {
98
+ captureOwnedHandles,
99
+ encodeHandles,
100
+ } from "../cache/handle-snapshot.js";
93
101
  import type { HandlerContext } from "./handler-context.js";
94
102
  import type { SSRModule } from "./types.js";
95
103
  import { buildFullPayload, payloadInitialTheme } from "./full-payload.js";
96
- import { resolveDeferredHandleValues } from "../handles/deferred-resolution.js";
104
+ import {
105
+ recordedHandleData,
106
+ resolveDeferredHandleValues,
107
+ } from "../handles/deferred-resolution.js";
97
108
  import { renderRscFlightStage } from "./render-pipeline.js";
98
109
  import { stripInternalParams } from "../router/handler-context.js";
99
110
 
@@ -482,6 +493,20 @@ function refuseOnCaptureGuard(
482
493
  return true;
483
494
  }
484
495
 
496
+ /** The refusal a tripped capture guard stands for (refuseOnCaptureGuard). */
497
+ function guardRefusal(derivedCtx: RequestContext<any>): ShellCaptureRefusal {
498
+ return derivedCtx._dynamic ? "dynamic" : "identity";
499
+ }
500
+
501
+ /** A refused attempt; the reason rides its event (ShellCaptureDebugEvent.refusal). */
502
+ function refuse(
503
+ stats: Pick<ShellCaptureDebugEvent, "refusal">,
504
+ refusal: ShellCaptureRefusal,
505
+ ): "refused" {
506
+ stats.refusal = refusal;
507
+ return "refused";
508
+ }
509
+
485
510
  /** The no-shell cause for handler output that missed the capture deadline. */
486
511
  const HANDLER_OUTPUT_TIMEOUT_REASON: string =
487
512
  "the handler output (a promise it passes or pushes, or an async server " +
@@ -658,6 +683,8 @@ export interface ShellCaptureDebugEvent {
658
683
  untaggedBake?: true;
659
684
  /** Outcome reported by a store that supports shell-write acknowledgements. */
660
685
  storeWrite?: "stored" | "invalidated" | "uncacheable";
686
+ /** Why a `refused` attempt refused. */
687
+ refusal?: ShellCaptureRefusal;
661
688
  /** Consecutive failure count in the key's backoff entry, when one exists. */
662
689
  backoffFailures?: number;
663
690
  /** Ms remaining in the key's backoff window, when one exists. */
@@ -719,6 +746,7 @@ export function describeShellCaptureEvent(
719
746
  if (event.storeWrite !== undefined) {
720
747
  parts.push(`store-write=${event.storeWrite}`);
721
748
  }
749
+ if (event.refusal !== undefined) parts.push(`refusal=${event.refusal}`);
722
750
  if (event.backoffFailures !== undefined) {
723
751
  parts.push(`backoff-failures=${event.backoffFailures}`);
724
752
  }
@@ -1042,6 +1070,15 @@ export interface ShellCaptureDescriptor {
1042
1070
  * re-render the page and drop the isolate's memo of the shell just stored.
1043
1071
  */
1044
1072
  storedSeqAtRead?: number;
1073
+ /**
1074
+ * A `router.prerender()` warm's capture (rsc-rendering.ts shellServePlan):
1075
+ * one explicit call that must store, so the guards that shed a herd of
1076
+ * visitors do not apply to it. scheduleShellCapture skips the in-flight
1077
+ * dedup and the refused-capture backoff, and attemptCapture waits longer on
1078
+ * the write barrier. Every correctness guard, the inert-store skip and the
1079
+ * queue limits still decide.
1080
+ */
1081
+ force?: true;
1045
1082
  }
1046
1083
 
1047
1084
  /** Per key, the sequence number of its latest stored capture (bounded). */
@@ -1086,7 +1123,10 @@ export function scheduleShellCapture(
1086
1123
  descriptor: ShellCaptureDescriptor,
1087
1124
  ): void {
1088
1125
  const key = descriptor.key;
1089
- const inFlight = inFlightCaptures.get(key);
1126
+ // A forced capture installs its own token below: a visitor's capture that
1127
+ // started before the warm may have read data from before the change the
1128
+ // warm publishes, and the queue runs it first, so the warm stores last.
1129
+ const inFlight = descriptor.force ? undefined : inFlightCaptures.get(key);
1090
1130
  if (inFlight) {
1091
1131
  if (Date.now() - inFlight.startedAt <= SHELL_CAPTURE_TASK_HARD_CAP_MS) {
1092
1132
  publishCaptureDebugEvent(descriptor, { key, outcome: "skip-in-flight" });
@@ -1107,7 +1147,7 @@ export function scheduleShellCapture(
1107
1147
  }
1108
1148
  // Refused/failed within the window → skip the doomed re-render (one probe per
1109
1149
  // key per window per isolate). Expired entries self-evict inside the check.
1110
- if (isCaptureBackedOff(key)) {
1150
+ if (!descriptor.force && isCaptureBackedOff(key)) {
1111
1151
  publishCaptureDebugEvent(descriptor, {
1112
1152
  key,
1113
1153
  outcome: "skip-backoff",
@@ -1436,6 +1476,7 @@ type CaptureAttemptStats = Pick<
1436
1476
  | "snapshotSkipped"
1437
1477
  | "untaggedBake"
1438
1478
  | "storeWrite"
1479
+ | "refusal"
1439
1480
  | "recordSettleMs"
1440
1481
  | "entryBytes"
1441
1482
  > & {
@@ -1597,7 +1638,7 @@ async function runShellCapture(
1597
1638
  export type CaptureRecordOutcome =
1598
1639
  | { kind: "record" | "prerender"; match: MatchResult }
1599
1640
  | { kind: "timeout"; reason: string }
1600
- | { kind: "refused" };
1641
+ | { kind: "refused"; refusal: ShellCaptureRefusal };
1601
1642
 
1602
1643
  /**
1603
1644
  * Record-first capture: the doc record is both the settle signal and the
@@ -1643,7 +1684,13 @@ export async function settleCaptureRecord(
1643
1684
  const loaderRecords = derivedCtx._shellCaptureLoaderRecords;
1644
1685
  const settled = await raceDeadline(
1645
1686
  Promise.all([
1646
- freshHandleStore.getData().then(resolveDeferredHandleValues),
1687
+ freshHandleStore
1688
+ .getData()
1689
+ .then((data) =>
1690
+ resolveDeferredHandleValues(
1691
+ recordedHandleData(freshHandleStore, data),
1692
+ ),
1693
+ ),
1647
1694
  derivation.handlerPushesSettled(),
1648
1695
  loaderRecords && loaderRecords.size > 0
1649
1696
  ? Promise.allSettled([...loaderRecords.values()])
@@ -1655,7 +1702,7 @@ export async function settleCaptureRecord(
1655
1702
  // write starts (no encode for a capture that already failed).
1656
1703
  if (!settled.done) {
1657
1704
  if (refuseOnCaptureGuard(capture.key, derivedCtx))
1658
- return { kind: "refused" };
1705
+ return { kind: "refused", refusal: guardRefusal(derivedCtx) };
1659
1706
  return { kind: "timeout", reason: HANDLER_OUTPUT_TIMEOUT_REASON };
1660
1707
  }
1661
1708
 
@@ -1676,7 +1723,9 @@ export async function settleCaptureRecord(
1676
1723
 
1677
1724
  // Capture guards first: a tripped guard usually also fails the record,
1678
1725
  // and its own message names the cause.
1679
- if (refuseOnCaptureGuard(capture.key, derivedCtx)) return { kind: "refused" };
1726
+ if (refuseOnCaptureGuard(capture.key, derivedCtx)) {
1727
+ return { kind: "refused", refusal: guardRefusal(derivedCtx) };
1728
+ }
1680
1729
 
1681
1730
  const docKey = derivedCtx._shellImplicitCache?.docKey;
1682
1731
  const record =
@@ -1694,7 +1743,7 @@ export async function settleCaptureRecord(
1694
1743
  return { kind: "timeout", reason: HANDLER_OUTPUT_TIMEOUT_REASON };
1695
1744
  }
1696
1745
  warnCaptureRefusedOnce(capture.key, NO_DOC_RECORD_REASON);
1697
- return { kind: "refused" };
1746
+ return { kind: "refused", refusal: "no-record" };
1698
1747
  }
1699
1748
 
1700
1749
  // A handle encode that timed out stores "" (handle-snapshot.ts
@@ -1708,7 +1757,7 @@ export async function settleCaptureRecord(
1708
1757
  "record. A promise inside a Map, Set, or class instance is not awaited " +
1709
1758
  "before the encode; push plain objects and arrays.",
1710
1759
  );
1711
- return { kind: "refused" };
1760
+ return { kind: "refused", refusal: "handles" };
1712
1761
  }
1713
1762
 
1714
1763
  const { fragmentSegments } = await import("../cache/segment-codec.js");
@@ -1805,8 +1854,21 @@ async function attemptCapture(
1805
1854
  // skipped, cache-store middleware's write path gated off by state.cacheHit), so
1806
1855
  // prelude, snapshot, and ring-3 agree on the foreground's generation. Runs per
1807
1856
  // attempt (the retry re-checks; already-settled promises are free).
1857
+ //
1858
+ // A forced capture (a warm) is not latency-bound and must not bake an entry
1859
+ // its own foreground is still replacing, so it waits for as long as the task
1860
+ // hard cap leaves after the capture budget.
1808
1861
  const barrierStart = performance.now();
1809
- await settleTrackedBackgroundTasks(reqCtx, SHELL_CAPTURE_WRITE_BARRIER_MS);
1862
+ await settleTrackedBackgroundTasks(
1863
+ reqCtx,
1864
+ descriptor.force
1865
+ ? Math.max(
1866
+ SHELL_CAPTURE_WRITE_BARRIER_MS,
1867
+ SHELL_CAPTURE_TASK_HARD_CAP_MS -
1868
+ (descriptor.captureTimeout ?? SHELL_CAPTURE_MAX_WAIT_MS),
1869
+ )
1870
+ : SHELL_CAPTURE_WRITE_BARRIER_MS,
1871
+ );
1810
1872
  stats.barrierWaitMs = Math.round(performance.now() - barrierStart);
1811
1873
 
1812
1874
  const derivation = deriveShellCaptureContext(
@@ -1863,7 +1925,7 @@ async function attemptCapture(
1863
1925
  stats.recordSettleMs,
1864
1926
  );
1865
1927
  }
1866
- if (settled.kind === "refused") return "refused";
1928
+ if (settled.kind === "refused") return refuse(stats, settled.refusal);
1867
1929
  if (settled.kind === "timeout") {
1868
1930
  stats.noShellCause = settled.reason;
1869
1931
  return "no-shell";
@@ -1961,8 +2023,8 @@ export function deriveShellCaptureContext(
1961
2023
  // (settleCaptureRecord) can wait for its promises, top-level and nested,
1962
2024
  // before the doc record encodes the handles a HIT replays. A route
1963
2025
  // cache() record's restore (handle-snapshot.ts restoreHandles) lands
1964
- // here too: a value it restores through pushReplayed keeps that loader
1965
- // as owner, so a HIT restores it as that loader's (pushRestored).
2026
+ // here too: a value it restores as a placeholder keeps that loader as
2027
+ // owner, so a HIT restores it as that loader's (pushRestored).
1966
2028
  // - DSL-LOADER pushes. Only bake-lane { ssr: false } loaders and the
1967
2029
  // loaders they await via ctx.use execute at capture (live loaders are
1968
2030
  // masked), their pushes are in the captured HTML (<head> title/meta,
@@ -1970,28 +2032,33 @@ export function deriveShellCaptureContext(
1970
2032
  // loader (loader-cache.ts), so the stored record carries them: each
1971
2033
  // settled, thenable-free push from a loader body keeps that loader as
1972
2034
  // `owner` (HandleStore.push), the record stores it
1973
- // (CachedEntryData.handleOwners), and the HIT restores it through
1974
- // pushRestored. Those copies stand: a promise-carrying bake-lane loader
1975
- // that runs on the HIT reads the store, so its settled pushes (and those
1976
- // replayed inside its body) are dropped and only its thenable ones are
1977
- // added. A loader the route also runs on the live lane is a hole: its
1978
- // live run replaces its copies (pushReplayed, #936), those of the
2035
+ // (CachedEntryData.handleOwners), and a replay that serves the loader
2036
+ // from its pin restores it through pushRestored (restoreHandles). Those
2037
+ // copies stand: a promise-carrying bake-lane loader that runs on the
2038
+ // replay reads the store, so its settled pushes (and those replayed
2039
+ // inside its body) are dropped and only its thenable ones are added. A
2040
+ // loader the replay does not serve from a pin is a hole: one the route
2041
+ // also runs on the live lane, or one whose pin the entry lost. Its run
2042
+ // replaces its copies (pushPlaceholder, #936), those of the
1979
2043
  // dependencies it awaits included (they are credited to it). Nested
1980
2044
  // thenables stay masked (the promise shape is the liveness
1981
2045
  // declaration, mask-nested.ts). A deferred (thenable) push,
1982
2046
  // one with masked nested promises, or one outside a loader body (a
1983
2047
  // loader-cache replay names its loader: pushReplayed below) stays
1984
2048
  // tagged (HandleStore.push loaderPush) so the
1985
- // handle encode cannot stall on a never-resolving mask; only the
1986
- // captureHandles cache-write call site excludes tagged pushes, and the
1987
- // loader records then carry `runs: 1` so a HIT runs those bodies for
1988
- // them.
2049
+ // handle encode cannot stall on a never-resolving mask. A tagged push
2050
+ // is out of the record (captureHandles) and out of the capture's own
2051
+ // render (resolvedHandleStream `recordedOnly`, issue #1035): the
2052
+ // prelude is rendered from what the record keeps, which is what a HIT
2053
+ // hydrates with. The loader records then carry `runs: 1` so a HIT runs
2054
+ // those bodies for them, and what they push reaches the client after
2055
+ // hydration (HandleStore.freezeDocumentSnapshot).
1989
2056
  const handlerPushSettles: Promise<void>[] = [];
1990
2057
  // A replay (a loader-cache HIT, loader-cache.ts replayLoaderHandles, or a
1991
2058
  // route cache() record's owned values, restoreHandles) re-pushes a loader's
1992
- // recorded values through pushReplayed, which calls push() outside any
1993
- // loader body: name that loader for the funnel so the record keeps them
1994
- // under it too.
2059
+ // recorded values through pushReplayed or pushPlaceholder, which call
2060
+ // push() outside any loader body: name that loader for the funnel so the
2061
+ // record keeps them under it too.
1995
2062
  let replayOwner: string | undefined;
1996
2063
  // The live-lane loader whose body a push is made in, at any depth, unless
1997
2064
  // a bake-lane loader's body is nearer (createMatchContextForFull sets the
@@ -2011,15 +2078,17 @@ export function deriveShellCaptureContext(
2011
2078
  : undefined)
2012
2079
  );
2013
2080
  };
2014
- const rawPushReplayed = freshHandleStore.pushReplayed.bind(freshHandleStore);
2015
- freshHandleStore.pushReplayed = (handleName, segmentId, value, loaderId) => {
2016
- replayOwner = loaderId;
2017
- try {
2018
- rawPushReplayed(handleName, segmentId, value, loaderId);
2019
- } finally {
2020
- replayOwner = undefined;
2021
- }
2022
- };
2081
+ for (const copy of ["pushReplayed", "pushPlaceholder"] as const) {
2082
+ const raw = freshHandleStore[copy].bind(freshHandleStore);
2083
+ freshHandleStore[copy] = (handleName, segmentId, value, loaderId) => {
2084
+ replayOwner = loaderId;
2085
+ try {
2086
+ raw(handleName, segmentId, value, loaderId);
2087
+ } finally {
2088
+ replayOwner = undefined;
2089
+ }
2090
+ };
2091
+ }
2023
2092
  const rawCapturePush = freshHandleStore.push.bind(freshHandleStore);
2024
2093
  freshHandleStore.push = (
2025
2094
  handleName: string,
@@ -2043,8 +2112,11 @@ export function deriveShellCaptureContext(
2043
2112
  let owner: string | undefined;
2044
2113
  if (isThenable(value)) {
2045
2114
  // Deferred (thenable) loader pushes always keep the tag — the
2046
- // carve-out below is for settled values only.
2047
- masked = value.then(mask);
2115
+ // carve-out below is for settled values only. Promise.resolve first: a
2116
+ // loader cache() entry replays a deferred push as the thenable Flight
2117
+ // decoded it to, whose then() returns nothing, and the slot was
2118
+ // `undefined` (the empty element of issue #1035).
2119
+ masked = Promise.resolve(value).then(mask);
2048
2120
  } else {
2049
2121
  masked = mask(value);
2050
2122
  if (!maskedNestedThenable) {
@@ -2112,6 +2184,7 @@ export function deriveShellCaptureContext(
2112
2184
  // Own list: the capture's render errors refuse the capture only, and the
2113
2185
  // foreground's never reach it.
2114
2186
  derivedCtx._renderErrors = [];
2187
+ derivedCtx._recoveredHandlerErrors = undefined;
2115
2188
  derivedCtx._shellCaptureRun = true;
2116
2189
  // Own dynamic() latch. The inherited method writes the FOREGROUND context
2117
2190
  // (its closure), which already served, and nothing read it for a runtime
@@ -2230,7 +2303,9 @@ async function captureAndStoreShell(
2230
2303
  // (boundary-less segment — lands in the catch) or is swallowed into
2231
2304
  // per-loader error UI (the render completes — caught after the try).
2232
2305
  const refuseOnGuardTrip = (): "refused" | undefined =>
2233
- refuseOnCaptureGuard(capture.key, reqCtx) ? "refused" : undefined;
2306
+ refuseOnCaptureGuard(capture.key, reqCtx)
2307
+ ? refuse(stats, guardRefusal(reqCtx))
2308
+ : undefined;
2234
2309
 
2235
2310
  // Dev diagnostics for a no-shell attempt (warnNullCaptureOnce): the stacks of
2236
2311
  // the tasks React reports as still pending at the capture's abort. React's
@@ -2346,7 +2421,7 @@ async function captureAndStoreShell(
2346
2421
  "Fix the loader, or drop its ssr: false to move it to the live lane.",
2347
2422
  PPR_LANE_HINT,
2348
2423
  );
2349
- return "refused";
2424
+ return refuse(stats, "loader");
2350
2425
  }
2351
2426
  // The container itself never settled: it is a hole (under an ancestor
2352
2427
  // boundary) or the trivial-prelude gate already fired. Omit — no pin.
@@ -2363,7 +2438,7 @@ async function captureAndStoreShell(
2363
2438
  "Drop its ssr: false to move it to the live lane, or move the decision into middleware.",
2364
2439
  PPR_LANE_HINT,
2365
2440
  );
2366
- return "refused";
2441
+ return refuse(stats, "loader");
2367
2442
  }
2368
2443
  // Past the hole check: this container settled with real material that
2369
2444
  // bakes into the shell prelude (independent of the snapshot pin below).
@@ -2403,6 +2478,21 @@ async function captureAndStoreShell(
2403
2478
  }
2404
2479
  }
2405
2480
 
2481
+ // Built before the render-error check below so its encode errors refuse
2482
+ // the capture (ShellSnapshotHandlesValue).
2483
+ const docKey = reqCtx._shellImplicitCache?.docKey;
2484
+ let prerenderHandles: ShellSnapshotHandlesValue | undefined;
2485
+ if (!capture.navigationOnly && !hasDocRecord(snapshot, docKey)) {
2486
+ // The store is sealed (settleCaptureRecord) and settled (quiesce).
2487
+ const { handles, owners } = captureOwnedHandles(reqCtx._handleStore);
2488
+ prerenderHandles = {
2489
+ handles: await encodeHandles(handles, (error) => {
2490
+ reqCtx._renderErrors?.push(error);
2491
+ }),
2492
+ handleOwners: owners,
2493
+ };
2494
+ }
2495
+
2406
2496
  // A shell component that threw inside a Suspense boundary does not reject
2407
2497
  // the capture: Flight and Fizz report it through onError and the prelude
2408
2498
  // carries the errored boundary, which every HIT would serve (issue #915).
@@ -2416,7 +2506,6 @@ async function captureAndStoreShell(
2416
2506
  // Store only what a HIT reads (issue #941): the doc record and, for a
2417
2507
  // document entry, the bake-lane loader pins. Before the size guards, so
2418
2508
  // they measure what is stored.
2419
- const docKey = reqCtx._shellImplicitCache?.docKey;
2420
2509
  let prunedRecords: string | undefined;
2421
2510
  const { kept, pruned } = pruneShellSnapshot(
2422
2511
  snapshot,
@@ -2454,6 +2543,15 @@ async function captureAndStoreShell(
2454
2543
  stats.snapshotSkipped = true;
2455
2544
  }
2456
2545
  }
2546
+ // Exempt from maxSnapshotBytes: the HIT needs it for its HTML and
2547
+ // hydration data to match.
2548
+ if (prerenderHandles) {
2549
+ snapshot.push({
2550
+ family: "handles",
2551
+ key: SHELL_HANDLES_RECORD_KEY,
2552
+ value: prerenderHandles,
2553
+ });
2554
+ }
2457
2555
 
2458
2556
  // settleCaptureRecord refused every capture without a doc record except a
2459
2557
  // prerender-served one (the prerender store supplies its handler layer on
@@ -2515,7 +2613,7 @@ async function captureAndStoreShell(
2515
2613
  "Shrink the page the shell bakes, or move large regions under a live " +
2516
2614
  "loader's boundary.",
2517
2615
  );
2518
- return "refused";
2616
+ return refuse(stats, "size");
2519
2617
  }
2520
2618
 
2521
2619
  // Missing tags are valid: the shell follows TTL/SWR-only invalidation. Expose
@@ -2587,7 +2685,7 @@ async function captureAndStoreShell(
2587
2685
  record,
2588
2686
  `${sinceWriteMs} ms after this capture wrote it`,
2589
2687
  );
2590
- return "refused";
2688
+ return refuse(stats, "record-expired");
2591
2689
  }
2592
2690
  stats.expired = {
2593
2691
  route,
@@ -2613,7 +2711,7 @@ async function captureAndStoreShell(
2613
2711
  "If capture code deterministically calls updateTag() on its own shell tag, move that mutation out of the render; " +
2614
2712
  "otherwise every generation is invalidated before it can be served.",
2615
2713
  );
2616
- return "refused";
2714
+ return refuse(stats, "invalidated");
2617
2715
  }
2618
2716
  if (storeWrite === "uncacheable") {
2619
2717
  // The store declared the entry unstorable under its current
@@ -2628,8 +2726,9 @@ async function captureAndStoreShell(
2628
2726
  "the write as permanently refusable, so the key is backed off. See the cache store's " +
2629
2727
  "own warning for the specific limit and remedy.",
2630
2728
  );
2631
- return "refused";
2729
+ return refuse(stats, "uncacheable");
2632
2730
  }
2731
+ noteWarmWrite(reqCtx, "shell");
2633
2732
  } catch (error) {
2634
2733
  // Best-effort: a failed put must never throw out of the background task.
2635
2734
  reportCacheError(
@@ -35,6 +35,7 @@ import {
35
35
  resetShellWarningsForTests,
36
36
  warnOnce,
37
37
  } from "./shell-capture-constants.js";
38
+ import { routerKeyPrefix } from "../cache/cache-key-utils.js";
38
39
 
39
40
  export {
40
41
  PPR_REPLAY_STATUS_HEADER,
@@ -529,8 +530,9 @@ export function warnPprWindowCappedOnce(
529
530
 
530
531
  /**
531
532
  * Paths a partitioned request found without a build shell. Build shells are
532
- * per path (a param route prerenders some of its paths), so a negative is
533
- * kept per path, not per route; cleared when full, so it stays bounded.
533
+ * per router and path (a param route prerenders some of its paths), so a
534
+ * negative is kept per router and path, not per route; cleared when full, so
535
+ * it stays bounded.
534
536
  */
535
537
  const pathsWithoutBuildShell = new Set<string>();
536
538
  const PATHS_WITHOUT_BUILD_SHELL_MAX = 1_000;
@@ -540,12 +542,14 @@ const PATHS_WITHOUT_BUILD_SHELL_MAX = 1_000;
540
542
  * already warned, or its path was already found without one.
541
543
  */
542
544
  export function partitionBuildShellCheckDone(
545
+ routerId: string,
543
546
  pathname: string,
544
547
  routeName: string | undefined,
545
548
  ): boolean {
549
+ const router = routerKeyPrefix(routerId);
546
550
  return (
547
- hasWarnedOnce("partition-build-shell", routeName ?? pathname) ||
548
- pathsWithoutBuildShell.has(pathname)
551
+ hasWarnedOnce("partition-build-shell", router + (routeName ?? pathname)) ||
552
+ pathsWithoutBuildShell.has(router + pathname)
549
553
  );
550
554
  }
551
555
 
@@ -560,21 +564,23 @@ export function partitionBuildShellCheckDone(
560
564
  * with no build shell stays silent.
561
565
  */
562
566
  export function notePartitionBuildShellCheck(
567
+ routerId: string,
563
568
  pathname: string,
564
569
  routeName: string | undefined,
565
570
  found: boolean,
566
571
  ): void {
572
+ const router = routerKeyPrefix(routerId);
567
573
  if (!found) {
568
574
  if (pathsWithoutBuildShell.size >= PATHS_WITHOUT_BUILD_SHELL_MAX) {
569
575
  pathsWithoutBuildShell.clear();
570
576
  }
571
- pathsWithoutBuildShell.add(pathname);
577
+ pathsWithoutBuildShell.add(router + pathname);
572
578
  return;
573
579
  }
574
580
  // Concurrent first requests can both probe; one of them warns.
575
581
  warnOnce(
576
582
  "partition-build-shell",
577
- routeName ?? pathname,
583
+ router + (routeName ?? pathname),
578
584
  () =>
579
585
  `[rango] Route ${routeName ? `"${routeName}" ` : ""}("${pathname}") has a ` +
580
586
  "build-time shell, but its request partition (a cache({ key }) enclosing the route, " +
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The SSR module loader a handler created without `loadSSRModule` falls back
3
+ * to in tests (`router.fetch` creates its handler that way). A leaf with no
4
+ * runtime imports: testing/serve-shell-request.ts installs its stub here when
5
+ * it loads, without importing rsc/handler.ts (which binds build-only virtual
6
+ * modules a flight-only test config does not alias).
7
+ */
8
+
9
+ import type { LoadSSRModule } from "./types.js";
10
+
11
+ let testLoader: LoadSSRModule | undefined;
12
+
13
+ /**
14
+ * @internal Tests only: `router.prerender()` warms through `router.fetch`,
15
+ * and the rsc Vitest project cannot run `import.meta.viteRsc.loadModule`.
16
+ */
17
+ export function setDefaultSSRModuleLoaderForTests(
18
+ loader: LoadSSRModule | undefined,
19
+ ): void {
20
+ testLoader = loader;
21
+ }
22
+
23
+ /** @internal Undefined everywhere but in tests. */
24
+ export function defaultSSRModuleLoaderForTests(): LoadSSRModule | undefined {
25
+ return testLoader;
26
+ }
package/src/rsc/types.ts CHANGED
@@ -97,6 +97,14 @@ export interface RscPayload {
97
97
  locationState?: Record<string, unknown>;
98
98
  };
99
99
  returnValue?: { ok: boolean; data: unknown };
100
+ /**
101
+ * useActionState form state of a progressive-enhancement POST re-render
102
+ * (the value the SSR render received). Present only on that document; the
103
+ * browser entry passes it to hydrateRoot so the hydrated hook matches the
104
+ * server HTML instead of resetting to its initial state. A promise so a
105
+ * state Flight cannot serialize rejects this slot alone.
106
+ */
107
+ formState?: Promise<ReactFormState>;
100
108
  }
101
109
 
102
110
  /**
@@ -357,13 +365,11 @@ export interface CreateRSCHandlerOptions<
357
365
  cache?: HandlerCacheConfig | ((env: TEnv) => HandlerCacheConfig);
358
366
 
359
367
  /**
360
- * RSC version string included in metadata.
361
- * The browser sends this back on partial requests to detect version mismatches.
362
- *
363
- * Defaults to the auto-generated VERSION from `rsc-router:version` virtual module.
364
- * Only set this if you need a custom versioning strategy.
368
+ * Version override for this handler. Same meaning as
369
+ * `createRouter({ version })`, which it takes precedence over: this exact
370
+ * value is used for both of the router's versions.
365
371
  *
366
- * @default VERSION from rsc-router:version
372
+ * @default createRouter({ version }), else the router's build versions
367
373
  */
368
374
  version?: string;
369
375
 
@@ -0,0 +1,25 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ const IS_BROWSER = typeof window !== "undefined";
4
+
5
+ /**
6
+ * What a RouteContentWrapper receives as `content`.
7
+ *
8
+ * Browser: hand over the settled value. React treats a promise as fulfilled
9
+ * only after it has read it, and a boundary on screen handed a promise it has
10
+ * not read, in a render that cannot wait, shows its fallback. A component that
11
+ * is already a promise (a Flight-streamed segment) is returned as is, since
12
+ * it can really be pending.
13
+ *
14
+ * Server: a fresh `Promise.resolve(component)` per call, so Suspense emits the
15
+ * fallback in the streamed HTML. A shared resolved promise would carry React's
16
+ * `.status` across requests and skip it.
17
+ *
18
+ * @internal
19
+ */
20
+ export function getBoundaryContent(component: ReactNode): ReactNode {
21
+ if (component instanceof Promise || IS_BROWSER) {
22
+ return component;
23
+ }
24
+ return Promise.resolve(component);
25
+ }