@rangojs/router 0.0.0-experimental.10 → 0.0.0-experimental.100

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 (329) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +1037 -4
  3. package/dist/bin/rango.js +1619 -157
  4. package/dist/vite/index.js +5762 -2301
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +71 -63
  7. package/skills/breadcrumbs/SKILL.md +252 -0
  8. package/skills/cache-guide/SKILL.md +294 -0
  9. package/skills/caching/SKILL.md +93 -23
  10. package/skills/composability/SKILL.md +172 -0
  11. package/skills/debug-manifest/SKILL.md +12 -8
  12. package/skills/document-cache/SKILL.md +18 -16
  13. package/skills/fonts/SKILL.md +6 -4
  14. package/skills/handler-use/SKILL.md +364 -0
  15. package/skills/hooks/SKILL.md +367 -71
  16. package/skills/host-router/SKILL.md +218 -0
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +176 -8
  19. package/skills/layout/SKILL.md +124 -3
  20. package/skills/links/SKILL.md +304 -25
  21. package/skills/loader/SKILL.md +474 -47
  22. package/skills/middleware/SKILL.md +207 -37
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +15 -11
  26. package/skills/parallel/SKILL.md +272 -1
  27. package/skills/prerender/SKILL.md +467 -65
  28. package/skills/rango/SKILL.md +89 -21
  29. package/skills/response-routes/SKILL.md +152 -91
  30. package/skills/route/SKILL.md +305 -14
  31. package/skills/router-setup/SKILL.md +210 -32
  32. package/skills/server-actions/SKILL.md +739 -0
  33. package/skills/streams-and-websockets/SKILL.md +283 -0
  34. package/skills/theme/SKILL.md +9 -8
  35. package/skills/typesafety/SKILL.md +333 -86
  36. package/skills/use-cache/SKILL.md +324 -0
  37. package/skills/view-transitions/SKILL.md +212 -0
  38. package/src/__internal.ts +102 -4
  39. package/src/bin/rango.ts +312 -15
  40. package/src/browser/action-coordinator.ts +97 -0
  41. package/src/browser/action-response-classifier.ts +99 -0
  42. package/src/browser/app-shell.ts +52 -0
  43. package/src/browser/app-version.ts +14 -0
  44. package/src/browser/event-controller.ts +136 -68
  45. package/src/browser/history-state.ts +80 -0
  46. package/src/browser/intercept-utils.ts +52 -0
  47. package/src/browser/link-interceptor.ts +24 -4
  48. package/src/browser/logging.ts +55 -0
  49. package/src/browser/merge-segment-loaders.ts +20 -12
  50. package/src/browser/navigation-bridge.ts +374 -561
  51. package/src/browser/navigation-client.ts +228 -70
  52. package/src/browser/navigation-store.ts +97 -55
  53. package/src/browser/navigation-transaction.ts +297 -0
  54. package/src/browser/network-error-handler.ts +61 -0
  55. package/src/browser/partial-update.ts +376 -315
  56. package/src/browser/prefetch/cache.ts +314 -0
  57. package/src/browser/prefetch/fetch.ts +282 -0
  58. package/src/browser/prefetch/observer.ts +65 -0
  59. package/src/browser/prefetch/policy.ts +48 -0
  60. package/src/browser/prefetch/queue.ts +191 -0
  61. package/src/browser/prefetch/resource-ready.ts +77 -0
  62. package/src/browser/rango-state.ts +152 -0
  63. package/src/browser/react/Link.tsx +255 -71
  64. package/src/browser/react/NavigationProvider.tsx +152 -24
  65. package/src/browser/react/context.ts +11 -0
  66. package/src/browser/react/filter-segment-order.ts +55 -0
  67. package/src/browser/react/index.ts +15 -12
  68. package/src/browser/react/location-state-shared.ts +95 -53
  69. package/src/browser/react/location-state.ts +60 -15
  70. package/src/browser/react/mount-context.ts +6 -1
  71. package/src/browser/react/nonce-context.ts +23 -0
  72. package/src/browser/react/shallow-equal.ts +27 -0
  73. package/src/browser/react/use-action.ts +29 -51
  74. package/src/browser/react/use-client-cache.ts +5 -3
  75. package/src/browser/react/use-handle.ts +30 -120
  76. package/src/browser/react/use-link-status.ts +6 -5
  77. package/src/browser/react/use-navigation.ts +44 -65
  78. package/src/browser/react/use-params.ts +78 -0
  79. package/src/browser/react/use-pathname.ts +47 -0
  80. package/src/browser/react/use-reverse.ts +99 -0
  81. package/src/browser/react/use-router.ts +83 -0
  82. package/src/browser/react/use-search-params.ts +56 -0
  83. package/src/browser/react/use-segments.ts +85 -99
  84. package/src/browser/response-adapter.ts +73 -0
  85. package/src/browser/rsc-router.tsx +246 -64
  86. package/src/browser/scroll-restoration.ts +127 -52
  87. package/src/browser/segment-reconciler.ts +243 -0
  88. package/src/browser/segment-structure-assert.ts +16 -0
  89. package/src/browser/server-action-bridge.ts +510 -603
  90. package/src/browser/shallow.ts +6 -1
  91. package/src/browser/types.ts +158 -48
  92. package/src/browser/validate-redirect-origin.ts +29 -0
  93. package/src/build/generate-manifest.ts +84 -23
  94. package/src/build/generate-route-types.ts +39 -828
  95. package/src/build/index.ts +4 -5
  96. package/src/build/route-trie.ts +85 -32
  97. package/src/build/route-types/ast-helpers.ts +25 -0
  98. package/src/build/route-types/ast-route-extraction.ts +98 -0
  99. package/src/build/route-types/codegen.ts +102 -0
  100. package/src/build/route-types/include-resolution.ts +418 -0
  101. package/src/build/route-types/param-extraction.ts +48 -0
  102. package/src/build/route-types/per-module-writer.ts +128 -0
  103. package/src/build/route-types/router-processing.ts +618 -0
  104. package/src/build/route-types/scan-filter.ts +85 -0
  105. package/src/build/runtime-discovery.ts +231 -0
  106. package/src/cache/background-task.ts +34 -0
  107. package/src/cache/cache-key-utils.ts +44 -0
  108. package/src/cache/cache-policy.ts +125 -0
  109. package/src/cache/cache-runtime.ts +342 -0
  110. package/src/cache/cache-scope.ts +167 -307
  111. package/src/cache/cf/cf-cache-store.ts +573 -21
  112. package/src/cache/cf/index.ts +13 -3
  113. package/src/cache/document-cache.ts +116 -77
  114. package/src/cache/handle-capture.ts +81 -0
  115. package/src/cache/handle-snapshot.ts +41 -0
  116. package/src/cache/index.ts +1 -15
  117. package/src/cache/memory-segment-store.ts +191 -13
  118. package/src/cache/profile-registry.ts +73 -0
  119. package/src/cache/read-through-swr.ts +134 -0
  120. package/src/cache/segment-codec.ts +256 -0
  121. package/src/cache/taint.ts +153 -0
  122. package/src/cache/types.ts +72 -122
  123. package/src/client.rsc.tsx +6 -1
  124. package/src/client.tsx +118 -302
  125. package/src/component-utils.ts +4 -4
  126. package/src/components/DefaultDocument.tsx +5 -1
  127. package/src/context-var.ts +156 -0
  128. package/src/debug.ts +19 -9
  129. package/src/errors.ts +77 -7
  130. package/src/handle.ts +55 -10
  131. package/src/handles/MetaTags.tsx +73 -20
  132. package/src/handles/breadcrumbs.ts +66 -0
  133. package/src/handles/index.ts +1 -0
  134. package/src/handles/meta.ts +30 -13
  135. package/src/host/cookie-handler.ts +21 -15
  136. package/src/host/errors.ts +8 -8
  137. package/src/host/index.ts +4 -7
  138. package/src/host/pattern-matcher.ts +27 -27
  139. package/src/host/router.ts +61 -39
  140. package/src/host/testing.ts +8 -8
  141. package/src/host/types.ts +15 -7
  142. package/src/host/utils.ts +1 -1
  143. package/src/href-client.ts +65 -45
  144. package/src/index.rsc.ts +138 -21
  145. package/src/index.ts +206 -51
  146. package/src/internal-debug.ts +11 -0
  147. package/src/loader.rsc.ts +25 -143
  148. package/src/loader.ts +27 -10
  149. package/src/network-error-thrower.tsx +3 -1
  150. package/src/outlet-context.ts +1 -1
  151. package/src/outlet-provider.tsx +45 -0
  152. package/src/prerender/param-hash.ts +4 -2
  153. package/src/prerender/store.ts +159 -13
  154. package/src/prerender.ts +397 -29
  155. package/src/response-utils.ts +28 -0
  156. package/src/reverse.ts +231 -121
  157. package/src/root-error-boundary.tsx +41 -29
  158. package/src/route-content-wrapper.tsx +7 -4
  159. package/src/route-definition/dsl-helpers.ts +1134 -0
  160. package/src/route-definition/helper-factories.ts +200 -0
  161. package/src/route-definition/helpers-types.ts +483 -0
  162. package/src/route-definition/index.ts +55 -0
  163. package/src/route-definition/redirect.ts +101 -0
  164. package/src/route-definition/resolve-handler-use.ts +155 -0
  165. package/src/route-definition.ts +1 -1431
  166. package/src/route-map-builder.ts +162 -123
  167. package/src/route-name.ts +53 -0
  168. package/src/route-types.ts +66 -9
  169. package/src/router/content-negotiation.ts +215 -0
  170. package/src/router/debug-manifest.ts +72 -0
  171. package/src/router/error-handling.ts +9 -9
  172. package/src/router/find-match.ts +160 -0
  173. package/src/router/handler-context.ts +418 -86
  174. package/src/router/intercept-resolution.ts +35 -20
  175. package/src/router/lazy-includes.ts +237 -0
  176. package/src/router/loader-resolution.ts +359 -128
  177. package/src/router/logging.ts +251 -0
  178. package/src/router/manifest.ts +98 -32
  179. package/src/router/match-api.ts +196 -261
  180. package/src/router/match-context.ts +4 -2
  181. package/src/router/match-handlers.ts +441 -0
  182. package/src/router/match-middleware/background-revalidation.ts +108 -93
  183. package/src/router/match-middleware/cache-lookup.ts +415 -86
  184. package/src/router/match-middleware/cache-store.ts +91 -29
  185. package/src/router/match-middleware/intercept-resolution.ts +48 -21
  186. package/src/router/match-middleware/segment-resolution.ts +73 -9
  187. package/src/router/match-pipelines.ts +10 -45
  188. package/src/router/match-result.ts +154 -35
  189. package/src/router/metrics.ts +240 -15
  190. package/src/router/middleware-cookies.ts +55 -0
  191. package/src/router/middleware-types.ts +209 -0
  192. package/src/router/middleware.ts +373 -371
  193. package/src/router/navigation-snapshot.ts +182 -0
  194. package/src/router/pattern-matching.ts +292 -52
  195. package/src/router/prerender-match.ts +502 -0
  196. package/src/router/preview-match.ts +98 -0
  197. package/src/router/request-classification.ts +310 -0
  198. package/src/router/revalidation.ts +152 -39
  199. package/src/router/route-snapshot.ts +245 -0
  200. package/src/router/router-context.ts +41 -21
  201. package/src/router/router-interfaces.ts +484 -0
  202. package/src/router/router-options.ts +618 -0
  203. package/src/router/router-registry.ts +24 -0
  204. package/src/router/segment-resolution/fresh.ts +756 -0
  205. package/src/router/segment-resolution/helpers.ts +268 -0
  206. package/src/router/segment-resolution/loader-cache.ts +199 -0
  207. package/src/router/segment-resolution/revalidation.ts +1407 -0
  208. package/src/router/segment-resolution/static-store.ts +67 -0
  209. package/src/router/segment-resolution.ts +21 -1315
  210. package/src/router/segment-wrappers.ts +291 -0
  211. package/src/router/substitute-pattern-params.ts +56 -0
  212. package/src/router/telemetry-otel.ts +299 -0
  213. package/src/router/telemetry.ts +300 -0
  214. package/src/router/timeout.ts +148 -0
  215. package/src/router/trie-matching.ts +111 -39
  216. package/src/router/types.ts +17 -9
  217. package/src/router/url-params.ts +49 -0
  218. package/src/router.ts +642 -2011
  219. package/src/rsc/handler-context.ts +45 -0
  220. package/src/rsc/handler.ts +864 -1114
  221. package/src/rsc/helpers.ts +181 -19
  222. package/src/rsc/index.ts +0 -20
  223. package/src/rsc/loader-fetch.ts +229 -0
  224. package/src/rsc/manifest-init.ts +90 -0
  225. package/src/rsc/nonce.ts +14 -0
  226. package/src/rsc/origin-guard.ts +141 -0
  227. package/src/rsc/progressive-enhancement.ts +395 -0
  228. package/src/rsc/response-error.ts +37 -0
  229. package/src/rsc/response-route-handler.ts +360 -0
  230. package/src/rsc/rsc-rendering.ts +256 -0
  231. package/src/rsc/runtime-warnings.ts +42 -0
  232. package/src/rsc/server-action.ts +360 -0
  233. package/src/rsc/ssr-setup.ts +128 -0
  234. package/src/rsc/types.ts +52 -11
  235. package/src/search-params.ts +230 -0
  236. package/src/segment-content-promise.ts +67 -0
  237. package/src/segment-loader-promise.ts +122 -0
  238. package/src/segment-system.tsx +187 -38
  239. package/src/server/context.ts +333 -59
  240. package/src/server/cookie-store.ts +190 -0
  241. package/src/server/fetchable-loader-store.ts +37 -0
  242. package/src/server/handle-store.ts +113 -15
  243. package/src/server/loader-registry.ts +24 -64
  244. package/src/server/request-context.ts +603 -109
  245. package/src/server.ts +35 -155
  246. package/src/ssr/index.tsx +107 -30
  247. package/src/static-handler.ts +126 -0
  248. package/src/theme/ThemeProvider.tsx +21 -15
  249. package/src/theme/ThemeScript.tsx +5 -5
  250. package/src/theme/constants.ts +5 -2
  251. package/src/theme/index.ts +4 -14
  252. package/src/theme/theme-context.ts +4 -30
  253. package/src/theme/theme-script.ts +21 -18
  254. package/src/types/boundaries.ts +158 -0
  255. package/src/types/cache-types.ts +198 -0
  256. package/src/types/error-types.ts +192 -0
  257. package/src/types/global-namespace.ts +100 -0
  258. package/src/types/handler-context.ts +764 -0
  259. package/src/types/index.ts +88 -0
  260. package/src/types/loader-types.ts +209 -0
  261. package/src/types/request-scope.ts +126 -0
  262. package/src/types/route-config.ts +170 -0
  263. package/src/types/route-entry.ts +120 -0
  264. package/src/types/segments.ts +167 -0
  265. package/src/types.ts +1 -1757
  266. package/src/urls/include-helper.ts +207 -0
  267. package/src/urls/index.ts +53 -0
  268. package/src/urls/path-helper-types.ts +372 -0
  269. package/src/urls/path-helper.ts +364 -0
  270. package/src/urls/pattern-types.ts +107 -0
  271. package/src/urls/response-types.ts +108 -0
  272. package/src/urls/type-extraction.ts +372 -0
  273. package/src/urls/urls-function.ts +98 -0
  274. package/src/urls.ts +1 -1282
  275. package/src/use-loader.tsx +161 -81
  276. package/src/vite/debug.ts +184 -0
  277. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  278. package/src/vite/discovery/discover-routers.ts +376 -0
  279. package/src/vite/discovery/gate-state.ts +171 -0
  280. package/src/vite/discovery/prerender-collection.ts +486 -0
  281. package/src/vite/discovery/route-types-writer.ts +258 -0
  282. package/src/vite/discovery/self-gen-tracking.ts +73 -0
  283. package/src/vite/discovery/state.ts +117 -0
  284. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  285. package/src/vite/index.ts +15 -2063
  286. package/src/vite/plugin-types.ts +103 -0
  287. package/src/vite/plugins/cjs-to-esm.ts +98 -0
  288. package/src/vite/plugins/client-ref-dedup.ts +131 -0
  289. package/src/vite/plugins/client-ref-hashing.ts +117 -0
  290. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  291. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  292. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  293. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +107 -64
  294. package/src/vite/plugins/expose-id-utils.ts +299 -0
  295. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  296. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  297. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  298. package/src/vite/plugins/expose-ids/router-transform.ts +127 -0
  299. package/src/vite/plugins/expose-ids/types.ts +45 -0
  300. package/src/vite/plugins/expose-internal-ids.ts +816 -0
  301. package/src/vite/plugins/performance-tracks.ts +96 -0
  302. package/src/vite/plugins/refresh-cmd.ts +127 -0
  303. package/src/vite/plugins/use-cache-transform.ts +336 -0
  304. package/src/vite/plugins/version-injector.ts +109 -0
  305. package/src/vite/plugins/version-plugin.ts +266 -0
  306. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  307. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  308. package/src/vite/rango.ts +497 -0
  309. package/src/vite/router-discovery.ts +1423 -0
  310. package/src/vite/utils/ast-handler-extract.ts +517 -0
  311. package/src/vite/utils/banner.ts +36 -0
  312. package/src/vite/utils/bundle-analysis.ts +137 -0
  313. package/src/vite/utils/manifest-utils.ts +70 -0
  314. package/src/vite/utils/package-resolution.ts +161 -0
  315. package/src/vite/utils/prerender-utils.ts +222 -0
  316. package/src/vite/utils/shared-utils.ts +170 -0
  317. package/CLAUDE.md +0 -43
  318. package/src/browser/lru-cache.ts +0 -69
  319. package/src/browser/request-controller.ts +0 -164
  320. package/src/cache/memory-store.ts +0 -253
  321. package/src/href-context.ts +0 -33
  322. package/src/router.gen.ts +0 -6
  323. package/src/urls.gen.ts +0 -8
  324. package/src/vite/expose-handle-id.ts +0 -209
  325. package/src/vite/expose-loader-id.ts +0 -426
  326. package/src/vite/expose-location-state-id.ts +0 -177
  327. package/src/vite/expose-prerender-handler-id.ts +0 -429
  328. package/src/vite/package-resolution.ts +0 -125
  329. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Origin Guard
