@rangojs/router 0.0.0-experimental.14 → 0.0.0-experimental.140

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 (460) hide show
  1. package/AGENTS.md +17 -0
  2. package/README.md +432 -7
  3. package/dist/bin/rango.js +2073 -213
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +7258 -2714
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +140 -67
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +329 -0
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +487 -0
  12. package/skills/caching/SKILL.md +357 -25
  13. package/skills/comparison/SKILL.md +50 -0
  14. package/skills/comparison/agents/openai.yaml +4 -0
  15. package/skills/comparison/references/framework-comparison.md +837 -0
  16. package/skills/composability/SKILL.md +246 -0
  17. package/skills/css/SKILL.md +76 -0
  18. package/skills/debug-manifest/SKILL.md +16 -10
  19. package/skills/document-cache/SKILL.md +87 -62
  20. package/skills/fonts/SKILL.md +6 -4
  21. package/skills/handler-use/SKILL.md +364 -0
  22. package/skills/hooks/SKILL.md +557 -79
  23. package/skills/host-router/SKILL.md +320 -0
  24. package/skills/i18n/SKILL.md +276 -0
  25. package/skills/intercept/SKILL.md +207 -15
  26. package/skills/layout/SKILL.md +146 -6
  27. package/skills/links/SKILL.md +304 -25
  28. package/skills/loader/SKILL.md +616 -54
  29. package/skills/middleware/SKILL.md +217 -37
  30. package/skills/migrate-nextjs/SKILL.md +611 -0
  31. package/skills/migrate-react-router/SKILL.md +927 -0
  32. package/skills/mime-routes/SKILL.md +42 -11
  33. package/skills/observability/SKILL.md +194 -0
  34. package/skills/parallel/SKILL.md +284 -3
  35. package/skills/ppr/SKILL.md +426 -0
  36. package/skills/prerender/SKILL.md +437 -52
  37. package/skills/rango/SKILL.md +369 -22
  38. package/skills/react-compiler/SKILL.md +168 -0
  39. package/skills/response-routes/SKILL.md +263 -121
  40. package/skills/route/SKILL.md +350 -21
  41. package/skills/router-setup/SKILL.md +246 -33
  42. package/skills/scripts/SKILL.md +179 -0
  43. package/skills/server-actions/SKILL.md +775 -0
  44. package/skills/shell-manifest/SKILL.md +185 -0
  45. package/skills/streams-and-websockets/SKILL.md +283 -0
  46. package/skills/tailwind/SKILL.md +27 -3
  47. package/skills/testing/SKILL.md +126 -222
  48. package/skills/testing/bindings.md +103 -0
  49. package/skills/testing/cache-prerender.md +127 -0
  50. package/skills/testing/client-components.md +124 -0
  51. package/skills/testing/e2e-parity.md +125 -0
  52. package/skills/testing/flight.md +91 -0
  53. package/skills/testing/handles.md +131 -0
  54. package/skills/testing/loader.md +128 -0
  55. package/skills/testing/middleware.md +99 -0
  56. package/skills/testing/render-handler.md +122 -0
  57. package/skills/testing/response-routes.md +95 -0
  58. package/skills/testing/reverse-and-types.md +85 -0
  59. package/skills/testing/server-actions.md +107 -0
  60. package/skills/testing/server-tree.md +128 -0
  61. package/skills/testing/setup.md +123 -0
  62. package/skills/theme/SKILL.md +9 -8
  63. package/skills/typesafety/SKILL.md +532 -103
  64. package/skills/use-cache/SKILL.md +367 -0
  65. package/skills/vercel/SKILL.md +128 -0
  66. package/skills/view-transitions/SKILL.md +337 -0
  67. package/src/__augment-tests__/augment.ts +81 -0
  68. package/src/__augment-tests__/augmented.check.ts +116 -0
  69. package/src/__internal.ts +77 -44
  70. package/src/bin/rango.ts +312 -15
  71. package/src/browser/action-coordinator.ts +114 -0
  72. package/src/browser/action-fence.ts +47 -0
  73. package/src/browser/app-shell.ts +39 -0
  74. package/src/browser/app-version.ts +14 -0
  75. package/src/browser/connection-warmup.ts +134 -0
  76. package/src/browser/cookie-name.ts +140 -0
  77. package/src/browser/event-controller.ts +293 -202
  78. package/src/browser/history-state.ts +101 -0
  79. package/src/browser/index.ts +3 -3
  80. package/src/browser/intercept-utils.ts +52 -0
  81. package/src/browser/invalidate-client-cache.ts +52 -0
  82. package/src/browser/link-interceptor.ts +24 -4
  83. package/src/browser/logging.ts +11 -0
  84. package/src/browser/merge-segment-loaders.ts +20 -12
  85. package/src/browser/navigation-bridge.ts +385 -576
  86. package/src/browser/navigation-client.ts +245 -75
  87. package/src/browser/navigation-store-handle.ts +38 -0
  88. package/src/browser/navigation-store.ts +184 -118
  89. package/src/browser/navigation-transaction.ts +247 -0
  90. package/src/browser/network-error-handler.ts +88 -0
  91. package/src/browser/partial-update.ts +412 -364
  92. package/src/browser/prefetch/cache.ts +359 -0
  93. package/src/browser/prefetch/fetch.ts +452 -0
  94. package/src/browser/prefetch/observer.ts +65 -0
  95. package/src/browser/prefetch/policy.ts +48 -0
  96. package/src/browser/prefetch/queue.ts +209 -0
  97. package/src/browser/prefetch/resource-ready.ts +77 -0
  98. package/src/browser/rango-state.ts +194 -0
  99. package/src/browser/react/Link.tsx +275 -68
  100. package/src/browser/react/NavigationProvider.tsx +265 -109
  101. package/src/browser/react/ScrollRestoration.tsx +10 -6
  102. package/src/browser/react/context.ts +11 -0
  103. package/src/browser/react/filter-segment-order.ts +70 -0
  104. package/src/browser/react/index.ts +0 -48
  105. package/src/browser/react/location-state-shared.ts +272 -60
  106. package/src/browser/react/location-state.ts +90 -20
  107. package/src/browser/react/mount-context.ts +6 -1
  108. package/src/browser/react/nonce-context.ts +23 -0
  109. package/src/browser/react/shallow-equal.ts +27 -0
  110. package/src/browser/react/use-action.ts +35 -66
  111. package/src/browser/react/use-handle.ts +39 -126
  112. package/src/browser/react/use-href.tsx +8 -1
  113. package/src/browser/react/use-link-status.ts +39 -13
  114. package/src/browser/react/use-navigation.ts +53 -69
  115. package/src/browser/react/use-params.ts +75 -0
  116. package/src/browser/react/use-pathname.ts +47 -0
  117. package/src/browser/react/use-reverse.ts +106 -0
  118. package/src/browser/react/use-router.ts +98 -0
  119. package/src/browser/react/use-search-params.ts +51 -0
  120. package/src/browser/react/use-segments.ts +72 -99
  121. package/src/browser/response-adapter.ts +164 -0
  122. package/src/browser/rsc-router.tsx +300 -72
  123. package/src/browser/scroll-restoration.ts +138 -50
  124. package/src/browser/segment-reconciler.ts +243 -0
  125. package/src/browser/segment-structure-assert.ts +17 -1
  126. package/src/browser/server-action-bridge.ts +668 -613
  127. package/src/browser/types.ts +223 -51
  128. package/src/browser/validate-redirect-origin.ts +56 -0
  129. package/src/build/collect-fallback-refs.ts +107 -0
  130. package/src/build/generate-manifest.ts +252 -161
  131. package/src/build/generate-route-types.ts +41 -1038
  132. package/src/build/index.ts +12 -7
  133. package/src/build/prefix-tree-utils.ts +123 -0
  134. package/src/build/route-trie.ts +225 -42
  135. package/src/build/route-types/ast-helpers.ts +25 -0
  136. package/src/build/route-types/ast-route-extraction.ts +105 -0
  137. package/src/build/route-types/codegen.ts +113 -0
  138. package/src/build/route-types/include-resolution.ts +812 -0
  139. package/src/build/route-types/param-extraction.ts +51 -0
  140. package/src/build/route-types/per-module-writer.ts +144 -0
  141. package/src/build/route-types/router-processing.ts +695 -0
  142. package/src/build/route-types/scan-filter.ts +85 -0
  143. package/src/build/route-types/source-scan.ts +216 -0
  144. package/src/build/runtime-discovery.ts +223 -0
  145. package/src/cache/background-task.ts +34 -0
  146. package/src/cache/cache-error.ts +104 -0
  147. package/src/cache/cache-key-utils.ts +60 -0
  148. package/src/cache/cache-policy.ts +199 -0
  149. package/src/cache/cache-runtime.ts +525 -0
  150. package/src/cache/cache-scope.ts +298 -332
  151. package/src/cache/cache-tag.ts +103 -0
  152. package/src/cache/cf/cf-base64.ts +33 -0
  153. package/src/cache/cf/cf-cache-constants.ts +127 -0
  154. package/src/cache/cf/cf-cache-store.ts +2500 -158
  155. package/src/cache/cf/cf-cache-types.ts +349 -0
  156. package/src/cache/cf/cf-kv-utils.ts +46 -0
  157. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  158. package/src/cache/cf/index.ts +17 -17
  159. package/src/cache/document-cache.ts +199 -92
  160. package/src/cache/handle-capture.ts +81 -0
  161. package/src/cache/handle-snapshot.ts +111 -0
  162. package/src/cache/index.ts +29 -35
  163. package/src/cache/memory-segment-store.ts +363 -30
  164. package/src/cache/profile-registry.ts +88 -0
  165. package/src/cache/read-through-swr.ts +178 -0
  166. package/src/cache/segment-codec.ts +248 -0
  167. package/src/cache/shell-cache.ts +386 -0
  168. package/src/cache/tag-invalidation.ts +230 -0
  169. package/src/cache/taint.ts +153 -0
  170. package/src/cache/types.ts +156 -211
  171. package/src/cache/vercel/index.ts +11 -0
  172. package/src/cache/vercel/vercel-cache-store.ts +1102 -0
  173. package/src/client.rsc.tsx +43 -21
  174. package/src/client.tsx +131 -347
  175. package/src/cloudflare/index.ts +11 -0
  176. package/src/cloudflare/tracing.ts +109 -0
  177. package/src/component-utils.ts +23 -4
  178. package/src/components/DefaultDocument.tsx +13 -3
  179. package/src/context-var.ts +168 -0
  180. package/src/debug.ts +19 -9
  181. package/src/decode-loader-results.ts +52 -0
  182. package/src/defer.ts +185 -0
  183. package/src/deps/ssr.ts +0 -1
  184. package/src/encode-kv.ts +49 -0
  185. package/src/errors.ts +106 -10
  186. package/src/escape-script.ts +52 -0
  187. package/src/handle.ts +110 -35
  188. package/src/handles/MetaTags.tsx +83 -59
  189. package/src/handles/Scripts.tsx +183 -0
  190. package/src/handles/breadcrumbs.ts +93 -0
  191. package/src/handles/deferred-resolution.ts +127 -0
  192. package/src/handles/is-thenable.ts +18 -0
  193. package/src/handles/meta.ts +44 -53
  194. package/src/handles/script.ts +244 -0
  195. package/src/host/cookie-handler.ts +20 -65
  196. package/src/host/errors.ts +21 -30
  197. package/src/host/index.ts +13 -9
  198. package/src/host/pattern-matcher.ts +50 -79
  199. package/src/host/router.ts +151 -121
  200. package/src/host/testing.ts +45 -32
  201. package/src/host/types.ts +52 -11
  202. package/src/host/utils.ts +2 -2
  203. package/src/href-client.ts +192 -57
  204. package/src/index.rsc.ts +177 -35
  205. package/src/index.ts +255 -71
  206. package/src/internal-debug.ts +9 -2
  207. package/src/loader-store.ts +500 -0
  208. package/src/loader.rsc.ts +31 -99
  209. package/src/loader.ts +30 -12
  210. package/src/missing-id-error.ts +68 -0
  211. package/src/outlet-context.ts +1 -1
  212. package/src/outlet-provider.tsx +41 -0
  213. package/src/prerender/param-hash.ts +16 -14
  214. package/src/prerender/store.ts +121 -21
  215. package/src/prerender.ts +460 -26
  216. package/src/redirect-origin.ts +100 -0
  217. package/src/regex-escape.ts +8 -0
  218. package/src/render-error-thrower.tsx +20 -0
  219. package/src/response-utils.ts +62 -0
  220. package/src/reverse.ts +198 -128
  221. package/src/root-error-boundary.tsx +42 -48
  222. package/src/route-content-wrapper.tsx +22 -77
  223. package/src/route-definition/dsl-helpers.ts +1116 -0
  224. package/src/route-definition/helper-factories.ts +88 -0
  225. package/src/route-definition/helpers-types.ts +505 -0
  226. package/src/route-definition/index.ts +54 -0
  227. package/src/route-definition/redirect.ts +134 -0
  228. package/src/route-definition/resolve-handler-use.ts +160 -0
  229. package/src/route-definition/use-item-types.ts +29 -0
  230. package/src/route-definition.ts +1 -1481
  231. package/src/route-map-builder.ts +82 -144
  232. package/src/route-name.ts +53 -0
  233. package/src/route-types.ts +71 -45
  234. package/src/router/basename.ts +14 -0
  235. package/src/router/content-negotiation.ts +263 -0
  236. package/src/router/debug-manifest.ts +72 -0
  237. package/src/router/error-handling.ts +54 -27
  238. package/src/router/find-match.ts +245 -0
  239. package/src/router/handler-context.ts +377 -125
  240. package/src/router/instrument.ts +350 -0
  241. package/src/router/intercept-resolution.ts +59 -28
  242. package/src/router/lazy-includes.ts +254 -0
  243. package/src/router/loader-resolution.ts +421 -157
  244. package/src/router/logging.ts +106 -6
  245. package/src/router/manifest.ts +131 -57
  246. package/src/router/match-api.ts +167 -246
  247. package/src/router/match-context.ts +4 -24
  248. package/src/router/match-handlers.ts +440 -0
  249. package/src/router/match-middleware/background-revalidation.ts +117 -93
  250. package/src/router/match-middleware/cache-lookup.ts +297 -150
  251. package/src/router/match-middleware/cache-store.ts +123 -51
  252. package/src/router/match-middleware/intercept-resolution.ts +44 -43
  253. package/src/router/match-middleware/segment-resolution.ts +64 -22
  254. package/src/router/match-pipelines.ts +11 -87
  255. package/src/router/match-result.ts +121 -50
  256. package/src/router/metrics.ts +219 -28
  257. package/src/router/middleware-types.ts +93 -0
  258. package/src/router/middleware.ts +505 -441
  259. package/src/router/navigation-snapshot.ts +133 -0
  260. package/src/router/params-util.ts +23 -0
  261. package/src/router/parse-pattern.ts +115 -0
  262. package/src/router/pattern-matching.ts +311 -142
  263. package/src/router/prefetch-cache-ttl.ts +51 -0
  264. package/src/router/prefetch-limits.ts +37 -0
  265. package/src/router/prerender-match.ts +547 -0
  266. package/src/router/preview-match.ts +102 -0
  267. package/src/router/request-classification.ts +278 -0
  268. package/src/router/revalidation.ts +203 -62
  269. package/src/router/route-snapshot.ts +246 -0
  270. package/src/router/router-context.ts +45 -48
  271. package/src/router/router-interfaces.ts +554 -0
  272. package/src/router/router-options.ts +779 -0
  273. package/src/router/router-registry.ts +21 -0
  274. package/src/router/segment-resolution/fresh.ts +772 -0
  275. package/src/router/segment-resolution/helpers.ts +348 -0
  276. package/src/router/segment-resolution/loader-cache.ts +250 -0
  277. package/src/router/segment-resolution/loader-mask.ts +44 -0
  278. package/src/router/segment-resolution/revalidation.ts +1331 -0
  279. package/src/router/segment-resolution/static-store.ts +81 -0
  280. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  281. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  282. package/src/router/segment-resolution.ts +25 -1354
  283. package/src/router/segment-wrappers.ts +292 -0
  284. package/src/router/state-cookie-name.ts +33 -0
  285. package/src/router/substitute-pattern-params.ts +75 -0
  286. package/src/router/telemetry-otel.ts +261 -0
  287. package/src/router/telemetry.ts +377 -0
  288. package/src/router/timeout.ts +128 -0
  289. package/src/router/tracing.ts +206 -0
  290. package/src/router/trie-matching.ts +240 -61
  291. package/src/router/types.ts +23 -70
  292. package/src/router/url-params.ts +57 -0
  293. package/src/router.ts +781 -2378
  294. package/src/rsc/full-payload.ts +70 -0
  295. package/src/rsc/handler-context.ts +46 -0
  296. package/src/rsc/handler.ts +905 -1142
  297. package/src/rsc/helpers.ts +275 -19
  298. package/src/rsc/index.ts +2 -25
  299. package/src/rsc/json-route-result.ts +38 -0
  300. package/src/rsc/loader-fetch.ts +305 -0
  301. package/src/rsc/manifest-init.ts +77 -0
  302. package/src/rsc/nonce.ts +14 -0
  303. package/src/rsc/origin-guard.ts +155 -0
  304. package/src/rsc/progressive-enhancement.ts +502 -0
  305. package/src/rsc/redirect-guard.ts +99 -0
  306. package/src/rsc/response-cache-serve.ts +238 -0
  307. package/src/rsc/response-error.ts +104 -0
  308. package/src/rsc/response-route-handler.ts +257 -0
  309. package/src/rsc/rsc-rendering.ts +337 -0
  310. package/src/rsc/runtime-warnings.ts +55 -0
  311. package/src/rsc/server-action.ts +522 -0
  312. package/src/rsc/shell-capture.ts +439 -0
  313. package/src/rsc/ssr-setup.ts +144 -0
  314. package/src/rsc/transition-gate.ts +89 -0
  315. package/src/rsc/types.ts +95 -12
  316. package/src/runtime-env.ts +18 -0
  317. package/src/search-params.ts +99 -82
  318. package/src/segment-content-promise.ts +67 -0
  319. package/src/segment-loader-promise.ts +149 -0
  320. package/src/segment-system.tsx +349 -134
  321. package/src/serialize.ts +243 -0
  322. package/src/server/context.ts +452 -85
  323. package/src/server/cookie-parse.ts +32 -0
  324. package/src/server/cookie-store.ts +310 -0
  325. package/src/server/fetchable-loader-store.ts +11 -6
  326. package/src/server/handle-store.ts +123 -42
  327. package/src/server/live.ts +130 -0
  328. package/src/server/loader-registry.ts +51 -100
  329. package/src/server/request-context.ts +842 -157
  330. package/src/server.ts +15 -8
  331. package/src/ssr/index.tsx +412 -136
  332. package/src/ssr/ssr-root.tsx +228 -0
  333. package/src/static-handler.ts +45 -18
  334. package/src/testing/cache-status.ts +162 -0
  335. package/src/testing/collect-handle.ts +46 -0
  336. package/src/testing/dispatch.ts +701 -0
  337. package/src/testing/dom.entry.ts +22 -0
  338. package/src/testing/e2e/fixture.ts +188 -0
  339. package/src/testing/e2e/index.ts +128 -0
  340. package/src/testing/e2e/matchers.ts +35 -0
  341. package/src/testing/e2e/page-helpers.ts +272 -0
  342. package/src/testing/e2e/parity.ts +387 -0
  343. package/src/testing/e2e/server.ts +195 -0
  344. package/src/testing/flight-matchers.ts +97 -0
  345. package/src/testing/flight-normalize.ts +11 -0
  346. package/src/testing/flight-runtime.d.ts +57 -0
  347. package/src/testing/flight-tree.ts +682 -0
  348. package/src/testing/flight.entry.ts +52 -0
  349. package/src/testing/flight.ts +257 -0
  350. package/src/testing/generated-routes.ts +199 -0
  351. package/src/testing/index.ts +105 -0
  352. package/src/testing/internal/context.ts +371 -0
  353. package/src/testing/internal/flight-client-globals.ts +30 -0
  354. package/src/testing/internal/seed-vars.ts +54 -0
  355. package/src/testing/render-handler.ts +357 -0
  356. package/src/testing/render-route.tsx +584 -0
  357. package/src/testing/run-loader.ts +385 -0
  358. package/src/testing/run-middleware.ts +205 -0
  359. package/src/testing/run-transition-when.ts +164 -0
  360. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  361. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  362. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  363. package/src/testing/vitest-stubs/version.ts +5 -0
  364. package/src/testing/vitest.ts +305 -0
  365. package/src/theme/ThemeProvider.tsx +40 -72
  366. package/src/theme/ThemeScript.tsx +12 -14
  367. package/src/theme/constants.ts +57 -15
  368. package/src/theme/index.ts +3 -20
  369. package/src/theme/theme-context.ts +5 -35
  370. package/src/theme/theme-script.ts +43 -39
  371. package/src/theme/use-theme.ts +0 -3
  372. package/src/types/boundaries.ts +123 -0
  373. package/src/types/cache-types.ts +207 -0
  374. package/src/types/error-types.ts +132 -0
  375. package/src/types/global-namespace.ts +113 -0
  376. package/src/types/handler-context.ts +839 -0
  377. package/src/types/index.ts +81 -0
  378. package/src/types/loader-types.ts +212 -0
  379. package/src/types/request-scope.ts +112 -0
  380. package/src/types/route-config.ts +138 -0
  381. package/src/types/route-entry.ts +114 -0
  382. package/src/types/segments.ts +271 -0
  383. package/src/types.ts +1 -1795
  384. package/src/urls/include-helper.ts +162 -0
  385. package/src/urls/include-provider.ts +71 -0
  386. package/src/urls/index.ts +43 -0
  387. package/src/urls/path-helper-types.ts +413 -0
  388. package/src/urls/path-helper.ts +275 -0
  389. package/src/urls/pattern-types.ts +124 -0
  390. package/src/urls/response-types.ts +109 -0
  391. package/src/urls/type-extraction.ts +316 -0
  392. package/src/urls/urls-function.ts +80 -0
  393. package/src/urls.ts +1 -1341
  394. package/src/use-loader.tsx +406 -141
  395. package/src/vercel/index.ts +11 -0
  396. package/src/vercel/tracing.ts +88 -0
  397. package/src/vite/debug.ts +185 -0
  398. package/src/vite/discovery/bundle-postprocess.ts +182 -0
  399. package/src/vite/discovery/discover-routers.ts +389 -0
  400. package/src/vite/discovery/discovery-errors.ts +255 -0
  401. package/src/vite/discovery/gate-state.ts +171 -0
  402. package/src/vite/discovery/prerender-collection.ts +467 -0
  403. package/src/vite/discovery/route-types-writer.ts +214 -0
  404. package/src/vite/discovery/self-gen-tracking.ts +73 -0
  405. package/src/vite/discovery/state.ts +161 -0
  406. package/src/vite/discovery/virtual-module-codegen.ts +183 -0
  407. package/src/vite/index.ts +23 -2255
  408. package/src/vite/inject-client-debug.ts +36 -0
  409. package/src/vite/plugin-types.ts +303 -0
  410. package/src/vite/plugins/cjs-to-esm.ts +90 -0
  411. package/src/vite/plugins/client-ref-dedup.ts +120 -0
  412. package/src/vite/plugins/client-ref-hashing.ts +118 -0
  413. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  414. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  415. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  416. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +88 -110
  417. package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +89 -79
  418. package/src/vite/plugins/expose-ids/export-analysis.ts +363 -0
  419. package/src/vite/plugins/expose-ids/handler-transform.ts +130 -0
  420. package/src/vite/plugins/expose-ids/loader-transform.ts +64 -0
  421. package/src/vite/plugins/expose-ids/router-transform.ts +199 -0
  422. package/src/vite/plugins/expose-ids/types.ts +45 -0
  423. package/src/vite/plugins/expose-internal-ids.ts +805 -0
  424. package/src/vite/plugins/performance-tracks.ts +89 -0
  425. package/src/vite/plugins/refresh-cmd.ts +127 -0
  426. package/src/vite/plugins/use-cache-transform.ts +313 -0
  427. package/src/vite/plugins/vercel-output.ts +384 -0
  428. package/src/vite/plugins/version-injector.ts +94 -0
  429. package/src/vite/plugins/version-plugin.ts +263 -0
  430. package/src/vite/plugins/virtual-entries.ts +234 -0
  431. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  432. package/src/vite/rango.ts +560 -0
  433. package/src/vite/router-discovery.ts +1638 -0
  434. package/src/vite/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +200 -37
  435. package/src/vite/utils/banner.ts +36 -0
  436. package/src/vite/utils/bundle-analysis.ts +132 -0
  437. package/src/vite/utils/client-chunks.ts +184 -0
  438. package/src/vite/utils/directive-prologue.ts +40 -0
  439. package/src/vite/utils/forward-user-plugins.ts +171 -0
  440. package/src/vite/utils/manifest-utils.ts +15 -0
  441. package/src/vite/utils/package-resolution.ts +89 -0
  442. package/src/vite/utils/prerender-utils.ts +249 -0
  443. package/src/vite/utils/shared-utils.ts +269 -0
  444. package/CLAUDE.md +0 -43
  445. package/dist/vite/index.named-routes.gen.ts +0 -103
  446. package/src/browser/lru-cache.ts +0 -69
  447. package/src/browser/react/use-client-cache.ts +0 -56
  448. package/src/browser/request-controller.ts +0 -164
  449. package/src/browser/shallow.ts +0 -35
  450. package/src/cache/memory-store.ts +0 -253
  451. package/src/handles/index.ts +0 -6
  452. package/src/href-context.ts +0 -33
  453. package/src/network-error-thrower.tsx +0 -21
  454. package/src/router.gen.ts +0 -6
  455. package/src/static-handler.gen.ts +0 -5
  456. package/src/urls.gen.ts +0 -8
  457. package/src/vite/expose-internal-ids.ts +0 -1167
  458. package/src/vite/package-resolution.ts +0 -125
  459. package/src/vite/virtual-entries.ts +0 -114
  460. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -1,21 +1,27 @@
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()
8
8
 
