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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (374) hide show
  1. package/README.md +211 -43
  2. package/dist/bin/rango.js +279 -102
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +3313 -1160
  5. package/dist/vite/index.js.bak +5448 -0
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +62 -11
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +63 -1
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +222 -30
  12. package/skills/caching/SKILL.md +263 -8
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +364 -0
  17. package/skills/hooks/SKILL.md +250 -30
  18. package/skills/host-router/SKILL.md +124 -22
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +49 -5
  21. package/skills/layout/SKILL.md +35 -9
  22. package/skills/links/SKILL.md +249 -17
  23. package/skills/loader/SKILL.md +223 -9
  24. package/skills/middleware/SKILL.md +52 -13
  25. package/skills/migrate-nextjs/SKILL.md +584 -0
  26. package/skills/migrate-react-router/SKILL.md +769 -0
  27. package/skills/mime-routes/SKILL.md +27 -0
  28. package/skills/observability/SKILL.md +137 -0
  29. package/skills/parallel/SKILL.md +77 -7
  30. package/skills/prerender/SKILL.md +123 -100
  31. package/skills/rango/SKILL.md +250 -22
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +122 -47
  34. package/skills/route/SKILL.md +66 -5
  35. package/skills/router-setup/SKILL.md +38 -3
  36. package/skills/server-actions/SKILL.md +775 -0
  37. package/skills/streams-and-websockets/SKILL.md +283 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +129 -0
  40. package/skills/testing/bindings.md +89 -0
  41. package/skills/testing/cache-prerender.md +124 -0
  42. package/skills/testing/client-components.md +122 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +92 -0
  45. package/skills/testing/handles.md +129 -0
  46. package/skills/testing/loader.md +128 -0
  47. package/skills/testing/middleware.md +99 -0
  48. package/skills/testing/render-handler.md +121 -0
  49. package/skills/testing/response-routes.md +95 -0
  50. package/skills/testing/reverse-and-types.md +84 -0
  51. package/skills/testing/server-actions.md +107 -0
  52. package/skills/testing/server-tree.md +128 -0
  53. package/skills/testing/setup.md +120 -0
  54. package/skills/typesafety/SKILL.md +319 -27
  55. package/skills/use-cache/SKILL.md +36 -5
  56. package/skills/vercel/SKILL.md +107 -0
  57. package/skills/view-transitions/SKILL.md +294 -0
  58. package/src/__augment-tests__/augment.ts +81 -0
  59. package/src/__augment-tests__/augmented.check.ts +116 -0
  60. package/src/__internal.ts +1 -66
  61. package/src/browser/action-coordinator.ts +53 -36
  62. package/src/browser/action-fence.ts +47 -0
  63. package/src/browser/app-shell.ts +39 -0
  64. package/src/browser/app-version.ts +14 -0
  65. package/src/browser/cookie-name.ts +140 -0
  66. package/src/browser/event-controller.ts +81 -147
  67. package/src/browser/history-state.ts +21 -0
  68. package/src/browser/index.ts +3 -3
  69. package/src/browser/invalidate-client-cache.ts +52 -0
  70. package/src/browser/navigation-bridge.ts +66 -14
  71. package/src/browser/navigation-client.ts +172 -109
  72. package/src/browser/navigation-store-handle.ts +38 -0
  73. package/src/browser/navigation-store.ts +76 -67
  74. package/src/browser/navigation-transaction.ts +9 -59
  75. package/src/browser/partial-update.ts +79 -93
  76. package/src/browser/prefetch/cache.ts +180 -62
  77. package/src/browser/prefetch/fetch.ts +252 -39
  78. package/src/browser/prefetch/queue.ts +42 -8
  79. package/src/browser/rango-state.ts +158 -76
  80. package/src/browser/react/Link.tsx +72 -10
  81. package/src/browser/react/NavigationProvider.tsx +83 -31
  82. package/src/browser/react/ScrollRestoration.tsx +10 -6
  83. package/src/browser/react/context.ts +7 -2
  84. package/src/browser/react/filter-segment-order.ts +49 -7
  85. package/src/browser/react/index.ts +0 -48
  86. package/src/browser/react/location-state-shared.ts +166 -8
  87. package/src/browser/react/location-state.ts +39 -14
  88. package/src/browser/react/use-action.ts +6 -15
  89. package/src/browser/react/use-handle.ts +23 -69
  90. package/src/browser/react/use-link-status.ts +0 -4
  91. package/src/browser/react/use-navigation.ts +22 -5
  92. package/src/browser/react/use-params.ts +20 -10
  93. package/src/browser/react/use-reverse.ts +106 -0
  94. package/src/browser/react/use-router.ts +46 -11
  95. package/src/browser/react/use-search-params.ts +0 -5
  96. package/src/browser/react/use-segments.ts +11 -21
  97. package/src/browser/response-adapter.ts +52 -1
  98. package/src/browser/rsc-router.tsx +111 -24
  99. package/src/browser/scroll-restoration.ts +29 -19
  100. package/src/browser/segment-reconciler.ts +36 -14
  101. package/src/browser/segment-structure-assert.ts +2 -2
  102. package/src/browser/server-action-bridge.ts +176 -62
  103. package/src/browser/types.ts +60 -11
  104. package/src/browser/validate-redirect-origin.ts +43 -16
  105. package/src/build/collect-fallback-refs.ts +107 -0
  106. package/src/build/generate-manifest.ts +65 -40
  107. package/src/build/generate-route-types.ts +6 -0
  108. package/src/build/index.ts +8 -2
  109. package/src/build/prefix-tree-utils.ts +123 -0
  110. package/src/build/route-trie.ts +137 -32
  111. package/src/build/route-types/codegen.ts +4 -4
  112. package/src/build/route-types/include-resolution.ts +9 -2
  113. package/src/build/route-types/param-extraction.ts +6 -3
  114. package/src/build/route-types/per-module-writer.ts +7 -4
  115. package/src/build/route-types/router-processing.ts +333 -94
  116. package/src/build/route-types/scan-filter.ts +9 -2
  117. package/src/build/route-types/source-scan.ts +118 -0
  118. package/src/build/runtime-discovery.ts +9 -20
  119. package/src/cache/cache-error.ts +104 -0
  120. package/src/cache/cache-policy.ts +68 -28
  121. package/src/cache/cache-runtime.ts +134 -32
  122. package/src/cache/cache-scope.ts +100 -74
  123. package/src/cache/cache-tag.ts +98 -0
  124. package/src/cache/cf/cf-cache-store.ts +2256 -241
  125. package/src/cache/cf/index.ts +6 -16
  126. package/src/cache/document-cache.ts +61 -20
  127. package/src/cache/handle-snapshot.ts +63 -0
  128. package/src/cache/index.ts +22 -20
  129. package/src/cache/memory-segment-store.ts +136 -37
  130. package/src/cache/profile-registry.ts +6 -30
  131. package/src/cache/read-through-swr.ts +41 -11
  132. package/src/cache/segment-codec.ts +0 -16
  133. package/src/cache/tag-invalidation.ts +230 -0
  134. package/src/cache/types.ts +33 -100
  135. package/src/cache/vercel/index.ts +11 -0
  136. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  137. package/src/client.rsc.tsx +6 -21
  138. package/src/client.tsx +108 -290
  139. package/src/component-utils.ts +19 -0
  140. package/src/context-var.ts +17 -5
  141. package/src/decode-loader-results.ts +36 -0
  142. package/src/defer.ts +196 -0
  143. package/src/deps/browser.ts +0 -1
  144. package/src/deps/ssr.ts +0 -1
  145. package/src/errors.ts +30 -4
  146. package/src/handle.ts +70 -22
  147. package/src/handles/MetaTags.tsx +0 -14
  148. package/src/handles/breadcrumbs.ts +16 -5
  149. package/src/handles/meta.ts +0 -39
  150. package/src/host/cookie-handler.ts +0 -36
  151. package/src/host/errors.ts +0 -24
  152. package/src/host/index.ts +8 -2
  153. package/src/host/pattern-matcher.ts +7 -50
  154. package/src/host/router.ts +107 -99
  155. package/src/host/testing.ts +40 -27
  156. package/src/host/types.ts +37 -4
  157. package/src/host/utils.ts +1 -1
  158. package/src/href-client.ts +137 -22
  159. package/src/index.rsc.ts +69 -10
  160. package/src/index.ts +112 -14
  161. package/src/internal-debug.ts +2 -4
  162. package/src/loader-store.ts +500 -0
  163. package/src/loader.rsc.ts +20 -13
  164. package/src/loader.ts +12 -11
  165. package/src/missing-id-error.ts +68 -0
  166. package/src/network-error-thrower.tsx +1 -6
  167. package/src/outlet-context.ts +1 -1
  168. package/src/outlet-provider.tsx +1 -5
  169. package/src/prerender/param-hash.ts +10 -11
  170. package/src/prerender/store.ts +37 -41
  171. package/src/prerender.ts +198 -82
  172. package/src/redirect-origin.ts +100 -0
  173. package/src/response-utils.ts +37 -0
  174. package/src/reverse.ts +65 -15
  175. package/src/root-error-boundary.tsx +1 -19
  176. package/src/route-content-wrapper.tsx +7 -72
  177. package/src/route-definition/dsl-helpers.ts +413 -275
  178. package/src/route-definition/helper-factories.ts +29 -139
  179. package/src/route-definition/helpers-types.ts +107 -32
  180. package/src/route-definition/index.ts +3 -0
  181. package/src/route-definition/redirect.ts +50 -8
  182. package/src/route-definition/resolve-handler-use.ts +161 -0
  183. package/src/route-definition/use-item-types.ts +32 -0
  184. package/src/route-map-builder.ts +0 -16
  185. package/src/route-types.ts +37 -41
  186. package/src/router/basename.ts +14 -0
  187. package/src/router/content-negotiation.ts +108 -9
  188. package/src/router/error-handling.ts +13 -17
  189. package/src/router/find-match.ts +44 -23
  190. package/src/router/handler-context.ts +46 -30
  191. package/src/router/intercept-resolution.ts +23 -23
  192. package/src/router/lazy-includes.ts +15 -52
  193. package/src/router/loader-resolution.ts +207 -30
  194. package/src/router/logging.ts +0 -6
  195. package/src/router/manifest.ts +40 -42
  196. package/src/router/match-api.ts +120 -204
  197. package/src/router/match-context.ts +0 -22
  198. package/src/router/match-handlers.ts +58 -58
  199. package/src/router/match-middleware/background-revalidation.ts +0 -7
  200. package/src/router/match-middleware/cache-lookup.ts +161 -262
  201. package/src/router/match-middleware/cache-store.ts +3 -33
  202. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  203. package/src/router/match-middleware/segment-resolution.ts +45 -14
  204. package/src/router/match-pipelines.ts +1 -42
  205. package/src/router/match-result.ts +87 -39
  206. package/src/router/metrics.ts +0 -34
  207. package/src/router/middleware-types.ts +7 -140
  208. package/src/router/middleware.ts +169 -140
  209. package/src/router/navigation-snapshot.ts +131 -0
  210. package/src/router/params-util.ts +23 -0
  211. package/src/router/pattern-matching.ts +109 -63
  212. package/src/router/prerender-match.ts +190 -54
  213. package/src/router/preview-match.ts +32 -102
  214. package/src/router/request-classification.ts +276 -0
  215. package/src/router/revalidation.ts +63 -55
  216. package/src/router/route-snapshot.ts +244 -0
  217. package/src/router/router-context.ts +0 -27
  218. package/src/router/router-interfaces.ts +100 -35
  219. package/src/router/router-options.ts +91 -11
  220. package/src/router/router-registry.ts +2 -5
  221. package/src/router/segment-resolution/fresh.ts +119 -65
  222. package/src/router/segment-resolution/helpers.ts +34 -0
  223. package/src/router/segment-resolution/loader-cache.ts +40 -37
  224. package/src/router/segment-resolution/revalidation.ts +329 -305
  225. package/src/router/segment-resolution/static-store.ts +19 -5
  226. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  227. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  228. package/src/router/segment-resolution.ts +4 -1
  229. package/src/router/segment-wrappers.ts +0 -3
  230. package/src/router/state-cookie-name.ts +33 -0
  231. package/src/router/substitute-pattern-params.ts +56 -0
  232. package/src/router/telemetry-otel.ts +0 -20
  233. package/src/router/telemetry.ts +96 -19
  234. package/src/router/timeout.ts +0 -20
  235. package/src/router/trie-matching.ts +91 -46
  236. package/src/router/types.ts +9 -63
  237. package/src/router/url-params.ts +44 -0
  238. package/src/router.ts +128 -42
  239. package/src/rsc/handler-context.ts +3 -2
  240. package/src/rsc/handler.ts +492 -409
  241. package/src/rsc/helpers.ts +162 -46
  242. package/src/rsc/index.ts +1 -1
  243. package/src/rsc/json-route-result.ts +38 -0
  244. package/src/rsc/loader-fetch.ts +18 -3
  245. package/src/rsc/manifest-init.ts +33 -42
  246. package/src/rsc/origin-guard.ts +39 -25
  247. package/src/rsc/progressive-enhancement.ts +28 -4
  248. package/src/rsc/redirect-guard.ts +99 -0
  249. package/src/rsc/response-error.ts +79 -12
  250. package/src/rsc/response-route-handler.ts +90 -63
  251. package/src/rsc/rsc-rendering.ts +53 -56
  252. package/src/rsc/runtime-warnings.ts +23 -10
  253. package/src/rsc/server-action.ts +74 -69
  254. package/src/rsc/ssr-setup.ts +18 -2
  255. package/src/rsc/types.ts +22 -9
  256. package/src/runtime-env.ts +18 -0
  257. package/src/search-params.ts +4 -20
  258. package/src/segment-content-promise.ts +67 -0
  259. package/src/segment-loader-promise.ts +134 -0
  260. package/src/segment-system.tsx +208 -201
  261. package/src/serialize.ts +243 -0
  262. package/src/server/context.ts +211 -52
  263. package/src/server/cookie-store.ts +80 -5
  264. package/src/server/handle-store.ts +26 -24
  265. package/src/server/loader-registry.ts +10 -28
  266. package/src/server/request-context.ts +289 -124
  267. package/src/ssr/index.tsx +22 -15
  268. package/src/static-handler.ts +27 -18
  269. package/src/testing/cache-status.ts +162 -0
  270. package/src/testing/collect-handle.ts +40 -0
  271. package/src/testing/dispatch.ts +618 -0
  272. package/src/testing/dom.entry.ts +22 -0
  273. package/src/testing/e2e/fixture.ts +188 -0
  274. package/src/testing/e2e/index.ts +128 -0
  275. package/src/testing/e2e/matchers.ts +35 -0
  276. package/src/testing/e2e/page-helpers.ts +272 -0
  277. package/src/testing/e2e/parity.ts +387 -0
  278. package/src/testing/e2e/server.ts +195 -0
  279. package/src/testing/flight-matchers.ts +97 -0
  280. package/src/testing/flight-normalize.ts +11 -0
  281. package/src/testing/flight-runtime.d.ts +57 -0
  282. package/src/testing/flight-tree.ts +682 -0
  283. package/src/testing/flight.entry.ts +52 -0
  284. package/src/testing/flight.ts +232 -0
  285. package/src/testing/generated-routes.ts +183 -0
  286. package/src/testing/index.ts +99 -0
  287. package/src/testing/internal/context.ts +348 -0
  288. package/src/testing/internal/flight-client-globals.ts +30 -0
  289. package/src/testing/internal/seed-vars.ts +54 -0
  290. package/src/testing/render-handler.ts +330 -0
  291. package/src/testing/render-route.tsx +566 -0
  292. package/src/testing/run-loader.ts +378 -0
  293. package/src/testing/run-middleware.ts +205 -0
  294. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  295. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  296. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  297. package/src/testing/vitest-stubs/version.ts +5 -0
  298. package/src/testing/vitest.ts +305 -0
  299. package/src/theme/ThemeProvider.tsx +0 -52
  300. package/src/theme/ThemeScript.tsx +0 -6
  301. package/src/theme/constants.ts +0 -12
  302. package/src/theme/index.ts +0 -7
  303. package/src/theme/theme-context.ts +1 -5
  304. package/src/theme/theme-script.ts +0 -14
  305. package/src/theme/use-theme.ts +0 -3
  306. package/src/types/boundaries.ts +0 -35
  307. package/src/types/cache-types.ts +17 -8
  308. package/src/types/error-types.ts +30 -90
  309. package/src/types/global-namespace.ts +54 -41
  310. package/src/types/handler-context.ts +124 -70
  311. package/src/types/index.ts +1 -10
  312. package/src/types/loader-types.ts +40 -11
  313. package/src/types/request-scope.ts +107 -0
  314. package/src/types/route-config.ts +6 -50
  315. package/src/types/route-entry.ts +12 -7
  316. package/src/types/segments.ts +36 -15
  317. package/src/urls/include-helper.ts +33 -70
  318. package/src/urls/index.ts +1 -11
  319. package/src/urls/path-helper-types.ts +58 -11
  320. package/src/urls/path-helper.ts +57 -111
  321. package/src/urls/pattern-types.ts +48 -19
  322. package/src/urls/response-types.ts +25 -22
  323. package/src/urls/type-extraction.ts +58 -139
  324. package/src/urls/urls-function.ts +1 -18
  325. package/src/use-loader.tsx +346 -89
  326. package/src/vite/debug.ts +185 -0
  327. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  328. package/src/vite/discovery/discover-routers.ts +130 -85
  329. package/src/vite/discovery/discovery-errors.ts +194 -0
  330. package/src/vite/discovery/gate-state.ts +171 -0
  331. package/src/vite/discovery/prerender-collection.ts +192 -99
  332. package/src/vite/discovery/route-types-writer.ts +40 -84
  333. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  334. package/src/vite/discovery/state.ts +51 -4
  335. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  336. package/src/vite/index.ts +8 -0
  337. package/src/vite/plugin-types.ts +236 -6
  338. package/src/vite/plugins/cjs-to-esm.ts +8 -18
  339. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  340. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  341. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  342. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  343. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  344. package/src/vite/plugins/expose-action-id.ts +48 -95
  345. package/src/vite/plugins/expose-id-utils.ts +11 -50
  346. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  347. package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
  348. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  349. package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
  350. package/src/vite/plugins/expose-internal-ids.ts +554 -317
  351. package/src/vite/plugins/performance-tracks.ts +65 -207
  352. package/src/vite/plugins/refresh-cmd.ts +89 -27
  353. package/src/vite/plugins/use-cache-transform.ts +73 -83
  354. package/src/vite/plugins/vercel-output.ts +258 -0
  355. package/src/vite/plugins/version-injector.ts +21 -25
  356. package/src/vite/plugins/version-plugin.ts +32 -23
  357. package/src/vite/plugins/virtual-entries.ts +46 -17
  358. package/src/vite/rango.ts +207 -125
  359. package/src/vite/router-discovery.ts +931 -133
  360. package/src/vite/utils/ast-handler-extract.ts +15 -31
  361. package/src/vite/utils/banner.ts +1 -1
  362. package/src/vite/utils/bundle-analysis.ts +10 -15
  363. package/src/vite/utils/client-chunks.ts +184 -0
  364. package/src/vite/utils/forward-user-plugins.ts +171 -0
  365. package/src/vite/utils/manifest-utils.ts +4 -59
  366. package/src/vite/utils/package-resolution.ts +20 -52
  367. package/src/vite/utils/prerender-utils.ts +27 -29
  368. package/src/vite/utils/shared-utils.ts +97 -44
  369. package/src/browser/action-response-classifier.ts +0 -99
  370. package/src/browser/debug-channel.ts +0 -93
  371. package/src/browser/react/use-client-cache.ts +0 -58
  372. package/src/browser/shallow.ts +0 -40
  373. package/src/handles/index.ts +0 -7
  374. package/src/router/middleware-cookies.ts +0 -55
