@rangojs/router 0.0.0-experimental.3 → 0.0.0-experimental.30

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 (297) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +883 -4
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +4655 -747
  5. package/package.json +78 -50
  6. package/skills/cache-guide/SKILL.md +262 -0
  7. package/skills/caching/SKILL.md +54 -25
  8. package/skills/composability/SKILL.md +172 -0
  9. package/skills/debug-manifest/SKILL.md +12 -8
  10. package/skills/document-cache/SKILL.md +23 -21
  11. package/skills/fonts/SKILL.md +167 -0
  12. package/skills/hooks/SKILL.md +390 -63
  13. package/skills/host-router/SKILL.md +218 -0
  14. package/skills/intercept/SKILL.md +133 -10
  15. package/skills/layout/SKILL.md +102 -5
  16. package/skills/links/SKILL.md +239 -0
  17. package/skills/loader/SKILL.md +366 -29
  18. package/skills/middleware/SKILL.md +173 -36
  19. package/skills/mime-routes/SKILL.md +128 -0
  20. package/skills/parallel/SKILL.md +80 -3
  21. package/skills/prerender/SKILL.md +643 -0
  22. package/skills/rango/SKILL.md +86 -16
  23. package/skills/response-routes/SKILL.md +411 -0
  24. package/skills/route/SKILL.md +227 -14
  25. package/skills/router-setup/SKILL.md +225 -32
  26. package/skills/tailwind/SKILL.md +129 -0
  27. package/skills/theme/SKILL.md +12 -11
  28. package/skills/typesafety/SKILL.md +401 -75
  29. package/skills/use-cache/SKILL.md +324 -0
  30. package/src/__internal.ts +10 -4
  31. package/src/bin/rango.ts +321 -0
  32. package/src/browser/action-coordinator.ts +97 -0
  33. package/src/browser/action-response-classifier.ts +99 -0
  34. package/src/browser/event-controller.ts +87 -64
  35. package/src/browser/history-state.ts +80 -0
  36. package/src/browser/intercept-utils.ts +52 -0
  37. package/src/browser/link-interceptor.ts +20 -4
  38. package/src/browser/logging.ts +55 -0
  39. package/src/browser/merge-segment-loaders.ts +20 -12
  40. package/src/browser/navigation-bridge.ts +201 -553
  41. package/src/browser/navigation-client.ts +124 -71
  42. package/src/browser/navigation-store.ts +33 -50
  43. package/src/browser/navigation-transaction.ts +295 -0
  44. package/src/browser/network-error-handler.ts +61 -0
  45. package/src/browser/partial-update.ts +267 -317
  46. package/src/browser/prefetch/cache.ts +146 -0
  47. package/src/browser/prefetch/fetch.ts +135 -0
  48. package/src/browser/prefetch/observer.ts +65 -0
  49. package/src/browser/prefetch/policy.ts +42 -0
  50. package/src/browser/prefetch/queue.ts +88 -0
  51. package/src/browser/rango-state.ts +112 -0
  52. package/src/browser/react/Link.tsx +173 -73
  53. package/src/browser/react/NavigationProvider.tsx +138 -27
  54. package/src/browser/react/context.ts +6 -0
  55. package/src/browser/react/filter-segment-order.ts +11 -0
  56. package/src/browser/react/index.ts +12 -12
  57. package/src/browser/react/location-state-shared.ts +95 -53
  58. package/src/browser/react/location-state.ts +60 -15
  59. package/src/browser/react/mount-context.ts +37 -0
  60. package/src/browser/react/nonce-context.ts +23 -0
  61. package/src/browser/react/shallow-equal.ts +27 -0
  62. package/src/browser/react/use-action.ts +29 -51
  63. package/src/browser/react/use-client-cache.ts +5 -3
  64. package/src/browser/react/use-handle.ts +49 -65
  65. package/src/browser/react/use-href.tsx +20 -188
  66. package/src/browser/react/use-link-status.ts +6 -5
  67. package/src/browser/react/use-mount.ts +31 -0
  68. package/src/browser/react/use-navigation.ts +27 -78
  69. package/src/browser/react/use-params.ts +65 -0
  70. package/src/browser/react/use-pathname.ts +47 -0
  71. package/src/browser/react/use-router.ts +63 -0
  72. package/src/browser/react/use-search-params.ts +56 -0
  73. package/src/browser/react/use-segments.ts +80 -97
  74. package/src/browser/response-adapter.ts +73 -0
  75. package/src/browser/rsc-router.tsx +111 -26
  76. package/src/browser/scroll-restoration.ts +92 -16
  77. package/src/browser/segment-reconciler.ts +216 -0
  78. package/src/browser/segment-structure-assert.ts +83 -0
  79. package/src/browser/server-action-bridge.ts +504 -584
  80. package/src/browser/shallow.ts +6 -1
  81. package/src/browser/types.ts +92 -57
  82. package/src/browser/validate-redirect-origin.ts +29 -0
  83. package/src/build/generate-manifest.ts +438 -0
  84. package/src/build/generate-route-types.ts +36 -0
  85. package/src/build/index.ts +35 -0
  86. package/src/build/route-trie.ts +265 -0
  87. package/src/build/route-types/ast-helpers.ts +25 -0
  88. package/src/build/route-types/ast-route-extraction.ts +98 -0
  89. package/src/build/route-types/codegen.ts +102 -0
  90. package/src/build/route-types/include-resolution.ts +411 -0
  91. package/src/build/route-types/param-extraction.ts +48 -0
  92. package/src/build/route-types/per-module-writer.ts +128 -0
  93. package/src/build/route-types/router-processing.ts +469 -0
  94. package/src/build/route-types/scan-filter.ts +78 -0
  95. package/src/build/runtime-discovery.ts +231 -0
  96. package/src/cache/background-task.ts +34 -0
  97. package/src/cache/cache-key-utils.ts +44 -0
  98. package/src/cache/cache-policy.ts +125 -0
  99. package/src/cache/cache-runtime.ts +338 -0
  100. package/src/cache/cache-scope.ts +120 -303
  101. package/src/cache/cf/cf-cache-store.ts +119 -7
  102. package/src/cache/cf/index.ts +8 -2
  103. package/src/cache/document-cache.ts +101 -72
  104. package/src/cache/handle-capture.ts +81 -0
  105. package/src/cache/handle-snapshot.ts +41 -0
  106. package/src/cache/index.ts +0 -15
  107. package/src/cache/memory-segment-store.ts +191 -13
  108. package/src/cache/profile-registry.ts +73 -0
  109. package/src/cache/read-through-swr.ts +134 -0
  110. package/src/cache/segment-codec.ts +256 -0
  111. package/src/cache/taint.ts +98 -0
  112. package/src/cache/types.ts +72 -122
  113. package/src/client.rsc.tsx +10 -15
  114. package/src/client.tsx +114 -135
  115. package/src/component-utils.ts +4 -4
  116. package/src/components/DefaultDocument.tsx +5 -1
  117. package/src/context-var.ts +86 -0
  118. package/src/debug.ts +17 -7
  119. package/src/errors.ts +108 -2
  120. package/src/handle.ts +34 -19
  121. package/src/handles/MetaTags.tsx +73 -20
  122. package/src/handles/meta.ts +30 -13
  123. package/src/host/cookie-handler.ts +165 -0
  124. package/src/host/errors.ts +97 -0
  125. package/src/host/index.ts +53 -0
  126. package/src/host/pattern-matcher.ts +214 -0
  127. package/src/host/router.ts +352 -0
  128. package/src/host/testing.ts +79 -0
  129. package/src/host/types.ts +146 -0
  130. package/src/host/utils.ts +25 -0
  131. package/src/href-client.ts +135 -49
  132. package/src/index.rsc.ts +182 -17
  133. package/src/index.ts +238 -24
  134. package/src/internal-debug.ts +11 -0
  135. package/src/loader.rsc.ts +27 -142
  136. package/src/loader.ts +27 -10
  137. package/src/network-error-thrower.tsx +3 -1
  138. package/src/outlet-provider.tsx +45 -0
  139. package/src/prerender/param-hash.ts +37 -0
  140. package/src/prerender/store.ts +185 -0
  141. package/src/prerender.ts +463 -0
  142. package/src/reverse.ts +330 -0
  143. package/src/root-error-boundary.tsx +41 -29
  144. package/src/route-content-wrapper.tsx +9 -11
  145. package/src/route-definition/dsl-helpers.ts +934 -0
  146. package/src/route-definition/helper-factories.ts +200 -0
  147. package/src/route-definition/helpers-types.ts +430 -0
  148. package/src/route-definition/index.ts +52 -0
  149. package/src/route-definition/redirect.ts +93 -0
  150. package/src/route-definition.ts +1 -1388
  151. package/src/route-map-builder.ts +241 -112
  152. package/src/route-name.ts +53 -0
  153. package/src/route-types.ts +70 -9
  154. package/src/router/content-negotiation.ts +116 -0
  155. package/src/router/debug-manifest.ts +72 -0
  156. package/src/router/error-handling.ts +9 -9
  157. package/src/router/find-match.ts +158 -0
  158. package/src/router/handler-context.ts +371 -81
  159. package/src/router/intercept-resolution.ts +395 -0
  160. package/src/router/lazy-includes.ts +234 -0
  161. package/src/router/loader-resolution.ts +215 -122
  162. package/src/router/logging.ts +248 -0
  163. package/src/router/manifest.ts +155 -32
  164. package/src/router/match-api.ts +620 -0
  165. package/src/router/match-context.ts +5 -3
  166. package/src/router/match-handlers.ts +440 -0
  167. package/src/router/match-middleware/background-revalidation.ts +80 -93
  168. package/src/router/match-middleware/cache-lookup.ts +382 -9
  169. package/src/router/match-middleware/cache-store.ts +51 -22
  170. package/src/router/match-middleware/intercept-resolution.ts +55 -17
  171. package/src/router/match-middleware/segment-resolution.ts +24 -6
  172. package/src/router/match-pipelines.ts +10 -45
  173. package/src/router/match-result.ts +34 -29
  174. package/src/router/metrics.ts +235 -15
  175. package/src/router/middleware-cookies.ts +55 -0
  176. package/src/router/middleware-types.ts +222 -0
  177. package/src/router/middleware.ts +324 -367
  178. package/src/router/pattern-matching.ts +321 -30
  179. package/src/router/prerender-match.ts +400 -0
  180. package/src/router/preview-match.ts +170 -0
  181. package/src/router/revalidation.ts +137 -38
  182. package/src/router/router-context.ts +36 -21
  183. package/src/router/router-interfaces.ts +452 -0
  184. package/src/router/router-options.ts +592 -0
  185. package/src/router/router-registry.ts +24 -0
  186. package/src/router/segment-resolution/fresh.ts +570 -0
  187. package/src/router/segment-resolution/helpers.ts +263 -0
  188. package/src/router/segment-resolution/loader-cache.ts +198 -0
  189. package/src/router/segment-resolution/revalidation.ts +1241 -0
  190. package/src/router/segment-resolution/static-store.ts +67 -0
  191. package/src/router/segment-resolution.ts +21 -0
  192. package/src/router/segment-wrappers.ts +289 -0
  193. package/src/router/telemetry-otel.ts +299 -0
  194. package/src/router/telemetry.ts +300 -0
  195. package/src/router/timeout.ts +148 -0
  196. package/src/router/trie-matching.ts +239 -0
  197. package/src/router/types.ts +77 -3
  198. package/src/router.ts +688 -3656
  199. package/src/rsc/handler-context.ts +45 -0
  200. package/src/rsc/handler.ts +786 -760
  201. package/src/rsc/helpers.ts +140 -6
  202. package/src/rsc/index.ts +5 -25
  203. package/src/rsc/loader-fetch.ts +209 -0
  204. package/src/rsc/manifest-init.ts +86 -0
  205. package/src/rsc/nonce.ts +14 -0
  206. package/src/rsc/origin-guard.ts +141 -0
  207. package/src/rsc/progressive-enhancement.ts +379 -0
  208. package/src/rsc/response-error.ts +37 -0
  209. package/src/rsc/response-route-handler.ts +347 -0
  210. package/src/rsc/rsc-rendering.ts +235 -0
  211. package/src/rsc/runtime-warnings.ts +42 -0
  212. package/src/rsc/server-action.ts +348 -0
  213. package/src/rsc/ssr-setup.ts +128 -0
  214. package/src/rsc/types.ts +40 -14
  215. package/src/search-params.ts +230 -0
  216. package/src/segment-system.tsx +57 -61
  217. package/src/server/context.ts +202 -51
  218. package/src/server/cookie-store.ts +190 -0
  219. package/src/server/fetchable-loader-store.ts +37 -0
  220. package/src/server/handle-store.ts +94 -15
  221. package/src/server/loader-registry.ts +15 -56
  222. package/src/server/request-context.ts +422 -70
  223. package/src/server.ts +36 -120
  224. package/src/ssr/index.tsx +157 -26
  225. package/src/static-handler.ts +114 -0
  226. package/src/theme/ThemeProvider.tsx +21 -15
  227. package/src/theme/ThemeScript.tsx +5 -5
  228. package/src/theme/constants.ts +5 -2
  229. package/src/theme/index.ts +4 -14
  230. package/src/theme/theme-context.ts +4 -30
  231. package/src/theme/theme-script.ts +21 -18
  232. package/src/types/boundaries.ts +158 -0
  233. package/src/types/cache-types.ts +198 -0
  234. package/src/types/error-types.ts +192 -0
  235. package/src/types/global-namespace.ts +100 -0
  236. package/src/types/handler-context.ts +687 -0
  237. package/src/types/index.ts +88 -0
  238. package/src/types/loader-types.ts +183 -0
  239. package/src/types/route-config.ts +170 -0
  240. package/src/types/route-entry.ts +102 -0
  241. package/src/types/segments.ts +148 -0
  242. package/src/types.ts +1 -1577
  243. package/src/urls/include-helper.ts +197 -0
  244. package/src/urls/index.ts +53 -0
  245. package/src/urls/path-helper-types.ts +339 -0
  246. package/src/urls/path-helper.ts +329 -0
  247. package/src/urls/pattern-types.ts +95 -0
  248. package/src/urls/response-types.ts +106 -0
  249. package/src/urls/type-extraction.ts +372 -0
  250. package/src/urls/urls-function.ts +98 -0
  251. package/src/urls.ts +1 -726
  252. package/src/use-loader.tsx +85 -77
  253. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  254. package/src/vite/discovery/discover-routers.ts +344 -0
  255. package/src/vite/discovery/prerender-collection.ts +385 -0
  256. package/src/vite/discovery/route-types-writer.ts +258 -0
  257. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  258. package/src/vite/discovery/state.ts +110 -0
  259. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  260. package/src/vite/index.ts +11 -782
  261. package/src/vite/plugin-types.ts +131 -0
  262. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  263. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  264. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  265. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
  266. package/src/vite/plugins/expose-id-utils.ts +287 -0
  267. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  268. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  269. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  270. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  271. package/src/vite/plugins/expose-ids/types.ts +45 -0
  272. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  273. package/src/vite/plugins/refresh-cmd.ts +65 -0
  274. package/src/vite/plugins/use-cache-transform.ts +323 -0
  275. package/src/vite/plugins/version-injector.ts +83 -0
  276. package/src/vite/plugins/version-plugin.ts +254 -0
  277. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +29 -15
  278. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  279. package/src/vite/rango.ts +510 -0
  280. package/src/vite/router-discovery.ts +785 -0
  281. package/src/vite/utils/ast-handler-extract.ts +517 -0
  282. package/src/vite/utils/banner.ts +36 -0
  283. package/src/vite/utils/bundle-analysis.ts +137 -0
  284. package/src/vite/utils/manifest-utils.ts +70 -0
  285. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  286. package/src/vite/utils/prerender-utils.ts +189 -0
  287. package/src/vite/utils/shared-utils.ts +169 -0
  288. package/CLAUDE.md +0 -3
  289. package/src/browser/lru-cache.ts +0 -69
  290. package/src/browser/request-controller.ts +0 -164
  291. package/src/cache/memory-store.ts +0 -253
  292. package/src/href-context.ts +0 -33
  293. package/src/href.ts +0 -255
  294. package/src/vite/expose-handle-id.ts +0 -209
  295. package/src/vite/expose-loader-id.ts +0 -357
  296. package/src/vite/expose-location-state-id.ts +0 -177
  297. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -1,5 +1,5 @@