9
- Loaders fetch data on the server and stream it to the client.
9
+ Loaders fetch data on the server and stream it to the client. For mutations
10
+ (writes triggered by forms or buttons), use server actions instead — see
11
+ `/server-actions`. Loaders re-resolve after an action runs, so the typical
12
+ flow is _action mutates → loader re-reads → UI updates_.
10
13
 
11
14
  ## Creating a Loader
12
15
 
13
16
  ```typescript
14
17
  import { createLoader } from "@rangojs/router";
15
18
 
16
- export const ProductLoader = createLoader("product", async (ctx) => {
17
- const product = await ctx.env.Bindings.DB
18
- .prepare("SELECT * FROM products WHERE slug = ?")
19
+ export const ProductLoader = createLoader(async (ctx) => {
20
+ "use server";
21
+
22
+ const product = await ctx.env.DB.prepare(
23
+ "SELECT * FROM products WHERE slug = ?",
24
+ )
19
25
  .bind(ctx.params.slug)
20
26
  .first();
21
27
 
@@ -29,19 +35,19 @@ All of the following are equivalent and fully supported by the Vite transform:
29
35
 
30
36
  ```typescript
31
37
  // Direct export (most common)
32
- export const ProductLoader = createLoader("product", handler);
38
+ export const ProductLoader = createLoader(handler);
33
39
 
