@rangojs/router 0.0.0-experimental.a769fbe7 → 0.0.0-experimental.ac99d918

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 (374) hide show
  1. package/README.md +211 -43
  2. package/dist/bin/rango.js +279 -102
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +3313 -1160
  5. package/dist/vite/index.js.bak +5448 -0
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +62 -11
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +63 -1
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +222 -30
  12. package/skills/caching/SKILL.md +263 -8
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +364 -0
  17. package/skills/hooks/SKILL.md +250 -30
  18. package/skills/host-router/SKILL.md +124 -22
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +49 -5
  21. package/skills/layout/SKILL.md +35 -9
  22. package/skills/links/SKILL.md +249 -17
  23. package/skills/loader/SKILL.md +223 -9
  24. package/skills/middleware/SKILL.md +52 -13
  25. package/skills/migrate-nextjs/SKILL.md +584 -0
  26. package/skills/migrate-react-router/SKILL.md +769 -0
  27. package/skills/mime-routes/SKILL.md +27 -0
  28. package/skills/observability/SKILL.md +137 -0
  29. package/skills/parallel/SKILL.md +77 -7
  30. package/skills/prerender/SKILL.md +123 -100
  31. package/skills/rango/SKILL.md +250 -22
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +122 -47
  34. package/skills/route/SKILL.md +66 -5
  35. package/skills/router-setup/SKILL.md +38 -3
  36. package/skills/server-actions/SKILL.md +775 -0
  37. package/skills/streams-and-websockets/SKILL.md +283 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +129 -0
  40. package/skills/testing/bindings.md +89 -0
  41. package/skills/testing/cache-prerender.md +124 -0
  42. package/skills/testing/client-components.md +122 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +92 -0
  45. package/skills/testing/handles.md +129 -0
  46. package/skills/testing/loader.md +128 -0
  47. package/skills/testing/middleware.md +99 -0
  48. package/skills/testing/render-handler.md +121 -0
  49. package/skills/testing/response-routes.md +95 -0
  50. package/skills/testing/reverse-and-types.md +84 -0
  51. package/skills/testing/server-actions.md +107 -0
  52. package/skills/testing/server-tree.md +128 -0
  53. package/skills/testing/setup.md +120 -0
  54. package/skills/typesafety/SKILL.md +319 -27
  55. package/skills/use-cache/SKILL.md +36 -5
  56. package/skills/vercel/SKILL.md +107 -0
  57. package/skills/view-transitions/SKILL.md +294 -0
  58. package/src/__augment-tests__/augment.ts +81 -0
  59. package/src/__augment-tests__/augmented.check.ts +116 -0
  60. package/src/__internal.ts +1 -66
  61. package/src/browser/action-coordinator.ts +53 -36
  62. package/src/browser/action-fence.ts +47 -0
  63. package/src/browser/app-shell.ts +39 -0
  64. package/src/browser/app-version.ts +14 -0
  65. package/src/browser/cookie-name.ts +140 -0
  66. package/src/browser/event-controller.ts +81 -147
  67. package/src/browser/history-state.ts +21 -0
  68. package/src/browser/index.ts +3 -3
  69. package/src/browser/invalidate-client-cache.ts +52 -0
  70. package/src/browser/navigation-bridge.ts +66 -14
  71. package/src/browser/navigation-client.ts +172 -109
  72. package/src/browser/navigation-store-handle.ts +38 -0
  73. package/src/browser/navigation-store.ts +76 -67
  74. package/src/browser/navigation-transaction.ts +9 -59
  75. package/src/browser/partial-update.ts +79 -93
  76. package/src/browser/prefetch/cache.ts +180 -62
  77. package/src/browser/prefetch/fetch.ts +252 -39
  78. package/src/browser/prefetch/queue.ts +42 -8
  79. package/src/browser/rango-state.ts +158 -76
  80. package/src/browser/react/Link.tsx +72 -10
  81. package/src/browser/react/NavigationProvider.tsx +83 -31
  82. package/src/browser/react/ScrollRestoration.tsx +10 -6
  83. package/src/browser/react/context.ts +7 -2
  84. package/src/browser/react/filter-segment-order.ts +49 -7
  85. package/src/browser/react/index.ts +0 -48
  86. package/src/browser/react/location-state-shared.ts +166 -8
  87. package/src/browser/react/location-state.ts +39 -14
  88. package/src/browser/react/use-action.ts +6 -15
  89. package/src/browser/react/use-handle.ts +23 -69
  90. package/src/browser/react/use-link-status.ts +0 -4
  91. package/src/browser/react/use-navigation.ts +22 -5
  92. package/src/browser/react/use-params.ts +20 -10
  93. package/src/browser/react/use-reverse.ts +106 -0
  94. package/src/browser/react/use-router.ts +46 -11
  95. package/src/browser/react/use-search-params.ts +0 -5
  96. package/src/browser/react/use-segments.ts +11 -21
  97. package/src/browser/response-adapter.ts +52 -1
  98. package/src/browser/rsc-router.tsx +111 -24
  99. package/src/browser/scroll-restoration.ts +29 -19
  100. package/src/browser/segment-reconciler.ts +36 -14
  101. package/src/browser/segment-structure-assert.ts +2 -2
  102. package/src/browser/server-action-bridge.ts +176 -62
  103. package/src/browser/types.ts +60 -11
  104. package/src/browser/validate-redirect-origin.ts +43 -16
  105. package/src/build/collect-fallback-refs.ts +107 -0
  106. package/src/build/generate-manifest.ts +65 -40
  107. package/src/build/generate-route-types.ts +6 -0
  108. package/src/build/index.ts +8 -2
  109. package/src/build/prefix-tree-utils.ts +123 -0
  110. package/src/build/route-trie.ts +137 -32
  111. package/src/build/route-types/codegen.ts +4 -4
  112. package/src/build/route-types/include-resolution.ts +9 -2
  113. package/src/build/route-types/param-extraction.ts +6 -3
  114. package/src/build/route-types/per-module-writer.ts +7 -4
  115. package/src/build/route-types/router-processing.ts +333 -94
  116. package/src/build/route-types/scan-filter.ts +9 -2
  117. package/src/build/route-types/source-scan.ts +118 -0
  118. package/src/build/runtime-discovery.ts +9 -20
  119. package/src/cache/cache-error.ts +104 -0
  120. package/src/cache/cache-policy.ts +68 -28
  121. package/src/cache/cache-runtime.ts +134 -32
  122. package/src/cache/cache-scope.ts +100 -74
  123. package/src/cache/cache-tag.ts +98 -0
  124. package/src/cache/cf/cf-cache-store.ts +2256 -241
  125. package/src/cache/cf/index.ts +6 -16
  126. package/src/cache/document-cache.ts +61 -20
  127. package/src/cache/handle-snapshot.ts +63 -0
  128. package/src/cache/index.ts +22 -20
  129. package/src/cache/memory-segment-store.ts +136 -37
  130. package/src/cache/profile-registry.ts +6 -30
  131. package/src/cache/read-through-swr.ts +41 -11
  132. package/src/cache/segment-codec.ts +0 -16
  133. package/src/cache/tag-invalidation.ts +230 -0
  134. package/src/cache/types.ts +33 -100
  135. package/src/cache/vercel/index.ts +11 -0
  136. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  137. package/src/client.rsc.tsx +6 -21
  138. package/src/client.tsx +108 -290
  139. package/src/component-utils.ts +19 -0
  140. package/src/context-var.ts +17 -5
  141. package/src/decode-loader-results.ts +36 -0
  142. package/src/defer.ts +196 -0
  143. package/src/deps/browser.ts +0 -1
  144. package/src/deps/ssr.ts +0 -1
  145. package/src/errors.ts +30 -4
  146. package/src/handle.ts +70 -22
  147. package/src/handles/MetaTags.tsx +0 -14
  148. package/src/handles/breadcrumbs.ts +16 -5
  149. package/src/handles/meta.ts +0 -39
  150. package/src/host/cookie-handler.ts +0 -36
  151. package/src/host/errors.ts +0 -24
  152. package/src/host/index.ts +8 -2
  153. package/src/host/pattern-matcher.ts +7 -50
  154. package/src/host/router.ts +107 -99
  155. package/src/host/testing.ts +40 -27
  156. package/src/host/types.ts +37 -4
  157. package/src/host/utils.ts +1 -1
  158. package/src/href-client.ts +137 -22
  159. package/src/index.rsc.ts +69 -10
  160. package/src/index.ts +112 -14
  161. package/src/internal-debug.ts +2 -4
  162. package/src/loader-store.ts +500 -0
  163. package/src/loader.rsc.ts +20 -13
  164. package/src/loader.ts +12 -11
  165. package/src/missing-id-error.ts +68 -0
  166. package/src/network-error-thrower.tsx +1 -6
  167. package/src/outlet-context.ts +1 -1
  168. package/src/outlet-provider.tsx +1 -5
  169. package/src/prerender/param-hash.ts +10 -11
  170. package/src/prerender/store.ts +37 -41
  171. package/src/prerender.ts +198 -82
  172. package/src/redirect-origin.ts +100 -0
  173. package/src/response-utils.ts +37 -0
  174. package/src/reverse.ts +65 -15
  175. package/src/root-error-boundary.tsx +1 -19
  176. package/src/route-content-wrapper.tsx +7 -72
  177. package/src/route-definition/dsl-helpers.ts +413 -275
  178. package/src/route-definition/helper-factories.ts +29 -139
  179. package/src/route-definition/helpers-types.ts +107 -32
  180. package/src/route-definition/index.ts +3 -0
  181. package/src/route-definition/redirect.ts +50 -8
  182. package/src/route-definition/resolve-handler-use.ts +161 -0
  183. package/src/route-definition/use-item-types.ts +32 -0
  184. package/src/route-map-builder.ts +0 -16
  185. package/src/route-types.ts +37 -41
  186. package/src/router/basename.ts +14 -0
  187. package/src/router/content-negotiation.ts +108 -9
  188. package/src/router/error-handling.ts +13 -17
  189. package/src/router/find-match.ts +44 -23
  190. package/src/router/handler-context.ts +46 -30
  191. package/src/router/intercept-resolution.ts +23 -23
  192. package/src/router/lazy-includes.ts +15 -52
  193. package/src/router/loader-resolution.ts +207 -30
  194. package/src/router/logging.ts +0 -6
  195. package/src/router/manifest.ts +40 -42
  196. package/src/router/match-api.ts +120 -204
  197. package/src/router/match-context.ts +0 -22
  198. package/src/router/match-handlers.ts +58 -58
  199. package/src/router/match-middleware/background-revalidation.ts +0 -7
  200. package/src/router/match-middleware/cache-lookup.ts +161 -262
  201. package/src/router/match-middleware/cache-store.ts +3 -33
  202. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  203. package/src/router/match-middleware/segment-resolution.ts +45 -14
  204. package/src/router/match-pipelines.ts +1 -42
  205. package/src/router/match-result.ts +87 -39
  206. package/src/router/metrics.ts +0 -34
  207. package/src/router/middleware-types.ts +7 -140
  208. package/src/router/middleware.ts +169 -140
  209. package/src/router/navigation-snapshot.ts +131 -0
  210. package/src/router/params-util.ts +23 -0
  211. package/src/router/pattern-matching.ts +109 -63
  212. package/src/router/prerender-match.ts +190 -54
  213. package/src/router/preview-match.ts +32 -102
  214. package/src/router/request-classification.ts +276 -0
  215. package/src/router/revalidation.ts +63 -55
  216. package/src/router/route-snapshot.ts +244 -0
  217. package/src/router/router-context.ts +0 -27
  218. package/src/router/router-interfaces.ts +100 -35
  219. package/src/router/router-options.ts +91 -11
  220. package/src/router/router-registry.ts +2 -5
  221. package/src/router/segment-resolution/fresh.ts +119 -65
  222. package/src/router/segment-resolution/helpers.ts +34 -0
  223. package/src/router/segment-resolution/loader-cache.ts +40 -37
  224. package/src/router/segment-resolution/revalidation.ts +329 -305
  225. package/src/router/segment-resolution/static-store.ts +19 -5
  226. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  227. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  228. package/src/router/segment-resolution.ts +4 -1
  229. package/src/router/segment-wrappers.ts +0 -3
  230. package/src/router/state-cookie-name.ts +33 -0
  231. package/src/router/substitute-pattern-params.ts +56 -0
  232. package/src/router/telemetry-otel.ts +0 -20
  233. package/src/router/telemetry.ts +96 -19
  234. package/src/router/timeout.ts +0 -20
  235. package/src/router/trie-matching.ts +91 -46
  236. package/src/router/types.ts +9 -63
  237. package/src/router/url-params.ts +44 -0
  238. package/src/router.ts +128 -42
  239. package/src/rsc/handler-context.ts +3 -2
  240. package/src/rsc/handler.ts +492 -409
  241. package/src/rsc/helpers.ts +162 -46
  242. package/src/rsc/index.ts +1 -1
  243. package/src/rsc/json-route-result.ts +38 -0
  244. package/src/rsc/loader-fetch.ts +18 -3
  245. package/src/rsc/manifest-init.ts +33 -42
  246. package/src/rsc/origin-guard.ts +39 -25
  247. package/src/rsc/progressive-enhancement.ts +28 -4
  248. package/src/rsc/redirect-guard.ts +99 -0
  249. package/src/rsc/response-error.ts +79 -12
  250. package/src/rsc/response-route-handler.ts +90 -63
  251. package/src/rsc/rsc-rendering.ts +53 -56
  252. package/src/rsc/runtime-warnings.ts +23 -10
  253. package/src/rsc/server-action.ts +74 -69
  254. package/src/rsc/ssr-setup.ts +18 -2
  255. package/src/rsc/types.ts +22 -9
  256. package/src/runtime-env.ts +18 -0
  257. package/src/search-params.ts +4 -20
  258. package/src/segment-content-promise.ts +67 -0
  259. package/src/segment-loader-promise.ts +134 -0
  260. package/src/segment-system.tsx +208 -201
  261. package/src/serialize.ts +243 -0
  262. package/src/server/context.ts +211 -52
  263. package/src/server/cookie-store.ts +80 -5
  264. package/src/server/handle-store.ts +26 -24
  265. package/src/server/loader-registry.ts +10 -28
  266. package/src/server/request-context.ts +289 -124
  267. package/src/ssr/index.tsx +22 -15
  268. package/src/static-handler.ts +27 -18
  269. package/src/testing/cache-status.ts +162 -0
  270. package/src/testing/collect-handle.ts +40 -0
  271. package/src/testing/dispatch.ts +618 -0
  272. package/src/testing/dom.entry.ts +22 -0
  273. package/src/testing/e2e/fixture.ts +188 -0
  274. package/src/testing/e2e/index.ts +128 -0
  275. package/src/testing/e2e/matchers.ts +35 -0
  276. package/src/testing/e2e/page-helpers.ts +272 -0
  277. package/src/testing/e2e/parity.ts +387 -0
  278. package/src/testing/e2e/server.ts +195 -0
  279. package/src/testing/flight-matchers.ts +97 -0
  280. package/src/testing/flight-normalize.ts +11 -0
  281. package/src/testing/flight-runtime.d.ts +57 -0
  282. package/src/testing/flight-tree.ts +682 -0
  283. package/src/testing/flight.entry.ts +52 -0
  284. package/src/testing/flight.ts +232 -0
  285. package/src/testing/generated-routes.ts +183 -0
  286. package/src/testing/index.ts +99 -0
  287. package/src/testing/internal/context.ts +348 -0
  288. package/src/testing/internal/flight-client-globals.ts +30 -0
  289. package/src/testing/internal/seed-vars.ts +54 -0
  290. package/src/testing/render-handler.ts +330 -0
  291. package/src/testing/render-route.tsx +566 -0
  292. package/src/testing/run-loader.ts +378 -0
  293. package/src/testing/run-middleware.ts +205 -0
  294. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  295. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  296. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  297. package/src/testing/vitest-stubs/version.ts +5 -0
  298. package/src/testing/vitest.ts +305 -0
  299. package/src/theme/ThemeProvider.tsx +0 -52
  300. package/src/theme/ThemeScript.tsx +0 -6
  301. package/src/theme/constants.ts +0 -12
  302. package/src/theme/index.ts +0 -7
  303. package/src/theme/theme-context.ts +1 -5
  304. package/src/theme/theme-script.ts +0 -14
  305. package/src/theme/use-theme.ts +0 -3
  306. package/src/types/boundaries.ts +0 -35
  307. package/src/types/cache-types.ts +17 -8
  308. package/src/types/error-types.ts +30 -90
  309. package/src/types/global-namespace.ts +54 -41
  310. package/src/types/handler-context.ts +124 -70
  311. package/src/types/index.ts +1 -10
  312. package/src/types/loader-types.ts +40 -11
  313. package/src/types/request-scope.ts +107 -0
  314. package/src/types/route-config.ts +6 -50
  315. package/src/types/route-entry.ts +12 -7
  316. package/src/types/segments.ts +36 -15
  317. package/src/urls/include-helper.ts +33 -70
  318. package/src/urls/index.ts +1 -11
  319. package/src/urls/path-helper-types.ts +58 -11
  320. package/src/urls/path-helper.ts +57 -111
  321. package/src/urls/pattern-types.ts +48 -19
  322. package/src/urls/response-types.ts +25 -22
  323. package/src/urls/type-extraction.ts +58 -139
  324. package/src/urls/urls-function.ts +1 -18
  325. package/src/use-loader.tsx +346 -89
  326. package/src/vite/debug.ts +185 -0
  327. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  328. package/src/vite/discovery/discover-routers.ts +130 -85
  329. package/src/vite/discovery/discovery-errors.ts +194 -0
  330. package/src/vite/discovery/gate-state.ts +171 -0
  331. package/src/vite/discovery/prerender-collection.ts +192 -99
  332. package/src/vite/discovery/route-types-writer.ts +40 -84
  333. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  334. package/src/vite/discovery/state.ts +51 -4
  335. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  336. package/src/vite/index.ts +8 -0
  337. package/src/vite/plugin-types.ts +236 -6
  338. package/src/vite/plugins/cjs-to-esm.ts +8 -18
  339. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  340. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  341. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  342. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  343. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  344. package/src/vite/plugins/expose-action-id.ts +48 -95
  345. package/src/vite/plugins/expose-id-utils.ts +11 -50
  346. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  347. package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
  348. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  349. package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
  350. package/src/vite/plugins/expose-internal-ids.ts +554 -317
  351. package/src/vite/plugins/performance-tracks.ts +65 -207
  352. package/src/vite/plugins/refresh-cmd.ts +89 -27
  353. package/src/vite/plugins/use-cache-transform.ts +73 -83
  354. package/src/vite/plugins/vercel-output.ts +258 -0
  355. package/src/vite/plugins/version-injector.ts +21 -25
  356. package/src/vite/plugins/version-plugin.ts +32 -23
  357. package/src/vite/plugins/virtual-entries.ts +46 -17
  358. package/src/vite/rango.ts +207 -125
  359. package/src/vite/router-discovery.ts +931 -133
  360. package/src/vite/utils/ast-handler-extract.ts +15 -31
  361. package/src/vite/utils/banner.ts +1 -1
  362. package/src/vite/utils/bundle-analysis.ts +10 -15
  363. package/src/vite/utils/client-chunks.ts +184 -0
  364. package/src/vite/utils/forward-user-plugins.ts +171 -0
  365. package/src/vite/utils/manifest-utils.ts +4 -59
  366. package/src/vite/utils/package-resolution.ts +20 -52
  367. package/src/vite/utils/prerender-utils.ts +27 -29
  368. package/src/vite/utils/shared-utils.ts +97 -44
  369. package/src/browser/action-response-classifier.ts +0 -99
  370. package/src/browser/debug-channel.ts +0 -93
  371. package/src/browser/react/use-client-cache.ts +0 -58
  372. package/src/browser/shallow.ts +0 -40
  373. package/src/handles/index.ts +0 -7
  374. package/src/router/middleware-cookies.ts +0 -55
