@rangojs/router 0.0.0-experimental.14 → 0.0.0-experimental.140

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 (460) hide show
  1. package/AGENTS.md +17 -0
  2. package/README.md +432 -7
  3. package/dist/bin/rango.js +2073 -213
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +7258 -2714
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +140 -67
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +329 -0
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +487 -0
  12. package/skills/caching/SKILL.md +357 -25
  13. package/skills/comparison/SKILL.md +50 -0
  14. package/skills/comparison/agents/openai.yaml +4 -0
  15. package/skills/comparison/references/framework-comparison.md +837 -0
  16. package/skills/composability/SKILL.md +246 -0
  17. package/skills/css/SKILL.md +76 -0
  18. package/skills/debug-manifest/SKILL.md +16 -10
  19. package/skills/document-cache/SKILL.md +87 -62
  20. package/skills/fonts/SKILL.md +6 -4
  21. package/skills/handler-use/SKILL.md +364 -0
  22. package/skills/hooks/SKILL.md +557 -79
  23. package/skills/host-router/SKILL.md +320 -0
  24. package/skills/i18n/SKILL.md +276 -0
  25. package/skills/intercept/SKILL.md +207 -15
  26. package/skills/layout/SKILL.md +146 -6
  27. package/skills/links/SKILL.md +304 -25
  28. package/skills/loader/SKILL.md +616 -54
  29. package/skills/middleware/SKILL.md +217 -37
  30. package/skills/migrate-nextjs/SKILL.md +611 -0
  31. package/skills/migrate-react-router/SKILL.md +927 -0
  32. package/skills/mime-routes/SKILL.md +42 -11
  33. package/skills/observability/SKILL.md +194 -0
  34. package/skills/parallel/SKILL.md +284 -3
  35. package/skills/ppr/SKILL.md +426 -0
  36. package/skills/prerender/SKILL.md +437 -52
  37. package/skills/rango/SKILL.md +369 -22
  38. package/skills/react-compiler/SKILL.md +168 -0
  39. package/skills/response-routes/SKILL.md +263 -121
  40. package/skills/route/SKILL.md +350 -21
  41. package/skills/router-setup/SKILL.md +246 -33
  42. package/skills/scripts/SKILL.md +179 -0
  43. package/skills/server-actions/SKILL.md +775 -0
  44. package/skills/shell-manifest/SKILL.md +185 -0
  45. package/skills/streams-and-websockets/SKILL.md +283 -0
  46. package/skills/tailwind/SKILL.md +27 -3
  47. package/skills/testing/SKILL.md +126 -222
  48. package/skills/testing/bindings.md +103 -0
  49. package/skills/testing/cache-prerender.md +127 -0
  50. package/skills/testing/client-components.md +124 -0
  51. package/skills/testing/e2e-parity.md +125 -0
  52. package/skills/testing/flight.md +91 -0
  53. package/skills/testing/handles.md +131 -0
  54. package/skills/testing/loader.md +128 -0
  55. package/skills/testing/middleware.md +99 -0
  56. package/skills/testing/render-handler.md +122 -0
  57. package/skills/testing/response-routes.md +95 -0
  58. package/skills/testing/reverse-and-types.md +85 -0
  59. package/skills/testing/server-actions.md +107 -0
  60. package/skills/testing/server-tree.md +128 -0
  61. package/skills/testing/setup.md +123 -0
  62. package/skills/theme/SKILL.md +9 -8
  63. package/skills/typesafety/SKILL.md +532 -103
  64. package/skills/use-cache/SKILL.md +367 -0
  65. package/skills/vercel/SKILL.md +128 -0
  66. package/skills/view-transitions/SKILL.md +337 -0
  67. package/src/__augment-tests__/augment.ts +81 -0
  68. package/src/__augment-tests__/augmented.check.ts +116 -0
  69. package/src/__internal.ts +77 -44
  70. package/src/bin/rango.ts +312 -15
  71. package/src/browser/action-coordinator.ts +114 -0
  72. package/src/browser/action-fence.ts +47 -0
  73. package/src/browser/app-shell.ts +39 -0
  74. package/src/browser/app-version.ts +14 -0
  75. package/src/browser/connection-warmup.ts +134 -0
  76. package/src/browser/cookie-name.ts +140 -0
  77. package/src/browser/event-controller.ts +293 -202
  78. package/src/browser/history-state.ts +101 -0
  79. package/src/browser/index.ts +3 -3
  80. package/src/browser/intercept-utils.ts +52 -0
  81. package/src/browser/invalidate-client-cache.ts +52 -0
  82. package/src/browser/link-interceptor.ts +24 -4
  83. package/src/browser/logging.ts +11 -0
  84. package/src/browser/merge-segment-loaders.ts +20 -12
  85. package/src/browser/navigation-bridge.ts +385 -576
  86. package/src/browser/navigation-client.ts +245 -75
  87. package/src/browser/navigation-store-handle.ts +38 -0
  88. package/src/browser/navigation-store.ts +184 -118
  89. package/src/browser/navigation-transaction.ts +247 -0
  90. package/src/browser/network-error-handler.ts +88 -0
  91. package/src/browser/partial-update.ts +412 -364
  92. package/src/browser/prefetch/cache.ts +359 -0
  93. package/src/browser/prefetch/fetch.ts +452 -0
  94. package/src/browser/prefetch/observer.ts +65 -0
  95. package/src/browser/prefetch/policy.ts +48 -0
  96. package/src/browser/prefetch/queue.ts +209 -0
  97. package/src/browser/prefetch/resource-ready.ts +77 -0
  98. package/src/browser/rango-state.ts +194 -0
  99. package/src/browser/react/Link.tsx +275 -68
  100. package/src/browser/react/NavigationProvider.tsx +265 -109
  101. package/src/browser/react/ScrollRestoration.tsx +10 -6
  102. package/src/browser/react/context.ts +11 -0
  103. package/src/browser/react/filter-segment-order.ts +70 -0
  104. package/src/browser/react/index.ts +0 -48
  105. package/src/browser/react/location-state-shared.ts +272 -60
  106. package/src/browser/react/location-state.ts +90 -20
  107. package/src/browser/react/mount-context.ts +6 -1
  108. package/src/browser/react/nonce-context.ts +23 -0
  109. package/src/browser/react/shallow-equal.ts +27 -0
  110. package/src/browser/react/use-action.ts +35 -66
  111. package/src/browser/react/use-handle.ts +39 -126
  112. package/src/browser/react/use-href.tsx +8 -1
  113. package/src/browser/react/use-link-status.ts +39 -13
  114. package/src/browser/react/use-navigation.ts +53 -69
  115. package/src/browser/react/use-params.ts +75 -0
  116. package/src/browser/react/use-pathname.ts +47 -0
  117. package/src/browser/react/use-reverse.ts +106 -0
  118. package/src/browser/react/use-router.ts +98 -0
  119. package/src/browser/react/use-search-params.ts +51 -0
  120. package/src/browser/react/use-segments.ts +72 -99
  121. package/src/browser/response-adapter.ts +164 -0
  122. package/src/browser/rsc-router.tsx +300 -72
  123. package/src/browser/scroll-restoration.ts +138 -50
  124. package/src/browser/segment-reconciler.ts +243 -0
  125. package/src/browser/segment-structure-assert.ts +17 -1
  126. package/src/browser/server-action-bridge.ts +668 -613
  127. package/src/browser/types.ts +223 -51
  128. package/src/browser/validate-redirect-origin.ts +56 -0
  129. package/src/build/collect-fallback-refs.ts +107 -0
  130. package/src/build/generate-manifest.ts +252 -161
  131. package/src/build/generate-route-types.ts +41 -1038
  132. package/src/build/index.ts +12 -7
  133. package/src/build/prefix-tree-utils.ts +123 -0
  134. package/src/build/route-trie.ts +225 -42
  135. package/src/build/route-types/ast-helpers.ts +25 -0
  136. package/src/build/route-types/ast-route-extraction.ts +105 -0
  137. package/src/build/route-types/codegen.ts +113 -0
  138. package/src/build/route-types/include-resolution.ts +812 -0
  139. package/src/build/route-types/param-extraction.ts +51 -0
  140. package/src/build/route-types/per-module-writer.ts +144 -0
  141. package/src/build/route-types/router-processing.ts +695 -0
  142. package/src/build/route-types/scan-filter.ts +85 -0
  143. package/src/build/route-types/source-scan.ts +216 -0
  144. package/src/build/runtime-discovery.ts +223 -0
  145. package/src/cache/background-task.ts +34 -0
  146. package/src/cache/cache-error.ts +104 -0
  147. package/src/cache/cache-key-utils.ts +60 -0
  148. package/src/cache/cache-policy.ts +199 -0
  149. package/src/cache/cache-runtime.ts +525 -0
  150. package/src/cache/cache-scope.ts +298 -332
  151. package/src/cache/cache-tag.ts +103 -0
  152. package/src/cache/cf/cf-base64.ts +33 -0
  153. package/src/cache/cf/cf-cache-constants.ts +127 -0
  154. package/src/cache/cf/cf-cache-store.ts +2500 -158
  155. package/src/cache/cf/cf-cache-types.ts +349 -0
  156. package/src/cache/cf/cf-kv-utils.ts +46 -0
  157. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  158. package/src/cache/cf/index.ts +17 -17
  159. package/src/cache/document-cache.ts +199 -92
  160. package/src/cache/handle-capture.ts +81 -0
  161. package/src/cache/handle-snapshot.ts +111 -0
  162. package/src/cache/index.ts +29 -35
  163. package/src/cache/memory-segment-store.ts +363 -30
  164. package/src/cache/profile-registry.ts +88 -0
  165. package/src/cache/read-through-swr.ts +178 -0
  166. package/src/cache/segment-codec.ts +248 -0
  167. package/src/cache/shell-cache.ts +386 -0
  168. package/src/cache/tag-invalidation.ts +230 -0
  169. package/src/cache/taint.ts +153 -0
  170. package/src/cache/types.ts +156 -211
  171. package/src/cache/vercel/index.ts +11 -0
  172. package/src/cache/vercel/vercel-cache-store.ts +1102 -0
  173. package/src/client.rsc.tsx +43 -21
  174. package/src/client.tsx +131 -347
  175. package/src/cloudflare/index.ts +11 -0
  176. package/src/cloudflare/tracing.ts +109 -0
  177. package/src/component-utils.ts +23 -4
  178. package/src/components/DefaultDocument.tsx +13 -3
  179. package/src/context-var.ts +168 -0
  180. package/src/debug.ts +19 -9
  181. package/src/decode-loader-results.ts +52 -0
  182. package/src/defer.ts +185 -0
  183. package/src/deps/ssr.ts +0 -1
  184. package/src/encode-kv.ts +49 -0
  185. package/src/errors.ts +106 -10
  186. package/src/escape-script.ts +52 -0
  187. package/src/handle.ts +110 -35
  188. package/src/handles/MetaTags.tsx +83 -59
  189. package/src/handles/Scripts.tsx +183 -0
  190. package/src/handles/breadcrumbs.ts +93 -0
  191. package/src/handles/deferred-resolution.ts +127 -0
  192. package/src/handles/is-thenable.ts +18 -0
  193. package/src/handles/meta.ts +44 -53
  194. package/src/handles/script.ts +244 -0
  195. package/src/host/cookie-handler.ts +20 -65
  196. package/src/host/errors.ts +21 -30
  197. package/src/host/index.ts +13 -9
  198. package/src/host/pattern-matcher.ts +50 -79
  199. package/src/host/router.ts +151 -121
  200. package/src/host/testing.ts +45 -32
  201. package/src/host/types.ts +52 -11
  202. package/src/host/utils.ts +2 -2
  203. package/src/href-client.ts +192 -57
  204. package/src/index.rsc.ts +177 -35
  205. package/src/index.ts +255 -71
  206. package/src/internal-debug.ts +9 -2
  207. package/src/loader-store.ts +500 -0
  208. package/src/loader.rsc.ts +31 -99
  209. package/src/loader.ts +30 -12
  210. package/src/missing-id-error.ts +68 -0
  211. package/src/outlet-context.ts +1 -1
  212. package/src/outlet-provider.tsx +41 -0
  213. package/src/prerender/param-hash.ts +16 -14
  214. package/src/prerender/store.ts +121 -21
  215. package/src/prerender.ts +460 -26
  216. package/src/redirect-origin.ts +100 -0
  217. package/src/regex-escape.ts +8 -0
  218. package/src/render-error-thrower.tsx +20 -0
  219. package/src/response-utils.ts +62 -0
  220. package/src/reverse.ts +198 -128
  221. package/src/root-error-boundary.tsx +42 -48
  222. package/src/route-content-wrapper.tsx +22 -77
  223. package/src/route-definition/dsl-helpers.ts +1116 -0
  224. package/src/route-definition/helper-factories.ts +88 -0
  225. package/src/route-definition/helpers-types.ts +505 -0
  226. package/src/route-definition/index.ts +54 -0
  227. package/src/route-definition/redirect.ts +134 -0
  228. package/src/route-definition/resolve-handler-use.ts +160 -0
  229. package/src/route-definition/use-item-types.ts +29 -0
  230. package/src/route-definition.ts +1 -1481
  231. package/src/route-map-builder.ts +82 -144
  232. package/src/route-name.ts +53 -0
  233. package/src/route-types.ts +71 -45
  234. package/src/router/basename.ts +14 -0
  235. package/src/router/content-negotiation.ts +263 -0
  236. package/src/router/debug-manifest.ts +72 -0
  237. package/src/router/error-handling.ts +54 -27
  238. package/src/router/find-match.ts +245 -0
  239. package/src/router/handler-context.ts +377 -125
  240. package/src/router/instrument.ts +350 -0
  241. package/src/router/intercept-resolution.ts +59 -28
  242. package/src/router/lazy-includes.ts +254 -0
  243. package/src/router/loader-resolution.ts +421 -157
  244. package/src/router/logging.ts +106 -6
  245. package/src/router/manifest.ts +131 -57
  246. package/src/router/match-api.ts +167 -246
  247. package/src/router/match-context.ts +4 -24
  248. package/src/router/match-handlers.ts +440 -0
  249. package/src/router/match-middleware/background-revalidation.ts +117 -93
  250. package/src/router/match-middleware/cache-lookup.ts +297 -150
  251. package/src/router/match-middleware/cache-store.ts +123 -51
  252. package/src/router/match-middleware/intercept-resolution.ts +44 -43
  253. package/src/router/match-middleware/segment-resolution.ts +64 -22
  254. package/src/router/match-pipelines.ts +11 -87
  255. package/src/router/match-result.ts +121 -50
  256. package/src/router/metrics.ts +219 -28
  257. package/src/router/middleware-types.ts +93 -0
  258. package/src/router/middleware.ts +505 -441
  259. package/src/router/navigation-snapshot.ts +133 -0
  260. package/src/router/params-util.ts +23 -0
  261. package/src/router/parse-pattern.ts +115 -0
  262. package/src/router/pattern-matching.ts +311 -142
  263. package/src/router/prefetch-cache-ttl.ts +51 -0
  264. package/src/router/prefetch-limits.ts +37 -0
  265. package/src/router/prerender-match.ts +547 -0
  266. package/src/router/preview-match.ts +102 -0
  267. package/src/router/request-classification.ts +278 -0
  268. package/src/router/revalidation.ts +203 -62
  269. package/src/router/route-snapshot.ts +246 -0
  270. package/src/router/router-context.ts +45 -48
  271. package/src/router/router-interfaces.ts +554 -0
  272. package/src/router/router-options.ts +779 -0
  273. package/src/router/router-registry.ts +21 -0
  274. package/src/router/segment-resolution/fresh.ts +772 -0
  275. package/src/router/segment-resolution/helpers.ts +348 -0
  276. package/src/router/segment-resolution/loader-cache.ts +250 -0
  277. package/src/router/segment-resolution/loader-mask.ts +44 -0
  278. package/src/router/segment-resolution/revalidation.ts +1331 -0
  279. package/src/router/segment-resolution/static-store.ts +81 -0
  280. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  281. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  282. package/src/router/segment-resolution.ts +25 -1354
  283. package/src/router/segment-wrappers.ts +292 -0
  284. package/src/router/state-cookie-name.ts +33 -0
  285. package/src/router/substitute-pattern-params.ts +75 -0
  286. package/src/router/telemetry-otel.ts +261 -0
  287. package/src/router/telemetry.ts +377 -0
  288. package/src/router/timeout.ts +128 -0
  289. package/src/router/tracing.ts +206 -0
  290. package/src/router/trie-matching.ts +240 -61
  291. package/src/router/types.ts +23 -70
  292. package/src/router/url-params.ts +57 -0
  293. package/src/router.ts +781 -2378
  294. package/src/rsc/full-payload.ts +70 -0
  295. package/src/rsc/handler-context.ts +46 -0
  296. package/src/rsc/handler.ts +905 -1142
  297. package/src/rsc/helpers.ts +275 -19
  298. package/src/rsc/index.ts +2 -25
  299. package/src/rsc/json-route-result.ts +38 -0
  300. package/src/rsc/loader-fetch.ts +305 -0
  301. package/src/rsc/manifest-init.ts +77 -0
  302. package/src/rsc/nonce.ts +14 -0
  303. package/src/rsc/origin-guard.ts +155 -0
  304. package/src/rsc/progressive-enhancement.ts +502 -0
  305. package/src/rsc/redirect-guard.ts +99 -0
  306. package/src/rsc/response-cache-serve.ts +238 -0
  307. package/src/rsc/response-error.ts +104 -0
  308. package/src/rsc/response-route-handler.ts +257 -0
  309. package/src/rsc/rsc-rendering.ts +337 -0
  310. package/src/rsc/runtime-warnings.ts +55 -0
  311. package/src/rsc/server-action.ts +522 -0
  312. package/src/rsc/shell-capture.ts +439 -0
  313. package/src/rsc/ssr-setup.ts +144 -0
  314. package/src/rsc/transition-gate.ts +89 -0
  315. package/src/rsc/types.ts +95 -12
  316. package/src/runtime-env.ts +18 -0
  317. package/src/search-params.ts +99 -82
  318. package/src/segment-content-promise.ts +67 -0
  319. package/src/segment-loader-promise.ts +149 -0
  320. package/src/segment-system.tsx +349 -134
  321. package/src/serialize.ts +243 -0
  322. package/src/server/context.ts +452 -85
  323. package/src/server/cookie-parse.ts +32 -0
  324. package/src/server/cookie-store.ts +310 -0
  325. package/src/server/fetchable-loader-store.ts +11 -6
  326. package/src/server/handle-store.ts +123 -42
  327. package/src/server/live.ts +130 -0
  328. package/src/server/loader-registry.ts +51 -100
  329. package/src/server/request-context.ts +842 -157
  330. package/src/server.ts +15 -8
  331. package/src/ssr/index.tsx +412 -136
  332. package/src/ssr/ssr-root.tsx +228 -0
  333. package/src/static-handler.ts +45 -18
  334. package/src/testing/cache-status.ts +162 -0
  335. package/src/testing/collect-handle.ts +46 -0
  336. package/src/testing/dispatch.ts +701 -0
  337. package/src/testing/dom.entry.ts +22 -0
  338. package/src/testing/e2e/fixture.ts +188 -0
  339. package/src/testing/e2e/index.ts +128 -0
  340. package/src/testing/e2e/matchers.ts +35 -0
  341. package/src/testing/e2e/page-helpers.ts +272 -0
  342. package/src/testing/e2e/parity.ts +387 -0
  343. package/src/testing/e2e/server.ts +195 -0
  344. package/src/testing/flight-matchers.ts +97 -0
  345. package/src/testing/flight-normalize.ts +11 -0
  346. package/src/testing/flight-runtime.d.ts +57 -0
  347. package/src/testing/flight-tree.ts +682 -0
  348. package/src/testing/flight.entry.ts +52 -0
  349. package/src/testing/flight.ts +257 -0
  350. package/src/testing/generated-routes.ts +199 -0
  351. package/src/testing/index.ts +105 -0
  352. package/src/testing/internal/context.ts +371 -0
  353. package/src/testing/internal/flight-client-globals.ts +30 -0
  354. package/src/testing/internal/seed-vars.ts +54 -0
  355. package/src/testing/render-handler.ts +357 -0
  356. package/src/testing/render-route.tsx +584 -0
  357. package/src/testing/run-loader.ts +385 -0
  358. package/src/testing/run-middleware.ts +205 -0
  359. package/src/testing/run-transition-when.ts +164 -0
  360. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  361. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  362. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  363. package/src/testing/vitest-stubs/version.ts +5 -0
  364. package/src/testing/vitest.ts +305 -0
  365. package/src/theme/ThemeProvider.tsx +40 -72
  366. package/src/theme/ThemeScript.tsx +12 -14
  367. package/src/theme/constants.ts +57 -15
  368. package/src/theme/index.ts +3 -20
  369. package/src/theme/theme-context.ts +5 -35
  370. package/src/theme/theme-script.ts +43 -39
  371. package/src/theme/use-theme.ts +0 -3
  372. package/src/types/boundaries.ts +123 -0
  373. package/src/types/cache-types.ts +207 -0
  374. package/src/types/error-types.ts +132 -0
  375. package/src/types/global-namespace.ts +113 -0
  376. package/src/types/handler-context.ts +839 -0
  377. package/src/types/index.ts +81 -0
  378. package/src/types/loader-types.ts +212 -0
  379. package/src/types/request-scope.ts +112 -0
  380. package/src/types/route-config.ts +138 -0
  381. package/src/types/route-entry.ts +114 -0
  382. package/src/types/segments.ts +271 -0
  383. package/src/types.ts +1 -1795
  384. package/src/urls/include-helper.ts +162 -0
  385. package/src/urls/include-provider.ts +71 -0
  386. package/src/urls/index.ts +43 -0
  387. package/src/urls/path-helper-types.ts +413 -0
  388. package/src/urls/path-helper.ts +275 -0
  389. package/src/urls/pattern-types.ts +124 -0
  390. package/src/urls/response-types.ts +109 -0
  391. package/src/urls/type-extraction.ts +316 -0
  392. package/src/urls/urls-function.ts +80 -0
  393. package/src/urls.ts +1 -1341
  394. package/src/use-loader.tsx +406 -141
  395. package/src/vercel/index.ts +11 -0
  396. package/src/vercel/tracing.ts +88 -0
  397. package/src/vite/debug.ts +185 -0
  398. package/src/vite/discovery/bundle-postprocess.ts +182 -0
  399. package/src/vite/discovery/discover-routers.ts +389 -0
  400. package/src/vite/discovery/discovery-errors.ts +255 -0
  401. package/src/vite/discovery/gate-state.ts +171 -0
  402. package/src/vite/discovery/prerender-collection.ts +467 -0
  403. package/src/vite/discovery/route-types-writer.ts +214 -0
  404. package/src/vite/discovery/self-gen-tracking.ts +73 -0
  405. package/src/vite/discovery/state.ts +161 -0
  406. package/src/vite/discovery/virtual-module-codegen.ts +183 -0
  407. package/src/vite/index.ts +23 -2255
  408. package/src/vite/inject-client-debug.ts +36 -0
  409. package/src/vite/plugin-types.ts +303 -0
  410. package/src/vite/plugins/cjs-to-esm.ts +90 -0
  411. package/src/vite/plugins/client-ref-dedup.ts +120 -0
  412. package/src/vite/plugins/client-ref-hashing.ts +118 -0
  413. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  414. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  415. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  416. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +88 -110
  417. package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +89 -79
  418. package/src/vite/plugins/expose-ids/export-analysis.ts +363 -0
  419. package/src/vite/plugins/expose-ids/handler-transform.ts +130 -0
  420. package/src/vite/plugins/expose-ids/loader-transform.ts +64 -0
  421. package/src/vite/plugins/expose-ids/router-transform.ts +199 -0
  422. package/src/vite/plugins/expose-ids/types.ts +45 -0
  423. package/src/vite/plugins/expose-internal-ids.ts +805 -0
  424. package/src/vite/plugins/performance-tracks.ts +89 -0
  425. package/src/vite/plugins/refresh-cmd.ts +127 -0
  426. package/src/vite/plugins/use-cache-transform.ts +313 -0
  427. package/src/vite/plugins/vercel-output.ts +384 -0
  428. package/src/vite/plugins/version-injector.ts +94 -0
  429. package/src/vite/plugins/version-plugin.ts +263 -0
  430. package/src/vite/plugins/virtual-entries.ts +234 -0
  431. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  432. package/src/vite/rango.ts +560 -0
  433. package/src/vite/router-discovery.ts +1638 -0
  434. package/src/vite/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +200 -37
  435. package/src/vite/utils/banner.ts +36 -0
  436. package/src/vite/utils/bundle-analysis.ts +132 -0
  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 +15 -0
  441. package/src/vite/utils/package-resolution.ts +89 -0
  442. package/src/vite/utils/prerender-utils.ts +249 -0
  443. package/src/vite/utils/shared-utils.ts +269 -0
  444. package/CLAUDE.md +0 -43
  445. package/dist/vite/index.named-routes.gen.ts +0 -103
  446. package/src/browser/lru-cache.ts +0 -69
  447. package/src/browser/react/use-client-cache.ts +0 -56
  448. package/src/browser/request-controller.ts +0 -164
  449. package/src/browser/shallow.ts +0 -35
  450. package/src/cache/memory-store.ts +0 -253
  451. package/src/handles/index.ts +0 -6
  452. package/src/href-context.ts +0 -33
  453. package/src/network-error-thrower.tsx +0 -21
  454. package/src/router.gen.ts +0 -6
  455. package/src/static-handler.gen.ts +0 -5
  456. package/src/urls.gen.ts +0 -8
  457. package/src/vite/expose-internal-ids.ts +0 -1167
  458. package/src/vite/package-resolution.ts +0 -125
  459. package/src/vite/virtual-entries.ts +0 -114
  460. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -1,184 +1,92 @@
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
- import type { RouterEnv } from "../types.js";
13
-
14
- /**
15
- * Helper type to extract Variables from RouterEnv
16
- * Uses 0 extends 1 & TEnv to detect `any` type and fall back to Record<string, unknown>
17
- */
18
- type ExtractVariables<TEnv> = 0 extends 1 & TEnv
19
- ? Record<string, unknown> // TEnv is any
20
- : TEnv extends RouterEnv<unknown, infer V>
21
- ? V
22
- : Record<string, unknown>;
23
-
24
- /**
25
- * Get variable function type
26
- */
27
- type GetVariableFn<TEnv> = <K extends keyof ExtractVariables<TEnv>>(
28
- key: K
29
- ) => ExtractVariables<TEnv>[K];
30
-
31
- /**
32
- * Set variable function type
33
- */
34
- type SetVariableFn<TEnv> = <K extends keyof ExtractVariables<TEnv>>(
35
- key: K,
36
- value: ExtractVariables<TEnv>[K]
37
- ) => void;
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";
8
+ import type {
9
+ CollectedMiddleware,
10
+ MiddlewareCollectableEntry,
11
+ MiddlewareContext,
12
+ MiddlewareEntry,
13
+ MiddlewareFn,
14
+ ResponseHolder,
15
+ } from "./middleware-types.js";
16
+ import { _getRequestContext } from "../server/request-context.js";
17
+ import {
18
+ EXTERNAL_REDIRECT_MARKER,
19
+ isExternalRedirect,
20
+ markExternalRedirect,
21
+ } from "../redirect-origin.js";
22
+ import { isAutoGeneratedRouteName } from "../route-name.js";
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";
27
+
28
+ // Re-export types consumed through this module's path.
29
+ export type {
30
+ CookieOptions,
31
+ MiddlewareContext,
32
+ MiddlewareEntry,
33
+ MiddlewareFn,
34
+ } from "./middleware-types.js";
35
+
36
+ const MIDDLEWARE_METRIC_DEPTH = 1;
37
+ const POST_METRIC_MIN_DURATION_MS = 0.01;
38
+
39
+ function getMiddlewareMetricLabel<TEnv>(
40
+ entry: MiddlewareEntry<TEnv>,
41
+ ordinal: number,
42
+ ): string {
43
+ const handlerName = entry.handler.name?.trim();
44
+ const scope = entry.pattern ?? "*";
38
45
 
39
- /**
40
- * Cookie options for setting cookies
41
- */
42
- export interface CookieOptions {
43
- domain?: string;
44
- path?: string;
45
- maxAge?: number;
46
- expires?: Date;
47
- httpOnly?: boolean;
48
- secure?: boolean;
49
- sameSite?: "strict" | "lax" | "none";
50
- }
46
+ if (handlerName) {
47
+ return `middleware:${handlerName}@${scope}`;
48
+ }
51
49
 
52
- /**
53
- * Context passed to middleware
54
- *
55
- * @template TEnv - Environment type (bindings, variables) - defaults to any for internal flexibility
56
- * @template TParams - URL params type (typed for route middleware, Record<string, string> for global middleware)
57
- */
58
- export interface MiddlewareContext<
59
- TEnv = any,
60
- TParams = Record<string, string>,
61
- > {
62
- /** Original request */
63
- request: Request;
64
-
65
- /** Parsed URL */
66
- url: URL;
67
-
68
- /** URL pathname */
69
- pathname: string;
70
-
71
- /** URL search params */
72
- searchParams: URLSearchParams;
73
-
74
- /** Platform bindings (Cloudflare, etc.) */
75
- env: TEnv extends RouterEnv<infer B, unknown> ? B : {};
76
-
77
- /** URL params extracted from route/middleware pattern */
78
- params: TParams;
79
-
80
- /**
81
- * Response object - available immediately via stub, real response after `await next()`
82
- *
83
- * Headers set before `next()` are merged into the final response.
84
- * Can be used to modify headers directly like Hono's `c.res`.
85
- *
86
- * @example
87
- * ```typescript
88
- * middleware(async (ctx, next) => {
89
- * // Set headers BEFORE next() - will be merged into final response
90
- * ctx.res.headers.set('X-Request-Id', generateId());
91
- *
92
- * await next();
93
- *
94
- * // Set headers AFTER next() - applied directly
95
- * ctx.res.headers.set('X-Custom', 'value');
96
- * // No return needed!
97
- * });
98
- * ```
99
- */
100
- res: Response;
101
-
102
- /** Get a cookie value */
103
- cookie(name: string): string | undefined;
104
-
105
- /** Get all cookies as object */
106
- cookies(): Record<string, string>;
107
-
108
- /** Set a cookie on the response */
109
- setCookie(name: string, value: string, options?: CookieOptions): void;
110
-
111
- /** Delete a cookie */
112
- deleteCookie(
113
- name: string,
114
- options?: Pick<CookieOptions, "domain" | "path">
115
- ): void;
116
-
117
- /** Get a context variable (shared with route handlers) */
118
- get: GetVariableFn<TEnv>;
119
-
120
- /** Set a context variable (shared with route handlers) */
121
- set: SetVariableFn<TEnv>;
122
-
123
- /**
124
- * Set a response header - can be called before or after `next()`
125
- *
126
- * When called before `next()`, headers are queued and merged into the final response.
127
- * When called after `next()`, headers are set directly on the response.
128
- * Shorthand for `ctx.res.headers.set()`.
129
- */
130
- header(name: string, value: string): void;
50
+ return `middleware:${scope}#${ordinal + 1}`;
131
51
  }
132
52
 
133
53
  /**
134
- * Middleware function signature
135
- *
136
- * @template TEnv - Environment type - defaults to any for internal flexibility
137
- * @template TParams - URL params type (typed for route middleware)
54
+ * Compile a middleware scope pattern to a regex + param names.
138
55
  *
139
- * When using middleware with global augmentation (RSCRouter.Env), explicitly
140
- * annotate your middleware functions, or the types will be inferred from context:
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.
141
63
  *
142
- * @example
143
- * ```typescript
144
- * // With explicit annotation (recommended for reusable middleware)
145
- * const authMiddleware: MiddlewareFn<AppEnv> = async (ctx, next) => {...}
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.
146
81
  *
147
- * // Types inferred from router.use() call
148
- * router.use((ctx, next) => {...}) // ctx is typed from router's TEnv
149
- * ```
150
- */
151
- export type MiddlewareFn<TEnv = any, TParams = Record<string, string>> = (
152
- ctx: MiddlewareContext<TEnv, TParams>,
153
- next: () => Promise<Response>
154
- ) => Response | void | Promise<Response | void>;
155
-
156
- /**
157
- * Stored middleware entry with pattern matching info
158
- * @internal - uses any for internal flexibility
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).
159
88
  */
160
- export interface MiddlewareEntry<TEnv = any> {
161
- /** Original pattern string */
162
- pattern: string | null;
163
-
164
- /** Compiled regex for matching */
165
- regex: RegExp | null;
166
-
167
- /** Param names extracted from pattern */
168
- paramNames: string[];
169
-
170
- /** The middleware function */
171
- handler: MiddlewareFn<TEnv>;
172
-
173
- /** Mount prefix this middleware is scoped to (null = global) */
174
- mountPrefix: string | null;
175
- }
176
-
177
- /**
178
- * Parse a route pattern into regex and param names
179
- * Supports: *, /path, /path/*, /path/:param, /path/:param/*
180
- */
181
- export function parsePattern(pattern: string): {
89
+ export function compileMiddlewarePattern(pattern: string): {
182
90
  regex: RegExp;
183
91
  paramNames: string[];
184
92
  } {
@@ -186,208 +94,219 @@ export function parsePattern(pattern: string): {
186
94
  return { regex: /^.*$/, paramNames: [] };
187
95
  }
188
96
 
97
+ const normalizedPattern = pattern.startsWith("/") ? pattern : `/${pattern}`;
98
+ const segments = parseRoutePattern(normalizedPattern);
189
99
  const paramNames: string[] = [];
190
100
  let regexStr = "^";
191
-
192
- const parts = pattern.split("/").filter(Boolean);
193
-
194
- for (let i = 0; i < parts.length; i++) {
195
- const part = parts[i];
196
-
197
- if (part === "*") {
198
- // Wildcard - match rest of path
199
- regexStr += "(?:/.*)?";
200
- } else if (part.startsWith(":")) {
201
- // Param
202
- const paramName = part.slice(1);
203
- paramNames.push(paramName);
204
- 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
+ }
205
135
  } else {
206
- // Literal
207
- regexStr += "/" + escapeRegex(part);
136
+ // Static literal
137
+ regexStr += "/" + escapeRegExp(segment.value);
208
138
  }
209
139
  }
210
140
 
211
- // If pattern doesn't end with *, match exact or with trailing segments
212
- if (!pattern.endsWith("*")) {
213
- regexStr += "/?$";
214
- } else {
215
- regexStr += "$";
216
- }
141
+ // Without a trailing `*`, match exactly with an optional trailing slash.
142
+ regexStr += hasTrailingWildcard ? "$" : "/?$";
217
143
 
218
144
  return { regex: new RegExp(regexStr), paramNames };
219
145
  }
220
146
 
221
147
  /**
222
- * Escape special regex characters
223
- */
224
- function escapeRegex(str: string): string {
225
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
226
- }
227
-
228
- /**
229
- * 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.
230
154
  */
231
155
  export function extractParams(
232
156
  pathname: string,
233
157
  regex: RegExp,
234
- paramNames: string[]
158
+ paramNames: string[],
235
159
  ): Record<string, string> {
236
160
  const match = pathname.match(regex);
237
161
  if (!match) return {};
238
162
 
239
163
  const params: Record<string, string> = {};
240
164
  for (let i = 0; i < paramNames.length; i++) {
241
- params[paramNames[i]] = match[i + 1] || "";
165
+ params[paramNames[i]] = safeDecodeURIComponent(match[i + 1] || "");
242
166
  }
243
167
  return params;
244
168
  }
245
169
 
246
- /**
247
- * Parse cookies from Cookie header
248
- */
249
- export function parseCookies(
250
- cookieHeader: string | null
251
- ): Record<string, string> {
252
- if (!cookieHeader) return {};
253
-
254
- const cookies: Record<string, string> = {};
255
- const pairs = cookieHeader.split(";");
256
-
257
- for (const pair of pairs) {
258
- const [name, ...rest] = pair.trim().split("=");
259
- if (name) {
260
- cookies[name] = decodeURIComponent(rest.join("="));
261
- }
262
- }
263
-
264
- return cookies;
265
- }
266
-
267
- /**
268
- * Serialize a cookie for Set-Cookie header
269
- */
270
- export function serializeCookie(
271
- name: string,
272
- value: string,
273
- options: CookieOptions = {}
274
- ): string {
275
- let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
276
-
277
- if (options.domain) cookie += `; Domain=${options.domain}`;
278
- if (options.path) cookie += `; Path=${options.path}`;
279
- if (options.maxAge !== undefined) cookie += `; Max-Age=${options.maxAge}`;
280
- if (options.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
281
- if (options.httpOnly) cookie += "; HttpOnly";
282
- if (options.secure) cookie += "; Secure";
283
- if (options.sameSite) cookie += `; SameSite=${options.sameSite}`;
284
-
285
- return cookie;
286
- }
287
-
288
- /**
289
- * Mutable response holder - allows ctx.res to be updated after next() is called
290
- */
291
- export interface ResponseHolder {
292
- response: Response | null;
293
- }
294
-
295
170
  /**
296
171
  * Create middleware context
297
172
  *
298
173
  * Note: The implementation uses runtime values while the interface provides
299
174
  * compile-time type safety. The env/get/set types are resolved at call sites
300
- * via conditional types based on TEnv extending RouterEnv.
175
+ * via conditional types based on TEnv from createRouter<TBindings>().
301
176
  */
302
177
  export function createMiddlewareContext<TEnv>(
303
178
  request: Request,
304
179
  env: TEnv,
305
180
  params: Record<string, string>,
306
181
  variables: Record<string, unknown>,
307
- responseHolder: ResponseHolder
182
+ responseHolder: ResponseHolder,
183
+ reverse?: (
184
+ name: string,
185
+ params?: Record<string, string>,
186
+ search?: Record<string, unknown>,
187
+ ) => string,
308
188
  ): MiddlewareContext<TEnv> {
309
- const url = new URL(request.url);
310
- const cookieHeader = request.headers.get("Cookie");
311
- let parsedCookies: Record<string, string> | null = null;
312
-
189
+ const url = stripInternalParams(new URL(request.url));
190
+
191
+ // Track the initial response to detect pre/post-next() phase.
192
+ // Before next(): responseHolder.response === initialResponse (the stub).
193
+ // After next(): responseHolder.response is the real downstream response.
194
+ const initialResponse = responseHolder.response;
195
+ const isPreNext = () => responseHolder.response === initialResponse;
196
+
197
+ // Delegation strategy for RequestContext (reqCtx):
198
+ // - res getter: before next() returns shared reqCtx stub; after next() returns
199
+ // the real downstream response.
200
+ // - header(): before next() delegates to reqCtx; after next() writes to the
201
+ // real downstream response.
202
+ // Cookie operations are handled by the standalone cookies() function which
203
+ // delegates to the shared RequestContext internally.
313
204
  // The runtime implementation - types are enforced at call sites via MiddlewareContext<TEnv>
205
+ // Internal helper: resolve the current response (stub before next(), real after).
206
+ // Not exposed on the public MiddlewareContext type — use ctx.headers instead.
207
+ const getResponse = (): Response => {
208
+ if (isPreNext()) {
209
+ const reqCtx = _getRequestContext();
210
+ if (reqCtx) return reqCtx.res;
211
+ }
212
+ if (!responseHolder.response) {
213
+ throw new Error(
214
+ "Response is not available - responseHolder was not initialized",
215
+ );
216
+ }
217
+ return responseHolder.response;
218
+ };
219
+
220
+ // Capture reqCtx once: the request-scoped platform fields
221
+ // (originalUrl, executionContext, waitUntil) are immutable per request,
222
+ // so snapshotting beats re-reading ALS on every access. The lazy getters
223
+ // below (routeName, theme, setTheme) stay lazy because those can change
224
+ // during `await next()`.
225
+ const reqCtx = _getRequestContext();
314
226
  return {
315
227
  request,
316
228
  url,
229
+ originalUrl: reqCtx?.originalUrl ?? new URL(request.url),
317
230
  pathname: url.pathname,
318
231
  searchParams: url.searchParams,
319
232
  env: env as MiddlewareContext<TEnv>["env"],
320
233
  params,
321
-
322
- // res getter - returns the stub or real response (always available)
323
- get res(): Response {
324
- if (!responseHolder.response) {
325
- throw new Error(
326
- "ctx.res is not available - responseHolder was not initialized"
327
- );
328
- }
329
- return responseHolder.response;
234
+ executionContext: reqCtx?.executionContext,
235
+ waitUntil: reqCtx ? reqCtx.waitUntil.bind(reqCtx) : fireAndForgetWaitUntil,
236
+ // Getter: re-derives from request context on each access so that global
237
+ // middleware sees the matched route name after await next().
238
+ get routeName(): MiddlewareContext<TEnv>["routeName"] {
239
+ const reqCtx = _getRequestContext();
240
+ const raw = reqCtx?._routeName;
241
+ return (
242
+ raw && !isAutoGeneratedRouteName(raw) ? raw : undefined
243
+ ) as MiddlewareContext<TEnv>["routeName"];
330
244
  },
331
245
 
332
- // res setter - allows middleware to replace the response
333
- set res(response: Response) {
334
- responseHolder.response = response;
246
+ get headers(): Headers {
247
+ return getResponse().headers;
335
248
  },
336
249
 
337
- cookie(name: string): string | undefined {
338
- if (!parsedCookies) {
339
- parsedCookies = parseCookies(cookieHeader);
340
- }
341
- return parsedCookies[name];
342
- },
250
+ get: ((keyOrVar: any) =>
251
+ contextGet(variables, keyOrVar)) as MiddlewareContext<TEnv>["get"],
343
252
 
344
- cookies(): Record<string, string> {
345
- if (!parsedCookies) {
346
- parsedCookies = parseCookies(cookieHeader);
253
+ set: ((keyOrVar: any, value: unknown, options?: any) => {
254
+ contextSet(variables, keyOrVar, value, options);
255
+ }) as MiddlewareContext<TEnv>["set"],
256
+ header(name: string, value: string): void {
257
+ // Before next(): delegate to shared RequestContext stub
258
+ if (isPreNext()) {
259
+ const reqCtx = _getRequestContext();
260
+ if (reqCtx) {
261
+ reqCtx.header(name, value);
262
+ return;
263
+ }
347
264
  }
348
- return { ...parsedCookies };
349
- },
350
-
351
- setCookie(name: string, value: string, options?: CookieOptions): void {
265
+ // After next() or standalone: write to current response
352
266
  if (!responseHolder.response) {
353
267
  throw new Error(
354
- "ctx.setCookie() is not available - responseHolder was not initialized"
268
+ "ctx.header() is not available - responseHolder was not initialized",
355
269
  );
356
270
  }
357
- responseHolder.response.headers.append(
358
- "Set-Cookie",
359
- serializeCookie(name, value, options)
360
- );
271
+ responseHolder.response.headers.set(name, value);
361
272
  },
362
273
 
363
- deleteCookie(
364
- name: string,
365
- options?: Pick<CookieOptions, "domain" | "path">
366
- ): void {
367
- if (!responseHolder.response) {
274
+ get theme(): MiddlewareContext<TEnv>["theme"] {
275
+ return _getRequestContext()?.theme;
276
+ },
277
+
278
+ get setTheme(): MiddlewareContext<TEnv>["setTheme"] {
279
+ return _getRequestContext()?.setTheme;
280
+ },
281
+
282
+ setLocationState(entries) {
283
+ const reqCtx = _getRequestContext();
284
+ if (!reqCtx) {
368
285
  throw new Error(
369
- "ctx.deleteCookie() is not available - responseHolder was not initialized"
286
+ "setLocationState() is not available outside a request context",
370
287
  );
371
288
  }
372
- responseHolder.response.headers.append(
373
- "Set-Cookie",
374
- serializeCookie(name, "", { ...options, maxAge: 0 })
375
- );
289
+ reqCtx.setLocationState(entries);
376
290
  },
377
291
 
378
- get: ((key: string) => variables[key]) as MiddlewareContext<TEnv>["get"],
379
-
380
- set: ((key: string, value: unknown) => {
381
- variables[key] = value;
382
- }) as MiddlewareContext<TEnv>["set"],
383
-
384
- header(name: string, value: string): void {
385
- if (!responseHolder.response) {
292
+ reverse:
293
+ reverse ??
294
+ ((
295
+ name: string,
296
+ _params?: Record<string, string>,
297
+ _search?: Record<string, unknown>,
298
+ ) => {
386
299
  throw new Error(
387
- "ctx.header() is not available - responseHolder was not initialized"
300
+ `ctx.reverse(${JSON.stringify(name)}) is not available: no route map is bound to this middleware context.`,
388
301
  );
302
+ }),
303
+
304
+ debugPerformance(): void {
305
+ const reqCtx = _getRequestContext();
306
+ if (reqCtx) {
307
+ reqCtx._debugPerformance = true;
308
+ reqCtx._metricsStore ??= createMetricsStore(true);
389
309
  }
390
- responseHolder.response.headers.set(name, value);
391
310
  },
392
311
  };
393
312
  }
@@ -398,7 +317,7 @@ export function createMiddlewareContext<TEnv>(
398
317
  */
399
318
  export function matchMiddleware<TEnv>(
400
319
  pathname: string,
401
- entries: MiddlewareEntry<TEnv>[]
320
+ entries: MiddlewareEntry<TEnv>[],
402
321
  ): Array<{ entry: MiddlewareEntry<TEnv>; params: Record<string, string> }> {
403
322
  const matches: Array<{
404
323
  entry: MiddlewareEntry<TEnv>;
@@ -412,9 +331,16 @@ export function matchMiddleware<TEnv>(
412
331
  continue;
413
332
  }
414
333
 
415
- // Check if pathname matches
416
- if (entry.regex.test(pathname)) {
417
- const params = extractParams(pathname, entry.regex, entry.paramNames);
334
+ // Run the scope regex ONCE per entry. The old code ran test() then, on a
335
+ // hit, extractParams' match() — a second full pass over the same string for
336
+ // every matching entry on the per-request hot path. The regexes carry no
337
+ // `g` flag, so there is no lastIndex statefulness across this single match.
338
+ const m = pathname.match(entry.regex);
339
+ if (m) {
340
+ const params: Record<string, string> = {};
341
+ for (let i = 0; i < entry.paramNames.length; i++) {
342
+ params[entry.paramNames[i]] = safeDecodeURIComponent(m[i + 1] || "");
343
+ }
418
344
  matches.push({ entry, params });
419
345
  }
420
346
  }
@@ -422,15 +348,97 @@ export function matchMiddleware<TEnv>(
422
348
  return matches;
423
349
  }
424
350
 
351
+ // Set-Cookie is appended; for other headers stubOverridesNonCookie=true
352
+ // overwrites (chain ran to completion), false fills only missing slots (an
353
+ // explicit short-circuit Response's own headers win).
354
+ function mergeStubHeaders(
355
+ target: Headers,
356
+ stub: Headers,
357
+ stubOverridesNonCookie: boolean,
358
+ ): void {
359
+ stub.forEach((value, name) => {
360
+ // The reserved external-redirect marker is internal and never a trust
361
+ // signal; never copy a stub value (e.g. a stray ctx.header() call) onto a
362
+ // browser-facing response. The opt-in is the out-of-band brand.
363
+ if (name.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
364
+ if (name.toLowerCase() === "set-cookie") {
365
+ target.append(name, value);
366
+ } else if (stubOverridesNonCookie || !target.has(name)) {
367
+ target.set(name, value);
368
+ }
369
+ });
370
+ }
371
+
372
+ // Set-Cookie is deduped so a nested inner executeMiddleware that already merged
373
+ // the same reqCtx cookies does not duplicate them; other headers fill if missing.
374
+ function mergeReqCtxStub(
375
+ target: Headers,
376
+ reqCtx: ReturnType<typeof _getRequestContext>,
377
+ ): void {
378
+ if (!reqCtx) return;
379
+ const stubCookies = reqCtx.res.headers.getSetCookie();
380
+ if (stubCookies.length > 0) {
381
+ const existing = new Set(target.getSetCookie());
382
+ for (const cookie of stubCookies) {
383
+ if (!existing.has(cookie)) {
384
+ target.append("set-cookie", cookie);
385
+ }
386
+ }
387
+ }
388
+ reqCtx.res.headers.forEach((value, name) => {
389
+ // Never propagate the reserved external-redirect marker (see mergeStubHeaders).
390
+ if (name.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
391
+ if (name !== "set-cookie" && !target.has(name)) {
392
+ target.set(name, value);
393
+ }
394
+ });
395
+ }
396
+
397
+ // Clone `base` with stub headers merged into a fresh Headers (the clone keeps
398
+ // the body mutable for post-next() modifications). Set-Cookie is always
399
+ // appended; other headers obey stubOverridesNonCookie (see mergeStubHeaders).
400
+ // mergeReqCtx folds in RequestContext stub cookies/headers; the intercept
401
+ // short-circuit path passes false (its reqCtx headers are not merged here),
402
+ // which is the one deliberate divergence between the call sites.
403
+ function mergeResponse(
404
+ base: Response,
405
+ stub: Headers,
406
+ opts: { stubOverridesNonCookie: boolean; mergeReqCtx: boolean },
407
+ ): Response {
408
+ const mergedHeaders = new Headers(base.headers);
409
+ // The reserved external-redirect marker is never a trust signal and must never
410
+ // reach the browser. The guard strips it on 3xx redirects; strip it here too so
411
+ // a forged value cannot ride a non-3xx middleware response (which the 3xx-only
412
+ // guard would not touch) to the client. The opt-in is the out-of-band brand.
413
+ mergedHeaders.delete(EXTERNAL_REDIRECT_MARKER);
414
+ mergeStubHeaders(mergedHeaders, stub, opts.stubOverridesNonCookie);
415
+ if (opts.mergeReqCtx) {
416
+ mergeReqCtxStub(mergedHeaders, _getRequestContext());
417
+ }
418
+ const merged = new Response(base.body, {
419
+ status: base.status,
420
+ statusText: base.statusText,
421
+ headers: mergedHeaders,
422
+ });
423
+ // Transfer the out-of-band external-redirect brand across this rebuild: a
424
+ // middleware short-circuit `return redirect(url, { external: true })` reaches
425
+ // the open-redirect guard only after this merge, and the brand lives on the
426
+ // Response object, not in its headers.
427
+ if (isExternalRedirect(base)) {
428
+ markExternalRedirect(merged);
429
+ }
430
+ return merged;
431
+ }
432
+
425
433
  /**
426
434
  * Execute middleware chain
427
435
  *
428
436
  * Features:
429
437
  * - `await next()` returns actual Response
430
- * - `ctx.res` available after `await next()` (like Hono's `c.res`)
431
- * - `ctx.header()` shorthand for setting headers
432
- * - Forgiving: if middleware doesn't return, uses `ctx.res`
433
- * - Short-circuit: return Response to stop chain
438
+ * - `ctx.headers` available before and after `await next()`
439
+ * - `ctx.header()` shorthand for setting a single header
440
+ * - Forgiving: if middleware doesn't return, uses the downstream response
441
+ * - Short-circuit: return OR throw a Response to stop chain
434
442
  * - Error catching: try/catch around `next()` works
435
443
  */
436
444
  export async function executeMiddleware<TEnv>(
@@ -441,7 +449,12 @@ export async function executeMiddleware<TEnv>(
441
449
  request: Request,
442
450
  env: TEnv,
443
451
  variables: Record<string, any>,
444
- finalHandler: () => Promise<Response>
452
+ finalHandler: () => Promise<Response>,
453
+ reverse?: (
454
+ name: string,
455
+ params?: Record<string, string>,
456
+ search?: Record<string, unknown>,
457
+ ) => string,
445
458
  ): Promise<Response> {
446
459
  let index = 0;
447
460
 
@@ -455,50 +468,130 @@ export async function executeMiddleware<TEnv>(
455
468
  // End of chain - call actual RSC handler
456
469
  const response = await finalHandler();
457
470
 
458
- // Merge headers set on stub into the real response
459
- // Use append for Set-Cookie to preserve multiple cookies
460
- const mergedHeaders = new Headers(response.headers);
461
- stubResponse.headers.forEach((value, name) => {
462
- if (name.toLowerCase() === "set-cookie") {
463
- mergedHeaders.append(name, value);
464
- } else {
465
- mergedHeaders.set(name, value);
466
- }
467
- });
471
+ if (isWebSocketUpgradeResponse(response)) {
472
+ responseHolder.response = response;
473
+ return response;
474
+ }
468
475
 
469
- // Clone response with merged headers (mutable for post-next() modifications)
470
- responseHolder.response = new Response(response.body, {
471
- status: response.status,
472
- statusText: response.statusText,
473
- headers: mergedHeaders,
476
+ // Chain ran to completion: stub headers overwrite (stubOverridesNonCookie)
477
+ // and reqCtx stub headers are merged in.
478
+ responseHolder.response = mergeResponse(response, stubResponse.headers, {
479
+ stubOverridesNonCookie: true,
480
+ mergeReqCtx: true,
474
481
  });
475
482
 
476
483
  return responseHolder.response;
477
484
  }
478
485
 
486
+ const middlewareOrdinal = index;
479
487
  const { entry, params } = middlewares[index++];
480
488
  const ctx = createMiddlewareContext(
481
489
  request,
482
490
  env,
483
491
  params,
484
492
  variables,
485
- responseHolder
493
+ responseHolder,
494
+ reverse,
486
495
  );
496
+ const metricStart = performance.now();
497
+ const metricLabel = getMiddlewareMetricLabel(entry, middlewareOrdinal);
498
+ let middlewareFinished = false;
499
+ const finishMiddleware = () => {
500
+ if (!middlewareFinished) {
501
+ middlewareFinished = true;
502
+ appendMetric(
503
+ _getRequestContext()?._metricsStore,
504
+ `${metricLabel}:pre`,
505
+ metricStart,
506
+ performance.now() - metricStart,
507
+ MIDDLEWARE_METRIC_DEPTH,
508
+ );
509
+ }
510
+ };
487
511
 
488
- // Track if next() was called and capture its Promise
489
- // This handles the case where middleware calls next() synchronously without await
512
+ // Track if next() was called and capture its Promise.
513
+ // Guard against double-calling: a second call would re-enter the
514
+ // downstream chain and overwrite responseHolder.response.
490
515
  let nextPromise: Promise<Response> | null = null;
516
+ let nextResolvedAt: number | undefined;
491
517
  const wrappedNext = (): Promise<Response> => {
492
- nextPromise = next();
518
+ if (nextPromise) {
519
+ throw new Error(
520
+ `[@rangojs/router] Middleware called next() more than once.`,
521
+ );
522
+ }
523
+ finishMiddleware();
524
+ const downstream = next();
525
+ nextPromise = downstream.then(
526
+ (res) => {
527
+ nextResolvedAt = performance.now();
528
+ return res;
529
+ },
530
+ (err) => {
531
+ nextResolvedAt = performance.now();
532
+ throw err;
533
+ },
534
+ );
493
535
  return nextPromise;
494
536
  };
495
537
 
496
- const result = await entry.handler(ctx, wrappedNext);
538
+ // Wrap the middleware (including its downstream next() chain) in its span
539
+ // via the unified phase API. metric:false — the middleware's perf metric is
540
+ // its exclusive pre/post own-time, recorded directly above and below, finer
541
+ // than a single wrap. Spans nest by async context, so this onions
542
+ // middleware-over-middleware and the core handler underneath. Pass-through
543
+ // when neither surface is active.
544
+ let result: Response | void;
545
+ try {
546
+ result = await observePhase(PHASES.middleware(metricLabel), () =>
547
+ entry.handler(ctx, wrappedNext),
548
+ );
549
+ } catch (error) {
550
+ // Thrown Response is short-circuit control flow, not an error.
551
+ // Fall through to the `if (result instanceof Response)` branch below
552
+ // so stub headers and request-context cookies merge as they do for
553
+ // an explicit `return new Response(...)`. Real errors propagate.
554
+ if (error instanceof Response) {
555
+ result = error;
556
+ } else {
557
+ finishMiddleware();
558
+ throw error;
559
+ }
560
+ }
561
+ finishMiddleware();
562
+
563
+ // Record post-next() processing time when middleware did work after
564
+ // the downstream chain resolved (e.g. adding headers, logging).
565
+ if (nextResolvedAt !== undefined) {
566
+ const postDur = performance.now() - nextResolvedAt;
567
+ if (postDur > POST_METRIC_MIN_DURATION_MS) {
568
+ appendMetric(
569
+ _getRequestContext()?._metricsStore,
570
+ `${metricLabel}:post`,
571
+ nextResolvedAt,
572
+ postDur,
573
+ MIDDLEWARE_METRIC_DEPTH,
574
+ );
575
+ }
576
+ }
497
577
 
498
- // Explicit return takes precedence
578
+ // Explicit return takes precedence (middleware short-circuit).
579
+ // Merge stub headers (from ctx.header before this point) and
580
+ // RequestContext stub headers (from cookies().set()) into the
581
+ // returned Response so they are not lost.
499
582
  if (result instanceof Response) {
500
- responseHolder.response = result;
501
- return result;
583
+ if (isWebSocketUpgradeResponse(result)) {
584
+ responseHolder.response = result;
585
+ return result;
586
+ }
587
+ // Explicit short-circuit: the returned Response's own headers win
588
+ // (stubOverridesNonCookie=false); reqCtx stub headers still merge in.
589
+ const merged = mergeResponse(result, stubResponse.headers, {
590
+ stubOverridesNonCookie: false,
591
+ mergeReqCtx: true,
592
+ });
593
+ responseHolder.response = merged;
594
+ return merged;
502
595
  }
503
596
 
504
597
  // Warn about unexpected return values (non-Response, non-undefined)
@@ -507,7 +600,7 @@ export async function executeMiddleware<TEnv>(
507
600
  const fnName = entry.handler.name || "(anonymous)";
508
601
  console.warn(
509
602
  `[Middleware] "${fnName}" returned ${typeof result} instead of Response or undefined. ` +
510
- `This return value will be ignored. Did you mean to return a Response?`
603
+ `This return value will be ignored. Did you mean to return a Response?`,
511
604
  );
512
605
  }
513
606
 
@@ -524,7 +617,7 @@ export async function executeMiddleware<TEnv>(
524
617
  `Middleware must call next() or return a Response. ` +
525
618
  `Function: ${fnName}, Pattern: ${entry.pattern ?? "(all)"}
526
619
  Source: ${import.meta.env.DEV ? entry.handler.toString().slice(0, 200) : "(source hidden in production)"}`,
527
- { cause: { url: request.url, fn: entry.handler } }
620
+ { cause: { url: request.url, fn: entry.handler } },
528
621
  );
529
622
  };
530
623
 
@@ -536,64 +629,19 @@ export async function executeMiddleware<TEnv>(
536
629
  throw new Error("No response generated by middleware chain");
537
630
  }
538
631
 
539
- return finalResponse;
540
- }
541
-
542
- /**
543
- * Execute middleware for server actions
544
- *
545
- * Server actions can't return Response directly, but headers/cookies set
546
- * on ctx.res (from getRequestContext().res) will be merged into the final response.
547
- *
548
- * - Runs middleware for auth checks, variable setting, headers, cookies
549
- * - Throws if middleware returns Response (can't short-circuit server action)
550
- */
551
- export async function executeServerActionMiddleware<TEnv>(
552
- middlewares: MiddlewareFn<TEnv>[],
553
- request: Request,
554
- env: TEnv,
555
- params: Record<string, string>,
556
- variables: Record<string, any>,
557
- stubResponse: Response
558
- ): Promise<void> {
559
- if (middlewares.length === 0) {
560
- return;
632
+ // Final re-merge: capture any RequestContext stub headers added after the
633
+ // last merge point (e.g. cookies().set() called after await next()).
634
+ // The reqCtx stub may have already been partially merged during finalHandler
635
+ // or early-return paths; only append *new* Set-Cookie entries to avoid dupes.
636
+ //
637
+ // Skip for upgrade responses: upgrade headers are semantically immutable and
638
+ // set-cookie on an upgrade is not meaningful.
639
+ const reqCtx = _getRequestContext();
640
+ if (reqCtx && !isWebSocketUpgradeResponse(finalResponse)) {
641
+ mergeReqCtxStub(finalResponse.headers, reqCtx);
561
642
  }
562
643
 
563
- let index = 0;
564
- const responseHolder: ResponseHolder = { response: stubResponse };
565
-
566
- const next = async (): Promise<Response> => {
567
- if (index >= middlewares.length) {
568
- return stubResponse;
569
- }
570
-
571
- const middleware = middlewares[index++];
572
- const ctx = createMiddlewareContext(
573
- request,
574
- env,
575
- params,
576
- variables,
577
- responseHolder
578
- );
579
-
580
- const result = await middleware(ctx, next);
581
-
582
- // If middleware returned a Response, throw an error
583
- // Server actions can't short-circuit with a Response
584
- if (result instanceof Response) {
585
- throw new Error(
586
- `Loader middleware returned a Response (status: ${result.status}). ` +
587
- `Server actions cannot return Response. ` +
588
- `Use GET-based loader fetching for redirects, or throw an error instead.`
589
- );
590
- }
591
-
592
- return stubResponse;
593
- };
594
-
595
- await next();
596
- // Headers/cookies set on stubResponse will be merged by the caller
644
+ return finalResponse;
597
645
  }
598
646
 
599
647
  /**
@@ -601,7 +649,7 @@ export async function executeServerActionMiddleware<TEnv>(
601
649
  *
602
650
  * Intercepts use a shared stubResponse from the request context. This function:
603
651
  * - Runs middleware in sequence with a simple next() chain
604
- * - Returns Response if any middleware short-circuits (returns Response or redirects BEFORE next())
652
+ * - Returns Response if any middleware short-circuits (returns OR throws a Response, or redirects, BEFORE next())
605
653
  * - Returns null if all middleware calls next() - headers set after next() remain on stubResponse
606
654
  *
607
655
  * @param middlewares - Array of middleware functions
@@ -617,7 +665,12 @@ export async function executeInterceptMiddleware<TEnv>(
617
665
  env: TEnv,
618
666
  params: Record<string, string>,
619
667
  variables: Record<string, any>,
620
- stubResponse: Response
668
+ stubResponse: Response,
669
+ reverse?: (
670
+ name: string,
671
+ params?: Record<string, string>,
672
+ search?: Record<string, unknown>,
673
+ ) => string,
621
674
  ): Promise<Response | null> {
622
675
  if (middlewares.length === 0) {
623
676
  return null;
@@ -634,28 +687,57 @@ export async function executeInterceptMiddleware<TEnv>(
634
687
  return stubResponse;
635
688
  }
636
689
 
690
+ const ordinal = index;
637
691
  const middleware = middlewares[index++];
638
692
  const ctx = createMiddlewareContext(
639
693
  request,
640
694
  env,
641
695
  params,
642
696
  variables,
643
- responseHolder
697
+ responseHolder,
698
+ reverse,
644
699
  );
645
700
 
646
- const result = await middleware(ctx, next);
701
+ let nextCalled = false;
702
+ const guardedNext = (): Promise<Response> => {
703
+ if (nextCalled) {
704
+ throw new Error(
705
+ `[@rangojs/router] Intercept middleware called next() more than once.`,
706
+ );
707
+ }
708
+ nextCalled = true;
709
+ return next();
710
+ };
711
+
712
+ // Span-wrap each intercept middleware as rango.middleware (metric:false —
713
+ // intercept runs inside the render phase already metered by render:total, so
714
+ // it contributes a span but no separate perf metric). Bare MiddlewareFns
715
+ // have no pattern, so the label is scoped to "*" like a pattern-less entry.
716
+ const label = getMiddlewareMetricLabel(
717
+ { handler: middleware, pattern: null } as MiddlewareEntry<TEnv>,
718
+ ordinal,
719
+ );
720
+
721
+ let result: Response | void;
722
+ try {
723
+ result = await observePhase(PHASES.middleware(label), () =>
724
+ middleware(ctx, guardedNext),
725
+ );
726
+ } catch (error) {
727
+ // Thrown Response is short-circuit control flow, parity with the
728
+ // explicit-return path below. Real errors propagate.
729
+ if (error instanceof Response) {
730
+ result = error;
731
+ } else {
732
+ throw error;
733
+ }
734
+ }
647
735
 
648
736
  if (result instanceof Response) {
649
737
  earlyResponse = result;
650
738
  return result;
651
739
  }
652
740
 
653
- // Check if middleware replaced ctx.res with a different response
654
- if (responseHolder.response && responseHolder.response !== stubResponse) {
655
- earlyResponse = responseHolder.response;
656
- return earlyResponse;
657
- }
658
-
659
741
  return stubResponse;
660
742
  };
661
743
 
@@ -673,19 +755,13 @@ export async function executeInterceptMiddleware<TEnv>(
673
755
  });
674
756
 
675
757
  if (hasStubHeaders) {
676
- // Clone and merge headers from stub into early response
677
- const mergedHeaders = new Headers(response.headers);
678
- stubResponse.headers.forEach((value, name) => {
679
- if (name.toLowerCase() === "set-cookie") {
680
- mergedHeaders.append(name, value);
681
- } else {
682
- mergedHeaders.set(name, value);
683
- }
684
- });
685
- return new Response(response.body, {
686
- status: response.status,
687
- statusText: response.statusText,
688
- headers: mergedHeaders,
758
+ // Only fill in missing headers — the returned Response's explicit headers
759
+ // take precedence (stubOverridesNonCookie=false), matching executeMiddleware.
760
+ // mergeReqCtx=false: the intercept path deliberately does NOT merge reqCtx
761
+ // stub headers here (pinned by intercept-middleware-headers.test.ts).
762
+ return mergeResponse(response, stubResponse.headers, {
763
+ stubOverridesNonCookie: false,
764
+ mergeReqCtx: false,
689
765
  });
690
766
  }
691
767
  return response;
@@ -708,7 +784,12 @@ export async function executeLoaderMiddleware<TEnv>(
708
784
  env: TEnv,
709
785
  params: Record<string, string>,
710
786
  variables: Record<string, any>,
711
- finalHandler: () => Promise<Response>
787
+ finalHandler: () => Promise<Response>,
788
+ reverse?: (
789
+ name: string,
790
+ params?: Record<string, string>,
791
+ search?: Record<string, unknown>,
792
+ ) => string,
712
793
  ): Promise<Response> {
713
794
  if (middlewares.length === 0) {
714
795
  return finalHandler();
@@ -721,7 +802,6 @@ export async function executeLoaderMiddleware<TEnv>(
721
802
  regex: null,
722
803
  paramNames: [],
723
804
  handler,
724
- mountPrefix: null,
725
805
  } as MiddlewareEntry<TEnv>,
726
806
  params,
727
807
  }));
@@ -731,27 +811,11 @@ export async function executeLoaderMiddleware<TEnv>(
731
811
  request,
732
812
  env,
733
813
  variables,
734
- finalHandler
814
+ finalHandler,
815
+ reverse,
735
816
  );
736
817
  }
737
818
 
738
- /**
739
- * Entry type for middleware collection
740
- * Matches the shape of EntryData used in router.ts
741
- */
742
- export interface MiddlewareCollectableEntry {
743
- middleware?: MiddlewareFn<any, any>[];
744
- layout?: MiddlewareCollectableEntry[];
745
- }
746
-
747
- /**
748
- * Collected route middleware with params
749
- */
750
- export interface CollectedMiddleware {
751
- handler: MiddlewareFn<any, any>;
752
- params: Record<string, string>;
753
- }
754
-
755
819
  /**
756
820
  * Collect route-level middleware from an entry tree
757
821
  *
@@ -764,7 +828,7 @@ export interface CollectedMiddleware {
764
828
  */
765
829
  export function collectRouteMiddleware(
766
830
  entries: Iterable<MiddlewareCollectableEntry>,
767
- params: Record<string, string>
831
+ params: Record<string, string>,
768
832
  ): CollectedMiddleware[] {
769
833
  const result: CollectedMiddleware[] = [];
770
834