34
40
  // Separate declaration + named export
35
- const ProductLoader = createLoader("product", handler);
41
+ const ProductLoader = createLoader(handler);
36
42
  export { ProductLoader };
37
43
 
38
44
  // Aliased export
39
- const InternalLoader = createLoader("product", handler);
45
+ const InternalLoader = createLoader(handler);
40
46
  export { InternalLoader as ProductLoader };
41
47
 
42
48
  // Aliased import
43
49
  import { createLoader as cl } from "@rangojs/router";
44
- export const ProductLoader = cl("product", handler);
50
+ export const ProductLoader = cl(handler);
45
51
  ```
46
52
 
47
53
  The `export const` form and the `const + export { }` form both work for
@@ -62,53 +68,179 @@ export const urlpatterns = urls(({ path, loader }) => [
62
68
 
63
69
  ## Consuming Loader Data
64
70
 
65
- ### In Server Components
71
+ Register loaders with `loader()` in the DSL and consume them in client
72
+ components with `useLoader()`. This is the recommended pattern — it keeps
73
+ data fetching on the server and consumption on the client, with a clean
74
+ separation that works correctly with `cache()`.
66
75
 
67
76
  ```typescript
68
- import { useLoader } from "@rangojs/router";
77
+ "use client";
78
+ import { useLoader } from "@rangojs/router/client";
69
79
  import { ProductLoader } from "./loaders/product";
