@rangojs/router 0.0.0-experimental.9c9afef3 → 0.0.0-experimental.a014d2b7

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