@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1c0bdfad

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 (406) hide show
  1. package/AGENTS.md +17 -0
  2. package/README.md +291 -61
  3. package/dist/bin/rango.js +544 -143
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3744 -1329
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +67 -13
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +312 -0
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +247 -23
  12. package/skills/caching/SKILL.md +322 -19
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/debug-manifest/SKILL.md +4 -2
  16. package/skills/document-cache/SKILL.md +78 -55
  17. package/skills/handler-use/SKILL.md +364 -0
  18. package/skills/hooks/SKILL.md +282 -60
  19. package/skills/host-router/SKILL.md +278 -0
  20. package/skills/i18n/SKILL.md +276 -0
  21. package/skills/intercept/SKILL.md +50 -6
  22. package/skills/layout/SKILL.md +35 -9
  23. package/skills/links/SKILL.md +249 -17
  24. package/skills/loader/SKILL.md +297 -31
  25. package/skills/middleware/SKILL.md +52 -13
  26. package/skills/migrate-nextjs/SKILL.md +584 -0
  27. package/skills/migrate-react-router/SKILL.md +771 -0
  28. package/skills/mime-routes/SKILL.md +28 -1
  29. package/skills/observability/SKILL.md +172 -0
  30. package/skills/parallel/SKILL.md +203 -7
  31. package/skills/prerender/SKILL.md +155 -111
  32. package/skills/rango/SKILL.md +251 -23
  33. package/skills/react-compiler/SKILL.md +168 -0
  34. package/skills/response-routes/SKILL.md +123 -48
  35. package/skills/route/SKILL.md +104 -9
  36. package/skills/router-setup/SKILL.md +124 -11
  37. package/skills/scripts/SKILL.md +179 -0
  38. package/skills/server-actions/SKILL.md +775 -0
  39. package/skills/streams-and-websockets/SKILL.md +283 -0
  40. package/skills/tailwind/SKILL.md +27 -3
  41. package/skills/testing/SKILL.md +125 -222
  42. package/skills/testing/bindings.md +103 -0
  43. package/skills/testing/cache-prerender.md +127 -0
  44. package/skills/testing/client-components.md +124 -0
  45. package/skills/testing/e2e-parity.md +125 -0
  46. package/skills/testing/flight.md +91 -0
  47. package/skills/testing/handles.md +129 -0
  48. package/skills/testing/loader.md +128 -0
  49. package/skills/testing/middleware.md +99 -0
  50. package/skills/testing/render-handler.md +121 -0
  51. package/skills/testing/response-routes.md +95 -0
  52. package/skills/testing/reverse-and-types.md +84 -0
  53. package/skills/testing/server-actions.md +107 -0
  54. package/skills/testing/server-tree.md +128 -0
  55. package/skills/testing/setup.md +123 -0
  56. package/skills/typesafety/SKILL.md +357 -52
  57. package/skills/use-cache/SKILL.md +46 -14
  58. package/skills/view-transitions/SKILL.md +294 -0
  59. package/src/__augment-tests__/augment.ts +81 -0
  60. package/src/__augment-tests__/augmented.check.ts +116 -0
  61. package/src/__internal.ts +67 -40
  62. package/src/bin/rango.ts +18 -0
  63. package/src/browser/action-coordinator.ts +53 -36
  64. package/src/browser/action-fence.ts +47 -0
  65. package/src/browser/app-shell.ts +39 -0
  66. package/src/browser/app-version.ts +14 -0
  67. package/src/browser/connection-warmup.ts +134 -0
  68. package/src/browser/cookie-name.ts +140 -0
  69. package/src/browser/event-controller.ts +197 -150
  70. package/src/browser/history-state.ts +21 -0
  71. package/src/browser/index.ts +3 -3
  72. package/src/browser/invalidate-client-cache.ts +52 -0
  73. package/src/browser/link-interceptor.ts +4 -0
  74. package/src/browser/navigation-bridge.ts +200 -30
  75. package/src/browser/navigation-client.ts +217 -58
  76. package/src/browser/navigation-store-handle.ts +38 -0
  77. package/src/browser/navigation-store.ts +76 -67
  78. package/src/browser/navigation-transaction.ts +18 -66
  79. package/src/browser/network-error-handler.ts +34 -7
  80. package/src/browser/partial-update.ts +187 -112
  81. package/src/browser/prefetch/cache.ts +312 -30
  82. package/src/browser/prefetch/fetch.ts +344 -47
  83. package/src/browser/prefetch/policy.ts +6 -0
  84. package/src/browser/prefetch/queue.ts +126 -20
  85. package/src/browser/prefetch/resource-ready.ts +77 -0
  86. package/src/browser/rango-state.ts +158 -76
  87. package/src/browser/react/Link.tsx +125 -18
  88. package/src/browser/react/NavigationProvider.tsx +135 -120
  89. package/src/browser/react/ScrollRestoration.tsx +10 -6
  90. package/src/browser/react/context.ts +7 -2
  91. package/src/browser/react/filter-segment-order.ts +66 -7
  92. package/src/browser/react/index.ts +0 -48
  93. package/src/browser/react/location-state-shared.ts +178 -8
  94. package/src/browser/react/location-state.ts +39 -14
  95. package/src/browser/react/use-action.ts +6 -15
  96. package/src/browser/react/use-handle.ts +23 -69
  97. package/src/browser/react/use-href.tsx +8 -1
  98. package/src/browser/react/use-link-status.ts +33 -8
  99. package/src/browser/react/use-navigation.ts +32 -7
  100. package/src/browser/react/use-params.ts +20 -10
  101. package/src/browser/react/use-reverse.ts +106 -0
  102. package/src/browser/react/use-router.ts +46 -11
  103. package/src/browser/react/use-search-params.ts +0 -5
  104. package/src/browser/react/use-segments.ts +11 -21
  105. package/src/browser/response-adapter.ts +80 -5
  106. package/src/browser/rsc-router.tsx +226 -75
  107. package/src/browser/scroll-restoration.ts +54 -42
  108. package/src/browser/segment-reconciler.ts +36 -9
  109. package/src/browser/segment-structure-assert.ts +2 -2
  110. package/src/browser/server-action-bridge.ts +619 -442
  111. package/src/browser/types.ts +115 -11
  112. package/src/browser/validate-redirect-origin.ts +43 -16
  113. package/src/build/collect-fallback-refs.ts +107 -0
  114. package/src/build/generate-manifest.ts +65 -40
  115. package/src/build/generate-route-types.ts +7 -1
  116. package/src/build/index.ts +8 -2
  117. package/src/build/prefix-tree-utils.ts +123 -0
  118. package/src/build/route-trie.ts +182 -37
  119. package/src/build/route-types/ast-route-extraction.ts +15 -8
  120. package/src/build/route-types/codegen.ts +16 -5
  121. package/src/build/route-types/include-resolution.ts +125 -24
  122. package/src/build/route-types/param-extraction.ts +6 -3
  123. package/src/build/route-types/per-module-writer.ts +22 -6
  124. package/src/build/route-types/router-processing.ts +392 -106
  125. package/src/build/route-types/scan-filter.ts +9 -2
  126. package/src/build/route-types/source-scan.ts +216 -0
  127. package/src/build/runtime-discovery.ts +9 -20
  128. package/src/cache/cache-error.ts +104 -0
  129. package/src/cache/cache-key-utils.ts +29 -13
  130. package/src/cache/cache-policy.ts +108 -34
  131. package/src/cache/cache-runtime.ts +214 -48
  132. package/src/cache/cache-scope.ts +236 -89
  133. package/src/cache/cache-tag.ts +103 -0
  134. package/src/cache/cf/cf-base64.ts +33 -0
  135. package/src/cache/cf/cf-cache-constants.ts +127 -0
  136. package/src/cache/cf/cf-cache-store.ts +2224 -171
  137. package/src/cache/cf/cf-cache-types.ts +349 -0
  138. package/src/cache/cf/cf-kv-utils.ts +46 -0
  139. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  140. package/src/cache/cf/index.ts +11 -17
  141. package/src/cache/document-cache.ts +89 -27
  142. package/src/cache/handle-snapshot.ts +70 -0
  143. package/src/cache/index.ts +11 -20
  144. package/src/cache/memory-segment-store.ts +136 -37
  145. package/src/cache/profile-registry.ts +31 -31
  146. package/src/cache/read-through-swr.ts +41 -11
  147. package/src/cache/segment-codec.ts +9 -17
  148. package/src/cache/tag-invalidation.ts +230 -0
  149. package/src/cache/taint.ts +55 -0
  150. package/src/cache/types.ts +37 -100
  151. package/src/client.rsc.tsx +45 -21
  152. package/src/client.tsx +120 -336
  153. package/src/cloudflare/index.ts +11 -0
  154. package/src/cloudflare/tracing.ts +109 -0
  155. package/src/component-utils.ts +19 -0
  156. package/src/components/DefaultDocument.tsx +8 -2
  157. package/src/context-var.ts +84 -2
  158. package/src/debug.ts +2 -2
  159. package/src/decode-loader-results.ts +52 -0
  160. package/src/defer.ts +196 -0
  161. package/src/deps/ssr.ts +0 -1
  162. package/src/encode-kv.ts +49 -0
  163. package/src/errors.ts +30 -4
  164. package/src/escape-script.ts +52 -0
  165. package/src/handle.ts +70 -22
  166. package/src/handles/MetaTags.tsx +56 -19
  167. package/src/handles/Scripts.tsx +183 -0
  168. package/src/handles/breadcrumbs.ts +95 -0
  169. package/src/handles/is-thenable.ts +19 -0
  170. package/src/handles/meta.ts +51 -40
  171. package/src/handles/script.ts +244 -0
  172. package/src/host/cookie-handler.ts +9 -60
  173. package/src/host/errors.ts +0 -24
  174. package/src/host/index.ts +8 -5
  175. package/src/host/pattern-matcher.ts +23 -52
  176. package/src/host/router.ts +107 -99
  177. package/src/host/testing.ts +40 -27
  178. package/src/host/types.ts +37 -4
  179. package/src/host/utils.ts +1 -1
  180. package/src/href-client.ts +137 -22
  181. package/src/index.rsc.ts +79 -29
  182. package/src/index.ts +149 -65
  183. package/src/internal-debug.ts +11 -10
  184. package/src/loader-store.ts +500 -0
  185. package/src/loader.rsc.ts +20 -13
  186. package/src/loader.ts +12 -11
  187. package/src/missing-id-error.ts +68 -0
  188. package/src/outlet-context.ts +1 -1
  189. package/src/outlet-provider.tsx +1 -5
  190. package/src/prerender/param-hash.ts +16 -16
  191. package/src/prerender/store.ts +63 -26
  192. package/src/prerender.ts +198 -82
  193. package/src/redirect-origin.ts +100 -0
  194. package/src/regex-escape.ts +8 -0
  195. package/src/render-error-thrower.tsx +20 -0
  196. package/src/response-utils.ts +62 -0
  197. package/src/reverse.ts +65 -15
  198. package/src/root-error-boundary.tsx +1 -19
  199. package/src/route-content-wrapper.tsx +7 -72
  200. package/src/route-definition/dsl-helpers.ts +469 -276
  201. package/src/route-definition/helper-factories.ts +29 -139
  202. package/src/route-definition/helpers-types.ts +113 -37
  203. package/src/route-definition/index.ts +3 -3
  204. package/src/route-definition/redirect.ts +53 -12
  205. package/src/route-definition/resolve-handler-use.ts +161 -0
  206. package/src/route-definition/use-item-types.ts +32 -0
  207. package/src/route-map-builder.ts +7 -17
  208. package/src/route-types.ts +37 -41
  209. package/src/router/basename.ts +14 -0
  210. package/src/router/content-negotiation.ts +164 -17
  211. package/src/router/error-handling.ts +45 -18
  212. package/src/router/find-match.ts +45 -22
  213. package/src/router/handler-context.ts +110 -39
  214. package/src/router/instrument.ts +350 -0
  215. package/src/router/intercept-resolution.ts +50 -24
  216. package/src/router/lazy-includes.ts +19 -53
  217. package/src/router/loader-resolution.ts +274 -56
  218. package/src/router/logging.ts +5 -8
  219. package/src/router/manifest.ts +49 -45
  220. package/src/router/match-api.ts +121 -205
  221. package/src/router/match-context.ts +0 -22
  222. package/src/router/match-handlers.ts +58 -58
  223. package/src/router/match-middleware/background-revalidation.ts +33 -6
  224. package/src/router/match-middleware/cache-lookup.ts +214 -263
  225. package/src/router/match-middleware/cache-store.ts +73 -33
  226. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  227. package/src/router/match-middleware/segment-resolution.ts +52 -18
  228. package/src/router/match-pipelines.ts +1 -42
  229. package/src/router/match-result.ts +104 -49
  230. package/src/router/metrics.ts +217 -26
  231. package/src/router/middleware-types.ts +24 -110
  232. package/src/router/middleware.ts +384 -197
  233. package/src/router/navigation-snapshot.ts +131 -0
  234. package/src/router/params-util.ts +23 -0
  235. package/src/router/pattern-matching.ts +148 -91
  236. package/src/router/prefetch-cache-ttl.ts +51 -0
  237. package/src/router/prerender-match.ts +199 -56
  238. package/src/router/preview-match.ts +32 -102
  239. package/src/router/request-classification.ts +276 -0
  240. package/src/router/revalidation.ts +144 -74
  241. package/src/router/route-snapshot.ts +244 -0
  242. package/src/router/router-context.ts +8 -28
  243. package/src/router/router-interfaces.ts +129 -36
  244. package/src/router/router-options.ts +185 -23
  245. package/src/router/router-registry.ts +2 -5
  246. package/src/router/segment-resolution/fresh.ts +281 -76
  247. package/src/router/segment-resolution/helpers.ts +116 -31
  248. package/src/router/segment-resolution/loader-cache.ts +63 -37
  249. package/src/router/segment-resolution/revalidation.ts +493 -391
  250. package/src/router/segment-resolution/static-store.ts +19 -5
  251. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  252. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  253. package/src/router/segment-resolution.ts +5 -1
  254. package/src/router/segment-wrappers.ts +8 -5
  255. package/src/router/state-cookie-name.ts +33 -0
  256. package/src/router/substitute-pattern-params.ts +56 -0
  257. package/src/router/telemetry-otel.ts +161 -199
  258. package/src/router/telemetry.ts +96 -19
  259. package/src/router/timeout.ts +0 -20
  260. package/src/router/tracing.ts +206 -0
  261. package/src/router/trie-matching.ts +180 -58
  262. package/src/router/types.ts +10 -63
  263. package/src/router/url-params.ts +44 -0
  264. package/src/router.ts +182 -54
  265. package/src/rsc/handler-context.ts +3 -2
  266. package/src/rsc/handler.ts +702 -460
  267. package/src/rsc/helpers.ts +168 -46
  268. package/src/rsc/index.ts +2 -25
  269. package/src/rsc/json-route-result.ts +38 -0
  270. package/src/rsc/loader-fetch.ts +127 -31
  271. package/src/rsc/manifest-init.ts +33 -42
  272. package/src/rsc/origin-guard.ts +39 -25
  273. package/src/rsc/progressive-enhancement.ts +98 -19
  274. package/src/rsc/redirect-guard.ts +99 -0
  275. package/src/rsc/response-cache-serve.ts +238 -0
  276. package/src/rsc/response-error.ts +79 -12
  277. package/src/rsc/response-route-handler.ts +99 -189
  278. package/src/rsc/rsc-rendering.ts +126 -106
  279. package/src/rsc/runtime-warnings.ts +23 -10
  280. package/src/rsc/server-action.ts +269 -114
  281. package/src/rsc/ssr-setup.ts +144 -0
  282. package/src/rsc/types.ts +34 -6
  283. package/src/runtime-env.ts +18 -0
  284. package/src/search-params.ts +49 -41
  285. package/src/segment-content-promise.ts +67 -0
  286. package/src/segment-loader-promise.ts +149 -0
  287. package/src/segment-system.tsx +281 -129
  288. package/src/serialize.ts +243 -0
  289. package/src/server/context.ts +317 -63
  290. package/src/server/cookie-parse.ts +32 -0
  291. package/src/server/cookie-store.ts +80 -5
  292. package/src/server/handle-store.ts +40 -38
  293. package/src/server/loader-registry.ts +26 -46
  294. package/src/server/request-context.ts +425 -177
  295. package/src/server.ts +6 -0
  296. package/src/ssr/index.tsx +25 -16
  297. package/src/static-handler.ts +27 -18
  298. package/src/testing/cache-status.ts +162 -0
  299. package/src/testing/collect-handle.ts +40 -0
  300. package/src/testing/dispatch.ts +701 -0
  301. package/src/testing/dom.entry.ts +22 -0
  302. package/src/testing/e2e/fixture.ts +188 -0
  303. package/src/testing/e2e/index.ts +128 -0
  304. package/src/testing/e2e/matchers.ts +35 -0
  305. package/src/testing/e2e/page-helpers.ts +272 -0
  306. package/src/testing/e2e/parity.ts +387 -0
  307. package/src/testing/e2e/server.ts +195 -0
  308. package/src/testing/flight-matchers.ts +97 -0
  309. package/src/testing/flight-normalize.ts +11 -0
  310. package/src/testing/flight-runtime.d.ts +57 -0
  311. package/src/testing/flight-tree.ts +682 -0
  312. package/src/testing/flight.entry.ts +52 -0
  313. package/src/testing/flight.ts +257 -0
  314. package/src/testing/generated-routes.ts +183 -0
  315. package/src/testing/index.ts +99 -0
  316. package/src/testing/internal/context.ts +371 -0
  317. package/src/testing/internal/flight-client-globals.ts +30 -0
  318. package/src/testing/internal/seed-vars.ts +54 -0
  319. package/src/testing/render-handler.ts +343 -0
  320. package/src/testing/render-route.tsx +581 -0
  321. package/src/testing/run-loader.ts +385 -0
  322. package/src/testing/run-middleware.ts +205 -0
  323. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  324. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  325. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  326. package/src/testing/vitest-stubs/version.ts +5 -0
  327. package/src/testing/vitest.ts +305 -0
  328. package/src/theme/ThemeProvider.tsx +20 -58
  329. package/src/theme/ThemeScript.tsx +7 -9
  330. package/src/theme/constants.ts +52 -13
  331. package/src/theme/index.ts +3 -19
  332. package/src/theme/theme-context.ts +1 -5
  333. package/src/theme/theme-script.ts +22 -21
  334. package/src/theme/use-theme.ts +0 -3
  335. package/src/types/boundaries.ts +0 -35
  336. package/src/types/cache-types.ts +17 -8
  337. package/src/types/error-types.ts +30 -90
  338. package/src/types/global-namespace.ts +54 -41
  339. package/src/types/handler-context.ts +236 -88
  340. package/src/types/index.ts +1 -10
  341. package/src/types/loader-types.ts +44 -15
  342. package/src/types/request-scope.ts +112 -0
  343. package/src/types/route-config.ts +10 -45
  344. package/src/types/route-entry.ts +19 -7
  345. package/src/types/segments.ts +37 -19
  346. package/src/urls/include-helper.ts +33 -70
  347. package/src/urls/index.ts +1 -11
  348. package/src/urls/path-helper-types.ts +58 -11
  349. package/src/urls/path-helper.ts +57 -111
  350. package/src/urls/pattern-types.ts +48 -19
  351. package/src/urls/response-types.ts +25 -22
  352. package/src/urls/type-extraction.ts +58 -139
  353. package/src/urls/urls-function.ts +1 -18
  354. package/src/use-loader.tsx +346 -89
  355. package/src/vite/debug.ts +185 -0
  356. package/src/vite/discovery/bundle-postprocess.ts +64 -91
  357. package/src/vite/discovery/discover-routers.ts +147 -88
  358. package/src/vite/discovery/discovery-errors.ts +194 -0
  359. package/src/vite/discovery/gate-state.ts +171 -0
  360. package/src/vite/discovery/prerender-collection.ts +247 -145
  361. package/src/vite/discovery/route-types-writer.ts +40 -84
  362. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  363. package/src/vite/discovery/state.ts +61 -13
  364. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  365. package/src/vite/index.ts +10 -3
  366. package/src/vite/inject-client-debug.ts +36 -0
  367. package/src/vite/plugin-types.ts +155 -65
  368. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  369. package/src/vite/plugins/client-ref-dedup.ts +120 -0
  370. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  371. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  372. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  373. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  374. package/src/vite/plugins/expose-action-id.ts +49 -98
  375. package/src/vite/plugins/expose-id-utils.ts +96 -51
  376. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  377. package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
  378. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  379. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  380. package/src/vite/plugins/expose-internal-ids.ts +553 -317
  381. package/src/vite/plugins/performance-tracks.ts +89 -0
  382. package/src/vite/plugins/refresh-cmd.ts +127 -0
  383. package/src/vite/plugins/use-cache-transform.ts +73 -83
  384. package/src/vite/plugins/version-injector.ts +21 -25
  385. package/src/vite/plugins/version-plugin.ts +46 -37
  386. package/src/vite/plugins/virtual-entries.ts +13 -18
  387. package/src/vite/rango.ts +241 -287
  388. package/src/vite/router-discovery.ts +956 -149
  389. package/src/vite/utils/ast-handler-extract.ts +26 -35
  390. package/src/vite/utils/banner.ts +4 -4
  391. package/src/vite/utils/bundle-analysis.ts +10 -15
  392. package/src/vite/utils/client-chunks.ts +184 -0
  393. package/src/vite/utils/directive-prologue.ts +40 -0
  394. package/src/vite/utils/forward-user-plugins.ts +171 -0
  395. package/src/vite/utils/manifest-utils.ts +4 -59
  396. package/src/vite/utils/package-resolution.ts +20 -52
  397. package/src/vite/utils/prerender-utils.ts +141 -34
  398. package/src/vite/utils/shared-utils.ts +92 -42
  399. package/CLAUDE.md +0 -5
  400. package/src/browser/action-response-classifier.ts +0 -99
  401. package/src/browser/react/use-client-cache.ts +0 -58
  402. package/src/browser/shallow.ts +0 -40
  403. package/src/handles/index.ts +0 -6
  404. package/src/network-error-thrower.tsx +0 -23
  405. package/src/route-definition/route-function.ts +0 -119
  406. package/src/router/middleware-cookies.ts +0 -55