70
80
 
71
- async function ProductPage() {
72
- const { product } = await useLoader(ProductLoader);
73
- return <h1>{product.name}</h1>;
81
+ function ProductDetails() {
82
+ const { data } = useLoader(ProductLoader);
83
+ return <div>{data.product.description}</div>;
74
84
  }
75
85
  ```
76
86
 
77
- ### In Client Components
78
-
79
87
  ```typescript
80
- "use client";
81
- import { useLoaderData } from "@rangojs/router/client";
82
- import { ProductLoader } from "./loaders/product";
88
+ // Route definition — loader() registration required
89
+ path("/product/:slug", ProductPage, { name: "product" }, () => [
90
+ loader(ProductLoader),
91
+ ]);
92
+ ```
83
93
 
84
- function ProductDetails() {
85
- const { product } = useLoaderData(ProductLoader);
86
- return <div>{product.description}</div>;
87
- }
94
+ > **Client refresh `key` vs. server `cache({ key })` vs. `revalidate()`.** Three
95
+ > different "what refreshes" knobs that are easy to confuse:
96
+ >
97
+ > - `useLoader(Loader, { key })` / `useFetchLoader(Loader, { key })` — a
98
+ > **client** refresh identity. It groups which mounted reads of one loader
99
+ > refresh together when one calls `load()`. It never touches the server
100
+ > request. For refreshing **different** loaders together, tag them with
101
+ > `{ refreshGroup }` (one name or several) and call `useRefreshLoaders()(name)`
102
+ > (plain GET only). See the hooks skill ("Scoping refetch with a `key`" and
103
+ > "Refreshing multiple loaders together").
104
+ > - `cache({ key })` — a **server** cache identity (storage hit/miss/ttl/swr).
105
+ > - `revalidate()` — which **server** segments/loaders recompute during
106
+ > navigation and action refreshes.
107
+
108
+ DSL loaders are the **live data layer** — they resolve fresh on every
109
+ request, even when the route is inside a `cache()` boundary. The router
110
+ excludes them from the segment cache at storage time and re-resolves them
111
+ on retrieval. This means `cache()` gives you cached UI + fresh data by
112
+ default.
113
+
114
+ ### Cache safety
115
+
116
+ DSL loaders can safely read `createVar({ cache: false })` variables
117
+ because they are always resolved fresh. The read guard is bypassed for
118
+ loader functions — they never produce stale data.
119
+
120
+ ### ctx.use(Loader) — escape hatch
121
+
122
+ For cases where you need loader data in the server handler itself (e.g.,
123
+ to set ctx variables or make routing decisions), use `ctx.use(Loader)`:
124
+
125
+ ```typescript
126
+ path("/product/:slug", async (ctx) => {
127
+ const { product } = await ctx.use(ProductLoader);
128
+ ctx.set(Product, product); // make available to children
129
+ return <ProductPage />;
130
+ }, { name: "product" }, () => [
131
+ loader(ProductLoader), // still register for client consumption
132
+ ])
88
133
  ```
89
134
 
135
+ When you register with `loader()` in the DSL, `ctx.use()` returns the
136
+ same memoized result — loaders never run twice per request.
137
+
138
+ **Limitations of ctx.use(Loader):**
139
+
140
+ - The handler output depends on the loader data. If the route is inside
141
+ `cache()`, the handler is cached with the loader result baked in —
142
+ defeating the live data guarantee.
143
+ - Non-cacheable variable reads (`createVar({ cache: false })`) inside the
144
+ handler still throw, even if the data came from a loader.
145
+ - Prefer DSL `loader()` + client `useLoader()` for data that depends on
146
+ non-cacheable context variables.
147
+
148
+ **Never use `useLoader()` in server components** — it is a client-only API.
149
+
150
+ ### Summary
151
+
152
+ | Pattern | API | Cache-safe | Recommended |
153
+ | ---------------------- | ------------------- | ---------- | ----------- |
154
+ | DSL + client component | `useLoader(Loader)` | Yes | Yes |
155
+ | Handler escape hatch | `ctx.use(Loader)` | No | When needed |
156
+
90
157
  ## Loader Context
91
158
 
92
- Loaders receive the same context as route handlers:
159
+ Loaders receive the same context shape as route handlers.
160
+
161
+ ### Full field surface
162
+
163
+ | Field | Type | Notes |
164
+ | -------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
165
+ | `params` | `TParams` | Merged route + explicit loader params; overridable by fetchable `load({ params })`. |
166
+ | `routeParams` | `Record<string, string>` | Server-trusted route params from URL pattern matching; cannot be overridden. |
167
+ | `request` | `Request` | The incoming `Request` (headers, method, body, `signal` for abort). |
168
+ | `url` | `URL` | Parsed request URL. |
169
+ | `pathname` | `string` | URL pathname (shortcut for `ctx.url.pathname`). |
170
+ | `searchParams` | `URLSearchParams` | Shortcut for `ctx.url.searchParams`. |
171
+ | `search` | `ResolveSearchSchema<TSearch>` | Typed query params when a search schema is declared on the route; `{}` otherwise. |
172
+ | `env` | `TEnv` | Plain bindings from `createRouter<TEnv>()` (DB, KV, secrets, etc.). |
173
+ | `get` | `(key \| ContextVar) => value` | Reads variables/context-vars set by middleware. |
174
+ | `use` | `(loader \| handle) => T` | Access another loader's data (Promise) or a handle's collected data (after `await ctx.rendered()`). |
175
+ | `rendered` | `() => Promise<void>` | **Experimental.** DSL loaders only — waits for all non-loader segments (including `loading()` streaming handlers) to settle before reading handle data. |
176
+ | `method` | `string` | HTTP method. `"GET"` for SSR loader runs; reflects real method for fetchable loaders. |
177
+ | `body` | `TBody \| undefined` | Parsed request body for fetchable POST/PUT/PATCH/DELETE calls. |
178
+ | `formData` | `FormData \| undefined` | Present when a fetchable loader is invoked via form submission. |
179
+ | `reverse` | `ScopedReverseFunction` | Generate type-checked URLs from route names (same scoped semantics as route handlers). |
180
+
181
+ ### Example
93
182
 
94
183
  ```typescript
95
- export const ProductLoader = createLoader("product", async (ctx) => {
96
- // URL params
184
+ export const ProductLoader = createLoader(async (ctx) => {
185
+ "use server";
186
+
187
+ // URL params (may include client-provided overrides for fetchable loaders)
97
188
  const { slug } = ctx.params;
98
189
 
190
+ // Server-trusted route params (from URL pattern matching, cannot be overridden)
191
+ const { slug: trustedSlug } = ctx.routeParams;
192
+
99
193
  // Query params
100
194
  const variant = ctx.url.searchParams.get("variant");
101
195
 
102
- // Environment (DB, KV, etc.)
103
- const db = ctx.env.Bindings.DB;
196
+ // Platform bindings (DB, KV, etc.) — plain bindings from createRouter<TEnv>()
197
+ const db = ctx.env.DB;
104
198
 
105
199
  // Request headers
106
200
  const auth = ctx.request.headers.get("Authorization");
107
201
 
108
- // Variables set by middleware
109
- const user = ctx.env.Variables.user;
202
+ // Variables set by middleware (from Rango.Vars augmentation)
203
+ const user = ctx.get("user");
204
+
205
+ // Type-checked URLs for payloads. `.name` resolves within the current
206
+ // include() scope; a bare `name` resolves globally. See /route and
207
+ // /typesafety for scope rules and route-name autocomplete.
208
+ const detailUrl = ctx.reverse(".detail", { slug });
110
209
 
111
- return { product: await fetchProduct(slug) };
210
+ return {
211
+ product: await fetchProduct(slug),
212
+ links: { self: detailUrl },
213
+ };
214
+ });
215
+ ```
216
+
217
+ See `/route` for the full handler-context contract (shared with loaders) and
218
+ `/typesafety` for route-name typing that powers `ctx.reverse` autocomplete.
219
+
220
+ ### params vs routeParams
221
+
222
+ - `ctx.params` — merged route params + explicit loader params. For fetchable
223
+ loaders called with `load(Loader, { params: { ... } })`, explicit params
224
+ override route-matched params.
225
+ - `ctx.routeParams` — server-trusted route params from URL pattern matching.
226
+ Cannot be overridden by client-provided params.
227
+
228
+ Use `ctx.routeParams` when you need trusted route identity for authorization
229
+ or resource scoping:
230
+
231
+ ```typescript
232
+ export const OrderLoader = createLoader(async (ctx) => {
233
+ "use server";
234
+
235
+ // Use routeParams for auth checks — client cannot spoof the URL-matched ID
236
+ const { orderId } = ctx.routeParams;
237
+ const user = ctx.get("user");
238
+
239
+ const order = await db.orders.get(orderId);
240
+ if (order.userId !== user.id)
241
+ throw new Response("Forbidden", { status: 403 });
242
+
243
+ return { order };
112
244
  });
113
245
  ```
114
246
 
@@ -117,24 +249,330 @@ export const ProductLoader = createLoader("product", async (ctx) => {
117
249
  Add caching or revalidation to specific loaders:
118
250
 
119
251
  ```typescript
252
+ import * as CartActions from "./actions/cart";
253
+
120
254
  path("/product/:slug", ProductPage, { name: "product" }, () => [
121
255
  // Cached loader
122
- loader(ProductLoader, () => [
123
- cache({ ttl: 300 }),
124
- ]),
256
+ loader(ProductLoader, () => [cache({ ttl: 300 })]),
125
257
 
126
258
  // Loader with revalidation control
127
259
  loader(RelatedProductsLoader, () => [
128
- revalidate(() => false), // Never revalidate
260
+ revalidate(() => false), // Never revalidate
129
261
  ]),
130
262
 
131
- // Loader that revalidates after cart actions
263
+ // Loader that revalidates after cart actions (defer otherwise — keeps the
264
+ // permissive loader defaults for navigation and other actions intact)
132
265
  loader(CartLoader, () => [
133
- revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
266
+ revalidate((ctx) => ctx.isAction(CartActions) || undefined),
134
267
  ]),
135
- ])
268
+ ]);
269
+ ```
270
+
271
+ ### `revalidate()` return shapes
272
+
273
+ > **Scope: `revalidate()` is a partial-render concern, not a cache concern.**
274
+ > It decides whether a segment (here, a loader) re-runs and streams to the
275
+ > client on a navigation or action — never whether a cached value is stale. The
276
+ > cache decides hit/miss/ttl/swr independently and never reads `revalidate()`.
277
+ > Caching a loader is a separate, opt-in step (`loader(Fn, () => [cache({...})])`).
278
+ > See `/cache-guide` → "Two axes" and `/rango` → "The shape of rango".
279
+
280
+ A `revalidate(fn)` callback can return one of four shapes. The chain
281
+ processes revalidators in order; each call's return controls how the
282
+ chain continues:
283
+
284
+ ```typescript
285
+ // 1) Hard decision — short-circuits the chain, used as the final answer.
286
+ revalidate(() => true);
287
+ revalidate(({ actionId }) => actionId?.includes("Cart") ?? false);
288
+
289
+ // 2) Soft decision — updates the running suggestion for downstream
290
+ // revalidators on the same segment, chain continues.
291
+ revalidate(({ defaultShouldRevalidate }) => ({
292
+ defaultShouldRevalidate: !defaultShouldRevalidate,
293
+ }));
294
+
295
+ // 3) Defer (no opinion) — leaves the running suggestion unchanged and
296
+ // continues to the next revalidator. Implicit return / null /
297
+ // undefined are all equivalent and consumer-friendly.
298
+ revalidate(({ actionId }) => {
299
+ if (actionId?.includes("Cart")) return true; // hard for this branch only
300
+ // implicit return — let downstream revalidators or the segment default decide
301
+ });
302
+ revalidate(() => undefined); // explicit defer
303
+ revalidate(() => null); // explicit defer
304
+ ```
305
+
306
+ If every revalidator on a segment defers, the segment-type default
307
+ (e.g. params-changed for routes, `false` for parallels) is used.
308
+
309
+ #### `|| undefined` (defer) vs `?? false` (hard) — pick deliberately
310
+
311
+ A boolean return — including `false` — is a **hard** decision: it short-circuits
312
+ the chain and overrides the segment default. `undefined` **defers** to the
313
+ running suggestion / segment default. They are not interchangeable:
314
+
315
+ ```typescript
316
+ // Defer: "revalidate on match, otherwise let the default/downstream decide."
317
+ revalidate(({ actionId }) => actionId?.includes("Cart") || undefined);
318
+
319
+ // Hard: "revalidate ONLY on match, suppress everything else."
320
+ revalidate(({ actionId }) => actionId?.includes("Cart") ?? false);
321
+ ```
322
+
323
+ This matters most for loaders, whose defaults are permissive: a loader defaults
324
+ to revalidating on **any** action (`POST`) and on **param/search changes**
325
+ during navigation. So `?? false` on a loader silently suppresses both — the
326
+ loader will not refetch when you navigate to a different `:id`. Use
327
+ `|| undefined` when you want to _add_ a revalidation signal on top of the
328
+ sensible defaults, and reserve `?? false` for the rare case where you genuinely
329
+ want the loader to refetch on nothing but your matched action.
330
+
331
+ When **composing multiple revalidators** on one segment (see below), defer is
332
+ mandatory: the first hard `?? false` ends the chain and the later contracts
333
+ never run.
334
+
335
+ #### Matching actions: `ctx.isAction()`
336
+
337
+ To revalidate after specific server actions, match them by **reference** with
338
+ `ctx.isAction()` rather than hand-written `actionId` substrings. A rename or
339
+ moved file then becomes a type error instead of silently failing to match:
340
+
341
+ ```typescript
342
+ import { addToCart, removeFromCart } from "../actions/cart";
343
+ import * as CartActions from "../actions/cart";
344
+
345
+ loader(CartLoader, () => [
346
+ revalidate((ctx) => ctx.isAction(addToCart) || undefined), // one action
347
+ ]);
348
+ revalidate((ctx) => ctx.isAction(addToCart, removeFromCart) || undefined); // several
349
+ revalidate((ctx) => ctx.isAction(CartActions) || undefined); // any action in the module
350
+ ```
351
+
352
+ `isAction()` is a method on the revalidate predicate's **context argument** —
353
+ there is no standalone `isAction` import; you always reach it through the callback
354
+ parameter (`revalidate((ctx) => ctx.isAction(...))`). It returns a raw boolean, so
355
+ pair it with `|| undefined` for the usual "revalidate on match, else defer"
356
+ intent. It returns `false` on plain navigation and on non-matches, and resolves
357
+ the reference the same way the router derives `actionId` (`$id` in production,
358
+ `$$id` in dev), so it matches in both modes. The raw `actionId` string stays
359
+ available on the same context as an escape hatch.
360
+
361
+ ### Revalidation Contracts for Loader Dependencies
362
+
363
+ If a loader reads `ctx.get()` data produced by an outer handler/layout, share
364
+ the same named revalidation contract across producer and consumer segments.
365
+
366
+ ```typescript
367
+ // revalidation-contracts.ts
368
+ import * as AccountActions from "./actions/account";
369
+
370
+ // Match by reference with ctx.isAction() (rename-safe), and defer (|| undefined)
371
+ // so these contracts compose — a hard `false` would short-circuit the rest.
372
+ export const revalidateAccountScope = (ctx) =>
373
+ ctx.isAction(AccountActions) || undefined;
374
+
375
+ layout(AccountLayout, () => [
376
+ revalidate(revalidateAccountScope), // producer reruns
377
+ path("/account/orders", OrdersPage, { name: "account.orders" }, () => [
378
+ loader(OrdersLoader, () => [
379
+ revalidate(revalidateAccountScope), // consumer reruns
380
+ ]),
381
+ ]),
382
+ ]);
383
+ ```
384
+
385
+ For segments that depend on multiple upstream domains, compose multiple
386
+ contracts on both sides.
387
+
388
+ To keep loader route trees concise, export helper wrappers:
389
+
390
+ ```typescript
391
+ import { revalidate } from "@rangojs/router";
392
+
393
+ export const revalidateAccount = () => [revalidate(revalidateAccountScope)];
394
+
395
+ layout(AccountLayout, () => [
396
+ revalidateAccount(),
397
+ path("/account/orders", OrdersPage, { name: "account.orders" }, () => [
398
+ loader(OrdersLoader, () => [revalidateAccount()]),
399
+ ]),
400
+ ]);
401
+ ```
402
+
403
+ ## Loaders: The Live Data Layer
404
+
405
+ Loaders are the live data layer of the router. They resolve fresh on every
406
+ request, even when the route's UI segments are served from cache. This is a
407
+ core design principle — route-level `cache()` caches rendered components but
408
+ never caches loader data. Loaders are excluded at storage time and re-resolved
409
+ on retrieval.
410
+
411
+ This means `cache()` gives you cached UI + fresh data by default. Pre-rendering
412
+ follows the same rule: at build time, loaders are skipped entirely (there is no
413
+ real request context), and at runtime the worker resolves them fresh against
414
+ the live database.
415
+
416
+ ### Parallel and streaming — latency overlaps first paint
417
+
418
+ Loaders do not block the page. As the render pass begins — the pass that route
419
+ middleware wraps, so loaders run right after middleware, not in a later
420
+ phase — every matched loader is kicked off **concurrently** (their promises start in the
421
+ same tick), and each result is **streamed** to the client as its own RSC Flight
422
+ chunk rather than awaited up front. Pair a loader with `loading()` (or a
423
+ client `<Suspense>`) and the shell paints immediately while the data streams in.
424
+
425
+ This is why **"cached UI still pays full data latency" is the wrong intuition**:
426
+ on a `cache()` hit the UI segments stream instantly from cache while the live
427
+ loaders resolve fresh **in parallel** — data latency _overlaps_ first paint
428
+ instead of being added on top of it. (Without a `loading()` / `<Suspense>`
429
+ boundary a parallel loader blocks its parent, so add one to keep the overlap.)
430
+
431
+ If you come from a framework where the loader is a blocking step that runs
432
+ before the response is built, this is the shift to internalize: here the
433
+ response starts streaming first and loader data fills in.
434
+
435
+ ### See it: `debugPerformance`
436
+
437
+ Turn on the per-request performance timeline early — it is the fastest way to
438
+ confirm loaders overlap rather than serialize, and to find the real bottleneck
439
+ locally instead of guessing:
440
+
441
+ ```typescript
442
+ const router = createRouter({ document: Document, debugPerformance: true });
443
+ ```
444
+
445
+ Or enable it per-request from middleware (e.g. only when `?debug` is present) by
446
+ calling `ctx.debugPerformance()` **before** `await next()`. Each HTML request
447
+ then prints a shared-axis waterfall (and emits a `Server-Timing` header):
448
+
449
+ ```
450
+ [RSC Perf] GET /product/widget (24.53ms)
451
+ start dur span timeline
452
+ 0.08ms 3.20ms route-matching |#####...................................|
453
+ 3.40ms 8.70ms ssr-render-html |.....##############.....................|
454
+ 3.42ms 11.90ms loader:…#ProductLoader |.....###################................|
455
+ 3.45ms 11.40ms loader:…#ReviewsLoader |.....##################.................|
456
+ 0.00ms 24.53ms handler:total |########################################|
457
+ ```
458
+
459
+ How to read it:
460
+
461
+ - **Humans:** scan the `#` bars on the shared axis. Bars that start at the same
462
+ offset and run side by side are executing **in parallel** — loaders should
463
+ overlap `ssr-render-html` / `render:total`, not sit alone to the right of
464
+ everything. A lone `loader:*` bar past the render bar is serialized latency to
465
+ chase. `handler:total` is the whole request; `render:total` is the render pass.
466
+ - **LLMs / programmatic:** read each row as `{ start, dur, label }`. A loader
467
+ overlaps paint when its `[start, start+dur]` interval intersects
468
+ `render:total` / `ssr-render-html`. Flag a regression when a `loader:*`
469
+ interval is **disjoint from and starts after** `render:total`, or when its
470
+ `dur` approaches `handler:total` — that loader is on the critical path instead
471
+ of overlapping it. Two `loader:*` rows with near-equal `start` confirm
472
+ parallel execution.
473
+
474
+ ### Opting a Loader into Caching
475
+
476
+ To cache a specific loader's data, attach a `cache()` child:
477
+
478
+ ```typescript
479
+ loader(ProductLoader, () => [cache({ ttl: 300 })]),
480
+ ```
481
+
482
+ The loader's data is cached independently from the route's segment cache,
483
+ using the same `SegmentCacheStore` (app-level or per-loader override).
484
+
485
+ Values are serialized through RSC Flight, so loaders can return ReactNode,
486
+ Promises, null, and any RSC-serializable type — all round-trip correctly
487
+ through the cache.
488
+
489
+ ### Cache Key
490
+
491
+ The default cache key is `loader:{loaderId}:{pathname}:{sortedParams}`.
492
+ This can be customized at two levels:
493
+
494
+ ```typescript
495
+ // Full override — key function replaces the default entirely
496
+ loader(ProductLoader, () => [
497
+ cache({
498
+ ttl: 300,
499
+ key: (ctx) => `product:${ctx.params.slug}:${cookies().get("locale")?.value ?? "en"}`,
500
+ }),
501
+ ]),
502
+
503
+ // Store-level keyGenerator — modifies the default key (e.g., adds a region prefix)
504
+ // Set in the store configuration, applies to all entries in that store
505
+ ```
506
+
507
+ Resolution priority (same as route-level `cache()`):
508
+
509
+ 1. `key(ctx)` from cache options — full override
510
+ 2. `store.keyGenerator(ctx, defaultKey)` — store-level modification
511
+ 3. Default key — `loader:{id}:{pathname}:{params}`
512
+
513
+ If a custom key function throws, it falls back to the default key silently
514
+ (logged to console.error).
515
+
516
+ ### Tags for Invalidation
517
+
518
+ ```typescript
519
+ // Static tags
520
+ loader(ProductLoader, () => [
521
+ cache({ ttl: 300, tags: ["products", "catalog"] }),
522
+ ]),
523
+
524
+ // Dynamic tags
525
+ loader(ProductLoader, () => [
526
+ cache({
527
+ ttl: 300,
528
+ tags: (ctx) => [`product:${ctx.params.slug}`, "products"],
529
+ }),
530
+ ]),
531
+ ```
532
+
533
+ ### Stale-While-Revalidate
534
+
535
+ ```typescript
536
+ loader(ProductLoader, () => [
537
+ cache({ ttl: 60, swr: 300 }),
538
+ ]),
136
539
  ```
137
540
 
541
+ During the SWR window (60-360s), stale data is returned immediately while
542
+ fresh data is fetched in the background via `waitUntil`. After the SWR window
543
+ expires (360s+), the entry is treated as a cache miss.
544
+
545
+ ### Conditional Caching
546
+
547
+ Skip the cache at runtime based on request properties:
548
+
549
+ ```typescript
550
+ loader(ProductLoader, () => [
551
+ cache({
552
+ ttl: 300,
553
+ condition: (ctx) => !ctx.request.headers.has("authorization"),
554
+ }),
555
+ ]),
556
+ ```
557
+
558
+ When `condition` returns false, the loader runs fresh and the cache is bypassed
559
+ entirely (no read, no write).
560
+
561
+ ### Per-Loader Store Override
562
+
563
+ ```typescript
564
+ import { MemorySegmentCacheStore } from "@rangojs/router/cache";
565
+
566
+ const hotStore = new MemorySegmentCacheStore({ defaults: { ttl: 10 } });
567
+
568
+ loader(PricingLoader, () => [
569
+ cache({ store: hotStore }),
570
+ ]),
571
+ ```
572
+
573
+ Without an explicit store, the loader uses the app-level store from the
574
+ handler config (`cache.store`).
575
+
138
576
  ## Multiple Loaders
139
577
 
140
578
  Routes can have multiple loaders that run in parallel:
@@ -144,7 +582,7 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
144
582
  loader(ProductLoader),
145
583
  loader(RelatedProductsLoader),
146
584
  loader(ReviewsLoader),
147
- ])
585
+ ]);
148
586
  ```
149
587
 
150
588
  ## Layout Loaders
@@ -210,39 +648,159 @@ function ProductPage() {
210
648
  }
211
649
  ```
