@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26

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 (225) hide show
  1. package/README.md +294 -28
  2. package/dist/bin/rango.js +355 -47
  3. package/dist/vite/index.js +1658 -1239
  4. package/package.json +3 -3
  5. package/skills/cache-guide/SKILL.md +9 -5
  6. package/skills/caching/SKILL.md +4 -4
  7. package/skills/document-cache/SKILL.md +2 -2
  8. package/skills/hooks/SKILL.md +40 -29
  9. package/skills/host-router/SKILL.md +218 -0
  10. package/skills/intercept/SKILL.md +79 -0
  11. package/skills/layout/SKILL.md +62 -2
  12. package/skills/loader/SKILL.md +229 -15
  13. package/skills/middleware/SKILL.md +109 -30
  14. package/skills/parallel/SKILL.md +57 -2
  15. package/skills/prerender/SKILL.md +189 -19
  16. package/skills/rango/SKILL.md +1 -2
  17. package/skills/response-routes/SKILL.md +3 -3
  18. package/skills/route/SKILL.md +44 -3
  19. package/skills/router-setup/SKILL.md +80 -3
  20. package/skills/theme/SKILL.md +5 -4
  21. package/skills/typesafety/SKILL.md +59 -16
  22. package/skills/use-cache/SKILL.md +16 -2
  23. package/src/__internal.ts +1 -1
  24. package/src/bin/rango.ts +56 -19
  25. package/src/browser/action-coordinator.ts +97 -0
  26. package/src/browser/event-controller.ts +29 -48
  27. package/src/browser/history-state.ts +80 -0
  28. package/src/browser/intercept-utils.ts +1 -1
  29. package/src/browser/link-interceptor.ts +19 -3
  30. package/src/browser/merge-segment-loaders.ts +9 -2
  31. package/src/browser/navigation-bridge.ts +66 -443
  32. package/src/browser/navigation-client.ts +34 -62
  33. package/src/browser/navigation-store.ts +4 -33
  34. package/src/browser/navigation-transaction.ts +295 -0
  35. package/src/browser/partial-update.ts +103 -151
  36. package/src/browser/prefetch/cache.ts +67 -0
  37. package/src/browser/prefetch/fetch.ts +137 -0
  38. package/src/browser/prefetch/observer.ts +65 -0
  39. package/src/browser/prefetch/policy.ts +42 -0
  40. package/src/browser/prefetch/queue.ts +88 -0
  41. package/src/browser/rango-state.ts +112 -0
  42. package/src/browser/react/Link.tsx +154 -44
  43. package/src/browser/react/NavigationProvider.tsx +32 -0
  44. package/src/browser/react/context.ts +6 -0
  45. package/src/browser/react/filter-segment-order.ts +11 -0
  46. package/src/browser/react/index.ts +2 -6
  47. package/src/browser/react/location-state-shared.ts +29 -11
  48. package/src/browser/react/location-state.ts +6 -4
  49. package/src/browser/react/nonce-context.ts +23 -0
  50. package/src/browser/react/shallow-equal.ts +27 -0
  51. package/src/browser/react/use-action.ts +23 -45
  52. package/src/browser/react/use-client-cache.ts +5 -3
  53. package/src/browser/react/use-handle.ts +21 -64
  54. package/src/browser/react/use-navigation.ts +7 -32
  55. package/src/browser/react/use-params.ts +5 -34
  56. package/src/browser/react/use-pathname.ts +2 -3
  57. package/src/browser/react/use-router.ts +3 -6
  58. package/src/browser/react/use-search-params.ts +2 -1
  59. package/src/browser/react/use-segments.ts +75 -114
  60. package/src/browser/response-adapter.ts +73 -0
  61. package/src/browser/rsc-router.tsx +46 -22
  62. package/src/browser/scroll-restoration.ts +10 -7
  63. package/src/browser/server-action-bridge.ts +458 -405
  64. package/src/browser/types.ts +21 -35
  65. package/src/browser/validate-redirect-origin.ts +29 -0
  66. package/src/build/generate-manifest.ts +38 -13
  67. package/src/build/generate-route-types.ts +4 -0
  68. package/src/build/index.ts +1 -0
  69. package/src/build/route-trie.ts +19 -3
  70. package/src/build/route-types/codegen.ts +13 -4
  71. package/src/build/route-types/include-resolution.ts +13 -0
  72. package/src/build/route-types/per-module-writer.ts +15 -3
  73. package/src/build/route-types/router-processing.ts +170 -18
  74. package/src/build/runtime-discovery.ts +13 -1
  75. package/src/cache/background-task.ts +34 -0
  76. package/src/cache/cache-key-utils.ts +44 -0
  77. package/src/cache/cache-policy.ts +125 -0
  78. package/src/cache/cache-runtime.ts +136 -123
  79. package/src/cache/cache-scope.ts +76 -83
  80. package/src/cache/cf/cf-cache-store.ts +12 -7
  81. package/src/cache/document-cache.ts +93 -69
  82. package/src/cache/handle-capture.ts +81 -0
  83. package/src/cache/index.ts +0 -15
  84. package/src/cache/memory-segment-store.ts +43 -69
  85. package/src/cache/profile-registry.ts +43 -8
  86. package/src/cache/read-through-swr.ts +134 -0
  87. package/src/cache/segment-codec.ts +140 -117
  88. package/src/cache/taint.ts +30 -3
  89. package/src/cache/types.ts +1 -115
  90. package/src/client.rsc.tsx +0 -1
  91. package/src/client.tsx +53 -76
  92. package/src/errors.ts +6 -1
  93. package/src/handle.ts +1 -1
  94. package/src/handles/MetaTags.tsx +5 -2
  95. package/src/host/cookie-handler.ts +8 -3
  96. package/src/host/index.ts +0 -3
  97. package/src/host/router.ts +14 -1
  98. package/src/href-client.ts +3 -1
  99. package/src/index.rsc.ts +53 -10
  100. package/src/index.ts +73 -43
  101. package/src/loader.rsc.ts +12 -4
  102. package/src/loader.ts +8 -0
  103. package/src/prerender/store.ts +60 -18
  104. package/src/prerender.ts +76 -18
  105. package/src/reverse.ts +11 -7
  106. package/src/root-error-boundary.tsx +30 -26
  107. package/src/route-definition/dsl-helpers.ts +9 -6
  108. package/src/route-definition/index.ts +0 -3
  109. package/src/route-definition/redirect.ts +15 -3
  110. package/src/route-map-builder.ts +38 -2
  111. package/src/route-name.ts +53 -0
  112. package/src/route-types.ts +7 -0
  113. package/src/router/content-negotiation.ts +1 -1
  114. package/src/router/debug-manifest.ts +16 -3
  115. package/src/router/handler-context.ts +96 -17
  116. package/src/router/intercept-resolution.ts +6 -4
  117. package/src/router/lazy-includes.ts +4 -0
  118. package/src/router/loader-resolution.ts +6 -11
  119. package/src/router/logging.ts +100 -3
  120. package/src/router/manifest.ts +32 -3
  121. package/src/router/match-api.ts +62 -54
  122. package/src/router/match-context.ts +3 -0
  123. package/src/router/match-handlers.ts +185 -11
  124. package/src/router/match-middleware/background-revalidation.ts +65 -85
  125. package/src/router/match-middleware/cache-lookup.ts +78 -10
  126. package/src/router/match-middleware/cache-store.ts +2 -0
  127. package/src/router/match-pipelines.ts +8 -43
  128. package/src/router/match-result.ts +0 -9
  129. package/src/router/metrics.ts +233 -13
  130. package/src/router/middleware-types.ts +34 -39
  131. package/src/router/middleware.ts +290 -130
  132. package/src/router/pattern-matching.ts +61 -10
  133. package/src/router/prerender-match.ts +36 -6
  134. package/src/router/preview-match.ts +7 -1
  135. package/src/router/revalidation.ts +61 -2
  136. package/src/router/router-context.ts +15 -0
  137. package/src/router/router-interfaces.ts +158 -40
  138. package/src/router/router-options.ts +223 -1
  139. package/src/router/router-registry.ts +5 -2
  140. package/src/router/segment-resolution/fresh.ts +165 -242
  141. package/src/router/segment-resolution/helpers.ts +263 -0
  142. package/src/router/segment-resolution/loader-cache.ts +102 -98
  143. package/src/router/segment-resolution/revalidation.ts +394 -272
  144. package/src/router/segment-resolution/static-store.ts +2 -2
  145. package/src/router/segment-resolution.ts +1 -3
  146. package/src/router/segment-wrappers.ts +3 -0
  147. package/src/router/telemetry-otel.ts +299 -0
  148. package/src/router/telemetry.ts +300 -0
  149. package/src/router/timeout.ts +148 -0
  150. package/src/router/trie-matching.ts +20 -2
  151. package/src/router/types.ts +7 -1
  152. package/src/router.ts +203 -18
  153. package/src/rsc/handler-context.ts +13 -2
  154. package/src/rsc/handler.ts +489 -438
  155. package/src/rsc/helpers.ts +125 -5
  156. package/src/rsc/index.ts +0 -20
  157. package/src/rsc/loader-fetch.ts +84 -42
  158. package/src/rsc/manifest-init.ts +3 -2
  159. package/src/rsc/origin-guard.ts +141 -0
  160. package/src/rsc/progressive-enhancement.ts +245 -19
  161. package/src/rsc/response-route-handler.ts +347 -0
  162. package/src/rsc/rsc-rendering.ts +47 -43
  163. package/src/rsc/runtime-warnings.ts +42 -0
  164. package/src/rsc/server-action.ts +166 -66
  165. package/src/rsc/ssr-setup.ts +128 -0
  166. package/src/rsc/types.ts +20 -2
  167. package/src/search-params.ts +38 -23
  168. package/src/server/context.ts +61 -7
  169. package/src/server/cookie-store.ts +190 -0
  170. package/src/server/fetchable-loader-store.ts +11 -6
  171. package/src/server/handle-store.ts +84 -12
  172. package/src/server/loader-registry.ts +11 -46
  173. package/src/server/request-context.ts +275 -49
  174. package/src/server.ts +6 -0
  175. package/src/ssr/index.tsx +67 -28
  176. package/src/static-handler.ts +7 -0
  177. package/src/theme/ThemeProvider.tsx +6 -1
  178. package/src/theme/index.ts +4 -18
  179. package/src/theme/theme-context.ts +1 -28
  180. package/src/theme/theme-script.ts +2 -1
  181. package/src/types/cache-types.ts +6 -1
  182. package/src/types/error-types.ts +3 -0
  183. package/src/types/global-namespace.ts +22 -0
  184. package/src/types/handler-context.ts +103 -16
  185. package/src/types/index.ts +1 -1
  186. package/src/types/loader-types.ts +9 -6
  187. package/src/types/route-config.ts +17 -26
  188. package/src/types/route-entry.ts +28 -0
  189. package/src/types/segments.ts +0 -5
  190. package/src/urls/include-helper.ts +49 -8
  191. package/src/urls/index.ts +1 -0
  192. package/src/urls/path-helper-types.ts +30 -12
  193. package/src/urls/path-helper.ts +17 -2
  194. package/src/urls/pattern-types.ts +21 -1
  195. package/src/urls/response-types.ts +29 -7
  196. package/src/urls/type-extraction.ts +23 -15
  197. package/src/use-loader.tsx +27 -9
  198. package/src/vite/discovery/bundle-postprocess.ts +32 -52
  199. package/src/vite/discovery/discover-routers.ts +52 -26
  200. package/src/vite/discovery/prerender-collection.ts +58 -41
  201. package/src/vite/discovery/route-types-writer.ts +7 -7
  202. package/src/vite/discovery/state.ts +7 -7
  203. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  204. package/src/vite/index.ts +10 -51
  205. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  206. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  207. package/src/vite/plugins/expose-internal-ids.ts +4 -3
  208. package/src/vite/plugins/refresh-cmd.ts +65 -0
  209. package/src/vite/plugins/use-cache-transform.ts +91 -3
  210. package/src/vite/plugins/version-plugin.ts +188 -18
  211. package/src/vite/rango.ts +61 -36
  212. package/src/vite/router-discovery.ts +173 -100
  213. package/src/vite/utils/prerender-utils.ts +81 -0
  214. package/src/vite/utils/shared-utils.ts +19 -9
  215. package/skills/testing/SKILL.md +0 -226
  216. package/src/browser/lru-cache.ts +0 -61
  217. package/src/browser/react/prefetch.ts +0 -27
  218. package/src/browser/request-controller.ts +0 -164
  219. package/src/cache/memory-store.ts +0 -253
  220. package/src/href-context.ts +0 -33
  221. package/src/route-definition/route-function.ts +0 -119
  222. package/src/router.gen.ts +0 -6
  223. package/src/static-handler.gen.ts +0 -5
  224. package/src/urls.gen.ts +0 -8
  225. /package/{CLAUDE.md → AGENTS.md} +0 -0
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: loader
3
3
  description: Define data loaders for fetching data in routes with createLoader