@@ -16,36 +16,35 @@
16
16
  */
17
17
 
18
18
  import {
19
- requireRequestContext,
19
+ getRequestContext,
20
20
  setRequestContextParams,
21
- getLocationState,
22
21
  } from "../server/request-context.js";
23
- import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
22
+ import { appendMetric } from "../router/metrics.js";
23
+ import { observePhase, PHASES } from "../router/instrument.js";
24
24
  import type { RscPayload } from "./types.js";
25
25
  import {
26
26
  hasBodyContent,
27
27
  createResponseWithMergedHeaders,
28
28
  createSimpleRedirectResponse,
29
- carryOverRedirectHeaders,
29
+ interceptRedirectForPartial,
30
+ attachLocationStateIfPresent,
30
31
  } from "./helpers.js";
32
+ import { warnNonRedirectActionResponse } from "./runtime-warnings.js";
31
33
  import type { HandlerContext } from "./handler-context.js";
32
-
33
- /**
34
- * Attach location state set during the action to a payload's metadata.
35
- * No-op if no location state was set.
36
- */
37
- function attachLocationState(payload: RscPayload): void {
38
- const locationState = getLocationState();
39
- if (locationState) {
40
- payload.metadata!.locationState =
41
- resolveLocationStateEntries(locationState);
42
- }
43
- }
34
+ import type { MatchResult } from "../types.js";
44
35
 
