@rangojs/router 0.0.0-experimental.0f44aca1

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 (305) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +899 -0
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +5214 -0
  5. package/package.json +176 -0
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +220 -0
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +112 -0
  11. package/skills/document-cache/SKILL.md +182 -0
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +704 -0
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +313 -0
  16. package/skills/layout/SKILL.md +310 -0
  17. package/skills/links/SKILL.md +239 -0
  18. package/skills/loader/SKILL.md +596 -0
  19. package/skills/middleware/SKILL.md +339 -0
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +305 -0
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +118 -0
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +385 -0
  26. package/skills/router-setup/SKILL.md +439 -0
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +79 -0
  29. package/skills/typesafety/SKILL.md +623 -0
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +273 -0
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/event-controller.ts +899 -0
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/index.ts +18 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +141 -0
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +134 -0
  42. package/src/browser/navigation-bridge.ts +645 -0
  43. package/src/browser/navigation-client.ts +215 -0
  44. package/src/browser/navigation-store.ts +806 -0
  45. package/src/browser/navigation-transaction.ts +295 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +550 -0
  48. package/src/browser/prefetch/cache.ts +146 -0
  49. package/src/browser/prefetch/fetch.ts +135 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +42 -0
  52. package/src/browser/prefetch/queue.ts +88 -0
  53. package/src/browser/rango-state.ts +112 -0
  54. package/src/browser/react/Link.tsx +360 -0
  55. package/src/browser/react/NavigationProvider.tsx +386 -0
  56. package/src/browser/react/ScrollRestoration.tsx +94 -0
  57. package/src/browser/react/context.ts +59 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +52 -0
  60. package/src/browser/react/location-state-shared.ts +162 -0
  61. package/src/browser/react/location-state.ts +107 -0
  62. package/src/browser/react/mount-context.ts +37 -0
  63. package/src/browser/react/nonce-context.ts +23 -0
  64. package/src/browser/react/shallow-equal.ts +27 -0
  65. package/src/browser/react/use-action.ts +218 -0
  66. package/src/browser/react/use-client-cache.ts +58 -0
  67. package/src/browser/react/use-handle.ts +162 -0
  68. package/src/browser/react/use-href.tsx +40 -0
  69. package/src/browser/react/use-link-status.ts +135 -0
  70. package/src/browser/react/use-mount.ts +31 -0
  71. package/src/browser/react/use-navigation.ts +99 -0
  72. package/src/browser/react/use-params.ts +65 -0
  73. package/src/browser/react/use-pathname.ts +47 -0
  74. package/src/browser/react/use-router.ts +63 -0
  75. package/src/browser/react/use-search-params.ts +56 -0
  76. package/src/browser/react/use-segments.ts +171 -0
  77. package/src/browser/response-adapter.ts +73 -0
  78. package/src/browser/rsc-router.tsx +431 -0
  79. package/src/browser/scroll-restoration.ts +400 -0
  80. package/src/browser/segment-reconciler.ts +216 -0
  81. package/src/browser/segment-structure-assert.ts +83 -0
  82. package/src/browser/server-action-bridge.ts +667 -0
  83. package/src/browser/shallow.ts +40 -0
  84. package/src/browser/types.ts +538 -0
  85. package/src/browser/validate-redirect-origin.ts +29 -0
  86. package/src/build/generate-manifest.ts +438 -0
  87. package/src/build/generate-route-types.ts +36 -0
  88. package/src/build/index.ts +35 -0
  89. package/src/build/route-trie.ts +265 -0
  90. package/src/build/route-types/ast-helpers.ts +25 -0
  91. package/src/build/route-types/ast-route-extraction.ts +98 -0
  92. package/src/build/route-types/codegen.ts +102 -0
  93. package/src/build/route-types/include-resolution.ts +411 -0
  94. package/src/build/route-types/param-extraction.ts +48 -0
  95. package/src/build/route-types/per-module-writer.ts +128 -0
  96. package/src/build/route-types/router-processing.ts +469 -0
  97. package/src/build/route-types/scan-filter.ts +78 -0
  98. package/src/build/runtime-discovery.ts +231 -0
  99. package/src/cache/background-task.ts +34 -0
  100. package/src/cache/cache-key-utils.ts +44 -0
  101. package/src/cache/cache-policy.ts +125 -0
  102. package/src/cache/cache-runtime.ts +338 -0
  103. package/src/cache/cache-scope.ts +382 -0
  104. package/src/cache/cf/cf-cache-store.ts +540 -0
  105. package/src/cache/cf/index.ts +25 -0
  106. package/src/cache/document-cache.ts +369 -0
  107. package/src/cache/handle-capture.ts +81 -0
  108. package/src/cache/handle-snapshot.ts +41 -0
  109. package/src/cache/index.ts +43 -0
  110. package/src/cache/memory-segment-store.ts +328 -0
  111. package/src/cache/profile-registry.ts +73 -0
  112. package/src/cache/read-through-swr.ts +134 -0
  113. package/src/cache/segment-codec.ts +256 -0
  114. package/src/cache/taint.ts +98 -0
  115. package/src/cache/types.ts +342 -0
  116. package/src/client.rsc.tsx +85 -0
  117. package/src/client.tsx +601 -0
  118. package/src/component-utils.ts +76 -0
  119. package/src/components/DefaultDocument.tsx +27 -0
  120. package/src/context-var.ts +86 -0
  121. package/src/debug.ts +243 -0
  122. package/src/default-error-boundary.tsx +88 -0
  123. package/src/deps/browser.ts +8 -0
  124. package/src/deps/html-stream-client.ts +2 -0
  125. package/src/deps/html-stream-server.ts +2 -0
  126. package/src/deps/rsc.ts +10 -0
  127. package/src/deps/ssr.ts +2 -0
  128. package/src/errors.ts +365 -0
  129. package/src/handle.ts +135 -0
  130. package/src/handles/MetaTags.tsx +246 -0
  131. package/src/handles/breadcrumbs.ts +66 -0
  132. package/src/handles/index.ts +7 -0
  133. package/src/handles/meta.ts +264 -0
  134. package/src/host/cookie-handler.ts +165 -0
  135. package/src/host/errors.ts +97 -0
  136. package/src/host/index.ts +53 -0
  137. package/src/host/pattern-matcher.ts +214 -0
  138. package/src/host/router.ts +352 -0
  139. package/src/host/testing.ts +79 -0
  140. package/src/host/types.ts +146 -0
  141. package/src/host/utils.ts +25 -0
  142. package/src/href-client.ts +222 -0
  143. package/src/index.rsc.ts +233 -0
  144. package/src/index.ts +277 -0
  145. package/src/internal-debug.ts +11 -0
  146. package/src/loader.rsc.ts +89 -0
  147. package/src/loader.ts +64 -0
  148. package/src/network-error-thrower.tsx +23 -0
  149. package/src/outlet-context.ts +15 -0
  150. package/src/outlet-provider.tsx +45 -0
  151. package/src/prerender/param-hash.ts +37 -0
  152. package/src/prerender/store.ts +185 -0
  153. package/src/prerender.ts +463 -0
  154. package/src/reverse.ts +330 -0
  155. package/src/root-error-boundary.tsx +289 -0
  156. package/src/route-content-wrapper.tsx +196 -0
  157. package/src/route-definition/dsl-helpers.ts +934 -0
  158. package/src/route-definition/helper-factories.ts +200 -0
  159. package/src/route-definition/helpers-types.ts +430 -0
  160. package/src/route-definition/index.ts +52 -0
  161. package/src/route-definition/redirect.ts +93 -0
  162. package/src/route-definition.ts +1 -0
  163. package/src/route-map-builder.ts +275 -0
  164. package/src/route-name.ts +53 -0
  165. package/src/route-types.ts +259 -0
  166. package/src/router/content-negotiation.ts +116 -0
  167. package/src/router/debug-manifest.ts +72 -0
  168. package/src/router/error-handling.ts +287 -0
  169. package/src/router/find-match.ts +158 -0
  170. package/src/router/handler-context.ts +451 -0
  171. package/src/router/intercept-resolution.ts +395 -0
  172. package/src/router/lazy-includes.ts +234 -0
  173. package/src/router/loader-resolution.ts +420 -0
  174. package/src/router/logging.ts +248 -0
  175. package/src/router/manifest.ts +267 -0
  176. package/src/router/match-api.ts +620 -0
  177. package/src/router/match-context.ts +266 -0
  178. package/src/router/match-handlers.ts +440 -0
  179. package/src/router/match-middleware/background-revalidation.ts +223 -0
  180. package/src/router/match-middleware/cache-lookup.ts +634 -0
  181. package/src/router/match-middleware/cache-store.ts +295 -0
  182. package/src/router/match-middleware/index.ts +81 -0
  183. package/src/router/match-middleware/intercept-resolution.ts +306 -0
  184. package/src/router/match-middleware/segment-resolution.ts +192 -0
  185. package/src/router/match-pipelines.ts +179 -0
  186. package/src/router/match-result.ts +219 -0
  187. package/src/router/metrics.ts +282 -0
  188. package/src/router/middleware-cookies.ts +55 -0
  189. package/src/router/middleware-types.ts +222 -0
  190. package/src/router/middleware.ts +748 -0
  191. package/src/router/pattern-matching.ts +563 -0
  192. package/src/router/prerender-match.ts +402 -0
  193. package/src/router/preview-match.ts +170 -0
  194. package/src/router/revalidation.ts +289 -0
  195. package/src/router/router-context.ts +316 -0
  196. package/src/router/router-interfaces.ts +452 -0
  197. package/src/router/router-options.ts +592 -0
  198. package/src/router/router-registry.ts +24 -0
  199. package/src/router/segment-resolution/fresh.ts +570 -0
  200. package/src/router/segment-resolution/helpers.ts +263 -0
  201. package/src/router/segment-resolution/loader-cache.ts +198 -0
  202. package/src/router/segment-resolution/revalidation.ts +1239 -0
  203. package/src/router/segment-resolution/static-store.ts +67 -0
  204. package/src/router/segment-resolution.ts +21 -0
  205. package/src/router/segment-wrappers.ts +289 -0
  206. package/src/router/telemetry-otel.ts +299 -0
  207. package/src/router/telemetry.ts +300 -0
  208. package/src/router/timeout.ts +148 -0
  209. package/src/router/trie-matching.ts +239 -0
  210. package/src/router/types.ts +170 -0
  211. package/src/router.ts +1002 -0
  212. package/src/rsc/handler-context.ts +45 -0
  213. package/src/rsc/handler.ts +1089 -0
  214. package/src/rsc/helpers.ts +198 -0
  215. package/src/rsc/index.ts +36 -0
  216. package/src/rsc/loader-fetch.ts +209 -0
  217. package/src/rsc/manifest-init.ts +86 -0
  218. package/src/rsc/nonce.ts +32 -0
  219. package/src/rsc/origin-guard.ts +141 -0
  220. package/src/rsc/progressive-enhancement.ts +379 -0
  221. package/src/rsc/response-error.ts +37 -0
  222. package/src/rsc/response-route-handler.ts +347 -0
  223. package/src/rsc/rsc-rendering.ts +235 -0
  224. package/src/rsc/runtime-warnings.ts +42 -0
  225. package/src/rsc/server-action.ts +348 -0
  226. package/src/rsc/ssr-setup.ts +128 -0
  227. package/src/rsc/types.ts +263 -0
  228. package/src/search-params.ts +230 -0
  229. package/src/segment-system.tsx +454 -0
  230. package/src/server/context.ts +591 -0
  231. package/src/server/cookie-store.ts +190 -0
  232. package/src/server/fetchable-loader-store.ts +37 -0
  233. package/src/server/handle-store.ts +308 -0
  234. package/src/server/loader-registry.ts +133 -0
  235. package/src/server/request-context.ts +914 -0
  236. package/src/server/root-layout.tsx +10 -0
  237. package/src/server/tsconfig.json +14 -0
  238. package/src/server.ts +51 -0
  239. package/src/ssr/index.tsx +365 -0
  240. package/src/static-handler.ts +114 -0
  241. package/src/theme/ThemeProvider.tsx +297 -0
  242. package/src/theme/ThemeScript.tsx +61 -0
  243. package/src/theme/constants.ts +62 -0
  244. package/src/theme/index.ts +48 -0
  245. package/src/theme/theme-context.ts +44 -0
  246. package/src/theme/theme-script.ts +155 -0
  247. package/src/theme/types.ts +182 -0
  248. package/src/theme/use-theme.ts +44 -0
  249. package/src/types/boundaries.ts +158 -0
  250. package/src/types/cache-types.ts +198 -0
  251. package/src/types/error-types.ts +192 -0
  252. package/src/types/global-namespace.ts +100 -0
  253. package/src/types/handler-context.ts +687 -0
  254. package/src/types/index.ts +88 -0
  255. package/src/types/loader-types.ts +183 -0
  256. package/src/types/route-config.ts +170 -0
  257. package/src/types/route-entry.ts +102 -0
  258. package/src/types/segments.ts +148 -0
  259. package/src/types.ts +1 -0
  260. package/src/urls/include-helper.ts +197 -0
  261. package/src/urls/index.ts +53 -0
  262. package/src/urls/path-helper-types.ts +339 -0
  263. package/src/urls/path-helper.ts +329 -0
  264. package/src/urls/pattern-types.ts +95 -0
  265. package/src/urls/response-types.ts +106 -0
  266. package/src/urls/type-extraction.ts +372 -0
  267. package/src/urls/urls-function.ts +98 -0
  268. package/src/urls.ts +1 -0
  269. package/src/use-loader.tsx +354 -0
  270. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  271. package/src/vite/discovery/discover-routers.ts +344 -0
  272. package/src/vite/discovery/prerender-collection.ts +385 -0
  273. package/src/vite/discovery/route-types-writer.ts +258 -0
  274. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  275. package/src/vite/discovery/state.ts +110 -0
  276. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  277. package/src/vite/index.ts +16 -0
  278. package/src/vite/plugin-types.ts +131 -0
  279. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  280. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  281. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  282. package/src/vite/plugins/expose-action-id.ts +365 -0
  283. package/src/vite/plugins/expose-id-utils.ts +287 -0
  284. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  285. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  286. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  287. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  288. package/src/vite/plugins/expose-ids/types.ts +45 -0
  289. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  290. package/src/vite/plugins/refresh-cmd.ts +65 -0
  291. package/src/vite/plugins/use-cache-transform.ts +323 -0
  292. package/src/vite/plugins/version-injector.ts +83 -0
  293. package/src/vite/plugins/version-plugin.ts +254 -0
  294. package/src/vite/plugins/version.d.ts +12 -0
  295. package/src/vite/plugins/virtual-entries.ts +123 -0
  296. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  297. package/src/vite/rango.ts +510 -0
  298. package/src/vite/router-discovery.ts +785 -0
  299. package/src/vite/utils/ast-handler-extract.ts +517 -0
  300. package/src/vite/utils/banner.ts +36 -0
  301. package/src/vite/utils/bundle-analysis.ts +137 -0
  302. package/src/vite/utils/manifest-utils.ts +70 -0
  303. package/src/vite/utils/package-resolution.ts +121 -0
  304. package/src/vite/utils/prerender-utils.ts +189 -0
  305. package/src/vite/utils/shared-utils.ts +169 -0