4
- argument-hint: [name]
4
+ argument-hint: [loader]
5
5
  ---
6
6
 
7
7
  # Data Loaders with loader()
@@ -13,7 +13,9 @@ Loaders fetch data on the server and stream it to the client.
13
13
  ```typescript
14
14
  import { createLoader } from "@rangojs/router";
15
15
 
16
- export const ProductLoader = createLoader("product", async (ctx) => {
16
+ export const ProductLoader = createLoader(async (ctx) => {
17
+ "use server";
18
+
17
19
  const product = await ctx.env.DB.prepare(
18
20
  "SELECT * FROM products WHERE slug = ?",
19
21
  )
@@ -30,19 +32,19 @@ All of the following are equivalent and fully supported by the Vite transform:
30
32
 
31
33
  ```typescript
32
34
  // Direct export (most common)
33
- export const ProductLoader = createLoader("product", handler);
35
+ export const ProductLoader = createLoader(handler);
34
36
 
35
37
  // Separate declaration + named export
36
- const ProductLoader = createLoader("product", handler);
38
+ const ProductLoader = createLoader(handler);
37
39
  export { ProductLoader };
38
40
 
39
41
  // Aliased export
40
- const InternalLoader = createLoader("product", handler);
42
+ const InternalLoader = createLoader(handler);
41
43
  export { InternalLoader as ProductLoader };
