@rangojs/router 0.0.0-experimental.7 → 0.0.0-experimental.71

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 (307) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +942 -4
  3. package/dist/bin/rango.js +1689 -0
  4. package/dist/vite/index.js +4951 -930
  5. package/package.json +70 -60
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +294 -0
  8. package/skills/caching/SKILL.md +93 -23
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +12 -8
  11. package/skills/document-cache/SKILL.md +18 -16
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +334 -72
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +131 -8
  16. package/skills/layout/SKILL.md +100 -3
  17. package/skills/links/SKILL.md +92 -31
  18. package/skills/loader/SKILL.md +404 -44
  19. package/skills/middleware/SKILL.md +173 -34
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +204 -1
  22. package/skills/prerender/SKILL.md +685 -0
  23. package/skills/rango/SKILL.md +85 -16
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +257 -14
  26. package/skills/router-setup/SKILL.md +210 -32
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +9 -8
  29. package/skills/typesafety/SKILL.md +328 -89
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +102 -4
  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/app-version.ts +14 -0
  36. package/src/browser/event-controller.ts +92 -64
  37. package/src/browser/history-state.ts +80 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +24 -4
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +20 -12
  42. package/src/browser/navigation-bridge.ts +296 -558
  43. package/src/browser/navigation-client.ts +179 -69
  44. package/src/browser/navigation-store.ts +73 -55
  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 +328 -313
  48. package/src/browser/prefetch/cache.ts +206 -0
  49. package/src/browser/prefetch/fetch.ts +150 -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 +160 -0
  53. package/src/browser/prefetch/resource-ready.ts +77 -0
  54. package/src/browser/rango-state.ts +112 -0
  55. package/src/browser/react/Link.tsx +230 -74
  56. package/src/browser/react/NavigationProvider.tsx +87 -11
  57. package/src/browser/react/context.ts +11 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +12 -12
  60. package/src/browser/react/location-state-shared.ts +95 -53
  61. package/src/browser/react/location-state.ts +60 -15
  62. package/src/browser/react/mount-context.ts +6 -1
  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 +29 -51
  66. package/src/browser/react/use-client-cache.ts +5 -3
  67. package/src/browser/react/use-handle.ts +30 -126
  68. package/src/browser/react/use-href.tsx +2 -2
  69. package/src/browser/react/use-link-status.ts +6 -5
  70. package/src/browser/react/use-navigation.ts +22 -63
  71. package/src/browser/react/use-params.ts +65 -0
  72. package/src/browser/react/use-pathname.ts +47 -0
  73. package/src/browser/react/use-router.ts +76 -0
  74. package/src/browser/react/use-search-params.ts +56 -0
  75. package/src/browser/react/use-segments.ts +80 -97
  76. package/src/browser/response-adapter.ts +73 -0
  77. package/src/browser/rsc-router.tsx +214 -58
  78. package/src/browser/scroll-restoration.ts +127 -52
  79. package/src/browser/segment-reconciler.ts +221 -0
  80. package/src/browser/segment-structure-assert.ts +16 -0
  81. package/src/browser/server-action-bridge.ts +510 -603
  82. package/src/browser/shallow.ts +6 -1
  83. package/src/browser/types.ts +141 -48
  84. package/src/browser/validate-redirect-origin.ts +29 -0
  85. package/src/build/generate-manifest.ts +235 -24
  86. package/src/build/generate-route-types.ts +39 -0
  87. package/src/build/index.ts +13 -0
  88. package/src/build/route-trie.ts +265 -0
  89. package/src/build/route-types/ast-helpers.ts +25 -0
  90. package/src/build/route-types/ast-route-extraction.ts +98 -0
  91. package/src/build/route-types/codegen.ts +102 -0
  92. package/src/build/route-types/include-resolution.ts +418 -0
  93. package/src/build/route-types/param-extraction.ts +48 -0
  94. package/src/build/route-types/per-module-writer.ts +128 -0
  95. package/src/build/route-types/router-processing.ts +618 -0
  96. package/src/build/route-types/scan-filter.ts +85 -0
  97. package/src/build/runtime-discovery.ts +231 -0
  98. package/src/cache/background-task.ts +34 -0
  99. package/src/cache/cache-key-utils.ts +44 -0
  100. package/src/cache/cache-policy.ts +125 -0
  101. package/src/cache/cache-runtime.ts +342 -0
  102. package/src/cache/cache-scope.ts +167 -309
  103. package/src/cache/cf/cf-cache-store.ts +571 -17
  104. package/src/cache/cf/index.ts +13 -3
  105. package/src/cache/document-cache.ts +116 -77
  106. package/src/cache/handle-capture.ts +81 -0
  107. package/src/cache/handle-snapshot.ts +41 -0
  108. package/src/cache/index.ts +1 -15
  109. package/src/cache/memory-segment-store.ts +191 -13
  110. package/src/cache/profile-registry.ts +73 -0
  111. package/src/cache/read-through-swr.ts +134 -0
  112. package/src/cache/segment-codec.ts +256 -0
  113. package/src/cache/taint.ts +153 -0
  114. package/src/cache/types.ts +72 -122
  115. package/src/client.rsc.tsx +3 -1
  116. package/src/client.tsx +105 -179
  117. package/src/component-utils.ts +4 -4
  118. package/src/components/DefaultDocument.tsx +5 -1
  119. package/src/context-var.ts +156 -0
  120. package/src/debug.ts +19 -9
  121. package/src/errors.ts +108 -2
  122. package/src/handle.ts +55 -29
  123. package/src/handles/MetaTags.tsx +73 -20
  124. package/src/handles/breadcrumbs.ts +66 -0
  125. package/src/handles/index.ts +1 -0
  126. package/src/handles/meta.ts +30 -13
  127. package/src/host/cookie-handler.ts +21 -15
  128. package/src/host/errors.ts +8 -8
  129. package/src/host/index.ts +4 -7
  130. package/src/host/pattern-matcher.ts +27 -27
  131. package/src/host/router.ts +61 -39
  132. package/src/host/testing.ts +8 -8
  133. package/src/host/types.ts +15 -7
  134. package/src/host/utils.ts +1 -1
  135. package/src/href-client.ts +119 -29
  136. package/src/index.rsc.ts +155 -19
  137. package/src/index.ts +223 -30
  138. package/src/internal-debug.ts +11 -0
  139. package/src/loader.rsc.ts +26 -157
  140. package/src/loader.ts +27 -10
  141. package/src/network-error-thrower.tsx +3 -1
  142. package/src/outlet-provider.tsx +45 -0
  143. package/src/prerender/param-hash.ts +37 -0
  144. package/src/prerender/store.ts +186 -0
  145. package/src/prerender.ts +524 -0
  146. package/src/reverse.ts +351 -0
  147. package/src/root-error-boundary.tsx +41 -29
  148. package/src/route-content-wrapper.tsx +7 -4
  149. package/src/route-definition/dsl-helpers.ts +982 -0
  150. package/src/route-definition/helper-factories.ts +200 -0
  151. package/src/route-definition/helpers-types.ts +434 -0
  152. package/src/route-definition/index.ts +55 -0
  153. package/src/route-definition/redirect.ts +101 -0
  154. package/src/route-definition/resolve-handler-use.ts +149 -0
  155. package/src/route-definition.ts +1 -1428
  156. package/src/route-map-builder.ts +217 -123
  157. package/src/route-name.ts +53 -0
  158. package/src/route-types.ts +70 -8
  159. package/src/router/content-negotiation.ts +215 -0
  160. package/src/router/debug-manifest.ts +72 -0
  161. package/src/router/error-handling.ts +9 -9
  162. package/src/router/find-match.ts +160 -0
  163. package/src/router/handler-context.ts +435 -86
  164. package/src/router/intercept-resolution.ts +402 -0
  165. package/src/router/lazy-includes.ts +237 -0
  166. package/src/router/loader-resolution.ts +356 -128
  167. package/src/router/logging.ts +251 -0
  168. package/src/router/manifest.ts +154 -35
  169. package/src/router/match-api.ts +555 -0
  170. package/src/router/match-context.ts +5 -3
  171. package/src/router/match-handlers.ts +440 -0
  172. package/src/router/match-middleware/background-revalidation.ts +108 -93
  173. package/src/router/match-middleware/cache-lookup.ts +459 -10
  174. package/src/router/match-middleware/cache-store.ts +98 -26
  175. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  176. package/src/router/match-middleware/segment-resolution.ts +80 -6
  177. package/src/router/match-pipelines.ts +10 -45
  178. package/src/router/match-result.ts +135 -35
  179. package/src/router/metrics.ts +240 -15
  180. package/src/router/middleware-cookies.ts +55 -0
  181. package/src/router/middleware-types.ts +220 -0
  182. package/src/router/middleware.ts +324 -369
  183. package/src/router/navigation-snapshot.ts +182 -0
  184. package/src/router/pattern-matching.ts +211 -43
  185. package/src/router/prerender-match.ts +502 -0
  186. package/src/router/preview-match.ts +98 -0
  187. package/src/router/request-classification.ts +310 -0
  188. package/src/router/revalidation.ts +137 -38
  189. package/src/router/route-snapshot.ts +245 -0
  190. package/src/router/router-context.ts +41 -21
  191. package/src/router/router-interfaces.ts +484 -0
  192. package/src/router/router-options.ts +618 -0
  193. package/src/router/router-registry.ts +24 -0
  194. package/src/router/segment-resolution/fresh.ts +748 -0
  195. package/src/router/segment-resolution/helpers.ts +268 -0
  196. package/src/router/segment-resolution/loader-cache.ts +199 -0
  197. package/src/router/segment-resolution/revalidation.ts +1379 -0
  198. package/src/router/segment-resolution/static-store.ts +67 -0
  199. package/src/router/segment-resolution.ts +21 -0
  200. package/src/router/segment-wrappers.ts +291 -0
  201. package/src/router/telemetry-otel.ts +299 -0
  202. package/src/router/telemetry.ts +300 -0
  203. package/src/router/timeout.ts +148 -0
  204. package/src/router/trie-matching.ts +239 -0
  205. package/src/router/types.ts +78 -3
  206. package/src/router.ts +740 -4252
  207. package/src/rsc/handler-context.ts +45 -0
  208. package/src/rsc/handler.ts +907 -797
  209. package/src/rsc/helpers.ts +140 -6
  210. package/src/rsc/index.ts +0 -20
  211. package/src/rsc/loader-fetch.ts +229 -0
  212. package/src/rsc/manifest-init.ts +90 -0
  213. package/src/rsc/nonce.ts +14 -0
  214. package/src/rsc/origin-guard.ts +141 -0
  215. package/src/rsc/progressive-enhancement.ts +391 -0
  216. package/src/rsc/response-error.ts +37 -0
  217. package/src/rsc/response-route-handler.ts +347 -0
  218. package/src/rsc/rsc-rendering.ts +246 -0
  219. package/src/rsc/runtime-warnings.ts +42 -0
  220. package/src/rsc/server-action.ts +356 -0
  221. package/src/rsc/ssr-setup.ts +128 -0
  222. package/src/rsc/types.ts +46 -11
  223. package/src/search-params.ts +230 -0
  224. package/src/segment-system.tsx +165 -17
  225. package/src/server/context.ts +315 -58
  226. package/src/server/cookie-store.ts +190 -0
  227. package/src/server/fetchable-loader-store.ts +37 -0
  228. package/src/server/handle-store.ts +113 -15
  229. package/src/server/loader-registry.ts +24 -64
  230. package/src/server/request-context.ts +607 -81
  231. package/src/server.ts +35 -130
  232. package/src/ssr/index.tsx +103 -30
  233. package/src/static-handler.ts +126 -0
  234. package/src/theme/ThemeProvider.tsx +21 -15
  235. package/src/theme/ThemeScript.tsx +5 -5
  236. package/src/theme/constants.ts +5 -2
  237. package/src/theme/index.ts +4 -14
  238. package/src/theme/theme-context.ts +4 -30
  239. package/src/theme/theme-script.ts +21 -18
  240. package/src/types/boundaries.ts +158 -0
  241. package/src/types/cache-types.ts +198 -0
  242. package/src/types/error-types.ts +192 -0
  243. package/src/types/global-namespace.ts +100 -0
  244. package/src/types/handler-context.ts +791 -0
  245. package/src/types/index.ts +88 -0
  246. package/src/types/loader-types.ts +210 -0
  247. package/src/types/route-config.ts +170 -0
  248. package/src/types/route-entry.ts +109 -0
  249. package/src/types/segments.ts +151 -0
  250. package/src/types.ts +1 -1623
  251. package/src/urls/include-helper.ts +197 -0
  252. package/src/urls/index.ts +53 -0
  253. package/src/urls/path-helper-types.ts +346 -0
  254. package/src/urls/path-helper.ts +364 -0
  255. package/src/urls/pattern-types.ts +107 -0
  256. package/src/urls/response-types.ts +116 -0
  257. package/src/urls/type-extraction.ts +372 -0
  258. package/src/urls/urls-function.ts +98 -0
  259. package/src/urls.ts +1 -802
  260. package/src/use-loader.tsx +161 -81
  261. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  262. package/src/vite/discovery/discover-routers.ts +348 -0
  263. package/src/vite/discovery/prerender-collection.ts +439 -0
  264. package/src/vite/discovery/route-types-writer.ts +258 -0
  265. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  266. package/src/vite/discovery/state.ts +117 -0
  267. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  268. package/src/vite/index.ts +15 -1129
  269. package/src/vite/plugin-types.ts +103 -0
  270. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  271. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  272. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  273. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  274. package/src/vite/plugins/expose-id-utils.ts +299 -0
  275. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  276. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  277. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  278. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  279. package/src/vite/plugins/expose-ids/types.ts +45 -0
  280. package/src/vite/plugins/expose-internal-ids.ts +786 -0
  281. package/src/vite/plugins/performance-tracks.ts +88 -0
  282. package/src/vite/plugins/refresh-cmd.ts +127 -0
  283. package/src/vite/plugins/use-cache-transform.ts +323 -0
  284. package/src/vite/plugins/version-injector.ts +83 -0
  285. package/src/vite/plugins/version-plugin.ts +266 -0
  286. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  287. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  288. package/src/vite/rango.ts +462 -0
  289. package/src/vite/router-discovery.ts +918 -0
  290. package/src/vite/utils/ast-handler-extract.ts +517 -0
  291. package/src/vite/utils/banner.ts +36 -0
  292. package/src/vite/utils/bundle-analysis.ts +137 -0
  293. package/src/vite/utils/manifest-utils.ts +70 -0
  294. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  295. package/src/vite/utils/prerender-utils.ts +207 -0
  296. package/src/vite/utils/shared-utils.ts +170 -0
  297. package/CLAUDE.md +0 -43
  298. package/src/browser/lru-cache.ts +0 -69
  299. package/src/browser/request-controller.ts +0 -164
  300. package/src/cache/memory-store.ts +0 -253
  301. package/src/href-context.ts +0 -33
  302. package/src/href.ts +0 -255
  303. package/src/server/route-manifest-cache.ts +0 -173
  304. package/src/vite/expose-handle-id.ts +0 -209
  305. package/src/vite/expose-loader-id.ts +0 -426
  306. package/src/vite/expose-location-state-id.ts +0 -177
  307. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -8,12 +8,95 @@ argument-hint: [middleware-name]