@@ -0,0 +1,347 @@
1
+ /**
2
+ * Response Route Handler
3
+ *
4
+ * Handles response routes (JSON, text, HTML, XML, markdown, image, stream)
5
+ * that bypass the RSC rendering pipeline entirely. Includes content-type
6
+ * dispatch, route middleware execution, and response caching with SWR.
7
+ */
8
+
9
+ import { RouterError } from "../errors.js";
10
+ import { requireRequestContext } from "../server/request-context.js";
11
+ import { contextGet } from "../context-var.js";
12
+ import { NOCACHE_SYMBOL } from "../cache/taint.js";
13
+ import { traverseBack } from "../router/pattern-matching.js";
14
+ import { createCacheScope } from "../cache/cache-scope.js";
15
+ import { executeMiddleware } from "../router/middleware.js";
16
+ import {
17
+ createReverseFunction,
18
+ stripInternalParams,
19
+ } from "../router/handler-context.js";
20
+ import type { MiddlewareFn } from "../router/middleware.js";
21
+ import type { EntryData } from "../server/context.js";
22
+ import type { HandlerContext } from "./handler-context.js";
23
+ import { createResponseErrorPayload } from "./response-error.js";
24
+ import {
25
+ createResponseWithMergedHeaders,
26
+ finalizeResponse,
27
+ isCacheableStatus,
28
+ buildRouteMiddlewareEntries,
29
+ } from "./helpers.js";
30
+
31
+ export interface ResponseRouteMatch {
32
+ responseType: string;
33
+ handler: Function;
34
+ params?: Record<string, string>;
35
+ negotiated?: boolean;
36
+ manifestEntry?: EntryData;
37
+ routeMiddleware?: Array<{
38
+ handler: MiddlewareFn;
39
+ params: Record<string, string>;
40
+ }>;
41
+ }
42
+
43
+ /**
44
+ * Handle a response route (non-RSC). Dispatches by content type, wraps
45
+ * with route middleware and response caching when configured.
46
+ *
47
+ * For partial (client-side navigation) requests, returns X-RSC-Reload
48
+ * so the browser triggers a hard navigation to the response route URL.
49
+ */
50
+ export async function handleResponseRoute<TEnv>(
51
+ handlerCtx: HandlerContext<TEnv>,
52
+ preview: ResponseRouteMatch,
53
+ request: Request,
54
+ env: TEnv,
55
+ url: URL,
56
+ variables: Record<string, any>,
57
+ ): Promise<Response> {
58
+ const isPartial = url.searchParams.has("_rsc_partial");
59
+
60
+ // Partial requests (client-side navigation) to response routes
61
+ // get X-RSC-Reload to trigger hard navigation in the browser
62
+ if (isPartial) {
63
+ return createResponseWithMergedHeaders(null, {
64
+ status: 200,
65
+ headers: {
66
+ "X-RSC-Reload": stripInternalParams(url).toString(),
67
+ "content-type": "text/x-component;charset=utf-8",
68
+ },
69
+ });
70
+ }
71
+
72
+ // Build lightweight context for response handler
73
+ const reqCtx = requireRequestContext();
74
+ const cleanUrl = stripInternalParams(url);
75
+ const responseHandlerCtx = {
76
+ request,
77
+ params: preview.params || {},
78
+ env,
79
+ searchParams: cleanUrl.searchParams,
80
+ url: cleanUrl,
81
+ pathname: url.pathname,
82
+ reverse: createReverseFunction(handlerCtx.getRequiredRouteMap()),
83
+ get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
84
+ header: (name: string, value: string) => reqCtx.header(name, value),
85
+ _responseType: preview.responseType,
86
+ };
87
+ // Brand with taint symbol so "use cache" detects it as request-scoped
88
+ // and extracts route-identifying properties (params, pathname, _responseType)
89
+ (responseHandlerCtx as any)[NOCACHE_SYMBOL] = true;
90
+
91
+ // Call handler directly, wrapped by route middleware if present
92
+ const callHandler = async () => {
93
+ const errorCtx = { request, url, env };
94
+
95
+ // Re-wrap a handler-returned Response through createResponseWithMergedHeaders
96
+ // so that stub headers (cookies, custom headers set via ctx.header()) are included.
97
+ // Use Headers (not Record<string, string>) to preserve duplicate entries like Set-Cookie.
98
+ const rewrapResponse = (result: Response) => {
99
+ const headers = new Headers();
100
+ result.headers.forEach((value, key) => {
101
+ if (key.toLowerCase() === "set-cookie") {
102
+ headers.append(key, value);
103
+ } else {
104
+ headers.set(key, value);
105
+ }
106
+ });
107
+ return createResponseWithMergedHeaders(result.body, {
108
+ status: result.status,
109
+ headers,
110
+ });
111
+ };
112
+
113
+ // JSON response routes: wrap in { data } / { error } envelope
114
+ if (preview.responseType === "json") {
115
+ try {
116
+ const result = await (preview.handler as Function)(responseHandlerCtx);
117
+ if (result instanceof Response) {
118
+ return rewrapResponse(result);
119
+ }
120
+ return createResponseWithMergedHeaders(
121
+ JSON.stringify({ data: result }),
122
+ {
123
+ status: 200,
124
+ headers: { "content-type": "application/json;charset=utf-8" },
125
+ },
126
+ );
127
+ } catch (error) {
128
+ handlerCtx.callOnError(error, "handler", errorCtx);
129
+ const isDev = process.env.NODE_ENV !== "production";
130
+ const status = error instanceof RouterError ? error.status : 500;
131
+ return createResponseWithMergedHeaders(
132
+ JSON.stringify({
133
+ error: createResponseErrorPayload(error, isDev),
134
+ }),
135
+ {
136
+ status,
137
+ headers: { "content-type": "application/json;charset=utf-8" },
138
+ },
139
+ );
140
+ }
141
+ }
142
+
143
+ // Non-JSON response routes: catch errors and return plain Response
144
+ try {
145
+ const result = await (preview.handler as Function)(responseHandlerCtx);
146
+
147
+ if (result instanceof Response) {
148
+ return rewrapResponse(result);
149
+ }
150
+
151
+ // Auto-wrap based on response type tag
152
+ switch (preview.responseType) {
153
+ case "text":
154
+ return createResponseWithMergedHeaders(String(result), {
155
+ status: 200,
156
+ headers: { "content-type": "text/plain;charset=utf-8" },
157
+ });
158
+ case "html":
159
+ return createResponseWithMergedHeaders(String(result), {
160
+ status: 200,
161
+ headers: { "content-type": "text/html;charset=utf-8" },
162
+ });
163
+ case "xml":
164
+ return createResponseWithMergedHeaders(String(result), {
165
+ status: 200,
166
+ headers: { "content-type": "application/xml;charset=utf-8" },
167
+ });
168
+ case "md":
169
+ return createResponseWithMergedHeaders(String(result), {
170
+ status: 200,
171
+ headers: { "content-type": "text/markdown;charset=utf-8" },
172
+ });
173
+ default:
174
+ // image, stream, any -- must return Response
175
+ throw new Error(
176
+ `Response route handler for "${preview.responseType}" must return a Response object, got ${typeof result}`,
177
+ );
178
+ }
179
+ } catch (error) {
180
+ handlerCtx.callOnError(error, "handler", errorCtx);
181
+ const isDev = process.env.NODE_ENV !== "production";
182
+ const status = error instanceof RouterError ? error.status : 500;
183
+ const message =
184
+ error instanceof RouterError
185
+ ? error.message
186
+ : isDev && error instanceof Error
187
+ ? error.message
188
+ : "Internal Server Error";
189
+ return createResponseWithMergedHeaders(message, {
190
+ status,
191
+ headers: { "content-type": "text/plain;charset=utf-8" },
192
+ });
193
+ }
194
+ };
195
+
196
+ // Wrap callHandler to append Vary: Accept on content-negotiated responses
197
+ const callHandlerWithVary = async () => {
198
+ const response = await callHandler();
199
+ if (preview.negotiated) {
200
+ response.headers.append("Vary", "Accept");
201
+ }
202
+ return response;
203
+ };
204
+
205
+ // Wrap with route middleware if present
206
+ const executeHandler = async () => {
207
+ if (preview.routeMiddleware && preview.routeMiddleware.length > 0) {
208
+ return executeMiddleware(
209
+ buildRouteMiddlewareEntries<TEnv>(preview.routeMiddleware),
210
+ request,
211
+ env,
212
+ variables,
213
+ callHandlerWithVary,
214
+ createReverseFunction(handlerCtx.getRequiredRouteMap()),
215
+ );
216
+ }
217
+ return callHandlerWithVary();
218
+ };
219
+
220
+ // Resolve cache config from entry tree (same pattern as match-api.ts)
221
+ if (preview.manifestEntry) {
222
+ let cacheScope: ReturnType<typeof createCacheScope> = null;
223
+ for (const entry of traverseBack(preview.manifestEntry)) {
224
+ if (entry.cache) {
225
+ cacheScope = createCacheScope(entry.cache, cacheScope);
226
+ }
227
+ }
228
+
229
+ if (cacheScope?.enabled) {
230
+ // Evaluate condition — skip response cache when condition returns false
231
+ let conditionPassed = true;
232
+ if (cacheScope.config !== false && cacheScope.config.condition) {
233
+ try {
234
+ conditionPassed = !!cacheScope.config.condition(reqCtx);
235
+ } catch {
236
+ conditionPassed = false;
237
+ }
238
+ }
239
+
240
+ const store = cacheScope.getStore() ?? reqCtx._cacheStore;
241
+ if (conditionPassed && store?.getResponse && store?.putResponse) {
242
+ // Build cache key with response:{type}: prefix to avoid collision
243
+ // with segment keys and differentiate between response types.
244
+ // Include host and url.search so query-driven and multi-host
245
+ // responses cache separately.
246
+ let cacheKey = `response:${preview.responseType}:${url.host}${url.pathname}${url.search}`;
247
+
248
+ // Priority 1: Route-level key function (full override)
249
+ if (cacheScope.config !== false && cacheScope.config.key) {
250
+ try {
251
+ const customKey = await cacheScope.config.key(reqCtx);
252
+ cacheKey = `response:${customKey}`;
253
+ } catch {
254
+ // Fall back to default key on route-level key failure
255
+ }
256
+ } else if (store.keyGenerator) {
257
+ // Priority 2: Store-level keyGenerator (modifies default key)
258
+ try {
259
+ cacheKey = await store.keyGenerator(reqCtx, cacheKey);
260
+ } catch {
261
+ // Fall back to default key on keyGenerator failure
262
+ }
263
+ }
264
+
265
+ // Save pre-handler callbacks (registered by app-level middleware
266
+ // before we reach the cache block) and clear the live array.
267
+ // createResponseWithMergedHeaders (inside the handler) eagerly
268
+ // executes any callbacks present in _onResponseCallbacks, so
269
+ // handler-registered callbacks are baked into the handler's
270
+ // response and the cached artifact. Pre-handler callbacks are
271
+ // NOT in the live array during execution, so they are applied
272
+ // once per serve on every path (hit + miss) below.
273
+ const savedCallbacks = reqCtx._onResponseCallbacks;
274
+ reqCtx._onResponseCallbacks = [];
275
+
276
+ const applyPreHandlerCallbacks = (response: Response): Response => {
277
+ let result = response;
278
+ for (const callback of savedCallbacks) {
279
+ result = callback(result) ?? result;
280
+ }
281
+ return result;
282
+ };
283
+
284
+ try {
285
+ const cached = await store.getResponse(cacheKey);
286
+
287
+ if (cached && isCacheableStatus(cached.response.status)) {
288
+ if (!cached.shouldRevalidate) {
289
+ // Fresh hit
290
+ return applyPreHandlerCallbacks(cached.response);
291
+ }
292
+
293
+ // Stale hit (SWR) - return cached, revalidate in background
294
+ reqCtx.waitUntil(async () => {
295
+ try {
296
+ // finalizeResponse drains any onResponse callbacks registered
297
+ // during middleware execution (e.g. middleware short-circuit)
298
+ // that createResponseWithMergedHeaders didn't reach.
299
+ const fresh = finalizeResponse(await executeHandler());
300
+ if (isCacheableStatus(fresh.status)) {
301
+ await store.putResponse!(
302
+ cacheKey,
303
+ fresh.clone(),
304
+ cacheScope!.ttl,
305
+ cacheScope!.swr,
306
+ );
307
+ }
308
+ } catch (error) {
309
+ console.error(`[ResponseCache] Revalidation failed:`, error);
310
+ }
311
+ });
312
+
313
+ return applyPreHandlerCallbacks(cached.response);
314
+ }
315
+ } catch (error) {
316
+ console.error(`[ResponseCache] Cache lookup failed:`, error);
317
+ }
318
+
319
+ // Cache miss - execute handler and cache the result.
320
+ // createResponseWithMergedHeaders inside the handler drains callbacks
321
+ // registered during handler execution. finalizeResponse catches any
322
+ // remaining callbacks (e.g. from middleware short-circuit where the
323
+ // handler never ran) so the cached artifact includes all transforms.
324
+ const response = finalizeResponse(await executeHandler());
325
+
326
+ if (isCacheableStatus(response.status)) {
327
+ reqCtx.waitUntil(async () => {
328
+ try {
329
+ await store.putResponse!(
330
+ cacheKey,
331
+ response.clone(),
332
+ cacheScope!.ttl,
333
+ cacheScope!.swr,
334
+ );
335
+ } catch (error) {
336
+ console.error(`[ResponseCache] Cache write failed:`, error);
337
+ }
338
+ });
339
+ }
340
+
341
+ return applyPreHandlerCallbacks(response);
342
+ }
343
+ }
344
+ }
345
+
346
+ return executeHandler().then(finalizeResponse);
347
+ }
@@ -0,0 +1,235 @@
1
+ /**
2
+ * RSC Rendering Handler (Navigation)
3
+ *
4
+ * Handles RSC rendering for both partial (client-side navigation) and full
5
+ * (initial page load) requests. Includes prerender collection for build-time
6
+ * static generation.
7
+ */
8
+
9
+ import {
10
+ requireRequestContext,
11
+ setRequestContextParams,
12
+ getLocationState,
13
+ } from "../server/request-context.js";
14
+ import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
15
+ import { appendMetric } from "../router/metrics.js";
16
+ import { getSSRSetup } from "./ssr-setup.js";
17
+ import type { RscPayload } from "./types.js";
18
+ import {
19
+ createResponseWithMergedHeaders,
20
+ createSimpleRedirectResponse,
21
+ } from "./helpers.js";
22
+ import type { HandlerContext } from "./handler-context.js";
23
+
24
+ export async function handleRscRendering<TEnv>(
25
+ ctx: HandlerContext<TEnv>,
26
+ request: Request,
27
+ env: TEnv,
28
+ url: URL,
29
+ isPartial: boolean,
30
+ handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
31
+ nonce: string | undefined,
32
+ ): Promise<Response> {
33
+ const reqCtx = requireRequestContext();
34
+
35
+ let payload: RscPayload;
36
+ let hasInterceptSlots = false;
37
+
38
+ if (isPartial) {
39
+ // Partial render (navigation)
40
+ const result = await ctx.router.matchPartial(request, { env });
41
+
42
+ if (!result) {
43
+ // Fall back to full render
44
+ const match = await ctx.router.match(request, { env });
45
+ setRequestContextParams(match.params, match.routeName);
46
+
47
+ if (match.redirect) {
48
+ // Partial request: use X-RSC-Redirect header so the client can
49
+ // perform SPA navigation. A raw 308 would be auto-followed by
50
+ // fetch, hitting the target without _rsc_partial.
51
+ return createSimpleRedirectResponse(match.redirect);
52
+ }
53
+
54
+ payload = {
55
+ metadata: {
56
+ pathname: url.pathname,
57
+ segments: match.segments,
58
+ matched: match.matched,
59
+ diff: match.diff,
60
+ params: match.params,
61
+ isPartial: false,
62
+ rootLayout: ctx.router.rootLayout,
63
+ handles: handleStore.stream(),
64
+ version: ctx.version,
65
+ prefetchCacheTTL: ctx.router.prefetchCacheTTL,
66
+ themeConfig: ctx.router.themeConfig,
67
+ initialTheme: reqCtx.theme,
68
+ },
69
+ };
70
+ } else {
71
+ setRequestContextParams(result.params, result.routeName);
72
+
73
+ hasInterceptSlots = !!result.slots;
74
+
75
+ payload = {
76
+ metadata: {
77
+ pathname: url.pathname,
78
+ segments: result.segments,
79
+ matched: result.matched,
80
+ diff: result.diff,
81
+ params: result.params,
82
+ isPartial: true,
83
+ slots: result.slots,
84
+ handles: handleStore.stream(),
85
+ version: ctx.version,
86
+ },
87
+ };
88
+ }
89
+ } else {
90
+ // Full render (initial page load)
91
+ const match = await ctx.router.match(request, { env });
92
+ setRequestContextParams(match.params, match.routeName);
93
+
94
+ if (match.redirect) {
95
+ return createResponseWithMergedHeaders(null, {
96
+ status: 308,
97
+ headers: { Location: match.redirect },
98
+ });
99
+ }
100
+
101
+ // Caching is now handled in router.match() via cache provider in request context
102
+ // match.segments already contains cached or fresh segments as appropriate
103
+
104
+ if (url.searchParams.has("__prerender_collect")) {
105
+ // Build-time prerender collection: serialize segments and handle data
106
+ // to JSON for storage as build artifacts. At runtime the worker
107
+ // deserializes these and feeds them through the normal segment pipeline.
108
+ const nonLoaderSegments = match.segments.filter(
109
+ (s) => s.type !== "loader",
110
+ );
111
+ handleStore.seal();
112
+ await handleStore.settled;
113
+ const { serializeSegments } = await import("../cache/segment-codec.js");
114
+ const serializedSegments = await serializeSegments(nonLoaderSegments);
115
+ const handles: Record<string, Record<string, unknown[]>> = {};
116
+ for (const seg of nonLoaderSegments) {
117
+ const segHandles = handleStore.getDataForSegment(seg.id);
118
+ if (Object.keys(segHandles).length > 0) {
119
+ handles[seg.id] = segHandles;
120
+ }
121
+ }
122
+ return new Response(
123
+ JSON.stringify({
124
+ segments: serializedSegments,
125
+ handles,
126
+ routeName: match.routeName,
127
+ params: match.params,
128
+ }),
129
+ { headers: { "Content-Type": "application/json" } },
130
+ );
131
+ } else {
132
+ payload = {
133
+ // Initial SSR can reconstruct the tree from segments + rootLayout,
134
+ // so we omit root to avoid sending the same structure twice.
135
+
136
+ metadata: {
137
+ pathname: url.pathname,
138
+ segments: match.segments,
139
+ matched: match.matched,
140
+ diff: match.diff,
141
+ params: match.params,
142
+ isPartial: false,
143
+ rootLayout: ctx.router.rootLayout,
144
+ handles: handleStore.stream(),
145
+ version: ctx.version,
146
+ themeConfig: ctx.router.themeConfig,
147
+ initialTheme: reqCtx.theme,
148
+ },
149
+ };
150
+ }
151
+ }
152
+
153
+ // For partial requests, include any server-set location state in the payload.
154
+ // SSR (full page) requests ignore location state since there's no history.state
155
+ // to write to on a fresh page load.
156
+ if (isPartial && payload.metadata) {
157
+ const locationState = getLocationState();
158
+ if (locationState) {
159
+ payload.metadata.locationState =
160
+ resolveLocationStateEntries(locationState);
161
+ }
162
+ }
163
+
164
+ const metricsStore = reqCtx._metricsStore;
165
+ const renderStart = performance.now();
166
+
167
+ // Serialize to RSC stream
168
+ const rscSerializeStart = performance.now();
169
+ const rscStream = ctx.renderToReadableStream<RscPayload>(payload);
170
+ const rscSerializeDur = performance.now() - rscSerializeStart;
171
+ // This measures synchronous stream creation, not end-to-end stream consumption.
172
+ appendMetric(
173
+ metricsStore,
174
+ "rsc-serialize",
175
+ rscSerializeStart,
176
+ rscSerializeDur,
177
+ );
178
+
179
+ // Determine if this is an RSC request or HTML request.
180
+ // Partial requests (_rsc_partial) are always RSC -- they come from client-side
181
+ // navigation or prefetch fetch(). We cannot rely on Accept alone since some
182
+ // browsers may send Accept: text/html for non-HTML requests.
183
+ const isRscRequest =
184
+ isPartial ||
185
+ (!request.headers.get("accept")?.includes("text/html") &&
186
+ !url.searchParams.has("__html")) ||
187
+ url.searchParams.has("__rsc");
188
+
189
+ if (isRscRequest) {
190
+ const renderDur = performance.now() - renderStart;
191
+ appendMetric(metricsStore, "render:total", renderStart, renderDur);
192
+ const rscHeaders: Record<string, string> = {
193
+ "content-type": "text/x-component;charset=utf-8",
194
+ vary: "accept, X-Rango-State, X-RSC-Router-Client-Path",
195
+ };
196
+ // Enable browser HTTP caching for prefetch responses only.
197
+ // Requires X-Rango-Prefetch header (sent by Link prefetch fetch),
198
+ // non-intercept context (intercept responses depend on source page),
199
+ // and a configured cache-control value (false disables caching).
200
+ const isPrefetch = request.headers.has("X-Rango-Prefetch");
201
+ if (isPrefetch && isPartial && !hasInterceptSlots) {
202
+ const cc = ctx.router.prefetchCacheControl;
203
+ if (cc) {
204
+ rscHeaders["cache-control"] = cc;
205
+ }
206
+ }
207
+ return createResponseWithMergedHeaders(rscStream, {
208
+ headers: rscHeaders,
209
+ });
210
+ }
211
+
212
+ // Delegate to SSR for HTML response (reuse early setup if available)
213
+ const [ssrModule, streamMode] = await getSSRSetup(
214
+ ctx,
215
+ request,
216
+ env,
217
+ url,
218
+ metricsStore,
219
+ );
220
+
221
+ const ssrRenderStart = performance.now();
222
+ const htmlStream = await ssrModule.renderHTML(rscStream, {
223
+ nonce,
224
+ streamMode,
225
+ });
226
+ const ssrRenderDur = performance.now() - ssrRenderStart;
227
+ appendMetric(metricsStore, "ssr-render-html", ssrRenderStart, ssrRenderDur);
228
+
229
+ const renderDur = performance.now() - renderStart;
230
+ appendMetric(metricsStore, "render:total", renderStart, renderDur);
231
+
232
+ return createResponseWithMergedHeaders(htmlStream, {
233
+ headers: { "content-type": "text/html;charset=utf-8" },
234
+ });
235
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Runtime guardrail warnings (dev-only).
3
+ *
4
+ * W3: PE action redirect / Response handling.
5
+ */
6
+
7
+ import {
8
+ createResponseWithMergedHeaders,
9
+ carryOverRedirectHeaders,
10
+ } from "./helpers.js";
11
+
12
+ // W3 -----------------------------------------------------------------------
13
+
14
+ /**
15
+ * Extract a redirect Response from a thrown or returned value.
16
+ * Returns a redirect Response to send to the client, or null if the value
17
+ * is not a redirect Response.
18
+ */
19
+ export function extractRedirectResponse(value: unknown): Response | null {
20
+ if (!(value instanceof Response)) return null;
21
+ const location = value.headers.get("Location");
22
+ if (value.status >= 300 && value.status < 400 && location) {
23
+ const redirect = createResponseWithMergedHeaders(null, {
24
+ status: value.status,
25
+ headers: { Location: location },
26
+ });
27
+ carryOverRedirectHeaders(value, redirect);
28
+ return redirect;
29
+ }
30
+ return null;
31
+ }
32
+
33
+ /**
34
+ * Warn when a non-redirect Response is returned from an action during PE.
35
+ */
36
+ export function warnNonRedirectPeResponse(): void {
37
+ console.warn(
38
+ `[rango] Server action returned a non-redirect Response during ` +
39
+ `progressive enhancement (no-JS) request. The Response will be ` +
40
+ `ignored — the page will re-render at the current URL instead.`,
41
+ );
42
+ }