1
1
  /// <reference types="@vitejs/plugin-rsc/types" />
2
- /// <reference path="../vite/version.d.ts" />
2
+ /// <reference path="../vite/plugins/version.d.ts" />
3
3
  /**
4
4
  * RSC Request Handler
5
5
  *
@@ -8,40 +8,96 @@
8
8
  */
9
9
 
10
10
  import { createElement } from "react";
11
- import { renderSegments } from "../segment-system.js";
12
11
  import { RouteNotFoundError } from "../errors.js";
13
- import { getLoaderLazy } from "../server/loader-registry.js";
14
- import {
15
- matchMiddleware,
16
- executeMiddleware,
17
- executeLoaderMiddleware,
18
- } from "../router/middleware.js";
12
+ import { matchMiddleware, executeMiddleware } from "../router/middleware.js";
19
13
  import {
20
14
  runWithRequestContext,
21
15
  setRequestContextParams,
22
16
  requireRequestContext,
23
17
  createRequestContext,
24
- type ExecutionContext,
25
18
  } from "../server/request-context.js";
26
19
  import * as rscDeps from "@vitejs/plugin-rsc/rsc";
27
20
 
28
21
  import type {
29
22
  RscPayload,
30
- ReactFormState,
31
23
  CreateRSCHandlerOptions,
24
+ LoadSSRModule,
25
+ SSRModule,
32
26
  } from "./types.js";
33
- import { hasBodyContent, createResponseWithMergedHeaders } from "./helpers.js";
34
- import { generateNonce } from "./nonce.js";
27
+ import {
28
+ createResponseWithMergedHeaders,
29
+ finalizeResponse,
30
+ interceptRedirectForPartial,
31
+ buildRouteMiddlewareEntries,
32
+ } from "./helpers.js";
33
+ import {
34
+ handleResponseRoute,
35
+ type ResponseRouteMatch,
36
+ } from "./response-route-handler.js";
37
+ import { generateNonce, nonce as nonceToken } from "./nonce.js";
35
38
  import { VERSION } from "@rangojs/router:version";
36
39
  import type { ErrorPhase } from "../types.js";
40
+ import type { RouterRequestInput } from "../router/router-interfaces.js";
37
41
  import { invokeOnError } from "../router/error-handling.js";
42
+ import {
43
+ createReverseFunction,
44
+ stripInternalParams,
45
+ } from "../router/handler-context.js";
46
+ import { getRouterContext } from "../router/router-context.js";
47
+ import { resolveSink, safeEmit } from "../router/telemetry.js";
48
+ import { contextSet } from "../context-var.js";
49
+ import {
50
+ hasCachedManifest,
51
+ getRouteTrie,
52
+ getPrecomputedEntries,
53
+ waitForManifestReady,
54
+ getRouterManifest,
55
+ getRouterTrie,
56
+ } from "../route-map-builder.js";
57
+ import type { HandlerContext } from "./handler-context.js";
58
+ import { buildRouterTrieFromUrlpatterns } from "./manifest-init.js";
59
+ import { handleProgressiveEnhancement } from "./progressive-enhancement.js";
60
+ import {
61
+ executeServerAction,
62
+ revalidateAfterAction,
63
+ type ActionContinuation,
64
+ } from "./server-action.js";
65
+ import { handleLoaderFetch } from "./loader-fetch.js";
66
+ import { checkRequestOrigin, type OriginCheckPhase } from "./origin-guard.js";
67
+ import { handleRscRendering } from "./rsc-rendering.js";
68
+ import {
69
+ withTimeout,
70
+ RouterTimeoutError,
71
+ createDefaultTimeoutResponse,
72
+ type TimeoutPhase,
73
+ } from "../router/timeout.js";
74
+ import {
75
+ createMetricsStore,
76
+ appendMetric,
77
+ buildMetricsTiming,
78
+ } from "../router/metrics.js";
79
+ import {
80
+ startSSRSetup,
81
+ getSSRSetup,
82
+ mayNeedSSR,
83
+ SSR_SETUP_VAR,
84
+ } from "./ssr-setup.js";
38
85
 
39
86
  /**
40
87
  * Create an RSC request handler.
41
88
  *
89
+ * **Recommended:** Use `router.createHandler()` instead for simpler setup:
90
+ * ```tsx
91
+ * const router = createRouter({ document, urls, nonce: () => true });
92
+ * export const fetch = router.createHandler();
93
+ * ```
94
+ *
95
+ * This function is still useful for advanced cases like per-request cache
96
+ * configuration (e.g., Cloudflare Workers with ExecutionContext).
97
+ *
42
98
  * @example Basic usage (deps and loadSSRModule have sensible defaults)
43
99
  * ```tsx
44
- * import { createRSCHandler } from "rsc-router/rsc";
100
+ * import { createRSCHandler } from "@rangojs/router/rsc";
45
101
  * import { router } from "./router.js";
46
102
  *
47
103
  * export default createRSCHandler({ router });
@@ -49,7 +105,7 @@ import { invokeOnError } from "../router/error-handling.js";
49
105
  *
50
106
  * @example With custom deps (advanced)
51
107
  * ```tsx
52
- * import { createRSCHandler } from "rsc-router/rsc";
108
+ * import { createRSCHandler } from "@rangojs/router/rsc";
53
109
  * import * as rsc from "@vitejs/plugin-rsc/rsc";
54
110
  * import { router } from "./router.js";
55
111
  *
@@ -66,9 +122,6 @@ export function createRSCHandler<
66
122
  >(options: CreateRSCHandlerOptions<TEnv, TRoutes>) {
67
123
  const { router, version = VERSION, nonce: nonceProvider } = options;
68
124
 
69
- // Get the route map for useHref() - converts route names to URL patterns
70
- const routeMap = router.routeMap as Record<string, string>;
71
-
72
125
  // Use provided deps or default to @vitejs/plugin-rsc/rsc exports
73
126
  const deps = options.deps ?? rscDeps;
74
127
  const {
@@ -80,46 +133,210 @@ export function createRSCHandler<
80
133
  decodeFormState,
81
134
  } = deps;
82
135
 
83
- // Use provided loadSSRModule or default to vite RSC module loader
84
- const loadSSRModule =
136
+ // Use provided loadSSRModule or default to vite RSC module loader.
137
+ // In production the SSR module is stable across requests, so memoize
138
+ // the dynamic import to avoid repeated module resolution overhead.
139
+ // In dev mode Vite may hot-reload the module, so skip memoization.
140
+ const rawLoadSSRModule: LoadSSRModule =
85
141
  options.loadSSRModule ??
86
142
  (() => import.meta.viteRsc.loadModule("ssr", "index"));
143
+ let _ssrModulePromise: Promise<SSRModule> | undefined;
144
+ const loadSSRModule: LoadSSRModule =
145
+ process.env.NODE_ENV === "production"
146
+ ? () =>
147
+ (_ssrModulePromise ??= rawLoadSSRModule().catch((err) => {
148
+ _ssrModulePromise = undefined;
149
+ throw err;
150
+ }))
151
+ : rawLoadSSRModule;
87
152
 
88
153
  /**
89
- * Wrapper for invokeOnError that binds the router's onError callback.
90
- * Uses the shared utility from router/error-handling.ts for consistent behavior.
154
+ * Per-request error reporter that deduplicates via the ALS request context.
155
+ *
156
+ * Uses the same _reportedErrors WeakSet as the router layer so errors
157
+ * that propagate across layers are only reported once per request.
91
158
  */
92
159
  function callOnError(
93
160
  error: unknown,
94
161
  phase: ErrorPhase,
95
162
  context: Parameters<typeof invokeOnError<TEnv>>[3],
96
163
  ): void {
164
+ if (error != null && typeof error === "object") {
165
+ const reportedErrors = requireRequestContext()._reportedErrors;
166
+ if (reportedErrors.has(error)) return;
167
+ reportedErrors.add(error);
168
+ }
97
169
  invokeOnError(router.onError, error, phase, context, "RSC");
98
170
  }
99
171
 