42
44
 
43
45
  // Aliased import
44
46
  import { createLoader as cl } from "@rangojs/router";
45
- export const ProductLoader = cl("product", handler);
47
+ export const ProductLoader = cl(handler);
46
48
  ```
47
49
 
48
50
  The `export const` form and the `const + export { }` form both work for
@@ -66,7 +68,7 @@ export const urlpatterns = urls(({ path, loader }) => [
66
68
  ### In Server Components
67
69
 
68
70
  ```typescript
69
- import { useLoader } from "@rangojs/router";
71
+ import { useLoader } from "@rangojs/router/client";
70
72
  import { ProductLoader } from "./loaders/product";
71
73
 
72
74
  async function ProductPage() {
@@ -79,12 +81,12 @@ async function ProductPage() {
79
81
 
80
82
  ```typescript
81
83
  "use client";
82
- import { useLoaderData } from "@rangojs/router/client";
84
+ import { useLoader } from "@rangojs/router/client";
83
85
  import { ProductLoader } from "./loaders/product";
84
86
 
85
87
  function ProductDetails() {
86
- const { product } = useLoaderData(ProductLoader);
87
- return <div>{product.description}</div>;
88
+ const { data } = useLoader(ProductLoader);
89
+ return <div>{data.product.description}</div>;
88
90
  }
89
91
  ```
90
92
 
@@ -93,10 +95,15 @@ function ProductDetails() {
93
95
  Loaders receive the same context as route handlers:
94
96
 
95
97
  ```typescript
96
- export const ProductLoader = createLoader("product", async (ctx) => {
97
- // URL params
98
+ export const ProductLoader = createLoader(async (ctx) => {
99
+ "use server";
100
+
101
+ // URL params (may include client-provided overrides for fetchable loaders)
98
102
  const { slug } = ctx.params;
99
103
 
104
+ // Server-trusted route params (from URL pattern matching, cannot be overridden)
105
+ const { slug: trustedSlug } = ctx.routeParams;
106
+
100
107
  // Query params
101
108
  const variant = ctx.url.searchParams.get("variant");
102
109
 
@@ -113,6 +120,33 @@ export const ProductLoader = createLoader("product", async (ctx) => {
113
120
  });
114
121
  ```
115
122
 
123
+ ### params vs routeParams
124
+
125
+ - `ctx.params` — merged route params + explicit loader params. For fetchable
126
+ loaders called with `load(Loader, { params: { ... } })`, explicit params
127
+ override route-matched params.
128
+ - `ctx.routeParams` — server-trusted route params from URL pattern matching.
129
+ Cannot be overridden by client-provided params.
130
+
131
+ Use `ctx.routeParams` when you need trusted route identity for authorization
132
+ or resource scoping:
133
+
134
+ ```typescript
135
+ export const OrderLoader = createLoader(async (ctx) => {
136
+ "use server";
137
+
138
+ // Use routeParams for auth checks — client cannot spoof the URL-matched ID
139
+ const { orderId } = ctx.routeParams;
140
+ const user = ctx.get("user");
141
+
142
+ const order = await db.orders.get(orderId);
143
+ if (order.userId !== user.id)
144
+ throw new Response("Forbidden", { status: 403 });
145
+
146
+ return { order };
147
+ });
148
+ ```
149
+
116
150
  ## Loader with Children
117
151
 
118
152
  Add caching or revalidation to specific loaders:
@@ -134,6 +168,157 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
134
168
  ]);
135
169
  ```
136
170
 
171
+ ### Revalidation Contracts for Loader Dependencies
172
+
173
+ If a loader reads `ctx.get()` data produced by an outer handler/layout, share
174
+ the same named revalidation contract across producer and consumer segments.
175
+
176
+ ```typescript
177
+ // revalidation-contracts.ts
178
+ export const revalidateAccountScope = ({ actionId }) =>
179
+ actionId?.includes("src/actions/account.ts#") ?? false;
180
+
181
+ layout(AccountLayout, () => [
182
+ revalidate(revalidateAccountScope), // producer reruns
183
+ path("/account/orders", OrdersPage, { name: "account.orders" }, () => [
184
+ loader(OrdersLoader, () => [
185
+ revalidate(revalidateAccountScope), // consumer reruns
186
+ ]),
187
+ ]),
188
+ ]);
189
+ ```
190
+
191
+ For segments that depend on multiple upstream domains, compose multiple
192
+ contracts on both sides.
193
+
194
+ To keep loader route trees concise, export helper wrappers:
195
+
196
+ ```typescript
197
+ import { revalidate } from "@rangojs/router";
198
+
199
+ export const revalidateAccount = () => [revalidate(revalidateAccountScope)];
200
+
201
+ layout(AccountLayout, () => [
202
+ revalidateAccount(),
203
+ path("/account/orders", OrdersPage, { name: "account.orders" }, () => [
204
+ loader(OrdersLoader, () => [revalidateAccount()]),
205
+ ]),
206
+ ]);
207
+ ```
208
+
209
+ ## Loaders: The Live Data Layer
210
+
211
+ Loaders are the live data layer of the router. They resolve fresh on every
212
+ request, even when the route's UI segments are served from cache. This is a
213
+ core design principle — route-level `cache()` caches rendered components but
214
+ never caches loader data. Loaders are excluded at storage time and re-resolved
215
+ on retrieval.
216
+
217
+ This means `cache()` gives you cached UI + fresh data by default. Pre-rendering
218
+ follows the same rule: at build time, loaders are skipped entirely (there is no
219
+ real request context), and at runtime the worker resolves them fresh against
220
+ the live database.
221
+
222
+ ### Opting a Loader into Caching
223
+
224
+ To cache a specific loader's data, attach a `cache()` child:
225
+
226
+ ```typescript
227
+ loader(ProductLoader, () => [cache({ ttl: 300 })]),
228
+ ```
229
+
230
+ The loader's data is cached independently from the route's segment cache,
231
+ using the same `SegmentCacheStore` (app-level or per-loader override).
232
+
233
+ Values are serialized through RSC Flight, so loaders can return ReactNode,
234
+ Promises, null, and any RSC-serializable type — all round-trip correctly
235
+ through the cache.
236
+
237
+ ### Cache Key
238
+
239
+ The default cache key is `loader:{loaderId}:{pathname}:{sortedParams}`.
240
+ This can be customized at two levels:
241
+
242
+ ```typescript
243
+ // Full override — key function replaces the default entirely
244
+ loader(ProductLoader, () => [
245
+ cache({
246
+ ttl: 300,
247
+ key: (ctx) => `product:${ctx.params.slug}:${cookies().get("locale")?.value ?? "en"}`,
248
+ }),
249
+ ]),
250
+
251
+ // Store-level keyGenerator — modifies the default key (e.g., adds a region prefix)
252
+ // Set in the store configuration, applies to all entries in that store
253
+ ```
254
+
255
+ Resolution priority (same as route-level `cache()`):
256
+
257
+ 1. `key(ctx)` from cache options — full override
258
+ 2. `store.keyGenerator(ctx, defaultKey)` — store-level modification
259
+ 3. Default key — `loader:{id}:{pathname}:{params}`
260
+
261
+ If a custom key function throws, it falls back to the default key silently
262
+ (logged to console.error).
263
+
264
+ ### Tags for Invalidation
265
+
266
+ ```typescript
267
+ // Static tags
268
+ loader(ProductLoader, () => [
269
+ cache({ ttl: 300, tags: ["products", "catalog"] }),
270
+ ]),
271
+
272
+ // Dynamic tags
273
+ loader(ProductLoader, () => [
274
+ cache({
275
+ ttl: 300,
276
+ tags: (ctx) => [`product:${ctx.params.slug}`, "products"],
277
+ }),
278
+ ]),
279
+ ```
280
+
281
+ ### Stale-While-Revalidate
282
+
283
+ ```typescript
284
+ loader(ProductLoader, () => [
285
+ cache({ ttl: 60, swr: 300 }),
286
+ ]),
287
+ ```
288
+
289
+ During the SWR window (60-360s), stale data is returned immediately while
290
+ fresh data is fetched in the background via `waitUntil`. After the SWR window
291
+ expires (360s+), the entry is treated as a cache miss.
292
+
293
+ ### Conditional Caching
294
+
295
+ Skip the cache at runtime based on request properties:
296
+
297
+ ```typescript
298
+ loader(ProductLoader, () => [
299
+ cache({
300
+ ttl: 300,
301
+ condition: (ctx) => !ctx.request.headers.has("authorization"),
302
+ }),
303
+ ]),
304
+ ```
305
+
306
+ When `condition` returns false, the loader runs fresh and the cache is bypassed
307
+ entirely (no read, no write).
308
+
309
+ ### Per-Loader Store Override
310
+
311
+ ```typescript
312
+ const hotStore = new MemorySegmentCacheStore({ defaults: { ttl: 10 } });
313
+
314
+ loader(PricingLoader, () => [
315
+ cache({ store: hotStore }),
316
+ ]),
317
+ ```
318
+
319
+ Without an explicit store, the loader uses the app-level store from the
320
+ handler config (`cache.store`).
321
+
137
322
  ## Multiple Loaders
138
323
 
139
324
  Routes can have multiple loaders that run in parallel:
@@ -232,6 +417,31 @@ export const SearchLoader = createLoader(async (ctx) => {
232
417
  }, true); // true = fetchable
233
418
  ```
234
419
 
420
+ ### Fetchable Loader with Middleware
421
+
422
+ Pass an options object instead of `true` to attach per-loader middleware.
423
+ This middleware runs only on `_rsc_loader` fetch requests (client-side
424
+ `load()` / `useFetchLoader()` calls), not during SSR `ctx.use()` execution:
425
+
426
+ ```typescript
427
+ import { createLoader } from "@rangojs/router";
428
+ import { authMiddleware } from "../middleware/auth";
429
+ import { rateLimitMiddleware } from "../middleware/rate-limit";
430
+
431
+ export const ProtectedLoader = createLoader(
432
+ async (ctx) => {
433
+ "use server";
434
+
435
+ const user = ctx.get("user");
436
+ return { orders: await db.orders.list(user.id) };
437
+ },
438
+ { middleware: [authMiddleware, rateLimitMiddleware] },
439
+ );
440
+ ```
441
+
442
+ The middleware uses the same `MiddlewareFn` signature as route/app middleware,
443
+ so you can reuse existing middleware functions directly.
444
+
235
445
  Fetchable loaders support both GET and POST (PUT, PATCH, DELETE) from the client.
236
446
  The `load()` function auto-detects the body type:
237
447
 
@@ -288,7 +498,9 @@ Client usage — see `/hooks useFetchLoader` for the full client-side pattern.
288
498
  // loaders/shop.ts
289
499
  import { createLoader } from "@rangojs/router";
290
500
 
291
- export const ProductLoader = createLoader("product", async (ctx) => {
501
+ export const ProductLoader = createLoader(async (ctx) => {
502
+ "use server";
503
+
292
504
  const product = await ctx.env.DB
293
505
  .prepare("SELECT * FROM products WHERE slug = ?")
294
506
  .bind(ctx.params.slug)
@@ -301,7 +513,9 @@ export const ProductLoader = createLoader("product", async (ctx) => {
301
513
  return { product };
302
514
  });
303
515
 
304
- export const CartLoader = createLoader("cart", async (ctx) => {
516
+ export const CartLoader = createLoader(async (ctx) => {
517
+ "use server";
518
+
305
519
  const user = ctx.get("user");
306
520
  if (!user) return { cart: null };
307
521
 
@@ -325,7 +539,7 @@ export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalid
325
539
  ]);
326
540
 
327
541
  // pages/product.tsx
328
- import { useLoader } from "@rangojs/router";
542
+ import { useLoader } from "@rangojs/router/client";
329
543
  import { ProductLoader, CartLoader } from "./loaders/shop";
330
544
 
331
545
  async function ProductPage() {
@@ -8,12 +8,93 @@ 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
+ ### 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
+
11
92
  ## Basic Middleware
12
93
 
13
94
  ```typescript
14
- import { createMiddleware } from "@rangojs/router";
95
+ import type { Middleware } from "@rangojs/router";
15
96
 
16
- export const authMiddleware = createMiddleware(async (ctx, next) => {
97
+ export const authMiddleware: Middleware = async (ctx, next) => {
17
98
  const token = ctx.request.headers.get("Authorization");
18
99
 
19
100
  if (!token) {
@@ -24,7 +105,7 @@ export const authMiddleware = createMiddleware(async (ctx, next) => {
24
105
  ctx.set("user", user);
25
106
 
26
107
  await next();
27
- });
108
+ };
28
109
  ```
29
110
 
30
111
  ## Using Middleware in Routes
@@ -68,7 +149,7 @@ layout(<ShopLayout />, () => [
68
149
  ## Middleware Context
69
150
 
70
151
  ```typescript
71
- export const myMiddleware = createMiddleware(async (ctx, next) => {
152
+ export const myMiddleware: Middleware = async (ctx, next) => {
72
153
  // Access request
73
154
  ctx.request; // Request object
74
155
  ctx.url; // Parsed URL
@@ -86,7 +167,7 @@ export const myMiddleware = createMiddleware(async (ctx, next) => {
86
167
 
87
168
  // After handler (response intercepting)
88
169
  console.log("Handler completed");
89
- });
170
+ };
90
171
  ```
91
172
 
92
173
  ### Typed context variables in middleware
@@ -94,19 +175,20 @@ export const myMiddleware = createMiddleware(async (ctx, next) => {
94
175
  Use `createVar<T>()` for type-safe data sharing between middleware and handlers:
95
176
 
96
177
  ```typescript
97
- import { createMiddleware, createVar } from "@rangojs/router";
178
+ import { createVar } from "@rangojs/router";
179
+ import type { Middleware } from "@rangojs/router";
98
180
 
99
181
  interface AuthUser { id: string; email: string; role: string }
100
182
  export const CurrentUser = createVar<AuthUser>();
101
183
 
102
- export const authMiddleware = createMiddleware(async (ctx, next) => {
184
+ export const authMiddleware: Middleware = async (ctx, next) => {
103
185
  const token = ctx.request.headers.get("Authorization");
104
186
  if (!token) throw new Response("Unauthorized", { status: 401 });
105
187
 
106
188
  const user = await verifyToken(token);
107
189
  ctx.set(CurrentUser, user); // type-checked
108
190
  await next();
109
- });
191
+ };
110
192
 
111
193
  // In a handler -- typed read
112
194
  import { CurrentUser } from "./middleware";
@@ -124,17 +206,14 @@ data; use RSCRouter.Vars for app-wide middleware state.
124
206
  ## Redirect with State in Middleware
125
207
 
126
208
  ```typescript
127
- import {
128
- createMiddleware,
129
- redirect,
130
- createLocationState,
131
- } from "@rangojs/router";
209
+ import { redirect, createLocationState } from "@rangojs/router";
210
+ import type { Middleware } from "@rangojs/router";
132
211
 
133
212
  export const FlashMessage = createLocationState<{ text: string }>({
134
213
  flash: true,
135
214
  });
136
215
 
137
- export const requireAuthMiddleware = createMiddleware(async (ctx, next) => {
216
+ export const requireAuthMiddleware: Middleware = async (ctx, next) => {
138
217
  const token = ctx.request.headers.get("Authorization");
139
218
  if (!token) {
140
219
  return redirect("/login", {
@@ -142,7 +221,7 @@ export const requireAuthMiddleware = createMiddleware(async (ctx, next) => {
142
221
  });
143
222
  }
144
223
  await next();
145
- });
224
+ };
146
225
  ```
147
226
 
148
227
  Read the flash on the target page with `useLocationState(FlashMessage)`. The `{ flash: true }` option makes it auto-clear after first render. See `/hooks`.
@@ -150,7 +229,7 @@ Read the flash on the target page with `useLocationState(FlashMessage)`. The `{
150
229
  ## Authentication Middleware
151
230
 
152
231
  ```typescript
153
- export const requireAuthMiddleware = createMiddleware(async (ctx, next) => {
232
+ export const requireAuthMiddleware: Middleware = async (ctx, next) => {
154
233
  const user = ctx.get("user");
155
234
 
156
235
  if (!user) {
@@ -158,9 +237,9 @@ export const requireAuthMiddleware = createMiddleware(async (ctx, next) => {
158
237
  }
159
238
 
160
239
  await next();
161
- });
240
+ };
162
241
 
163
- export const permissionsMiddleware = createMiddleware(async (ctx, next) => {
242
+ export const permissionsMiddleware: Middleware = async (ctx, next) => {
164
243
  const user = ctx.get("user");
165
244
  const requiredPermission = "admin";
166
245
 
@@ -169,13 +248,13 @@ export const permissionsMiddleware = createMiddleware(async (ctx, next) => {
169
248
  }
170
249
 
171
250
  await next();
172
- });
251
+ };
173
252
  ```
174
253
 
175
254
  ## Logger Middleware
176
255
 
177
256
  ```typescript
178
- export const loggerMiddleware = createMiddleware(async (ctx, next) => {
257
+ export const loggerMiddleware: Middleware = async (ctx, next) => {
179
258
  const start = Date.now();
180
259
 
181
260
  console.log(`[${ctx.request.method}] ${ctx.url.pathname}`);
@@ -184,13 +263,13 @@ export const loggerMiddleware = createMiddleware(async (ctx, next) => {
184
263
 
185
264
  const duration = Date.now() - start;
186
265
  console.log(`[${ctx.request.method}] ${ctx.url.pathname} - ${duration}ms`);
187
- });
266
+ };
188
267
  ```
189
268
 
190
269
  ## Rate Limiting Middleware
191
270
 
192
271
  ```typescript
193
- export const rateLimitMiddleware = createMiddleware(async (ctx, next) => {
272
+ export const rateLimitMiddleware: Middleware = async (ctx, next) => {
194
273
  const ip = ctx.request.headers.get("CF-Connecting-IP") ?? "unknown";
195
274
  const key = `rate-limit:${ip}`;
196
275
 
@@ -206,32 +285,32 @@ export const rateLimitMiddleware = createMiddleware(async (ctx, next) => {
206
285
  });
207
286
 
208
287
  await next();
209
- });
288
+ };
210
289
  ```
211
290
 
212
291
  ## Complete Example
213
292
 
214
293
  ```typescript
215
294
  // middleware/index.ts
216
- import { createMiddleware } from "@rangojs/router";
295
+ import type { Middleware } from "@rangojs/router";
217
296
 
218
- export const loggerMiddleware = createMiddleware(async (ctx, next) => {
297
+ export const loggerMiddleware: Middleware = async (ctx, next) => {
219
298
  console.log(`[${ctx.request.method}] ${ctx.url.pathname}`);
220
299
  await next();
221
- });
300
+ };
222
301
 
223
- export const mockAuthMiddleware = createMiddleware(async (ctx, next) => {
302
+ export const mockAuthMiddleware: Middleware = async (ctx, next) => {
224
303
  // Mock user for development
225
304
  ctx.set("user", { id: "1", name: "Demo User" });
226
305
  await next();
227
- });
306
+ };
228
307
 
229
- export const requireAuthMiddleware = createMiddleware(async (ctx, next) => {
308
+ export const requireAuthMiddleware: Middleware = async (ctx, next) => {
230
309
  if (!ctx.get("user")) {
231
310
  throw new Response("Unauthorized", { status: 401 });
232
311
  }
233
312
  await next();
234
- });
313
+ };
235
314
 
236
315
  // urls.tsx
237
316
  import { urls } from "@rangojs/router";
@@ -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
@@ -56,8 +59,21 @@ parallel({
56
59
 
57
60
  ## Reading Handler Data
58
61
 
59
- When a parallel is inside a route that uses `ctx.set()`, it can read that
60
- data via `ctx.get()`. The route handler always executes before its children.
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.
61
77
 
62
78
  ```typescript
63
79
  path("/dashboard/:id", (ctx) => {
@@ -142,6 +158,45 @@ parallel(
142
158
  )
143
159
  ```
144
160
 
161
+ Revalidating only the parallel does not re-run outer handlers/layouts.
162
+ If the slot reads `ctx.get()` data established above it, opt the outer
163
+ segment into revalidation as well.
164
+
165
+ ### Revalidation Contracts for Parallel Dependencies
166
+
167
+ Prefer named revalidation contracts shared by both the upstream producer and
168
+ the parallel consumer:
169
+
170
+ ```typescript
171
+ // revalidation-contracts.ts
172
+ export const revalidateCartData = ({ actionId }) =>
173
+ actionId?.includes("src/actions/cart.ts#") ?? false;
174
+
175
+ layout(CartLayout, () => [
176
+ revalidate(revalidateCartData), // producer reruns
177
+ parallel(
178
+ { "@cart": CartSummary },
179
+ () => [revalidate(revalidateCartData)], // consumer reruns
180
+ ),
181
+ ]);
182
+ ```
183
+
184
+ If the slot consumes multiple upstream domains, compose the contracts on both
185
+ segments.
186
+
187
+ Handoff helper style also works:
188
+
189
+ ```typescript
190
+ import { revalidate } from "@rangojs/router";
191
+
192
+ export const revalidateCart = () => [revalidate(revalidateCartData)];
193
+
194
+ layout(CartLayout, () => [
195
+ revalidateCart(),
196
+ parallel({ "@cart": CartSummary }, () => [revalidateCart()]),
197
+ ]);
198
+ ```
199
+
145
200
  ## Named Outlets
146
201
 
147
202
  Use `ParallelOutlet` to render slots in layouts: