@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.8a4d0430

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 (300) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +884 -4
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +4474 -867
  5. package/package.json +60 -51
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +50 -21
  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 +89 -30
  18. package/skills/loader/SKILL.md +388 -38
  19. package/skills/middleware/SKILL.md +171 -34
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +78 -1
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +85 -16
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +226 -14
  26. package/skills/router-setup/SKILL.md +123 -30
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +9 -8
  29. package/skills/typesafety/SKILL.md +318 -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/event-controller.ts +87 -64
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/intercept-utils.ts +52 -0
  38. package/src/browser/link-interceptor.ts +24 -4
  39. package/src/browser/logging.ts +55 -0
  40. package/src/browser/merge-segment-loaders.ts +20 -12
  41. package/src/browser/navigation-bridge.ts +285 -553
  42. package/src/browser/navigation-client.ts +124 -71
  43. package/src/browser/navigation-store.ts +33 -50
  44. package/src/browser/navigation-transaction.ts +295 -0
  45. package/src/browser/network-error-handler.ts +61 -0
  46. package/src/browser/partial-update.ts +258 -308
  47. package/src/browser/prefetch/cache.ts +146 -0
  48. package/src/browser/prefetch/fetch.ts +135 -0
  49. package/src/browser/prefetch/observer.ts +65 -0
  50. package/src/browser/prefetch/policy.ts +42 -0
  51. package/src/browser/prefetch/queue.ts +88 -0
  52. package/src/browser/rango-state.ts +112 -0
  53. package/src/browser/react/Link.tsx +185 -73
  54. package/src/browser/react/NavigationProvider.tsx +51 -11
  55. package/src/browser/react/context.ts +6 -0
  56. package/src/browser/react/filter-segment-order.ts +11 -0
  57. package/src/browser/react/index.ts +12 -12
  58. package/src/browser/react/location-state-shared.ts +95 -53
  59. package/src/browser/react/location-state.ts +60 -15
  60. package/src/browser/react/mount-context.ts +6 -1
  61. package/src/browser/react/nonce-context.ts +23 -0
  62. package/src/browser/react/shallow-equal.ts +27 -0
  63. package/src/browser/react/use-action.ts +29 -51
  64. package/src/browser/react/use-client-cache.ts +5 -3
  65. package/src/browser/react/use-handle.ts +32 -79
  66. package/src/browser/react/use-href.tsx +2 -2
  67. package/src/browser/react/use-link-status.ts +6 -5
  68. package/src/browser/react/use-navigation.ts +22 -63
  69. package/src/browser/react/use-params.ts +65 -0
  70. package/src/browser/react/use-pathname.ts +47 -0
  71. package/src/browser/react/use-router.ts +63 -0
  72. package/src/browser/react/use-search-params.ts +56 -0
  73. package/src/browser/react/use-segments.ts +80 -97
  74. package/src/browser/response-adapter.ts +73 -0
  75. package/src/browser/rsc-router.tsx +107 -26
  76. package/src/browser/scroll-restoration.ts +92 -16
  77. package/src/browser/segment-reconciler.ts +216 -0
  78. package/src/browser/segment-structure-assert.ts +16 -0
  79. package/src/browser/server-action-bridge.ts +504 -599
  80. package/src/browser/shallow.ts +6 -1
  81. package/src/browser/types.ts +109 -47
  82. package/src/browser/validate-redirect-origin.ts +29 -0
  83. package/src/build/generate-manifest.ts +235 -24
  84. package/src/build/generate-route-types.ts +36 -0
  85. package/src/build/index.ts +13 -0
  86. package/src/build/route-trie.ts +265 -0
  87. package/src/build/route-types/ast-helpers.ts +25 -0
  88. package/src/build/route-types/ast-route-extraction.ts +98 -0
  89. package/src/build/route-types/codegen.ts +102 -0
  90. package/src/build/route-types/include-resolution.ts +411 -0
  91. package/src/build/route-types/param-extraction.ts +48 -0
  92. package/src/build/route-types/per-module-writer.ts +128 -0
  93. package/src/build/route-types/router-processing.ts +469 -0
  94. package/src/build/route-types/scan-filter.ts +78 -0
  95. package/src/build/runtime-discovery.ts +231 -0
  96. package/src/cache/background-task.ts +34 -0
  97. package/src/cache/cache-key-utils.ts +44 -0
  98. package/src/cache/cache-policy.ts +125 -0
  99. package/src/cache/cache-runtime.ts +338 -0
  100. package/src/cache/cache-scope.ts +120 -303
  101. package/src/cache/cf/cf-cache-store.ts +119 -7
  102. package/src/cache/cf/index.ts +8 -2
  103. package/src/cache/document-cache.ts +101 -72
  104. package/src/cache/handle-capture.ts +81 -0
  105. package/src/cache/handle-snapshot.ts +41 -0
  106. package/src/cache/index.ts +0 -15
  107. package/src/cache/memory-segment-store.ts +191 -13
  108. package/src/cache/profile-registry.ts +73 -0
  109. package/src/cache/read-through-swr.ts +134 -0
  110. package/src/cache/segment-codec.ts +256 -0
  111. package/src/cache/taint.ts +98 -0
  112. package/src/cache/types.ts +72 -122
  113. package/src/client.rsc.tsx +3 -1
  114. package/src/client.tsx +106 -126
  115. package/src/component-utils.ts +4 -4
  116. package/src/components/DefaultDocument.tsx +5 -1
  117. package/src/context-var.ts +86 -0
  118. package/src/debug.ts +17 -7
  119. package/src/errors.ts +108 -2
  120. package/src/handle.ts +15 -29
  121. package/src/handles/MetaTags.tsx +73 -20
  122. package/src/handles/breadcrumbs.ts +66 -0
  123. package/src/handles/index.ts +1 -0
  124. package/src/handles/meta.ts +30 -13
  125. package/src/host/cookie-handler.ts +21 -15
  126. package/src/host/errors.ts +8 -8
  127. package/src/host/index.ts +4 -7
  128. package/src/host/pattern-matcher.ts +27 -27
  129. package/src/host/router.ts +61 -39
  130. package/src/host/testing.ts +8 -8
  131. package/src/host/types.ts +15 -7
  132. package/src/host/utils.ts +1 -1
  133. package/src/href-client.ts +119 -29
  134. package/src/index.rsc.ts +153 -19
  135. package/src/index.ts +211 -30
  136. package/src/internal-debug.ts +11 -0
  137. package/src/loader.rsc.ts +26 -157
  138. package/src/loader.ts +27 -10
  139. package/src/network-error-thrower.tsx +3 -1
  140. package/src/outlet-provider.tsx +45 -0
  141. package/src/prerender/param-hash.ts +37 -0
  142. package/src/prerender/store.ts +185 -0
  143. package/src/prerender.ts +463 -0
  144. package/src/reverse.ts +330 -0
  145. package/src/root-error-boundary.tsx +41 -29
  146. package/src/route-content-wrapper.tsx +7 -4
  147. package/src/route-definition/dsl-helpers.ts +934 -0
  148. package/src/route-definition/helper-factories.ts +200 -0
  149. package/src/route-definition/helpers-types.ts +430 -0
  150. package/src/route-definition/index.ts +52 -0
  151. package/src/route-definition/redirect.ts +93 -0
  152. package/src/route-definition.ts +1 -1428
  153. package/src/route-map-builder.ts +211 -123
  154. package/src/route-name.ts +53 -0
  155. package/src/route-types.ts +59 -8
  156. package/src/router/content-negotiation.ts +116 -0
  157. package/src/router/debug-manifest.ts +72 -0
  158. package/src/router/error-handling.ts +9 -9
  159. package/src/router/find-match.ts +158 -0
  160. package/src/router/handler-context.ts +374 -81
  161. package/src/router/intercept-resolution.ts +395 -0
  162. package/src/router/lazy-includes.ts +234 -0
  163. package/src/router/loader-resolution.ts +215 -122
  164. package/src/router/logging.ts +248 -0
  165. package/src/router/manifest.ts +148 -35
  166. package/src/router/match-api.ts +620 -0
  167. package/src/router/match-context.ts +5 -3
  168. package/src/router/match-handlers.ts +440 -0
  169. package/src/router/match-middleware/background-revalidation.ts +80 -93
  170. package/src/router/match-middleware/cache-lookup.ts +382 -9
  171. package/src/router/match-middleware/cache-store.ts +51 -22
  172. package/src/router/match-middleware/intercept-resolution.ts +55 -17
  173. package/src/router/match-middleware/segment-resolution.ts +24 -6
  174. package/src/router/match-pipelines.ts +10 -45
  175. package/src/router/match-result.ts +34 -28
  176. package/src/router/metrics.ts +235 -15
  177. package/src/router/middleware-cookies.ts +55 -0
  178. package/src/router/middleware-types.ts +222 -0
  179. package/src/router/middleware.ts +324 -367
  180. package/src/router/pattern-matching.ts +211 -43
  181. package/src/router/prerender-match.ts +402 -0
  182. package/src/router/preview-match.ts +170 -0
  183. package/src/router/revalidation.ts +137 -38
  184. package/src/router/router-context.ts +36 -21
  185. package/src/router/router-interfaces.ts +452 -0
  186. package/src/router/router-options.ts +592 -0
  187. package/src/router/router-registry.ts +24 -0
  188. package/src/router/segment-resolution/fresh.ts +570 -0
  189. package/src/router/segment-resolution/helpers.ts +263 -0
  190. package/src/router/segment-resolution/loader-cache.ts +198 -0
  191. package/src/router/segment-resolution/revalidation.ts +1241 -0
  192. package/src/router/segment-resolution/static-store.ts +67 -0
  193. package/src/router/segment-resolution.ts +21 -0
  194. package/src/router/segment-wrappers.ts +289 -0
  195. package/src/router/telemetry-otel.ts +299 -0
  196. package/src/router/telemetry.ts +300 -0
  197. package/src/router/timeout.ts +148 -0
  198. package/src/router/trie-matching.ts +239 -0
  199. package/src/router/types.ts +77 -3
  200. package/src/router.ts +692 -4257
  201. package/src/rsc/handler-context.ts +45 -0
  202. package/src/rsc/handler.ts +764 -754
  203. package/src/rsc/helpers.ts +140 -6
  204. package/src/rsc/index.ts +0 -20
  205. package/src/rsc/loader-fetch.ts +209 -0
  206. package/src/rsc/manifest-init.ts +86 -0
  207. package/src/rsc/nonce.ts +14 -0
  208. package/src/rsc/origin-guard.ts +141 -0
  209. package/src/rsc/progressive-enhancement.ts +379 -0
  210. package/src/rsc/response-error.ts +37 -0
  211. package/src/rsc/response-route-handler.ts +347 -0
  212. package/src/rsc/rsc-rendering.ts +235 -0
  213. package/src/rsc/runtime-warnings.ts +42 -0
  214. package/src/rsc/server-action.ts +348 -0
  215. package/src/rsc/ssr-setup.ts +128 -0
  216. package/src/rsc/types.ts +38 -11
  217. package/src/search-params.ts +230 -0
  218. package/src/segment-system.tsx +25 -13
  219. package/src/server/context.ts +182 -51
  220. package/src/server/cookie-store.ts +190 -0
  221. package/src/server/fetchable-loader-store.ts +37 -0
  222. package/src/server/handle-store.ts +94 -15
  223. package/src/server/loader-registry.ts +15 -56
  224. package/src/server/request-context.ts +430 -70
  225. package/src/server.ts +35 -130
  226. package/src/ssr/index.tsx +100 -31
  227. package/src/static-handler.ts +114 -0
  228. package/src/theme/ThemeProvider.tsx +21 -15
  229. package/src/theme/ThemeScript.tsx +5 -5
  230. package/src/theme/constants.ts +5 -2
  231. package/src/theme/index.ts +4 -14
  232. package/src/theme/theme-context.ts +4 -30
  233. package/src/theme/theme-script.ts +21 -18
  234. package/src/types/boundaries.ts +158 -0
  235. package/src/types/cache-types.ts +198 -0
  236. package/src/types/error-types.ts +192 -0
  237. package/src/types/global-namespace.ts +100 -0
  238. package/src/types/handler-context.ts +687 -0
  239. package/src/types/index.ts +88 -0
  240. package/src/types/loader-types.ts +183 -0
  241. package/src/types/route-config.ts +170 -0
  242. package/src/types/route-entry.ts +102 -0
  243. package/src/types/segments.ts +148 -0
  244. package/src/types.ts +1 -1623
  245. package/src/urls/include-helper.ts +197 -0
  246. package/src/urls/index.ts +53 -0
  247. package/src/urls/path-helper-types.ts +339 -0
  248. package/src/urls/path-helper.ts +329 -0
  249. package/src/urls/pattern-types.ts +95 -0
  250. package/src/urls/response-types.ts +106 -0
  251. package/src/urls/type-extraction.ts +372 -0
  252. package/src/urls/urls-function.ts +98 -0
  253. package/src/urls.ts +1 -802
  254. package/src/use-loader.tsx +85 -77
  255. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  256. package/src/vite/discovery/discover-routers.ts +344 -0
  257. package/src/vite/discovery/prerender-collection.ts +385 -0
  258. package/src/vite/discovery/route-types-writer.ts +258 -0
  259. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  260. package/src/vite/discovery/state.ts +110 -0
  261. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  262. package/src/vite/index.ts +11 -1133
  263. package/src/vite/plugin-types.ts +131 -0
  264. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  265. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  266. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  267. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
  268. package/src/vite/plugins/expose-id-utils.ts +287 -0
  269. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  270. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  271. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  272. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  273. package/src/vite/plugins/expose-ids/types.ts +45 -0
  274. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  275. package/src/vite/plugins/refresh-cmd.ts +65 -0
  276. package/src/vite/plugins/use-cache-transform.ts +323 -0
  277. package/src/vite/plugins/version-injector.ts +83 -0
  278. package/src/vite/plugins/version-plugin.ts +254 -0
  279. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  280. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  281. package/src/vite/rango.ts +510 -0
  282. package/src/vite/router-discovery.ts +785 -0
  283. package/src/vite/utils/ast-handler-extract.ts +517 -0
  284. package/src/vite/utils/banner.ts +36 -0
  285. package/src/vite/utils/bundle-analysis.ts +137 -0
  286. package/src/vite/utils/manifest-utils.ts +70 -0
  287. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  288. package/src/vite/utils/prerender-utils.ts +189 -0
  289. package/src/vite/utils/shared-utils.ts +169 -0
  290. package/CLAUDE.md +0 -43
  291. package/src/browser/lru-cache.ts +0 -69
  292. package/src/browser/request-controller.ts +0 -164
  293. package/src/cache/memory-store.ts +0 -253
  294. package/src/href-context.ts +0 -33
  295. package/src/href.ts +0 -255
  296. package/src/server/route-manifest-cache.ts +0 -173
  297. package/src/vite/expose-handle-id.ts +0 -209
  298. package/src/vite/expose-loader-id.ts +0 -426
  299. package/src/vite/expose-location-state-id.ts +0 -177
  300. /package/src/vite/{version.d.ts → plugins/version.d.ts} +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,9 +13,12 @@ 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) => {
17
- const product = await ctx.env.Bindings.DB
18
- .prepare("SELECT * FROM products WHERE slug = ?")
16
+ export const ProductLoader = createLoader(async (ctx) => {
17
+ "use server";
18
+
19
+ const product = await ctx.env.DB.prepare(
20
+ "SELECT * FROM products WHERE slug = ?",
21
+ )
19
22
  .bind(ctx.params.slug)
20
23
  .first();
21
24
 
@@ -23,6 +26,30 @@ export const ProductLoader = createLoader("product", async (ctx) => {
23
26
  });
24
27
  ```
25
28
 
29
+ ### Supported export patterns
30
+
31
+ All of the following are equivalent and fully supported by the Vite transform:
32
+
33
+ ```typescript
34
+ // Direct export (most common)
35
+ export const ProductLoader = createLoader(handler);
36
+
37
+ // Separate declaration + named export
38
+ const ProductLoader = createLoader(handler);
39
+ export { ProductLoader };
40
+
41
+ // Aliased export
42
+ const InternalLoader = createLoader(handler);
43
+ export { InternalLoader as ProductLoader };
44
+
45
+ // Aliased import
46
+ import { createLoader as cl } from "@rangojs/router";
47
+ export const ProductLoader = cl(handler);
48
+ ```
49
+
50
+ The `export const` form and the `const + export { }` form both work for
51
+ client stubs, ID injection, and loader manifest tracking.
52
+
26
53
  ## Using Loaders in Routes
27
54
 
28
55
  ```typescript
@@ -38,56 +65,129 @@ export const urlpatterns = urls(({ path, loader }) => [
38
65
 
39
66
  ## Consuming Loader Data
40
67
 
41
- ### In Server Components
68
+ Loaders are the **live data layer** — they resolve fresh on every request.
69
+ The way you consume them depends on whether you're in a server component
70
+ (route handler) or a client component.
71
+
72
+ > **IMPORTANT: Prefer consuming loaders in client components.** Keeping data
73
+ > fetching in loaders and consumption in client components creates a clean
74
+ > separation: the server-side handler renders static markup that can be
75
+ > freely cached with `cache()`, while loader data stays fresh on every
76
+ > request. When you consume loaders in server handlers via `ctx.use()`, the
77
+ > handler output depends on the loader data, which means caching the handler
78
+ > also caches the data — defeating the purpose of the live data layer.
79
+
80
+ ### In Client Components (Preferred)
81
+
82
+ Client components use `useLoader()` from `@rangojs/router/client`.
83
+ The loader **must** be registered with `loader()` in the route's DSL
84
+ segments so the framework knows to resolve it during SSR and stream
85
+ the data to the client:
42
86
 
43
87
  ```typescript
44
- import { useLoader } from "@rangojs/router";
88
+ "use client";
89
+ import { useLoader } from "@rangojs/router/client";
45
90
  import { ProductLoader } from "./loaders/product";
46
91
 
47
- async function ProductPage() {
48
- const { product } = await useLoader(ProductLoader);
49
- return <h1>{product.name}</h1>;
92
+ function ProductDetails() {
93
+ const { data } = useLoader(ProductLoader);
94
+ return <div>{data.product.description}</div>;
50
95
  }
51
96
  ```
52
97
 
53
- ### In Client Components
98
+ ```typescript
99
+ // Route definition — loader() registration required for client consumption
100
+ path("/product/:slug", ProductPage, { name: "product" }, () => [
101
+ loader(ProductLoader), // Required for useLoader() in client components
102
+ ]);
103
+ ```
104
+
105
+ ### In Route Handlers (Server Components)
106
+
107
+ In server components, use `ctx.use(Loader)` directly in the route handler.
108
+ This doesn't require `loader()` registration in the DSL — it works
109
+ standalone. **However**, prefer client-side consumption when possible (see
110
+ note above).
54
111
 
55
112
  ```typescript
56
- "use client";
57
- import { useLoaderData } from "@rangojs/router/client";
58
113
  import { ProductLoader } from "./loaders/product";
59
114
 
60
- function ProductDetails() {
61
- const { product } = useLoaderData(ProductLoader);
62
- return <div>{product.description}</div>;
63
- }
115
+ // Route handler — server component
116
+ path("/product/:slug", async (ctx) => {
117
+ const { product } = await ctx.use(ProductLoader);
118
+ return <h1>{product.name}</h1>;
119
+ }, { name: "product" })
64
120
  ```
65
121
 
122
+ When you do register with `loader()` in the DSL, `ctx.use()` returns the
123
+ same memoized result — loaders never run twice per request.
124
+
125
+ **Never use `useLoader()` in server components** — it is a client-only API.
126
+
127
+ ### Summary
128
+
129
+ | Context | API | `loader()` DSL required? |
130
+ | ---------------------------- | ------------------- | ------------------------ |
131
+ | Client component (preferred) | `useLoader(Loader)` | **Yes** |
132
+ | Route handler (server) | `ctx.use(Loader)` | No |
133
+
66
134
  ## Loader Context
67
135
 
68
136
  Loaders receive the same context as route handlers:
69
137
 
70
138
  ```typescript
71
- export const ProductLoader = createLoader("product", async (ctx) => {
72
- // URL params
139
+ export const ProductLoader = createLoader(async (ctx) => {
140
+ "use server";
141
+
142
+ // URL params (may include client-provided overrides for fetchable loaders)
73
143
  const { slug } = ctx.params;
74
144
 
145
+ // Server-trusted route params (from URL pattern matching, cannot be overridden)
146
+ const { slug: trustedSlug } = ctx.routeParams;
147
+
75
148
  // Query params
76
149
  const variant = ctx.url.searchParams.get("variant");
77
150
 
78
- // Environment (DB, KV, etc.)
79
- const db = ctx.env.Bindings.DB;
151
+ // Platform bindings (DB, KV, etc.) — plain bindings from createRouter<TEnv>()
152
+ const db = ctx.env.DB;
80
153
 
81
154
  // Request headers
82
155
  const auth = ctx.request.headers.get("Authorization");
83
156
 
84
- // Variables set by middleware
85
- const user = ctx.env.Variables.user;
157
+ // Variables set by middleware (from RSCRouter.Vars augmentation)
158
+ const user = ctx.get("user");
86
159
 
87
160
  return { product: await fetchProduct(slug) };
88
161
  });
89
162
  ```
90
163
 
164
+ ### params vs routeParams
165
+
166
+ - `ctx.params` — merged route params + explicit loader params. For fetchable
167
+ loaders called with `load(Loader, { params: { ... } })`, explicit params
168
+ override route-matched params.
169
+ - `ctx.routeParams` — server-trusted route params from URL pattern matching.
170
+ Cannot be overridden by client-provided params.
171
+
172
+ Use `ctx.routeParams` when you need trusted route identity for authorization
173
+ or resource scoping:
174
+
175
+ ```typescript
176
+ export const OrderLoader = createLoader(async (ctx) => {
177
+ "use server";
178
+
179
+ // Use routeParams for auth checks — client cannot spoof the URL-matched ID
180
+ const { orderId } = ctx.routeParams;
181
+ const user = ctx.get("user");
182
+
183
+ const order = await db.orders.get(orderId);
184
+ if (order.userId !== user.id)
185
+ throw new Response("Forbidden", { status: 403 });
186
+
187
+ return { order };
188
+ });
189
+ ```
190
+
91
191
  ## Loader with Children
92
192
 
93
193
  Add caching or revalidation to specific loaders:
@@ -95,22 +195,171 @@ Add caching or revalidation to specific loaders:
95
195
  ```typescript
96
196
  path("/product/:slug", ProductPage, { name: "product" }, () => [
97
197
  // Cached loader
98
- loader(ProductLoader, () => [
99
- cache({ ttl: 300 }),
100
- ]),
198
+ loader(ProductLoader, () => [cache({ ttl: 300 })]),
101
199
 
102
200
  // Loader with revalidation control
103
201
  loader(RelatedProductsLoader, () => [
104
- revalidate(() => false), // Never revalidate
202
+ revalidate(() => false), // Never revalidate
105
203
  ]),
106
204
 
107
205
  // Loader that revalidates after cart actions
108
206
  loader(CartLoader, () => [
109
207
  revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
110
208
  ]),
111
- ])
209
+ ]);
210
+ ```
211
+
212
+ ### Revalidation Contracts for Loader Dependencies
213
+
214
+ If a loader reads `ctx.get()` data produced by an outer handler/layout, share
215
+ the same named revalidation contract across producer and consumer segments.
216
+
217
+ ```typescript
218
+ // revalidation-contracts.ts
219
+ export const revalidateAccountScope = ({ actionId }) =>
220
+ actionId?.includes("src/actions/account.ts#") ?? false;
221
+
222
+ layout(AccountLayout, () => [
223
+ revalidate(revalidateAccountScope), // producer reruns
224
+ path("/account/orders", OrdersPage, { name: "account.orders" }, () => [
225
+ loader(OrdersLoader, () => [
226
+ revalidate(revalidateAccountScope), // consumer reruns
227
+ ]),
228
+ ]),
229
+ ]);
230
+ ```
231
+
232
+ For segments that depend on multiple upstream domains, compose multiple
233
+ contracts on both sides.
234
+
235
+ To keep loader route trees concise, export helper wrappers:
236
+
237
+ ```typescript
238
+ import { revalidate } from "@rangojs/router";
239
+
240
+ export const revalidateAccount = () => [revalidate(revalidateAccountScope)];
241
+
242
+ layout(AccountLayout, () => [
243
+ revalidateAccount(),
244
+ path("/account/orders", OrdersPage, { name: "account.orders" }, () => [
245
+ loader(OrdersLoader, () => [revalidateAccount()]),
246
+ ]),
247
+ ]);
248
+ ```
249
+
250
+ ## Loaders: The Live Data Layer
251
+
252
+ Loaders are the live data layer of the router. They resolve fresh on every
253
+ request, even when the route's UI segments are served from cache. This is a
254
+ core design principle — route-level `cache()` caches rendered components but
255
+ never caches loader data. Loaders are excluded at storage time and re-resolved
256
+ on retrieval.
257
+
258
+ This means `cache()` gives you cached UI + fresh data by default. Pre-rendering
259
+ follows the same rule: at build time, loaders are skipped entirely (there is no
260
+ real request context), and at runtime the worker resolves them fresh against
261
+ the live database.
262
+
263
+ ### Opting a Loader into Caching
264
+
265
+ To cache a specific loader's data, attach a `cache()` child:
266
+
267
+ ```typescript
268
+ loader(ProductLoader, () => [cache({ ttl: 300 })]),
112
269
  ```
113
270
 
271
+ The loader's data is cached independently from the route's segment cache,
272
+ using the same `SegmentCacheStore` (app-level or per-loader override).
273
+
274
+ Values are serialized through RSC Flight, so loaders can return ReactNode,
275
+ Promises, null, and any RSC-serializable type — all round-trip correctly
276
+ through the cache.
277
+
278
+ ### Cache Key
279
+
280
+ The default cache key is `loader:{loaderId}:{pathname}:{sortedParams}`.
281
+ This can be customized at two levels:
282
+
283
+ ```typescript
284
+ // Full override — key function replaces the default entirely
285
+ loader(ProductLoader, () => [
286
+ cache({
287
+ ttl: 300,
288
+ key: (ctx) => `product:${ctx.params.slug}:${cookies().get("locale")?.value ?? "en"}`,
289
+ }),
290
+ ]),
291
+
292
+ // Store-level keyGenerator — modifies the default key (e.g., adds a region prefix)
293
+ // Set in the store configuration, applies to all entries in that store
294
+ ```
295
+
296
+ Resolution priority (same as route-level `cache()`):
297
+
298
+ 1. `key(ctx)` from cache options — full override
299
+ 2. `store.keyGenerator(ctx, defaultKey)` — store-level modification
300
+ 3. Default key — `loader:{id}:{pathname}:{params}`
301
+
302
+ If a custom key function throws, it falls back to the default key silently
303
+ (logged to console.error).
304
+
305
+ ### Tags for Invalidation
306
+
307
+ ```typescript
308
+ // Static tags
309
+ loader(ProductLoader, () => [
310
+ cache({ ttl: 300, tags: ["products", "catalog"] }),
311
+ ]),
312
+
313
+ // Dynamic tags
314
+ loader(ProductLoader, () => [
315
+ cache({
316
+ ttl: 300,
317
+ tags: (ctx) => [`product:${ctx.params.slug}`, "products"],
318
+ }),
319
+ ]),
320
+ ```
321
+
322
+ ### Stale-While-Revalidate
323
+
324
+ ```typescript
325
+ loader(ProductLoader, () => [
326
+ cache({ ttl: 60, swr: 300 }),
327
+ ]),
328
+ ```
329
+
330
+ During the SWR window (60-360s), stale data is returned immediately while
331
+ fresh data is fetched in the background via `waitUntil`. After the SWR window
332
+ expires (360s+), the entry is treated as a cache miss.
333
+
334
+ ### Conditional Caching
335
+
336
+ Skip the cache at runtime based on request properties:
337
+
338
+ ```typescript
339
+ loader(ProductLoader, () => [
340
+ cache({
341
+ ttl: 300,
342
+ condition: (ctx) => !ctx.request.headers.has("authorization"),
343
+ }),
344
+ ]),
345
+ ```
346
+
347
+ When `condition` returns false, the loader runs fresh and the cache is bypassed
348
+ entirely (no read, no write).
349
+
350
+ ### Per-Loader Store Override
351
+
352
+ ```typescript
353
+ const hotStore = new MemorySegmentCacheStore({ defaults: { ttl: 10 } });
354
+
355
+ loader(PricingLoader, () => [
356
+ cache({ store: hotStore }),
357
+ ]),
358
+ ```
359
+
360
+ Without an explicit store, the loader uses the app-level store from the
361
+ handler config (`cache.store`).
362
+
114
363
  ## Multiple Loaders
115
364
 
116
365
  Routes can have multiple loaders that run in parallel:
@@ -120,7 +369,7 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
120
369
  loader(ProductLoader),
121
370
  loader(RelatedProductsLoader),
122
371
  loader(ReviewsLoader),
123
- ])
372
+ ]);
124
373
  ```
125
374
 
126
375
  ## Layout Loaders
@@ -186,14 +435,114 @@ function ProductPage() {
186
435
  }
187
436
  ```
188
437
 
438
+ ## Fetchable Loaders
439
+
440
+ By default, loaders only run during SSR and navigation. Pass `true` as the second
441
+ argument to `createLoader` to make a loader **fetchable** — callable from the client
442
+ via `useFetchLoader()` and `load()`:
443
+
444
+ ```typescript
445
+ import { createLoader } from "@rangojs/router";
446
+
447
+ export const SearchLoader = createLoader(async (ctx) => {
448
+ "use server";
449
+
450
+ const query = ctx.params.query ?? "";
451
+ const results = await ctx.env.DB.prepare(
452
+ "SELECT * FROM products WHERE name LIKE ?",
453
+ )
454
+ .bind(`%${query}%`)
455
+ .all();
456
+
457
+ return { results: results.results ?? [] };
458
+ }, true); // true = fetchable
459
+ ```
460
+
461
+ ### Fetchable Loader with Middleware
462
+
463
+ Pass an options object instead of `true` to attach per-loader middleware.
464
+ This middleware runs only on `_rsc_loader` fetch requests (client-side
465
+ `load()` / `useFetchLoader()` calls), not during SSR `ctx.use()` execution:
466
+
467
+ ```typescript
468
+ import { createLoader } from "@rangojs/router";
469
+ import { authMiddleware } from "../middleware/auth";
470
+ import { rateLimitMiddleware } from "../middleware/rate-limit";
471
+
472
+ export const ProtectedLoader = createLoader(
473
+ async (ctx) => {
474
+ "use server";
475
+
476
+ const user = ctx.get("user");
477
+ return { orders: await db.orders.list(user.id) };
478
+ },
479
+ { middleware: [authMiddleware, rateLimitMiddleware] },
480
+ );
481
+ ```
482
+
483
+ The middleware uses the same `MiddlewareFn` signature as route/app middleware,
484
+ so you can reuse existing middleware functions directly.
485
+
486
+ Fetchable loaders support both GET and POST (PUT, PATCH, DELETE) from the client.
487
+ The `load()` function auto-detects the body type:
488
+
489
+ - **JSON body** (`body: { ... }`) — sent as `application/json`, available as `ctx.body`
490
+ - **FormData body** (`body: formData`) — sent as `multipart/form-data`, available as `ctx.formData`
491
+
492
+ ### Mutation Context
493
+
494
+ When a fetchable loader receives a POST/PUT/PATCH/DELETE request, the context
495
+ includes additional fields depending on the body type:
496
+
497
+ ```typescript
498
+ export const MutationLoader = createLoader(async (ctx) => {
499
+ "use server";
500
+
501
+ // JSON body — available as ctx.body (parsed object)
502
+ const data = ctx.body as { name: string; email: string };
503
+
504
+ // FormData body — available as ctx.formData
505
+ const file = ctx.formData?.get("file") as File | null;
506
+ const name = ctx.formData?.get("name") as string | null;
507
+
508
+ // Route params are always available
509
+ const { slug } = ctx.params;
510
+
511
+ return { success: true };
512
+ }, true);
513
+ ```
514
+
515
+ ### File Upload Example
516
+
517
+ ```typescript
518
+ // loaders/upload.ts
519
+ import { createLoader } from "@rangojs/router";
520
+
521
+ export const FileUploadLoader = createLoader(async (ctx) => {
522
+ "use server";
523
+
524
+ const file = ctx.formData?.get("file") as File | null;
525
+ if (file && file.size > 0) {
526
+ // Save to R2, D1, etc.
527
+ await ctx.env.BUCKET.put(file.name, file.stream());
528
+ return { uploaded: { name: file.name, size: file.size, type: file.type } };
529
+ }
530
+ return { uploaded: null };
531
+ }, true);
532
+ ```
533
+
534
+ Client usage — see `/hooks useFetchLoader` for the full client-side pattern.
535
+
189
536
  ## Complete Example
190
537
 
191
538
  ```typescript
192
539
  // loaders/shop.ts
193
540
  import { createLoader } from "@rangojs/router";
194
541
 
195
- export const ProductLoader = createLoader("product", async (ctx) => {
196
- const product = await ctx.env.Bindings.DB
542
+ export const ProductLoader = createLoader(async (ctx) => {
543
+ "use server";
544
+
545
+ const product = await ctx.env.DB
197
546
  .prepare("SELECT * FROM products WHERE slug = ?")
198
547
  .bind(ctx.params.slug)
199
548
  .first();
@@ -205,11 +554,13 @@ export const ProductLoader = createLoader("product", async (ctx) => {
205
554
  return { product };
206
555
  });
207
556
 
208
- export const CartLoader = createLoader("cart", async (ctx) => {
209
- const user = ctx.env.Variables.user;
557
+ export const CartLoader = createLoader(async (ctx) => {
558
+ "use server";
559
+
560
+ const user = ctx.get("user");
210
561
  if (!user) return { cart: null };
211
562
 
212
- const cart = await ctx.env.Bindings.KV.get(`cart:${user.id}`, "json");
563
+ const cart = await ctx.env.KV.get(`cart:${user.id}`, "json");
213
564
  return { cart };
214
565
  });
215
566
 
@@ -228,13 +579,12 @@ export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalid
228
579
  ]),
229
580
  ]);
230
581
 
231
- // pages/product.tsx
232
- import { useLoader } from "@rangojs/router";
582
+ // pages/product.tsx — server component (route handler)
233
583
  import { ProductLoader, CartLoader } from "./loaders/shop";
234
584
 
235
- async function ProductPage() {
236
- const { product } = await useLoader(ProductLoader);
237
- const { cart } = await useLoader(CartLoader);
585
+ async function ProductPage(ctx) {
586
+ const { product } = await ctx.use(ProductLoader);
587
+ const { cart } = await ctx.use(CartLoader);
238
588
 
239
589
  return (
240
590
  <div>