@rangojs/router 0.0.0-experimental.b9cb8739 → 0.0.0-experimental.bdaf10aa

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 (449) hide show
  1. package/AGENTS.md +8 -4
  2. package/README.md +303 -741
  3. package/dist/bin/rango.js +730 -184
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +4344 -1335
  6. package/dist/vite/index.js.bak +5448 -0
  7. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  8. package/package.json +86 -15
  9. package/skills/api-client/SKILL.md +211 -0
  10. package/skills/breadcrumbs/SKILL.md +85 -6
  11. package/skills/bundle-analysis/SKILL.md +159 -0
  12. package/skills/cache-guide/SKILL.md +251 -24
  13. package/skills/caching/SKILL.md +375 -17
  14. package/skills/catalog.json +271 -0
  15. package/skills/comparison/SKILL.md +50 -0
  16. package/skills/comparison/agents/openai.yaml +4 -0
  17. package/skills/comparison/references/framework-comparison.md +837 -0
  18. package/skills/composability/SKILL.md +110 -4
  19. package/skills/css/SKILL.md +76 -0
  20. package/skills/debug-manifest/SKILL.md +5 -3
  21. package/skills/defer-hydration/SKILL.md +235 -0
  22. package/skills/document-cache/SKILL.md +87 -56
  23. package/skills/fonts/SKILL.md +1 -1
  24. package/skills/handler-use/SKILL.md +364 -0
  25. package/skills/hooks/SKILL.md +73 -691
  26. package/skills/hooks/data.md +273 -0
  27. package/skills/hooks/handle-and-actions.md +103 -0
  28. package/skills/hooks/navigation.md +110 -0
  29. package/skills/hooks/outlets.md +41 -0
  30. package/skills/hooks/state.md +228 -0
  31. package/skills/hooks/urls.md +135 -0
  32. package/skills/host-router/SKILL.md +129 -27
  33. package/skills/i18n/SKILL.md +276 -0
  34. package/skills/intercept/SKILL.md +94 -18
  35. package/skills/layout/SKILL.md +62 -19
  36. package/skills/links/SKILL.md +249 -17
  37. package/skills/loader/SKILL.md +302 -54
  38. package/skills/middleware/SKILL.md +59 -16
  39. package/skills/migrate-nextjs/SKILL.md +745 -0
  40. package/skills/migrate-react-router/SKILL.md +153 -0
  41. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  42. package/skills/migrate-react-router/component-migration.md +196 -0
  43. package/skills/migrate-react-router/data-and-actions.md +225 -0
  44. package/skills/migrate-react-router/route-mapping.md +271 -0
  45. package/skills/mime-routes/SKILL.md +29 -2
  46. package/skills/observability/SKILL.md +202 -0
  47. package/skills/parallel/SKILL.md +225 -10
  48. package/skills/ppr/SKILL.md +622 -0
  49. package/skills/prerender/SKILL.md +178 -124
  50. package/skills/rango/SKILL.md +318 -24
  51. package/skills/react-compiler/SKILL.md +168 -0
  52. package/skills/response-routes/SKILL.md +138 -49
  53. package/skills/route/SKILL.md +172 -9
  54. package/skills/router-setup/SKILL.md +131 -11
  55. package/skills/scripts/SKILL.md +179 -0
  56. package/skills/server-actions/SKILL.md +776 -0
  57. package/skills/shell-manifest/SKILL.md +185 -0
  58. package/skills/streams-and-websockets/SKILL.md +283 -0
  59. package/skills/tailwind/SKILL.md +28 -4
  60. package/skills/testing/SKILL.md +130 -0
  61. package/skills/testing/bindings.md +103 -0
  62. package/skills/testing/cache-prerender.md +127 -0
  63. package/skills/testing/client-components.md +124 -0
  64. package/skills/testing/e2e-parity.md +125 -0
  65. package/skills/testing/flight.md +91 -0
  66. package/skills/testing/handles.md +131 -0
  67. package/skills/testing/loader.md +128 -0
  68. package/skills/testing/middleware.md +99 -0
  69. package/skills/testing/render-handler.md +122 -0
  70. package/skills/testing/response-routes.md +95 -0
  71. package/skills/testing/reverse-and-types.md +85 -0
  72. package/skills/testing/server-actions.md +107 -0
  73. package/skills/testing/server-tree.md +128 -0
  74. package/skills/testing/setup.md +123 -0
  75. package/skills/theme/SKILL.md +1 -1
  76. package/skills/typesafety/SKILL.md +45 -616
  77. package/skills/typesafety/env-and-bindings.md +254 -0
  78. package/skills/typesafety/generated-files-and-cli.md +335 -0
  79. package/skills/typesafety/params-and-search.md +153 -0
  80. package/skills/typesafety/route-types.md +209 -0
  81. package/skills/use-cache/SKILL.md +74 -15
  82. package/skills/vercel/SKILL.md +128 -0
  83. package/skills/view-transitions/SKILL.md +337 -0
  84. package/src/__augment-tests__/augment.ts +81 -0
  85. package/src/__augment-tests__/augmented.check.ts +116 -0
  86. package/src/__internal.ts +1 -66
  87. package/src/browser/action-coordinator.ts +53 -36
  88. package/src/browser/action-fence.ts +47 -0
  89. package/src/browser/app-shell.ts +39 -0
  90. package/src/browser/app-version.ts +14 -0
  91. package/src/browser/connection-warmup.ts +134 -0
  92. package/src/browser/cookie-name.ts +140 -0
  93. package/src/browser/event-controller.ts +257 -158
  94. package/src/browser/history-state.ts +21 -0
  95. package/src/browser/index.ts +3 -3
  96. package/src/browser/invalidate-client-cache.ts +52 -0
  97. package/src/browser/logging.ts +28 -0
  98. package/src/browser/merge-segment-loaders.ts +6 -4
  99. package/src/browser/navigation-bridge.ts +132 -33
  100. package/src/browser/navigation-client.ts +218 -68
  101. package/src/browser/navigation-store-handle.ts +38 -0
  102. package/src/browser/navigation-store.ts +203 -80
  103. package/src/browser/navigation-transaction.ts +18 -66
  104. package/src/browser/network-error-handler.ts +34 -7
  105. package/src/browser/partial-update.ts +241 -127
  106. package/src/browser/prefetch/cache.ts +271 -44
  107. package/src/browser/prefetch/fetch.ts +367 -40
  108. package/src/browser/prefetch/queue.ts +144 -23
  109. package/src/browser/prefetch/resource-ready.ts +77 -0
  110. package/src/browser/rango-state.ts +158 -76
  111. package/src/browser/react/Link.tsx +121 -16
  112. package/src/browser/react/NavigationProvider.tsx +240 -122
  113. package/src/browser/react/ScrollRestoration.tsx +10 -6
  114. package/src/browser/react/context.ts +7 -2
  115. package/src/browser/react/filter-segment-order.ts +66 -7
  116. package/src/browser/react/index.ts +0 -48
  117. package/src/browser/react/location-state-shared.ts +178 -8
  118. package/src/browser/react/location-state.ts +39 -14
  119. package/src/browser/react/use-action.ts +6 -15
  120. package/src/browser/react/use-handle.ts +23 -69
  121. package/src/browser/react/use-href.tsx +8 -1
  122. package/src/browser/react/use-link-status.ts +33 -8
  123. package/src/browser/react/use-navigation.ts +32 -7
  124. package/src/browser/react/use-params.ts +20 -10
  125. package/src/browser/react/use-reverse.ts +106 -0
  126. package/src/browser/react/use-router.ts +46 -11
  127. package/src/browser/react/use-search-params.ts +0 -5
  128. package/src/browser/react/use-segments.ts +11 -21
  129. package/src/browser/response-adapter.ts +99 -8
  130. package/src/browser/rsc-router.tsx +272 -80
  131. package/src/browser/scroll-restoration.ts +56 -22
  132. package/src/browser/segment-reconciler.ts +44 -7
  133. package/src/browser/segment-structure-assert.ts +2 -2
  134. package/src/browser/server-action-bridge.ts +244 -71
  135. package/src/browser/types.ts +136 -12
  136. package/src/browser/validate-redirect-origin.ts +43 -16
  137. package/src/build/collect-fallback-refs.ts +107 -0
  138. package/src/build/generate-manifest.ts +207 -158
  139. package/src/build/generate-route-types.ts +6 -1
  140. package/src/build/index.ts +11 -3
  141. package/src/build/prefix-tree-utils.ts +123 -0
  142. package/src/build/route-trie.ts +198 -41
  143. package/src/build/route-types/ast-route-extraction.ts +15 -8
  144. package/src/build/route-types/codegen.ts +16 -5
  145. package/src/build/route-types/include-resolution.ts +464 -63
  146. package/src/build/route-types/param-extraction.ts +6 -3
  147. package/src/build/route-types/per-module-writer.ts +22 -6
  148. package/src/build/route-types/router-processing.ts +336 -110
  149. package/src/build/route-types/scan-filter.ts +9 -2
  150. package/src/build/route-types/source-scan.ts +216 -0
  151. package/src/build/runtime-discovery.ts +13 -21
  152. package/src/cache/cache-error.ts +104 -0
  153. package/src/cache/cache-key-utils.ts +58 -13
  154. package/src/cache/cache-policy.ts +108 -34
  155. package/src/cache/cache-runtime.ts +454 -97
  156. package/src/cache/cache-scope.ts +235 -103
  157. package/src/cache/cache-tag.ts +149 -0
  158. package/src/cache/cf/cf-base64.ts +33 -0
  159. package/src/cache/cf/cf-cache-constants.ts +127 -0
  160. package/src/cache/cf/cf-cache-store.ts +2446 -170
  161. package/src/cache/cf/cf-cache-types.ts +349 -0
  162. package/src/cache/cf/cf-kv-utils.ts +46 -0
  163. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  164. package/src/cache/cf/index.ts +11 -17
  165. package/src/cache/document-cache.ts +144 -49
  166. package/src/cache/handle-snapshot.ts +70 -0
  167. package/src/cache/index.ts +24 -20
  168. package/src/cache/memory-segment-store.ts +243 -37
  169. package/src/cache/profile-registry.ts +46 -31
  170. package/src/cache/read-through-swr.ts +56 -12
  171. package/src/cache/segment-codec.ts +13 -21
  172. package/src/cache/shell-snapshot.ts +417 -0
  173. package/src/cache/tag-invalidation.ts +230 -0
  174. package/src/cache/taint.ts +55 -0
  175. package/src/cache/types.ts +194 -99
  176. package/src/cache/vercel/index.ts +11 -0
  177. package/src/cache/vercel/vercel-cache-store.ts +1132 -0
  178. package/src/client.rsc.tsx +41 -21
  179. package/src/client.tsx +116 -290
  180. package/src/cloudflare/index.ts +11 -0
  181. package/src/cloudflare/tracing.ts +108 -0
  182. package/src/component-utils.ts +19 -0
  183. package/src/components/DefaultDocument.tsx +8 -2
  184. package/src/context-var.ts +84 -2
  185. package/src/debug.ts +2 -2
  186. package/src/decode-loader-results.ts +52 -0
  187. package/src/defer.ts +185 -0
  188. package/src/deps/ssr.ts +0 -1
  189. package/src/encode-kv.ts +49 -0
  190. package/src/errors.ts +30 -4
  191. package/src/escape-script.ts +52 -0
  192. package/src/handle.ts +104 -34
  193. package/src/handles/MetaTags.tsx +24 -53
  194. package/src/handles/Scripts.tsx +183 -0
  195. package/src/handles/breadcrumbs.ts +35 -8
  196. package/src/handles/deferred-resolution.ts +127 -0
  197. package/src/handles/is-thenable.ts +18 -0
  198. package/src/handles/meta.ts +14 -40
  199. package/src/handles/script.ts +244 -0
  200. package/src/host/cookie-handler.ts +9 -60
  201. package/src/host/errors.ts +13 -22
  202. package/src/host/index.ts +9 -2
  203. package/src/host/pattern-matcher.ts +23 -52
  204. package/src/host/router.ts +107 -99
  205. package/src/host/testing.ts +40 -27
  206. package/src/host/types.ts +37 -4
  207. package/src/host/utils.ts +1 -1
  208. package/src/href-client.ts +137 -22
  209. package/src/index.rsc.ts +100 -13
  210. package/src/index.ts +143 -19
  211. package/src/internal-debug.ts +11 -10
  212. package/src/loader-store.ts +500 -0
  213. package/src/loader.rsc.ts +20 -13
  214. package/src/loader.ts +12 -11
  215. package/src/missing-id-error.ts +68 -0
  216. package/src/outlet-context.ts +1 -1
  217. package/src/outlet-provider.tsx +1 -5
  218. package/src/prerender/param-hash.ts +16 -16
  219. package/src/prerender/store.ts +37 -41
  220. package/src/prerender.ts +215 -86
  221. package/src/redirect-origin.ts +114 -0
  222. package/src/regex-escape.ts +8 -0
  223. package/src/render-error-thrower.tsx +20 -0
  224. package/src/response-utils.ts +62 -0
  225. package/src/reverse.ts +65 -15
  226. package/src/root-error-boundary.tsx +1 -19
  227. package/src/route-content-wrapper.tsx +19 -77
  228. package/src/route-definition/dsl-helpers.ts +485 -303
  229. package/src/route-definition/helper-factories.ts +28 -140
  230. package/src/route-definition/helpers-types.ts +153 -77
  231. package/src/route-definition/index.ts +4 -2
  232. package/src/route-definition/redirect.ts +53 -12
  233. package/src/route-definition/resolve-handler-use.ts +160 -0
  234. package/src/route-definition/use-item-types.ts +29 -0
  235. package/src/route-map-builder.ts +48 -21
  236. package/src/route-types.ts +37 -46
  237. package/src/router/basename.ts +14 -0
  238. package/src/router/content-negotiation.ts +164 -17
  239. package/src/router/error-handling.ts +45 -18
  240. package/src/router/find-match.ts +130 -29
  241. package/src/router/handler-context.ts +83 -39
  242. package/src/router/instrument.ts +355 -0
  243. package/src/router/intercept-resolution.ts +50 -24
  244. package/src/router/lazy-includes.ts +89 -63
  245. package/src/router/loader-resolution.ts +286 -56
  246. package/src/router/logging.ts +5 -8
  247. package/src/router/manifest.ts +105 -56
  248. package/src/router/match-api.ts +178 -218
  249. package/src/router/match-context.ts +0 -22
  250. package/src/router/match-handlers.ts +211 -165
  251. package/src/router/match-middleware/background-revalidation.ts +66 -22
  252. package/src/router/match-middleware/cache-lookup.ts +214 -263
  253. package/src/router/match-middleware/cache-store.ts +105 -50
  254. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  255. package/src/router/match-middleware/segment-resolution.ts +52 -18
  256. package/src/router/match-pipelines.ts +1 -42
  257. package/src/router/match-result.ts +128 -44
  258. package/src/router/metrics.ts +5 -34
  259. package/src/router/middleware-types.ts +13 -142
  260. package/src/router/middleware.ts +301 -177
  261. package/src/router/navigation-snapshot.ts +133 -0
  262. package/src/router/params-util.ts +23 -0
  263. package/src/router/parse-pattern.ts +115 -0
  264. package/src/router/pattern-matching.ts +181 -150
  265. package/src/router/prefetch-cache-ttl.ts +51 -0
  266. package/src/router/prefetch-limits.ts +37 -0
  267. package/src/router/prerender-match.ts +203 -58
  268. package/src/router/preview-match.ts +35 -103
  269. package/src/router/request-classification.ts +291 -0
  270. package/src/router/revalidation.ts +123 -73
  271. package/src/router/route-snapshot.ts +256 -0
  272. package/src/router/router-context.ts +11 -29
  273. package/src/router/router-interfaces.ts +146 -35
  274. package/src/router/router-options.ts +202 -15
  275. package/src/router/router-registry.ts +2 -5
  276. package/src/router/segment-resolution/fresh.ts +301 -78
  277. package/src/router/segment-resolution/helpers.ts +115 -30
  278. package/src/router/segment-resolution/loader-cache.ts +156 -39
  279. package/src/router/segment-resolution/loader-mask.ts +60 -0
  280. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  281. package/src/router/segment-resolution/mask-nested.ts +83 -0
  282. package/src/router/segment-resolution/revalidation.ts +477 -385
  283. package/src/router/segment-resolution/static-store.ts +19 -5
  284. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  285. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  286. package/src/router/segment-resolution.ts +5 -1
  287. package/src/router/segment-wrappers.ts +8 -5
  288. package/src/router/state-cookie-name.ts +33 -0
  289. package/src/router/substitute-pattern-params.ts +75 -0
  290. package/src/router/telemetry-otel.ts +160 -200
  291. package/src/router/telemetry.ts +105 -20
  292. package/src/router/timeout.ts +0 -20
  293. package/src/router/tracing.ts +215 -0
  294. package/src/router/trie-matching.ts +171 -59
  295. package/src/router/types.ts +10 -63
  296. package/src/router/url-params.ts +57 -0
  297. package/src/router.ts +210 -71
  298. package/src/rsc/full-payload.ts +70 -0
  299. package/src/rsc/handler-context.ts +3 -2
  300. package/src/rsc/handler.ts +682 -508
  301. package/src/rsc/helpers.ts +168 -46
  302. package/src/rsc/index.ts +2 -5
  303. package/src/rsc/json-route-result.ts +38 -0
  304. package/src/rsc/loader-fetch.ts +127 -31
  305. package/src/rsc/manifest-init.ts +33 -42
  306. package/src/rsc/nonce.ts +10 -1
  307. package/src/rsc/origin-guard.ts +39 -25
  308. package/src/rsc/progressive-enhancement.ts +138 -15
  309. package/src/rsc/redirect-guard.ts +100 -0
  310. package/src/rsc/response-cache-serve.ts +238 -0
  311. package/src/rsc/response-error.ts +79 -12
  312. package/src/rsc/response-route-handler.ts +99 -189
  313. package/src/rsc/rsc-rendering.ts +509 -73
  314. package/src/rsc/runtime-warnings.ts +23 -10
  315. package/src/rsc/server-action.ts +287 -113
  316. package/src/rsc/shell-capture.ts +1190 -0
  317. package/src/rsc/shell-serve.ts +181 -0
  318. package/src/rsc/ssr-setup.ts +18 -2
  319. package/src/rsc/transition-gate.ts +89 -0
  320. package/src/rsc/types.ts +62 -6
  321. package/src/runtime-env.ts +18 -0
  322. package/src/search-params.ts +35 -30
  323. package/src/segment-content-promise.ts +67 -0
  324. package/src/segment-loader-promise.ts +167 -0
  325. package/src/segment-system.tsx +449 -132
  326. package/src/serialize.ts +243 -0
  327. package/src/server/context.ts +367 -61
  328. package/src/server/cookie-parse.ts +32 -0
  329. package/src/server/cookie-store.ts +152 -5
  330. package/src/server/handle-store.ts +40 -38
  331. package/src/server/loader-registry.ts +38 -46
  332. package/src/server/request-context.ts +558 -173
  333. package/src/ssr/index.tsx +491 -174
  334. package/src/ssr/inject-rsc-eager.ts +167 -0
  335. package/src/ssr/ssr-root.tsx +228 -0
  336. package/src/static-handler.ts +27 -18
  337. package/src/testing/cache-status.ts +162 -0
  338. package/src/testing/collect-handle.ts +46 -0
  339. package/src/testing/dispatch.ts +813 -0
  340. package/src/testing/dom.entry.ts +22 -0
  341. package/src/testing/e2e/fixture.ts +188 -0
  342. package/src/testing/e2e/index.ts +128 -0
  343. package/src/testing/e2e/matchers.ts +35 -0
  344. package/src/testing/e2e/page-helpers.ts +272 -0
  345. package/src/testing/e2e/parity.ts +387 -0
  346. package/src/testing/e2e/server.ts +195 -0
  347. package/src/testing/flight-matchers.ts +97 -0
  348. package/src/testing/flight-normalize.ts +11 -0
  349. package/src/testing/flight-runtime.d.ts +57 -0
  350. package/src/testing/flight-tree.ts +682 -0
  351. package/src/testing/flight.entry.ts +52 -0
  352. package/src/testing/flight.ts +257 -0
  353. package/src/testing/generated-routes.ts +199 -0
  354. package/src/testing/index.ts +105 -0
  355. package/src/testing/internal/context.ts +371 -0
  356. package/src/testing/internal/flight-client-globals.ts +30 -0
  357. package/src/testing/internal/seed-vars.ts +54 -0
  358. package/src/testing/render-handler.ts +357 -0
  359. package/src/testing/render-route.tsx +584 -0
  360. package/src/testing/run-loader.ts +385 -0
  361. package/src/testing/run-middleware.ts +205 -0
  362. package/src/testing/run-transition-when.ts +164 -0
  363. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  364. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  365. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  366. package/src/testing/vitest-stubs/version.ts +5 -0
  367. package/src/testing/vitest.ts +305 -0
  368. package/src/theme/ThemeProvider.tsx +56 -84
  369. package/src/theme/ThemeScript.tsx +7 -9
  370. package/src/theme/constants.ts +52 -13
  371. package/src/theme/index.ts +0 -7
  372. package/src/theme/theme-context.ts +1 -5
  373. package/src/theme/theme-script.ts +22 -21
  374. package/src/theme/use-theme.ts +0 -3
  375. package/src/types/boundaries.ts +0 -35
  376. package/src/types/cache-types.ts +17 -8
  377. package/src/types/error-types.ts +30 -90
  378. package/src/types/global-namespace.ts +54 -41
  379. package/src/types/handler-context.ts +234 -82
  380. package/src/types/index.ts +3 -10
  381. package/src/types/loader-types.ts +44 -15
  382. package/src/types/request-scope.ts +112 -0
  383. package/src/types/route-config.ts +20 -52
  384. package/src/types/route-entry.ts +19 -7
  385. package/src/types/segments.ts +137 -14
  386. package/src/urls/include-helper.ts +40 -75
  387. package/src/urls/include-provider.ts +71 -0
  388. package/src/urls/index.ts +2 -11
  389. package/src/urls/path-helper-types.ts +102 -23
  390. package/src/urls/path-helper.ts +62 -111
  391. package/src/urls/pattern-types.ts +84 -19
  392. package/src/urls/response-types.ts +25 -22
  393. package/src/urls/type-extraction.ts +98 -154
  394. package/src/urls/urls-function.ts +1 -19
  395. package/src/use-loader.tsx +346 -89
  396. package/src/vercel/index.ts +11 -0
  397. package/src/vercel/tracing.ts +88 -0
  398. package/src/vite/debug.ts +185 -0
  399. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  400. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  401. package/src/vite/discovery/discover-routers.ts +130 -85
  402. package/src/vite/discovery/discovery-errors.ts +255 -0
  403. package/src/vite/discovery/gate-state.ts +171 -0
  404. package/src/vite/discovery/prerender-collection.ts +214 -132
  405. package/src/vite/discovery/route-types-writer.ts +40 -84
  406. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  407. package/src/vite/discovery/state.ts +57 -6
  408. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  409. package/src/vite/index.ts +15 -0
  410. package/src/vite/inject-client-debug.ts +88 -0
  411. package/src/vite/plugin-types.ts +234 -62
  412. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  413. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  414. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  415. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  416. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  417. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  418. package/src/vite/plugins/expose-action-id.ts +49 -98
  419. package/src/vite/plugins/expose-id-utils.ts +96 -51
  420. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  421. package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
  422. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  423. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  424. package/src/vite/plugins/expose-internal-ids.ts +553 -317
  425. package/src/vite/plugins/performance-tracks.ts +89 -0
  426. package/src/vite/plugins/refresh-cmd.ts +89 -27
  427. package/src/vite/plugins/use-cache-transform.ts +73 -83
  428. package/src/vite/plugins/vercel-output.ts +384 -0
  429. package/src/vite/plugins/version-injector.ts +40 -29
  430. package/src/vite/plugins/version-plugin.ts +46 -37
  431. package/src/vite/plugins/virtual-entries.ts +138 -27
  432. package/src/vite/rango.ts +353 -303
  433. package/src/vite/router-discovery.ts +1090 -166
  434. package/src/vite/utils/ast-handler-extract.ts +26 -35
  435. package/src/vite/utils/banner.ts +4 -4
  436. package/src/vite/utils/bundle-analysis.ts +10 -15
  437. package/src/vite/utils/client-chunks.ts +184 -0
  438. package/src/vite/utils/directive-prologue.ts +40 -0
  439. package/src/vite/utils/forward-user-plugins.ts +171 -0
  440. package/src/vite/utils/manifest-utils.ts +4 -59
  441. package/src/vite/utils/package-resolution.ts +20 -52
  442. package/src/vite/utils/prerender-utils.ts +98 -38
  443. package/src/vite/utils/shared-utils.ts +144 -44
  444. package/src/browser/action-response-classifier.ts +0 -99
  445. package/src/browser/react/use-client-cache.ts +0 -58
  446. package/src/browser/shallow.ts +0 -40
  447. package/src/handles/index.ts +0 -7
  448. package/src/network-error-thrower.tsx +0 -23
  449. package/src/router/middleware-cookies.ts +0 -55