45
36
  /**
46
37
  * Data flowing from action execution to the revalidation phase.
47
- * When the action completes without redirect/error-boundary, the handler
48
- * passes this to route middleware → revalidateAfterAction.
38
+ * When the action completes without redirect, the handler passes this to route
39
+ * middleware → revalidateAfterAction.
40
+ *
41
+ * `errorBoundary` carries the matched error-boundary result when the action
42
+ * threw and a boundary matched. The error-boundary render is then performed in
43
+ * the revalidation phase so it runs INSIDE the same route-middleware wrapper as
44
+ * a successful revalidation — route middleware (context vars, headers, cookies)
45
+ * must apply to the error render too, matching the module doc's "identical to a
46
+ * normal render". When `errorBoundary` is set, `actionContext` is unused (the
47
+ * boundary is already matched; no matchPartial is run).
49
48
  */
50
49
  export interface ActionContinuation {
51
50
  returnValue: { ok: boolean; data: unknown };
@@ -59,6 +58,7 @@ export interface ActionContinuation {
59
58
  actionResult: unknown;
60
59
  formData?: FormData;
61
60
  };
61
+ errorBoundary?: MatchResult;
62
62
  }
63
63
 
64
64
  /**
@@ -74,7 +74,7 @@ export async function executeServerAction<TEnv>(
74
74
  env: TEnv,
75
75
  url: URL,
76
76
  actionId: string,
77
- handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
77
+ handleStore: ReturnType<typeof getRequestContext>["_handleStore"],
78
78
  ): Promise<Response | ActionContinuation> {
79
79
  const temporaryReferences = ctx.createTemporaryReferenceSet();
80
80
 
@@ -88,17 +88,82 @@ export async function executeServerAction<TEnv>(
88
88
  ? await request.formData()
89
89
  : await request.text();
90
90
 
91
- if (body instanceof FormData) {
92
- actionFormData = body;
93
- }
94
-
95
91
  if (hasBodyContent(body)) {
96
92
  args = await ctx.decodeReply(body, { temporaryReferences });
97
93
  }
94
+
95
+ // Surface the action's FormData to shouldRevalidate({ formData }) for a JS
96
+ // server action, matching the PE path (progressive-enhancement.ts populates
97
+ // formData from request.formData()). A form-driven action is invoked as
98
+ // action(formData) (direct) or action(prevState, formData) (useActionState),
99
+ // so the FormData arrives INSIDE the decoded args. Use the LAST FormData arg:
100
+ // for useActionState the submitted form is the final arg, and a prior state
101
+ // that is itself a FormData would otherwise be picked first.
102
+ //
103
+ // The raw request body is NOT usable here: encodeReply wraps a FormData arg
104
+ // in a multipart envelope whose keys are Flight-encoded (e.g. `_1_name`,
105
+ // `0`), so request.formData() would hand shouldRevalidate a FormData with
106
+ // internal keys instead of the consumer's `name`. The decoded arg has the
107
+ // original keys.
108
+ for (let i = args.length - 1; i >= 0; i--) {
109
+ if (args[i] instanceof FormData) {
110
+ actionFormData = args[i] as FormData;
111
+ break;
112
+ }
113
+ }
98
114
  } catch (error) {
99
- throw new Error(`Failed to decode action arguments: ${error}`, {
115
+ // Keep the original error as `cause` for server-side logging, but do not
116
+ // interpolate it into the message: that string can surface to the client
117
+ // and may leak decode internals.
118
+ const decodeError = new Error("Failed to decode action arguments", {
100
119
  cause: error,
101
120
  });
121
+
122
+ // Produce a router-controlled response instead of re-throwing into the
123
+ // host (which would surface as an opaque 500). This mirrors the no-JS PE
124
+ // path, where a malformed form body renders the route error boundary or
125
+ // returns an explicit 400 (progressive-enhancement.ts). Attempt boundary
126
+ // rendering first; if a boundary matches, defer the render to the
127
+ // revalidation phase (errorBoundary continuation) so it runs inside route
128
+ // middleware, identical to the action-threw path below. Otherwise return a
129
+ // plain 400 — the JS and no-JS paths now converge on the same outcome.
130
+ let decodeBoundary: MatchResult | undefined;
131
+ try {
132
+ decodeBoundary =
133
+ (await ctx.router.matchError(request, { env }, decodeError, "route")) ??
134
+ undefined;
135
+ } catch {
136
+ // matchError itself failed — fall through to the plain 400 below.
137
+ decodeBoundary = undefined;
138
+ }
139
+
140
+ ctx.callOnError(decodeError, "action", {
141
+ request,
142
+ url,
143
+ env,
144
+ actionId,
145
+ handledByBoundary: !!decodeBoundary,
146
+ });
147
+
148
+ if (decodeBoundary) {
149
+ return {
150
+ returnValue: { ok: false, data: decodeError },
151
+ // 400: malformed action request, matching the PE explicit-400 status
152
+ // class (the action-threw path uses 500; a decode failure is a bad
153
+ // request, not an action runtime error).
154
+ actionStatus: 400,
155
+ temporaryReferences,
156
+ actionContext: {
157
+ actionId,
158
+ actionUrl: new URL(url),
159
+ actionResult: decodeError,
160
+ formData: actionFormData,
161
+ },
162
+ errorBoundary: decodeBoundary,
163
+ };
164
+ }
165
+
166
+ return createResponseWithMergedHeaders(null, { status: 400 });
102
167
  }
103
168
 
104
169
  // Execute the server action
@@ -108,51 +173,37 @@ export async function executeServerAction<TEnv>(
108
173
 
109
174
  try {
110
175
  loadedAction = await ctx.loadServerAction(actionId);
111
- const data = await loadedAction!.apply(null, args);
176
+ let data = await loadedAction!.apply(null, args);
112
177
 
113
- // Intercept redirect responses from actions. Without this, the redirect
114
- // Response would be serialized as the action returnValue (which fails)
115
- // and the revalidation step would run unnecessarily.
178
+ // Intercept redirect Responses: serializing one as the action returnValue
179
+ // would fail, and revalidation would run needlessly.
116
180
  if (data instanceof Response) {
117
- const redirectUrl = data.headers.get("Location");
118
- const isRedirect = data.status >= 300 && data.status < 400 && redirectUrl;
119
- if (isRedirect) {
120
- const locationState = getLocationState();
121
- let redirect: Response;
122
- if (locationState) {
123
- redirect = ctx.createRedirectFlightResponse(
124
- redirectUrl,
125
- resolveLocationStateEntries(locationState),
126
- );
127
- } else {
128
- redirect = createSimpleRedirectResponse(redirectUrl);
129
- }
130
- carryOverRedirectHeaders(data, redirect);
131
- return redirect;
181
+ const intercepted = interceptRedirectForPartial(
182
+ data,
183
+ ctx.createRedirectFlightResponse,
184
+ );
185
+ if (intercepted) return intercepted;
186
+
187
+ // Non-redirect Response returned (not thrown): a raw Response cannot be
188
+ // serialized into Flight. Discard it and re-render — mirroring the PE
189
+ // path (progressive-enhancement.ts) so JS and no-JS behave identically.
190
+ if (process.env.NODE_ENV !== "production") {
191
+ warnNonRedirectActionResponse(actionId);
132
192
  }
193
+ data = undefined;
133
194
  }
134
195
 
135
196
  returnValue = { ok: true, data };
136
197
  } catch (error) {
198
+ let actionResultData: unknown = error;
199
+
137
200
  // Handle thrown redirect (e.g., throw redirect('/path'))
138
201
  if (error instanceof Response) {
139
- const redirectUrl = error.headers.get("Location");
140
- const isRedirect =
141
- error.status >= 300 && error.status < 400 && redirectUrl;
142
- if (isRedirect) {
143
- const locationState = getLocationState();
144
- let redirect: Response;
145
- if (locationState) {
146
- redirect = ctx.createRedirectFlightResponse(
147
- redirectUrl,
148
- resolveLocationStateEntries(locationState),
149
- );
150
- } else {
151
- redirect = createSimpleRedirectResponse(redirectUrl);
152
- }
153
- carryOverRedirectHeaders(error, redirect);
154
- return redirect;
155
- }
202
+ const intercepted = interceptRedirectForPartial(
203
+ error,
204
+ ctx.createRedirectFlightResponse,
205
+ );
206
+ if (intercepted) return intercepted;
156
207
 
157
208
  // Non-redirect Response thrown from action — this will be treated
158
209
  // as a regular error and routed to the error boundary. Warn in dev
@@ -165,9 +216,18 @@ export async function executeServerAction<TEnv>(
165
216
  `Use \`throw redirect('/path')\` for redirects.`,
166
217
  );
167
218
  }
219
+
220
+ // A raw Response cannot be serialized into Flight; storing it as the
221
+ // action returnValue.data would make the error payload serialization
222
+ // throw and mask the boundary render. Replace it with a serializable
223
+ // error (mirrors the discard of a returned non-redirect Response above).
224
+ // matchError/onError still receive the original Response.
225
+ actionResultData = new Error(
226
+ `Server action "${actionId}" threw a non-redirect Response (status ${error.status})`,
227
+ );
168
228
  }
169
229
 
170
- returnValue = { ok: false, data: error };
230
+ returnValue = { ok: false, data: actionResultData };
171
231
  actionStatus = 500;
172
232
 
173
233
  // Try to render error boundary.
@@ -202,50 +262,46 @@ export async function executeServerAction<TEnv>(
202
262
  });
203
263
 
204
264
  if (errorResult) {
205
- setRequestContextParams(errorResult.params, errorResult.routeName);
206
-
207
- const payload: RscPayload = {
208
- metadata: {
209
- pathname: url.pathname,
210
- segments: errorResult.segments,
211
- isPartial: true,
212
- matched: errorResult.matched,
213
- diff: errorResult.diff,
214
- isError: true,
215
- handles: handleStore.stream(),
216
- version: ctx.version,
217
- },
265
+ // Defer the error-boundary render to the revalidation phase so it runs
266
+ // inside the same route-middleware wrapper as a successful revalidation
267
+ // (handler.ts executeRenderWithMiddleware). Building + returning the
268
+ // Response here would bypass route middleware: context vars, headers, and
269
+ // cookies set by route middleware would NOT apply to the error render,
270
+ // diverging from the success path and from the module doc's "identical to
271
+ // a normal render". The boundary is already matched; the render in
272
+ // revalidateAfterAction uses errorResult directly (no matchPartial).
273
+ return {
218
274
  returnValue,
219
- };
220
-
221
- // Intentionally omit attachLocationState for error payloads:
222
- // location state is a success-only semantic. Error boundary responses
223
- // update the error UI but should not mutate browser history state.
224
-
225
- const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
275
+ actionStatus,
226
276
  temporaryReferences,
227
- });
228
-
229
- return createResponseWithMergedHeaders(rscStream, {
230
- status: actionStatus,
231
- headers: { "content-type": "text/x-component;charset=utf-8" },
232
- });
277
+ // actionContext is unused on the errorBoundary path (no matchPartial).
278
+ actionContext: {
279
+ actionId,
280
+ actionUrl: new URL(url),
281
+ actionResult: returnValue.data,
282
+ formData: actionFormData,
283
+ },
284
+ errorBoundary: errorResult,
285
+ };
233
286
  }
234
287
  }
235
288
 
236
289
  // Build continuation for the revalidation phase
237
- const resolvedActionId =
238
- (loadedAction as { $id?: string; $$id?: string } | undefined)?.$id ??
239
- (loadedAction as { $$id?: string } | undefined)?.$$id ??
240
- actionId;
290
+ const actionMeta = loadedAction as
291
+ | { $id?: string; $$id?: string }
292
+ | undefined;
293
+ const resolvedActionId = actionMeta?.$id ?? actionMeta?.$$id ?? actionId;
241
294
 
242
295
  return {
243
296
  returnValue,
244
297
  actionStatus,
245
298
  temporaryReferences,
246
299
  actionContext: {
300
+ // Defensive copy of the already-parsed url (avoids re-parsing
301
+ // request.url). actionUrl is persisted into the continuation and later
302
+ // flows into matchPartial, so it must not alias the handler's live url.
247
303
  actionId: resolvedActionId,
248
- actionUrl: new URL(request.url),
304
+ actionUrl: new URL(url),
249
305
  actionResult: returnValue.data,
250
306
  formData: actionFormData,
251
307
  },
@@ -264,16 +320,103 @@ export async function executeServerAction<TEnv>(
264
320
  * provide. Redirects are the only non-partial outcome and are handled via
265
321
  * X-RSC-Redirect headers before Flight deserialization.
266
322
  */
