@rangojs/router 0.0.0-experimental.eb0645d3 → 0.0.0-experimental.f1468e3c

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 (392) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.md +126 -16
  3. package/dist/bin/rango.js +319 -95
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +2724 -1053
  6. package/package.json +68 -14
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +64 -2
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +224 -32
  11. package/skills/caching/SKILL.md +279 -17
  12. package/skills/composability/SKILL.md +27 -3
  13. package/skills/css/SKILL.md +76 -0
  14. package/skills/debug-manifest/SKILL.md +4 -2
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +11 -9
  17. package/skills/hooks/SKILL.md +243 -29
  18. package/skills/host-router/SKILL.md +83 -23
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +68 -19
  21. package/skills/layout/SKILL.md +13 -9
  22. package/skills/links/SKILL.md +190 -23
  23. package/skills/loader/SKILL.md +235 -9
  24. package/skills/middleware/SKILL.md +18 -10
  25. package/skills/migrate-nextjs/SKILL.md +43 -19
  26. package/skills/migrate-react-router/SKILL.md +8 -2
  27. package/skills/mime-routes/SKILL.md +28 -1
  28. package/skills/observability/SKILL.md +172 -0
  29. package/skills/parallel/SKILL.md +18 -7
  30. package/skills/prerender/SKILL.md +65 -60
  31. package/skills/rango/SKILL.md +251 -24
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +115 -48
  34. package/skills/route/SKILL.md +46 -5
  35. package/skills/router-setup/SKILL.md +30 -8
  36. package/skills/scripts/SKILL.md +179 -0
  37. package/skills/server-actions/SKILL.md +775 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +130 -0
  40. package/skills/testing/bindings.md +103 -0
  41. package/skills/testing/cache-prerender.md +127 -0
  42. package/skills/testing/client-components.md +124 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +91 -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 +122 -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 +123 -0
  54. package/skills/typesafety/SKILL.md +322 -29
  55. package/skills/use-cache/SKILL.md +57 -14
  56. package/skills/view-transitions/SKILL.md +337 -0
  57. package/src/__augment-tests__/augment.ts +81 -0
  58. package/src/__augment-tests__/augmented.check.ts +116 -0
  59. package/src/__internal.ts +0 -65
  60. package/src/browser/action-coordinator.ts +53 -36
  61. package/src/browser/action-fence.ts +47 -0
  62. package/src/browser/app-shell.ts +39 -0
  63. package/src/browser/connection-warmup.ts +134 -0
  64. package/src/browser/cookie-name.ts +140 -0
  65. package/src/browser/event-controller.ts +192 -150
  66. package/src/browser/history-state.ts +21 -0
  67. package/src/browser/index.ts +3 -3
  68. package/src/browser/invalidate-client-cache.ts +52 -0
  69. package/src/browser/navigation-bridge.ts +94 -25
  70. package/src/browser/navigation-client.ts +121 -84
  71. package/src/browser/navigation-store-handle.ts +38 -0
  72. package/src/browser/navigation-store.ts +115 -67
  73. package/src/browser/navigation-transaction.ts +9 -59
  74. package/src/browser/network-error-handler.ts +34 -7
  75. package/src/browser/partial-update.ts +147 -128
  76. package/src/browser/prefetch/cache.ts +107 -56
  77. package/src/browser/prefetch/fetch.ts +204 -34
  78. package/src/browser/prefetch/queue.ts +6 -3
  79. package/src/browser/rango-state.ts +158 -76
  80. package/src/browser/react/Link.tsx +30 -7
  81. package/src/browser/react/NavigationProvider.tsx +283 -118
  82. package/src/browser/react/ScrollRestoration.tsx +10 -6
  83. package/src/browser/react/deferred-handle-resolution.ts +75 -0
  84. package/src/browser/react/filter-segment-order.ts +66 -7
  85. package/src/browser/react/index.ts +0 -48
  86. package/src/browser/react/location-state-shared.ts +178 -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 +17 -14
  90. package/src/browser/react/use-href.tsx +8 -1
  91. package/src/browser/react/use-link-status.ts +33 -8
  92. package/src/browser/react/use-navigation.ts +10 -5
  93. package/src/browser/react/use-params.ts +11 -11
  94. package/src/browser/react/use-reverse.ts +106 -0
  95. package/src/browser/react/use-router.ts +25 -3
  96. package/src/browser/react/use-search-params.ts +0 -5
  97. package/src/browser/react/use-segments.ts +11 -21
  98. package/src/browser/response-adapter.ts +99 -8
  99. package/src/browser/rsc-router.tsx +91 -24
  100. package/src/browser/scroll-restoration.ts +30 -17
  101. package/src/browser/segment-structure-assert.ts +2 -2
  102. package/src/browser/server-action-bridge.ts +214 -55
  103. package/src/browser/types.ts +80 -9
  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 +60 -35
  107. package/src/build/generate-route-types.ts +2 -1
  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 +117 -14
  111. package/src/build/route-types/ast-route-extraction.ts +15 -8
  112. package/src/build/route-types/codegen.ts +16 -5
  113. package/src/build/route-types/include-resolution.ts +117 -23
  114. package/src/build/route-types/param-extraction.ts +6 -3
  115. package/src/build/route-types/per-module-writer.ts +22 -6
  116. package/src/build/route-types/router-processing.ts +55 -28
  117. package/src/build/route-types/scan-filter.ts +1 -1
  118. package/src/build/route-types/source-scan.ts +216 -0
  119. package/src/build/runtime-discovery.ts +9 -20
  120. package/src/cache/cache-error.ts +104 -0
  121. package/src/cache/cache-key-utils.ts +29 -13
  122. package/src/cache/cache-policy.ts +108 -34
  123. package/src/cache/cache-runtime.ts +224 -41
  124. package/src/cache/cache-scope.ts +188 -82
  125. package/src/cache/cache-tag.ts +103 -0
  126. package/src/cache/cf/cf-base64.ts +33 -0
  127. package/src/cache/cf/cf-cache-constants.ts +127 -0
  128. package/src/cache/cf/cf-cache-store.ts +1989 -378
  129. package/src/cache/cf/cf-cache-types.ts +349 -0
  130. package/src/cache/cf/cf-kv-utils.ts +46 -0
  131. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  132. package/src/cache/cf/index.ts +6 -16
  133. package/src/cache/document-cache.ts +89 -21
  134. package/src/cache/handle-snapshot.ts +70 -0
  135. package/src/cache/index.ts +10 -20
  136. package/src/cache/memory-segment-store.ts +136 -37
  137. package/src/cache/profile-registry.ts +46 -31
  138. package/src/cache/read-through-swr.ts +56 -12
  139. package/src/cache/segment-codec.ts +9 -17
  140. package/src/cache/tag-invalidation.ts +230 -0
  141. package/src/cache/types.ts +37 -100
  142. package/src/client.rsc.tsx +44 -21
  143. package/src/client.tsx +36 -61
  144. package/src/cloudflare/index.ts +11 -0
  145. package/src/cloudflare/tracing.ts +109 -0
  146. package/src/component-utils.ts +19 -0
  147. package/src/components/DefaultDocument.tsx +8 -2
  148. package/src/context-var.ts +18 -6
  149. package/src/decode-loader-results.ts +52 -0
  150. package/src/defer.ts +196 -0
  151. package/src/deps/ssr.ts +0 -1
  152. package/src/encode-kv.ts +49 -0
  153. package/src/errors.ts +30 -4
  154. package/src/escape-script.ts +52 -0
  155. package/src/handle.ts +31 -23
  156. package/src/handles/MetaTags.tsx +62 -19
  157. package/src/handles/Scripts.tsx +183 -0
  158. package/src/handles/breadcrumbs.ts +37 -8
  159. package/src/handles/is-thenable.ts +19 -0
  160. package/src/handles/meta.ts +51 -40
  161. package/src/handles/script.ts +244 -0
  162. package/src/host/cookie-handler.ts +9 -60
  163. package/src/host/errors.ts +0 -24
  164. package/src/host/index.ts +8 -2
  165. package/src/host/pattern-matcher.ts +23 -52
  166. package/src/host/router.ts +107 -99
  167. package/src/host/testing.ts +40 -27
  168. package/src/host/types.ts +37 -4
  169. package/src/host/utils.ts +1 -1
  170. package/src/href-client.ts +137 -22
  171. package/src/index.rsc.ts +96 -12
  172. package/src/index.ts +94 -14
  173. package/src/internal-debug.ts +11 -10
  174. package/src/loader-store.ts +500 -0
  175. package/src/loader.rsc.ts +20 -13
  176. package/src/loader.ts +12 -11
  177. package/src/missing-id-error.ts +68 -0
  178. package/src/outlet-context.ts +1 -1
  179. package/src/outlet-provider.tsx +1 -5
  180. package/src/prerender/param-hash.ts +16 -16
  181. package/src/prerender/store.ts +32 -37
  182. package/src/prerender.ts +61 -6
  183. package/src/redirect-origin.ts +100 -0
  184. package/src/regex-escape.ts +8 -0
  185. package/src/render-error-thrower.tsx +20 -0
  186. package/src/response-utils.ts +34 -0
  187. package/src/reverse.ts +65 -40
  188. package/src/root-error-boundary.tsx +1 -19
  189. package/src/route-content-wrapper.tsx +19 -77
  190. package/src/route-definition/dsl-helpers.ts +304 -309
  191. package/src/route-definition/helper-factories.ts +28 -140
  192. package/src/route-definition/helpers-types.ts +82 -55
  193. package/src/route-definition/index.ts +1 -2
  194. package/src/route-definition/redirect.ts +44 -11
  195. package/src/route-definition/resolve-handler-use.ts +12 -1
  196. package/src/route-definition/use-item-types.ts +29 -0
  197. package/src/route-map-builder.ts +0 -16
  198. package/src/route-types.ts +19 -46
  199. package/src/router/basename.ts +14 -0
  200. package/src/router/content-negotiation.ts +73 -25
  201. package/src/router/error-handling.ts +45 -18
  202. package/src/router/find-match.ts +44 -23
  203. package/src/router/handler-context.ts +27 -43
  204. package/src/router/instrument.ts +350 -0
  205. package/src/router/intercept-resolution.ts +39 -20
  206. package/src/router/lazy-includes.ts +10 -47
  207. package/src/router/loader-resolution.ts +155 -72
  208. package/src/router/logging.ts +0 -6
  209. package/src/router/manifest.ts +18 -29
  210. package/src/router/match-api.ts +9 -24
  211. package/src/router/match-context.ts +0 -22
  212. package/src/router/match-handlers.ts +58 -58
  213. package/src/router/match-middleware/background-revalidation.ts +40 -24
  214. package/src/router/match-middleware/cache-lookup.ts +159 -285
  215. package/src/router/match-middleware/cache-store.ts +64 -52
  216. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  217. package/src/router/match-middleware/segment-resolution.ts +0 -22
  218. package/src/router/match-pipelines.ts +1 -42
  219. package/src/router/match-result.ts +44 -74
  220. package/src/router/metrics.ts +0 -34
  221. package/src/router/middleware-types.ts +7 -134
  222. package/src/router/middleware.ts +247 -166
  223. package/src/router/navigation-snapshot.ts +0 -51
  224. package/src/router/params-util.ts +23 -0
  225. package/src/router/pattern-matching.ts +85 -94
  226. package/src/router/prefetch-cache-ttl.ts +51 -0
  227. package/src/router/prerender-match.ts +104 -65
  228. package/src/router/preview-match.ts +3 -1
  229. package/src/router/request-classification.ts +28 -62
  230. package/src/router/revalidation.ts +123 -73
  231. package/src/router/route-snapshot.ts +0 -1
  232. package/src/router/router-context.ts +3 -28
  233. package/src/router/router-interfaces.ts +83 -35
  234. package/src/router/router-options.ts +136 -5
  235. package/src/router/router-registry.ts +2 -5
  236. package/src/router/segment-resolution/fresh.ts +97 -84
  237. package/src/router/segment-resolution/helpers.ts +86 -6
  238. package/src/router/segment-resolution/loader-cache.ts +76 -39
  239. package/src/router/segment-resolution/revalidation.ts +272 -320
  240. package/src/router/segment-resolution/static-store.ts +19 -5
  241. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  242. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  243. package/src/router/segment-resolution.ts +5 -1
  244. package/src/router/segment-wrappers.ts +6 -5
  245. package/src/router/state-cookie-name.ts +33 -0
  246. package/src/router/substitute-pattern-params.ts +56 -0
  247. package/src/router/telemetry-otel.ts +161 -199
  248. package/src/router/telemetry.ts +96 -19
  249. package/src/router/timeout.ts +0 -20
  250. package/src/router/tracing.ts +206 -0
  251. package/src/router/trie-matching.ts +162 -64
  252. package/src/router/types.ts +9 -63
  253. package/src/router/url-params.ts +0 -5
  254. package/src/router.ts +110 -55
  255. package/src/rsc/handler-context.ts +3 -2
  256. package/src/rsc/handler.ts +264 -220
  257. package/src/rsc/helpers.ts +100 -6
  258. package/src/rsc/index.ts +2 -5
  259. package/src/rsc/json-route-result.ts +38 -0
  260. package/src/rsc/loader-fetch.ts +114 -38
  261. package/src/rsc/manifest-init.ts +28 -41
  262. package/src/rsc/origin-guard.ts +39 -25
  263. package/src/rsc/progressive-enhancement.ts +117 -11
  264. package/src/rsc/redirect-guard.ts +99 -0
  265. package/src/rsc/response-cache-serve.ts +238 -0
  266. package/src/rsc/response-error.ts +79 -12
  267. package/src/rsc/response-route-handler.ts +88 -188
  268. package/src/rsc/rsc-rendering.ts +98 -76
  269. package/src/rsc/runtime-warnings.ts +23 -10
  270. package/src/rsc/server-action.ts +281 -117
  271. package/src/rsc/ssr-setup.ts +16 -0
  272. package/src/rsc/transition-gate.ts +89 -0
  273. package/src/rsc/types.ts +23 -5
  274. package/src/runtime-env.ts +18 -0
  275. package/src/search-params.ts +35 -30
  276. package/src/segment-loader-promise.ts +31 -4
  277. package/src/segment-system.tsx +254 -143
  278. package/src/serialize.ts +243 -0
  279. package/src/server/context.ts +163 -51
  280. package/src/server/cookie-parse.ts +32 -0
  281. package/src/server/cookie-store.ts +80 -5
  282. package/src/server/handle-store.ts +21 -38
  283. package/src/server/loader-registry.ts +33 -42
  284. package/src/server/request-context.ts +287 -178
  285. package/src/ssr/index.tsx +21 -16
  286. package/src/static-handler.ts +10 -13
  287. package/src/testing/cache-status.ts +162 -0
  288. package/src/testing/collect-handle.ts +40 -0
  289. package/src/testing/dispatch.ts +701 -0
  290. package/src/testing/dom.entry.ts +22 -0
  291. package/src/testing/e2e/fixture.ts +188 -0
  292. package/src/testing/e2e/index.ts +128 -0
  293. package/src/testing/e2e/matchers.ts +35 -0
  294. package/src/testing/e2e/page-helpers.ts +272 -0
  295. package/src/testing/e2e/parity.ts +387 -0
  296. package/src/testing/e2e/server.ts +195 -0
  297. package/src/testing/flight-matchers.ts +97 -0
  298. package/src/testing/flight-normalize.ts +11 -0
  299. package/src/testing/flight-runtime.d.ts +57 -0
  300. package/src/testing/flight-tree.ts +682 -0
  301. package/src/testing/flight.entry.ts +52 -0
  302. package/src/testing/flight.ts +257 -0
  303. package/src/testing/generated-routes.ts +183 -0
  304. package/src/testing/index.ts +105 -0
  305. package/src/testing/internal/context.ts +371 -0
  306. package/src/testing/internal/flight-client-globals.ts +30 -0
  307. package/src/testing/internal/seed-vars.ts +54 -0
  308. package/src/testing/render-handler.ts +357 -0
  309. package/src/testing/render-route.tsx +581 -0
  310. package/src/testing/run-loader.ts +385 -0
  311. package/src/testing/run-middleware.ts +205 -0
  312. package/src/testing/run-transition-when.ts +164 -0
  313. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  314. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  315. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  316. package/src/testing/vitest-stubs/version.ts +5 -0
  317. package/src/testing/vitest.ts +305 -0
  318. package/src/theme/ThemeProvider.tsx +20 -58
  319. package/src/theme/ThemeScript.tsx +7 -9
  320. package/src/theme/constants.ts +52 -13
  321. package/src/theme/index.ts +0 -7
  322. package/src/theme/theme-context.ts +1 -5
  323. package/src/theme/theme-script.ts +22 -21
  324. package/src/theme/use-theme.ts +0 -3
  325. package/src/types/boundaries.ts +0 -35
  326. package/src/types/cache-types.ts +13 -4
  327. package/src/types/error-types.ts +30 -90
  328. package/src/types/global-namespace.ts +54 -41
  329. package/src/types/handler-context.ts +110 -62
  330. package/src/types/index.ts +3 -10
  331. package/src/types/loader-types.ts +11 -9
  332. package/src/types/request-scope.ts +112 -0
  333. package/src/types/route-config.ts +6 -50
  334. package/src/types/route-entry.ts +0 -6
  335. package/src/types/segments.ts +135 -14
  336. package/src/urls/include-helper.ts +9 -56
  337. package/src/urls/index.ts +1 -11
  338. package/src/urls/path-helper-types.ts +29 -12
  339. package/src/urls/path-helper.ts +17 -106
  340. package/src/urls/pattern-types.ts +36 -19
  341. package/src/urls/response-types.ts +22 -29
  342. package/src/urls/type-extraction.ts +58 -139
  343. package/src/urls/urls-function.ts +1 -19
  344. package/src/use-loader.tsx +292 -107
  345. package/src/vite/debug.ts +185 -0
  346. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  347. package/src/vite/discovery/discover-routers.ts +126 -85
  348. package/src/vite/discovery/discovery-errors.ts +194 -0
  349. package/src/vite/discovery/gate-state.ts +171 -0
  350. package/src/vite/discovery/prerender-collection.ts +96 -68
  351. package/src/vite/discovery/route-types-writer.ts +40 -84
  352. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  353. package/src/vite/discovery/state.ts +44 -0
  354. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  355. package/src/vite/index.ts +2 -0
  356. package/src/vite/inject-client-debug.ts +36 -0
  357. package/src/vite/plugin-types.ts +126 -8
  358. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  359. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  360. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  361. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  362. package/src/vite/plugins/expose-action-id.ts +48 -95
  363. package/src/vite/plugins/expose-id-utils.ts +88 -55
  364. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  365. package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
  366. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  367. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  368. package/src/vite/plugins/expose-internal-ids.ts +505 -486
  369. package/src/vite/plugins/performance-tracks.ts +26 -25
  370. package/src/vite/plugins/refresh-cmd.ts +1 -1
  371. package/src/vite/plugins/use-cache-transform.ts +73 -83
  372. package/src/vite/plugins/version-injector.ts +40 -29
  373. package/src/vite/plugins/version-plugin.ts +37 -40
  374. package/src/vite/plugins/virtual-entries.ts +39 -25
  375. package/src/vite/rango.ts +109 -118
  376. package/src/vite/router-discovery.ts +718 -119
  377. package/src/vite/utils/ast-handler-extract.ts +26 -35
  378. package/src/vite/utils/banner.ts +1 -1
  379. package/src/vite/utils/bundle-analysis.ts +10 -15
  380. package/src/vite/utils/client-chunks.ts +184 -0
  381. package/src/vite/utils/directive-prologue.ts +40 -0
  382. package/src/vite/utils/forward-user-plugins.ts +171 -0
  383. package/src/vite/utils/manifest-utils.ts +4 -59
  384. package/src/vite/utils/package-resolution.ts +20 -52
  385. package/src/vite/utils/prerender-utils.ts +54 -39
  386. package/src/vite/utils/shared-utils.ts +90 -41
  387. package/src/browser/action-response-classifier.ts +0 -99
  388. package/src/browser/react/use-client-cache.ts +0 -58
  389. package/src/browser/shallow.ts +0 -40
  390. package/src/handles/index.ts +0 -7
  391. package/src/network-error-thrower.tsx +0 -23
  392. package/src/router/middleware-cookies.ts +0 -55