8
8
 
9
9
  Middleware runs before/after route handlers using the onion model.
10
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
+ When the router has a `basename`, pattern-scoped `.use()` patterns are automatically prefixed. For example, with `basename: "/app"`, `.use("/admin/*", mw)` matches `/app/admin/*`.
30
+
31
+ ### Route middleware (`middleware()` in `urls()`)
32
+
33
+ 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).
34
+
35
+ ```
36
+ Request flow (with action):
37
+ global mw -> action executes -> route mw -> layout -> handler -> loaders
38
+
39
+ Request flow (no action):
40
+ global mw -> route mw -> layout -> handler -> loaders
41
+
42
+ Progressive enhancement (no-JS form POST):
43
+ global mw -> action executes -> route mw -> full page re-render
44
+ ```
45
+
46
+ 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.
47
+
48
+ Revalidation is still partial. Route middleware wraps the render pass that
49
+ does happen, but it does not force unrelated outer segments to recompute.
50
+ If a child segment depends on data established by an outer handler/layout,
51
+ revalidate that outer segment too, or have the child guard/reload the
52
+ data itself.
53
+
54
+ ### Revalidation Contracts with Middleware-Backed Trees
55
+
56
+ Middleware can establish request-level context (`ctx.set`) for segments that
57
+ execute in the current render pass. It does not change partial revalidation
58
+ boundaries between handler/layout/parallel segments.
59
+
60
+ For shared segment data, use named revalidation contracts on both the producer
61
+ and consumer segments, even when middleware is present in the chain.
62
+
63
+ ```typescript
64
+ export const revalidateCartData = ({ actionId }) =>
65
+ actionId?.includes("src/actions/cart.ts#") ?? false;
66
+
67
+ layout(CartLayout, () => [
68
+ middleware(cartRenderMiddleware),
69
+ revalidate(revalidateCartData), // producer reruns
70
+ parallel(
71
+ { "@cart": CartSummary },
72
+ () => [revalidate(revalidateCartData)], // consumer reruns
73
+ ),
74
+ ]);
75
+ ```
76
+
77
+ You can package those contracts as importable helpers to avoid repeating
78
+ `revalidate(...)` at each segment:
79
+
80
+ ```typescript
81
+ import { revalidate } from "@rangojs/router";
82
+
83
+ export const revalidateCart = () => [revalidate(revalidateCartData)];
84
+
85
+ layout(CartLayout, () => [
86
+ middleware(cartRenderMiddleware),
87
+ revalidateCart(),
88
+ parallel({ "@cart": CartSummary }, () => [revalidateCart()]),
89
+ ]);
90
+ ```
91
+
92
+ 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.
93
+
11
94
  ## Basic Middleware