212
650
 
651
+ ## Fetchable Loaders
652
+
653
+ By default, loaders only run during SSR and navigation. Pass `true` as the second
654
+ argument to `createLoader` to make a loader **fetchable** — callable from the client
655
+ via `useFetchLoader()` and `load()`:
656
+
657
+ ```typescript
658
+ import { createLoader } from "@rangojs/router";
659
+
660
+ export const SearchLoader = createLoader(async (ctx) => {
661
+ "use server";
662
+
663
+ const query = ctx.params.query ?? "";
664
+ const results = await ctx.env.DB.prepare(
665
+ "SELECT * FROM products WHERE name LIKE ?",
666
+ )
667
+ .bind(`%${query}%`)
668
+ .all();
669
+
670
+ return { results: results.results ?? [] };
671
+ }, true); // true = fetchable
672
+ ```
673
+
674
+ > **No registration needed — and no worker-entry import.** A fetchable loader
675
+ > does not have to be registered with `loader()` in the route DSL, and it does
676
+ > not have to be imported by any server module. Importing it into the client
677
+ > component that calls `useFetchLoader()` / `load()` is enough. Rango discovers
678
+ > every `createLoader(fn, true)` at build time and registers it for the
679
+ > `_rsc_loader` endpoint, so a loader reachable only through a client component
680
+ > still resolves in production — on both the generated entry and a hand-written
681
+ > worker entry (e.g. a Cloudflare `worker.rsc.tsx`). You do **not** need to
682
+ > force-import the loader in your worker entry to make it resolve.
683
+
684
+ ### Fetchable Loader with Middleware
685
+
686
+ Pass an options object instead of `true` to attach per-loader middleware.
687
+ This middleware runs only on `_rsc_loader` fetch requests (client-side
688
+ `load()` / `useFetchLoader()` calls), not during SSR `ctx.use()` execution:
689
+
690
+ ```typescript
691
+ import { createLoader } from "@rangojs/router";
692
+ import { authMiddleware } from "../middleware/auth";
693
+ import { rateLimitMiddleware } from "../middleware/rate-limit";
694
+
695
+ export const ProtectedLoader = createLoader(
696
+ async (ctx) => {
697
+ "use server";
698
+
699
+ const user = ctx.get("user");
700
+ return { orders: await db.orders.list(user.id) };
701
+ },
702
+ { middleware: [authMiddleware, rateLimitMiddleware] },
703
+ );
704
+ ```
705
+
706
+ The middleware uses the same `MiddlewareFn` signature as route/app middleware,
707
+ so you can reuse existing middleware functions directly.
708
+
709
+ Fetchable loaders support both GET and POST (PUT, PATCH, DELETE) from the client.
710
+ The `load()` function auto-detects the body type:
711
+
712
+ - **JSON body** (`body: { ... }`) — sent as `application/json`, available as `ctx.body`
713
+ - **FormData body** (`body: formData`) — sent as `multipart/form-data`, available as `ctx.formData`
714
+
715
+ ### Mutation Context
716
+
717
+ When a fetchable loader receives a POST/PUT/PATCH/DELETE request, the context
718
+ includes additional fields depending on the body type:
719
+
720
+ ```typescript
721
+ export const MutationLoader = createLoader(async (ctx) => {
722
+ "use server";
723
+
724
+ // JSON body — available as ctx.body (parsed object)
725
+ const data = ctx.body as { name: string; email: string };
726
+
727
+ // FormData body — available as ctx.formData
728
+ const file = ctx.formData?.get("file") as File | null;
729
+ const name = ctx.formData?.get("name") as string | null;
730
+
731
+ // Route params are always available
732
+ const { slug } = ctx.params;
733
+
734
+ return { success: true };
735
+ }, true);
736
+ ```
737
+
738
+ ### File Upload Example
739
+
740
+ ```typescript
741
+ // loaders/upload.ts
742
+ import { createLoader } from "@rangojs/router";
743
+
744
+ export const FileUploadLoader = createLoader(async (ctx) => {
745
+ "use server";
746
+
747
+ const file = ctx.formData?.get("file") as File | null;
748
+ if (file && file.size > 0) {
749
+ // Save to R2, D1, etc.
750
+ await ctx.env.BUCKET.put(file.name, file.stream());
751
+ return { uploaded: { name: file.name, size: file.size, type: file.type } };
752
+ }
753
+ return { uploaded: null };
754
+ }, true);
755
+ ```
756
+
757
+ Client usage — see `/hooks useFetchLoader` for the full client-side pattern.
758
+
759
+ > **Refetch sharing**: when the loader is registered on the route via
760
+ > `loader()`, a plain `load()` call (no `params`, no `body`) broadcasts
761
+ > the new value to every component reading the same loader id —
762
+ > `useLoader` reads in layouts, pages, and parallel slots all converge.
763
+ > Calls with `params` or a non-GET method stay local to the call site.
764
+ > See `/hooks` → "Shared refetch behavior" for the full contract.
765
+
213
766
  ## Complete Example