@@ -1,10 +1,3 @@
1
- /**
2
- * Middleware Types
3
- *
4
- * Type definitions and interfaces for the middleware system.
5
- * Separated from execution logic for cleaner imports.
6
- */
7
-
8
1
  import type { ContextVar } from "../context-var.js";
9
2
  import type {
10
3
  DefaultReverseRouteMap,
@@ -14,18 +7,13 @@ import type {
14
7
  import type { ScopedReverseFunction } from "../reverse.js";
15
8
  import type { Theme } from "../theme/types.js";
16
9
  import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
10
+ import type { RequestScope } from "../types/request-scope.js";
17
11
 
18
- /**
19
- * Get variable function type
20
- */
21
12
  type GetVariableFn = {
22
13
  <T>(contextVar: ContextVar<T>): T | undefined;
23
14
  <K extends keyof DefaultVars>(key: K): DefaultVars[K];
24
15
  };
25
16
 
26
- /**
27
- * Set variable function type
28
- */
29
17
  type SetVariableFn = {
30
18
  <T>(contextVar: ContextVar<T>, value: T, options?: { cache?: boolean }): void;
31
19
  <K extends keyof DefaultVars>(
@@ -35,9 +23,6 @@ type SetVariableFn = {
35
23
  ): void;
36
24
  };
37
25
 
38
- /**
39
- * Cookie options for setting cookies
40
- */
41
26
  export interface CookieOptions {
42
27
  domain?: string;
43
28
  path?: string;
@@ -48,178 +33,60 @@ export interface CookieOptions {
48
33
  sameSite?: "strict" | "lax" | "none";
49
34
  }
50
35
 
51
- /**
52
- * Context passed to middleware
53
- *
54
- * @template TEnv - Environment type (bindings, variables) - defaults to any for internal flexibility
55
- * @template TParams - URL params type (typed for route middleware, Record<string, string> for global middleware)
56
- */
57
36
  export interface MiddlewareContext<
58
37
  TEnv = any,
59
- TParams = Record<string, string>,
60
- > {
61
- /** Original request */
62
- request: Request;
63
-
64
- /** Parsed URL (with internal `_rsc*` params stripped) */
65
- url: URL;
66
-
67
- /**
68
- * The original request URL with all parameters intact, including
69
- * internal `_rsc*` transport params.
70
- */
71
- originalUrl: URL;
72
-
73
- /** URL pathname */
74
- pathname: string;
75
-
76
- /** URL search params */
77
- searchParams: URLSearchParams;
78
-
79
- /** Platform bindings (Cloudflare, etc.) */
80
- env: TEnv;
81
-
82
- /** URL params extracted from route/middleware pattern */
38
+ TParams = Record<string, string | undefined>,
39
+ > extends RequestScope<TEnv> {
83
40
  params: TParams;
84
41
 
85
- /**
86
- * Response headers.
87
- * Before `next()`, returns headers from the shared response stub.
88
- * After `next()`, returns headers from the downstream response.
89
- */
90
42
  readonly headers: Headers;
91
43
 
92
- /** Get a context variable (shared with route handlers) */
93
44
  get: GetVariableFn;
94
45
 
95
- /** Set a context variable (shared with route handlers) */
96
46
  set: SetVariableFn;
97
47
 
98
- /**
99
- * Middleware-injected variables.
100
- * Same shared dictionary as `ctx.get()`/`ctx.set()`.
101
- */
102
- var: DefaultVars;
103
-
104
- /**
105
- * Set a response header - can be called before or after `next()`.
106
- *
107
- * When called before `next()`, headers are queued and merged into the final response.
108
- * When called after `next()`, headers are set directly on the response.
109
- */
110
48
  header(name: string, value: string): void;
111
49
 
112
- /**
113
- * The matched route name, if available and the route has an explicit name.
114
- * Undefined for global middleware (runs before route matching) or unnamed routes.
115
- */
116
50
  routeName?: DefaultRouteName;
117
51
 
118
- /**
119
- * Enable performance metrics for this request.
120
- * When called, granular timing breakdown is logged to console and
121
- * included in the Server-Timing response header, regardless of the
122
- * router-level `debugPerformance` option.
123
- *
124
- * Call **before** `await next()` so the metrics store exists when
125
- * downstream phases (route matching, rendering, SSR) record their
126
- * spans. Calling after `next()` returns still emits `handler:total`
127
- * but misses all upstream metrics.
128
- */
129
52
  debugPerformance(): void;
130
53
 
131
- /**
132
- * Current theme (from cookie or default).
133
- * Only available when theme is enabled in router config.
134
- */
135
54
  theme?: Theme;
136
55
 
137
- /**
138
- * Set the theme (only available when theme is enabled in router config).
139
- * Sets a cookie with the new theme value.
140
- */
141
56
  setTheme?: (theme: Theme) => void;
142
57
 
143
- /**
144
- * Attach location state entries to this response.
145
- * State is delivered to the client via history.pushState and accessible
146
- * through the useLocationState() hook.
147
- */
148
58
  setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void;
149
59
 
150
- /**
151
- * Generate URLs from route names.
152
- * - `name` — global route, from the named-routes definition
153
- */
154
60
  reverse: ScopedReverseFunction<
155
61
  Record<string, string>,
156
62
  DefaultReverseRouteMap
157
63
  >;
158
64
  }
159
65
 
160
- /**
161
- * Middleware function signature
162
- *
163
- * @template TEnv - Environment type - defaults to any for internal flexibility
164
- * @template TParams - URL params type (typed for route middleware)
165
- *
166
- * When using middleware with global augmentation (RSCRouter.Env), explicitly
167
- * annotate your middleware functions, or the types will be inferred from context:
168
- *
169
- * @example
170
- * ```typescript
171
- * // With explicit annotation (recommended for reusable middleware)
172
- * const authMiddleware: MiddlewareFn<AppEnv> = async (ctx, next) => {...}
173
- *
174
- * // Types inferred from router.use() call
175
- * router.use((ctx, next) => {...}) // ctx is typed from router's TEnv
176
- * ```
177
- */
178
- export type MiddlewareFn<TEnv = any, TParams = Record<string, string>> = (
66
+ export type MiddlewareFn<
67
+ TEnv = any,
68
+ TParams = Record<string, string | undefined>,
69
+ > = (
179
70
  ctx: MiddlewareContext<TEnv, TParams>,
180
71
  next: () => Promise<Response>,
181
72
  ) => Response | void | Promise<Response | void>;
182
73
 
183
- /**
184
- * Stored middleware entry with pattern matching info
185
- * @internal - uses any for internal flexibility
186
- */
187
74
  export interface MiddlewareEntry<TEnv = any> {
188
- /** Original pattern string */
189
75
  pattern: string | null;
190
-
191
- /** Compiled regex for matching */
192
76
  regex: RegExp | null;
193
-
194
- /** Param names extracted from pattern */
195
77
  paramNames: string[];
196
-
197
- /** The middleware function */
198
78
  handler: MiddlewareFn<TEnv>;
199
-
200
- /** Mount prefix this middleware is scoped to (null = global) */
201
- mountPrefix: string | null;
202
79
  }
203
80
 
204
- /**
205
- * Mutable response holder - tracks the current response through the middleware chain.
206
- */
207
81
  export interface ResponseHolder {
208
82
  response: Response | null;
209
83
  }
210
84
 
211
- /**
212
- * Entry type for middleware collection
213
- * Matches the shape of EntryData used in router.ts
214
- */
215
85
  export interface MiddlewareCollectableEntry {
216
86
  middleware?: MiddlewareFn<any, any>[];
217
87
  layout?: MiddlewareCollectableEntry[];
218
88
  }
219
89
 
220
- /**
221
- * Collected route middleware with params
222
- */
223
90
  export interface CollectedMiddleware {
224
91
  handler: MiddlewareFn<any, any>;
225
92
  params: Record<string, string>;
@@ -1,15 +1,8 @@
1
1
  /// <reference types="vite/types/importMeta.d.ts" />
2
- /**
3
- * Middleware Execution
4
- *
5
- * True middleware that wraps the entire RSC handler.
6
- * - `await next()` returns actual Response
7
- * - Can modify response headers
8
- * - Can catch errors from RSC rendering
9
- * - Forgiving API: if middleware doesn't return, original response is used
10
- */
11
2
 
12
3
  import { contextGet, contextSet } from "../context-var.js";
4
+ import { safeDecodeURIComponent } from "./url-params.js";
5
+ import { fireAndForgetWaitUntil } from "../types/request-scope.js";
13
6
  import type {
14
7
  CollectedMiddleware,
15
8
  MiddlewareCollectableEntry,
@@ -19,27 +12,28 @@ import type {
19
12
  ResponseHolder,
20
13
  } from "./middleware-types.js";
21
14
  import { _getRequestContext } from "../server/request-context.js";
15
+ import {
16
+ EXTERNAL_REDIRECT_MARKER,
17
+ isExternalRedirect,
18
+ markExternalRedirect,
19
+ } from "../redirect-origin.js";
22
20
  import { isAutoGeneratedRouteName } from "../route-name.js";
23
21
  import { appendMetric, createMetricsStore } from "./metrics.js";
24
22
  import { stripInternalParams } from "./handler-context.js";
23
+ import { isWebSocketUpgradeResponse } from "../response-utils.js";
25
24
 
26
- // Re-export types and cookie utilities for backward compatibility
25
+ // Re-export types consumed through this module's path.
27
26
  export type {
28
27
  CookieOptions,
29
- CollectedMiddleware,
30
- MiddlewareCollectableEntry,
31
28
  MiddlewareContext,
32
29
  MiddlewareEntry,
33
30
  MiddlewareFn,
34
- ResponseHolder,
35
31
  } from "./middleware-types.js";
36
- export { parseCookies, serializeCookie } from "./middleware-cookies.js";
37
32
 
38
33
  const MIDDLEWARE_METRIC_DEPTH = 1;
39
- /** Ignore post-next() durations below this threshold (measurement noise). */
40
34
  const POST_METRIC_MIN_DURATION_MS = 0.01;
41
35
 
42
- function getMiddlewareMetricBase<TEnv>(
36
+ function getMiddlewareMetricLabel<TEnv>(
43
37
  entry: MiddlewareEntry<TEnv>,
44
38
  ordinal: number,
45
39
  ): string {
@@ -47,23 +41,12 @@ function getMiddlewareMetricBase<TEnv>(
47
41
  const scope = entry.pattern ?? "*";
48
42
 
49
43
  if (handlerName) {
50
- return `${handlerName}@${scope}`;
44
+ return `middleware:${handlerName}@${scope}`;
51
45
  }
52
46
 
53
- return `${scope}#${ordinal + 1}`;
47
+ return `middleware:${scope}#${ordinal + 1}`;
54
48
  }
55
49
 
56
- function getMiddlewareMetricLabel<TEnv>(
57
- entry: MiddlewareEntry<TEnv>,
58
- ordinal: number,
59
- ): string {
60
- return `middleware:${getMiddlewareMetricBase(entry, ordinal)}`;
61
- }
62
-
63
- /**
64
- * Parse a route pattern into regex and param names
65
- * Supports: *, /path, /path/*, /path/:param, /path/:param/*
66
- */
67
50
  export function parsePattern(pattern: string): {
68
51
  regex: RegExp;
69
52
  paramNames: string[];
@@ -112,7 +95,12 @@ function escapeRegex(str: string): string {
112
95
  }
113
96
 
114
97
  /**
115
- * Extract params from a pathname using a pattern's regex and param names
98
+ * Extract params from a pathname using a pattern's regex and param names.
99
+ *
100
+ * Values are URL-decoded so apps see the raw string (e.g. "ivo@example.com")
101
+ * instead of the percent-encoded form ("ivo%40example.com"). This matches the
102
+ * contract assumed by ctx.reverse (which re-encodes) and aligns with
103
+ * Express/React Router/Fastify/Koa.
116
104
  */
117
105
  export function extractParams(
118
106
  pathname: string,
@@ -124,7 +112,7 @@ export function extractParams(
124
112
 
125
113
  const params: Record<string, string> = {};
126
114
  for (let i = 0; i < paramNames.length; i++) {
127
- params[paramNames[i]] = match[i + 1] || "";
115
+ params[paramNames[i]] = safeDecodeURIComponent(match[i + 1] || "");
128
116
  }
129
117
  return params;
130
118
  }
@@ -179,14 +167,22 @@ export function createMiddlewareContext<TEnv>(
179
167
  return responseHolder.response;
180
168
  };
181
169
 
170
+ // Capture reqCtx once: the request-scoped platform fields
171
+ // (originalUrl, executionContext, waitUntil) are immutable per request,
172
+ // so snapshotting beats re-reading ALS on every access. The lazy getters
173
+ // below (routeName, theme, setTheme) stay lazy because those can change
174
+ // during `await next()`.
175
+ const reqCtx = _getRequestContext();
182
176
  return {
183
177
  request,
184
178
  url,
185
- originalUrl: new URL(request.url),
179
+ originalUrl: reqCtx?.originalUrl ?? new URL(request.url),
186
180
  pathname: url.pathname,
187
181
  searchParams: url.searchParams,
188
182
  env: env as MiddlewareContext<TEnv>["env"],
189
183
  params,
184
+ executionContext: reqCtx?.executionContext,
185
+ waitUntil: reqCtx ? reqCtx.waitUntil.bind(reqCtx) : fireAndForgetWaitUntil,
190
186
  // Getter: re-derives from request context on each access so that global
191
187
  // middleware sees the matched route name after await next().
192
188
  get routeName(): MiddlewareContext<TEnv>["routeName"] {
@@ -207,9 +203,6 @@ export function createMiddlewareContext<TEnv>(
207
203
  set: ((keyOrVar: any, value: unknown, options?: any) => {
208
204
  contextSet(variables, keyOrVar, value, options);
209
205
  }) as MiddlewareContext<TEnv>["set"],
210
-
211
- var: variables as MiddlewareContext<TEnv>["var"],
212
-
213
206
  header(name: string, value: string): void {
214
207
  // Before next(): delegate to shared RequestContext stub
215
208
  if (isPreNext()) {
@@ -248,9 +241,13 @@ export function createMiddlewareContext<TEnv>(
248
241
 
249
242
  reverse:
250
243
  reverse ??
251
- ((name: string) => {
244
+ ((
245
+ name: string,
246
+ _params?: Record<string, string>,
247
+ _search?: Record<string, unknown>,
248
+ ) => {
252
249
  throw new Error(
253
- `ctx.reverse() is not available - route map was not provided to middleware context`,
250
+ `ctx.reverse(${JSON.stringify(name)}) is not available: no route map is bound to this middleware context.`,
254
251
  );
255
252
  }),
256
253
 
@@ -294,6 +291,88 @@ export function matchMiddleware<TEnv>(
294
291
  return matches;
295
292
  }
296
293
 
294
+ // Set-Cookie is appended; for other headers stubOverridesNonCookie=true
295
+ // overwrites (chain ran to completion), false fills only missing slots (an
296
+ // explicit short-circuit Response's own headers win).
297
+ function mergeStubHeaders(
298
+ target: Headers,
299
+ stub: Headers,
300
+ stubOverridesNonCookie: boolean,
301
+ ): void {
302
+ stub.forEach((value, name) => {
303
+ // The reserved external-redirect marker is internal and never a trust
304
+ // signal; never copy a stub value (e.g. a stray ctx.header() call) onto a
305
+ // browser-facing response. The opt-in is the out-of-band brand.
306
+ if (name.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
307
+ if (name.toLowerCase() === "set-cookie") {
308
+ target.append(name, value);
309
+ } else if (stubOverridesNonCookie || !target.has(name)) {
310
+ target.set(name, value);
311
+ }
312
+ });
313
+ }
314
+
315
+ // Set-Cookie is deduped so a nested inner executeMiddleware that already merged
316
+ // the same reqCtx cookies does not duplicate them; other headers fill if missing.
317
+ function mergeReqCtxStub(
318
+ target: Headers,
319
+ reqCtx: ReturnType<typeof _getRequestContext>,
320
+ ): void {
321
+ if (!reqCtx) return;
322
+ const stubCookies = reqCtx.res.headers.getSetCookie();
323
+ if (stubCookies.length > 0) {
324
+ const existing = new Set(target.getSetCookie());
325
+ for (const cookie of stubCookies) {
326
+ if (!existing.has(cookie)) {
327
+ target.append("set-cookie", cookie);
328
+ }
329
+ }
330
+ }
331
+ reqCtx.res.headers.forEach((value, name) => {
332
+ // Never propagate the reserved external-redirect marker (see mergeStubHeaders).
333
+ if (name.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
334
+ if (name !== "set-cookie" && !target.has(name)) {
335
+ target.set(name, value);
336
+ }
337
+ });
338
+ }
339
+
340
+ // Clone `base` with stub headers merged into a fresh Headers (the clone keeps
341
+ // the body mutable for post-next() modifications). Set-Cookie is always
342
+ // appended; other headers obey stubOverridesNonCookie (see mergeStubHeaders).
343
+ // mergeReqCtx folds in RequestContext stub cookies/headers; the intercept
344
+ // short-circuit path passes false (its reqCtx headers are not merged here),
345
+ // which is the one deliberate divergence between the call sites.
346
+ function mergeResponse(
347
+ base: Response,
348
+ stub: Headers,
349
+ opts: { stubOverridesNonCookie: boolean; mergeReqCtx: boolean },
350
+ ): Response {
351
+ const mergedHeaders = new Headers(base.headers);
352
+ // The reserved external-redirect marker is never a trust signal and must never
353
+ // reach the browser. The guard strips it on 3xx redirects; strip it here too so
354
+ // a forged value cannot ride a non-3xx middleware response (which the 3xx-only
355
+ // guard would not touch) to the client. The opt-in is the out-of-band brand.
356
+ mergedHeaders.delete(EXTERNAL_REDIRECT_MARKER);
357
+ mergeStubHeaders(mergedHeaders, stub, opts.stubOverridesNonCookie);
358
+ if (opts.mergeReqCtx) {
359
+ mergeReqCtxStub(mergedHeaders, _getRequestContext());
360
+ }
361
+ const merged = new Response(base.body, {
362
+ status: base.status,
363
+ statusText: base.statusText,
364
+ headers: mergedHeaders,
365
+ });
366
+ // Transfer the out-of-band external-redirect brand across this rebuild: a
367
+ // middleware short-circuit `return redirect(url, { external: true })` reaches
368
+ // the open-redirect guard only after this merge, and the brand lives on the
369
+ // Response object, not in its headers.
370
+ if (isExternalRedirect(base)) {
371
+ markExternalRedirect(merged);
372
+ }
373
+ return merged;
374
+ }
375
+
297
376
  /**
298
377
  * Execute middleware chain
299
378
  *
@@ -302,7 +381,7 @@ export function matchMiddleware<TEnv>(
302
381
  * - `ctx.headers` available before and after `await next()`
303
382
  * - `ctx.header()` shorthand for setting a single header
304
383
  * - Forgiving: if middleware doesn't return, uses the downstream response
305
- * - Short-circuit: return Response to stop chain
384
+ * - Short-circuit: return OR throw a Response to stop chain
306
385
  * - Error catching: try/catch around `next()` works
307
386
  */
308
387
  export async function executeMiddleware<TEnv>(
@@ -332,42 +411,16 @@ export async function executeMiddleware<TEnv>(
332
411
  // End of chain - call actual RSC handler
333
412
  const response = await finalHandler();
334
413
 
335
- // Merge headers set on stub into the real response.
336
- // Use append for Set-Cookie to preserve multiple cookies.
337
- const mergedHeaders = new Headers(response.headers);
338
- stubResponse.headers.forEach((value, name) => {
339
- if (name.toLowerCase() === "set-cookie") {
340
- mergedHeaders.append(name, value);
341
- } else {
342
- mergedHeaders.set(name, value);
343
- }
344
- });
345
- // Also merge shared RequestContext stub (cookies written via cookies().set()).
346
- // Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
347
- // may have already merged the same reqCtx cookies into the response.
348
- const reqCtx = _getRequestContext();
349
- if (reqCtx) {
350
- const stubCookies = reqCtx.res.headers.getSetCookie();
351
- if (stubCookies.length > 0) {
352
- const existing = new Set(mergedHeaders.getSetCookie());
353
- for (const cookie of stubCookies) {
354
- if (!existing.has(cookie)) {
355
- mergedHeaders.append("set-cookie", cookie);
356
- }
357
- }
358
- }
359
- reqCtx.res.headers.forEach((value, name) => {
360
- if (name !== "set-cookie" && !mergedHeaders.has(name)) {
361
- mergedHeaders.set(name, value);
362
- }
363
- });
414
+ if (isWebSocketUpgradeResponse(response)) {
415
+ responseHolder.response = response;
416
+ return response;
364
417
  }
365
418
 
366
- // Clone response with merged headers (mutable for post-next() modifications)
367
- responseHolder.response = new Response(response.body, {
368
- status: response.status,
369
- statusText: response.statusText,
370
- headers: mergedHeaders,
419
+ // Chain ran to completion: stub headers overwrite (stubOverridesNonCookie)
420
+ // and reqCtx stub headers are merged in.
421
+ responseHolder.response = mergeResponse(response, stubResponse.headers, {
422
+ stubOverridesNonCookie: true,
423
+ mergeReqCtx: true,
371
424
  });
372
425
 
373
426
  return responseHolder.response;
@@ -429,8 +482,16 @@ export async function executeMiddleware<TEnv>(
429
482
  try {
430
483
  result = await entry.handler(ctx, wrappedNext);
431
484
  } catch (error) {
432
- finishMiddleware();
433
- throw error;
485
+ // Thrown Response is short-circuit control flow, not an error.
486
+ // Fall through to the `if (result instanceof Response)` branch below
487
+ // so stub headers and request-context cookies merge as they do for
488
+ // an explicit `return new Response(...)`. Real errors propagate.
489
+ if (error instanceof Response) {
490
+ result = error;
491
+ } else {
492
+ finishMiddleware();
493
+ throw error;
494
+ }
434
495
  }
435
496
  finishMiddleware();
436
497
 
@@ -451,41 +512,18 @@ export async function executeMiddleware<TEnv>(
451
512
 
452
513
  // Explicit return takes precedence (middleware short-circuit).
453
514
  // Merge stub headers (from ctx.header before this point) and
454
- // RequestContext stub headers (from ctx.setCookie) into the
515
+ // RequestContext stub headers (from cookies().set()) into the
455
516
  // returned Response so they are not lost.
456
517
  if (result instanceof Response) {
457
- const mergedHeaders = new Headers(result.headers);
458
- stubResponse.headers.forEach((value, name) => {
459
- if (name.toLowerCase() === "set-cookie") {
460
- mergedHeaders.append(name, value);
461
- } else if (!mergedHeaders.has(name)) {
462
- mergedHeaders.set(name, value);
463
- }
464
- });
465
- // Also merge shared RequestContext stub (cookies written via setCookie).
466
- // Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
467
- // may have already merged the same reqCtx cookies into the response.
468
- const reqCtx = _getRequestContext();
469
- if (reqCtx) {
470
- const stubCookies = reqCtx.res.headers.getSetCookie();
471
- if (stubCookies.length > 0) {
472
- const existing = new Set(mergedHeaders.getSetCookie());
473
- for (const cookie of stubCookies) {
474
- if (!existing.has(cookie)) {
475
- mergedHeaders.append("set-cookie", cookie);
476
- }
477
- }
478
- }
479
- reqCtx.res.headers.forEach((value, name) => {
480
- if (name !== "set-cookie" && !mergedHeaders.has(name)) {
481
- mergedHeaders.set(name, value);
482
- }
483
- });
518
+ if (isWebSocketUpgradeResponse(result)) {
519
+ responseHolder.response = result;
520
+ return result;
484
521
  }
485
- const merged = new Response(result.body, {
486
- status: result.status,
487
- statusText: result.statusText,
488
- headers: mergedHeaders,
522
+ // Explicit short-circuit: the returned Response's own headers win
523
+ // (stubOverridesNonCookie=false); reqCtx stub headers still merge in.
524
+ const merged = mergeResponse(result, stubResponse.headers, {
525
+ stubOverridesNonCookie: false,
526
+ mergeReqCtx: true,
489
527
  });
490
528
  responseHolder.response = merged;
491
529
  return merged;
@@ -530,23 +568,12 @@ export async function executeMiddleware<TEnv>(
530
568
  // last merge point (e.g. cookies().set() called after await next()).
531
569
  // The reqCtx stub may have already been partially merged during finalHandler
532
570
  // or early-return paths; only append *new* Set-Cookie entries to avoid dupes.
571
+ //
572
+ // Skip for upgrade responses: upgrade headers are semantically immutable and
573
+ // set-cookie on an upgrade is not meaningful.
533
574
  const reqCtx = _getRequestContext();
534
- if (reqCtx) {
535
- const stubCookies = reqCtx.res.headers.getSetCookie();
536
- if (stubCookies.length > 0) {
537
- const existingCookies = new Set(finalResponse.headers.getSetCookie());
538
- for (const cookie of stubCookies) {
539
- if (!existingCookies.has(cookie)) {
540
- finalResponse.headers.append("set-cookie", cookie);
541
- }
542
- }
543
- }
544
- // Fill in non-cookie headers that aren't already on the response
545
- reqCtx.res.headers.forEach((value, name) => {
546
- if (name !== "set-cookie" && !finalResponse.headers.has(name)) {
547
- finalResponse.headers.set(name, value);
548
- }
549
- });
575
+ if (reqCtx && !isWebSocketUpgradeResponse(finalResponse)) {
576
+ mergeReqCtxStub(finalResponse.headers, reqCtx);
550
577
  }
551
578
 
552
579
  return finalResponse;
@@ -557,7 +584,7 @@ export async function executeMiddleware<TEnv>(
557
584
  *
558
585
  * Intercepts use a shared stubResponse from the request context. This function:
559
586
  * - Runs middleware in sequence with a simple next() chain
560
- * - Returns Response if any middleware short-circuits (returns Response or redirects BEFORE next())
587
+ * - Returns Response if any middleware short-circuits (returns OR throws a Response, or redirects, BEFORE next())
561
588
  * - Returns null if all middleware calls next() - headers set after next() remain on stubResponse
562
589
  *
563
590
  * @param middlewares - Array of middleware functions
@@ -616,7 +643,18 @@ export async function executeInterceptMiddleware<TEnv>(
616
643
  return next();
617
644
  };
618
645
 
619
- const result = await middleware(ctx, guardedNext);
646
+ let result: Response | void;
647
+ try {
648
+ result = await middleware(ctx, guardedNext);
649
+ } catch (error) {
650
+ // Thrown Response is short-circuit control flow, parity with the
651
+ // explicit-return path below. Real errors propagate.
652
+ if (error instanceof Response) {
653
+ result = error;
654
+ } else {
655
+ throw error;
656
+ }
657
+ }
620
658
 
621
659
  if (result instanceof Response) {
622
660
  earlyResponse = result;
@@ -640,21 +678,13 @@ export async function executeInterceptMiddleware<TEnv>(
640
678
  });
641
679
 
642
680
  if (hasStubHeaders) {
643
- // Clone and merge headers from stub into early response.
644
- // Only fill in missing headers — the returned Response's explicit
645
- // headers take precedence, matching executeMiddleware behavior.
646
- const mergedHeaders = new Headers(response.headers);
647
- stubResponse.headers.forEach((value, name) => {
648
- if (name.toLowerCase() === "set-cookie") {
649
- mergedHeaders.append(name, value);
650
- } else if (!mergedHeaders.has(name)) {
651
- mergedHeaders.set(name, value);
652
- }
653
- });
654
- return new Response(response.body, {
655
- status: response.status,
656
- statusText: response.statusText,
657
- headers: mergedHeaders,
681
+ // Only fill in missing headers the returned Response's explicit headers
682
+ // take precedence (stubOverridesNonCookie=false), matching executeMiddleware.
683
+ // mergeReqCtx=false: the intercept path deliberately does NOT merge reqCtx
684
+ // stub headers here (pinned by intercept-middleware-headers.test.ts).
685
+ return mergeResponse(response, stubResponse.headers, {
686
+ stubOverridesNonCookie: false,
687
+ mergeReqCtx: false,
658
688
  });
659
689
  }
660
690
  return response;
@@ -695,7 +725,6 @@ export async function executeLoaderMiddleware<TEnv>(
695
725
  regex: null,
696
726
  paramNames: [],
697
727
  handler,
698
- mountPrefix: null,
699
728
  } as MiddlewareEntry<TEnv>,
700
729
  params,
701
730
  }));