@rangojs/router 0.0.0-experimental.0f44aca1

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 +5 -0
  2. package/README.md +899 -0
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +5214 -0
  5. package/package.json +176 -0
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +220 -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 +645 -0
  43. package/src/browser/navigation-client.ts +215 -0
  44. package/src/browser/navigation-store.ts +806 -0
  45. package/src/browser/navigation-transaction.ts +295 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +550 -0
  48. package/src/browser/prefetch/cache.ts +146 -0
  49. package/src/browser/prefetch/fetch.ts +135 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +42 -0
  52. package/src/browser/prefetch/queue.ts +88 -0
  53. package/src/browser/rango-state.ts +112 -0
  54. package/src/browser/react/Link.tsx +360 -0
  55. package/src/browser/react/NavigationProvider.tsx +386 -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 +431 -0
  79. package/src/browser/scroll-restoration.ts +400 -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 +538 -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 +469 -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 +540 -0
  105. package/src/cache/cf/index.ts +25 -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 +43 -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 +275 -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 +158 -0
  170. package/src/router/handler-context.ts +451 -0
  171. package/src/router/intercept-resolution.ts +395 -0
  172. package/src/router/lazy-includes.ts +234 -0
  173. package/src/router/loader-resolution.ts +420 -0
  174. package/src/router/logging.ts +248 -0
  175. package/src/router/manifest.ts +267 -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 +192 -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 +748 -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 +316 -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 +1239 -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 +289 -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 +1002 -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 +235 -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 +914 -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 +102 -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 +110 -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 +131 -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 +365 -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 +254 -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 +510 -0
  298. package/src/vite/router-discovery.ts +785 -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,339 @@