214
767
 
215
768
  ```typescript
216
769
  // loaders/shop.ts
217
770
  import { createLoader } from "@rangojs/router";
218
771
 
219
- export const ProductLoader = createLoader("product", async (ctx) => {
220
- const product = await ctx.env.Bindings.DB
772
+ export const ProductLoader = createLoader(async (ctx) => {
773
+ "use server";
774
+
775
+ const product = await ctx.env.DB
221
776
  .prepare("SELECT * FROM products WHERE slug = ?")
222
777
  .bind(ctx.params.slug)
223
778
  .first();
224
779
 
225
780
  if (!product) {
226
- throw new Response("Product not found", { status: 404 });
781
+ notFound("Product not found");
227
782
  }
228
783
 
229
784
  return { product };
230
785
  });
231
786
 
232
- export const CartLoader = createLoader("cart", async (ctx) => {
233
- const user = ctx.env.Variables.user;
787
+ export const CartLoader = createLoader(async (ctx) => {
788
+ "use server";
789
+
790
+ const user = ctx.get("user");
234
791
  if (!user) return { cart: null };
235
792
 
236
- const cart = await ctx.env.Bindings.KV.get(`cart:${user.id}`, "json");
793
+ const cart = await ctx.env.KV.get(`cart:${user.id}`, "json");
237
794
  return { cart };
238
795
  });
239
796
 
240
- // urls.tsx
797
+ // urls.tsx — register loaders in the DSL
798
+ import * as CartActions from "./actions/cart";
799
+
241
800
  export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalidate }) => [
242
801
  layout(<ShopLayout />, () => [
243
- // Shared cart loader for all shop routes
244
802
  loader(CartLoader, () => [
245
- revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
803
+ revalidate((ctx) => ctx.isAction(CartActions) || undefined),
246
804
  ]),
247
805
 
248
806
  path("/shop/product/:slug", ProductPage, { name: "product" }, () => [
@@ -252,18 +810,22 @@ export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalid
252
810
  ]),
253
811
  ]);
254
812
 
255
- // pages/product.tsx
256
- import { useLoader } from "@rangojs/router";
813
+ // components/ProductDetails.tsx — consume in client component
814
+ "use client";
815
+ import { useLoader } from "@rangojs/router/client";
257
816
  import { ProductLoader, CartLoader } from "./loaders/shop";
258
817
 
259
- async function ProductPage() {
260
- const { product } = await useLoader(ProductLoader);
261
- const { cart } = await useLoader(CartLoader);
818
+ function ProductDetails() {
819
+ const { data: { product } } = useLoader(ProductLoader);
820
+ const { data: { cart } } = useLoader(CartLoader);
262
821
 
263
822
  return (
264
823
  <div>
265
824
  <h1>{product.name}</h1>
266
- <AddToCartButton productId={product.id} inCart={cart?.items.includes(product.id)} />
825
+ <AddToCartButton
826
+ productId={product.id}
827
+ inCart={cart?.items.includes(product.id)}
828
+ />
267
829
  </div>
268
830
  );
269
831
  }