@@ -1,16 +1,10 @@
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 { escapeRegExp } from "../regex-escape.js";
5
+ import { parsePattern as parseRoutePattern } from "./pattern-matching.js";
13
6
  import { safeDecodeURIComponent } from "./url-params.js";
7
+ import { fireAndForgetWaitUntil } from "../types/request-scope.js";
14
8
  import type {
15
9
  CollectedMiddleware,
16
10
  MiddlewareCollectableEntry,
@@ -20,28 +14,29 @@ import type {
20
14
  ResponseHolder,
21
15
  } from "./middleware-types.js";
22
16
  import { _getRequestContext } from "../server/request-context.js";
17
+ import {
18
+ EXTERNAL_REDIRECT_MARKER,
19
+ isExternalRedirect,
20
+ markExternalRedirect,
21
+ } from "../redirect-origin.js";
23
22
  import { isAutoGeneratedRouteName } from "../route-name.js";
24
23
  import { appendMetric, createMetricsStore } from "./metrics.js";
24
+ import { observePhase, PHASES } from "./instrument.js";
25
25
  import { stripInternalParams } from "./handler-context.js";
26
26
  import { isWebSocketUpgradeResponse } from "../response-utils.js";
27
27
 
28
- // Re-export types and cookie utilities for backward compatibility
28
+ // Re-export types consumed through this module's path.
29
29
  export type {
30
30
  CookieOptions,
31
- CollectedMiddleware,
32
- MiddlewareCollectableEntry,
33
31
  MiddlewareContext,
34
32
  MiddlewareEntry,
35
33
  MiddlewareFn,
36
- ResponseHolder,
37
34
  } from "./middleware-types.js";
38
- export { parseCookies, serializeCookie } from "./middleware-cookies.js";
39
35
 
40
36
  const MIDDLEWARE_METRIC_DEPTH = 1;
41
- /** Ignore post-next() durations below this threshold (measurement noise). */
42
37
  const POST_METRIC_MIN_DURATION_MS = 0.01;
43
38
 
44
- function getMiddlewareMetricBase<TEnv>(
39
+ function getMiddlewareMetricLabel<TEnv>(
45
40
  entry: MiddlewareEntry<TEnv>,
46
41
  ordinal: number,
47
42
  ): string {
@@ -49,24 +44,49 @@ function getMiddlewareMetricBase<TEnv>(
49
44
  const scope = entry.pattern ?? "*";
50
45
 
51
46
  if (handlerName) {
52
- return `${handlerName}@${scope}`;
47
+ return `middleware:${handlerName}@${scope}`;
53
48
  }
54
49
 
55
- return `${scope}#${ordinal + 1}`;
56
- }
57
-
58
- function getMiddlewareMetricLabel<TEnv>(
59
- entry: MiddlewareEntry<TEnv>,
60
- ordinal: number,
61
- ): string {
62
- return `middleware:${getMiddlewareMetricBase(entry, ordinal)}`;
50
+ return `middleware:${scope}#${ordinal + 1}`;
63
51
  }
64
52
 
65
53
  /**
66
- * Parse a route pattern into regex and param names
67
- * Supports: *, /path, /path/*, /path/:param, /path/:param/*
54
+ * Compile a middleware scope pattern to a regex + param names.
55
+ *
56
+ * Middleware scopes reuse the route pattern parser (`parsePattern` from
57
+ * pattern-matching.ts), so they support the same param forms as routes —
58
+ * optional (`:x?`), constrained (`:x(en|gb)`), and suffix (`:x.html`) — in
59
+ * addition to the trailing-`*` wildcard middleware relies on. Before this
60
+ * unification the middleware-side parser handled only static, bare `:param`,
61
+ * and trailing `*`, so e.g. `router.use("/:locale(en|gb)/*", mw)` silently
62
+ * named the param "locale(en|gb)" and never enforced the constraint.
63
+ *
64
+ * Middleware matching semantics deliberately differ from route matching, so we
65
+ * emit the regex here rather than route through `compilePattern`:
66
+ * - `*` alone matches every path (`/^.*$/`).
67
+ * - A trailing `*` segment is an OPTIONAL subtree match (`(?:/.*)?`): `/admin/*`
68
+ * matches `/admin`, `/admin/`, and `/admin/users`. It contributes no param
69
+ * name (unlike route wildcards, which capture `*`).
70
+ * - A NON-trailing `*` is also OPTIONAL (`(?:/.*)?`), matching zero-or-more
71
+ * intermediate segments: `/a/<star>/b` matches both `/a/b` and `/a/x/b`. This
72
+ * mirrors the pre-unification parser, which compiled every `*` part as
73
+ * optional regardless of position.
74
+ * - A pattern without a trailing `*` tolerates a trailing slash (`/?$`).
75
+ * - Constraints are baked into the regex as an alternation so `matchMiddleware`
76
+ * (a bare `regex.test`) enforces them without extra validation. Constraint
77
+ * values are matched against the raw (still URL-encoded) path segment, which
78
+ * matches the pre-unification middleware behavior (it never decoded for
79
+ * matching); the constraint string is regex-escaped so values like `en.gb`
80
+ * are treated literally.
81
+ *
82
+ * The route segment parser only recognizes `/`-prefixed segments, but the
83
+ * pre-unification middleware parser split on `/` and dropped empty parts, so a
84
+ * leading slash was irrelevant: `use("admin/*")` and `use("/admin/*")` scoped
85
+ * identically. Normalize a non-`*` pattern to have a leading slash before
86
+ * parsing so that behavior is preserved (without it, `parseRoutePattern("admin/*")`
87
+ * drops the static `admin` and the scope explodes to every path).
68
88
  */
69
- export function parsePattern(pattern: string): {
89
+ export function compileMiddlewarePattern(pattern: string): {
70
90
  regex: RegExp;
71
91
  paramNames: string[];
72
92
  } {
@@ -74,45 +94,47 @@ export function parsePattern(pattern: string): {
74
94
  return { regex: /^.*$/, paramNames: [] };
75
95
  }
76
96
 
97
+ const normalizedPattern = pattern.startsWith("/") ? pattern : `/${pattern}`;
98
+ const segments = parseRoutePattern(normalizedPattern);
77
99
  const paramNames: string[] = [];
78
100
  let regexStr = "^";
101
+ let hasTrailingWildcard = false;
79
102
 
80
- const parts = pattern.split("/").filter(Boolean);
81
-
82
- for (let i = 0; i < parts.length; i++) {
83
- const part = parts[i];
103
+ for (let i = 0; i < segments.length; i++) {
104
+ const segment = segments[i];
84
105
 
85
- if (part === "*") {
86
- // Wildcard - match rest of path
106
+ if (segment.type === "wildcard") {
107
+ // Optional subtree match (parity with the original middleware parser,
108
+ // which compiled every `*` as `(?:/.*)?`). A trailing `*` matches the
109
+ // subtree; a non-trailing `*` matches zero-or-more intermediate segments,
110
+ // so `/a/<star>/b` still matches `/a/b`.
87
111
  regexStr += "(?:/.*)?";
88
- } else if (part.startsWith(":")) {
89
- // Param
90
- const paramName = part.slice(1);
91
- paramNames.push(paramName);
92
- regexStr += "/([^/]+)";
112
+ if (i === segments.length - 1) {
113
+ hasTrailingWildcard = true;
114
+ }
115
+ } else if (segment.type === "param") {
116
+ paramNames.push(segment.value);
117
+ const suffixPattern = segment.suffix ? escapeRegExp(segment.suffix) : "";
118
+ const valuePattern = segment.constraint
119
+ ? `(${segment.constraint.map(escapeRegExp).join("|")})`
120
+ : "([^/]+)";
121
+ if (segment.optional) {
122
+ regexStr += `(?:/${valuePattern}${suffixPattern})?`;
123
+ } else {
124
+ regexStr += `/${valuePattern}${suffixPattern}`;
125
+ }
93
126
  } else {
94
- // Literal
95
- regexStr += "/" + escapeRegex(part);
127
+ // Static literal
128
+ regexStr += "/" + escapeRegExp(segment.value);
96
129
  }
97
130
  }
98
131
 
99
- // If pattern doesn't end with *, match exact or with trailing segments
100
- if (!pattern.endsWith("*")) {
101
- regexStr += "/?$";
102
- } else {
103
- regexStr += "$";
104
- }
132
+ // Without a trailing `*`, match exactly with an optional trailing slash.
133
+ regexStr += hasTrailingWildcard ? "$" : "/?$";
105
134
 
106
135
  return { regex: new RegExp(regexStr), paramNames };
107
136
  }
108
137
 
109
- /**
110
- * Escape special regex characters
111
- */
112
- function escapeRegex(str: string): string {
113
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
114
- }
115
-
116
138
  /**
117
139
  * Extract params from a pathname using a pattern's regex and param names.
118
140
  *
@@ -186,14 +208,22 @@ export function createMiddlewareContext<TEnv>(
186
208
  return responseHolder.response;
187
209
  };
188
210
 
211
+ // Capture reqCtx once: the request-scoped platform fields
212
+ // (originalUrl, executionContext, waitUntil) are immutable per request,
213
+ // so snapshotting beats re-reading ALS on every access. The lazy getters
214
+ // below (routeName, theme, setTheme) stay lazy because those can change
215
+ // during `await next()`.
216
+ const reqCtx = _getRequestContext();
189
217
  return {
190
218
  request,
191
219
  url,
192
- originalUrl: new URL(request.url),
220
+ originalUrl: reqCtx?.originalUrl ?? new URL(request.url),
193
221
  pathname: url.pathname,
194
222
  searchParams: url.searchParams,
195
223
  env: env as MiddlewareContext<TEnv>["env"],
196
224
  params,
225
+ executionContext: reqCtx?.executionContext,
226
+ waitUntil: reqCtx ? reqCtx.waitUntil.bind(reqCtx) : fireAndForgetWaitUntil,
197
227
  // Getter: re-derives from request context on each access so that global
198
228
  // middleware sees the matched route name after await next().
199
229
  get routeName(): MiddlewareContext<TEnv>["routeName"] {
@@ -252,9 +282,13 @@ export function createMiddlewareContext<TEnv>(
252
282
 
253
283
  reverse:
254
284
  reverse ??
255
- ((name: string) => {
285
+ ((
286
+ name: string,
287
+ _params?: Record<string, string>,
288
+ _search?: Record<string, unknown>,
289
+ ) => {
256
290
  throw new Error(
257
- `ctx.reverse() is not available - route map was not provided to middleware context`,
291
+ `ctx.reverse(${JSON.stringify(name)}) is not available: no route map is bound to this middleware context.`,
258
292
  );
259
293
  }),
260
294
 
@@ -288,9 +322,16 @@ export function matchMiddleware<TEnv>(
288
322
  continue;
289
323
  }
290
324
 
291
- // Check if pathname matches
292
- if (entry.regex.test(pathname)) {
293
- const params = extractParams(pathname, entry.regex, entry.paramNames);
325
+ // Run the scope regex ONCE per entry. The old code ran test() then, on a
326
+ // hit, extractParams' match() — a second full pass over the same string for
327
+ // every matching entry on the per-request hot path. The regexes carry no
328
+ // `g` flag, so there is no lastIndex statefulness across this single match.
329
+ const m = pathname.match(entry.regex);
330
+ if (m) {
331
+ const params: Record<string, string> = {};
332
+ for (let i = 0; i < entry.paramNames.length; i++) {
333
+ params[entry.paramNames[i]] = safeDecodeURIComponent(m[i + 1] || "");
334
+ }
294
335
  matches.push({ entry, params });
295
336
  }
296
337
  }
@@ -298,6 +339,88 @@ export function matchMiddleware<TEnv>(
298
339
  return matches;
299
340
  }
300
341
 
342
+ // Set-Cookie is appended; for other headers stubOverridesNonCookie=true
343
+ // overwrites (chain ran to completion), false fills only missing slots (an
344
+ // explicit short-circuit Response's own headers win).
345
+ function mergeStubHeaders(
346
+ target: Headers,
347
+ stub: Headers,
348
+ stubOverridesNonCookie: boolean,
349
+ ): void {
350
+ stub.forEach((value, name) => {
351
+ // The reserved external-redirect marker is internal and never a trust
352
+ // signal; never copy a stub value (e.g. a stray ctx.header() call) onto a
353
+ // browser-facing response. The opt-in is the out-of-band brand.
354
+ if (name.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
355
+ if (name.toLowerCase() === "set-cookie") {
356
+ target.append(name, value);
357
+ } else if (stubOverridesNonCookie || !target.has(name)) {
358
+ target.set(name, value);
359
+ }
360
+ });
361
+ }
362
+
363
+ // Set-Cookie is deduped so a nested inner executeMiddleware that already merged
364
+ // the same reqCtx cookies does not duplicate them; other headers fill if missing.
365
+ function mergeReqCtxStub(
366
+ target: Headers,
367
+ reqCtx: ReturnType<typeof _getRequestContext>,
368
+ ): void {
369
+ if (!reqCtx) return;
370
+ const stubCookies = reqCtx.res.headers.getSetCookie();
371
+ if (stubCookies.length > 0) {
372
+ const existing = new Set(target.getSetCookie());
373
+ for (const cookie of stubCookies) {
374
+ if (!existing.has(cookie)) {
375
+ target.append("set-cookie", cookie);
376
+ }
377
+ }
378
+ }
379
+ reqCtx.res.headers.forEach((value, name) => {
380
+ // Never propagate the reserved external-redirect marker (see mergeStubHeaders).
381
+ if (name.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
382
+ if (name !== "set-cookie" && !target.has(name)) {
383
+ target.set(name, value);
384
+ }
385
+ });
386
+ }
387
+
388
+ // Clone `base` with stub headers merged into a fresh Headers (the clone keeps
389
+ // the body mutable for post-next() modifications). Set-Cookie is always
390
+ // appended; other headers obey stubOverridesNonCookie (see mergeStubHeaders).
391
+ // mergeReqCtx folds in RequestContext stub cookies/headers; the intercept
392
+ // short-circuit path passes false (its reqCtx headers are not merged here),
393
+ // which is the one deliberate divergence between the call sites.
394
+ function mergeResponse(
395
+ base: Response,
396
+ stub: Headers,
397
+ opts: { stubOverridesNonCookie: boolean; mergeReqCtx: boolean },
398
+ ): Response {
399
+ const mergedHeaders = new Headers(base.headers);
400
+ // The reserved external-redirect marker is never a trust signal and must never
401
+ // reach the browser. The guard strips it on 3xx redirects; strip it here too so
402
+ // a forged value cannot ride a non-3xx middleware response (which the 3xx-only
403
+ // guard would not touch) to the client. The opt-in is the out-of-band brand.
404
+ mergedHeaders.delete(EXTERNAL_REDIRECT_MARKER);
405
+ mergeStubHeaders(mergedHeaders, stub, opts.stubOverridesNonCookie);
406
+ if (opts.mergeReqCtx) {
407
+ mergeReqCtxStub(mergedHeaders, _getRequestContext());
408
+ }
409
+ const merged = new Response(base.body, {
410
+ status: base.status,
411
+ statusText: base.statusText,
412
+ headers: mergedHeaders,
413
+ });
414
+ // Transfer the out-of-band external-redirect brand across this rebuild: a
415
+ // middleware short-circuit `return redirect(url, { external: true })` reaches
416
+ // the open-redirect guard only after this merge, and the brand lives on the
417
+ // Response object, not in its headers.
418
+ if (isExternalRedirect(base)) {
419
+ markExternalRedirect(merged);
420
+ }
421
+ return merged;
422
+ }
423
+
301
424
  /**
302
425
  * Execute middleware chain
303
426
  *
@@ -306,7 +429,7 @@ export function matchMiddleware<TEnv>(
306
429
  * - `ctx.headers` available before and after `await next()`
307
430
  * - `ctx.header()` shorthand for setting a single header
308
431
  * - Forgiving: if middleware doesn't return, uses the downstream response
309
- * - Short-circuit: return Response to stop chain
432
+ * - Short-circuit: return OR throw a Response to stop chain
310
433
  * - Error catching: try/catch around `next()` works
311
434
  */
312
435
  export async function executeMiddleware<TEnv>(
@@ -336,47 +459,16 @@ export async function executeMiddleware<TEnv>(
336
459
  // End of chain - call actual RSC handler
337
460
  const response = await finalHandler();
338
461
 
339
- // Merge headers set on stub into the real response.
340
- // Use append for Set-Cookie to preserve multiple cookies.
341
- const mergedHeaders = new Headers(response.headers);
342
- stubResponse.headers.forEach((value, name) => {
343
- if (name.toLowerCase() === "set-cookie") {
344
- mergedHeaders.append(name, value);
345
- } else {
346
- mergedHeaders.set(name, value);
347
- }
348
- });
349
- // Also merge shared RequestContext stub (cookies written via cookies().set()).
350
- // Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
351
- // may have already merged the same reqCtx cookies into the response.
352
- const reqCtx = _getRequestContext();
353
- if (reqCtx) {
354
- const stubCookies = reqCtx.res.headers.getSetCookie();
355
- if (stubCookies.length > 0) {
356
- const existing = new Set(mergedHeaders.getSetCookie());
357
- for (const cookie of stubCookies) {
358
- if (!existing.has(cookie)) {
359
- mergedHeaders.append("set-cookie", cookie);
360
- }
361
- }
362
- }
363
- reqCtx.res.headers.forEach((value, name) => {
364
- if (name !== "set-cookie" && !mergedHeaders.has(name)) {
365
- mergedHeaders.set(name, value);
366
- }
367
- });
368
- }
369
-
370
462
  if (isWebSocketUpgradeResponse(response)) {
371
463
  responseHolder.response = response;
372
464
  return response;
373
465
  }
374
466
 
375
- // Clone response with merged headers (mutable for post-next() modifications)
376
- responseHolder.response = new Response(response.body, {
377
- status: response.status,
378
- statusText: response.statusText,
379
- headers: mergedHeaders,
467
+ // Chain ran to completion: stub headers overwrite (stubOverridesNonCookie)
468
+ // and reqCtx stub headers are merged in.
469
+ responseHolder.response = mergeResponse(response, stubResponse.headers, {
470
+ stubOverridesNonCookie: true,
471
+ mergeReqCtx: true,
380
472
  });
381
473
 
382
474
  return responseHolder.response;
@@ -434,12 +526,28 @@ export async function executeMiddleware<TEnv>(
434
526
  return nextPromise;
435
527
  };
436
528
 
529
+ // Wrap the middleware (including its downstream next() chain) in its span
530
+ // via the unified phase API. metric:false — the middleware's perf metric is
531
+ // its exclusive pre/post own-time, recorded directly above and below, finer
532
+ // than a single wrap. Spans nest by async context, so this onions
533
+ // middleware-over-middleware and the core handler underneath. Pass-through
534
+ // when neither surface is active.
437
535
  let result: Response | void;
438
536
  try {
439
- result = await entry.handler(ctx, wrappedNext);
537
+ result = await observePhase(PHASES.middleware(metricLabel), () =>
538
+ entry.handler(ctx, wrappedNext),
539
+ );
440
540
  } catch (error) {
441
- finishMiddleware();
442
- throw error;
541
+ // Thrown Response is short-circuit control flow, not an error.
542
+ // Fall through to the `if (result instanceof Response)` branch below
543
+ // so stub headers and request-context cookies merge as they do for
544
+ // an explicit `return new Response(...)`. Real errors propagate.
545
+ if (error instanceof Response) {
546
+ result = error;
547
+ } else {
548
+ finishMiddleware();
549
+ throw error;
550
+ }
443
551
  }
444
552
  finishMiddleware();
445
553
 
@@ -460,45 +568,18 @@ export async function executeMiddleware<TEnv>(
460
568
 
461
569
  // Explicit return takes precedence (middleware short-circuit).
462
570
  // Merge stub headers (from ctx.header before this point) and
463
- // RequestContext stub headers (from ctx.setCookie) into the
571
+ // RequestContext stub headers (from cookies().set()) into the
464
572
  // returned Response so they are not lost.
465
573
  if (result instanceof Response) {
466
574
  if (isWebSocketUpgradeResponse(result)) {
467
575
  responseHolder.response = result;
468
576
  return result;
469
577
  }
470
- const mergedHeaders = new Headers(result.headers);
471
- stubResponse.headers.forEach((value, name) => {
472
- if (name.toLowerCase() === "set-cookie") {
473
- mergedHeaders.append(name, value);
474
- } else if (!mergedHeaders.has(name)) {
475
- mergedHeaders.set(name, value);
476
- }
477
- });
478
- // Also merge shared RequestContext stub (cookies written via setCookie).
479
- // Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
480
- // may have already merged the same reqCtx cookies into the response.
481
- const reqCtx = _getRequestContext();
482
- if (reqCtx) {
483
- const stubCookies = reqCtx.res.headers.getSetCookie();
484
- if (stubCookies.length > 0) {
485
- const existing = new Set(mergedHeaders.getSetCookie());
486
- for (const cookie of stubCookies) {
487
- if (!existing.has(cookie)) {
488
- mergedHeaders.append("set-cookie", cookie);
489
- }
490
- }
491
- }
492
- reqCtx.res.headers.forEach((value, name) => {
493
- if (name !== "set-cookie" && !mergedHeaders.has(name)) {
494
- mergedHeaders.set(name, value);
495
- }
496
- });
497
- }
498
- const merged = new Response(result.body, {
499
- status: result.status,
500
- statusText: result.statusText,
501
- headers: mergedHeaders,
578
+ // Explicit short-circuit: the returned Response's own headers win
579
+ // (stubOverridesNonCookie=false); reqCtx stub headers still merge in.
580
+ const merged = mergeResponse(result, stubResponse.headers, {
581
+ stubOverridesNonCookie: false,
582
+ mergeReqCtx: true,
502
583
  });
503
584
  responseHolder.response = merged;
504
585
  return merged;
@@ -548,21 +629,7 @@ export async function executeMiddleware<TEnv>(
548
629
  // set-cookie on an upgrade is not meaningful.
549
630
  const reqCtx = _getRequestContext();
550
631
  if (reqCtx && !isWebSocketUpgradeResponse(finalResponse)) {
551
- const stubCookies = reqCtx.res.headers.getSetCookie();
552
- if (stubCookies.length > 0) {
553
- const existingCookies = new Set(finalResponse.headers.getSetCookie());
554
- for (const cookie of stubCookies) {
555
- if (!existingCookies.has(cookie)) {
556
- finalResponse.headers.append("set-cookie", cookie);
557
- }
558
- }
559
- }
560
- // Fill in non-cookie headers that aren't already on the response
561
- reqCtx.res.headers.forEach((value, name) => {
562
- if (name !== "set-cookie" && !finalResponse.headers.has(name)) {
563
- finalResponse.headers.set(name, value);
564
- }
565
- });
632
+ mergeReqCtxStub(finalResponse.headers, reqCtx);
566
633
  }
567
634
 
568
635
  return finalResponse;
@@ -573,7 +640,7 @@ export async function executeMiddleware<TEnv>(
573
640
  *
574
641
  * Intercepts use a shared stubResponse from the request context. This function:
575
642
  * - Runs middleware in sequence with a simple next() chain
576
- * - Returns Response if any middleware short-circuits (returns Response or redirects BEFORE next())
643
+ * - Returns Response if any middleware short-circuits (returns OR throws a Response, or redirects, BEFORE next())
577
644
  * - Returns null if all middleware calls next() - headers set after next() remain on stubResponse
578
645
  *
579
646
  * @param middlewares - Array of middleware functions
@@ -611,6 +678,7 @@ export async function executeInterceptMiddleware<TEnv>(
611
678
  return stubResponse;
612
679
  }
613
680
 
681
+ const ordinal = index;
614
682
  const middleware = middlewares[index++];
615
683
  const ctx = createMiddlewareContext(
616
684
  request,
@@ -632,7 +700,29 @@ export async function executeInterceptMiddleware<TEnv>(
632
700
  return next();
633
701
  };
634
702
 
635
- const result = await middleware(ctx, guardedNext);
703
+ // Span-wrap each intercept middleware as rango.middleware (metric:false
704
+ // intercept runs inside the render phase already metered by render:total, so
705
+ // it contributes a span but no separate perf metric). Bare MiddlewareFns
706
+ // have no pattern, so the label is scoped to "*" like a pattern-less entry.
707
+ const label = getMiddlewareMetricLabel(
708
+ { handler: middleware, pattern: null } as MiddlewareEntry<TEnv>,
709
+ ordinal,
710
+ );
711
+
712
+ let result: Response | void;
713
+ try {
714
+ result = await observePhase(PHASES.middleware(label), () =>
715
+ middleware(ctx, guardedNext),
716
+ );
717
+ } catch (error) {
718
+ // Thrown Response is short-circuit control flow, parity with the
719
+ // explicit-return path below. Real errors propagate.
720
+ if (error instanceof Response) {
721
+ result = error;
722
+ } else {
723
+ throw error;
724
+ }
725
+ }
636
726
 
637
727
  if (result instanceof Response) {
638
728
  earlyResponse = result;
@@ -656,21 +746,13 @@ export async function executeInterceptMiddleware<TEnv>(
656
746
  });
657
747
 
658
748
  if (hasStubHeaders) {
659
- // Clone and merge headers from stub into early response.
660
- // Only fill in missing headers — the returned Response's explicit
661
- // headers take precedence, matching executeMiddleware behavior.
662
- const mergedHeaders = new Headers(response.headers);
663
- stubResponse.headers.forEach((value, name) => {
664
- if (name.toLowerCase() === "set-cookie") {
665
- mergedHeaders.append(name, value);
666
- } else if (!mergedHeaders.has(name)) {
667
- mergedHeaders.set(name, value);
668
- }
669
- });
670
- return new Response(response.body, {
671
- status: response.status,
672
- statusText: response.statusText,
673
- headers: mergedHeaders,
749
+ // Only fill in missing headers the returned Response's explicit headers
750
+ // take precedence (stubOverridesNonCookie=false), matching executeMiddleware.
751
+ // mergeReqCtx=false: the intercept path deliberately does NOT merge reqCtx
752
+ // stub headers here (pinned by intercept-middleware-headers.test.ts).
753
+ return mergeResponse(response, stubResponse.headers, {
754
+ stubOverridesNonCookie: false,
755
+ mergeReqCtx: false,
674
756
  });
675
757
  }
676
758
  return response;
@@ -711,7 +793,6 @@ export async function executeLoaderMiddleware<TEnv>(
711
793
  regex: null,
712
794
  paramNames: [],
713
795
  handler,
714
- mountPrefix: null,
715
796
  } as MiddlewareEntry<TEnv>,
716
797
  params,
717
798
  }));