1
+ ---
2
+ name: middleware
3
+ description: Define middleware for authentication, logging, and request processing in @rangojs/router
4
+ argument-hint: [middleware-name]
5
+ ---
6
+
7
+ # Middleware
8
+
9
+ Middleware runs before/after route handlers using the onion model.
10
+
11
+ ## Execution Model
12
+
13
+ Canonical semantics reference:
14
+ [docs/execution-model.md](../../docs/internal/execution-model.md)
15
+
16
+ There are two levels of middleware with different execution scopes:
17
+
18
+ ### Global middleware (`router.use()`)
19
+
20
+ Registered on the router instance. Wraps the **entire request**, including server actions, rendering, and progressive enhancement (PE) re-renders.
21
+
22
+ ```typescript
23
+ const router = createRouter<AppEnv>({})
24
+ .use(loggerMiddleware) // all routes
25
+ .use("/admin/*", authMiddleware) // pattern-scoped
26
+ .routes(urlpatterns);
27
+ ```
28
+
29
+ ### Route middleware (`middleware()` in `urls()`)
30
+
31
+ Registered inside `urls()` callback. Wraps **rendering only** -- it does NOT wrap server action execution. Actions run before route middleware, so when route middleware executes during post-action revalidation, it can observe state that the action set (cookies, context variables, headers).
32
+
33
+ ```
34
+ Request flow (with action):
35
+ global mw -> action executes -> route mw -> layout -> handler -> loaders
36
+
37
+ Request flow (no action):
38
+ global mw -> route mw -> layout -> handler -> loaders
39
+
40
+ Progressive enhancement (no-JS form POST):
41
+ global mw -> action executes -> route mw -> full page re-render
42
+ ```
43
+
44
+ The contract is: **route middleware wraps rendering regardless of transport** (JS-enabled RSC stream or no-JS HTML). During PE re-render, route middleware observes action-set state (cookies, context variables) the same way it does during JS-enabled post-action revalidation.
45
+
46
+ Revalidation is still partial. Route middleware wraps the render pass that
47
+ does happen, but it does not force unrelated outer segments to recompute.
48
+ If a child segment depends on data established by an outer handler/layout,
49
+ revalidate that outer segment too, or have the child guard/reload the
50
+ data itself.
51
+
52
+ ### Revalidation Contracts with Middleware-Backed Trees
53
+
54
+ Middleware can establish request-level context (`ctx.set`) for segments that
55
+ execute in the current render pass. It does not change partial revalidation
56
+ boundaries between handler/layout/parallel segments.
57
+
58
+ For shared segment data, use named revalidation contracts on both the producer
59
+ and consumer segments, even when middleware is present in the chain.
60
+
61
+ ```typescript
62
+ export const revalidateCartData = ({ actionId }) =>
63
+ actionId?.includes("src/actions/cart.ts#") ?? false;
64
+
65
+ layout(CartLayout, () => [
66
+ middleware(cartRenderMiddleware),
67
+ revalidate(revalidateCartData), // producer reruns
68
+ parallel(
69
+ { "@cart": CartSummary },
70
+ () => [revalidate(revalidateCartData)], // consumer reruns
71
+ ),
72
+ ]);
73
+ ```
74
+
75
+ You can package those contracts as importable helpers to avoid repeating
76
+ `revalidate(...)` at each segment:
77
+
78
+ ```typescript
79
+ import { revalidate } from "@rangojs/router";
80
+
81
+ export const revalidateCart = () => [revalidate(revalidateCartData)];
82
+
83
+ layout(CartLayout, () => [
84
+ middleware(cartRenderMiddleware),
85
+ revalidateCart(),
86
+ parallel({ "@cart": CartSummary }, () => [revalidateCart()]),
87
+ ]);
88
+ ```
89
+
90
+ Route middleware is the right place for per-route concerns that affect rendering (setting context variables for handlers, adding response headers, reading cookies set by actions). It is NOT the right place for action guards -- use global middleware for that.
91
+
92
+ ## Basic Middleware
93
+
94
+ ```typescript
95
+ import type { Middleware } from "@rangojs/router";
96
+
97
+ export const authMiddleware: Middleware = async (ctx, next) => {
98
+ const token = ctx.request.headers.get("Authorization");
99
+
100
+ if (!token) {
101
+ throw new Response("Unauthorized", { status: 401 });
102
+ }
103
+
104
+ const user = await verifyToken(token);
105
+ ctx.set("user", user);
106
+
107
+ await next();
108
+ };
109
+ ```
110
+
111
+ ## Using Middleware in Routes
112
+
113
+ ```typescript
114
+ import { urls } from "@rangojs/router";
115
+ import { authMiddleware, loggerMiddleware } from "./middleware";
116
+
117
+ export const urlpatterns = urls(({ path, layout, middleware }) => [
118
+ // Global middleware for all routes in this file
119
+ middleware(loggerMiddleware),
120
+
121
+ // Layout with scoped middleware
122
+ layout(<AdminLayout />, () => [
123
+ middleware(authMiddleware), // Only for admin routes
124
+
125
+ path("/admin", AdminDashboard, { name: "admin.index" }),
126
+ path("/admin/users", AdminUsers, { name: "admin.users" }),
127
+ ]),
128
+
129
+ // Public routes (no auth middleware)
130
+ path("/", HomePage, { name: "home" }),
131
+ path("/about", AboutPage, { name: "about" }),
132
+ ]);
133
+ ```
134
+
135
+ ## Middleware with Multiple Handlers
136
+
137
+ ```typescript
138
+ // Spread multiple middleware from a single export
139
+ export const shopMiddleware = [loggerMiddleware, mockAuthMiddleware];
140
+
141
+ // In routes
142
+ layout(<ShopLayout />, () => [
143
+ middleware(...shopMiddleware),
144
+
145
+ path("/shop", ShopIndex, { name: "shop" }),
146
+ ])
147
+ ```
148
+
149
+ ## Middleware Context
150
+
151
+ ```typescript
152
+ export const myMiddleware: Middleware = async (ctx, next) => {
153
+ // Access request
154
+ ctx.request; // Request object
155
+ ctx.url; // Parsed URL
156
+ ctx.params; // Route parameters
157
+
158
+ // Access platform bindings (plain bindings from createRouter<TEnv>())
159
+ ctx.env.DB; // D1Database
160
+ ctx.env.KV; // KVNamespace
161
+
162
+ // Set variables for downstream handlers (typed via RSCRouter.Vars)
163
+ ctx.set("user", { id: "123", name: "John" });
164
+
165
+ // Continue to next middleware/handler
166
+ await next();
167
+
168
+ // After handler (response intercepting)
169
+ console.log("Handler completed");
170
+ };
171
+ ```
172
+
173
+ ### Typed context variables in middleware
174
+
175
+ Use `createVar<T>()` for type-safe data sharing between middleware and handlers:
176
+
177
+ ```typescript
178
+ import { createVar } from "@rangojs/router";
179
+ import type { Middleware } from "@rangojs/router";
180
+
181
+ interface AuthUser { id: string; email: string; role: string }
182
+ export const CurrentUser = createVar<AuthUser>();
183
+
184
+ export const authMiddleware: Middleware = async (ctx, next) => {
185
+ const token = ctx.request.headers.get("Authorization");
186
+ if (!token) throw new Response("Unauthorized", { status: 401 });
187
+
188
+ const user = await verifyToken(token);
189
+ ctx.set(CurrentUser, user); // type-checked
190
+ await next();
191
+ };
192
+
193
+ // In a handler -- typed read
194
+ import { CurrentUser } from "./middleware";
195
+
196
+ const Dashboard: Handler<"dashboard"> = (ctx) => {
197
+ const user = ctx.get(CurrentUser); // typed as AuthUser | undefined
198
+ return <DashboardPage user={user!} />;
199
+ };
200
+ ```
201
+
202
+ This works alongside `ctx.get("key")` / `ctx.set("key", value)` (global typing
203
+ via RSCRouter.Vars augmentation). Use `createVar` for route-local or feature-scoped
204
+ data; use RSCRouter.Vars for app-wide middleware state.
205
+
206
+ ## Redirect with State in Middleware
207
+
208
+ ```typescript
209
+ import { redirect, createLocationState } from "@rangojs/router";
210
+ import type { Middleware } from "@rangojs/router";
211
+
212
+ export const FlashMessage = createLocationState<{ text: string }>({
213
+ flash: true,
214
+ });
215
+
216
+ export const requireAuthMiddleware: Middleware = async (ctx, next) => {
217
+ const token = ctx.request.headers.get("Authorization");
218
+ if (!token) {
219
+ return redirect("/login", {
220
+ state: [FlashMessage({ text: "Please log in to continue" })],
221
+ });
222
+ }
223
+ await next();
224
+ };
225
+ ```
226
+
227
+ Read the flash on the target page with `useLocationState(FlashMessage)`. The `{ flash: true }` option makes it auto-clear after first render. See `/hooks`.
228
+
229
+ ## Authentication Middleware
230
+
231
+ ```typescript
232
+ export const requireAuthMiddleware: Middleware = async (ctx, next) => {
233
+ const user = ctx.get("user");
234
+
235
+ if (!user) {
236
+ throw new Response("Unauthorized", { status: 401 });
237
+ }
238
+
239
+ await next();
240
+ };
241
+
242
+ export const permissionsMiddleware: Middleware = async (ctx, next) => {
243
+ const user = ctx.get("user");
244
+ const requiredPermission = "admin";
245
+
246
+ if (!user?.permissions?.includes(requiredPermission)) {
247
+ throw new Response("Forbidden", { status: 403 });
248
+ }
249
+
250
+ await next();
251
+ };
252
+ ```
253
+
254
+ ## Logger Middleware
255
+
256
+ ```typescript
257
+ export const loggerMiddleware: Middleware = async (ctx, next) => {
258
+ const start = Date.now();
259
+
260
+ console.log(`[${ctx.request.method}] ${ctx.url.pathname}`);
261
+
262
+ await next();
263
+
264
+ const duration = Date.now() - start;
265
+ console.log(`[${ctx.request.method}] ${ctx.url.pathname} - ${duration}ms`);
266
+ };
267
+ ```
268
+
269
+ ## Rate Limiting Middleware
270
+
271
+ ```typescript
272
+ export const rateLimitMiddleware: Middleware = async (ctx, next) => {
273
+ const ip = ctx.request.headers.get("CF-Connecting-IP") ?? "unknown";
274
+ const key = `rate-limit:${ip}`;
275
+
276
+ const count = await ctx.env.KV.get(key);
277
+ const requests = count ? parseInt(count) : 0;
278
+
279
+ if (requests > 100) {
280
+ throw new Response("Too Many Requests", { status: 429 });
281
+ }
282
+
283
+ await ctx.env.KV.put(key, String(requests + 1), {
284
+ expirationTtl: 60,
285
+ });
286
+
287
+ await next();
288
+ };
289
+ ```
290
+
291
+ ## Complete Example
292
+
293
+ ```typescript
294
+ // middleware/index.ts
295
+ import type { Middleware } from "@rangojs/router";
296
+
297
+ export const loggerMiddleware: Middleware = async (ctx, next) => {
298
+ console.log(`[${ctx.request.method}] ${ctx.url.pathname}`);
299
+ await next();
300
+ };
301
+
302
+ export const mockAuthMiddleware: Middleware = async (ctx, next) => {
303
+ // Mock user for development
304
+ ctx.set("user", { id: "1", name: "Demo User" });
305
+ await next();
306
+ };
307
+
308
+ export const requireAuthMiddleware: Middleware = async (ctx, next) => {
309
+ if (!ctx.get("user")) {
310
+ throw new Response("Unauthorized", { status: 401 });
311
+ }
312
+ await next();
313
+ };
314
+
315
+ // urls.tsx
316
+ import { urls } from "@rangojs/router";
317
+ import {
318
+ loggerMiddleware,
319
+ mockAuthMiddleware,
320
+ requireAuthMiddleware,
321
+ } from "./middleware";
322
+
323
+ export const urlpatterns = urls(({ path, layout, middleware }) => [
324
+ // Global middleware
325
+ middleware(loggerMiddleware),
326
+ middleware(mockAuthMiddleware),
327
+
328
+ // Public routes
329
+ path("/", HomePage, { name: "home" }),
330
+
331
+ // Protected routes
332
+ layout(<AccountLayout />, () => [
333
+ middleware(requireAuthMiddleware),
334
+
335
+ path("/account", AccountPage, { name: "account" }),
336
+ path("/account/settings", SettingsPage, { name: "settings" }),
337
+ ]),
338
+ ]);
339
+ ```
@@ -0,0 +1,128 @@
1
+ ---
2
+ name: mime-routes
3
+ description: Content negotiation — serve different response types (RSC, JSON, text, XML) from the same URL based on Accept header
4
+ argument-hint: [negotiate|vary|accept]
5
+ ---
6
+
7
+ # Content Negotiation (MIME Routes)
8
+
9
+ Content negotiation lets you register multiple response types on the same URL pattern.
10
+ The router inspects the `Accept` header and dispatches to the matching handler.
11
+ All negotiated responses include `Vary: Accept` for correct CDN/cache behavior.
12
+
13
+ See also: `/response-routes` for the base response route API (path.json, path.text, etc.).
14
+
15
+ ## Defining Negotiated Routes
16
+
17
+ Declare the same URL pattern with both an RSC route and one or more response-type routes.
18
+ Order within the `urls()` array does not matter — the trie merges them at build time.
19
+
20
+ ```typescript
21
+ import { urls } from "@rangojs/router";
22
+
23
+ export const urlpatterns = urls(({ path, layout, include }) => [
24
+ // RSC page + JSON API on the same URL
25
+ path("/products/:id", ProductPage, { name: "product" }),
26
+ path.json(
27
+ "/products/:id",
28
+ (ctx) => {
29
+ return db.getProduct(ctx.params.id);
30
+ },
31
+ { name: "productJson" },
32
+ ),
33
+ ]);
34
+ ```
35
+
36
+ When a browser requests `/products/42` (`Accept: text/html`), the RSC page renders.
37
+ When an API client requests the same URL (`Accept: application/json`), the JSON handler runs.
38
+
39
+ ## Negotiation Rules
40
+
41
+ 1. **Q-value priority** — higher `q` wins (`Accept: application/json;q=0.9, text/html;q=1.0` serves RSC)
42
+ 2. **Client order tiebreaker** — when q-values are equal, the type listed first in Accept wins (matches Express/Hono behavior)
43
+ 3. **Specific MIME match** — the variant whose MIME type appears in Accept wins
44
+ 4. **Wildcard / empty Accept** — `*/*` and missing Accept fall back to route definition order (the first-defined variant wins)
45
+ 5. **All responses** on a negotiated URL get `Vary: Accept` header, including the RSC side
46
+
47
+ RSC participates as a `text/html` candidate alongside response-type variants.
48
+ There is no special short-circuit — RSC follows the same negotiation rules as other types.
49
+
50
+ The MIME mapping used for matching:
51
+
52
+ | Tag | MIME type |
53
+ | -------------------- | ------------------------------------------------------------ |
54
+ | RSC (plain `path()`) | `text/html` (negotiation) / `text/x-component` (wire format) |
55
+ | `json` | `application/json` |
56
+ | `text` | `text/plain` |
57
+ | `xml` | `application/xml` |
58
+ | `html` | `text/html` |
59
+ | `md` | `text/markdown` |
60
+
61
+ RSC routes negotiate as `text/html` but respond with `text/x-component` (the RSC wire format).
62
+ The browser's RSC runtime decodes this transparently — clients requesting `text/html` get
63
+ the RSC page rendered normally.
64
+
65
+ Tags `image`, `stream`, and `any` are pass-through and do not participate in Accept matching.
66
+
67
+ ## Multiple Response Types
68
+
69
+ A single URL can have an RSC route plus multiple response-type variants:
70
+
71
+ ```typescript
72
+ export const urlpatterns = urls(({ path }) => [
73
+ path("/data", DataPage, { name: "data" }),
74
+ path.json("/data", () => ({ format: "json" }), { name: "dataJson" }),
75
+ path.text("/data", () => "plain text", { name: "dataText" }),
76
+ path.xml("/data", () => "<root>xml</root>", { name: "dataXml" }),
77
+ ]);
78
+ ```
79
+
80
+ - `Accept: text/html` — RSC page
81
+ - `Accept: application/json` — JSON handler
82
+ - `Accept: text/plain` — text handler
83
+ - `Accept: application/xml` — XML handler
84
+ - `Accept: */*` — first variant (JSON, since it was registered first)
85
+
86
+ ## Wildcard Routes
87
+
88
+ Content negotiation works with wildcard `/*` patterns:
89
+
90
+ ```typescript
91
+ path("/files/*", FileBrowserPage, { name: "files" }),
92
+ path.json("/files/*", (ctx) => {
93
+ const filePath = ctx.params["*"];
94
+ return { entries: listDir(filePath) };
95
+ }, { name: "filesJson" }),
96
+ ```
97
+
98
+ ## Response-Only Negotiation (No RSC Primary)
99
+
100
+ Two or more response-type routes can share a URL without an RSC route.
101
+ The last registered route becomes the primary; earlier ones become variants:
102
+
103
+ ```typescript
104
+ path.json("/api/data", () => ({ format: "json" }), { name: "dataJson" }),
105
+ path.text("/api/data", () => "plain text version", { name: "dataText" }),
106
+ ```
107
+
108
+ Without an RSC primary, there is no `text/html` candidate — the Accept header
109
+ picks among the response-type candidates directly.
110
+
111
+ ## How It Works
112
+
113
+ 1. **Build time**: `buildRouteTrie()` calls `mergeLeaves()` when multiple routes share a pattern.
114
+ RSC routes become the primary trie leaf; response-type routes are stored in the `nv`
115
+ (negotiate variants) array on the leaf. The `rf` (rsc-first) flag tracks definition order.
116
+ 2. **Runtime**: `previewRoute()` reads `negotiateVariants` from the trie match result.
117
+ It parses the `Accept` header (extracting q-values and order), builds a candidate list
118
+ (RSC as `text/html` + response-type variants), and calls `pickNegotiateVariant()`.
119
+ 3. **Candidate matching**: walks the client's sorted Accept list (by q desc, then order asc),
120
+ matching each entry against candidates. Wildcards (`*/*`, `text/*`) fall back to definition order.
121
+ 4. **Vary header**: both the response-route handler wrapper and the RSC handler wrapper
122
+ append `Vary: Accept` when the `negotiated` flag is set on the preview result.
123
+
124
+ ## Caching Considerations
125
+
126
+ `Vary: Accept` is set automatically on all negotiated responses. This tells CDNs and
127
+ HTTP caches to store separate entries per Accept header value. No additional cache
128
+ configuration is needed for negotiated routes — the framework handles it.