@rangojs/router 0.0.0-experimental.002d056c

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 (305) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +899 -0
  3. package/dist/bin/rango.js +1606 -0
  4. package/dist/vite/index.js +5153 -0
  5. package/package.json +177 -0
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +253 -0
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +112 -0
  11. package/skills/document-cache/SKILL.md +182 -0
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +704 -0
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +313 -0
  16. package/skills/layout/SKILL.md +310 -0
  17. package/skills/links/SKILL.md +239 -0
  18. package/skills/loader/SKILL.md +596 -0
  19. package/skills/middleware/SKILL.md +339 -0
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +305 -0
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +118 -0
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +385 -0
  26. package/skills/router-setup/SKILL.md +439 -0
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +79 -0
  29. package/skills/typesafety/SKILL.md +623 -0
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +273 -0
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/event-controller.ts +899 -0
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/index.ts +18 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +141 -0
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +134 -0
  42. package/src/browser/navigation-bridge.ts +638 -0
  43. package/src/browser/navigation-client.ts +261 -0
  44. package/src/browser/navigation-store.ts +806 -0
  45. package/src/browser/navigation-transaction.ts +297 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +582 -0
  48. package/src/browser/prefetch/cache.ts +206 -0
  49. package/src/browser/prefetch/fetch.ts +145 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +48 -0
  52. package/src/browser/prefetch/queue.ts +128 -0
  53. package/src/browser/rango-state.ts +112 -0
  54. package/src/browser/react/Link.tsx +368 -0
  55. package/src/browser/react/NavigationProvider.tsx +413 -0
  56. package/src/browser/react/ScrollRestoration.tsx +94 -0
  57. package/src/browser/react/context.ts +59 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +52 -0
  60. package/src/browser/react/location-state-shared.ts +162 -0
  61. package/src/browser/react/location-state.ts +107 -0
  62. package/src/browser/react/mount-context.ts +37 -0
  63. package/src/browser/react/nonce-context.ts +23 -0
  64. package/src/browser/react/shallow-equal.ts +27 -0
  65. package/src/browser/react/use-action.ts +218 -0
  66. package/src/browser/react/use-client-cache.ts +58 -0
  67. package/src/browser/react/use-handle.ts +162 -0
  68. package/src/browser/react/use-href.tsx +40 -0
  69. package/src/browser/react/use-link-status.ts +135 -0
  70. package/src/browser/react/use-mount.ts +31 -0
  71. package/src/browser/react/use-navigation.ts +99 -0
  72. package/src/browser/react/use-params.ts +65 -0
  73. package/src/browser/react/use-pathname.ts +47 -0
  74. package/src/browser/react/use-router.ts +63 -0
  75. package/src/browser/react/use-search-params.ts +56 -0
  76. package/src/browser/react/use-segments.ts +171 -0
  77. package/src/browser/response-adapter.ts +73 -0
  78. package/src/browser/rsc-router.tsx +464 -0
  79. package/src/browser/scroll-restoration.ts +397 -0
  80. package/src/browser/segment-reconciler.ts +216 -0
  81. package/src/browser/segment-structure-assert.ts +83 -0
  82. package/src/browser/server-action-bridge.ts +667 -0
  83. package/src/browser/shallow.ts +40 -0
  84. package/src/browser/types.ts +547 -0
  85. package/src/browser/validate-redirect-origin.ts +29 -0
  86. package/src/build/generate-manifest.ts +438 -0
  87. package/src/build/generate-route-types.ts +36 -0
  88. package/src/build/index.ts +35 -0
  89. package/src/build/route-trie.ts +265 -0
  90. package/src/build/route-types/ast-helpers.ts +25 -0
  91. package/src/build/route-types/ast-route-extraction.ts +98 -0
  92. package/src/build/route-types/codegen.ts +102 -0
  93. package/src/build/route-types/include-resolution.ts +411 -0
  94. package/src/build/route-types/param-extraction.ts +48 -0
  95. package/src/build/route-types/per-module-writer.ts +128 -0
  96. package/src/build/route-types/router-processing.ts +479 -0
  97. package/src/build/route-types/scan-filter.ts +78 -0
  98. package/src/build/runtime-discovery.ts +231 -0
  99. package/src/cache/background-task.ts +34 -0
  100. package/src/cache/cache-key-utils.ts +44 -0
  101. package/src/cache/cache-policy.ts +125 -0
  102. package/src/cache/cache-runtime.ts +338 -0
  103. package/src/cache/cache-scope.ts +382 -0
  104. package/src/cache/cf/cf-cache-store.ts +982 -0
  105. package/src/cache/cf/index.ts +29 -0
  106. package/src/cache/document-cache.ts +369 -0
  107. package/src/cache/handle-capture.ts +81 -0
  108. package/src/cache/handle-snapshot.ts +41 -0
  109. package/src/cache/index.ts +44 -0
  110. package/src/cache/memory-segment-store.ts +328 -0
  111. package/src/cache/profile-registry.ts +73 -0
  112. package/src/cache/read-through-swr.ts +134 -0
  113. package/src/cache/segment-codec.ts +256 -0
  114. package/src/cache/taint.ts +98 -0
  115. package/src/cache/types.ts +342 -0
  116. package/src/client.rsc.tsx +85 -0
  117. package/src/client.tsx +601 -0
  118. package/src/component-utils.ts +76 -0
  119. package/src/components/DefaultDocument.tsx +27 -0
  120. package/src/context-var.ts +86 -0
  121. package/src/debug.ts +243 -0
  122. package/src/default-error-boundary.tsx +88 -0
  123. package/src/deps/browser.ts +8 -0
  124. package/src/deps/html-stream-client.ts +2 -0
  125. package/src/deps/html-stream-server.ts +2 -0
  126. package/src/deps/rsc.ts +10 -0
  127. package/src/deps/ssr.ts +2 -0
  128. package/src/errors.ts +365 -0
  129. package/src/handle.ts +135 -0
  130. package/src/handles/MetaTags.tsx +246 -0
  131. package/src/handles/breadcrumbs.ts +66 -0
  132. package/src/handles/index.ts +7 -0
  133. package/src/handles/meta.ts +264 -0
  134. package/src/host/cookie-handler.ts +165 -0
  135. package/src/host/errors.ts +97 -0
  136. package/src/host/index.ts +53 -0
  137. package/src/host/pattern-matcher.ts +214 -0
  138. package/src/host/router.ts +352 -0
  139. package/src/host/testing.ts +79 -0
  140. package/src/host/types.ts +146 -0
  141. package/src/host/utils.ts +25 -0
  142. package/src/href-client.ts +222 -0
  143. package/src/index.rsc.ts +233 -0
  144. package/src/index.ts +277 -0
  145. package/src/internal-debug.ts +11 -0
  146. package/src/loader.rsc.ts +89 -0
  147. package/src/loader.ts +64 -0
  148. package/src/network-error-thrower.tsx +23 -0
  149. package/src/outlet-context.ts +15 -0
  150. package/src/outlet-provider.tsx +45 -0
  151. package/src/prerender/param-hash.ts +37 -0
  152. package/src/prerender/store.ts +185 -0
  153. package/src/prerender.ts +463 -0
  154. package/src/reverse.ts +330 -0
  155. package/src/root-error-boundary.tsx +289 -0
  156. package/src/route-content-wrapper.tsx +196 -0
  157. package/src/route-definition/dsl-helpers.ts +934 -0
  158. package/src/route-definition/helper-factories.ts +200 -0
  159. package/src/route-definition/helpers-types.ts +430 -0
  160. package/src/route-definition/index.ts +52 -0
  161. package/src/route-definition/redirect.ts +93 -0
  162. package/src/route-definition.ts +1 -0
  163. package/src/route-map-builder.ts +281 -0
  164. package/src/route-name.ts +53 -0
  165. package/src/route-types.ts +259 -0
  166. package/src/router/content-negotiation.ts +116 -0
  167. package/src/router/debug-manifest.ts +72 -0
  168. package/src/router/error-handling.ts +287 -0
  169. package/src/router/find-match.ts +160 -0
  170. package/src/router/handler-context.ts +451 -0
  171. package/src/router/intercept-resolution.ts +397 -0
  172. package/src/router/lazy-includes.ts +236 -0
  173. package/src/router/loader-resolution.ts +420 -0
  174. package/src/router/logging.ts +251 -0
  175. package/src/router/manifest.ts +269 -0
  176. package/src/router/match-api.ts +620 -0
  177. package/src/router/match-context.ts +266 -0
  178. package/src/router/match-handlers.ts +440 -0
  179. package/src/router/match-middleware/background-revalidation.ts +223 -0
  180. package/src/router/match-middleware/cache-lookup.ts +634 -0
  181. package/src/router/match-middleware/cache-store.ts +295 -0
  182. package/src/router/match-middleware/index.ts +81 -0
  183. package/src/router/match-middleware/intercept-resolution.ts +306 -0
  184. package/src/router/match-middleware/segment-resolution.ts +193 -0
  185. package/src/router/match-pipelines.ts +179 -0
  186. package/src/router/match-result.ts +219 -0
  187. package/src/router/metrics.ts +282 -0
  188. package/src/router/middleware-cookies.ts +55 -0
  189. package/src/router/middleware-types.ts +222 -0
  190. package/src/router/middleware.ts +749 -0
  191. package/src/router/pattern-matching.ts +563 -0
  192. package/src/router/prerender-match.ts +402 -0
  193. package/src/router/preview-match.ts +170 -0
  194. package/src/router/revalidation.ts +289 -0
  195. package/src/router/router-context.ts +320 -0
  196. package/src/router/router-interfaces.ts +452 -0
  197. package/src/router/router-options.ts +592 -0
  198. package/src/router/router-registry.ts +24 -0
  199. package/src/router/segment-resolution/fresh.ts +570 -0
  200. package/src/router/segment-resolution/helpers.ts +263 -0
  201. package/src/router/segment-resolution/loader-cache.ts +198 -0
  202. package/src/router/segment-resolution/revalidation.ts +1242 -0
  203. package/src/router/segment-resolution/static-store.ts +67 -0
  204. package/src/router/segment-resolution.ts +21 -0
  205. package/src/router/segment-wrappers.ts +291 -0
  206. package/src/router/telemetry-otel.ts +299 -0
  207. package/src/router/telemetry.ts +300 -0
  208. package/src/router/timeout.ts +148 -0
  209. package/src/router/trie-matching.ts +239 -0
  210. package/src/router/types.ts +170 -0
  211. package/src/router.ts +1006 -0
  212. package/src/rsc/handler-context.ts +45 -0
  213. package/src/rsc/handler.ts +1089 -0
  214. package/src/rsc/helpers.ts +198 -0
  215. package/src/rsc/index.ts +36 -0
  216. package/src/rsc/loader-fetch.ts +209 -0
  217. package/src/rsc/manifest-init.ts +86 -0
  218. package/src/rsc/nonce.ts +32 -0
  219. package/src/rsc/origin-guard.ts +141 -0
  220. package/src/rsc/progressive-enhancement.ts +379 -0
  221. package/src/rsc/response-error.ts +37 -0
  222. package/src/rsc/response-route-handler.ts +347 -0
  223. package/src/rsc/rsc-rendering.ts +237 -0
  224. package/src/rsc/runtime-warnings.ts +42 -0
  225. package/src/rsc/server-action.ts +348 -0
  226. package/src/rsc/ssr-setup.ts +128 -0
  227. package/src/rsc/types.ts +263 -0
  228. package/src/search-params.ts +230 -0
  229. package/src/segment-system.tsx +454 -0
  230. package/src/server/context.ts +591 -0
  231. package/src/server/cookie-store.ts +190 -0
  232. package/src/server/fetchable-loader-store.ts +37 -0
  233. package/src/server/handle-store.ts +308 -0
  234. package/src/server/loader-registry.ts +133 -0
  235. package/src/server/request-context.ts +920 -0
  236. package/src/server/root-layout.tsx +10 -0
  237. package/src/server/tsconfig.json +14 -0
  238. package/src/server.ts +51 -0
  239. package/src/ssr/index.tsx +365 -0
  240. package/src/static-handler.ts +114 -0
  241. package/src/theme/ThemeProvider.tsx +297 -0
  242. package/src/theme/ThemeScript.tsx +61 -0
  243. package/src/theme/constants.ts +62 -0
  244. package/src/theme/index.ts +48 -0
  245. package/src/theme/theme-context.ts +44 -0
  246. package/src/theme/theme-script.ts +155 -0
  247. package/src/theme/types.ts +182 -0
  248. package/src/theme/use-theme.ts +44 -0
  249. package/src/types/boundaries.ts +158 -0
  250. package/src/types/cache-types.ts +198 -0
  251. package/src/types/error-types.ts +192 -0
  252. package/src/types/global-namespace.ts +100 -0
  253. package/src/types/handler-context.ts +687 -0
  254. package/src/types/index.ts +88 -0
  255. package/src/types/loader-types.ts +183 -0
  256. package/src/types/route-config.ts +170 -0
  257. package/src/types/route-entry.ts +109 -0
  258. package/src/types/segments.ts +148 -0
  259. package/src/types.ts +1 -0
  260. package/src/urls/include-helper.ts +197 -0
  261. package/src/urls/index.ts +53 -0
  262. package/src/urls/path-helper-types.ts +339 -0
  263. package/src/urls/path-helper.ts +329 -0
  264. package/src/urls/pattern-types.ts +95 -0
  265. package/src/urls/response-types.ts +106 -0
  266. package/src/urls/type-extraction.ts +372 -0
  267. package/src/urls/urls-function.ts +98 -0
  268. package/src/urls.ts +1 -0
  269. package/src/use-loader.tsx +354 -0
  270. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  271. package/src/vite/discovery/discover-routers.ts +344 -0
  272. package/src/vite/discovery/prerender-collection.ts +385 -0
  273. package/src/vite/discovery/route-types-writer.ts +258 -0
  274. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  275. package/src/vite/discovery/state.ts +108 -0
  276. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  277. package/src/vite/index.ts +16 -0
  278. package/src/vite/plugin-types.ts +48 -0
  279. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  280. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  281. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  282. package/src/vite/plugins/expose-action-id.ts +363 -0
  283. package/src/vite/plugins/expose-id-utils.ts +287 -0
  284. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  285. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  286. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  287. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  288. package/src/vite/plugins/expose-ids/types.ts +45 -0
  289. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  290. package/src/vite/plugins/refresh-cmd.ts +65 -0
  291. package/src/vite/plugins/use-cache-transform.ts +323 -0
  292. package/src/vite/plugins/version-injector.ts +83 -0
  293. package/src/vite/plugins/version-plugin.ts +266 -0
  294. package/src/vite/plugins/version.d.ts +12 -0
  295. package/src/vite/plugins/virtual-entries.ts +123 -0
  296. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  297. package/src/vite/rango.ts +445 -0
  298. package/src/vite/router-discovery.ts +777 -0
  299. package/src/vite/utils/ast-handler-extract.ts +517 -0
  300. package/src/vite/utils/banner.ts +36 -0
  301. package/src/vite/utils/bundle-analysis.ts +137 -0
  302. package/src/vite/utils/manifest-utils.ts +70 -0
  303. package/src/vite/utils/package-resolution.ts +121 -0
  304. package/src/vite/utils/prerender-utils.ts +189 -0
  305. package/src/vite/utils/shared-utils.ts +169 -0
