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

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