@@ -8,21 +8,17 @@
8
8
  */
9
9
 
10
10
  import { createElement } from "react";
11
- import { RouteNotFoundError } from "../errors.js";
11
+ import { isRouteNotFoundError } from "../errors.js";
12
12
  import { matchMiddleware, executeMiddleware } from "../router/middleware.js";
13
13
  import {
14
14
  runWithRequestContext,
15
15
  setRequestContextParams,
16
16
  requireRequestContext,
17
17
  getRequestContext,
18
+ _getRequestContext,
18
19
  createRequestContext,
19
20
  } from "../server/request-context.js";
20
21
  import * as rscDeps from "@vitejs/plugin-rsc/rsc";
21
- import {
22
- DEBUG_ID_HEADER,
23
- createServerDebugChannel,
24
- } from "../vite/plugins/performance-tracks.js";
25
-
26
22
  import type {
27
23
  RscPayload,
28
24
  CreateRSCHandlerOptions,
@@ -35,6 +31,8 @@ import {
35
31
  interceptRedirectForPartial,
36
32
  buildRouteMiddlewareEntries,
37
33
  } from "./helpers.js";
34
+ import { guardOutgoingRedirect } from "./redirect-guard.js";
35
+ import { isWebSocketUpgradeResponse } from "../response-utils.js";
38
36
  import {
39
37
  handleResponseRoute,
40
38
  type ResponseRouteMatch,
@@ -60,6 +58,8 @@ import {
60
58
  getRouterTrie,
61
59
  } from "../route-map-builder.js";
62
60
  import type { HandlerContext } from "./handler-context.js";
61
+ import type { CacheErrorCategory } from "../cache/cache-error.js";
62
+ import type { SegmentCacheStore } from "../cache/types.js";
63
63
  import { buildRouterTrieFromUrlpatterns } from "./manifest-init.js";
64
64
  import { handleProgressiveEnhancement } from "./progressive-enhancement.js";
65
65
  import {
@@ -68,7 +68,10 @@ import {
68
68
  type ActionContinuation,
69
69
  } from "./server-action.js";
70
70
  import { handleLoaderFetch } from "./loader-fetch.js";
71
- import { checkRequestOrigin, type OriginCheckPhase } from "./origin-guard.js";
71
+ import {
72
+ checkRequestOrigin,
73
+ ORIGIN_CHECK_PHASE_BY_MODE,
74
+ } from "./origin-guard.js";
72
75
  import { handleRscRendering } from "./rsc-rendering.js";
73
76
  import {
74
77
  withTimeout,
@@ -85,8 +88,14 @@ import {
85
88
  startSSRSetup,
86
89
  getSSRSetup,
87
90
  mayNeedSSR,
91
+ isRscRequest,
88
92
  SSR_SETUP_VAR,
89
93
  } from "./ssr-setup.js";
94
+ import {
95
+ classifyRequest,
96
+ type RequestPlan,
97
+ type ExecutableRequestPlan,
98
+ } from "../router/request-classification.js";
90
99
 
91
100
  /**
92
101
  * Create an RSC request handler.
@@ -121,12 +130,35 @@ import {
121
130
  * });
122
131
  * ```
123
132
  */
133
+
134
+ /**
135
+ * Response that tells the client to do a full document navigation. Shared by
136
+ * the terminal reload plans (version-mismatch and app-switch): an empty 200
137
+ * carrying X-RSC-Reload, which the client turns into window.location.href.
138
+ */
139
+ function createReloadResponse(reloadUrl: string) {
140
+ return createResponseWithMergedHeaders(null, {
141
+ status: 200,
142
+ headers: {
143
+ "X-RSC-Reload": reloadUrl,
144
+ "content-type": "text/x-component;charset=utf-8",
145
+ },
146
+ });
147
+ }
148
+
124
149
  export function createRSCHandler<
125
150
  TEnv = unknown,
126
151
  TRoutes extends Record<string, string> = Record<string, string>,
127
152
  >(options: CreateRSCHandlerOptions<TEnv, TRoutes>) {
128
153
  const { router, version = VERSION, nonce: nonceProvider } = options;
129
154
 
155
+ // Handler-owned registry of explicit per-scope stores from cache({ store }).
156
+ // Lives in the closure so it is scoped per handler (multi-router deployments
157
+ // get separate registries) and accumulates every explicit store this handler
158
+ // resolves across requests. updateTag()/revalidateTag() iterate it to reach
159
+ // stores not covered by the app-level ctx._cacheStore.
160
+ const explicitTaggedStores = new Set<SegmentCacheStore>();
161
+
130
162
  // Use provided deps or default to @vitejs/plugin-rsc/rsc exports
131
163
  const deps = options.deps ?? rscDeps;
132
164
  const {
@@ -166,10 +198,13 @@ export function createRSCHandler<
166
198
  phase: ErrorPhase,
167
199
  context: Parameters<typeof invokeOnError<TEnv>>[3],
168
200
  ): void {
169
- if (error != null && typeof error === "object") {
170
- const reportedErrors = requireRequestContext()._reportedErrors;
171
- if (reportedErrors.has(error)) return;
172
- reportedErrors.add(error);
201
+ // Guard: abort signal handlers fire asynchronously outside the ALS
202
+ // request scope, so the context may be gone. Skip dedup in that
203
+ // case — the error is from a cancelled stream, not a real failure.
204
+ const reqCtx = _getRequestContext();
205
+ if (error != null && typeof error === "object" && reqCtx) {
206
+ if (reqCtx._reportedErrors.has(error)) return;
207
+ reqCtx._reportedErrors.add(error);
173
208
  }
174
209
  invokeOnError(router.onError, error, phase, context, "RSC");
175
210
  }
@@ -258,19 +293,17 @@ export function createRSCHandler<
258
293
  function createRedirectFlightResponse(
259
294
  redirectUrl: string,
260
295
  locationState?: Record<string, unknown>,
296
+ external?: boolean,
261
297
  ): Response {
262
298
  const redirectPayload: RscPayload = {
263
299
  metadata: {
264
300
  pathname: redirectUrl,
265
301
  segments: [],
266
- redirect: { url: redirectUrl },
302
+ redirect: { url: redirectUrl, ...(external && { external: true }) },
267
303
  ...(locationState && { locationState }),
268
304
  },
269
305
  };
270
- const debugChannel = getRequestContext()?._debugChannel;
271
- const rscStream = renderToReadableStream<RscPayload>(redirectPayload, {
272
- ...(debugChannel && { debugChannel }),
273
- });
306
+ const rscStream = renderToReadableStream<RscPayload>(redirectPayload);
274
307
  return createResponseWithMergedHeaders(rscStream, {
275
308
  status: 200,
276
309
  headers: { "content-type": "text/x-component;charset=utf-8" },
@@ -351,7 +384,7 @@ export function createRSCHandler<
351
384
  // Resolve cache store configuration
352
385
  // Priority: options.cache (handler override) > router.cache (router default)
353
386
  // Store is enabled only if: config provided, enabled, and no ?__no_cache query param
354
- let cacheStore = undefined;
387
+ let cacheStore: SegmentCacheStore | undefined;
355
388
  const cacheOption = options.cache ?? router.cache;
356
389
  if (cacheOption && !url.searchParams.has("__no_cache")) {
357
390
  const cacheConfig =
@@ -418,34 +451,22 @@ export function createRSCHandler<
418
451
  url,
419
452
  variables,
420
453
  cacheStore,
454
+ explicitTaggedStores,
421
455
  cacheProfiles: router.cacheProfiles,
422
456
  executionContext: executionCtx,
423
457
  themeConfig: router.themeConfig,
458
+ stateCookieName: router.resolvedStateCookieName,
459
+ version,
424
460
  });
425
461
  if (earlyMetricsStore) {
426
462
  requestContext._debugPerformance = true;
427
463
  requestContext._metricsStore = earlyMetricsStore;
428
464
  }
429
- // Dev-only: wire debug channel for React Performance Tracks
430
- if (process.env.NODE_ENV !== "production") {
431
- const debugId = request.headers.get(DEBUG_ID_HEADER);
432
- console.log("[perf-tracks] handler: debugId header =", debugId);
433
- if (debugId) {
434
- const channel = createServerDebugChannel(debugId);
435
- console.log(
436
- "[perf-tracks] handler: channel =",
437
- channel ? "created" : "NOT FOUND",
438
- );
439
- if (channel) {
440
- requestContext._debugChannel = channel;
441
- }
442
- }
443
- }
444
465
  // Wire background error reporting so "use cache" and other subsystems
445
466
  // can surface non-fatal errors through the router's onError callback.
446
467
  requestContext._reportBackgroundError = (
447
468
  error: unknown,
448
- category: string,
469
+ category: CacheErrorCategory,
449
470
  ) => {
450
471
  callOnError(error, "cache", {
451
472
  request,
@@ -475,6 +496,9 @@ export function createRSCHandler<
475
496
  // - Server components during rendering
476
497
  // - Error boundaries
477
498
  // - Streaming
499
+ // Store basename on request context (scoped per-request via existing ALS)
500
+ requestContext._basename = router.basename;
501
+
478
502
  return runWithRequestContext(requestContext, async () => {
479
503
  // Core handler logic (wrapped by middleware)
480
504
  const coreHandler = async (): Promise<Response> => {
@@ -544,13 +568,22 @@ export function createRSCHandler<
544
568
  }
545
569
 
546
570
  const fullTiming = timingParts.join(", ");
547
- if (fullTiming) response.headers.set("Server-Timing", fullTiming);
571
+ if (fullTiming && !isWebSocketUpgradeResponse(response)) {
572
+ response.headers.set("Server-Timing", fullTiming);
573
+ }
548
574
 
549
- return response;
575
+ // Single open-redirect chokepoint: every response (PE, full-page,
576
+ // middleware short-circuit, response-route) funnels through here, so
577
+ // guarding browser-followed (3xx) redirects once covers them all and any
578
+ // future redirect exit. Soft SPA/Flight redirects are 200/204 and pass
579
+ // through untouched (validated client-side instead).
580
+ return guardOutgoingRedirect(response, url.origin, router.basename);
550
581
  });
551
582
  };
552
583
 
553
- // Core request handling logic (separated for middleware wrapping)
584
+ // Core request handling logic (separated for middleware wrapping).
585
+ // Uses the classify → execute model: classifyRequest produces a RequestPlan,
586
+ // then execution dispatches on the plan mode.
554
587
  async function coreRequestHandler(
555
588
  request: Request,
556
589
  env: TEnv,
@@ -558,71 +591,103 @@ export function createRSCHandler<
558
591
  variables: Record<string, any>,
559
592
  nonce: string | undefined,
560
593
  ): Promise<Response> {
561
- const previewStart = performance.now();
562
- const preview = await router.previewMatch(request, { env });
563
- const previewDur = performance.now() - previewStart;
564
594
  const handlerTiming: string[] = variables.__handlerTiming || [];
565
- handlerTiming.push(`handler-preview-match;dur=${previewDur.toFixed(2)}`);
566
- // Response route short-circuit: skip entire RSC pipeline
567
- if (preview?.responseType && preview.handler) {
568
- const responseOutcome = await withTimeout(
569
- handleResponseRoute(
570
- handlerCtx,
571
- preview as ResponseRouteMatch,
572
- request,
573
- env,
574
- url,
575
- variables,
595
+
596
+ // Debug manifest endpoint: handled before classification since it
597
+ // doesn't need a route match and needs trie access from the closure.
598
+ const isDev = process.env.NODE_ENV !== "production";
599
+ if (
600
+ url.searchParams.has("__debug_manifest") &&
601
+ (isDev || router.allowDebugManifest)
602
+ ) {
603
+ const trie = getRouterTrie(router.id) ?? getRouteTrie();
604
+ const routeManifest = getRequiredRouteMap();
605
+ const { extractAncestryFromTrie } =
606
+ await import("../build/route-trie.js");
607
+ return new Response(
608
+ JSON.stringify(
609
+ {
610
+ routerId: router.id,
611
+ routeManifest,
612
+ routeAncestry: trie ? extractAncestryFromTrie(trie) : {},
613
+ routeTrie: trie,
614
+ precomputedEntries: getPrecomputedEntries(),
615
+ },
616
+ null,
617
+ 2,
576
618
  ),
577
- router.timeouts.renderStartMs,
578
- "render-start",
619
+ {
620
+ headers: { "Content-Type": "application/json" },
621
+ },
579
622
  );
580
- if (responseOutcome.timedOut) {
581
- return handleTimeoutResponse(
582
- request,
583
- env,
584
- url,
585
- "render-start",
586
- responseOutcome.durationMs,
587
- preview?.routeKey,
588
- );
623
+ }
624
+
625
+ // ---- 1. Classify ----
626
+ // classifyRequest may throw RouteNotFoundError for unknown routes.
627
+ // In that case, fall through to a full-render plan so the pipeline
628
+ // can render the 404 page via the existing error handling path.
629
+ const classifyStart = performance.now();
630
+ let plan: RequestPlan<TEnv>;
631
+ try {
632
+ plan = await classifyRequest<TEnv>(request, url, {
633
+ findMatch: router.findMatch,
634
+ routerVersion: version,
635
+ routerId: router.id,
636
+ });
637
+ } catch (error) {
638
+ if (isRouteNotFoundError(error)) {
639
+ // Let the render path handle 404 — match()/matchPartial() will
640
+ // re-throw RouteNotFoundError and the catch block in
641
+ // executeRenderWithMiddleware renders the not-found page.
642
+ plan = {
643
+ mode: "full-render",
644
+ route: {
645
+ matched: null as any,
646
+ manifestEntry: null as any,
647
+ entries: [],
648
+ routeKey: "",
649
+ localRouteName: "",
650
+ params: {},
651
+ routeMiddleware: [],
652
+ cacheScope: null,
653
+ isPassthrough: false,
654
+ },
655
+ negotiated: false,
656
+ };
657
+ } else {
658
+ throw error;
589
659
  }
590
- return responseOutcome.result;
660
+ }
661
+ const classifyDur = performance.now() - classifyStart;
662
+ handlerTiming.push(`handler-classify;dur=${classifyDur.toFixed(2)}`);
663
+
664
+ // ---- 2. Terminal plans (no execution needed) ----
665
+ if (plan.mode === "redirect") {
666
+ // Redirects are handled by the pipeline (match/matchPartial),
667
+ // but for partial requests we short-circuit with a Flight redirect.
668
+ if (url.searchParams.has("_rsc_partial")) {
669
+ return createRedirectFlightResponse(plan.redirectUrl);
670
+ }
671
+ // Full requests: let the pipeline handle the redirect via match()
672
+ // which returns { redirect: url }. Fall through to full-render.
591
673
  }
592
674
 
593
- // Kick off SSR module loading + stream mode resolution in parallel with
594
- // segment resolution. Placed after the response-route short-circuit so
595
- // response/mime routes never pay for SSR work.
596
- if (mayNeedSSR(request, url)) {
597
- variables[SSR_SETUP_VAR] = startSSRSetup(
598
- handlerCtx,
599
- request,
600
- env,
601
- url,
602
- router.debugPerformance
603
- ? () => requireRequestContext()._metricsStore
604
- : undefined,
675
+ if (plan.mode === "version-mismatch") {
676
+ console.log(
677
+ `[RSC] Version mismatch: client=${url.searchParams.get("_rsc_v")}, server=${version}. Forcing reload.`,
605
678
  );
679
+ return createReloadResponse(plan.reloadUrl);
606
680
  }
607
681
 
608
- const routeReverse = createReverseFunction(getRequiredRouteMap());
682
+ if (plan.mode === "app-switch") {
683
+ // Cross-app SPA navigation crossed a host-router app boundary. Force a
684
+ // real document navigation so the target app's document is re-established
685
+ // (stylesheets, theme, warmup, prefetch-TTL). See request-classification.
686
+ return createReloadResponse(plan.reloadUrl);
687
+ }
609
688
 
610
- const isAction =
611
- request.headers.has("rsc-action") || url.searchParams.has("_rsc_action");
612
- const isLoaderFetch = url.searchParams.has("_rsc_loader");
613
- const actionId =
614
- request.headers.get("rsc-action") || url.searchParams.get("_rsc_action");
615
-
616
- // Origin guard: reject cross-origin actions, loader fetches, and
617
- // PE form submissions before any execution. Regular page navigations
618
- // (GET without _rsc_loader/_rsc_action) are not affected.
619
- const originPhase: OriginCheckPhase | null = isAction
620
- ? "action"
621
- : isLoaderFetch
622
- ? "loader"
623
- : request.method === "POST"
624
- ? "pe-form"
625
- : null;
689
+ // ---- 3. Origin guard (gate for action/loader/PE modes) ----
690
+ const originPhase = ORIGIN_CHECK_PHASE_BY_MODE[plan.mode];
626
691
  if (originPhase) {
627
692
  const originResult = await checkRequestOrigin(
628
693
  request,
@@ -672,13 +737,33 @@ export function createRSCHandler<
672
737
  }
673
738
  }
674
739
 
675
- // Get handle store from request context
740
+ // ---- 4. Execute ----
741
+ return executeRequest(
742
+ plan as ExecutableRequestPlan<TEnv>,
743
+ request,
744
+ env,
745
+ url,
746
+ variables,
747
+ nonce,
748
+ );
749
+ }
750
+
751
+ // Execute a classified request plan. Dispatches to the appropriate handler
752
+ // based on plan.mode. Lives in the createRSCHandler closure for access to
753
+ // handlerCtx, router, callOnError, etc.
754
+ // Only receives executable plans (version-mismatch is handled above).
755
+ async function executeRequest(
756
+ plan: ExecutableRequestPlan<TEnv>,
757
+ request: Request,
758
+ env: TEnv,
759
+ url: URL,
760
+ variables: Record<string, any>,
761
+ nonce: string | undefined,
762
+ ): Promise<Response> {
763
+ // Common setup
676
764
  const handleStore = requireRequestContext()._handleStore;
677
765
 
678
766
  // Wire up error reporting for late streaming-handle failures
679
- // (LateHandlePushError: handle pushed after stream completion).
680
- // Without this, these errors are only caught by React's error boundary
681
- // and never reach the router's onError callback or telemetry.
682
767
  handleStore.onError = (error: Error) => {
683
768
  const reqCtx = requireRequestContext();
684
769
  callOnError(error, "handler", {
@@ -708,37 +793,106 @@ export function createRSCHandler<
708
793
  };
709
794
 
710
795
  // Set route params early so all execution paths can access ctx.params.
711
- if (preview?.params) {
712
- setRequestContextParams(preview.params, preview.routeKey);
796
+ // Also store the classified snapshot so match/matchPartial can reuse it
797
+ // instead of calling resolveRoute again.
798
+ if (plan.mode !== "redirect") {
799
+ setRequestContextParams(plan.route.params, plan.route.routeKey);
800
+ requireRequestContext()._classifiedRoute = plan.route;
713
801
  }
714
802
 
715
- // Progressive enhancement runs before the normal action/render paths.
716
- // Route middleware wraps the PE re-render so handlers see the same
717
- // context variables regardless of JS/no-JS transport.
718
- const progressiveResult = await handleProgressiveEnhancement(
719
- handlerCtx,
720
- request,
721
- env,
722
- url,
723
- isAction,
724
- handleStore,
725
- nonce,
726
- {
727
- routeMiddleware: preview?.routeMiddleware,
803
+ const routeReverse = createReverseFunction(getRequiredRouteMap());
804
+
805
+ // ---- Response route: skip entire RSC pipeline ----
806
+ if (plan.mode === "response") {
807
+ // Build ResponseRouteMatch from plan fields. handleResponseRoute
808
+ // expects a flat object with params at the top level.
809
+ const responseMatch: ResponseRouteMatch = {
810
+ responseType: plan.responseType,
811
+ handler: plan.handler,
812
+ params: plan.route.params,
813
+ negotiated: plan.negotiated,
814
+ manifestEntry: plan.manifestEntry,
815
+ routeMiddleware: plan.routeMiddleware,
816
+ };
817
+ const responseOutcome = await withTimeout(
818
+ handleResponseRoute(
819
+ handlerCtx,
820
+ responseMatch,
821
+ request,
822
+ env,
823
+ url,
824
+ variables,
825
+ ),
826
+ router.timeouts.renderStartMs,
827
+ "render-start",
828
+ );
829
+ if (responseOutcome.timedOut) {
830
+ return handleTimeoutResponse(
831
+ request,
832
+ env,
833
+ url,
834
+ "render-start",
835
+ responseOutcome.durationMs,
836
+ plan.route.routeKey,
837
+ );
838
+ }
839
+ const response = responseOutcome.result;
840
+ if (plan.negotiated && !isWebSocketUpgradeResponse(response)) {
841
+ response.headers.append("Vary", "Accept");
842
+ }
843
+ return response;
844
+ }
845
+
846
+ // SSR setup: kick off in parallel for modes that need HTML rendering.
847
+ // Placed after response-route short-circuit so response/mime routes
848
+ // never pay for SSR work.
849
+ if (plan.mode !== "loader" && mayNeedSSR(request, url)) {
850
+ variables[SSR_SETUP_VAR] = startSSRSetup(
851
+ handlerCtx,
852
+ request,
853
+ env,
854
+ url,
855
+ router.debugPerformance
856
+ ? () => requireRequestContext()._metricsStore
857
+ : undefined,
858
+ );
859
+ }
860
+
861
+ // ---- Loader fetch ----
862
+ if (plan.mode === "loader") {
863
+ return handleLoaderFetch(
864
+ handlerCtx,
865
+ request,
866
+ env,
867
+ url,
728
868
  variables,
729
- routeReverse,
730
- },
731
- );
732
- if (progressiveResult) {
733
- return progressiveResult;
869
+ plan.route.params,
870
+ );
871
+ }
872
+
873
+ // ---- Progressive enhancement ----
874
+ if (plan.mode === "pe-render") {
875
+ const peResult = await handleProgressiveEnhancement(
876
+ handlerCtx,
877
+ request,
878
+ env,
879
+ url,
880
+ false, // isAction = false for PE
881
+ handleStore,
882
+ nonce,
883
+ {
884
+ routeMiddleware: plan.route.routeMiddleware,
885
+ variables,
886
+ routeReverse,
887
+ },
888
+ );
889
+ if (peResult) return peResult;
890
+ // PE handler returned null (not a PE form) — fall through to render
734
891
  }
735
892
 
736
- // --- Action execution: runs BEFORE route middleware ---
737
- // Route middleware wraps rendering only. For actions, the action runs
738
- // first in the global middleware context, then route middleware wraps
739
- // the revalidation pass (identical to a normal render).
740
- let actionContinuation: ActionContinuation | undefined;
741
- if (isAction && actionId) {
893
+ // ---- Action: execute action, then revalidate wrapped in route middleware ----
894
+ if (plan.mode === "action") {
895
+ let actionContinuation: ActionContinuation | undefined;
742
896
  try {
743
897
  const actionOutcome = await withTimeout(
744
898
  executeServerAction(
@@ -746,7 +900,7 @@ export function createRSCHandler<
746
900
  request,
747
901
  env,
748
902
  url,
749
- actionId,
903
+ plan.actionId,
750
904
  handleStore,
751
905
  ),
752
906
  router.timeouts.actionMs,
@@ -759,8 +913,8 @@ export function createRSCHandler<
759
913
  url,
760
914
  "action",
761
915
  actionOutcome.durationMs,
762
- preview?.routeKey,
763
- actionId,
916
+ plan.route.routeKey,
917
+ plan.actionId,
764
918
  );
765
919
  }
766
920
  const result = actionOutcome.result;
@@ -772,40 +926,237 @@ export function createRSCHandler<
772
926
  request,
773
927
  url,
774
928
  env,
775
- actionId,
929
+ actionId: plan.actionId,
776
930
  handledByBoundary: false,
777
931
  });
778
932
  console.error(`[RSC] Action error:`, error);
779
933
  throw error;
780
934
  }
781
- }
782
935
 
783
- // --- Rendering (action revalidation or navigation) ---
784
- // Route middleware wraps this same code path for both cases.
785
- const renderHandler = async () => {
786
- const response = await coreRequestHandlerInner(
936
+ // Revalidation render wrapped in route middleware.
937
+ // Actions from client-side navigation include _rsc_partial preserve
938
+ // the partial flag so the revalidation returns a Flight stream, not HTML.
939
+ // App-switch is already excluded by classifyRequest (would be full-render).
940
+ const isPartialAction = url.searchParams.has("_rsc_partial");
941
+ return executeRenderWithMiddleware(
942
+ plan.route.routeMiddleware,
943
+ plan.negotiated,
944
+ plan.route.routeKey,
945
+ routeReverse,
787
946
  request,
788
947
  env,
789
948
  url,
790
949
  variables,
791
950
  nonce,
792
- preview?.params,
793
- preview?.routeKey,
794
951
  handleStore,
952
+ isPartialAction,
795
953
  actionContinuation,
796
954
  );
797
- if (preview?.negotiated) {
798
- response.headers.append("Vary", "Accept");
955
+ }
956
+
957
+ // Full render, partial render, fallen-through PE, and full-page redirect all
958
+ // render through the same middleware-wrapped path. Only full/partial-render
959
+ // carry negotiation + the partial flag; pe/redirect render plainly.
960
+ const isPartial = plan.mode === "partial-render";
961
+ const negotiated =
962
+ plan.mode === "full-render" || plan.mode === "partial-render"
963
+ ? plan.negotiated
964
+ : false;
965
+ return executeRenderWithMiddleware(
966
+ plan.route.routeMiddleware,
967
+ negotiated,
968
+ plan.route.routeKey,
969
+ routeReverse,
970
+ request,
971
+ env,
972
+ url,
973
+ variables,
974
+ nonce,
975
+ handleStore,
976
+ isPartial,
977
+ );
978
+ }
979
+
980
+ // Shared render execution: wraps handleRscRendering (or revalidateAfterAction)
981
+ // in route middleware and timeout handling. Consolidates the pattern used by
982
+ // action-revalidate, full-render, and partial-render modes.
983
+ async function executeRenderWithMiddleware(
984
+ routeMiddleware: import("../router/middleware-types.js").CollectedMiddleware[],
985
+ negotiated: boolean,
986
+ routeKey: string,
987
+ routeReverse: ReturnType<typeof createReverseFunction>,
988
+ request: Request,
989
+ env: TEnv,
990
+ url: URL,
991
+ variables: Record<string, any>,
992
+ nonce: string | undefined,
993
+ handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
994
+ isPartial: boolean,
995
+ actionContinuation?: ActionContinuation,
996
+ ): Promise<Response> {
997
+ const renderHandler = async (): Promise<Response> => {
998
+ try {
999
+ let response: Response;
1000
+ if (actionContinuation) {
1001
+ response = await revalidateAfterAction(
1002
+ handlerCtx,
1003
+ request,
1004
+ env,
1005
+ url,
1006
+ handleStore,
1007
+ actionContinuation,
1008
+ );
1009
+ } else {
1010
+ response = await handleRscRendering(
1011
+ handlerCtx,
1012
+ request,
1013
+ env,
1014
+ url,
1015
+ isPartial,
1016
+ handleStore,
1017
+ nonce,
1018
+ );
1019
+ }
1020
+ if (negotiated && !isWebSocketUpgradeResponse(response)) {
1021
+ response.headers.append("Vary", "Accept");
1022
+ }
1023
+ return response;
1024
+ } catch (error) {
1025
+ // Check if middleware/handler returned Response
1026
+ if (error instanceof Response) {
1027
+ // An action revalidation render is delivered to the client over the
1028
+ // same Flight-parsing path as a partial navigation, so a Response
1029
+ // thrown during it must be converted exactly like a partial one
1030
+ // (raw 200 -> hard-nav hint, 3xx -> Flight redirect). Without this,
1031
+ // the no-middleware path returns the raw Response (the with-middleware
1032
+ // path is already covered by the isPartial || actionContinuation
1033
+ // guard below).
1034
+ const treatAsPartial = isPartial || actionContinuation != null;
1035
+
1036
+ // During partial (client-side navigation), a 200 Response from a handler
1037
+ // means the route serves raw content (JSON, text, etc.), not JSX.
1038
+ // Signal the browser to hard-navigate so it renders the raw response.
1039
+ if (treatAsPartial && error.status === 200) {
1040
+ console.warn(
1041
+ `[RSC] Route handler at ${url.pathname} returned a Response during client-side navigation. ` +
1042
+ `Falling back to hard navigation. Use data-external on the <Link> to avoid the extra round-trip.`,
1043
+ );
1044
+ return createResponseWithMergedHeaders(null, {
1045
+ status: 200,
1046
+ headers: {
1047
+ "X-RSC-Reload": stripInternalParams(url).toString(),
1048
+ "content-type": "text/x-component;charset=utf-8",
1049
+ },
1050
+ });
1051
+ }
1052
+
1053
+ if (treatAsPartial) {
1054
+ const intercepted = interceptRedirectForPartial(
1055
+ error,
1056
+ createRedirectFlightResponse,
1057
+ );
1058
+ if (intercepted) return intercepted;
1059
+ }
1060
+
1061
+ return error;
1062
+ }
1063
+
1064
+ // Render 404 page for unmatched routes
1065
+ if (isRouteNotFoundError(error)) {
1066
+ callOnError(error, "routing", {
1067
+ request,
1068
+ url,
1069
+ env,
1070
+ handledByBoundary: true,
1071
+ });
1072
+
1073
+ const notFoundOption = router.notFound;
1074
+ const notFoundComponent =
1075
+ typeof notFoundOption === "function"
1076
+ ? notFoundOption({ pathname: url.pathname })
1077
+ : (notFoundOption ?? createElement("h1", null, "Not Found"));
1078
+
1079
+ const notFoundSegment = {
1080
+ id: "notFound",
1081
+ namespace: "notFound",
1082
+ type: "route" as const,
1083
+ index: 0,
1084
+ component: notFoundComponent,
1085
+ params: {},
1086
+ };
1087
+
1088
+ const payload: RscPayload = {
1089
+ metadata: {
1090
+ pathname: url.pathname,
1091
+ routerId: router.id,
1092
+ basename: router.basename,
1093
+ segments: [notFoundSegment],
1094
+ matched: [],
1095
+ diff: [],
1096
+ isPartial: false,
1097
+ rootLayout: router.rootLayout,
1098
+ handles: handleStore.stream(),
1099
+ version,
1100
+ stateCookieName: router.resolvedStateCookieName,
1101
+ themeConfig: router.themeConfig,
1102
+ warmupEnabled: router.warmupEnabled,
1103
+ initialTheme: requireRequestContext().theme,
1104
+ },
1105
+ };
1106
+
1107
+ const rscStream = renderToReadableStream(payload, {
1108
+ onError: (error: unknown) => {
1109
+ callOnError(error, "rendering", { request, url, env });
1110
+ },
1111
+ });
1112
+
1113
+ if (isRscRequest(request, url, isPartial)) {
1114
+ return createResponseWithMergedHeaders(rscStream, {
1115
+ status: 404,
1116
+ headers: {
1117
+ "content-type": "text/x-component;charset=utf-8",
1118
+ // Router identity for the client's pre-decode integrity check; a
1119
+ // same-app 404 matches and applies in place. See response-adapter.
1120
+ "X-RSC-Router-Id": router.id,
1121
+ },
1122
+ });
1123
+ }
1124
+
1125
+ const [ssrModule, streamMode] = await getSSRSetup(
1126
+ handlerCtx,
1127
+ request,
1128
+ env,
1129
+ url,
1130
+ requireRequestContext()._metricsStore,
1131
+ );
1132
+ const htmlStream = await ssrModule.renderHTML(rscStream, {
1133
+ nonce,
1134
+ streamMode,
1135
+ });
1136
+
1137
+ return createResponseWithMergedHeaders(htmlStream, {
1138
+ status: 404,
1139
+ headers: { "content-type": "text/html;charset=utf-8" },
1140
+ });
1141
+ }
1142
+
1143
+ // Report unhandled errors
1144
+ callOnError(error, "routing", {
1145
+ request,
1146
+ url,
1147
+ env,
1148
+ handledByBoundary: false,
1149
+ });
1150
+ console.error(`[RSC] Error:`, error);
1151
+ throw error;
799
1152
  }
800
- return response;
801
1153
  };
802
1154
 
803
- // Wrap the render path (with or without route middleware) in a
804
- // renderStartMs timeout so slow renders are caught before output.
1155
+ // Wrap the render path in a renderStartMs timeout
805
1156
  const executeRender = async (): Promise<Response> => {
806
- if (preview?.routeMiddleware && preview.routeMiddleware.length > 0) {
1157
+ if (routeMiddleware.length > 0) {
807
1158
  const mwResponse = await executeMiddleware(
808
- buildRouteMiddlewareEntries<TEnv>(preview.routeMiddleware),
1159
+ buildRouteMiddlewareEntries<TEnv>(routeMiddleware),
809
1160
  request,
810
1161
  env,
811
1162
  variables,
@@ -813,10 +1164,7 @@ export function createRSCHandler<
813
1164
  routeReverse,
814
1165
  );
815
1166
 
816
- if (
817
- url.searchParams.has("_rsc_partial") ||
818
- url.searchParams.has("_rsc_action")
819
- ) {
1167
+ if (isPartial || actionContinuation) {
820
1168
  const intercepted = interceptRedirectForPartial(
821
1169
  mwResponse,
822
1170
  createRedirectFlightResponse,
@@ -827,7 +1175,6 @@ export function createRSCHandler<
827
1175
  return finalizeResponse(mwResponse);
828
1176
  }
829
1177
 
830
- // No route middleware, proceed directly
831
1178
  return renderHandler();
832
1179
  };
833
1180
 
@@ -843,273 +1190,9 @@ export function createRSCHandler<
843
1190
  url,
844
1191
  "render-start",
845
1192
  renderOutcome.durationMs,
846
- preview?.routeKey,
1193
+ routeKey,
847
1194
  );
848
1195
  }
849
1196
  return renderOutcome.result;
850
1197
  }
851
-
852
- // Inner request handler: rendering logic wrapped by route middleware.
853
- // Handles action revalidation (when actionContinuation is present),
854
- // loader fetches, and regular RSC rendering.
855
- async function coreRequestHandlerInner(
856
- request: Request,
857
- env: TEnv,
858
- url: URL,
859
- variables: Record<string, any>,
860
- nonce: string | undefined,
861
- routeParams?: Record<string, string>,
862
- routeKey?: string,
863
- handleStore?: ReturnType<typeof requireRequestContext>["_handleStore"],
864
- actionContinuation?: ActionContinuation,
865
- ): Promise<Response> {
866
- const isPartial = url.searchParams.has("_rsc_partial");
867
- const isAction =
868
- request.headers.has("rsc-action") || url.searchParams.has("_rsc_action");
869
-
870
- // Version mismatch detection - client may have stale code after HMR/deployment
871
- // If versions don't match, tell the client to reload
872
- const clientVersion = url.searchParams.get("_rsc_v");
873
- if (version && clientVersion && clientVersion !== version) {
874
- console.log(
875
- `[RSC] Version mismatch: client=${clientVersion}, server=${version}. Forcing reload.`,
876
- );
877
-
878
- // For actions, reload current page (referer) if same origin.
879
- // For navigation, load the target URL.
880
- // Validate referer origin to prevent open redirect via crafted header.
881
- let reloadUrl = stripInternalParams(url).toString();
882
- if (isAction) {
883
- const referer = request.headers.get("referer");
884
- if (referer) {
885
- try {
886
- const refererUrl = new URL(referer);
887
- if (refererUrl.origin === url.origin) {
888
- reloadUrl = referer;
889
- }
890
- } catch {
891
- // Malformed referer, fall back to cleanUrl
892
- }
893
- }
894
- }
895
-
896
- // Return special response that tells client to reload
897
- return createResponseWithMergedHeaders(null, {
898
- status: 200,
899
- headers: {
900
- "X-RSC-Reload": reloadUrl,
901
- "content-type": "text/x-component;charset=utf-8",
902
- },
903
- });
904
- }
905
- // Debug manifest endpoint: ?__debug_manifest on any route.
906
- // Always available in dev, requires allowDebugManifest option in production.
907
- const isDev = process.env.NODE_ENV !== "production";
908
- if (
909
- url.searchParams.has("__debug_manifest") &&
910
- (isDev || router.allowDebugManifest)
911
- ) {
912
- const trie = getRouterTrie(router.id) ?? getRouteTrie();
913
- const routeManifest = getRequiredRouteMap();
914
- const { extractAncestryFromTrie } =
915
- await import("../build/route-trie.js");
916
- return new Response(
917
- JSON.stringify(
918
- {
919
- routerId: router.id,
920
- routeManifest,
921
- routeAncestry: trie ? extractAncestryFromTrie(trie) : {},
922
- routeTrie: trie,
923
- precomputedEntries: getPrecomputedEntries(),
924
- },
925
- null,
926
- 2,
927
- ),
928
- {
929
- headers: { "Content-Type": "application/json" },
930
- },
931
- );
932
- }
933
-
934
- const store = handleStore ?? requireRequestContext()._handleStore;
935
-
936
- try {
937
- // Route params were already set in coreRequestHandler, but set again
938
- // for callers that enter coreRequestHandlerInner directly.
939
- if (routeParams) {
940
- setRequestContextParams(routeParams, routeKey);
941
- }
942
-
943
- // ============================================================================
944
- // ACTION REVALIDATION (action already executed, revalidate segments)
945
- // ============================================================================
946
- if (actionContinuation) {
947
- return await revalidateAfterAction(
948
- handlerCtx,
949
- request,
950
- env,
951
- url,
952
- store,
953
- actionContinuation,
954
- );
955
- }
956
-
957
- // ============================================================================
958
- // LOADER FETCH EXECUTION (data fetching with RSC serialization)
959
- // ============================================================================
960
- const isLoaderRequest = url.searchParams.has("_rsc_loader");
961
- if (isLoaderRequest) {
962
- return handleLoaderFetch(
963
- handlerCtx,
964
- request,
965
- env,
966
- url,
967
- variables,
968
- routeParams,
969
- );
970
- }
971
-
972
- // ============================================================================
973
- // REGULAR RSC RENDERING (Navigation)
974
- // ============================================================================
975
- // Note: Must use "return await" for try/catch to catch async rejections
976
- return await handleRscRendering(
977
- handlerCtx,
978
- request,
979
- env,
980
- url,
981
- isPartial,
982
- store,
983
- nonce,
984
- );
985
- } catch (error) {
986
- // Check if middleware/handler returned Response
987
- if (error instanceof Response) {
988
- // During partial (client-side navigation), a 200 Response from a handler
989
- // means the route serves raw content (JSON, text, etc.), not JSX.
990
- // Signal the browser to hard-navigate so it renders the raw response.
991
- // Only for 200 — redirects (3xx) work already because the browser follows
992
- // them automatically to a URL that serves Flight data.
993
- if (isPartial && error.status === 200) {
994
- console.warn(
995
- `[RSC] Route handler at ${url.pathname} returned a Response during client-side navigation. ` +
996
- `Falling back to hard navigation. Use data-external on the <Link> to avoid the extra round-trip.`,
997
- );
998
- return createResponseWithMergedHeaders(null, {
999
- status: 200,
1000
- headers: {
1001
- "X-RSC-Reload": stripInternalParams(url).toString(),
1002
- "content-type": "text/x-component;charset=utf-8",
1003
- },
1004
- });
1005
- }
1006
-
1007
- if (isPartial) {
1008
- const intercepted = interceptRedirectForPartial(
1009
- error,
1010
- createRedirectFlightResponse,
1011
- );
1012
- if (intercepted) return intercepted;
1013
- }
1014
-
1015
- return error;
1016
- }
1017
-
1018
- // Render 404 page for unmatched routes
1019
- // Check both instanceof and error.name for cross-bundle compatibility
1020
- const isRouteNotFound =
1021
- error instanceof RouteNotFoundError ||
1022
- (error instanceof Error && error.name === "RouteNotFoundError");
1023
- if (isRouteNotFound) {
1024
- callOnError(error, "routing", {
1025
- request,
1026
- url,
1027
- env,
1028
- handledByBoundary: true, // Handled by notFound component
1029
- });
1030
-
1031
- // Get notFound component from router options or use default
1032
- const notFoundOption = router.notFound;
1033
- const notFoundComponent =
1034
- typeof notFoundOption === "function"
1035
- ? notFoundOption({ pathname: url.pathname })
1036
- : (notFoundOption ?? createElement("h1", null, "Not Found"));
1037
-
1038
- // Create a simple segment for the 404 page
1039
- const notFoundSegment = {
1040
- id: "notFound",
1041
- namespace: "notFound",
1042
- type: "route" as const,
1043
- index: 0,
1044
- component: notFoundComponent,
1045
- params: {},
1046
- };
1047
-
1048
- const payload: RscPayload = {
1049
- metadata: {
1050
- pathname: url.pathname,
1051
- segments: [notFoundSegment],
1052
- matched: [],
1053
- diff: [],
1054
- isPartial: false,
1055
- rootLayout: router.rootLayout,
1056
- handles: store.stream(),
1057
- version,
1058
- themeConfig: router.themeConfig,
1059
- warmupEnabled: router.warmupEnabled,
1060
- initialTheme: requireRequestContext().theme,
1061
- // No routeName for not-found routes
1062
- },
1063
- };
1064
-
1065
- const debugChannel = requireRequestContext()._debugChannel;
1066
- const rscStream = renderToReadableStream(payload, {
1067
- ...(debugChannel && { debugChannel }),
1068
- });
1069
-
1070
- // Determine if this is an RSC request or HTML request.
1071
- // Partial requests are always RSC (see main isRscRequest comment).
1072
- const isRscRequest =
1073
- isPartial ||
1074
- (!request.headers.get("accept")?.includes("text/html") &&
1075
- !url.searchParams.has("__html")) ||
1076
- url.searchParams.has("__rsc");
1077
-
1078
- if (isRscRequest) {
1079
- return createResponseWithMergedHeaders(rscStream, {
1080
- status: 404,
1081
- headers: { "content-type": "text/x-component;charset=utf-8" },
1082
- });
1083
- }
1084
-
1085
- // Delegate to SSR for HTML response (reuse early setup if available)
1086
- const [ssrModule, streamMode] = await getSSRSetup(
1087
- handlerCtx,
1088
- request,
1089
- env,
1090
- url,
1091
- requireRequestContext()._metricsStore,
1092
- );
1093
- const htmlStream = await ssrModule.renderHTML(rscStream, {
1094
- nonce,
1095
- streamMode,
1096
- });
1097
-
1098
- return createResponseWithMergedHeaders(htmlStream, {
1099
- status: 404,
1100
- headers: { "content-type": "text/html;charset=utf-8" },
1101
- });
1102
- }
1103
-
1104
- // Report unhandled errors
1105
- callOnError(error, "routing", {
1106
- request,
1107
- url,
1108
- env,
1109
- handledByBoundary: false,
1110
- });
1111
- console.error(`[RSC] Error:`, error);
1112
- throw error;
1113
- }
1114
- }
1115
1198
  }