100
- return async function handler(
172
+ function getRequiredRouteMap(): Record<string, string> {
173
+ const routeMap = getRouterManifest(router.id);
174
+ if (!routeMap) {
175
+ throw new Error(
176
+ `Route manifest for router "${router.id}" is not available.`,
177
+ );
178
+ }
179
+ return routeMap;
180
+ }
181
+
182
+ /**
183
+ * Handle a timeout by reporting the error, emitting telemetry,
184
+ * and returning either the custom onTimeout response or a default 504.
185
+ */
186
+ async function handleTimeoutResponse(
101
187
  request: Request,
102
- env: TEnv & { ctx?: ExecutionContext } = {} as TEnv & {
103
- ctx?: ExecutionContext;
188
+ env: TEnv,
189
+ url: URL,
190
+ phase: TimeoutPhase,
191
+ durationMs: number,
192
+ routeKey?: string,
193
+ actionId?: string,
194
+ ): Promise<Response> {
195
+ const timeoutError = new RouterTimeoutError(phase, durationMs);
196
+
197
+ callOnError(timeoutError, phase === "action" ? "action" : "handler", {
198
+ request,
199
+ url,
200
+ env,
201
+ routeKey,
202
+ actionId,
203
+ handledByBoundary: false,
204
+ metadata: { timeout: true, phase, durationMs },
205
+ });
206
+
207
+ try {
208
+ const routerCtx = getRouterContext();
209
+ if (routerCtx?.telemetry) {
210
+ safeEmit(resolveSink(routerCtx.telemetry), {
211
+ type: "request.timeout" as const,
212
+ timestamp: performance.now(),
213
+ requestId: routerCtx.requestId,
214
+ phase,
215
+ pathname: url.pathname,
216
+ routeKey,
217
+ actionId,
218
+ durationMs,
219
+ customHandler: !!router.onTimeout,
220
+ });
221
+ }
222
+ } catch {
223
+ // Router context may not be available
224
+ }
225
+
226
+ if (router.onTimeout) {
227
+ try {
228
+ return await router.onTimeout({
229
+ phase,
230
+ request,
231
+ url,
232
+ env,
233
+ routeKey,
234
+ actionId,
235
+ durationMs,
236
+ });
237
+ } catch (e) {
238
+ if (process.env.NODE_ENV !== "production") {
239
+ console.error("[RSC] onTimeout callback error:", e);
240
+ }
241
+ return createDefaultTimeoutResponse(phase);
242
+ }
243
+ }
244
+
245
+ return createDefaultTimeoutResponse(phase);
246
+ }
247
+
248
+ /**
249
+ * Build a 200 Flight response that carries a redirect URL and optional state.
250
+ * Used when a partial/action request results in a redirect -- fetch
251
+ * auto-follows 3xx so we send the redirect as payload metadata instead.
252
+ */
253
+ function createRedirectFlightResponse(
254
+ redirectUrl: string,
255
+ locationState?: Record<string, unknown>,
256
+ ): Response {
257
+ const redirectPayload: RscPayload = {
258
+ metadata: {
259
+ pathname: redirectUrl,
260
+ segments: [],
261
+ redirect: { url: redirectUrl },
262
+ ...(locationState && { locationState }),
263
+ },
264
+ };
265
+ const rscStream = renderToReadableStream<RscPayload>(redirectPayload);
266
+ return createResponseWithMergedHeaders(rscStream, {
267
+ status: 200,
268
+ headers: { "content-type": "text/x-component;charset=utf-8" },
269
+ });
270
+ }
271
+
272
+ // Bundle shared dependencies for extracted handler functions.
273
+ // callOnError reads from ALS so it's inherently per-request scoped.
274
+ const handlerCtx: HandlerContext<TEnv> = {
275
+ router,
276
+ version,
277
+ renderToReadableStream,
278
+ decodeReply,
279
+ createTemporaryReferenceSet,
280
+ loadServerAction,
281
+ decodeAction,
282
+ decodeFormState,
283
+ loadSSRModule,
284
+ callOnError,
285
+ getRequiredRouteMap,
286
+ createRedirectFlightResponse,
287
+ resolveStreamMode: async (request, env, url) => {
288
+ const resolver = router.ssr?.resolveStreaming;
289
+ if (!resolver) return "stream";
290
+ return resolver({ request, env, url });
104
291
  },
292
+ };
293
+
294
+ return async function handler(
295
+ request: Request,
296
+ input: RouterRequestInput<TEnv> = {},
105
297
  ): Promise<Response> {
298
+ const handlerStart = performance.now();
299
+ // Create the metrics store at handler start so handler:total has startTime=0
300
+ // and all metrics are relative to the request entry point.
301
+ const earlyMetricsStore = router.debugPerformance
302
+ ? createMetricsStore(true, handlerStart)
303
+ : undefined;
304
+
305
+ const { env = {} as TEnv, vars: initialVars, ctx: executionCtx } = input;
306
+
307
+ // Connection warmup: return 204 immediately before any processing
308
+ if (router?.warmupEnabled && request.method === "HEAD") {
309
+ const warmupUrl = new URL(request.url);
310
+ if (warmupUrl.searchParams.has("_rsc_warmup")) {
311
+ return new Response(null, { status: 204 });
312
+ }
313
+ }
314
+
106
315
  // Resolve nonce if provider is set
316
+ const nonceStart = performance.now();
107
317
  let nonce: string | undefined;
108
318
  if (nonceProvider) {
109
319
  const result = await nonceProvider(request, env);
110
320
  nonce = result === true ? generateNonce() : result;
111
321
  }
322
+ const nonceDur = performance.now() - nonceStart;
112
323
 
113
324
  const url = new URL(request.url);
114
325
 
115
326
  // Match global middleware
327
+ const mwMatchStart = performance.now();
116
328
  const matchedMiddleware = matchMiddleware(url.pathname, router.middleware);
329
+ const mwMatchDur = performance.now() - mwMatchStart;
117
330
 
118
331
  // Shared variables between middleware and route handlers
119
- const variables: Record<string, any> = {};
332
+ // Initialize from input.vars if provided (allows pre-seeding from worker entry)
333
+ const variables: Record<string, any> = initialVars
334
+ ? { ...initialVars }
335
+ : {};
120
336
 
121
- // Store nonce in variables so middleware can access via ctx.get('nonce')
337
+ // Store nonce via ContextVar token and string key for backward compat
122
338
  if (nonce) {
339
+ contextSet(variables, nonceToken, nonce);
123
340
  variables.nonce = nonce;
124
341
  }
125
342
 
@@ -130,25 +347,103 @@ export function createRSCHandler<
130
347
  const cacheOption = options.cache ?? router.cache;
131
348
  if (cacheOption && !url.searchParams.has("__no_cache")) {
132
349
  const cacheConfig =
133
- typeof cacheOption === "function" ? cacheOption(env) : cacheOption;
350
+ typeof cacheOption === "function"
351
+ ? cacheOption(env, executionCtx)
352
+ : cacheOption;
134
353
 
135
354
  if (cacheConfig.enabled !== false) {
136
355
  cacheStore = cacheConfig.store;
137
356
  }
138
357
  }
139
358
 
359
+ // Route manifest is populated at startup via the virtual module
360
+ // (virtual:rsc-router/routes-manifest). In build/production, it's inlined
361
+ // into the bundle. In dev mode (Node), the discovery plugin populates it
362
+ // via setManifestReadyPromise(). In dev mode (Cloudflare), Miniflare runs
363
+ // in a separate isolate where module-level state doesn't carry over, so
364
+ // we generate inline from the router's urlpatterns.
365
+ //
366
+ // In multi-router setups (e.g. createHostRouter), each router must have
367
+ // its own per-router manifest. We check per-router data first: even if
368
+ // the global manifest was set by a different router, this router still
369
+ // needs its own trie and manifest for correct matching.
370
+ const manifestCacheStart = performance.now();
371
+ const hasRouterData = getRouterManifest(router.id) !== undefined;
372
+ if (!hasRouterData) {
373
+ if (!hasCachedManifest()) {
374
+ const readyPromise = waitForManifestReady();
375
+ if (readyPromise) {
376
+ await readyPromise;
377
+ }
378
+ }
379
+ if (!getRouterManifest(router.id) && router.urlpatterns) {
380
+ // Cloudflare dev: generate manifest inline for this router.
381
+ // Each router generates its own manifest independently so
382
+ // multi-router setups (host routing) work correctly.
383
+ await buildRouterTrieFromUrlpatterns(router);
384
+ }
385
+ if (!getRouterManifest(router.id) && !hasCachedManifest()) {
386
+ throw new Error(
387
+ 'Route manifest not available. Ensure "virtual:rsc-router/routes-manifest" is imported in your entry file.',
388
+ );
389
+ }
390
+ }
391
+
392
+ // Rebuild the trie when the manifest exists but the per-router trie is
393
+ // missing. This happens in dev mode after HMR: the virtual module sets
394
+ // the manifest (from fresh gen files) but the trie is intentionally not
395
+ // injected to avoid stale discovery-time data. Without the trie, route
396
+ // matching falls back to regex iteration which does not handle wildcard
397
+ // priority correctly (catch-all patterns match before specific routes).
398
+ if (!getRouterTrie(router.id) && router.urlpatterns) {
399
+ await buildRouterTrieFromUrlpatterns(router);
400
+ }
401
+ const manifestCacheDur = performance.now() - manifestCacheStart;
402
+
140
403
  // Create unified request context with all methods
141
404
  // Includes: stub response, handle store, loader memoization, use(), cookies, headers, cache store
142
405
  // params starts empty, populated after route matching via setRequestContextParams
406
+ const ctxCreateStart = performance.now();
143
407
  const requestContext = createRequestContext({
144
408
  env,
145
409
  request,
146
410
  url,
147
411
  variables,
148
412
  cacheStore,
149
- executionContext: env.ctx,
413
+ cacheProfiles: router.cacheProfiles,
414
+ executionContext: executionCtx,
150
415
  themeConfig: router.themeConfig,
151
416
  });
417
+ if (earlyMetricsStore) {
418
+ requestContext._debugPerformance = true;
419
+ requestContext._metricsStore = earlyMetricsStore;
420
+ }
421
+ // Wire background error reporting so "use cache" and other subsystems
422
+ // can surface non-fatal errors through the router's onError callback.
423
+ requestContext._reportBackgroundError = (
424
+ error: unknown,
425
+ category: string,
426
+ ) => {
427
+ callOnError(error, "cache", {
428
+ request,
429
+ url,
430
+ metadata: { category },
431
+ });
432
+ };
433
+
434
+ const ctxCreateDur = performance.now() - ctxCreateStart;
435
+
436
+ // Accumulate handler-level timing for Server-Timing header
437
+ const handlerTiming = [
438
+ `handler-nonce;dur=${nonceDur.toFixed(2)}`,
439
+ `handler-mw-match;dur=${mwMatchDur.toFixed(2)}`,
440
+ `handler-manifest-cache;dur=${manifestCacheDur.toFixed(2)}`,
441
+ `handler-ctx-create;dur=${ctxCreateDur.toFixed(2)}`,
442
+ ];
443
+
444
+ // Store timing data in variables for downstream access
445
+ variables.__handlerTiming = handlerTiming;
446
+ variables.__handlerStart = handlerStart;
152
447
 
153
448
  // Wrap entire request handling in request context
154
449
  // Makes context available via getRequestContext() throughout:
@@ -164,17 +459,71 @@ export function createRSCHandler<
164
459
  };
165
460
 
166
461
  // Execute middleware chain if any, otherwise call core handler directly
462
+ let response: Response;
167
463
  if (matchedMiddleware.length > 0) {
168
- return executeMiddleware(
464
+ const mwResponse = await executeMiddleware(
169
465
  matchedMiddleware,
170
466
  request,
171
467
  env,
172
468
  variables,
173
469
  coreHandler,
470
+ createReverseFunction(getRequiredRouteMap()),
471
+ );
472
+
473
+ if (
474
+ url.searchParams.has("_rsc_partial") ||
475
+ url.searchParams.has("_rsc_action")
476
+ ) {
477
+ const intercepted = interceptRedirectForPartial(
478
+ mwResponse,
479
+ createRedirectFlightResponse,
480
+ );
481
+ response = intercepted ?? finalizeResponse(mwResponse);
482
+ } else {
483
+ response = finalizeResponse(mwResponse);
484
+ }
485
+ } else {
486
+ response = await coreHandler();
487
+ }
488
+
489
+ // Finalize metrics after all middleware (including post-next work)
490
+ // has completed so :post spans are captured in the timeline.
491
+ // Handler timing parts are always emitted (even without debug metrics)
492
+ // so non-debug requests still get bootstrap Server-Timing entries.
493
+ const handlerTimingArr: string[] = variables.__handlerTiming || [];
494
+ // Preserve any existing Server-Timing set by response routes or middleware
495
+ const existingTiming = response.headers.get("Server-Timing");
496
+ const timingParts = existingTiming
497
+ ? [existingTiming, ...handlerTimingArr]
498
+ : [...handlerTimingArr];
499
+
500
+ const metricsStore = requestContext._metricsStore;
501
+ if (metricsStore) {
502
+ // When the store was created at handler start (earlyMetricsStore),
503
+ // handler:total covers the full request. When ctx.debugPerformance()
504
+ // created the store mid-request, use its requestStart to avoid a
505
+ // negative startTime offset.
506
+ const totalStart = earlyMetricsStore
507
+ ? handlerStart
508
+ : metricsStore.requestStart;
509
+ appendMetric(
510
+ metricsStore,
511
+ "handler:total",
512
+ totalStart,
513
+ performance.now() - totalStart,
514
+ );
515
+ const metricsTiming = buildMetricsTiming(
516
+ request.method,
517
+ url.pathname,
518
+ metricsStore,
174
519
  );
520
+ if (metricsTiming) timingParts.push(metricsTiming);
175
521
  }
176
522
 
177
- return coreHandler();
523
+ const fullTiming = timingParts.join(", ");
524
+ if (fullTiming) response.headers.set("Server-Timing", fullTiming);
525
+
526
+ return response;
178
527
  });
179
528
  };
180
529
 
@@ -186,57 +535,314 @@ export function createRSCHandler<
186
535
  variables: Record<string, any>,
187
536
  nonce: string | undefined,