267
- export async function revalidateAfterAction<TEnv>(
323
+ export function revalidateAfterAction<TEnv>(
268
324
  ctx: HandlerContext<TEnv>,
269
325
  request: Request,
270
326
  env: TEnv,
271
327
  url: URL,
272
- handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
328
+ handleStore: ReturnType<typeof getRequestContext>["_handleStore"],
273
329
  continuation: ActionContinuation,
274
330
  ): Promise<Response> {
275
- const { returnValue, actionStatus, temporaryReferences, actionContext } =
276
- continuation;
331
+ // Instrument the action-revalidation render through the unified phase API,
332
+ // exactly like a normal navigation render (handleRscRendering). It records
333
+ // "render:total" AND opens "rango.render" from one boundary covering
334
+ // matchPartial -> serialize, so the revalidation loaders' rango.loader spans
335
+ // nest under a rango.render parent instead of dangling at the request root.
336
+ return observePhase(PHASES.render, () =>
337
+ revalidateAfterActionInner(
338
+ ctx,
339
+ request,
340
+ env,
341
+ url,
342
+ handleStore,
343
+ continuation,
344
+ ),
345
+ );
346
+ }
347
+
348
+ async function revalidateAfterActionInner<TEnv>(
349
+ ctx: HandlerContext<TEnv>,
350
+ request: Request,
351
+ env: TEnv,
352
+ url: URL,
353
+ handleStore: ReturnType<typeof getRequestContext>["_handleStore"],
354
+ continuation: ActionContinuation,
355
+ ): Promise<Response> {
356
+ const {
357
+ returnValue,
358
+ actionStatus,
359
+ temporaryReferences,
360
+ actionContext,
361
+ errorBoundary,
362
+ } = continuation;
363
+ const reqCtx = getRequestContext();
364
+ const metricsStore = reqCtx._metricsStore;
365
+
366
+ // Action threw and a boundary matched: render the (already-matched) error
367
+ // boundary here so it runs inside the route-middleware wrapper, exactly like
368
+ // the success branch below. setRequestContextParams + the payload mirror the
369
+ // pre-deferral render that executeServerAction used to do inline.
370
+ if (errorBoundary) {
371
+ setRequestContextParams(errorBoundary.params, errorBoundary.routeName);
372
+
373
+ const errorPayload: RscPayload = {
374
+ metadata: {
375
+ pathname: url.pathname,
376
+ // routerId exposed for the frontend (current app identity); see
377
+ // rsc-rendering.ts partial branch.
378
+ routerId: ctx.router.id,
379
+ segments: errorBoundary.segments,
380
+ isPartial: true,
381
+ matched: errorBoundary.matched,
382
+ diff: errorBoundary.diff,
383
+ resolvedIds: errorBoundary.resolvedIds,
384
+ params: errorBoundary.params,
385
+ isError: true,
386
+ handles: handleStore.stream(),
387
+ version: ctx.version,
388
+ },
389
+ returnValue,
390
+ };
391
+
392
+ // Intentionally omit attachLocationState for error payloads: location state
393
+ // is a success-only semantic. Error boundary responses update the error UI
394
+ // but should not mutate browser history state.
395
+
396
+ const errorStart = performance.now();
397
+ const errorStream = ctx.renderToReadableStream<RscPayload>(errorPayload, {
398
+ temporaryReferences,
399
+ onError: (error: unknown) => {
400
+ ctx.callOnError(error, "rendering", { request, url, env });
401
+ },
402
+ });
403
+ appendMetric(
404
+ metricsStore,
405
+ "rsc-serialize",
406
+ errorStart,
407
+ performance.now() - errorStart,
408
+ );
409
+
410
+ return createResponseWithMergedHeaders(errorStream, {
411
+ status: actionStatus,
412
+ headers: {
413
+ "content-type": "text/x-component;charset=utf-8",
414
+ // Router identity for the client's pre-decode integrity check (the
415
+ // action apply path has no post-decode guard). See response-adapter.
416
+ "X-RSC-Router-Id": ctx.router.id,
417
+ },
418
+ });
419
+ }
277
420
 
278
421
  const matchResult = await ctx.router.matchPartial(
279
422
  request,
@@ -282,8 +425,8 @@ export async function revalidateAfterAction<TEnv>(
282
425
  );
283
426
 
284
427
  if (!matchResult) {
285
- // matchPartial returns null when the route is a redirect or the request
286
- // is missing required headers (previousUrl). Check for redirect first.
428
+ // matchPartial returns null when the route is a redirect or no previous-URL
429
+ // context could be resolved. Check for redirect first.
287
430
  const fullMatch = await ctx.router.match(request, { env });
288
431
  setRequestContextParams(fullMatch.params, fullMatch.routeName);
289
432
 
@@ -294,29 +437,35 @@ export async function revalidateAfterAction<TEnv>(
294
437
  return createSimpleRedirectResponse(fullMatch.redirect);
295
438
  }
296
439
 
297
- // Non-redirect: this branch is only reachable when the action request
298
- // is missing the X-RSC-Router-Client-Path header (defensive). The
299
- // client requires isPartial for action responses, so producing a full
300
- // payload here would be rejected. Return 500 instead.
440
+ // Non-redirect: this branch is only reachable when no previous URL could
441
+ // be resolved (neither X-RSC-Router-Client-Path nor a usable Referer), or
442
+ // the previous URL was unparseable (defensive). The client requires
443
+ // isPartial for action responses, so producing a full payload here would
444
+ // be rejected. Return 500 instead.
301
445
  throw new Error(
302
446
  `[RSC] matchPartial returned null for a non-redirect route ` +
303
447
  `during action revalidation (${url.pathname}). This indicates ` +
304
- `a malformed action request (missing X-RSC-Router-Client-Path header).`,
448
+ `a malformed action request: no previous-URL context could be ` +
449
+ `resolved (neither X-RSC-Router-Client-Path nor a usable Referer), ` +
450
+ `or the previous URL was unparseable.`,
305
451
  );
306
452
  }
307
453
 
308
454
  // Return updated segments
309
455
  setRequestContextParams(matchResult.params, matchResult.routeName);
310
456
 
311
- const serverTiming = matchResult.serverTiming;
312
-
313
457
  const payload: RscPayload = {
314
458
  metadata: {
315
459
  pathname: url.pathname,
460
+ // routerId exposed for the frontend (current app identity); see
461
+ // rsc-rendering.ts partial branch.
462
+ routerId: ctx.router.id,
316
463
  segments: matchResult.segments,
317
464
  isPartial: true,
318
465
  matched: matchResult.matched,
319
466
  diff: matchResult.diff,
467
+ resolvedIds: matchResult.resolvedIds,
468
+ params: matchResult.params,
320
469
  slots: matchResult.slots,
321
470
  handles: handleStore.stream(),
322
471
  version: ctx.version,
@@ -324,21 +473,27 @@ export async function revalidateAfterAction<TEnv>(
324
473
  returnValue,
325
474
  };
326
475
 
327
- attachLocationState(payload);
476
+ attachLocationStateIfPresent(payload);
328
477
 
478
+ const renderStart = performance.now();
329
479
  const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
330
480
  temporaryReferences,
481
+ onError: (error: unknown) => {
482
+ ctx.callOnError(error, "rendering", { request, url, env });
483
+ },
331
484
  });
332
-
333
- const actionHeaders: Record<string, string> = {
334
- "content-type": "text/x-component;charset=utf-8",
335
- };
336
- if (serverTiming) {
337
- actionHeaders["Server-Timing"] = serverTiming;
338
- }
485
+ const rscSerializeDur = performance.now() - renderStart;
486
+ // This measures synchronous stream creation, not end-to-end stream consumption.
487
+ // render:total is recorded by the observePhase wrapper in revalidateAfterAction.
488
+ appendMetric(metricsStore, "rsc-serialize", renderStart, rscSerializeDur);
339
489
 
340
490
  return createResponseWithMergedHeaders(rscStream, {
341
491
  status: actionStatus,
342
- headers: actionHeaders,
492
+ headers: {
493
+ "content-type": "text/x-component;charset=utf-8",
494
+ // Router identity for the client's pre-decode integrity check (the action
495
+ // apply path has no post-decode guard). See response-adapter.
496
+ "X-RSC-Router-Id": ctx.router.id,
497
+ },
343
498
  });
344
499
  }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * SSR Setup Utilities
3
+ *
4
+ * Manages early kickoff and retrieval of SSR module loading and stream mode
5
+ * resolution. Both operations are request-scoped but independent of route
6
+ * matching, so they can run in parallel with segment resolution.
7
+ */
8
+
9
+ import type { HandlerContext } from "./handler-context.js";
10
+ import type { SSRModule } from "./types.js";
11
+ import type { SSRStreamMode } from "../router/router-options.js";
12
+ import type { MetricsStore } from "../server/context.js";
13
+ import { appendMetric } from "../router/metrics.js";
14
+ import { _getRequestContext } from "../server/request-context.js";
15
+
16
+ export type SSRSetup = readonly [SSRModule, SSRStreamMode];
17
+
18
+ /**
19
+ * Key used to stash the early SSR setup promise on request variables.
20
+ * Read back via `getSSRSetup`.
21
+ */
22
+ export const SSR_SETUP_VAR = "__ssrSetup";
23
+
24
+ /**
25
+ * Start loading the SSR module and resolving the stream mode in parallel.
26
+ * When a `getMetricsStore` getter is provided, records individual
27
+ * `ssr:module-load` and `ssr:stream-mode` metrics (the getter is called
28
+ * lazily so stores created after kickoff are still captured). Without a
29
+ * getter the promises run bare — no `.then()` microtasks, no
30
+ * `performance.now()` calls — keeping the non-debug hot path lean.
31
+ */
32
+ export function startSSRSetup<TEnv>(
33
+ ctx: HandlerContext<TEnv>,
34
+ request: Request,
35
+ env: TEnv,
36
+ url: URL,
37
+ getMetricsStore?: () => MetricsStore | undefined,
38
+ ): Promise<SSRSetup> {
39
+ if (!getMetricsStore) {
40
+ return Promise.all([
41
+ ctx.loadSSRModule(),
42
+ ctx.resolveStreamMode(request, env, url),
43
+ ]);
44
+ }
45
+ const start = performance.now();
46
+ return Promise.all([
47
+ ctx.loadSSRModule().then((mod) => {
48
+ appendMetric(
49
+ getMetricsStore(),
50
+ "ssr:module-load",
51
+ start,
52
+ performance.now() - start,
53
+ );
54
+ return mod;
55
+ }),
56
+ ctx.resolveStreamMode(request, env, url).then((mode) => {
57
+ appendMetric(
58
+ getMetricsStore(),
59
+ "ssr:stream-mode",
60
+ start,
61
+ performance.now() - start,
62
+ );
63
+ return mode;
64
+ }),
65
+ ]);
66
+ }
67
+
68
+ /**
69
+ * Retrieve the SSR setup result. Returns the early-kicked-off promise
70
+ * when available (stashed on request variables), otherwise starts a
71
+ * fresh setup.
72
+ */
73
+ export function getSSRSetup<TEnv>(
74
+ ctx: HandlerContext<TEnv>,
75
+ request: Request,
76
+ env: TEnv,
77
+ url: URL,
78
+ metricsStore: MetricsStore | undefined,
79
+ ): Promise<SSRSetup> {
80
+ const early = _getRequestContext()?._variables?.[SSR_SETUP_VAR] as
81
+ | Promise<SSRSetup>
82
+ | undefined;
83
+ if (early) return early;
84
+ return startSSRSetup(
85
+ ctx,
86
+ request,
87
+ env,
88
+ url,
89
+ metricsStore ? () => metricsStore : undefined,
90
+ );
91
+ }
92
+
93
+ /**
94
+ * Classify whether a request may require SSR (HTML rendering).
95
+ *
96
+ * Returns false for requests that are definitively RSC-only, loader fetches,
97
+ * prerender collection, or Accept-based RSC (no text/html). This mirrors
98
+ * the isRscRequest decision in rsc-rendering.ts.
99
+ *
100
+ * Note: response/mime routes are excluded by the caller — this function
101
+ * runs after classifyRequest() determines the request mode.
102
+ */
103
+ export function mayNeedSSR(request: Request, url: URL): boolean {
104
+ if (
105
+ url.searchParams.has("_rsc_partial") ||
106
+ url.searchParams.has("_rsc_action") ||
107
+ request.headers.has("rsc-action") ||
108
+ url.searchParams.has("_rsc_loader") ||
109
+ url.searchParams.has("__rsc") ||
110
+ url.searchParams.has("__prerender_collect")
111
+ ) {
112
+ return false;
113
+ }
114
+
115
+ // Mirror the Accept-based RSC decision from rsc-rendering.ts:
116
+ // if Accept is present and does not include text/html (and no __html override),
117
+ // the response will be RSC, not HTML.
118
+ const accept = request.headers.get("accept");
119
+ if (
120
+ accept &&
121
+ !accept.includes("text/html") &&
122
+ !url.searchParams.has("__html")
123
+ ) {
124
+ return false;
125
+ }
126
+
127
+ return true;
128
+ }
129
+
130
+ // Final render-time decision: is the response an RSC stream (vs HTML)? Distinct
131
+ // from mayNeedSSR, which is a conservative pre-classifier (it treats a missing
132
+ // Accept header as needing SSR; this treats it as RSC).
133
+ export function isRscRequest(
134
+ request: Request,
135
+ url: URL,
136
+ isPartial: boolean,
137
+ ): boolean {
138
+ return (
139
+ isPartial ||
140
+ (!request.headers.get("accept")?.includes("text/html") &&
141
+ !url.searchParams.has("__html")) ||
142
+ url.searchParams.has("__rsc")
143
+ );
144
+ }