3
+ *
4
+ * Cross-origin request protection for server actions, loader fetches, and
5
+ * progressive enhancement form submissions. Validates that the Origin header
6
+ * (or Referer fallback) matches the request Host before executing.
7
+ *
8
+ * Requests without an Origin or Referer header are allowed — same-origin
9
+ * navigations, bookmarks, and non-browser clients don't send Origin.
10
+ */
11
+
12
+ /**
13
+ * Request phase that triggered the origin check.
14
+ */
15
+ export type OriginCheckPhase = "action" | "loader" | "pe-form";
16
+
17
+ /**
18
+ * Context passed to the originCheck callback.
19
+ */
20
+ export interface OriginCheckContext<TEnv = any> {
21
+ request: Request;
22
+ url: URL;
23
+ env: TEnv;
24
+ routerId: string;
25
+ phase: OriginCheckPhase;
26
+ /** Run the built-in conservative check (Origin/Referer vs Host + url.protocol). */
27
+ defaultCheck: () => boolean;
28
+ }
29
+
30
+ /**
31
+ * Configuration for the origin check.
32
+ *
33
+ * - `true` (default) — built-in conservative check
34
+ * - `false` — disabled
35
+ * - function — custom control; return true to allow, false to reject with
36
+ * default 403, or a Response for custom rejection
37
+ */
38
+ export type OriginCheckConfig<TEnv = any> =
39
+ | boolean
40
+ | ((
41
+ ctx: OriginCheckContext<TEnv>,
42
+ ) => boolean | Response | Promise<boolean | Response>);
43
+
44
+ /**
45
+ * Built-in conservative origin check.
46
+ * Compares Origin (or Referer fallback) against Host + url.protocol.
47
+ * Does NOT trust X-Forwarded-Host/Proto headers.
48
+ *
49
+ * Returns true to allow, false to reject.
50
+ */
51
+ export function defaultOriginCheck(request: Request, url: URL): boolean {
52
+ // 1. Read Origin header (present on all cross-origin requests and
53
+ // same-origin POST/PUT/PATCH/DELETE in modern browsers)
54
+ let requestOrigin = request.headers.get("origin");
55
+
56
+ // 2. Fallback to Referer if Origin is absent (some proxies strip it)
57
+ if (!requestOrigin) {
58
+ const referer = request.headers.get("referer");
59
+ if (referer) {
60
+ try {
61
+ requestOrigin = new URL(referer).origin;
62
+ } catch {
63
+ // Malformed referer — treat as absent
64
+ }
65
+ }
66
+ }
67
+
68
+ // 3. No Origin or Referer — allow (can't be browser-initiated CSRF)
69
+ if (!requestOrigin) return true;
70
+
71
+ // "null" origin comes from privacy-sensitive contexts (data: URLs,
72
+ // sandboxed iframes, cross-origin redirects). Reject it.
73
+ if (requestOrigin === "null") return false;
74
+
75
+ // 4. Determine expected host from Host header or URL.
76
+ // X-Forwarded-Host/Proto are NOT used — they are client-controllable
77
+ // unless a trusted proxy strips them. On standard deployments (Cloudflare
78
+ // Workers, Node behind nginx/caddy) the Host header is already correct.
79
+ // For non-standard setups, use the custom function escape hatch.
80
+ const expectedHost = request.headers.get("host") || url.host;
81
+ const expectedProtocol = url.protocol;
82
+
83
+ // 5. Build expected origin and compare (case-insensitive)
84
+ const expectedOrigin = `${expectedProtocol}//${expectedHost}`;
85
+
86
+ return requestOrigin.toLowerCase() === expectedOrigin.toLowerCase();
87
+ }
88
+
89
+ function createForbiddenResponse(request: Request): Response {
90
+ const isDev = process.env.NODE_ENV !== "production";
91
+ const body = isDev
92
+ ? "Forbidden: Origin mismatch. The request origin does not match the server host. " +
93
+ `Set originCheck: false in createRouter() to disable this check. ` +
94
+ `(Origin: ${request.headers.get("origin") ?? "none"}, ` +
95
+ `Host: ${request.headers.get("host") ?? "none"})`
96
+ : "Forbidden";
97
+
98
+ return new Response(body, {
99
+ status: 403,
100
+ headers: { "X-Rango-Origin-Check": "failed" },
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Configuration-aware origin check dispatcher.
106
+ * Builds the OriginCheckContext and delegates to the configured check.
107
+ */
108
+ export async function checkRequestOrigin<TEnv = any>(
109
+ request: Request,
110
+ url: URL,
111
+ config: OriginCheckConfig<TEnv> | undefined,
112
+ env: TEnv,
113
+ routerId: string,
114
+ phase: OriginCheckPhase,
115
+ ): Promise<Response | null> {
116
+ // Disabled by explicit opt-out
117
+ if (config === false) return null;
118
+
119
+ // Default: built-in validation (config === true or undefined)
120
+ if (config === true || config === undefined) {
121
+ const allowed = defaultOriginCheck(request, url);
122
+ if (allowed) return null;
123
+ return createForbiddenResponse(request);
124
+ }
125
+
126
+ // Custom function — build context and call
127
+ const ctx: OriginCheckContext<TEnv> = {
128
+ request,
129
+ url,
130
+ env,
131
+ routerId,
132
+ phase,
133
+ defaultCheck: () => defaultOriginCheck(request, url),
134
+ };
135
+
136
+ const result = await config(ctx);
137
+
138
+ if (result instanceof Response) return result;
139
+ if (result === true) return null;
140
+ return createForbiddenResponse(request);
141
+ }
@@ -0,0 +1,395 @@
1
+ /**
2
+ * Progressive Enhancement Handler
3
+ *
4
+ * Handles no-JS form submissions. When JavaScript is disabled, React renders
5
+ * forms with hidden fields ($ACTION_REF_*, $ACTION_KEY) containing the action
6
+ * reference. We detect these and return HTML instead of RSC stream.
7
+ */
8
+
9
+ import {
10
+ requireRequestContext,
11
+ setRequestContextParams,
12
+ } from "../server/request-context.js";
13
+ import { getSSRSetup } from "./ssr-setup.js";
14
+ import type { MiddlewareFn } from "../router/middleware.js";
15
+ import { executeMiddleware } from "../router/middleware.js";
16
+ import type { RscPayload, ReactFormState } from "./types.js";
17
+ import {
18
+ createResponseWithMergedHeaders,
19
+ finalizeResponse,
20
+ buildRouteMiddlewareEntries,
21
+ } from "./helpers.js";
22
+ import type { HandlerContext } from "./handler-context.js";
23
+ import {
24
+ extractRedirectResponse,
25
+ warnNonRedirectPeResponse,
26
+ } from "./runtime-warnings.js";
27
+
28
+ export interface PeRouteMiddlewareInfo {
29
+ routeMiddleware?: Array<{
30
+ handler: MiddlewareFn;
31
+ params: Record<string, string>;
32
+ }>;
33
+ variables: Record<string, any>;
34
+ routeReverse?: (
35
+ name: string,
36
+ params?: Record<string, string>,
37
+ search?: Record<string, unknown>,
38
+ ) => string;
39
+ }
40
+
41
+ export async function handleProgressiveEnhancement<TEnv>(
42
+ ctx: HandlerContext<TEnv>,
43
+ request: Request,
44
+ env: TEnv,
45
+ url: URL,
46
+ isAction: boolean,
47
+ handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
48
+ nonce: string | undefined,
49
+ routeMwInfo?: PeRouteMiddlewareInfo,
50
+ ): Promise<Response | null> {
51
+ const contentType = request.headers.get("content-type") || "";
52
+ const isFormSubmission =
53
+ contentType.includes("multipart/form-data") ||
54
+ contentType.includes("application/x-www-form-urlencoded");
55
+
56
+ if (request.method !== "POST" || isAction || !isFormSubmission) {
57
+ return null;
58
+ }
59
+
60
+ // Clone the request to read FormData without consuming it.
61
+ // Wrap in try-catch so malformed POST bodies are reported as action
62
+ // errors, not routing errors from the outer catch in handler.ts.
63
+ let formData: FormData;
64
+ try {
65
+ formData = await request.clone().formData();
66
+ } catch (error) {
67
+ // Attempt error boundary rendering so the user sees a meaningful page.
68
+ const errorHtml = await renderPeErrorBoundary(
69
+ ctx,
70
+ request,
71
+ env,
72
+ url,
73
+ error,
74
+ handleStore,
75
+ nonce,
76
+ );
77
+ if (errorHtml) {
78
+ ctx.callOnError(error, "action", {
79
+ request,
80
+ url,
81
+ env,
82
+ handledByBoundary: true,
83
+ });
84
+ return errorHtml;
85
+ }
86
+
87
+ ctx.callOnError(error, "action", {
88
+ request,
89
+ url,
90
+ env,
91
+ handledByBoundary: false,
92
+ });
93
+ console.error("[RSC] Progressive enhancement form parse error:", error);
94
+ return createResponseWithMergedHeaders(null, { status: 400 });
95
+ }
96
+
97
+ // Look for React's progressive enhancement hidden fields
98
+ let isDirectAction = false;
99
+ let isUseActionState = false;
100
+ let directActionId: string | null = null;
101
+
102
+ formData.forEach((_value, key) => {
103
+ if (key.startsWith("$ACTION_ID_")) {
104
+ isDirectAction = true;
105
+ directActionId = key.slice("$ACTION_ID_".length);
106
+ } else if (key.startsWith("$ACTION_REF_")) {
107
+ isUseActionState = true;
108
+ }
109
+ });
110
+
111
+ if (!isDirectAction && !isUseActionState) {
112
+ return null;
113
+ }
114
+
115
+ // Execute action and return HTML
116
+ let actionResult: unknown = undefined;
117
+ let reactFormState: ReactFormState | null = null;
118
+
119
+ if (isUseActionState) {
120
+ // Decode and extract action identity before execution so error
121
+ // handlers can report actionId even when the action throws.
122
+ let useActionStateId: string | undefined;
123
+ try {
124
+ const boundAction = await ctx.decodeAction(formData);
125
+ // React's custom .bind() preserves $$id on server references.
126
+ useActionStateId = (boundAction as { $$id?: string }).$$id ?? undefined;
127
+ actionResult = await boundAction();
128
+ } catch (error) {
129
+ // Handle thrown redirect (e.g., throw redirect('/path'))
130
+ const redirectResponse = extractRedirectResponse(error);
131
+ if (redirectResponse) return redirectResponse;
132
+
133
+ // Attempt error boundary rendering for the PE path
134
+ const errorHtml = await renderPeErrorBoundary(
135
+ ctx,
136
+ request,
137
+ env,
138
+ url,
139
+ error,
140
+ handleStore,
141
+ nonce,
142
+ useActionStateId,
143
+ );
144
+ if (errorHtml) return errorHtml;
145
+
146
+ ctx.callOnError(error, "action", {
147
+ request,
148
+ url,
149
+ env,
150
+ actionId: useActionStateId,
151
+ handledByBoundary: false,
152
+ });
153
+ console.error("[RSC] Progressive enhancement action error:", error);
154
+ }
155
+ } else if (isDirectAction && directActionId) {
156
+ const temporaryReferences = ctx.createTemporaryReferenceSet();
157
+
158
+ let args: unknown[] = [];
159
+ try {
160
+ args = await ctx.decodeReply(formData, { temporaryReferences });
161
+ } catch {
162
+ args = [formData];
163
+ }
164
+
165
+ try {
166
+ const loadedAction = await ctx.loadServerAction(directActionId);
167
+ actionResult = await loadedAction.apply(null, args);
168
+ } catch (error) {
169
+ // Handle thrown redirect (e.g., throw redirect('/path'))
170
+ const redirectResponse = extractRedirectResponse(error);
171
+ if (redirectResponse) return redirectResponse;
172
+
173
+ // Attempt error boundary rendering for the PE path
174
+ const errorHtml = await renderPeErrorBoundary(
175
+ ctx,
176
+ request,
177
+ env,
178
+ url,
179
+ error,
180
+ handleStore,
181
+ nonce,
182
+ directActionId,
183
+ );
184
+ if (errorHtml) return errorHtml;
185
+
186
+ ctx.callOnError(error, "action", {
187
+ request,
188
+ url,
189
+ env,
190
+ actionId: directActionId,
191
+ handledByBoundary: false,
192
+ });
193
+ console.error("[RSC] Progressive enhancement action error:", error);
194
+ }
195
+ }
196
+
197
+ // Handle Response returned from action during PE.
198
+ // In the JS path, executeServerAction intercepts redirect Responses and
199
+ // short-circuits. The PE path must handle them too.
200
+ if (actionResult instanceof Response) {
201
+ const redirectResponse = extractRedirectResponse(actionResult);
202
+ if (redirectResponse) return redirectResponse;
203
+ // W3: Non-redirect Response — discard it so it doesn't flow into
204
+ // decodeFormState or the re-render payload.
205
+ if (process.env.NODE_ENV !== "production") {
206
+ warnNonRedirectPeResponse();
207
+ }
208
+ actionResult = undefined;
209
+ }
210
+
211
+ // Decode form state for useActionState progressive enhancement
212
+ try {
213
+ reactFormState = await ctx.decodeFormState(actionResult, formData);
214
+ } catch (error) {
215
+ ctx.callOnError(error, "action", {
216
+ request,
217
+ url,
218
+ env,
219
+ handledByBoundary: false,
220
+ });
221
+ console.error("[RSC] Failed to decode form state:", error);
222
+ }
223
+
224
+ // Re-render the page and return HTML.
225
+ // Route middleware wraps the render so context variables, headers, and
226
+ // cookies set by route middleware are available during re-render — matching
227
+ // the behavior of JS-enabled requests.
228
+ const renderPage = async (): Promise<Response> => {
229
+ const renderRequest = new Request(url.toString(), {
230
+ method: "GET",
231
+ headers: new Headers({ accept: "text/html" }),
232
+ });
233
+
234
+ const match = await ctx.router.match(renderRequest, { env });
235
+
236
+ if (match.redirect) {
237
+ return createResponseWithMergedHeaders(null, {
238
+ status: 308,
239
+ headers: { Location: match.redirect },
240
+ });
241
+ }
242
+
243
+ const payload: RscPayload = {
244
+ metadata: {
245
+ pathname: url.pathname,
246
+ routerId: ctx.router.id,
247
+ basename: ctx.router.basename,
248
+ segments: match.segments,
249
+ matched: match.matched,
250
+ diff: match.diff,
251
+ resolvedIds: match.resolvedIds,
252
+ params: match.params,
253
+ isPartial: false,
254
+ rootLayout: ctx.router.rootLayout,
255
+ handles: handleStore.stream(),
256
+ version: ctx.version,
257
+ themeConfig: ctx.router.themeConfig,
258
+ warmupEnabled: ctx.router.warmupEnabled,
259
+ initialTheme: requireRequestContext().theme,
260
+ },
261
+ formState: actionResult,
262
+ };
263
+
264
+ const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
265
+ onError: (error: unknown) => {
266
+ ctx.callOnError(error, "rendering", { request, url, env });
267
+ },
268
+ });
269
+ // metricsStore=undefined is safe: the handler already stashed the early
270
+ // SSR setup promise on request variables, so getSSRSetup returns it
271
+ // without falling back to a fresh startSSRSetup.
272
+ const [ssrModule, streamMode] = await getSSRSetup(
273
+ ctx,
274
+ request,
275
+ env,
276
+ url,
277
+ undefined,
278
+ );
279
+ const htmlStream = await ssrModule.renderHTML(rscStream, {
280
+ formState: reactFormState,
281
+ nonce,
282
+ streamMode,
283
+ });
284
+
285
+ return createResponseWithMergedHeaders(htmlStream, {
286
+ headers: { "content-type": "text/html;charset=utf-8" },
287
+ });
288
+ };
289
+
290
+ // Execute route middleware wrapping the render, if any.
291
+ // finalizeResponse drains onResponse callbacks that middleware short-circuits
292
+ // may leave behind (executeMiddleware does not finalize them itself).
293
+ if (routeMwInfo?.routeMiddleware && routeMwInfo.routeMiddleware.length > 0) {
294
+ return finalizeResponse(
295
+ await executeMiddleware(
296
+ buildRouteMiddlewareEntries(routeMwInfo.routeMiddleware),
297
+ request,
298
+ env,
299
+ routeMwInfo.variables,
300
+ renderPage,
301
+ routeMwInfo.routeReverse,
302
+ ),
303
+ );
304
+ }
305
+
306
+ return renderPage();
307
+ }
308
+
309
+ /**
310
+ * Attempt to render an error boundary as full HTML for the PE path.
311
+ * Returns null if no error boundary is found (caller falls through to
312
+ * normal page re-render).
313
+ */
314
+ async function renderPeErrorBoundary<TEnv>(
315
+ ctx: HandlerContext<TEnv>,
316
+ request: Request,
317
+ env: TEnv,
318
+ url: URL,
319
+ error: unknown,
320
+ handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
321
+ nonce: string | undefined,
322
+ actionId?: string | null,
323
+ ): Promise<Response | null> {
324
+ let errorResult;
325
+ try {
326
+ errorResult = await ctx.router.matchError(request, { env }, error, "route");
327
+ } catch (matchErr) {
328
+ ctx.callOnError(error, "action", {
329
+ request,
330
+ url,
331
+ env,
332
+ actionId: actionId ?? undefined,
333
+ handledByBoundary: false,
334
+ });
335
+ throw matchErr;
336
+ }
337
+
338
+ if (!errorResult) return null;
339
+
340
+ ctx.callOnError(error, "action", {
341
+ request,
342
+ url,
343
+ env,
344
+ actionId: actionId ?? undefined,
345
+ handledByBoundary: true,
346
+ });
347
+
348
+ setRequestContextParams(errorResult.params, errorResult.routeName);
349
+
350
+ const payload: RscPayload = {
351
+ metadata: {
352
+ pathname: url.pathname,
353
+ routerId: ctx.router.id,
354
+ basename: ctx.router.basename,
355
+ segments: errorResult.segments,
356
+ matched: errorResult.matched,
357
+ diff: errorResult.diff,
358
+ resolvedIds: errorResult.resolvedIds,
359
+ params: errorResult.params,
360
+ isPartial: false,
361
+ isError: true,
362
+ rootLayout: ctx.router.rootLayout,
363
+ handles: handleStore.stream(),
364
+ version: ctx.version,
365
+ themeConfig: ctx.router.themeConfig,
366
+ warmupEnabled: ctx.router.warmupEnabled,
367
+ initialTheme: requireRequestContext().theme,
368
+ },
369
+ };
370
+
371
+ const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
372
+ onError: (error: unknown) => {
373
+ ctx.callOnError(error, "rendering", { request, url, env });
374
+ },
375
+ });
376
+ // metricsStore=undefined is safe: the handler already stashed the early
377
+ // SSR setup promise on request variables, so getSSRSetup returns it
378
+ // without falling back to a fresh startSSRSetup.
379
+ const [ssrModule, streamMode] = await getSSRSetup(
380
+ ctx,
381
+ request,
382
+ env,
383
+ url,
384
+ undefined,
385
+ );
386
+ const htmlStream = await ssrModule.renderHTML(rscStream, {
387
+ nonce,
388
+ streamMode,
389
+ });
390
+
391
+ return createResponseWithMergedHeaders(htmlStream, {
392
+ status: 500,
393
+ headers: { "content-type": "text/html;charset=utf-8" },
394
+ });
395
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Response Error Payload Builder
3
+ *
4
+ * Builds a ResponseError object from a caught error, controlling
5
+ * what information is exposed based on error type and environment.
6
+ */
7
+
8
+ import { RouterError } from "../errors.js";
9
+ import type { ResponseError } from "../urls.js";
10
+
11
+ /**
12
+ * Build a ResponseError payload from a caught error.
13
+ * RouterError messages are always exposed (developer-crafted).
14
+ * Standard Error messages are hidden in production.
15
+ */
16
+ export function createResponseErrorPayload(
17
+ error: unknown,
18
+ isDev: boolean,
19
+ ): ResponseError {
20
+ if (error instanceof RouterError) {
21
+ return {
22
+ message: error.message,
23
+ code: error.code,
24
+ ...(error.type ? { type: error.type } : {}),
25
+ ...(isDev && error.stack ? { stack: error.stack } : {}),
26
+ };
27
+ }
28
+ if (error instanceof Error) {
29
+ return {
30
+ message: isDev ? error.message : "Internal Server Error",
31
+ ...(isDev && error.stack ? { stack: error.stack } : {}),
32
+ };
33
+ }
34
+ return {
35
+ message: isDev ? String(error) : "Internal Server Error",
36
+ };
37
+ }