188
537
  ): Promise<Response> {
189
- // First, check for route-level middleware
190
- const preview = await router.previewMatch(request, env);
191
- if (preview?.routeMiddleware && preview.routeMiddleware.length > 0) {
192
- // Convert route middleware to app middleware format for execution
193
- const middlewareEntries = preview.routeMiddleware.map((mw) => ({
194
- entry: {
195
- pattern: null,
196
- regex: null,
197
- paramNames: [],
198
- handler: mw.handler,
199
- mountPrefix: null,
200
- },
201
- params: mw.params,
202
- }));
538
+ const previewStart = performance.now();
539
+ const preview = await router.previewMatch(request, { env });
540
+ const previewDur = performance.now() - previewStart;
541
+ const handlerTiming: string[] = variables.__handlerTiming || [];
542
+ handlerTiming.push(`handler-preview-match;dur=${previewDur.toFixed(2)}`);
543
+ // Response route short-circuit: skip entire RSC pipeline
544
+ if (preview?.responseType && preview.handler) {
545
+ const responseOutcome = await withTimeout(
546
+ handleResponseRoute(
547
+ handlerCtx,
548
+ preview as ResponseRouteMatch,
549
+ request,
550
+ env,
551
+ url,
552
+ variables,
553
+ ),
554
+ router.timeouts.renderStartMs,
555
+ "render-start",
556
+ );
557
+ if (responseOutcome.timedOut) {
558
+ return handleTimeoutResponse(
559
+ request,
560
+ env,
561
+ url,
562
+ "render-start",
563
+ responseOutcome.durationMs,
564
+ preview?.routeKey,
565
+ );
566
+ }
567
+ return responseOutcome.result;
568
+ }
203
569
 
204
- // Execute route middleware wrapping the actual request handling
205
- return executeMiddleware(middlewareEntries, request, env, variables, () =>
206
- coreRequestHandlerInner(request, env, url, variables, nonce),
570
+ // Kick off SSR module loading + stream mode resolution in parallel with
571
+ // segment resolution. Placed after the response-route short-circuit so
572
+ // response/mime routes never pay for SSR work.
573
+ if (mayNeedSSR(request, url)) {
574
+ variables[SSR_SETUP_VAR] = startSSRSetup(
575
+ handlerCtx,
576
+ request,
577
+ env,
578
+ url,
579
+ router.debugPerformance
580
+ ? () => requireRequestContext()._metricsStore
581
+ : undefined,
207
582
  );
208
583
  }
209
584
 
210
- // No route middleware, proceed directly
211
- return coreRequestHandlerInner(request, env, url, variables, nonce);
585
+ const routeReverse = createReverseFunction(getRequiredRouteMap());
586
+
587
+ const isAction =
588
+ request.headers.has("rsc-action") || url.searchParams.has("_rsc_action");
589
+ const isLoaderFetch = url.searchParams.has("_rsc_loader");
590
+ const actionId =
591
+ request.headers.get("rsc-action") || url.searchParams.get("_rsc_action");
592
+
593
+ // Origin guard: reject cross-origin actions, loader fetches, and
594
+ // PE form submissions before any execution. Regular page navigations
595
+ // (GET without _rsc_loader/_rsc_action) are not affected.
596
+ const originPhase: OriginCheckPhase | null = isAction
597
+ ? "action"
598
+ : isLoaderFetch
599
+ ? "loader"
600
+ : request.method === "POST"
601
+ ? "pe-form"
602
+ : null;
603
+ if (originPhase) {
604
+ const originResult = await checkRequestOrigin(
605
+ request,
606
+ url,
607
+ router.originCheck,
608
+ env,
609
+ router.id,
610
+ originPhase,
611
+ );
612
+ if (originResult) {
613
+ const originError = new Error(
614
+ `Origin check rejected: ${request.headers.get("origin") ?? "none"} vs ${request.headers.get("host") ?? "none"}`,
615
+ );
616
+ originError.name = "OriginCheckError";
617
+
618
+ callOnError(originError, "origin", {
619
+ request,
620
+ url,
621
+ env,
622
+ handledByBoundary: false,
623
+ metadata: {
624
+ phase: originPhase,
625
+ origin: request.headers.get("origin"),
626
+ host: request.headers.get("host"),
627
+ },
628
+ });
629
+
630
+ try {
631
+ const routerCtx = getRouterContext();
632
+ if (routerCtx?.telemetry) {
633
+ safeEmit(resolveSink(routerCtx.telemetry), {
634
+ type: "request.origin-rejected" as const,
635
+ timestamp: performance.now(),
636
+ requestId: routerCtx.requestId,
637
+ method: request.method,
638
+ pathname: url.pathname,
639
+ phase: originPhase,
640
+ origin: request.headers.get("origin"),
641
+ host: request.headers.get("host"),
642
+ });
643
+ }
644
+ } catch {
645
+ // Router context may not be available
646
+ }
647
+
648
+ return originResult;
649
+ }
650
+ }
651
+
652
+ // Get handle store from request context
653
+ const handleStore = requireRequestContext()._handleStore;
654
+
655
+ // Wire up error reporting for late streaming-handle failures
656
+ // (LateHandlePushError: handle pushed after stream completion).
657
+ // Without this, these errors are only caught by React's error boundary
658
+ // and never reach the router's onError callback or telemetry.
659
+ handleStore.onError = (error: Error) => {
660
+ const reqCtx = requireRequestContext();
661
+ callOnError(error, "handler", {
662
+ request,
663
+ url,
664
+ routeKey: reqCtx._routeName,
665
+ params: reqCtx.params as Record<string, string>,
666
+ handledByBoundary: true,
667
+ });
668
+ try {
669
+ const routerCtx = getRouterContext();
670
+ if (routerCtx?.telemetry) {
671
+ safeEmit(resolveSink(routerCtx.telemetry), {
672
+ type: "handler.error" as const,
673
+ timestamp: performance.now(),
674
+ requestId: routerCtx.requestId,
675
+ error,
676
+ handledByBoundary: true,
677
+ pathname: url.pathname,
678
+ routeKey: reqCtx._routeName,
679
+ params: reqCtx.params as Record<string, string>,
680
+ });
681
+ }
682
+ } catch {
683
+ // Router context may not be available (e.g. prerender path)
684
+ }
685
+ };
686
+
687
+ // Set route params early so all execution paths can access ctx.params.
688
+ if (preview?.params) {
689
+ setRequestContextParams(preview.params, preview.routeKey);
690
+ }
691
+
692
+ // Progressive enhancement runs before the normal action/render paths.
693
+ // Route middleware wraps the PE re-render so handlers see the same
694
+ // context variables regardless of JS/no-JS transport.
695
+ const progressiveResult = await handleProgressiveEnhancement(
696
+ handlerCtx,
697
+ request,
698
+ env,
699
+ url,
700
+ isAction,
701
+ handleStore,
702
+ nonce,
703
+ {
704
+ routeMiddleware: preview?.routeMiddleware,
705
+ variables,
706
+ routeReverse,
707
+ },
708
+ );
709
+ if (progressiveResult) {
710
+ return progressiveResult;
711
+ }
712
+
713
+ // --- Action execution: runs BEFORE route middleware ---
714
+ // Route middleware wraps rendering only. For actions, the action runs
715
+ // first in the global middleware context, then route middleware wraps
716
+ // the revalidation pass (identical to a normal render).
717
+ let actionContinuation: ActionContinuation | undefined;
718
+ if (isAction && actionId) {
719
+ try {
720
+ const actionOutcome = await withTimeout(
721
+ executeServerAction(
722
+ handlerCtx,
723
+ request,
724
+ env,
725
+ url,
726
+ actionId,
727
+ handleStore,
728
+ ),
729
+ router.timeouts.actionMs,
730
+ "action",
731
+ );
732
+ if (actionOutcome.timedOut) {
733
+ return handleTimeoutResponse(
734
+ request,
735
+ env,
736
+ url,
737
+ "action",
738
+ actionOutcome.durationMs,
739
+ preview?.routeKey,
740
+ actionId,
741
+ );
742
+ }
743
+ const result = actionOutcome.result;
744
+ // Response means redirect or error boundary — done.
745
+ if (result instanceof Response) return result;
746
+ actionContinuation = result;
747
+ } catch (error) {
748
+ callOnError(error, "action", {
749
+ request,
750
+ url,
751
+ env,
752
+ actionId,
753
+ handledByBoundary: false,
754
+ });
755
+ console.error(`[RSC] Action error:`, error);
756
+ throw error;
757
+ }
758
+ }
759
+
760
+ // --- Rendering (action revalidation or navigation) ---
761
+ // Route middleware wraps this — same code path for both cases.
762
+ const renderHandler = async () => {
763
+ const response = await coreRequestHandlerInner(
764
+ request,
765
+ env,
766
+ url,
767
+ variables,
768
+ nonce,
769
+ preview?.params,
770
+ preview?.routeKey,
771
+ handleStore,
772
+ actionContinuation,
773
+ );
774
+ if (preview?.negotiated) {
775
+ response.headers.append("Vary", "Accept");
776
+ }
777
+ return response;
778
+ };
779
+
780
+ // Wrap the render path (with or without route middleware) in a
781
+ // renderStartMs timeout so slow renders are caught before output.
782
+ const executeRender = async (): Promise<Response> => {
783
+ if (preview?.routeMiddleware && preview.routeMiddleware.length > 0) {
784
+ const mwResponse = await executeMiddleware(
785
+ buildRouteMiddlewareEntries<TEnv>(preview.routeMiddleware),
786
+ request,
787
+ env,
788
+ variables,
789
+ renderHandler,
790
+ routeReverse,
791
+ );
792
+
793
+ if (
794
+ url.searchParams.has("_rsc_partial") ||
795
+ url.searchParams.has("_rsc_action")
796
+ ) {
797
+ const intercepted = interceptRedirectForPartial(
798
+ mwResponse,
799
+ createRedirectFlightResponse,
800
+ );
801
+ if (intercepted) return intercepted;
802
+ }
803
+
804
+ return finalizeResponse(mwResponse);
805
+ }
806
+
807
+ // No route middleware, proceed directly
808
+ return renderHandler();
809
+ };
810
+
811
+ const renderOutcome = await withTimeout(
812
+ executeRender(),
813
+ router.timeouts.renderStartMs,
814
+ "render-start",
815
+ );
816
+ if (renderOutcome.timedOut) {
817
+ return handleTimeoutResponse(
818
+ request,
819
+ env,
820
+ url,
821
+ "render-start",
822
+ renderOutcome.durationMs,
823
+ preview?.routeKey,
824
+ );
825
+ }
826
+ return renderOutcome.result;
212
827
  }
213
828
 
214
- // Inner request handler (actual RSC logic, wrapped by route middleware if any)
829
+ // Inner request handler: rendering logic wrapped by route middleware.
830
+ // Handles action revalidation (when actionContinuation is present),
831
+ // loader fetches, and regular RSC rendering.
215
832
  async function coreRequestHandlerInner(
216
833
  request: Request,
217
834
  env: TEnv,
218
835
  url: URL,
219
836
  variables: Record<string, any>,
220
837
  nonce: string | undefined,
838
+ routeParams?: Record<string, string>,
839
+ routeKey?: string,
840
+ handleStore?: ReturnType<typeof requireRequestContext>["_handleStore"],
841
+ actionContinuation?: ActionContinuation,
221
842
  ): Promise<Response> {
222
- // Early return for static file requests that don't need RSC handling
223
- if (url.pathname === "/favicon.ico" || url.pathname === "/robots.txt") {
224
- return new Response(null, { status: 404 });
225
- }
226
-
227
- // Debug endpoint - only in development
228
- if (url.pathname === "/__debug_manifest" && process.env.NODE_ENV !== "production") {
229
- const manifest = await router.debugManifest();
230
- return new Response(JSON.stringify(manifest, null, 2), {
231
- headers: { "Content-Type": "application/json" },
232
- });
233
- }
234
-
235
843
  const isPartial = url.searchParams.has("_rsc_partial");
236
844
  const isAction =
237
845
  request.headers.has("rsc-action") || url.searchParams.has("_rsc_action");
238
- const actionId =
239
- request.headers.get("rsc-action") || url.searchParams.get("_rsc_action");
240
846
 
241
847
  // Version mismatch detection - client may have stale code after HMR/deployment
242
848
  // If versions don't match, tell the client to reload
@@ -246,20 +852,23 @@ export function createRSCHandler<
246
852
  `[RSC] Version mismatch: client=${clientVersion}, server=${version}. Forcing reload.`,
247
853
  );
248
854
 
249
- // Clean URL by removing RSC params
250
- const cleanUrl = new URL(url);
251
- cleanUrl.searchParams.delete("_rsc_partial");
252
- cleanUrl.searchParams.delete("_rsc_segments");
253
- cleanUrl.searchParams.delete("_rsc_v");
254
- cleanUrl.searchParams.delete("_rsc_stale");
255
- cleanUrl.searchParams.delete("_rsc_action");
256
- cleanUrl.searchParams.delete("_rsc_prev");
257
-
258
- // For actions, reload current page (referer)
259
- // For navigation, load the target URL
260
- const reloadUrl = isAction
261
- ? request.headers.get("referer") || cleanUrl.toString()
262
- : cleanUrl.toString();
855
+ // For actions, reload current page (referer) if same origin.
856
+ // For navigation, load the target URL.
857
+ // Validate referer origin to prevent open redirect via crafted header.
858
+ let reloadUrl = stripInternalParams(url).toString();
859
+ if (isAction) {
860
+ const referer = request.headers.get("referer");
861
+ if (referer) {
862
+ try {
863
+ const refererUrl = new URL(referer);
864
+ if (refererUrl.origin === url.origin) {
865
+ reloadUrl = referer;
866
+ }
867
+ } catch {
868
+ // Malformed referer, fall back to cleanUrl
869
+ }
870
+ }
871
+ }
263
872
 
264
873
  // Return special response that tells client to reload
265
874
  return createResponseWithMergedHeaders(null, {
@@ -270,31 +879,56 @@ export function createRSCHandler<
270
879
  },
271
880
  });
272
881
  }