@@ -0,0 +1,749 @@
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
+
12
+ import { contextGet, contextSet } from "../context-var.js";
13
+ import type {
14
+ CollectedMiddleware,
15
+ MiddlewareCollectableEntry,
16
+ MiddlewareContext,
17
+ MiddlewareEntry,
18
+ MiddlewareFn,
19
+ ResponseHolder,
20
+ } from "./middleware-types.js";
21
+ import { _getRequestContext } from "../server/request-context.js";
22
+ import { isAutoGeneratedRouteName } from "../route-name.js";
23
+ import { appendMetric, createMetricsStore } from "./metrics.js";
24
+ import { stripInternalParams } from "./handler-context.js";
25
+
26
+ // Re-export types and cookie utilities for backward compatibility
27
+ export type {
28
+ CookieOptions,
29
+ CollectedMiddleware,
30
+ MiddlewareCollectableEntry,
31
+ MiddlewareContext,
32
+ MiddlewareEntry,
33
+ MiddlewareFn,
34
+ ResponseHolder,
35
+ } from "./middleware-types.js";
36
+ export { parseCookies, serializeCookie } from "./middleware-cookies.js";
37
+
38
+ const MIDDLEWARE_METRIC_DEPTH = 1;
39
+ /** Ignore post-next() durations below this threshold (measurement noise). */
40
+ const POST_METRIC_MIN_DURATION_MS = 0.01;
41
+
42
+ function getMiddlewareMetricBase<TEnv>(
43
+ entry: MiddlewareEntry<TEnv>,
44
+ ordinal: number,
45
+ ): string {
46
+ const handlerName = entry.handler.name?.trim();
47
+ const scope = entry.pattern ?? "*";
48
+
49
+ if (handlerName) {
50
+ return `${handlerName}@${scope}`;
51
+ }
52
+
53
+ return `${scope}#${ordinal + 1}`;
54
+ }
55
+
56
+ function getMiddlewareMetricLabel<TEnv>(
57
+ entry: MiddlewareEntry<TEnv>,
58
+ ordinal: number,
59
+ ): string {
60
+ return `middleware:${getMiddlewareMetricBase(entry, ordinal)}`;
61
+ }
62
+
63
+ /**
64
+ * Parse a route pattern into regex and param names
65
+ * Supports: *, /path, /path/*, /path/:param, /path/:param/*
66
+ */
67
+ export function parsePattern(pattern: string): {
68
+ regex: RegExp;
69
+ paramNames: string[];
70
+ } {
71
+ if (pattern === "*") {
72
+ return { regex: /^.*$/, paramNames: [] };
73
+ }
74
+
75
+ const paramNames: string[] = [];
76
+ let regexStr = "^";
77
+
78
+ const parts = pattern.split("/").filter(Boolean);
79
+
80
+ for (let i = 0; i < parts.length; i++) {
81
+ const part = parts[i];
82
+
83
+ if (part === "*") {
84
+ // Wildcard - match rest of path
85
+ regexStr += "(?:/.*)?";
86
+ } else if (part.startsWith(":")) {
87
+ // Param
88
+ const paramName = part.slice(1);
89
+ paramNames.push(paramName);
90
+ regexStr += "/([^/]+)";
91
+ } else {
92
+ // Literal
93
+ regexStr += "/" + escapeRegex(part);
94
+ }
95
+ }
96
+
97
+ // If pattern doesn't end with *, match exact or with trailing segments
98
+ if (!pattern.endsWith("*")) {
99
+ regexStr += "/?$";
100
+ } else {
101
+ regexStr += "$";
102
+ }
103
+
104
+ return { regex: new RegExp(regexStr), paramNames };
105
+ }
106
+
107
+ /**
108
+ * Escape special regex characters
109
+ */
110
+ function escapeRegex(str: string): string {
111
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
112
+ }
113
+
114
+ /**
115
+ * Extract params from a pathname using a pattern's regex and param names
116
+ */
117
+ export function extractParams(
118
+ pathname: string,
119
+ regex: RegExp,
120
+ paramNames: string[],
121
+ ): Record<string, string> {
122
+ const match = pathname.match(regex);
123
+ if (!match) return {};
124
+
125
+ const params: Record<string, string> = {};
126
+ for (let i = 0; i < paramNames.length; i++) {
127
+ params[paramNames[i]] = match[i + 1] || "";
128
+ }
129
+ return params;
130
+ }
131
+
132
+ /**
133
+ * Create middleware context
134
+ *
135
+ * Note: The implementation uses runtime values while the interface provides
136
+ * compile-time type safety. The env/get/set types are resolved at call sites
137
+ * via conditional types based on TEnv from createRouter<TBindings>().
138
+ */
139
+ export function createMiddlewareContext<TEnv>(
140
+ request: Request,
141
+ env: TEnv,
142
+ params: Record<string, string>,
143
+ variables: Record<string, unknown>,
144
+ responseHolder: ResponseHolder,
145
+ reverse?: (
146
+ name: string,
147
+ params?: Record<string, string>,
148
+ search?: Record<string, unknown>,
149
+ ) => string,
150
+ ): MiddlewareContext<TEnv> {
151
+ const url = stripInternalParams(new URL(request.url));
152
+
153
+ // Track the initial response to detect pre/post-next() phase.
154
+ // Before next(): responseHolder.response === initialResponse (the stub).
155
+ // After next(): responseHolder.response is the real downstream response.
156
+ const initialResponse = responseHolder.response;
157
+ const isPreNext = () => responseHolder.response === initialResponse;
158
+
159
+ // Delegation strategy for RequestContext (reqCtx):
160
+ // - res getter: before next() returns shared reqCtx stub; after next() returns
161
+ // the real downstream response.
162
+ // - header(): before next() delegates to reqCtx; after next() writes to the
163
+ // real downstream response.
164
+ // Cookie operations are handled by the standalone cookies() function which
165
+ // delegates to the shared RequestContext internally.
166
+ // The runtime implementation - types are enforced at call sites via MiddlewareContext<TEnv>
167
+ // Internal helper: resolve the current response (stub before next(), real after).
168
+ // Not exposed on the public MiddlewareContext type — use ctx.headers instead.
169
+ const getResponse = (): Response => {
170
+ if (isPreNext()) {
171
+ const reqCtx = _getRequestContext();
172
+ if (reqCtx) return reqCtx.res;
173
+ }
174
+ if (!responseHolder.response) {
175
+ throw new Error(
176
+ "Response is not available - responseHolder was not initialized",
177
+ );
178
+ }
179
+ return responseHolder.response;
180
+ };
181
+
182
+ return {
183
+ request,
184
+ url,
185
+ originalUrl: new URL(request.url),
186
+ pathname: url.pathname,
187
+ searchParams: url.searchParams,
188
+ env: env as MiddlewareContext<TEnv>["env"],
189
+ params,
190
+ // Getter: re-derives from request context on each access so that global
191
+ // middleware sees the matched route name after await next().
192
+ get routeName(): MiddlewareContext<TEnv>["routeName"] {
193
+ const reqCtx = _getRequestContext();
194
+ const raw = reqCtx?._routeName;
195
+ return (
196
+ raw && !isAutoGeneratedRouteName(raw) ? raw : undefined
197
+ ) as MiddlewareContext<TEnv>["routeName"];
198
+ },
199
+
200
+ get headers(): Headers {
201
+ return getResponse().headers;
202
+ },
203
+
204
+ get: ((keyOrVar: any) =>
205
+ contextGet(variables, keyOrVar)) as MiddlewareContext<TEnv>["get"],
206
+
207
+ set: ((keyOrVar: any, value: unknown) => {
208
+ contextSet(variables, keyOrVar, value);
209
+ }) as MiddlewareContext<TEnv>["set"],
210
+
211
+ var: variables as MiddlewareContext<TEnv>["var"],
212
+
213
+ header(name: string, value: string): void {
214
+ // Before next(): delegate to shared RequestContext stub
215
+ if (isPreNext()) {
216
+ const reqCtx = _getRequestContext();
217
+ if (reqCtx) {
218
+ reqCtx.header(name, value);
219
+ return;
220
+ }
221
+ }
222
+ // After next() or standalone: write to current response
223
+ if (!responseHolder.response) {
224
+ throw new Error(
225
+ "ctx.header() is not available - responseHolder was not initialized",
226
+ );
227
+ }
228
+ responseHolder.response.headers.set(name, value);
229
+ },
230
+
231
+ get theme(): MiddlewareContext<TEnv>["theme"] {
232
+ return _getRequestContext()?.theme;
233
+ },
234
+
235
+ get setTheme(): MiddlewareContext<TEnv>["setTheme"] {
236
+ return _getRequestContext()?.setTheme;
237
+ },
238
+
239
+ setLocationState(entries) {
240
+ const reqCtx = _getRequestContext();
241
+ if (!reqCtx) {
242
+ throw new Error(
243
+ "setLocationState() is not available outside a request context",
244
+ );
245
+ }
246
+ reqCtx.setLocationState(entries);
247
+ },
248
+
249
+ reverse:
250
+ reverse ??
251
+ ((name: string) => {
252
+ throw new Error(
253
+ `ctx.reverse() is not available - route map was not provided to middleware context`,
254
+ );
255
+ }),
256
+
257
+ debugPerformance(): void {
258
+ const reqCtx = _getRequestContext();
259
+ if (reqCtx) {
260
+ reqCtx._debugPerformance = true;
261
+ reqCtx._metricsStore ??= createMetricsStore(true);
262
+ }
263
+ },
264
+ };
265
+ }
266
+
267
+ /**
268
+ * Match middleware entries against a pathname
269
+ * Returns entries that match, with extracted params
270
+ */
271
+ export function matchMiddleware<TEnv>(
272
+ pathname: string,
273
+ entries: MiddlewareEntry<TEnv>[],
274
+ ): Array<{ entry: MiddlewareEntry<TEnv>; params: Record<string, string> }> {
275
+ const matches: Array<{
276
+ entry: MiddlewareEntry<TEnv>;
277
+ params: Record<string, string>;
278
+ }> = [];
279
+
280
+ for (const entry of entries) {
281
+ // No pattern = matches all (global middleware without pattern)
282
+ if (!entry.regex) {
283
+ matches.push({ entry, params: {} });
284
+ continue;
285
+ }
286
+
287
+ // Check if pathname matches
288
+ if (entry.regex.test(pathname)) {
289
+ const params = extractParams(pathname, entry.regex, entry.paramNames);
290
+ matches.push({ entry, params });
291
+ }
292
+ }
293
+
294
+ return matches;
295
+ }
296
+
297
+ /**
298
+ * Execute middleware chain
299
+ *
300
+ * Features:
301
+ * - `await next()` returns actual Response
302
+ * - `ctx.headers` available before and after `await next()`
303
+ * - `ctx.header()` shorthand for setting a single header
304
+ * - Forgiving: if middleware doesn't return, uses the downstream response
305
+ * - Short-circuit: return Response to stop chain
306
+ * - Error catching: try/catch around `next()` works
307
+ */
308
+ export async function executeMiddleware<TEnv>(
309
+ middlewares: Array<{
310
+ entry: MiddlewareEntry<TEnv>;
311
+ params: Record<string, string>;
312
+ }>,
313
+ request: Request,
314
+ env: TEnv,
315
+ variables: Record<string, any>,
316
+ finalHandler: () => Promise<Response>,
317
+ reverse?: (
318
+ name: string,
319
+ params?: Record<string, string>,
320
+ search?: Record<string, unknown>,
321
+ ) => string,
322
+ ): Promise<Response> {
323
+ let index = 0;
324
+
325
+ // Create a stub response that's available immediately
326
+ // This allows middleware to set headers/cookies before calling next()
327
+ const stubResponse = new Response(null, { status: 200 });
328
+ const responseHolder: ResponseHolder = { response: stubResponse };
329
+
330
+ const next = async (): Promise<Response> => {
331
+ if (index >= middlewares.length) {
332
+ // End of chain - call actual RSC handler
333
+ const response = await finalHandler();
334
+
335
+ // Merge headers set on stub into the real response.
336
+ // Use append for Set-Cookie to preserve multiple cookies.
337
+ const mergedHeaders = new Headers(response.headers);
338
+ stubResponse.headers.forEach((value, name) => {
339
+ if (name.toLowerCase() === "set-cookie") {
340
+ mergedHeaders.append(name, value);
341
+ } else {
342
+ mergedHeaders.set(name, value);
343
+ }
344
+ });
345
+ // Also merge shared RequestContext stub (cookies written via cookies().set()).
346
+ // Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
347
+ // may have already merged the same reqCtx cookies into the response.
348
+ const reqCtx = _getRequestContext();
349
+ if (reqCtx) {
350
+ const stubCookies = reqCtx.res.headers.getSetCookie();
351
+ if (stubCookies.length > 0) {
352
+ const existing = new Set(mergedHeaders.getSetCookie());
353
+ for (const cookie of stubCookies) {
354
+ if (!existing.has(cookie)) {
355
+ mergedHeaders.append("set-cookie", cookie);
356
+ }
357
+ }
358
+ }
359
+ reqCtx.res.headers.forEach((value, name) => {
360
+ if (name !== "set-cookie" && !mergedHeaders.has(name)) {
361
+ mergedHeaders.set(name, value);
362
+ }
363
+ });
364
+ }
365
+
366
+ // Clone response with merged headers (mutable for post-next() modifications)
367
+ responseHolder.response = new Response(response.body, {
368
+ status: response.status,
369
+ statusText: response.statusText,
370
+ headers: mergedHeaders,
371
+ });
372
+
373
+ return responseHolder.response;
374
+ }
375
+
376
+ const middlewareOrdinal = index;
377
+ const { entry, params } = middlewares[index++];
378
+ const ctx = createMiddlewareContext(
379
+ request,
380
+ env,
381
+ params,
382
+ variables,
383
+ responseHolder,
384
+ reverse,
385
+ );
386
+ const metricStart = performance.now();
387
+ const metricLabel = getMiddlewareMetricLabel(entry, middlewareOrdinal);
388
+ let middlewareFinished = false;
389
+ const finishMiddleware = () => {
390
+ if (!middlewareFinished) {
391
+ middlewareFinished = true;
392
+ appendMetric(
393
+ _getRequestContext()?._metricsStore,
394
+ `${metricLabel}:pre`,
395
+ metricStart,
396
+ performance.now() - metricStart,
397
+ MIDDLEWARE_METRIC_DEPTH,
398
+ );
399
+ }
400
+ };
401
+
402
+ // Track if next() was called and capture its Promise.
403
+ // Guard against double-calling: a second call would re-enter the
404
+ // downstream chain and overwrite responseHolder.response.
405
+ let nextPromise: Promise<Response> | null = null;
406
+ let nextResolvedAt: number | undefined;
407
+ const wrappedNext = (): Promise<Response> => {
408
+ if (nextPromise) {
409
+ throw new Error(
410
+ `[@rangojs/router] Middleware called next() more than once.`,
411
+ );
412
+ }
413
+ finishMiddleware();
414
+ const downstream = next();
415
+ nextPromise = downstream.then(
416
+ (res) => {
417
+ nextResolvedAt = performance.now();
418
+ return res;
419
+ },
420
+ (err) => {
421
+ nextResolvedAt = performance.now();
422
+ throw err;
423
+ },
424
+ );
425
+ return nextPromise;
426
+ };
427
+
428
+ let result: Response | void;
429
+ try {
430
+ result = await entry.handler(ctx, wrappedNext);
431
+ } catch (error) {
432
+ finishMiddleware();
433
+ throw error;
434
+ }
435
+ finishMiddleware();
436
+
437
+ // Record post-next() processing time when middleware did work after
438
+ // the downstream chain resolved (e.g. adding headers, logging).
439
+ if (nextResolvedAt !== undefined) {
440
+ const postDur = performance.now() - nextResolvedAt;
441
+ if (postDur > POST_METRIC_MIN_DURATION_MS) {
442
+ appendMetric(
443
+ _getRequestContext()?._metricsStore,
444
+ `${metricLabel}:post`,
445
+ nextResolvedAt,
446
+ postDur,
447
+ MIDDLEWARE_METRIC_DEPTH,
448
+ );
449
+ }
450
+ }
451
+
452
+ // Explicit return takes precedence (middleware short-circuit).
453
+ // Merge stub headers (from ctx.header before this point) and
454
+ // RequestContext stub headers (from ctx.setCookie) into the
455
+ // returned Response so they are not lost.
456
+ if (result instanceof Response) {
457
+ const mergedHeaders = new Headers(result.headers);
458
+ stubResponse.headers.forEach((value, name) => {
459
+ if (name.toLowerCase() === "set-cookie") {
460
+ mergedHeaders.append(name, value);
461
+ } else if (!mergedHeaders.has(name)) {
462
+ mergedHeaders.set(name, value);
463
+ }
464
+ });
465
+ // Also merge shared RequestContext stub (cookies written via setCookie).
466
+ // Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
467
+ // may have already merged the same reqCtx cookies into the response.
468
+ const reqCtx = _getRequestContext();
469
+ if (reqCtx) {
470
+ const stubCookies = reqCtx.res.headers.getSetCookie();
471
+ if (stubCookies.length > 0) {
472
+ const existing = new Set(mergedHeaders.getSetCookie());
473
+ for (const cookie of stubCookies) {
474
+ if (!existing.has(cookie)) {
475
+ mergedHeaders.append("set-cookie", cookie);
476
+ }
477
+ }
478
+ }
479
+ reqCtx.res.headers.forEach((value, name) => {
480
+ if (name !== "set-cookie" && !mergedHeaders.has(name)) {
481
+ mergedHeaders.set(name, value);
482
+ }
483
+ });
484
+ }
485
+ const merged = new Response(result.body, {
486
+ status: result.status,
487
+ statusText: result.statusText,
488
+ headers: mergedHeaders,
489
+ });
490
+ responseHolder.response = merged;
491
+ return merged;
492
+ }
493
+
494
+ // Warn about unexpected return values (non-Response, non-undefined)
495
+ // This catches common mistakes like returning strings or objects
496
+ if (result !== undefined) {
497
+ const fnName = entry.handler.name || "(anonymous)";
498
+ console.warn(
499
+ `[Middleware] "${fnName}" returned ${typeof result} instead of Response or undefined. ` +
500
+ `This return value will be ignored. Did you mean to return a Response?`,
501
+ );
502
+ }
503
+
504
+ // If middleware called next(), await it and return the response
505
+ if (nextPromise) {
506
+ await nextPromise;
507
+ return responseHolder.response!;
508
+ }
509
+
510
+ // Middleware didn't call next() and didn't return a Response - that's an error
511
+ // (Note: responseHolder.response is the stub, but we require next() or explicit return)
512
+ const fnName = entry.handler.name || "(anonymous)";
513
+ throw new Error(
514
+ `Middleware must call next() or return a Response. ` +
515
+ `Function: ${fnName}, Pattern: ${entry.pattern ?? "(all)"}
516
+ Source: ${import.meta.env.DEV ? entry.handler.toString().slice(0, 200) : "(source hidden in production)"}`,
517
+ { cause: { url: request.url, fn: entry.handler } },
518
+ );
519
+ };
520
+
521
+ await next();
522
+
523
+ // Use the final response from responseHolder (may have been modified by middleware)
524
+ const finalResponse = responseHolder.response;
525
+ if (!finalResponse) {
526
+ throw new Error("No response generated by middleware chain");
527
+ }
528
+
529
+ // Final re-merge: capture any RequestContext stub headers added after the
530
+ // last merge point (e.g. cookies().set() called after await next()).
531
+ // The reqCtx stub may have already been partially merged during finalHandler
532
+ // or early-return paths; only append *new* Set-Cookie entries to avoid dupes.
533
+ const reqCtx = _getRequestContext();
534
+ if (reqCtx) {
535
+ const stubCookies = reqCtx.res.headers.getSetCookie();
536
+ if (stubCookies.length > 0) {
537
+ const existingCookies = new Set(finalResponse.headers.getSetCookie());
538
+ for (const cookie of stubCookies) {
539
+ if (!existingCookies.has(cookie)) {
540
+ finalResponse.headers.append("set-cookie", cookie);
541
+ }
542
+ }
543
+ }
544
+ // Fill in non-cookie headers that aren't already on the response
545
+ reqCtx.res.headers.forEach((value, name) => {
546
+ if (name !== "set-cookie" && !finalResponse.headers.has(name)) {
547
+ finalResponse.headers.set(name, value);
548
+ }
549
+ });
550
+ }
551
+
552
+ return finalResponse;
553
+ }
554
+
555
+ /**
556
+ * Execute middleware for intercepts (simplified execution)
557
+ *
558
+ * Intercepts use a shared stubResponse from the request context. This function:
559
+ * - Runs middleware in sequence with a simple next() chain
560
+ * - Returns Response if any middleware short-circuits (returns Response or redirects BEFORE next())
561
+ * - Returns null if all middleware calls next() - headers set after next() remain on stubResponse
562
+ *
563
+ * @param middlewares - Array of middleware functions
564
+ * @param request - Original request
565
+ * @param env - Environment bindings
566
+ * @param params - Route params
567
+ * @param variables - Shared variables object
568
+ * @param stubResponse - Response from request context for collecting headers/cookies
569
+ */
570
+ export async function executeInterceptMiddleware<TEnv>(
571
+ middlewares: MiddlewareFn<TEnv>[],
572
+ request: Request,
573
+ env: TEnv,
574
+ params: Record<string, string>,
575
+ variables: Record<string, any>,
576
+ stubResponse: Response,
577
+ reverse?: (
578
+ name: string,
579
+ params?: Record<string, string>,
580
+ search?: Record<string, unknown>,
581
+ ) => string,
582
+ ): Promise<Response | null> {
583
+ if (middlewares.length === 0) {
584
+ return null;
585
+ }
586
+
587
+ let index = 0;
588
+ let earlyResponse: Response | null = null;
589
+
590
+ // Use provided stubResponse - headers/cookies set here will be merged by the caller
591
+ const responseHolder: ResponseHolder = { response: stubResponse };
592
+
593
+ const next = async (): Promise<Response> => {
594
+ if (index >= middlewares.length || earlyResponse) {
595
+ return stubResponse;
596
+ }
597
+
598
+ const middleware = middlewares[index++];
599
+ const ctx = createMiddlewareContext(
600
+ request,
601
+ env,
602
+ params,
603
+ variables,
604
+ responseHolder,
605
+ reverse,
606
+ );
607
+
608
+ let nextCalled = false;
609
+ const guardedNext = (): Promise<Response> => {
610
+ if (nextCalled) {
611
+ throw new Error(
612
+ `[@rangojs/router] Intercept middleware called next() more than once.`,
613
+ );
614
+ }
615
+ nextCalled = true;
616
+ return next();
617
+ };
618
+
619
+ const result = await middleware(ctx, guardedNext);
620
+
621
+ if (result instanceof Response) {
622
+ earlyResponse = result;
623
+ return result;
624
+ }
625
+
626
+ return stubResponse;
627
+ };
628
+
629
+ await next();
630
+
631
+ // Return early response if middleware short-circuited (returned Response BEFORE next())
632
+ if (earlyResponse) {
633
+ // Capture in const for TypeScript narrowing (earlyResponse is `let` which loses narrowing in callbacks)
634
+ const response: Response = earlyResponse;
635
+
636
+ // Merge any headers/cookies set on stub into the early response
637
+ let hasStubHeaders = false;
638
+ stubResponse.headers.forEach(() => {
639
+ hasStubHeaders = true;
640
+ });
641
+
642
+ if (hasStubHeaders) {
643
+ // Clone and merge headers from stub into early response.
644
+ // Only fill in missing headers — the returned Response's explicit
645
+ // headers take precedence, matching executeMiddleware behavior.
646
+ const mergedHeaders = new Headers(response.headers);
647
+ stubResponse.headers.forEach((value, name) => {
648
+ if (name.toLowerCase() === "set-cookie") {
649
+ mergedHeaders.append(name, value);
650
+ } else if (!mergedHeaders.has(name)) {
651
+ mergedHeaders.set(name, value);
652
+ }
653
+ });
654
+ return new Response(response.body, {
655
+ status: response.status,
656
+ statusText: response.statusText,
657
+ headers: mergedHeaders,
658
+ });
659
+ }
660
+ return response;
661
+ }
662
+
663
+ // All middleware completed without short-circuit
664
+ // Headers/cookies set on stubResponse will be merged into the final response by the caller
665
+ return null;
666
+ }
667
+
668
+ /**
669
+ * Execute middleware chain for loaders (simpler signature)
670
+ *
671
+ * Takes an array of MiddlewareFn directly (no entry wrapper needed).
672
+ * Used for fetchable loader middleware execution.
673
+ */
674
+ export async function executeLoaderMiddleware<TEnv>(
675
+ middlewares: MiddlewareFn<TEnv>[],
676
+ request: Request,
677
+ env: TEnv,
678
+ params: Record<string, string>,
679
+ variables: Record<string, any>,
680
+ finalHandler: () => Promise<Response>,
681
+ reverse?: (
682
+ name: string,
683
+ params?: Record<string, string>,
684
+ search?: Record<string, unknown>,
685
+ ) => string,
686
+ ): Promise<Response> {
687
+ if (middlewares.length === 0) {
688
+ return finalHandler();
689
+ }
690
+
691
+ // Convert to the format executeMiddleware expects
692
+ const middlewareEntries = middlewares.map((handler) => ({
693
+ entry: {
694
+ pattern: null,
695
+ regex: null,
696
+ paramNames: [],
697
+ handler,
698
+ mountPrefix: null,
699
+ } as MiddlewareEntry<TEnv>,
700
+ params,
701
+ }));
702
+
703
+ return executeMiddleware(
704
+ middlewareEntries,
705
+ request,
706
+ env,
707
+ variables,
708
+ finalHandler,
709
+ reverse,
710
+ );
711
+ }
712
+
713
+ /**
714
+ * Collect route-level middleware from an entry tree
715
+ *
716
+ * Recursively collects middleware from entries and their orphan layouts.
717
+ * Used by match(), matchPartial(), and previewMatch() to gather route middleware.
718
+ *
719
+ * @param entries - Iterable of entries to collect middleware from (typically from traverseBack)
720
+ * @param params - Route params to attach to each middleware entry
721
+ * @returns Array of collected middleware with params
722
+ */
723
+ export function collectRouteMiddleware(
724
+ entries: Iterable<MiddlewareCollectableEntry>,
725
+ params: Record<string, string>,
726
+ ): CollectedMiddleware[] {
727
+ const result: CollectedMiddleware[] = [];
728
+
729
+ const collect = (entry: MiddlewareCollectableEntry): void => {
730
+ // Collect entry's own middleware
731
+ if (entry.middleware && entry.middleware.length > 0) {
732
+ for (const mw of entry.middleware) {
733
+ result.push({ handler: mw, params });
734
+ }
735
+ }
736
+ // Collect middleware from orphan layouts (recursive)
737
+ if (entry.layout && entry.layout.length > 0) {
738
+ for (const orphan of entry.layout) {
739
+ collect(orphan);
740
+ }
741
+ }
742
+ };
743
+
744
+ for (const entry of entries) {
745
+ collect(entry);
746
+ }
747
+
748
+ return result;
749
+ }