12
95
 
13
96
  ```typescript
14
- import { createMiddleware } from "@rangojs/router";
97
+ import type { Middleware } from "@rangojs/router";
15
98
 
16
- export const authMiddleware = createMiddleware(async (ctx, next) => {
99
+ export const authMiddleware: Middleware = async (ctx, next) => {
17
100
  const token = ctx.request.headers.get("Authorization");
18
101
 
19
102
  if (!token) {
@@ -21,10 +104,10 @@ export const authMiddleware = createMiddleware(async (ctx, next) => {
21
104
  }
22
105
 
23
106
  const user = await verifyToken(token);
24
- ctx.env.Variables.user = user;
107
+ ctx.set("user", user);
25
108
 
26
109
  await next();
27
- });
110
+ };
28
111
  ```
29
112
 
30
113
  ## Using Middleware in Routes
@@ -68,42 +151,98 @@ layout(<ShopLayout />, () => [
68
151
  ## Middleware Context
69
152
 
70
153
  ```typescript
71
- export const myMiddleware = createMiddleware(async (ctx, next) => {
154
+ export const myMiddleware: Middleware = async (ctx, next) => {
72
155
  // Access request
73
- ctx.request; // Request object
74
- ctx.url; // Parsed URL
75
- ctx.params; // Route parameters
156
+ ctx.request; // Request object
157
+ ctx.url; // Parsed URL
158
+ ctx.params; // Route parameters
76
159
 
77
- // Access environment
78
- ctx.env.Bindings.DB; // Cloudflare bindings
79
- ctx.env.Variables; // Mutable variables
160
+ // Access platform bindings (plain bindings from createRouter<TEnv>())
161
+ ctx.env.DB; // D1Database
162
+ ctx.env.KV; // KVNamespace
80
163
 
81
- // Set variables for downstream handlers
82
- ctx.env.Variables.user = { id: "123", name: "John" };
164
+ // Set variables for downstream handlers (typed via RSCRouter.Vars)
165
+ ctx.set("user", { id: "123", name: "John" });
83
166
 
84
167
  // Continue to next middleware/handler
85
168
  await next();
86
169
 
87
170
  // After handler (response intercepting)
88
171
  console.log("Handler completed");
172
+ };
173
+ ```
174
+
175
+ ### Typed context variables in middleware
176
+
177
+ Use `createVar<T>()` for type-safe data sharing between middleware and handlers:
178
+
179
+ ```typescript
180
+ import { createVar } from "@rangojs/router";
181
+ import type { Middleware } from "@rangojs/router";
182
+
183
+ interface AuthUser { id: string; email: string; role: string }
184
+ export const CurrentUser = createVar<AuthUser>();
185
+
186
+ export const authMiddleware: Middleware = async (ctx, next) => {
187
+ const token = ctx.request.headers.get("Authorization");
188
+ if (!token) throw new Response("Unauthorized", { status: 401 });
189
+
190
+ const user = await verifyToken(token);
191
+ ctx.set(CurrentUser, user); // type-checked
192
+ await next();
193
+ };
194
+
195
+ // In a handler -- typed read
196
+ import { CurrentUser } from "./middleware";
197
+
198
+ const Dashboard: Handler<"dashboard"> = (ctx) => {
199
+ const user = ctx.get(CurrentUser); // typed as AuthUser | undefined
200
+ return <DashboardPage user={user!} />;
201
+ };
202
+ ```
203
+
204
+ This works alongside `ctx.get("key")` / `ctx.set("key", value)` (global typing
205
+ via RSCRouter.Vars augmentation). Use `createVar` for route-local or feature-scoped
206
+ data; use RSCRouter.Vars for app-wide middleware state.
207
+
208
+ ## Redirect with State in Middleware
209
+
210
+ ```typescript
211
+ import { redirect, createLocationState } from "@rangojs/router";
212
+ import type { Middleware } from "@rangojs/router";
213
+
214
+ export const FlashMessage = createLocationState<{ text: string }>({
215
+ flash: true,
89
216
  });
217
+
218
+ export const requireAuthMiddleware: Middleware = async (ctx, next) => {
219
+ const token = ctx.request.headers.get("Authorization");
220
+ if (!token) {
221
+ return redirect("/login", {
222
+ state: [FlashMessage({ text: "Please log in to continue" })],
223
+ });
224
+ }
225
+ await next();
226
+ };
90
227
  ```
91
228
 
229
+ Read the flash on the target page with `useLocationState(FlashMessage)`. The `{ flash: true }` option makes it auto-clear after first render. See `/hooks`.
230
+
92
231
  ## Authentication Middleware
93
232
 
94
233
  ```typescript
95
- export const requireAuthMiddleware = createMiddleware(async (ctx, next) => {
96
- const user = ctx.env.Variables.user;
234
+ export const requireAuthMiddleware: Middleware = async (ctx, next) => {
235
+ const user = ctx.get("user");
97
236
 
98
237
  if (!user) {
99
238
  throw new Response("Unauthorized", { status: 401 });
100
239
  }
101
240
 
102
241
  await next();
103
- });
242
+ };
104
243
 
105
- export const permissionsMiddleware = createMiddleware(async (ctx, next) => {
106
- const user = ctx.env.Variables.user;
244
+ export const permissionsMiddleware: Middleware = async (ctx, next) => {
245
+ const user = ctx.get("user");
107
246
  const requiredPermission = "admin";
108
247
 
109
248
  if (!user?.permissions?.includes(requiredPermission)) {
@@ -111,13 +250,13 @@ export const permissionsMiddleware = createMiddleware(async (ctx, next) => {
111
250
  }
112
251
 
113
252
  await next();
114
- });
253
+ };
115
254
  ```
116
255
 
117
256
  ## Logger Middleware
118
257
 
119
258
  ```typescript
120
- export const loggerMiddleware = createMiddleware(async (ctx, next) => {
259
+ export const loggerMiddleware: Middleware = async (ctx, next) => {
121
260
  const start = Date.now();
122
261
 
123
262
  console.log(`[${ctx.request.method}] ${ctx.url.pathname}`);
@@ -126,54 +265,54 @@ export const loggerMiddleware = createMiddleware(async (ctx, next) => {
126
265
 
127
266
  const duration = Date.now() - start;
128
267
  console.log(`[${ctx.request.method}] ${ctx.url.pathname} - ${duration}ms`);
129
- });
268
+ };
130
269
  ```
131
270
 
132
271
  ## Rate Limiting Middleware
133
272
 
134
273
  ```typescript
135
- export const rateLimitMiddleware = createMiddleware(async (ctx, next) => {
274
+ export const rateLimitMiddleware: Middleware = async (ctx, next) => {
136
275
  const ip = ctx.request.headers.get("CF-Connecting-IP") ?? "unknown";
137
276
  const key = `rate-limit:${ip}`;
138
277
 
139
- const count = await ctx.env.Bindings.KV.get(key);
278
+ const count = await ctx.env.KV.get(key);
140
279
  const requests = count ? parseInt(count) : 0;
141
280
 
142
281
  if (requests > 100) {
143
282
  throw new Response("Too Many Requests", { status: 429 });
144
283
  }
145
284
 
146
- await ctx.env.Bindings.KV.put(key, String(requests + 1), {
285
+ await ctx.env.KV.put(key, String(requests + 1), {
147
286
  expirationTtl: 60,
148
287
  });
149
288
 
150
289
  await next();
151
- });
290
+ };
152
291
  ```
153
292
 
154
293
  ## Complete Example
155
294
 
156
295
  ```typescript
157
296
  // middleware/index.ts
158
- import { createMiddleware } from "@rangojs/router";
297
+ import type { Middleware } from "@rangojs/router";
159
298
 
160
- export const loggerMiddleware = createMiddleware(async (ctx, next) => {
299
+ export const loggerMiddleware: Middleware = async (ctx, next) => {
161
300
  console.log(`[${ctx.request.method}] ${ctx.url.pathname}`);
162
301
  await next();
163
- });
302
+ };
164
303
 
165
- export const mockAuthMiddleware = createMiddleware(async (ctx, next) => {
304
+ export const mockAuthMiddleware: Middleware = async (ctx, next) => {
166
305
  // Mock user for development
167
- ctx.env.Variables.user = { id: "1", name: "Demo User" };
306
+ ctx.set("user", { id: "1", name: "Demo User" });
168
307
  await next();
169
- });
308
+ };
170
309
 
171
- export const requireAuthMiddleware = createMiddleware(async (ctx, next) => {
172
- if (!ctx.env.Variables.user) {
310
+ export const requireAuthMiddleware: Middleware = async (ctx, next) => {
311
+ if (!ctx.get("user")) {
173
312
  throw new Response("Unauthorized", { status: 401 });
174
313
  }
175
314
  await next();
176
- });
315
+ };
177
316
 
178
317
  // urls.tsx
179
318
  import { urls } from "@rangojs/router";
@@ -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.
@@ -8,6 +8,9 @@ argument-hint: [@slot-name]
8
8
 
9
9
  Parallel routes render multiple components simultaneously in named slots.
10
10
 
11
+ Canonical semantics reference:
12
+ [docs/execution-model.md](../../docs/internal/execution-model.md)
13
+
11
14
  ## Basic Parallel Routes
12
15
 
13
16
  ```typescript
@@ -54,6 +57,108 @@ parallel({
54
57
  })
55
58
  ```
56
59
 
60
+ ## Reading Handler Data
61
+
62
+ Parallels can read `ctx.set()` values from their parent handler or layout
63
+ via `ctx.get()`. The handler always executes before its parallels
64
+ (handler-first).
65
+
66
+ Visibility follows tree structure:
67
+
68
+ - Layout-level parallels see layout data, but not path handler data
69
+ (the path is a separate entry).
70
+ - Parallels inside a path (or its orphan layouts) see both layout and
71
+ path handler data.
72
+
73
+ This applies to full render passes. During partial action revalidation,
74
+ only revalidated segments are recomputed. If a parallel depends on data
75
+ set by an outer handler or layout, revalidate that outer segment too, or
76
+ have the parallel reload/guard the data itself.
77
+
78
+ ```typescript
79
+ path("/dashboard/:id", (ctx) => {
80
+ const user = await getUser(ctx.params.id);
81
+ ctx.set("user", user);
82
+ return <DashboardPage user={user} />;
83
+ }, { name: "dashboard" }, () => [
84
+ layout(DashboardLayout, () => [
85
+ parallel({
86
+ "@sidebar": (ctx) => {
87
+ const user = ctx.get("user");
88
+ return <Sidebar role={user?.role} />;
89
+ },
90
+ }),
91
+ ]),
92
+ ])
93
+ ```
94
+
95
+ ## Setting Handles (Meta, Breadcrumbs)
96
+
97
+ Parallel slot handlers can call `ctx.use(Meta)` or `ctx.use(Breadcrumbs)` to
98
+ push handle data. The data is associated with the **parent** layout or route
99
+ segment, not the parallel segment itself. This is because parallels execute
100
+ after their parent handler and inherit its segment scope.
101
+
102
+ This works well for document-level metadata — the handle data follows the
103
+ parent's lifecycle (appears when the parent is mounted, removed when it
104
+ unmounts).
105
+
106
+ ```typescript
107
+ parallel({
108
+ "@meta": (ctx) => {
109
+ const meta = ctx.use(Meta);
110
+ meta({ title: "Product Detail" });
111
+ meta({ name: "description", content: "..." });
112
+ return null; // UI-less slot, only sets metadata
113
+ },
114
+ "@sidebar": (ctx) => <Sidebar />,
115
+ })
116
+ ```
117
+
118
+ Multiple parallels on the same parent can each push handle data — they all
119
+ accumulate under the parent segment ID.
120
+
121
+ ### Pattern: `@meta` slot for per-route metadata overrides
122
+
123
+ A dedicated `@meta` parallel slot lets routes define metadata separately from
124
+ their handler logic. The layout sets defaults via a title template, and each
125
+ route overrides via its own `@meta` slot. Since child segments push after
126
+ parents and `collectMeta` uses last-wins deduplication, overrides work
127
+ naturally.
128
+
129
+ ```typescript
130
+ // Layout sets defaults
131
+ layout((ctx) => {
132
+ ctx.use(Meta)({ title: { template: "%s | Store", default: "Store" } });
133
+ return <StoreLayout />;
134
+ }, () => [
135
+ // Route with @meta override — decoupled from handler rendering
136
+ path("/:slug", ProductPage, { name: "product" }, () => [
137
+ parallel({
138
+ "@meta": async (ctx) => {
139
+ const product = await ctx.use(ProductLoader);
140
+ const meta = ctx.use(Meta);
141
+ meta({ title: product.name });
142
+ meta({ name: "description", content: product.description });
143
+ meta({
144
+ "script:ld+json": {
145
+ "@context": "https://schema.org",
146
+ "@type": "Product",
147
+ name: product.name,
148
+ description: product.description,
149
+ },
150
+ });
151
+ return null; // UI-less slot
152
+ },
153
+ }),
154
+ ]),
155
+ ])
156
+ ```
157
+
158
+ This keeps the route handler focused on rendering UI while metadata
159
+ (title, description, Open Graph, JSON-LD) lives in a composable slot that
160
+ can be added, removed, or swapped per route without touching the handler.
161
+
57
162
  ## Parallel Routes with Loaders
58
163
 
59
164
  Add loaders and loading states to parallel routes:
@@ -71,6 +176,65 @@ parallel(
71
176
  )
72
177
  ```
73
178
 
179
+ ### Streaming Behavior
180
+
181
+ Parallels with `loading()` are **independent streaming units**. They don't
182
+ block the parent layout or sibling routes during SSR:
183
+
184
+ - **With `loading()`**: The skeleton renders immediately. The loader runs
185
+ in the background and streams data to the client when ready. The rest
186
+ of the page (layout, route content, other parallels) renders without
187
+ waiting.
188
+ - **Without `loading()`**: The parallel's loaders block the parent layout's
189
+ rendering. Use this when the data must be available before the page
190
+ paints (e.g., critical above-the-fold content).
191
+ - **SPA navigation**: Parallel loaders resolve in the background. The
192
+ existing parallel UI stays visible — no skeleton flash on route changes
193
+ within the same layout.
194
+
195
+ ```typescript
196
+ // Sidebar streams independently — page renders immediately
197
+ parallel(
198
+ { "@sidebar": () => <Sidebar /> },
199
+ () => [loader(SlowSidebarLoader), loading(<SidebarSkeleton />)]
200
+ )
201
+
202
+ // Cart data blocks layout — must be ready before paint
203
+ parallel(
204
+ { "@cartBadge": () => <CartBadge /> },
205
+ () => [loader(CartCountLoader)] // No loading() = awaited
206
+ )
207
+ ```
208
+
209
+ ## Slot Override Semantics
210
+
211
+ When multiple `parallel()` calls define the same slot name, **the last
212
+ definition wins**. Earlier definitions of that slot are removed. Other
213
+ slots from the earlier call are preserved.
214
+
215
+ This enables composition patterns where included routes override
216
+ parent-defined slots:
217
+
218
+ ```typescript
219
+ layout(DashboardLayout, () => [
220
+ // Base slots
221
+ parallel({
222
+ "@sidebar": () => <DefaultSidebar />,
223
+ "@footer": () => <Footer />,
224
+ }),
225
+
226
+ // Override just @sidebar — @footer is preserved
227
+ parallel({ "@sidebar": () => <CustomSidebar /> }),
228
+
229
+ path("/", DashboardIndex, { name: "index" }),
230
+ ])
231
+ ```
232
+
233
+ After resolution, the layout has two parallel entries:
234
+
235
+ - `{ "@footer": () => <Footer /> }` (first call, `@sidebar` removed)
236
+ - `{ "@sidebar": () => <CustomSidebar /> }` (second call, wins)
237
+
74
238
  ## Multiple Parallel Slots
75
239
 
76
240
  ```typescript
@@ -97,7 +261,7 @@ Render different content based on context:
97
261
  ```typescript
98
262
  parallel({
99
263
  "@sidebar": (ctx) => {
100
- const user = ctx.env.Variables.user;
264
+ const user = ctx.get("user");
101
265
  return user ? <UserSidebar user={user} /> : <GuestSidebar />;
102
266
  },
103
267
  })
@@ -120,6 +284,45 @@ parallel(
120
284
  )
121
285
  ```
122
286
 
287
+ Revalidating only the parallel does not re-run outer handlers/layouts.
288
+ If the slot reads `ctx.get()` data established above it, opt the outer
289
+ segment into revalidation as well.
290
+
291
+ ### Revalidation Contracts for Parallel Dependencies
292
+
293
+ Prefer named revalidation contracts shared by both the upstream producer and
294
+ the parallel consumer:
295
+
296
+ ```typescript
297
+ // revalidation-contracts.ts
298
+ export const revalidateCartData = ({ actionId }) =>
299
+ actionId?.includes("src/actions/cart.ts#") ?? false;
300
+
301
+ layout(CartLayout, () => [
302
+ revalidate(revalidateCartData), // producer reruns
303
+ parallel(
304
+ { "@cart": CartSummary },
305
+ () => [revalidate(revalidateCartData)], // consumer reruns
306
+ ),
307
+ ]);
308
+ ```
309
+
310
+ If the slot consumes multiple upstream domains, compose the contracts on both
311
+ segments.
312
+
313
+ Handoff helper style also works:
314
+
315
+ ```typescript
316
+ import { revalidate } from "@rangojs/router";
317
+
318
+ export const revalidateCart = () => [revalidate(revalidateCartData)];
319
+
320
+ layout(CartLayout, () => [
321
+ revalidateCart(),
322
+ parallel({ "@cart": CartSummary }, () => [revalidateCart()]),
323
+ ]);
324
+ ```
325
+
123
326
  ## Named Outlets
124
327
 
125
328
  Use `ParallelOutlet` to render slots in layouts: