@rangojs/router 0.0.0-experimental.3 → 0.0.0-experimental.30

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 (297) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +883 -4
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +4655 -747
  5. package/package.json +78 -50
  6. package/skills/cache-guide/SKILL.md +262 -0
  7. package/skills/caching/SKILL.md +54 -25
  8. package/skills/composability/SKILL.md +172 -0
  9. package/skills/debug-manifest/SKILL.md +12 -8
  10. package/skills/document-cache/SKILL.md +23 -21
  11. package/skills/fonts/SKILL.md +167 -0
  12. package/skills/hooks/SKILL.md +390 -63
  13. package/skills/host-router/SKILL.md +218 -0
  14. package/skills/intercept/SKILL.md +133 -10
  15. package/skills/layout/SKILL.md +102 -5
  16. package/skills/links/SKILL.md +239 -0
  17. package/skills/loader/SKILL.md +366 -29
  18. package/skills/middleware/SKILL.md +173 -36
  19. package/skills/mime-routes/SKILL.md +128 -0
  20. package/skills/parallel/SKILL.md +80 -3
  21. package/skills/prerender/SKILL.md +643 -0
  22. package/skills/rango/SKILL.md +86 -16
  23. package/skills/response-routes/SKILL.md +411 -0
  24. package/skills/route/SKILL.md +227 -14
  25. package/skills/router-setup/SKILL.md +225 -32
  26. package/skills/tailwind/SKILL.md +129 -0
  27. package/skills/theme/SKILL.md +12 -11
  28. package/skills/typesafety/SKILL.md +401 -75
  29. package/skills/use-cache/SKILL.md +324 -0
  30. package/src/__internal.ts +10 -4
  31. package/src/bin/rango.ts +321 -0
  32. package/src/browser/action-coordinator.ts +97 -0
  33. package/src/browser/action-response-classifier.ts +99 -0
  34. package/src/browser/event-controller.ts +87 -64
  35. package/src/browser/history-state.ts +80 -0
  36. package/src/browser/intercept-utils.ts +52 -0
  37. package/src/browser/link-interceptor.ts +20 -4
  38. package/src/browser/logging.ts +55 -0
  39. package/src/browser/merge-segment-loaders.ts +20 -12
  40. package/src/browser/navigation-bridge.ts +201 -553
  41. package/src/browser/navigation-client.ts +124 -71
  42. package/src/browser/navigation-store.ts +33 -50
  43. package/src/browser/navigation-transaction.ts +295 -0
  44. package/src/browser/network-error-handler.ts +61 -0
  45. package/src/browser/partial-update.ts +267 -317
  46. package/src/browser/prefetch/cache.ts +146 -0
  47. package/src/browser/prefetch/fetch.ts +135 -0
  48. package/src/browser/prefetch/observer.ts +65 -0
  49. package/src/browser/prefetch/policy.ts +42 -0
  50. package/src/browser/prefetch/queue.ts +88 -0
  51. package/src/browser/rango-state.ts +112 -0
  52. package/src/browser/react/Link.tsx +173 -73
  53. package/src/browser/react/NavigationProvider.tsx +138 -27
  54. package/src/browser/react/context.ts +6 -0
  55. package/src/browser/react/filter-segment-order.ts +11 -0
  56. package/src/browser/react/index.ts +12 -12
  57. package/src/browser/react/location-state-shared.ts +95 -53
  58. package/src/browser/react/location-state.ts +60 -15
  59. package/src/browser/react/mount-context.ts +37 -0
  60. package/src/browser/react/nonce-context.ts +23 -0
  61. package/src/browser/react/shallow-equal.ts +27 -0
  62. package/src/browser/react/use-action.ts +29 -51
  63. package/src/browser/react/use-client-cache.ts +5 -3
  64. package/src/browser/react/use-handle.ts +49 -65
  65. package/src/browser/react/use-href.tsx +20 -188
  66. package/src/browser/react/use-link-status.ts +6 -5
  67. package/src/browser/react/use-mount.ts +31 -0
  68. package/src/browser/react/use-navigation.ts +27 -78
  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 +111 -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 +83 -0
  79. package/src/browser/server-action-bridge.ts +504 -584
  80. package/src/browser/shallow.ts +6 -1
  81. package/src/browser/types.ts +92 -57
  82. package/src/browser/validate-redirect-origin.ts +29 -0
  83. package/src/build/generate-manifest.ts +438 -0
  84. package/src/build/generate-route-types.ts +36 -0
  85. package/src/build/index.ts +35 -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 +10 -15
  114. package/src/client.tsx +114 -135
  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 +34 -19
  121. package/src/handles/MetaTags.tsx +73 -20
  122. package/src/handles/meta.ts +30 -13
  123. package/src/host/cookie-handler.ts +165 -0
  124. package/src/host/errors.ts +97 -0
  125. package/src/host/index.ts +53 -0
  126. package/src/host/pattern-matcher.ts +214 -0
  127. package/src/host/router.ts +352 -0
  128. package/src/host/testing.ts +79 -0
  129. package/src/host/types.ts +146 -0
  130. package/src/host/utils.ts +25 -0
  131. package/src/href-client.ts +135 -49
  132. package/src/index.rsc.ts +182 -17
  133. package/src/index.ts +238 -24
  134. package/src/internal-debug.ts +11 -0
  135. package/src/loader.rsc.ts +27 -142
  136. package/src/loader.ts +27 -10
  137. package/src/network-error-thrower.tsx +3 -1
  138. package/src/outlet-provider.tsx +45 -0
  139. package/src/prerender/param-hash.ts +37 -0
  140. package/src/prerender/store.ts +185 -0
  141. package/src/prerender.ts +463 -0
  142. package/src/reverse.ts +330 -0
  143. package/src/root-error-boundary.tsx +41 -29
  144. package/src/route-content-wrapper.tsx +9 -11
  145. package/src/route-definition/dsl-helpers.ts +934 -0
  146. package/src/route-definition/helper-factories.ts +200 -0
  147. package/src/route-definition/helpers-types.ts +430 -0
  148. package/src/route-definition/index.ts +52 -0
  149. package/src/route-definition/redirect.ts +93 -0
  150. package/src/route-definition.ts +1 -1388
  151. package/src/route-map-builder.ts +241 -112
  152. package/src/route-name.ts +53 -0
  153. package/src/route-types.ts +70 -9
  154. package/src/router/content-negotiation.ts +116 -0
  155. package/src/router/debug-manifest.ts +72 -0
  156. package/src/router/error-handling.ts +9 -9
  157. package/src/router/find-match.ts +158 -0
  158. package/src/router/handler-context.ts +371 -81
  159. package/src/router/intercept-resolution.ts +395 -0
  160. package/src/router/lazy-includes.ts +234 -0
  161. package/src/router/loader-resolution.ts +215 -122
  162. package/src/router/logging.ts +248 -0
  163. package/src/router/manifest.ts +155 -32
  164. package/src/router/match-api.ts +620 -0
  165. package/src/router/match-context.ts +5 -3
  166. package/src/router/match-handlers.ts +440 -0
  167. package/src/router/match-middleware/background-revalidation.ts +80 -93
  168. package/src/router/match-middleware/cache-lookup.ts +382 -9
  169. package/src/router/match-middleware/cache-store.ts +51 -22
  170. package/src/router/match-middleware/intercept-resolution.ts +55 -17
  171. package/src/router/match-middleware/segment-resolution.ts +24 -6
  172. package/src/router/match-pipelines.ts +10 -45
  173. package/src/router/match-result.ts +34 -29
  174. package/src/router/metrics.ts +235 -15
  175. package/src/router/middleware-cookies.ts +55 -0
  176. package/src/router/middleware-types.ts +222 -0
  177. package/src/router/middleware.ts +324 -367
  178. package/src/router/pattern-matching.ts +321 -30
  179. package/src/router/prerender-match.ts +400 -0
  180. package/src/router/preview-match.ts +170 -0
  181. package/src/router/revalidation.ts +137 -38
  182. package/src/router/router-context.ts +36 -21
  183. package/src/router/router-interfaces.ts +452 -0
  184. package/src/router/router-options.ts +592 -0
  185. package/src/router/router-registry.ts +24 -0
  186. package/src/router/segment-resolution/fresh.ts +570 -0
  187. package/src/router/segment-resolution/helpers.ts +263 -0
  188. package/src/router/segment-resolution/loader-cache.ts +198 -0
  189. package/src/router/segment-resolution/revalidation.ts +1241 -0
  190. package/src/router/segment-resolution/static-store.ts +67 -0
  191. package/src/router/segment-resolution.ts +21 -0
  192. package/src/router/segment-wrappers.ts +289 -0
  193. package/src/router/telemetry-otel.ts +299 -0
  194. package/src/router/telemetry.ts +300 -0
  195. package/src/router/timeout.ts +148 -0
  196. package/src/router/trie-matching.ts +239 -0
  197. package/src/router/types.ts +77 -3
  198. package/src/router.ts +688 -3656
  199. package/src/rsc/handler-context.ts +45 -0
  200. package/src/rsc/handler.ts +786 -760
  201. package/src/rsc/helpers.ts +140 -6
  202. package/src/rsc/index.ts +5 -25
  203. package/src/rsc/loader-fetch.ts +209 -0
  204. package/src/rsc/manifest-init.ts +86 -0
  205. package/src/rsc/nonce.ts +14 -0
  206. package/src/rsc/origin-guard.ts +141 -0
  207. package/src/rsc/progressive-enhancement.ts +379 -0
  208. package/src/rsc/response-error.ts +37 -0
  209. package/src/rsc/response-route-handler.ts +347 -0
  210. package/src/rsc/rsc-rendering.ts +235 -0
  211. package/src/rsc/runtime-warnings.ts +42 -0
  212. package/src/rsc/server-action.ts +348 -0
  213. package/src/rsc/ssr-setup.ts +128 -0
  214. package/src/rsc/types.ts +40 -14
  215. package/src/search-params.ts +230 -0
  216. package/src/segment-system.tsx +57 -61
  217. package/src/server/context.ts +202 -51
  218. package/src/server/cookie-store.ts +190 -0
  219. package/src/server/fetchable-loader-store.ts +37 -0
  220. package/src/server/handle-store.ts +94 -15
  221. package/src/server/loader-registry.ts +15 -56
  222. package/src/server/request-context.ts +422 -70
  223. package/src/server.ts +36 -120
  224. package/src/ssr/index.tsx +157 -26
  225. package/src/static-handler.ts +114 -0
  226. package/src/theme/ThemeProvider.tsx +21 -15
  227. package/src/theme/ThemeScript.tsx +5 -5
  228. package/src/theme/constants.ts +5 -2
  229. package/src/theme/index.ts +4 -14
  230. package/src/theme/theme-context.ts +4 -30
  231. package/src/theme/theme-script.ts +21 -18
  232. package/src/types/boundaries.ts +158 -0
  233. package/src/types/cache-types.ts +198 -0
  234. package/src/types/error-types.ts +192 -0
  235. package/src/types/global-namespace.ts +100 -0
  236. package/src/types/handler-context.ts +687 -0
  237. package/src/types/index.ts +88 -0
  238. package/src/types/loader-types.ts +183 -0
  239. package/src/types/route-config.ts +170 -0
  240. package/src/types/route-entry.ts +102 -0
  241. package/src/types/segments.ts +148 -0
  242. package/src/types.ts +1 -1577
  243. package/src/urls/include-helper.ts +197 -0
  244. package/src/urls/index.ts +53 -0
  245. package/src/urls/path-helper-types.ts +339 -0
  246. package/src/urls/path-helper.ts +329 -0
  247. package/src/urls/pattern-types.ts +95 -0
  248. package/src/urls/response-types.ts +106 -0
  249. package/src/urls/type-extraction.ts +372 -0
  250. package/src/urls/urls-function.ts +98 -0
  251. package/src/urls.ts +1 -726
  252. package/src/use-loader.tsx +85 -77
  253. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  254. package/src/vite/discovery/discover-routers.ts +344 -0
  255. package/src/vite/discovery/prerender-collection.ts +385 -0
  256. package/src/vite/discovery/route-types-writer.ts +258 -0
  257. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  258. package/src/vite/discovery/state.ts +110 -0
  259. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  260. package/src/vite/index.ts +11 -782
  261. package/src/vite/plugin-types.ts +131 -0
  262. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  263. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  264. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  265. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
  266. package/src/vite/plugins/expose-id-utils.ts +287 -0
  267. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  268. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  269. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  270. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  271. package/src/vite/plugins/expose-ids/types.ts +45 -0
  272. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  273. package/src/vite/plugins/refresh-cmd.ts +65 -0
  274. package/src/vite/plugins/use-cache-transform.ts +323 -0
  275. package/src/vite/plugins/version-injector.ts +83 -0
  276. package/src/vite/plugins/version-plugin.ts +254 -0
  277. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +29 -15
  278. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  279. package/src/vite/rango.ts +510 -0
  280. package/src/vite/router-discovery.ts +785 -0
  281. package/src/vite/utils/ast-handler-extract.ts +517 -0
  282. package/src/vite/utils/banner.ts +36 -0
  283. package/src/vite/utils/bundle-analysis.ts +137 -0
  284. package/src/vite/utils/manifest-utils.ts +70 -0
  285. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  286. package/src/vite/utils/prerender-utils.ts +189 -0
  287. package/src/vite/utils/shared-utils.ts +169 -0
  288. package/CLAUDE.md +0 -3
  289. package/src/browser/lru-cache.ts +0 -69
  290. package/src/browser/request-controller.ts +0 -164
  291. package/src/cache/memory-store.ts +0 -253
  292. package/src/href-context.ts +0 -33
  293. package/src/href.ts +0 -255
  294. package/src/vite/expose-handle-id.ts +0 -209
  295. package/src/vite/expose-loader-id.ts +0 -357
  296. package/src/vite/expose-location-state-id.ts +0 -177
  297. /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()
@@ -11,11 +11,14 @@ Loaders fetch data on the server and stream it to the client.
11
11
  ## Creating a Loader
12
12
 
13
13
  ```typescript
14
- import { createLoader } from "@rangojs/router/server";
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,10 +26,34 @@ 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
29
- import { urls } from "@rangojs/router/server";
56
+ import { urls } from "@rangojs/router";
30
57
  import { ProductLoader } from "./loaders/product";
31
58
 
32
59
  export const urlpatterns = urls(({ path, loader }) => [
@@ -41,7 +68,7 @@ export const urlpatterns = urls(({ path, loader }) => [
41
68
  ### In Server Components
42
69
 
43
70
  ```typescript
44
- import { useLoader } from "@rangojs/router/server";
71
+ import { useLoader } from "@rangojs/router/client";
45
72
  import { ProductLoader } from "./loaders/product";
46
73
 
47
74
  async function ProductPage() {
@@ -54,12 +81,12 @@ async function ProductPage() {
54
81
 
55
82
  ```typescript
56
83
  "use client";
57
- import { useLoaderData } from "@rangojs/router/client";
84
+ import { useLoader } from "@rangojs/router/client";
58
85
  import { ProductLoader } from "./loaders/product";
59
86
 
60
87
  function ProductDetails() {
61
- const { product } = useLoaderData(ProductLoader);
62
- return <div>{product.description}</div>;
88
+ const { data } = useLoader(ProductLoader);
89
+ return <div>{data.product.description}</div>;
63
90
  }
64
91
  ```
65
92
 
@@ -68,26 +95,58 @@ function ProductDetails() {
68
95
  Loaders receive the same context as route handlers:
69
96
 
70
97
  ```typescript
71
- export const ProductLoader = createLoader("product", async (ctx) => {
72
- // URL params
98
+ export const ProductLoader = createLoader(async (ctx) => {
99
+ "use server";
100
+
101
+ // URL params (may include client-provided overrides for fetchable loaders)
73
102
  const { slug } = ctx.params;
74
103
 
104
+ // Server-trusted route params (from URL pattern matching, cannot be overridden)
105
+ const { slug: trustedSlug } = ctx.routeParams;
106
+
75
107
  // Query params
76
108
  const variant = ctx.url.searchParams.get("variant");
77
109
 
78
- // Environment (DB, KV, etc.)
79
- const db = ctx.env.Bindings.DB;
110
+ // Platform bindings (DB, KV, etc.) — plain bindings from createRouter<TEnv>()
111
+ const db = ctx.env.DB;
80
112
 
81
113
  // Request headers
82
114
  const auth = ctx.request.headers.get("Authorization");
83
115
 
84
- // Variables set by middleware
85
- const user = ctx.env.Variables.user;
116
+ // Variables set by middleware (from RSCRouter.Vars augmentation)
117
+ const user = ctx.get("user");
86
118
 
87
119
  return { product: await fetchProduct(slug) };
88
120
  });
89
121
  ```
90
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
+
91
150
  ## Loader with Children
92
151
 
93
152
  Add caching or revalidation to specific loaders:
@@ -95,22 +154,171 @@ Add caching or revalidation to specific loaders:
95
154
  ```typescript
96
155
  path("/product/:slug", ProductPage, { name: "product" }, () => [
97
156
  // Cached loader
98
- loader(ProductLoader, () => [
99
- cache({ ttl: 300 }),
100
- ]),
157
+ loader(ProductLoader, () => [cache({ ttl: 300 })]),
101
158
 
102
159
  // Loader with revalidation control
103
160
  loader(RelatedProductsLoader, () => [
104
- revalidate(() => false), // Never revalidate
161
+ revalidate(() => false), // Never revalidate
105
162
  ]),
106
163
 
107
164
  // Loader that revalidates after cart actions
108
165
  loader(CartLoader, () => [
109
166
  revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
110
167
  ]),
111
- ])
168
+ ]);
169
+ ```
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
+ ]),
112
304
  ```
113
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
+
114
322
  ## Multiple Loaders
115
323
 
116
324
  Routes can have multiple loaders that run in parallel:
@@ -120,7 +328,7 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
120
328
  loader(ProductLoader),
121
329
  loader(RelatedProductsLoader),
122
330
  loader(ReviewsLoader),
123
- ])
331
+ ]);
124
332
  ```
125
333
 
126
334
  ## Layout Loaders
@@ -138,6 +346,33 @@ layout(<ShopLayout />, () => [
138
346
  ])
139
347
  ```
140
348
 
349
+ ## Passing Loaders as Props
350
+
351
+ Loaders can be passed as props from server to client components. RSC serialization
352
+ uses `toJSON()` to send only `{ __brand, $$id }` — the loader function is stripped.
353
+
354
+ ```typescript
355
+ // Server component (route handler)
356
+ import { SlowLoader } from "../loaders";
357
+
358
+ path("/dashboard", () => <DashboardContent loader={SlowLoader} />, { name: "dashboard" }, () => [
359
+ loader(SlowLoader),
360
+ loading(<DashboardSkeleton />),
361
+ ])
362
+
363
+ // Client component — use typeof for type-safe props
364
+ "use client";
365
+ import { useLoader } from "@rangojs/router/client";
366
+ import type { SlowLoader } from "../loaders";
367
+
368
+ function DashboardContent({ loader }: { loader: typeof SlowLoader }) {
369
+ const { data } = useLoader(loader);
370
+ return <div>{data.message}</div>;
371
+ }
372
+ ```
373
+
374
+ Use `typeof MyLoader` for the prop type — it infers the full generic automatically.
375
+
141
376
  ## Streaming with Suspense
142
377
 
143
378
  Loaders stream data. Use Suspense for loading states:
@@ -159,14 +394,114 @@ function ProductPage() {
159
394
  }
160
395
  ```
161
396
 
397
+ ## Fetchable Loaders
398
+
399
+ By default, loaders only run during SSR and navigation. Pass `true` as the second
400
+ argument to `createLoader` to make a loader **fetchable** — callable from the client
401
+ via `useFetchLoader()` and `load()`:
402
+
403
+ ```typescript
404
+ import { createLoader } from "@rangojs/router";
405
+
406
+ export const SearchLoader = createLoader(async (ctx) => {
407
+ "use server";
408
+
409
+ const query = ctx.params.query ?? "";
410
+ const results = await ctx.env.DB.prepare(
411
+ "SELECT * FROM products WHERE name LIKE ?",
412
+ )
413
+ .bind(`%${query}%`)
414
+ .all();
415
+
416
+ return { results: results.results ?? [] };
417
+ }, true); // true = fetchable
418
+ ```
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
+
445
+ Fetchable loaders support both GET and POST (PUT, PATCH, DELETE) from the client.
446
+ The `load()` function auto-detects the body type:
447
+
448
+ - **JSON body** (`body: { ... }`) — sent as `application/json`, available as `ctx.body`
449
+ - **FormData body** (`body: formData`) — sent as `multipart/form-data`, available as `ctx.formData`
450
+
451
+ ### Mutation Context
452
+
453
+ When a fetchable loader receives a POST/PUT/PATCH/DELETE request, the context
454
+ includes additional fields depending on the body type:
455
+
456
+ ```typescript
457
+ export const MutationLoader = createLoader(async (ctx) => {
458
+ "use server";
459
+
460
+ // JSON body — available as ctx.body (parsed object)
461
+ const data = ctx.body as { name: string; email: string };
462
+
463
+ // FormData body — available as ctx.formData
464
+ const file = ctx.formData?.get("file") as File | null;
465
+ const name = ctx.formData?.get("name") as string | null;
466
+
467
+ // Route params are always available
468
+ const { slug } = ctx.params;
469
+
470
+ return { success: true };
471
+ }, true);
472
+ ```
473
+
474
+ ### File Upload Example
475
+
476
+ ```typescript
477
+ // loaders/upload.ts
478
+ import { createLoader } from "@rangojs/router";
479
+
480
+ export const FileUploadLoader = createLoader(async (ctx) => {
481
+ "use server";
482
+
483
+ const file = ctx.formData?.get("file") as File | null;
484
+ if (file && file.size > 0) {
485
+ // Save to R2, D1, etc.
486
+ await ctx.env.BUCKET.put(file.name, file.stream());
487
+ return { uploaded: { name: file.name, size: file.size, type: file.type } };
488
+ }
489
+ return { uploaded: null };
490
+ }, true);
491
+ ```
492
+
493
+ Client usage — see `/hooks useFetchLoader` for the full client-side pattern.
494
+
162
495
  ## Complete Example
163
496
 
164
497
  ```typescript
165
498
  // loaders/shop.ts
166
- import { createLoader } from "@rangojs/router/server";
499
+ import { createLoader } from "@rangojs/router";
167
500
 
168
- export const ProductLoader = createLoader("product", async (ctx) => {
169
- const product = await ctx.env.Bindings.DB
501
+ export const ProductLoader = createLoader(async (ctx) => {
502
+ "use server";
503
+
504
+ const product = await ctx.env.DB
170
505
  .prepare("SELECT * FROM products WHERE slug = ?")
171
506
  .bind(ctx.params.slug)
172
507
  .first();
@@ -178,11 +513,13 @@ export const ProductLoader = createLoader("product", async (ctx) => {
178
513
  return { product };
179
514
  });
180
515
 
181
- export const CartLoader = createLoader("cart", async (ctx) => {
182
- const user = ctx.env.Variables.user;
516
+ export const CartLoader = createLoader(async (ctx) => {
517
+ "use server";
518
+
519
+ const user = ctx.get("user");
183
520
  if (!user) return { cart: null };
184
521
 
185
- const cart = await ctx.env.Bindings.KV.get(`cart:${user.id}`, "json");
522
+ const cart = await ctx.env.KV.get(`cart:${user.id}`, "json");
186
523
  return { cart };
187
524
  });
188
525
 
@@ -202,7 +539,7 @@ export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalid
202
539
  ]);
203
540
 
204
541
  // pages/product.tsx
205
- import { useLoader } from "@rangojs/router/server";
542
+ import { useLoader } from "@rangojs/router/client";
206
543
  import { ProductLoader, CartLoader } from "./loaders/shop";
207
544
 
208
545
  async function ProductPage() {