882
+ // Debug manifest endpoint: ?__debug_manifest on any route.
883
+ // Always available in dev, requires allowDebugManifest option in production.
884
+ const isDev = process.env.NODE_ENV !== "production";
885
+ if (
886
+ url.searchParams.has("__debug_manifest") &&
887
+ (isDev || router.allowDebugManifest)
888
+ ) {
889
+ const trie = getRouterTrie(router.id) ?? getRouteTrie();
890
+ const routeManifest = getRequiredRouteMap();
891
+ const { extractAncestryFromTrie } =
892
+ await import("../build/route-trie.js");
893
+ return new Response(
894
+ JSON.stringify(
895
+ {
896
+ routerId: router.id,
897
+ routeManifest,
898
+ routeAncestry: trie ? extractAncestryFromTrie(trie) : {},
899
+ routeTrie: trie,
900
+ precomputedEntries: getPrecomputedEntries(),
901
+ },
902
+ null,
903
+ 2,
904
+ ),
905
+ {
906
+ headers: { "Content-Type": "application/json" },
907
+ },
908
+ );
909
+ }
273
910
 
274
- // Get handle store from request context (created at start of request)
275
- const handleStore = requireRequestContext()._handleStore;
911
+ const store = handleStore ?? requireRequestContext()._handleStore;
276
912
 
277
913
  try {
278
- // ============================================================================
279
- // PROGRESSIVE ENHANCEMENT: No-JS Form Submissions
280
- // ============================================================================
281
- const progressiveResult = await handleProgressiveEnhancement(
282
- request,
283
- env,
284
- url,
285
- isAction,
286
- handleStore,
287
- nonce,
288
- );
289
- if (progressiveResult) {
290
- return progressiveResult;
914
+ // Route params were already set in coreRequestHandler, but set again
915
+ // for callers that enter coreRequestHandlerInner directly.
916
+ if (routeParams) {
917
+ setRequestContextParams(routeParams, routeKey);
291
918
  }
292
919
 
293
920
  // ============================================================================
294
- // SERVER ACTION EXECUTION (JavaScript-enabled client)
921
+ // ACTION REVALIDATION (action already executed, revalidate segments)
295
922
  // ============================================================================
296
- if (isAction && actionId) {
297
- return handleServerAction(request, env, url, actionId, handleStore);
923
+ if (actionContinuation) {
924
+ return await revalidateAfterAction(
925
+ handlerCtx,
926
+ request,
927
+ env,
928
+ url,
929
+ store,
930
+ actionContinuation,
931
+ );
298
932
  }
299
933
 
300
934
  // ============================================================================
@@ -302,7 +936,14 @@ export function createRSCHandler<
302
936
  // ============================================================================
303
937
  const isLoaderRequest = url.searchParams.has("_rsc_loader");
304
938
  if (isLoaderRequest) {
305
- return handleLoaderFetch(request, env, url, variables);
939
+ return handleLoaderFetch(
940
+ handlerCtx,
941
+ request,
942
+ env,
943
+ url,
944
+ variables,
945
+ routeParams,
946
+ );
306
947
  }
307
948
 
308
949
  // ============================================================================
@@ -310,16 +951,44 @@ export function createRSCHandler<
310
951
  // ============================================================================
311
952
  // Note: Must use "return await" for try/catch to catch async rejections
312
953
  return await handleRscRendering(
954
+ handlerCtx,
313
955
  request,
314
956
  env,
315
957
  url,
316
958
  isPartial,
317
- handleStore,
959
+ store,
318
960
  nonce,
319
961
  );
320
962
  } catch (error) {
321
963
  // Check if middleware/handler returned Response
322
964
  if (error instanceof Response) {
965
+ // During partial (client-side navigation), a 200 Response from a handler
966
+ // means the route serves raw content (JSON, text, etc.), not JSX.
967
+ // Signal the browser to hard-navigate so it renders the raw response.
968
+ // Only for 200 — redirects (3xx) work already because the browser follows
969
+ // them automatically to a URL that serves Flight data.
970
+ if (isPartial && error.status === 200) {
971
+ console.warn(
972
+ `[RSC] Route handler at ${url.pathname} returned a Response during client-side navigation. ` +
973
+ `Falling back to hard navigation. Use data-external on the <Link> to avoid the extra round-trip.`,
974
+ );
975
+ return createResponseWithMergedHeaders(null, {
976
+ status: 200,
977
+ headers: {
978
+ "X-RSC-Reload": stripInternalParams(url).toString(),
979
+ "content-type": "text/x-component;charset=utf-8",
980
+ },
981
+ });
982
+ }
983
+
984
+ if (isPartial) {
985
+ const intercepted = interceptRedirectForPartial(
986
+ error,
987
+ createRedirectFlightResponse,
988
+ );
989
+ if (intercepted) return intercepted;
990
+ }
991
+
323
992
  return error;
324
993
  }
325
994
 
@@ -353,34 +1022,29 @@ export function createRSCHandler<
353
1022
  params: {},
354
1023
  };
355
1024
 
356
- // Render with rootLayout to maintain app shell
357
- const root = await renderSegments([notFoundSegment], {
358
- rootLayout: router.rootLayout,
359
- routeMap,
360
- // No routeName for not-found routes
361
- });
362
-
363
1025
  const payload: RscPayload = {
364
- root,
365
1026
  metadata: {
366
1027
  pathname: url.pathname,
367
1028
  segments: [notFoundSegment],
368
1029
  matched: [],
369
1030
  diff: [],
370
1031
  isPartial: false,
371
- handles: handleStore.stream(),
1032
+ rootLayout: router.rootLayout,
1033
+ handles: store.stream(),
372
1034
  version,
373
1035
  themeConfig: router.themeConfig,
1036
+ warmupEnabled: router.warmupEnabled,
374
1037
  initialTheme: requireRequestContext().theme,
375
- routeMap,
376
1038
  // No routeName for not-found routes
377
1039
  },
378
1040
  };
379
1041
 
380
1042
  const rscStream = renderToReadableStream(payload);
381
1043
 
382
- // Determine if this is an RSC request or HTML request
1044
+ // Determine if this is an RSC request or HTML request.
1045
+ // Partial requests are always RSC (see main isRscRequest comment).
383
1046
  const isRscRequest =
1047
+ isPartial ||
384
1048
  (!request.headers.get("accept")?.includes("text/html") &&
385
1049
  !url.searchParams.has("__html")) ||
386
1050
  url.searchParams.has("__rsc");
@@ -392,9 +1056,18 @@ export function createRSCHandler<
392
1056
  });
393
1057
  }
394
1058
 
395
- // Delegate to SSR for HTML response
396
- const ssrModule = await loadSSRModule();
397
- const htmlStream = await ssrModule.renderHTML(rscStream, { nonce });
1059
+ // Delegate to SSR for HTML response (reuse early setup if available)
1060
+ const [ssrModule, streamMode] = await getSSRSetup(
1061
+ handlerCtx,
1062
+ request,
1063
+ env,
1064
+ url,
1065
+ requireRequestContext()._metricsStore,
1066
+ );
1067
+ const htmlStream = await ssrModule.renderHTML(rscStream, {
1068
+ nonce,
1069
+ streamMode,
1070
+ });
398
1071
 
399
1072
  return createResponseWithMergedHeaders(htmlStream, {
400
1073
  status: 404,
@@ -413,651 +1086,4 @@ export function createRSCHandler<
413
1086
  throw error;
414
1087
  }
415
1088
  }
416
-
417
- // ============================================================================
418
- // PROGRESSIVE ENHANCEMENT HANDLER
419
- // When JavaScript is disabled, React renders forms with hidden fields
420
- // ($ACTION_REF_*, $ACTION_KEY) containing the action reference.
421
- // We detect these and return HTML instead of RSC stream.
422
- // ============================================================================
423
- async function handleProgressiveEnhancement(
424
- request: Request,
425
- env: TEnv,
426
- url: URL,
427
- isAction: boolean,
428
- handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
429
- nonce: string | undefined,
430
- ): Promise<Response | null> {
431
- const contentType = request.headers.get("content-type") || "";
432
- const isFormSubmission =
433
- contentType.includes("multipart/form-data") ||
434
- contentType.includes("application/x-www-form-urlencoded");
435
-
436
- if (request.method !== "POST" || isAction || !isFormSubmission) {
437
- return null;
438
- }
439
-
440
- // Clone the request to read FormData without consuming it
441
- const formData = await request.clone().formData();
442
-
443
- // Look for React's progressive enhancement hidden fields
444
- let isDirectAction = false;
445
- let isUseActionState = false;
446
- let directActionId: string | null = null;
447
-
448
- formData.forEach((_value, key) => {
449
- if (key.startsWith("$ACTION_ID_")) {
450
- isDirectAction = true;
451
- directActionId = key.slice("$ACTION_ID_".length);
452
- } else if (key.startsWith("$ACTION_REF_")) {
453
- isUseActionState = true;
454
- }
455
- });
456
-
457
- if (!isDirectAction && !isUseActionState) {
458
- return null;
459
- }
460
-
461
- // Execute action and return HTML
462
- let actionResult: unknown = undefined;
463
- let reactFormState: ReactFormState | null = null;
464
-
465
- if (isUseActionState) {
466
- try {
467
- const boundAction = await decodeAction(formData);
468
- actionResult = await boundAction();
469
- } catch (error) {
470
- callOnError(error, "action", {
471
- request,
472
- url,
473
- env,
474
- handledByBoundary: false,
475
- });
476
- console.error("[RSC] Progressive enhancement action error:", error);
477
- }
478
- } else if (isDirectAction && directActionId) {
479
- const temporaryReferences = createTemporaryReferenceSet();
480
-
481
- let args: unknown[] = [];
482
- try {
483
- args = await decodeReply(formData, { temporaryReferences });
484
- } catch {
485
- args = [formData];
486
- }
487
-
488
- try {
489
- const loadedAction = await loadServerAction(directActionId);
490
- actionResult = await loadedAction.apply(null, args);
491
- } catch (error) {
492
- callOnError(error, "action", {
493
- request,
494
- url,
495
- env,
496
- actionId: directActionId,
497
- handledByBoundary: false,
498
- });
499
- console.error("[RSC] Progressive enhancement action error:", error);
500
- }
501
- }
502
-
503
- // Decode form state for useActionState progressive enhancement
504
- try {
505
- reactFormState = await decodeFormState(actionResult, formData);
506
- } catch (error) {
507
- callOnError(error, "action", {
508
- request,
509
- url,
510
- env,
511
- handledByBoundary: false,
512
- });
513
- console.error("[RSC] Failed to decode form state:", error);
514
- }
515
-
516
- // Re-render the page and return HTML
517
- const renderRequest = new Request(url.toString(), {
518
- method: "GET",
519
- headers: new Headers({ accept: "text/html" }),
520
- });
521
-
522
- const match = await router.match(renderRequest, env);
523
-
524
- if (match.redirect) {
525
- return new Response(null, {
526
- status: 308,
527
- headers: { Location: match.redirect },
528
- });
529
- }
530
-
531
- const root = renderSegments(match.segments, {
532
- rootLayout: router.rootLayout,
533
- routeMap,
534
- routeName: match.routeName,
535
- });
536
-
537
- const payload: RscPayload = {
538
- root,
539
- metadata: {
540
- pathname: url.pathname,
541
- segments: match.segments,
542
- matched: match.matched,
543
- diff: match.diff,
544
- isPartial: false,
545
- rootLayout: router.rootLayout,
546
- handles: handleStore.stream(),
547
- version,
548
- themeConfig: router.themeConfig,
549
- initialTheme: requireRequestContext().theme,
550
- routeMap,
551
- routeName: match.routeName,
552
- },
553
- formState: actionResult,
554
- };
555
-
556
- const rscStream = renderToReadableStream<RscPayload>(payload);
557
- const ssrModule = await loadSSRModule();
558
- const htmlStream = await ssrModule.renderHTML(rscStream, {
559
- formState: reactFormState,
560
- nonce,
561
- });
562
-
563
- return new Response(htmlStream, {
564
- headers: { "content-type": "text/html;charset=utf-8" },
565
- });
566
- }
567
-
568
- // ============================================================================
569
- // SERVER ACTION HANDLER
570
- // ============================================================================
571
- async function handleServerAction(
572
- request: Request,
573
- env: TEnv,
574
- url: URL,
575
- actionId: string,
576
- handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
577
- ): Promise<Response> {
578
- const temporaryReferences = createTemporaryReferenceSet();
579
-
580
- // Decode action arguments from request body
581
- const contentType = request.headers.get("content-type") || "";
582
- let args: unknown[] = [];
583
- let actionFormData: FormData | undefined;
584
-
585
- try {
586
- const body = contentType.includes("multipart/form-data")
587
- ? await request.formData()
588
- : await request.text();
589
-
590
- if (body instanceof FormData) {
591
- actionFormData = body;
592
- }
593
-
594
- if (hasBodyContent(body)) {
595
- args = await decodeReply(body, { temporaryReferences });
596
- }
597
- } catch (error) {
598
- callOnError(error, "action", {
599
- request,
600
- url,
601
- env,
602
- actionId,
603
- handledByBoundary: false,
604
- });
605
- throw new Error(`Failed to decode action arguments: ${error}`, {
606
- cause: error,
607
- });
608
- }
609
-
610
- // Execute the server action
611
- let returnValue: { ok: boolean; data: unknown };
612
- let actionStatus = 200;
613
- let loadedAction: Function | undefined;
614
-
615
- try {
616
- loadedAction = await loadServerAction(actionId);
617
- const data = await loadedAction!.apply(null, args);
618
- returnValue = { ok: true, data };
619
- } catch (error) {
620
- returnValue = { ok: false, data: error };
621
- actionStatus = 500;
622
-
623
- // Try to render error boundary
624
- const errorResult = await router.matchError(request, env, error, "route");
625
-
626
- // Report the action error (handledByBoundary indicates if error boundary will render)
627
- callOnError(error, "action", {
628
- request,
629
- url,
630
- env,
631
- actionId,
632
- handledByBoundary: !!errorResult,
633
- });
634
-
635
- if (errorResult) {
636
- setRequestContextParams(errorResult.params);
637
-
638
- const payload: RscPayload = {
639
- root: null,
640
- metadata: {
641
- pathname: url.pathname,
642
- segments: errorResult.segments,
643
- isPartial: true,
644
- matched: errorResult.matched,
645
- diff: errorResult.diff,
646
- isError: true,
647
- handles: handleStore.stream(),
648
- version,
649
- routeMap,
650
- routeName: errorResult.routeName,
651
- },
652
- returnValue,
653
- };
654
-
655
- const rscStream = renderToReadableStream<RscPayload>(payload, {
656
- temporaryReferences,
657
- });
658
-
659
- return createResponseWithMergedHeaders(rscStream, {
660
- status: actionStatus,
661
- headers: { "content-type": "text/x-component;charset=utf-8" },
662
- });
663
- }
664
- }
665
-
666
- // Revalidate after action
667
- const resolvedActionId =
668
- (loadedAction as { $id?: string; $$id?: string } | undefined)?.$id ??
669
- (loadedAction as { $$id?: string } | undefined)?.$$id ??
670
- actionId;
671
- const actionContext = {
672
- actionId: resolvedActionId,
673
- actionUrl: new URL(request.url),
674
- actionResult: returnValue.data,
675
- formData: actionFormData,
676
- };
677
-
678
- const matchResult = await router.matchPartial(request, env, actionContext);
679
-
680
- if (!matchResult) {
681
- // Fall back to full render
682
- const fullMatch = await router.match(request, env);
683
- setRequestContextParams(fullMatch.params);
684
-
685
- if (fullMatch.redirect) {
686
- return createResponseWithMergedHeaders(null, {
687
- status: 308,
688
- headers: { Location: fullMatch.redirect },
689
- });
690
- }
691
-
692
- const renderStart = performance.now();
693
- const root = renderSegments(fullMatch.segments, {
694
- rootLayout: router.rootLayout,
695
- isAction: true,
696
- routeMap,
697
- routeName: fullMatch.routeName,
698
- });
699
- const renderDuration = performance.now() - renderStart;
700
- const serverTiming = fullMatch.serverTiming
701
- ? `${fullMatch.serverTiming}, rendering;dur=${renderDuration.toFixed(2)}`
702
- : `rendering;dur=${renderDuration.toFixed(2)}`;
703
-
704
- const payload: RscPayload = {
705
- root,
706
- metadata: {
707
- pathname: url.pathname,
708
- segments: fullMatch.segments,
709
- matched: fullMatch.matched,
710
- diff: fullMatch.diff,
711
- handles: handleStore.stream(),
712
- version,
713
- routeMap,
714
- routeName: fullMatch.routeName,
715
- },
716
- returnValue,
717
- };
718
-
719
- const rscStream = renderToReadableStream<RscPayload>(payload, {
720
- temporaryReferences,
721
- });
722
-
723
- const headers: Record<string, string> = {
724
- "content-type": "text/x-component;charset=utf-8",
725
- };
726
- if (serverTiming) {
727
- headers["Server-Timing"] = serverTiming;
728
- }
729
-
730
- return createResponseWithMergedHeaders(rscStream, {
731
- status: actionStatus,
732
- headers,
733
- });
734
- }
735
-
736
- // Return updated segments
737
- setRequestContextParams(matchResult.params);
738
-
739
- const renderStart = performance.now();
740
-
741
- const renderDuration = performance.now() - renderStart;
742
- const serverTiming = matchResult.serverTiming
743
- ? `${matchResult.serverTiming}, rendering;dur=${renderDuration.toFixed(2)}`
744
- : `rendering;dur=${renderDuration.toFixed(2)}`;
745
-
746
- const payload: RscPayload = {
747
- root: null,
748
- metadata: {
749
- pathname: url.pathname,
750
- segments: matchResult.segments,
751
- isPartial: true,
752
- matched: matchResult.matched,
753
- diff: matchResult.diff,
754
- slots: matchResult.slots,
755
- handles: handleStore.stream(),
756
- version,
757
- routeMap,
758
- routeName: matchResult.routeName,
759
- },
760
- returnValue,
761
- };
762
-
763
- const rscStream = renderToReadableStream<RscPayload>(payload, {
764
- temporaryReferences,
765
- });
766
-
767
- const actionHeaders: Record<string, string> = {
768
- "content-type": "text/x-component;charset=utf-8",
769
- };
770
- if (serverTiming) {
771
- actionHeaders["Server-Timing"] = serverTiming;
772
- }
773
-
774
- return createResponseWithMergedHeaders(rscStream, {
775
- status: actionStatus,
776
- headers: actionHeaders,
777
- });
778
- }
779
-
780
- // ============================================================================
781
- // LOADER FETCH HANDLER
782
- // Supports GET (params in query string) and POST/PUT/PATCH/DELETE (JSON body)
783
- // ============================================================================
784
- async function handleLoaderFetch(
785
- request: Request,
786
- env: TEnv,
787
- url: URL,
788
- variables: Record<string, any>,
789
- ): Promise<Response> {
790
- const loaderId = url.searchParams.get("_rsc_loader");
791
-
792
- if (!loaderId) {
793
- return createResponseWithMergedHeaders("Missing _rsc_loader parameter", {
794
- status: 400,
795
- });
796
- }
797
-
798
- // Look up loader lazily
799
- const registeredLoader = await getLoaderLazy(loaderId);
800
- if (!registeredLoader) {
801
- return createResponseWithMergedHeaders(
802
- `Loader "${loaderId}" not found in registry`,
803
- { status: 404 },
804
- );
805
- }
806
-
807
- // Parse params and body based on request method
808
- let loaderParams: Record<string, string> = {};
809
- let loaderBody: unknown = undefined;
810
- const isBodyMethod = request.method !== "GET" && request.method !== "HEAD";
811
-
812
- if (isBodyMethod) {
813
- try {
814
- const contentType = request.headers.get("content-type") || "";
815
- if (contentType.includes("application/json")) {
816
- const jsonBody = (await request.json()) as {
817
- params?: Record<string, string>;
818
- body?: unknown;
819
- };
820
- loaderParams = jsonBody.params ?? {};
821
- loaderBody = jsonBody.body;
822
- }
823
- } catch {
824
- return createResponseWithMergedHeaders("Invalid JSON body", {
825
- status: 400,
826
- });
827
- }
828
- } else {
829
- const loaderParamsJson = url.searchParams.get("_rsc_loader_params");
830
- if (loaderParamsJson) {
831
- try {
832
- loaderParams = JSON.parse(loaderParamsJson);
833
- } catch {
834
- return createResponseWithMergedHeaders(
835
- "Invalid _rsc_loader_params JSON",
836
- { status: 400 },
837
- );
838
- }
839
- }
840
- }
841
-
842
- // Execute the loader with middleware
843
- try {
844
- const { fn, middleware } = registeredLoader;
845
-
846
- return await executeLoaderMiddleware(
847
- middleware,
848
- request,
849
- env,
850
- loaderParams,
851
- variables,
852
- async () => {
853
- const ctx = requireRequestContext();
854
- const loaderCtx: any = {
855
- ...ctx,
856
- params: loaderParams,
857
- body: loaderBody,
858
- };
859
-
860
- const result = await fn(loaderCtx);
861
-
862
- interface LoaderPayload {
863
- loaderResult: unknown;
864
- }
865
- const loaderPayload: LoaderPayload = { loaderResult: result };
866
- const rscStream =
867
- renderToReadableStream<LoaderPayload>(loaderPayload);
868
-
869
- return createResponseWithMergedHeaders(rscStream, {
870
- headers: { "content-type": "text/x-component;charset=utf-8" },
871
- });
872
- },
873
- );
874
- } catch (error) {
875
- const err = error instanceof Error ? error : new Error(String(error));
876
- const isDev = process.env.NODE_ENV !== "production";
877
-
878
- console.error("[RSC] Loader error:", error);
879
-
880
- callOnError(error, "loader", {
881
- request,
882
- url,
883
- env,
884
- loaderName: loaderId,
885
- handledByBoundary: false,
886
- });
887
-
888
- const errorPayload = {
889
- loaderResult: null,
890
- loaderError: {
891
- message: isDev ? err.message : "An error occurred",
892
- name: err.name,
893
- },
894
- };
895
- const rscStream = renderToReadableStream(errorPayload);
896
-
897
- return createResponseWithMergedHeaders(rscStream, {
898
- status: 500,
899
- headers: { "content-type": "text/x-component;charset=utf-8" },
900
- });
901
- }
902
- }
903
-
904
- // ============================================================================
905
- // RSC RENDERING HANDLER (Navigation)
906
- // ============================================================================
907
- async function handleRscRendering(
908
- request: Request,
909
- env: TEnv,
910
- url: URL,
911
- isPartial: boolean,
912
- handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
913
- nonce: string | undefined,
914
- ): Promise<Response> {
915
- let payload: RscPayload;
916
- let serverTiming: string | undefined;
917
-
918
- if (isPartial) {
919
- // Partial render (navigation)
920
- const result = await router.matchPartial(request, env);
921
-
922
- if (!result) {
923
- // Fall back to full render
924
- const match = await router.match(request, env);
925
- setRequestContextParams(match.params);
926
-
927
- if (match.redirect) {
928
- return createResponseWithMergedHeaders(null, {
929
- status: 308,
930
- headers: { Location: match.redirect },
931
- });
932
- }
933
-
934
- const renderStart = performance.now();
935
- const root = renderSegments(match.segments, {
936
- rootLayout: router.rootLayout,
937
- routeMap,
938
- routeName: match.routeName,
939
- });
940
- const renderDuration = performance.now() - renderStart;
941
- serverTiming = match.serverTiming
942
- ? `${match.serverTiming}, rendering;dur=${renderDuration.toFixed(2)}`
943
- : `rendering;dur=${renderDuration.toFixed(2)}`;
944
-
945
- payload = {
946
- root,
947
- metadata: {
948
- pathname: url.pathname,
949
- segments: match.segments,
950
- matched: match.matched,
951
- diff: match.diff,
952
- isPartial: false,
953
- handles: handleStore.stream(),
954
- version,
955
- themeConfig: router.themeConfig,
956
- initialTheme: requireRequestContext().theme,
957
- routeMap,
958
- routeName: match.routeName,
959
- },
960
- };
961
- } else {
962
- setRequestContextParams(result.params);
963
- serverTiming = result.serverTiming;
964
-
965
- payload = {
966
- root: null,
967
- metadata: {
968
- pathname: url.pathname,
969
- segments: result.segments,
970
- matched: result.matched,
971
- diff: result.diff,
972
- isPartial: true,
973
- slots: result.slots,
974
- handles: handleStore.stream(),
975
- version,
976
- routeMap,
977
- routeName: result.routeName,
978
- },
979
- };
980
- }
981
- } else {
982
- // Full render (initial page load)
983
- const match = await router.match(request, env);
984
- setRequestContextParams(match.params);
985
-
986
- if (match.redirect) {
987
- return createResponseWithMergedHeaders(null, {
988
- status: 308,
989
- headers: { Location: match.redirect },
990
- });
991
- }
992
-
993
- // Caching is now handled in router.match() via cache provider in request context
994
- // match.segments already contains cached or fresh segments as appropriate
995
-
996
- const renderStart = performance.now();
997
- const root = renderSegments(match.segments, {
998
- rootLayout: router.rootLayout,
999
- routeMap,
1000
- routeName: match.routeName,
1001
- });
1002
- const renderDuration = performance.now() - renderStart;
1003
- serverTiming = match.serverTiming
1004
- ? `${match.serverTiming}, rendering;dur=${renderDuration.toFixed(2)}`
1005
- : `rendering;dur=${renderDuration.toFixed(2)}`;
1006
-
1007
- payload = {
1008
- root,
1009
- metadata: {
1010
- pathname: url.pathname,
1011
- segments: match.segments,
1012
- matched: match.matched,
1013
- diff: match.diff,
1014
- isPartial: false,
1015
- rootLayout: router.rootLayout,
1016
- handles: handleStore.stream(),
1017
- version,
1018
- themeConfig: router.themeConfig,
1019
- initialTheme: requireRequestContext().theme,
1020
- routeMap,
1021
- routeName: match.routeName,
1022
- },
1023
- };
1024
- }
1025
-
1026
- // Serialize to RSC stream
1027
- const rscStream = renderToReadableStream<RscPayload>(payload);
1028
-
1029
- // Determine if this is an RSC request or HTML request
1030
- const isRscRequest =
1031
- (!request.headers.get("accept")?.includes("text/html") &&
1032
- !url.searchParams.has("__html")) ||
1033
- url.searchParams.has("__rsc");
1034
-
1035
- if (isRscRequest) {
1036
- const rscHeaders: Record<string, string> = {
1037
- "content-type": "text/x-component;charset=utf-8",
1038
- vary: "accept",
1039
- };
1040
- if (serverTiming) {
1041
- rscHeaders["Server-Timing"] = serverTiming;
1042
- }
1043
- return createResponseWithMergedHeaders(rscStream, {
1044
- headers: rscHeaders,
1045
- });
1046
- }
1047
-
1048
- // Delegate to SSR for HTML response
1049
- const ssrModule = await loadSSRModule();
1050
- const htmlStream = await ssrModule.renderHTML(rscStream, { nonce });
1051
-
1052
- const htmlHeaders: Record<string, string> = {
1053
- "content-type": "text/html;charset=utf-8",
1054
- };
1055
- if (serverTiming) {
1056
- htmlHeaders["Server-Timing"] = serverTiming;
1057
- }
1058
-
1059
- return createResponseWithMergedHeaders(htmlStream, {
1060
- headers: htmlHeaders,
1061
- });
1062
- }
1063
1089
  }