@@ -1,15 +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";
6
+ import { safeDecodeURIComponent } from "./url-params.js";
7
+ import { fireAndForgetWaitUntil } from "../types/request-scope.js";
13
8
  import type {
14
9
  CollectedMiddleware,
15
10
  MiddlewareCollectableEntry,
@@ -19,26 +14,29 @@ import type {
19
14
  ResponseHolder,
20
15
  } from "./middleware-types.js";
21
16
  import { _getRequestContext } from "../server/request-context.js";
17
+ import {
18
+ EXTERNAL_REDIRECT_MARKER,
19
+ isExternalRedirect,
20
+ markExternalRedirect,
21
+ } from "../redirect-origin.js";
22
22
  import { isAutoGeneratedRouteName } from "../route-name.js";
23
23
  import { appendMetric, createMetricsStore } from "./metrics.js";
24
+ import { observePhase, PHASES } from "./instrument.js";
25
+ import { stripInternalParams } from "./handler-context.js";
26
+ import { isWebSocketUpgradeResponse } from "../response-utils.js";
24
27
 
25
- // Re-export types and cookie utilities for backward compatibility
28
+ // Re-export types consumed through this module's path.
26
29
  export type {
27
30
  CookieOptions,
28
- CollectedMiddleware,
29
- MiddlewareCollectableEntry,
30
31
  MiddlewareContext,
31
32
  MiddlewareEntry,
32
33
  MiddlewareFn,
33
- ResponseHolder,
34
34
  } from "./middleware-types.js";
35
- export { parseCookies, serializeCookie } from "./middleware-cookies.js";
36
35
 
37
36
  const MIDDLEWARE_METRIC_DEPTH = 1;
38
- /** Ignore post-next() durations below this threshold (measurement noise). */
39
37
  const POST_METRIC_MIN_DURATION_MS = 0.01;
40
38
 
41
- function getMiddlewareMetricBase<TEnv>(
39
+ function getMiddlewareMetricLabel<TEnv>(
42
40
  entry: MiddlewareEntry<TEnv>,
43
41
  ordinal: number,
44
42
  ): string {
@@ -46,24 +44,49 @@ function getMiddlewareMetricBase<TEnv>(
46
44
  const scope = entry.pattern ?? "*";
47
45
 
48
46
  if (handlerName) {
49
- return `${handlerName}@${scope}`;
47
+ return `middleware:${handlerName}@${scope}`;
50
48
  }
51
49
 
52
- return `${scope}#${ordinal + 1}`;
53
- }
54
-
55
- function getMiddlewareMetricLabel<TEnv>(
56
- entry: MiddlewareEntry<TEnv>,
57
- ordinal: number,
58
- ): string {
59
- return `middleware:${getMiddlewareMetricBase(entry, ordinal)}`;
50
+ return `middleware:${scope}#${ordinal + 1}`;
60
51
  }
61
52
 
62
53
  /**
63
- * Parse a route pattern into regex and param names
64
- * 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).
65
88
  */
66
- export function parsePattern(pattern: string): {
89
+ export function compileMiddlewarePattern(pattern: string): {
67
90
  regex: RegExp;
68
91
  paramNames: string[];
69
92
  } {
@@ -71,47 +94,63 @@ export function parsePattern(pattern: string): {
71
94
  return { regex: /^.*$/, paramNames: [] };
72
95
  }
73
96
 
97
+ const normalizedPattern = pattern.startsWith("/") ? pattern : `/${pattern}`;
98
+ const segments = parseRoutePattern(normalizedPattern);
74
99
  const paramNames: string[] = [];
75
100
  let regexStr = "^";
76
-
77
- const parts = pattern.split("/").filter(Boolean);
78
-
79
- for (let i = 0; i < parts.length; i++) {
80
- const part = parts[i];
81
-
82
- if (part === "*") {
83
- // Wildcard - match rest of path
84
- regexStr += "(?:/.*)?";
85
- } else if (part.startsWith(":")) {
86
- // Param
87
- const paramName = part.slice(1);
88
- paramNames.push(paramName);
89
- regexStr += "/([^/]+)";
101
+ let hasTrailingWildcard = false;
102
+
103
+ for (let i = 0; i < segments.length; i++) {
104
+ const segment = segments[i];
105
+
106
+ if (segment.type === "wildcard") {
107
+ if (segment.value === "*") {
108
+ // Bare `*`: optional subtree match, no capture (parity with the original
109
+ // middleware parser). A trailing `*` matches the subtree; a non-trailing
110
+ // `*` matches zero-or-more intermediate segments, so `/a/<star>/b` still
111
+ // matches `/a/b`.
112
+ regexStr += "(?:/.*)?";
113
+ } else {
114
+ // Named catch-all `:name+` / `:name*`: capture the remainder under the
115
+ // name so a scoping middleware sees `ctx.params.<name>`, and respect the
116
+ // one-or-more arity (`+` must not match the bare prefix), mirroring the
117
+ // route matcher instead of collapsing to the bare-`*` subtree.
118
+ paramNames.push(segment.value);
119
+ regexStr += segment.oneOrMore ? "/(.+)" : "(?:/(.*))?";
120
+ }
121
+ if (i === segments.length - 1) {
122
+ hasTrailingWildcard = true;
123
+ }
124
+ } else if (segment.type === "param") {
125
+ paramNames.push(segment.value);
126
+ const suffixPattern = segment.suffix ? escapeRegExp(segment.suffix) : "";
127
+ const valuePattern = segment.constraint
128
+ ? `(${segment.constraint.map(escapeRegExp).join("|")})`
129
+ : "([^/]+)";
130
+ if (segment.optional) {
131
+ regexStr += `(?:/${valuePattern}${suffixPattern})?`;
132
+ } else {
133
+ regexStr += `/${valuePattern}${suffixPattern}`;
134
+ }
90
135
  } else {
91
- // Literal
92
- regexStr += "/" + escapeRegex(part);
136
+ // Static literal
137
+ regexStr += "/" + escapeRegExp(segment.value);
93
138
  }
94
139
  }
95
140
 
96
- // If pattern doesn't end with *, match exact or with trailing segments
97
- if (!pattern.endsWith("*")) {
98
- regexStr += "/?$";
99
- } else {
100
- regexStr += "$";
101
- }
141
+ // Without a trailing `*`, match exactly with an optional trailing slash.
142
+ regexStr += hasTrailingWildcard ? "$" : "/?$";
102
143
 
103
144
  return { regex: new RegExp(regexStr), paramNames };
104
145
  }
105
146
 
106
147
  /**
107
- * Escape special regex characters
108
- */
109
- function escapeRegex(str: string): string {
110
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
111
- }
112
-
113
- /**
114
- * Extract params from a pathname using a pattern's regex and param names
148
+ * Extract params from a pathname using a pattern's regex and param names.
149
+ *
150
+ * Values are URL-decoded so apps see the raw string (e.g. "ivo@example.com")
151
+ * instead of the percent-encoded form ("ivo%40example.com"). This matches the
152
+ * contract assumed by ctx.reverse (which re-encodes) and aligns with
153
+ * Express/React Router/Fastify/Koa.
115
154
  */
116
155
  export function extractParams(
117
156
  pathname: string,
@@ -123,7 +162,7 @@ export function extractParams(
123
162
 
124
163
  const params: Record<string, string> = {};
125
164
  for (let i = 0; i < paramNames.length; i++) {
126
- params[paramNames[i]] = match[i + 1] || "";
165
+ params[paramNames[i]] = safeDecodeURIComponent(match[i + 1] || "");
127
166
  }
128
167
  return params;
129
168
  }
@@ -147,7 +186,15 @@ export function createMiddlewareContext<TEnv>(
147
186
  search?: Record<string, unknown>,
148
187
  ) => string,
149
188
  ): MiddlewareContext<TEnv> {
150
- const url = new URL(request.url);
189
+ // Reuse the request context's clean URL when available. Each middleware still
190
+ // gets its OWN URL clone (a middleware may mutate ctx.url / ctx.searchParams),
191
+ // but cloning the already-clean URL is cheaper than re-parsing + re-stripping
192
+ // request.url per middleware. Fall back to parsing when no ALS context.
193
+ const reqCtx = _getRequestContext();
194
+ const ctxUrl = reqCtx?.url;
195
+ const url = ctxUrl
196
+ ? new URL(ctxUrl)
197
+ : stripInternalParams(new URL(request.url));
151
198
 
152
199
  // Track the initial response to detect pre/post-next() phase.
153
200
  // Before next(): responseHolder.response === initialResponse (the stub).
@@ -178,14 +225,21 @@ export function createMiddlewareContext<TEnv>(
178
225
  return responseHolder.response;
179
226
  };
180
227
 
228
+ // reqCtx captured once above: the request-scoped platform fields
229
+ // (originalUrl, executionContext, waitUntil) are immutable per request,
230
+ // so snapshotting beats re-reading ALS on every access. The lazy getters
231
+ // below (routeName, theme, setTheme) stay lazy because those can change
232
+ // during `await next()`.
181
233
  return {
182
234
  request,
183
235
  url,
184
- originalUrl: new URL(request.url),
236
+ originalUrl: reqCtx?.originalUrl ?? new URL(request.url),
185
237
  pathname: url.pathname,
186
238
  searchParams: url.searchParams,
187
239
  env: env as MiddlewareContext<TEnv>["env"],
188
240
  params,
241
+ executionContext: reqCtx?.executionContext,
242
+ waitUntil: reqCtx ? reqCtx.waitUntil.bind(reqCtx) : fireAndForgetWaitUntil,
189
243
  // Getter: re-derives from request context on each access so that global
190
244
  // middleware sees the matched route name after await next().
191
245
  get routeName(): MiddlewareContext<TEnv>["routeName"] {
@@ -203,12 +257,9 @@ export function createMiddlewareContext<TEnv>(
203
257
  get: ((keyOrVar: any) =>
204
258
  contextGet(variables, keyOrVar)) as MiddlewareContext<TEnv>["get"],
205
259
 
206
- set: ((keyOrVar: any, value: unknown) => {
207
- contextSet(variables, keyOrVar, value);
260
+ set: ((keyOrVar: any, value: unknown, options?: any) => {
261
+ contextSet(variables, keyOrVar, value, options);
208
262
  }) as MiddlewareContext<TEnv>["set"],
209
-
210
- var: variables as MiddlewareContext<TEnv>["var"],
211
-
212
263
  header(name: string, value: string): void {
213
264
  // Before next(): delegate to shared RequestContext stub
214
265
  if (isPreNext()) {
@@ -247,9 +298,13 @@ export function createMiddlewareContext<TEnv>(
247
298
 
248
299
  reverse:
249
300
  reverse ??
250
- ((name: string) => {
301
+ ((
302
+ name: string,
303
+ _params?: Record<string, string>,
304
+ _search?: Record<string, unknown>,
305
+ ) => {
251
306
  throw new Error(
252
- `ctx.reverse() is not available - route map was not provided to middleware context`,
307
+ `ctx.reverse(${JSON.stringify(name)}) is not available: no route map is bound to this middleware context.`,
253
308
  );
254
309
  }),
255
310
 
@@ -257,7 +312,10 @@ export function createMiddlewareContext<TEnv>(
257
312
  const reqCtx = _getRequestContext();
258
313
  if (reqCtx) {
259
314
  reqCtx._debugPerformance = true;
260
- reqCtx._metricsStore ??= createMetricsStore(true);
315
+ // Anchor to the true request entry (reqCtx._handlerStart) so phases that
316
+ // began before this opt-in report non-negative offsets; undefined falls
317
+ // back to performance.now() in createMetricsStore.
318
+ reqCtx._metricsStore ??= createMetricsStore(true, reqCtx._handlerStart);
261
319
  }
262
320
  },
263
321
  };
@@ -283,9 +341,16 @@ export function matchMiddleware<TEnv>(
283
341
  continue;
284
342
  }
285
343
 
286
- // Check if pathname matches
287
- if (entry.regex.test(pathname)) {
288
- const params = extractParams(pathname, entry.regex, entry.paramNames);
344
+ // Run the scope regex ONCE per entry. The old code ran test() then, on a
345
+ // hit, extractParams' match() — a second full pass over the same string for
346
+ // every matching entry on the per-request hot path. The regexes carry no
347
+ // `g` flag, so there is no lastIndex statefulness across this single match.
348
+ const m = pathname.match(entry.regex);
349
+ if (m) {
350
+ const params: Record<string, string> = {};
351
+ for (let i = 0; i < entry.paramNames.length; i++) {
352
+ params[entry.paramNames[i]] = safeDecodeURIComponent(m[i + 1] || "");
353
+ }
289
354
  matches.push({ entry, params });
290
355
  }
291
356
  }
@@ -293,6 +358,88 @@ export function matchMiddleware<TEnv>(
293
358
  return matches;
294
359
  }
295
360
 
361
+ // Set-Cookie is appended; for other headers stubOverridesNonCookie=true
362
+ // overwrites (chain ran to completion), false fills only missing slots (an
363
+ // explicit short-circuit Response's own headers win).
364
+ function mergeStubHeaders(
365
+ target: Headers,
366
+ stub: Headers,
367
+ stubOverridesNonCookie: boolean,
368
+ ): void {
369
+ stub.forEach((value, name) => {
370
+ // The reserved external-redirect marker is internal and never a trust
371
+ // signal; never copy a stub value (e.g. a stray ctx.header() call) onto a
372
+ // browser-facing response. The opt-in is the out-of-band brand.
373
+ if (name.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
374
+ if (name.toLowerCase() === "set-cookie") {
375
+ target.append(name, value);
376
+ } else if (stubOverridesNonCookie || !target.has(name)) {
377
+ target.set(name, value);
378
+ }
379
+ });
380
+ }
381
+
382
+ // Set-Cookie is deduped so a nested inner executeMiddleware that already merged
383
+ // the same reqCtx cookies does not duplicate them; other headers fill if missing.
384
+ function mergeReqCtxStub(
385
+ target: Headers,
386
+ reqCtx: ReturnType<typeof _getRequestContext>,
387
+ ): void {
388
+ if (!reqCtx) return;
389
+ const stubCookies = reqCtx.res.headers.getSetCookie();
390
+ if (stubCookies.length > 0) {
391
+ const existing = new Set(target.getSetCookie());
392
+ for (const cookie of stubCookies) {
393
+ if (!existing.has(cookie)) {
394
+ target.append("set-cookie", cookie);
395
+ }
396
+ }
397
+ }
398
+ reqCtx.res.headers.forEach((value, name) => {
399
+ // Never propagate the reserved external-redirect marker (see mergeStubHeaders).
400
+ if (name.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
401
+ if (name !== "set-cookie" && !target.has(name)) {
402
+ target.set(name, value);
403
+ }
404
+ });
405
+ }
406
+
407
+ // Clone `base` with stub headers merged into a fresh Headers (the clone keeps
408
+ // the body mutable for post-next() modifications). Set-Cookie is always
409
+ // appended; other headers obey stubOverridesNonCookie (see mergeStubHeaders).
410
+ // mergeReqCtx folds in RequestContext stub cookies/headers; the intercept
411
+ // short-circuit path passes false (its reqCtx headers are not merged here),
412
+ // which is the one deliberate divergence between the call sites.
413
+ function mergeResponse(
414
+ base: Response,
415
+ stub: Headers,
416
+ opts: { stubOverridesNonCookie: boolean; mergeReqCtx: boolean },
417
+ ): Response {
418
+ const mergedHeaders = new Headers(base.headers);
419
+ // The reserved external-redirect marker is never a trust signal and must never
420
+ // reach the browser. The guard strips it on 3xx redirects; strip it here too so
421
+ // a forged value cannot ride a non-3xx middleware response (which the 3xx-only
422
+ // guard would not touch) to the client. The opt-in is the out-of-band brand.
423
+ mergedHeaders.delete(EXTERNAL_REDIRECT_MARKER);
424
+ mergeStubHeaders(mergedHeaders, stub, opts.stubOverridesNonCookie);
425
+ if (opts.mergeReqCtx) {
426
+ mergeReqCtxStub(mergedHeaders, _getRequestContext());
427
+ }
428
+ const merged = new Response(base.body, {
429
+ status: base.status,
430
+ statusText: base.statusText,
431
+ headers: mergedHeaders,
432
+ });
433
+ // Transfer the out-of-band external-redirect brand across this rebuild: a
434
+ // middleware short-circuit `return redirect(url, { external: true })` reaches
435
+ // the open-redirect guard only after this merge, and the brand lives on the
436
+ // Response object, not in its headers.
437
+ if (isExternalRedirect(base)) {
438
+ markExternalRedirect(merged);
439
+ }
440
+ return merged;
441
+ }
442
+
296
443
  /**
297
444
  * Execute middleware chain
298
445
  *
@@ -301,7 +448,7 @@ export function matchMiddleware<TEnv>(
301
448
  * - `ctx.headers` available before and after `await next()`
302
449
  * - `ctx.header()` shorthand for setting a single header
303
450
  * - Forgiving: if middleware doesn't return, uses the downstream response
304
- * - Short-circuit: return Response to stop chain
451
+ * - Short-circuit: return OR throw a Response to stop chain
305
452
  * - Error catching: try/catch around `next()` works
306
453
  */
307
454
  export async function executeMiddleware<TEnv>(
@@ -331,42 +478,16 @@ export async function executeMiddleware<TEnv>(
331
478
  // End of chain - call actual RSC handler
332
479
  const response = await finalHandler();
333
480
 
334
- // Merge headers set on stub into the real response.
335
- // Use append for Set-Cookie to preserve multiple cookies.
336
- const mergedHeaders = new Headers(response.headers);
337
- stubResponse.headers.forEach((value, name) => {
338
- if (name.toLowerCase() === "set-cookie") {
339
- mergedHeaders.append(name, value);
340
- } else {
341
- mergedHeaders.set(name, value);
342
- }
343
- });
344
- // Also merge shared RequestContext stub (cookies written via cookies().set()).
345
- // Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
346
- // may have already merged the same reqCtx cookies into the response.
347
- const reqCtx = _getRequestContext();
348
- if (reqCtx) {
349
- const stubCookies = reqCtx.res.headers.getSetCookie();
350
- if (stubCookies.length > 0) {
351
- const existing = new Set(mergedHeaders.getSetCookie());
352
- for (const cookie of stubCookies) {
353
- if (!existing.has(cookie)) {
354
- mergedHeaders.append("set-cookie", cookie);
355
- }
356
- }
357
- }
358
- reqCtx.res.headers.forEach((value, name) => {
359
- if (name !== "set-cookie" && !mergedHeaders.has(name)) {
360
- mergedHeaders.set(name, value);
361
- }
362
- });
481
+ if (isWebSocketUpgradeResponse(response)) {
482
+ responseHolder.response = response;
483
+ return response;
363
484
  }
364
485
 
365
- // Clone response with merged headers (mutable for post-next() modifications)
366
- responseHolder.response = new Response(response.body, {
367
- status: response.status,
368
- statusText: response.statusText,
369
- headers: mergedHeaders,
486
+ // Chain ran to completion: stub headers overwrite (stubOverridesNonCookie)
487
+ // and reqCtx stub headers are merged in.
488
+ responseHolder.response = mergeResponse(response, stubResponse.headers, {
489
+ stubOverridesNonCookie: true,
490
+ mergeReqCtx: true,
370
491
  });
371
492
 
372
493
  return responseHolder.response;
@@ -424,14 +545,36 @@ export async function executeMiddleware<TEnv>(
424
545
  return nextPromise;
425
546
  };
426
547
 
548
+ // Wrap the middleware (including its downstream next() chain) in its span
549
+ // via the unified phase API. metric:false — the middleware's perf metric is
550
+ // its exclusive pre/post own-time, recorded directly above and below, finer
551
+ // than a single wrap. Spans nest by async context, so this onions
552
+ // middleware-over-middleware and the core handler underneath. Pass-through
553
+ // when neither surface is active.
427
554
  let result: Response | void;
428
555
  try {
429
- result = await entry.handler(ctx, wrappedNext);
430
- } catch (error) {
556
+ result = await observePhase(PHASES.middleware(metricLabel), async () => {
557
+ try {
558
+ return await entry.handler(ctx, wrappedNext);
559
+ } catch (error) {
560
+ // Thrown Response is short-circuit control flow, not an error —
561
+ // absorb it INSIDE the span so the tracing runner settles the
562
+ // rango.middleware span as success, not STATUS_ERROR (every auth
563
+ // redirect would otherwise inflate trace error rates). Returning it
564
+ // routes through the `if (result instanceof Response)` branch below,
565
+ // so stub headers and request-context cookies merge identically to an
566
+ // explicit `return new Response(...)`. Segment handlers already follow
567
+ // this convention (segment-resolution/helpers.ts keeps result handling
568
+ // outside the span). Real errors propagate past the span.
569
+ if (error instanceof Response) return error;
570
+ throw error;
571
+ }
572
+ });
573
+ } finally {
574
+ // Settle the middleware own-time metric once on both the success and
575
+ // error paths (idempotent guard in finishMiddleware).
431
576
  finishMiddleware();
432
- throw error;
433
577
  }
434
- finishMiddleware();
435
578
 
436
579
  // Record post-next() processing time when middleware did work after
437
580
  // the downstream chain resolved (e.g. adding headers, logging).
@@ -450,41 +593,18 @@ export async function executeMiddleware<TEnv>(
450
593
 
451
594
  // Explicit return takes precedence (middleware short-circuit).
452
595
  // Merge stub headers (from ctx.header before this point) and
453
- // RequestContext stub headers (from ctx.setCookie) into the
596
+ // RequestContext stub headers (from cookies().set()) into the
454
597
  // returned Response so they are not lost.
455
598
  if (result instanceof Response) {
456
- const mergedHeaders = new Headers(result.headers);
457
- stubResponse.headers.forEach((value, name) => {
458
- if (name.toLowerCase() === "set-cookie") {
459
- mergedHeaders.append(name, value);
460
- } else if (!mergedHeaders.has(name)) {
461
- mergedHeaders.set(name, value);
462
- }
463
- });
464
- // Also merge shared RequestContext stub (cookies written via setCookie).
465
- // Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
466
- // may have already merged the same reqCtx cookies into the response.
467
- const reqCtx = _getRequestContext();
468
- if (reqCtx) {
469
- const stubCookies = reqCtx.res.headers.getSetCookie();
470
- if (stubCookies.length > 0) {
471
- const existing = new Set(mergedHeaders.getSetCookie());
472
- for (const cookie of stubCookies) {
473
- if (!existing.has(cookie)) {
474
- mergedHeaders.append("set-cookie", cookie);
475
- }
476
- }
477
- }
478
- reqCtx.res.headers.forEach((value, name) => {
479
- if (name !== "set-cookie" && !mergedHeaders.has(name)) {
480
- mergedHeaders.set(name, value);
481
- }
482
- });
599
+ if (isWebSocketUpgradeResponse(result)) {
600
+ responseHolder.response = result;
601
+ return result;
483
602
  }
484
- const merged = new Response(result.body, {
485
- status: result.status,
486
- statusText: result.statusText,
487
- headers: mergedHeaders,
603
+ // Explicit short-circuit: the returned Response's own headers win
604
+ // (stubOverridesNonCookie=false); reqCtx stub headers still merge in.
605
+ const merged = mergeResponse(result, stubResponse.headers, {
606
+ stubOverridesNonCookie: false,
607
+ mergeReqCtx: true,
488
608
  });
489
609
  responseHolder.response = merged;
490
610
  return merged;
@@ -529,23 +649,12 @@ export async function executeMiddleware<TEnv>(
529
649
  // last merge point (e.g. cookies().set() called after await next()).
530
650
  // The reqCtx stub may have already been partially merged during finalHandler
531
651
  // or early-return paths; only append *new* Set-Cookie entries to avoid dupes.
652
+ //
653
+ // Skip for upgrade responses: upgrade headers are semantically immutable and
654
+ // set-cookie on an upgrade is not meaningful.
532
655
  const reqCtx = _getRequestContext();
533
- if (reqCtx) {
534
- const stubCookies = reqCtx.res.headers.getSetCookie();
535
- if (stubCookies.length > 0) {
536
- const existingCookies = new Set(finalResponse.headers.getSetCookie());
537
- for (const cookie of stubCookies) {
538
- if (!existingCookies.has(cookie)) {
539
- finalResponse.headers.append("set-cookie", cookie);
540
- }
541
- }
542
- }
543
- // Fill in non-cookie headers that aren't already on the response
544
- reqCtx.res.headers.forEach((value, name) => {
545
- if (name !== "set-cookie" && !finalResponse.headers.has(name)) {
546
- finalResponse.headers.set(name, value);
547
- }
548
- });
656
+ if (reqCtx && !isWebSocketUpgradeResponse(finalResponse)) {
657
+ mergeReqCtxStub(finalResponse.headers, reqCtx);
549
658
  }
550
659
 
551
660
  return finalResponse;
@@ -556,7 +665,7 @@ export async function executeMiddleware<TEnv>(
556
665
  *
557
666
  * Intercepts use a shared stubResponse from the request context. This function:
558
667
  * - Runs middleware in sequence with a simple next() chain
559
- * - Returns Response if any middleware short-circuits (returns Response or redirects BEFORE next())
668
+ * - Returns Response if any middleware short-circuits (returns OR throws a Response, or redirects, BEFORE next())
560
669
  * - Returns null if all middleware calls next() - headers set after next() remain on stubResponse
561
670
  *
562
671
  * @param middlewares - Array of middleware functions
@@ -594,6 +703,7 @@ export async function executeInterceptMiddleware<TEnv>(
594
703
  return stubResponse;
595
704
  }
596
705
 
706
+ const ordinal = index;
597
707
  const middleware = middlewares[index++];
598
708
  const ctx = createMiddlewareContext(
599
709
  request,
@@ -615,7 +725,30 @@ export async function executeInterceptMiddleware<TEnv>(
615
725
  return next();
616
726
  };
617
727
 
618
- const result = await middleware(ctx, guardedNext);
728
+ // Span-wrap each intercept middleware as rango.middleware (metric:false
729
+ // intercept runs inside the render phase already metered by render:total, so
730
+ // it contributes a span but no separate perf metric). Bare MiddlewareFns
731
+ // have no pattern, so the label is scoped to "*" like a pattern-less entry.
732
+ const label = getMiddlewareMetricLabel(
733
+ { handler: middleware, pattern: null } as MiddlewareEntry<TEnv>,
734
+ ordinal,
735
+ );
736
+
737
+ const result: Response | void = await observePhase(
738
+ PHASES.middleware(label),
739
+ async () => {
740
+ try {
741
+ return await middleware(ctx, guardedNext);
742
+ } catch (error) {
743
+ // Thrown Response is short-circuit control flow, parity with the
744
+ // explicit-return path below. Absorb it INSIDE the span so the tracing
745
+ // runner settles rango.middleware as success, not STATUS_ERROR (same
746
+ // reasoning as executeMiddleware's main chain). Real errors propagate.
747
+ if (error instanceof Response) return error;
748
+ throw error;
749
+ }
750
+ },
751
+ );
619
752
 
620
753
  if (result instanceof Response) {
621
754
  earlyResponse = result;
@@ -639,21 +772,13 @@ export async function executeInterceptMiddleware<TEnv>(
639
772
  });
640
773
 
641
774
  if (hasStubHeaders) {
642
- // Clone and merge headers from stub into early response.
643
- // Only fill in missing headers — the returned Response's explicit
644
- // headers take precedence, matching executeMiddleware behavior.
645
- const mergedHeaders = new Headers(response.headers);
646
- stubResponse.headers.forEach((value, name) => {
647
- if (name.toLowerCase() === "set-cookie") {
648
- mergedHeaders.append(name, value);
649
- } else if (!mergedHeaders.has(name)) {
650
- mergedHeaders.set(name, value);
651
- }
652
- });
653
- return new Response(response.body, {
654
- status: response.status,
655
- statusText: response.statusText,
656
- headers: mergedHeaders,
775
+ // Only fill in missing headers the returned Response's explicit headers
776
+ // take precedence (stubOverridesNonCookie=false), matching executeMiddleware.
777
+ // mergeReqCtx=false: the intercept path deliberately does NOT merge reqCtx
778
+ // stub headers here (pinned by intercept-middleware-headers.test.ts).
779
+ return mergeResponse(response, stubResponse.headers, {
780
+ stubOverridesNonCookie: false,
781
+ mergeReqCtx: false,
657
782
  });
658
783
  }
659
784
  return response;
@@ -694,7 +819,6 @@ export async function executeLoaderMiddleware<TEnv>(
694
819
  regex: null,
695
820
  paramNames: [],
696
821
  handler,
697
- mountPrefix: null,
698
822
  } as MiddlewareEntry<TEnv>,
699
823
  params,
700
824
  }));