@rangojs/router 0.0.0-experimental.97 → 0.0.0-experimental.98914650

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 (356) hide show
  1. package/README.md +24 -9
  2. package/dist/bin/rango.js +157 -63
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +1584 -639
  5. package/package.json +71 -21
  6. package/skills/api-client/SKILL.md +211 -0
  7. package/skills/breadcrumbs/SKILL.md +60 -0
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +222 -30
  10. package/skills/caching/SKILL.md +263 -8
  11. package/skills/composability/SKILL.md +27 -2
  12. package/skills/css/SKILL.md +76 -0
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +3 -1
  15. package/skills/hooks/SKILL.md +235 -28
  16. package/skills/host-router/SKILL.md +122 -22
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +29 -5
  19. package/skills/layout/SKILL.md +13 -9
  20. package/skills/links/SKILL.md +173 -17
  21. package/skills/loader/SKILL.md +170 -23
  22. package/skills/middleware/SKILL.md +16 -10
  23. package/skills/migrate-nextjs/SKILL.md +38 -16
  24. package/skills/mime-routes/SKILL.md +27 -0
  25. package/skills/observability/SKILL.md +137 -0
  26. package/skills/parallel/SKILL.md +11 -7
  27. package/skills/prerender/SKILL.md +14 -33
  28. package/skills/rango/SKILL.md +250 -25
  29. package/skills/react-compiler/SKILL.md +168 -0
  30. package/skills/response-routes/SKILL.md +114 -47
  31. package/skills/route/SKILL.md +42 -5
  32. package/skills/router-setup/SKILL.md +3 -3
  33. package/skills/server-actions/SKILL.md +78 -42
  34. package/skills/tailwind/SKILL.md +27 -3
  35. package/skills/testing/SKILL.md +129 -0
  36. package/skills/testing/bindings.md +89 -0
  37. package/skills/testing/cache-prerender.md +124 -0
  38. package/skills/testing/client-components.md +122 -0
  39. package/skills/testing/e2e-parity.md +125 -0
  40. package/skills/testing/flight.md +92 -0
  41. package/skills/testing/handles.md +129 -0
  42. package/skills/testing/loader.md +128 -0
  43. package/skills/testing/middleware.md +99 -0
  44. package/skills/testing/render-handler.md +121 -0
  45. package/skills/testing/response-routes.md +95 -0
  46. package/skills/testing/reverse-and-types.md +84 -0
  47. package/skills/testing/server-actions.md +107 -0
  48. package/skills/testing/server-tree.md +128 -0
  49. package/skills/testing/setup.md +120 -0
  50. package/skills/typesafety/SKILL.md +316 -26
  51. package/skills/use-cache/SKILL.md +36 -5
  52. package/skills/vercel/SKILL.md +107 -0
  53. package/skills/view-transitions/SKILL.md +294 -0
  54. package/src/__augment-tests__/augment.ts +81 -0
  55. package/src/__augment-tests__/augmented.check.ts +116 -0
  56. package/src/__internal.ts +0 -65
  57. package/src/browser/action-coordinator.ts +53 -36
  58. package/src/browser/action-fence.ts +47 -0
  59. package/src/browser/app-shell.ts +14 -27
  60. package/src/browser/cookie-name.ts +140 -0
  61. package/src/browser/event-controller.ts +37 -143
  62. package/src/browser/history-state.ts +21 -0
  63. package/src/browser/index.ts +3 -3
  64. package/src/browser/invalidate-client-cache.ts +52 -0
  65. package/src/browser/navigation-bridge.ts +30 -59
  66. package/src/browser/navigation-client.ts +96 -84
  67. package/src/browser/navigation-store-handle.ts +38 -0
  68. package/src/browser/navigation-store.ts +32 -82
  69. package/src/browser/navigation-transaction.ts +9 -59
  70. package/src/browser/partial-update.ts +60 -127
  71. package/src/browser/prefetch/cache.ts +82 -72
  72. package/src/browser/prefetch/fetch.ts +108 -33
  73. package/src/browser/prefetch/queue.ts +6 -3
  74. package/src/browser/rango-state.ts +157 -115
  75. package/src/browser/react/Link.tsx +0 -2
  76. package/src/browser/react/NavigationProvider.tsx +41 -48
  77. package/src/browser/react/ScrollRestoration.tsx +10 -6
  78. package/src/browser/react/filter-segment-order.ts +0 -2
  79. package/src/browser/react/index.ts +0 -48
  80. package/src/browser/react/location-state-shared.ts +166 -8
  81. package/src/browser/react/location-state.ts +39 -14
  82. package/src/browser/react/use-action.ts +6 -15
  83. package/src/browser/react/use-handle.ts +17 -14
  84. package/src/browser/react/use-link-status.ts +0 -4
  85. package/src/browser/react/use-navigation.ts +0 -3
  86. package/src/browser/react/use-params.ts +11 -11
  87. package/src/browser/react/use-reverse.ts +106 -0
  88. package/src/browser/react/use-router.ts +20 -5
  89. package/src/browser/react/use-search-params.ts +0 -5
  90. package/src/browser/react/use-segments.ts +0 -13
  91. package/src/browser/response-adapter.ts +52 -1
  92. package/src/browser/rsc-router.tsx +70 -34
  93. package/src/browser/scroll-restoration.ts +22 -14
  94. package/src/browser/segment-structure-assert.ts +2 -2
  95. package/src/browser/server-action-bridge.ts +168 -44
  96. package/src/browser/types.ts +36 -21
  97. package/src/browser/validate-redirect-origin.ts +43 -16
  98. package/src/build/collect-fallback-refs.ts +107 -0
  99. package/src/build/generate-manifest.ts +60 -35
  100. package/src/build/generate-route-types.ts +3 -0
  101. package/src/build/index.ts +8 -2
  102. package/src/build/prefix-tree-utils.ts +123 -0
  103. package/src/build/route-trie.ts +89 -10
  104. package/src/build/route-types/codegen.ts +4 -4
  105. package/src/build/route-types/include-resolution.ts +1 -1
  106. package/src/build/route-types/param-extraction.ts +6 -3
  107. package/src/build/route-types/per-module-writer.ts +7 -4
  108. package/src/build/route-types/router-processing.ts +122 -22
  109. package/src/build/route-types/scan-filter.ts +1 -1
  110. package/src/build/route-types/source-scan.ts +118 -0
  111. package/src/build/runtime-discovery.ts +9 -20
  112. package/src/cache/cache-error.ts +104 -0
  113. package/src/cache/cache-policy.ts +68 -28
  114. package/src/cache/cache-runtime.ts +134 -32
  115. package/src/cache/cache-scope.ts +100 -74
  116. package/src/cache/cache-tag.ts +98 -0
  117. package/src/cache/cf/cf-cache-store.ts +2255 -238
  118. package/src/cache/cf/index.ts +6 -16
  119. package/src/cache/document-cache.ts +61 -20
  120. package/src/cache/handle-snapshot.ts +63 -0
  121. package/src/cache/index.ts +22 -20
  122. package/src/cache/memory-segment-store.ts +136 -37
  123. package/src/cache/profile-registry.ts +6 -30
  124. package/src/cache/read-through-swr.ts +41 -11
  125. package/src/cache/segment-codec.ts +0 -16
  126. package/src/cache/tag-invalidation.ts +230 -0
  127. package/src/cache/types.ts +33 -100
  128. package/src/cache/vercel/index.ts +11 -0
  129. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  130. package/src/client.rsc.tsx +6 -21
  131. package/src/client.tsx +25 -61
  132. package/src/component-utils.ts +19 -0
  133. package/src/context-var.ts +17 -5
  134. package/src/decode-loader-results.ts +36 -0
  135. package/src/defer.ts +196 -0
  136. package/src/deps/ssr.ts +0 -1
  137. package/src/errors.ts +30 -4
  138. package/src/handle.ts +31 -23
  139. package/src/handles/MetaTags.tsx +0 -14
  140. package/src/handles/breadcrumbs.ts +16 -5
  141. package/src/handles/meta.ts +0 -39
  142. package/src/host/cookie-handler.ts +0 -36
  143. package/src/host/errors.ts +0 -24
  144. package/src/host/index.ts +8 -2
  145. package/src/host/pattern-matcher.ts +7 -50
  146. package/src/host/router.ts +107 -99
  147. package/src/host/testing.ts +40 -27
  148. package/src/host/types.ts +37 -4
  149. package/src/host/utils.ts +1 -1
  150. package/src/href-client.ts +137 -22
  151. package/src/index.rsc.ts +63 -9
  152. package/src/index.ts +64 -9
  153. package/src/internal-debug.ts +2 -4
  154. package/src/loader-store.ts +500 -0
  155. package/src/loader.rsc.ts +20 -13
  156. package/src/loader.ts +12 -11
  157. package/src/missing-id-error.ts +68 -0
  158. package/src/network-error-thrower.tsx +1 -6
  159. package/src/outlet-provider.tsx +1 -5
  160. package/src/prerender/param-hash.ts +10 -11
  161. package/src/prerender/store.ts +32 -37
  162. package/src/prerender.ts +61 -6
  163. package/src/redirect-origin.ts +100 -0
  164. package/src/response-utils.ts +9 -0
  165. package/src/reverse.ts +65 -40
  166. package/src/root-error-boundary.tsx +1 -19
  167. package/src/route-content-wrapper.tsx +7 -72
  168. package/src/route-definition/dsl-helpers.ts +244 -281
  169. package/src/route-definition/helper-factories.ts +29 -139
  170. package/src/route-definition/helpers-types.ts +40 -17
  171. package/src/route-definition/redirect.ts +43 -9
  172. package/src/route-definition/resolve-handler-use.ts +6 -0
  173. package/src/route-definition/use-item-types.ts +32 -0
  174. package/src/route-map-builder.ts +0 -16
  175. package/src/route-types.ts +19 -41
  176. package/src/router/basename.ts +14 -0
  177. package/src/router/content-negotiation.ts +15 -15
  178. package/src/router/error-handling.ts +13 -17
  179. package/src/router/find-match.ts +44 -23
  180. package/src/router/handler-context.ts +4 -41
  181. package/src/router/intercept-resolution.ts +14 -19
  182. package/src/router/lazy-includes.ts +9 -46
  183. package/src/router/loader-resolution.ts +91 -46
  184. package/src/router/logging.ts +0 -6
  185. package/src/router/manifest.ts +18 -29
  186. package/src/router/match-api.ts +0 -20
  187. package/src/router/match-context.ts +0 -22
  188. package/src/router/match-handlers.ts +57 -58
  189. package/src/router/match-middleware/background-revalidation.ts +0 -7
  190. package/src/router/match-middleware/cache-lookup.ts +150 -271
  191. package/src/router/match-middleware/cache-store.ts +3 -33
  192. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  193. package/src/router/match-middleware/segment-resolution.ts +0 -22
  194. package/src/router/match-pipelines.ts +1 -42
  195. package/src/router/match-result.ts +31 -80
  196. package/src/router/metrics.ts +0 -34
  197. package/src/router/middleware-types.ts +5 -112
  198. package/src/router/middleware.ts +118 -133
  199. package/src/router/navigation-snapshot.ts +0 -51
  200. package/src/router/params-util.ts +23 -0
  201. package/src/router/pattern-matching.ts +62 -67
  202. package/src/router/prerender-match.ts +99 -63
  203. package/src/router/preview-match.ts +3 -1
  204. package/src/router/request-classification.ts +28 -62
  205. package/src/router/revalidation.ts +50 -56
  206. package/src/router/route-snapshot.ts +0 -1
  207. package/src/router/router-context.ts +0 -27
  208. package/src/router/router-interfaces.ts +68 -35
  209. package/src/router/router-options.ts +55 -1
  210. package/src/router/router-registry.ts +2 -5
  211. package/src/router/segment-resolution/fresh.ts +44 -63
  212. package/src/router/segment-resolution/helpers.ts +34 -0
  213. package/src/router/segment-resolution/loader-cache.ts +40 -37
  214. package/src/router/segment-resolution/revalidation.ts +203 -285
  215. package/src/router/segment-resolution/static-store.ts +19 -5
  216. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  217. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  218. package/src/router/segment-resolution.ts +4 -1
  219. package/src/router/segment-wrappers.ts +0 -3
  220. package/src/router/state-cookie-name.ts +33 -0
  221. package/src/router/substitute-pattern-params.ts +56 -0
  222. package/src/router/telemetry-otel.ts +0 -20
  223. package/src/router/telemetry.ts +96 -19
  224. package/src/router/timeout.ts +0 -20
  225. package/src/router/trie-matching.ts +87 -48
  226. package/src/router/types.ts +9 -63
  227. package/src/router/url-params.ts +0 -5
  228. package/src/router.ts +80 -41
  229. package/src/rsc/handler-context.ts +3 -2
  230. package/src/rsc/handler.ts +83 -78
  231. package/src/rsc/helpers.ts +93 -5
  232. package/src/rsc/index.ts +1 -1
  233. package/src/rsc/json-route-result.ts +38 -0
  234. package/src/rsc/manifest-init.ts +28 -41
  235. package/src/rsc/origin-guard.ts +39 -25
  236. package/src/rsc/progressive-enhancement.ts +12 -1
  237. package/src/rsc/redirect-guard.ts +99 -0
  238. package/src/rsc/response-error.ts +79 -12
  239. package/src/rsc/response-route-handler.ts +76 -62
  240. package/src/rsc/rsc-rendering.ts +41 -60
  241. package/src/rsc/runtime-warnings.ts +23 -10
  242. package/src/rsc/server-action.ts +62 -67
  243. package/src/rsc/ssr-setup.ts +16 -0
  244. package/src/rsc/types.ts +10 -5
  245. package/src/runtime-env.ts +18 -0
  246. package/src/search-params.ts +4 -20
  247. package/src/segment-loader-promise.ts +14 -2
  248. package/src/segment-system.tsx +199 -142
  249. package/src/serialize.ts +243 -0
  250. package/src/server/context.ts +150 -51
  251. package/src/server/cookie-store.ts +80 -5
  252. package/src/server/handle-store.ts +7 -24
  253. package/src/server/loader-registry.ts +5 -24
  254. package/src/server/request-context.ts +165 -87
  255. package/src/ssr/index.tsx +14 -14
  256. package/src/static-handler.ts +10 -13
  257. package/src/testing/cache-status.ts +162 -0
  258. package/src/testing/collect-handle.ts +40 -0
  259. package/src/testing/dispatch.ts +618 -0
  260. package/src/testing/dom.entry.ts +22 -0
  261. package/src/testing/e2e/fixture.ts +188 -0
  262. package/src/testing/e2e/index.ts +128 -0
  263. package/src/testing/e2e/matchers.ts +35 -0
  264. package/src/testing/e2e/page-helpers.ts +272 -0
  265. package/src/testing/e2e/parity.ts +387 -0
  266. package/src/testing/e2e/server.ts +195 -0
  267. package/src/testing/flight-matchers.ts +97 -0
  268. package/src/testing/flight-normalize.ts +11 -0
  269. package/src/testing/flight-runtime.d.ts +57 -0
  270. package/src/testing/flight-tree.ts +682 -0
  271. package/src/testing/flight.entry.ts +52 -0
  272. package/src/testing/flight.ts +232 -0
  273. package/src/testing/generated-routes.ts +183 -0
  274. package/src/testing/index.ts +99 -0
  275. package/src/testing/internal/context.ts +348 -0
  276. package/src/testing/internal/flight-client-globals.ts +30 -0
  277. package/src/testing/internal/seed-vars.ts +54 -0
  278. package/src/testing/render-handler.ts +330 -0
  279. package/src/testing/render-route.tsx +566 -0
  280. package/src/testing/run-loader.ts +378 -0
  281. package/src/testing/run-middleware.ts +205 -0
  282. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  283. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  284. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  285. package/src/testing/vitest-stubs/version.ts +5 -0
  286. package/src/testing/vitest.ts +305 -0
  287. package/src/theme/ThemeProvider.tsx +0 -52
  288. package/src/theme/ThemeScript.tsx +0 -6
  289. package/src/theme/constants.ts +0 -12
  290. package/src/theme/index.ts +0 -7
  291. package/src/theme/theme-context.ts +1 -5
  292. package/src/theme/theme-script.ts +0 -14
  293. package/src/theme/use-theme.ts +0 -3
  294. package/src/types/boundaries.ts +0 -35
  295. package/src/types/cache-types.ts +13 -4
  296. package/src/types/error-types.ts +30 -90
  297. package/src/types/global-namespace.ts +54 -41
  298. package/src/types/handler-context.ts +97 -22
  299. package/src/types/index.ts +1 -10
  300. package/src/types/loader-types.ts +6 -3
  301. package/src/types/request-scope.ts +0 -19
  302. package/src/types/route-config.ts +6 -50
  303. package/src/types/route-entry.ts +0 -6
  304. package/src/types/segments.ts +18 -14
  305. package/src/urls/include-helper.ts +9 -56
  306. package/src/urls/index.ts +1 -11
  307. package/src/urls/path-helper-types.ts +19 -5
  308. package/src/urls/path-helper.ts +17 -106
  309. package/src/urls/pattern-types.ts +36 -19
  310. package/src/urls/response-types.ts +20 -19
  311. package/src/urls/type-extraction.ts +58 -139
  312. package/src/urls/urls-function.ts +1 -18
  313. package/src/use-loader.tsx +292 -107
  314. package/src/vite/debug.ts +1 -0
  315. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  316. package/src/vite/discovery/discover-routers.ts +95 -82
  317. package/src/vite/discovery/discovery-errors.ts +194 -0
  318. package/src/vite/discovery/prerender-collection.ts +26 -34
  319. package/src/vite/discovery/route-types-writer.ts +40 -84
  320. package/src/vite/discovery/state.ts +39 -1
  321. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  322. package/src/vite/index.ts +4 -0
  323. package/src/vite/plugin-types.ts +185 -10
  324. package/src/vite/plugins/cjs-to-esm.ts +3 -18
  325. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  326. package/src/vite/plugins/client-ref-hashing.ts +12 -11
  327. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  328. package/src/vite/plugins/expose-action-id.ts +4 -75
  329. package/src/vite/plugins/expose-id-utils.ts +3 -54
  330. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  331. package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
  332. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  333. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  334. package/src/vite/plugins/expose-internal-ids.ts +57 -67
  335. package/src/vite/plugins/performance-tracks.ts +9 -16
  336. package/src/vite/plugins/refresh-cmd.ts +1 -1
  337. package/src/vite/plugins/use-cache-transform.ts +26 -49
  338. package/src/vite/plugins/vercel-output.ts +258 -0
  339. package/src/vite/plugins/version-injector.ts +2 -32
  340. package/src/vite/plugins/version-plugin.ts +32 -23
  341. package/src/vite/plugins/virtual-entries.ts +35 -17
  342. package/src/vite/rango.ts +148 -115
  343. package/src/vite/router-discovery.ts +220 -68
  344. package/src/vite/utils/ast-handler-extract.ts +15 -31
  345. package/src/vite/utils/bundle-analysis.ts +10 -15
  346. package/src/vite/utils/client-chunks.ts +184 -0
  347. package/src/vite/utils/forward-user-plugins.ts +171 -0
  348. package/src/vite/utils/manifest-utils.ts +4 -59
  349. package/src/vite/utils/package-resolution.ts +1 -73
  350. package/src/vite/utils/prerender-utils.ts +0 -34
  351. package/src/vite/utils/shared-utils.ts +95 -43
  352. package/src/browser/action-response-classifier.ts +0 -99
  353. package/src/browser/react/use-client-cache.ts +0 -58
  354. package/src/browser/shallow.ts +0 -40
  355. package/src/handles/index.ts +0 -7
  356. package/src/router/middleware-cookies.ts +0 -55
@@ -12,18 +12,26 @@ import {
12
12
  type ReactNode,
13
13
  } from "react";
14
14
  import { OutletContext, type OutletContextValue } from "./outlet-context.js";
15
+ import { loaderStore, type LoaderEntry } from "./loader-store.js";
15
16
  import type { LoaderDefinition, LoadOptions } from "./types.js";
16
17
 
17
- /**
18
- * Extract a specific loader's data from a content ReactNode.
19
- *
20
- * When a route registers loaders via loader(), the resolved data lives in
21
- * the route's OutletProvider (rendered as <Outlet /> content). Parallel
22
- * slots are siblings of <Outlet />, so they can't find it by walking
23
- * the parent context chain. This helper traverses wrapper elements
24
- * (MountContextProvider, ViewTransition, etc.) to reach the OutletProvider
25
- * and extract the loader data directly.
26
- */
18
+ function isShareableGet(options: LoadOptions | undefined): boolean {
19
+ if (!options) return true;
20
+ if (options.method && options.method !== "GET") return false;
21
+ if ("body" in options && (options as { body?: unknown }).body !== undefined) {
22
+ return false;
23
+ }
24
+ return true;
25
+ }
26
+
27
+ function isPlainRefetch(options: LoadOptions | undefined): boolean {
28
+ if (!isShareableGet(options)) return false;
29
+ if (options?.params && Object.keys(options.params).length > 0) return false;
30
+ return true;
31
+ }
32
+
33
+ let privateGroupBucketSeq = 0;
34
+
27
35
  const NOT_FOUND = Symbol("not-found");
28
36
 
29
37
  function extractContentLoaderData(
@@ -39,10 +47,6 @@ function extractContentLoaderData(
39
47
  return props.loaderData[loaderId];
40
48
  }
41
49
 
42
- // LoaderBoundary: loaderIds + loaderDataPromise (already resolved array).
43
- // When the segment has loading(), loaderData is resolved inside
44
- // LoaderBoundary via use(). If the promise was pre-awaited (forceAwait
45
- // or isAction), the prop is a raw array we can index into.
46
50
  if (
47
51
  props.loaderIds &&
48
52
  Array.isArray(props.loaderIds) &&
@@ -52,7 +56,6 @@ function extractContentLoaderData(
52
56
  const idx = (props.loaderIds as string[]).indexOf(loaderId);
53
57
  if (idx !== -1) {
54
58
  const data = (props.loaderDataPromise as any[])[idx];
55
- // loaderDataPromise entries may be { ok, data } result objects
56
59
  if (data && typeof data === "object" && "ok" in data) {
57
60
  return data.ok ? data.data : NOT_FOUND;
58
61
  }
@@ -60,150 +63,265 @@ function extractContentLoaderData(
60
63
  }
61
64
  }
62
65
 
63
- // Traverse into wrapper elements (MountContextProvider, ViewTransition,
64
- // Suspense wrappers, etc.)
65
66
  if (props.children) return extractContentLoaderData(props.children, loaderId);
66
67
  return NOT_FOUND;
67
68
  }
68
69
 
69
- /**
70
- * Payload returned by loader RSC requests
71
- */
72
70
  interface LoaderRscPayload<T = unknown> {
73
71
  loaderResult: T;
74
72
  loaderError?: { message: string; name: string };
75
73
  }
76
74
 
77
- /**
78
- * Load function type for fetching loader data from the client
79
- */
80
75
  export type LoadFunction<T> = (options?: LoadOptions) => Promise<T>;
81
76
 
82
- /**
83
- * Result type for useLoader hook (strict - data is required)
84
- */
85
77
  export interface UseLoaderResult<T> {
86
- /** The loaded data - guaranteed to exist when loader is registered on route */
87
78
  data: T;
88
- /** True while a load() is in progress */
89
79
  isLoading: boolean;
90
- /** Error from the most recent load attempt, null if successful */
91
80
  error: Error | null;
92
- /** Function to trigger a fetch (only works if loader is fetchable) */
93
81
  load: LoadFunction<T>;
94
- /** Alias for load */
95
82
  refetch: LoadFunction<T>;
96
83
  }
97
84
 
98
- /**
99
- * Result type for useFetchLoader hook (flexible - data is optional)
100
- */
101
85
  export interface UseFetchLoaderResult<T> {
102
- /** The loaded data - may be undefined if not yet fetched or not in context */
103
86
  data: T | undefined;
104
- /** True while a load() is in progress */
105
87
  isLoading: boolean;
106
- /** Error from the most recent load attempt, null if successful */
107
88
  error: Error | null;
108
- /** Function to trigger a fetch (only works if loader is fetchable) */
109
89
  load: LoadFunction<T>;
110
- /** Alias for load */
111
90
  refetch: LoadFunction<T>;
112
91
  }
113
92
 
114
- /**
115
- * Options for useLoader hook
116
- */
117
93
  export interface UseLoaderOptions {
118
- /**
119
- * If true (default), errors from load() will be thrown to the nearest error boundary.
120
- * If false, errors are only captured in the `error` state.
121
- * @default true
122
- */
123
94
  throwOnError?: boolean;
95
+ key?: string;
96
+ refreshGroup?: string | string[];
124
97
  }
125
98
 
126
- /**
127
- * Internal hook implementation shared by useLoader and useFetchLoader
128
- */
129
99
  function useLoaderInternal<T>(
130
100
  loader: LoaderDefinition<T>,
131
101
  options?: UseLoaderOptions,
132
102
  ): UseFetchLoaderResult<T> {
133
103
  const context = useContext(OutletContext);
134
104
 
135
- // Get data from context (SSR/navigation)
136
- const contextData = useMemo((): T | undefined => {
105
+ const { contextData, hasContextData } = useMemo((): {
106
+ contextData: T | undefined;
107
+ hasContextData: boolean;
108
+ } => {
137
109
  let current: OutletContextValue | null | undefined = context;
138
110
  while (current) {
139
111
  if (current.loaderData && loader.$$id in current.loaderData) {
140
- return current.loaderData[loader.$$id] as T;
112
+ return {
113
+ contextData: current.loaderData[loader.$$id] as T,
114
+ hasContextData: true,
115
+ };
141
116
  }
142
- // Check content element — the route's OutletProvider is rendered as
143
- // <Outlet /> content (a child), so its loaderData isn't in the parent
144
- // chain. Parallel slots need to reach into it to find route-level loaders.
145
117
  const contentData = extractContentLoaderData(
146
118
  current.content,
147
119
  loader.$$id,
148
120
  );
149
121
  if (contentData !== NOT_FOUND) {
150
- return contentData as T;
122
+ return { contextData: contentData as T, hasContextData: true };
151
123
  }
152
124
  current = current.parent;
153
125
  }
154
- return undefined;
126
+ return { contextData: undefined, hasContextData: false };
155
127
  }, [context, loader.$$id]);
156
128
 
157
- // Local state for fetched data (from load() calls)
158
- const [fetchedData, setFetchedData] = useState<T | undefined>(undefined);
159
- const [isLoading, setIsLoading] = useState(false);
160
- const [error, setError] = useState<Error | null>(null);
161
- const requestIdRef = useRef(0);
162
-
163
- // Track context data changes to reset fetched data on navigation
129
+ const loaderId = loader.$$id;
130
+ const key = options?.key;
131
+ const refreshGroupOption = options?.refreshGroup;
132
+ const groupKey =
133
+ refreshGroupOption === undefined
134
+ ? ""
135
+ : JSON.stringify(
136
+ typeof refreshGroupOption === "string"
137
+ ? [refreshGroupOption]
138
+ : [...new Set(refreshGroupOption)].sort(),
139
+ );
140
+ const groupList = useMemo<string[]>(
141
+ () => (groupKey === "" ? [] : (JSON.parse(groupKey) as string[])),
142
+ [groupKey],
143
+ );
144
+ const hasGroups = groupList.length > 0;
145
+ const privateBucketIdRef = useRef<string | null>(null);
146
+ if (hasGroups && key === undefined && privateBucketIdRef.current === null) {
147
+ privateBucketIdRef.current = `__rg${privateGroupBucketSeq++}`;
148
+ }
149
+ const effectiveKey =
150
+ key ?? (hasGroups ? privateBucketIdRef.current! : undefined);
151
+ const bucketKey =
152
+ effectiveKey === undefined ? loaderId : `${loaderId}::${effectiveKey}`;
153
+
154
+ const groupRefetch = useCallback(async (): Promise<void> => {
155
+ if (!loaderId) return;
156
+ const requestId = loaderStore.reserveRequestId(bucketKey);
157
+ loaderStore.beginRequest(bucketKey, requestId);
158
+ try {
159
+ const url = new URL(window.location.href);
160
+ url.searchParams.set("_rsc_loader", loaderId);
161
+ const response = fetch(url.toString(), {
162
+ method: "GET",
163
+ headers: { Accept: "text/x-component" },
164
+ });
165
+ const { createFromFetch } = await import("./deps/browser.js");
166
+ const payload = await createFromFetch<LoaderRscPayload<T>>(response);
167
+ if (payload.loaderError) {
168
+ throw new Error(payload.loaderError.message);
169
+ }
170
+ loaderStore.finishData(bucketKey, requestId, payload.loaderResult);
171
+ } catch (e) {
172
+ const err = e instanceof Error ? e : new Error(String(e));
173
+ loaderStore.finishError(bucketKey, requestId, err);
174
+ throw err;
175
+ } finally {
176
+ loaderStore.setLoading(bucketKey, requestId, false);
177
+ }
178
+ }, [loaderId, bucketKey]);
179
+
180
+ const [sharedState, setSharedState] = useState<{
181
+ bucketKey: string;
182
+ snapshot: LoaderEntry;
183
+ }>(() => ({
184
+ bucketKey,
185
+ snapshot: loaderStore.getSnapshot(bucketKey),
186
+ }));
187
+ const sharedSnapshot =
188
+ sharedState.bucketKey === bucketKey
189
+ ? sharedState.snapshot
190
+ : loaderStore.getSnapshot(bucketKey);
191
+ useEffect(() => {
192
+ const initial = loaderStore.getSnapshot(bucketKey);
193
+ if (initial !== sharedSnapshot) {
194
+ startTransition(() => {
195
+ setSharedState({ bucketKey, snapshot: initial });
196
+ });
197
+ }
198
+ // ephemeral: a reader with no route context has no route-context reset
199
+ // trigger, so its keyed bucket is reference-counted by the store. A
200
+ // route-registered reader makes the bucket sticky (reset via clearFamily).
201
+ return loaderStore.subscribe(
202
+ bucketKey,
203
+ () => {
204
+ const next = loaderStore.getSnapshot(bucketKey);
205
+ startTransition(() => {
206
+ setSharedState({ bucketKey, snapshot: next });
207
+ });
208
+ },
209
+ {
210
+ loaderId,
211
+ ephemeral: !hasContextData,
212
+ group: hasGroups ? groupList : undefined,
213
+ refetch: hasGroups ? groupRefetch : undefined,
214
+ },
215
+ );
216
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- intentional:
217
+ // sharedSnapshot is captured for the one-shot init sync; we don't want
218
+ // to re-subscribe on every snapshot change. bucketKey, hasContextData,
219
+ // groupKey, and groupRefetch are the only inputs that require a fresh
220
+ // subscription (groupList is memoized on groupKey; groupRefetch is stable
221
+ // per bucketKey).
222
+ }, [bucketKey, hasContextData, groupKey, groupRefetch]);
223
+
224
+ // Local state holds the result of:
225
+ // - parameterized / mutation `load()` calls (load({ params }), POST,
226
+ // etc.) — stay scoped so concurrent same-loader different-params
227
+ // fetches don't clobber each other through the shared store;
228
+ // - any `load()` made by hooks that are NOT in route context (i.e.
229
+ // useFetchLoader of an unregistered loader) — keeping those local
230
+ // prevents two unrelated components from accidentally sharing data
231
+ // through the global store just because they reference the same
232
+ // loader id.
233
+ // `has` distinguishes a committed local result (including `null`/`undefined`)
234
+ // from "no local load yet", so a load() that resolves to a falsy value is not
235
+ // discarded in favor of the shared snapshot or the seeded context.
236
+ const [localFetchedData, setLocalFetchedData] = useState<{
237
+ has: boolean;
238
+ value: T | undefined;
239
+ }>({ has: false, value: undefined });
240
+ const [localIsLoading, setLocalIsLoading] = useState(false);
241
+ const [localError, setLocalError] = useState<Error | null>(null);
242
+
243
+ // Local request id, mirrors the per-hook gating the previous
244
+ // implementation provided. Two quick parameterized loads from the same
245
+ // hook (e.g. load({ params: { q: "a" } }) then load({ params: { q: "b" } }))
246
+ // can resolve out of order — only the latest must commit.
247
+ const localRequestIdRef = useRef(0);
248
+
249
+ // Tracks the request id of the most recent SHARED load() this hook
250
+ // initiated. The render-throw rule below uses it to scope the throw
251
+ // to the originating hook only — sibling readers see the error in
252
+ // `error` but don't blow up their own boundaries.
253
+ const lastSharedRequestIdRef = useRef<number | null>(null);
254
+
255
+ // Reset on navigation. clear() bumps the entry's latest request id so
256
+ // any pre-navigation load() promise that resolves later fails its gate
257
+ // and is dropped — fixes the race where a stale fetch overwrites the
258
+ // new route's context.
164
259
  const prevContextDataRef = useRef(contextData);
165
260
  useEffect(() => {
166
261
  if (prevContextDataRef.current !== contextData) {
167
- // Navigation happened, clear fetched data so context takes precedence
168
- setFetchedData(undefined);
169
- setError(null);
262
+ setLocalFetchedData({ has: false, value: undefined });
263
+ setLocalIsLoading(false);
264
+ setLocalError(null);
265
+ lastSharedRequestIdRef.current = null;
266
+ // Reset every sticky bucket of this loader (keyed or not). Ephemeral
267
+ // (unregistered keyed) buckets are left to their refcount lifecycle.
268
+ loaderStore.clearFamily(loaderId);
170
269
  prevContextDataRef.current = contextData;
171
270
  }
172
- }, [contextData]);
173
-
174
- // Data priority: fetched data (if any) > context data
175
- const data = fetchedData ?? contextData;
271
+ }, [contextData, loaderId]);
272
+
273
+ // Read priority: a committed parameterized load() result overrides the shared
274
+ // snapshot; a committed shared snapshot overrides the server-seeded context.
275
+ // `has`/`hasValue` gate each level so a committed falsy value is not skipped.
276
+ const data = localFetchedData.has
277
+ ? localFetchedData.value
278
+ : sharedSnapshot.hasValue
279
+ ? (sharedSnapshot.value as T | undefined)
280
+ : contextData;
281
+ const isLoading = localIsLoading || sharedSnapshot.isLoading;
282
+ const error = localError ?? sharedSnapshot.error;
176
283
 
177
284
  const throwOnError = options?.throwOnError ?? true;
178
285
 
179
- // Refs for values used inside load() that should NOT cause callback identity
180
- // churn. loader.$$id can change if a reusable component receives a different
181
- // loader without remounting; data changes on every navigation. Refs keep the
182
- // callback stable while always reading the latest values.
183
- const loaderIdRef = useRef(loader.$$id);
184
- loaderIdRef.current = loader.$$id;
286
+ const loaderIdRef = useRef(loaderId);
287
+ loaderIdRef.current = loaderId;
288
+ const bucketKeyRef = useRef(bucketKey);
289
+ bucketKeyRef.current = bucketKey;
185
290
  const dataRef = useRef(data);
186
291
  dataRef.current = data;
292
+ const hasContextDataRef = useRef(hasContextData);
293
+ hasContextDataRef.current = hasContextData;
187
294
 
188
- // Load function for fetching data via the ?_rsc_loader endpoint.
189
- // Supports GET (data fetching) and POST/PUT/PATCH/DELETE (mutations).
190
295
  const load = useCallback(
191
296
  async (loadOptions?: LoadOptions): Promise<T> => {
192
- const requestId = ++requestIdRef.current;
193
- const loaderId = loaderIdRef.current;
194
- // Verify the loader has $$id
195
- if (!loaderId) {
297
+ const id = loaderIdRef.current;
298
+ if (!id) {
196
299
  throw new Error(
197
300
  `Loader is missing $$id. Make sure the exposeLoaderId Vite plugin is enabled.`,
198
301
  );
199
302
  }
200
303
 
201
- setIsLoading(true);
202
- setError(null);
304
+ const bucket = bucketKeyRef.current;
305
+ const hasDedicatedBucket = bucket !== id;
306
+
307
+ const shared = hasDedicatedBucket
308
+ ? isShareableGet(loadOptions)
309
+ : isPlainRefetch(loadOptions) && hasContextDataRef.current;
310
+ let sharedRequestId = -1;
311
+ let localRequestId = -1;
312
+ if (shared) {
313
+ sharedRequestId = loaderStore.reserveRequestId(bucket);
314
+ lastSharedRequestIdRef.current = sharedRequestId;
315
+ loaderStore.beginRequest(bucket, sharedRequestId);
316
+ } else {
317
+ localRequestId = ++localRequestIdRef.current;
318
+ setLocalIsLoading(true);
319
+ setLocalError(null);
320
+ }
203
321
 
204
322
  try {
205
323
  const url = new URL(window.location.href);
206
- url.searchParams.set("_rsc_loader", loaderId);
324
+ url.searchParams.set("_rsc_loader", id);
207
325
 
208
326
  const method = loadOptions?.method ?? "GET";
209
327
  const isBodyMethod = method !== "GET";
@@ -219,8 +337,6 @@ function useLoaderInternal<T>(
219
337
  loadOptions?.params && Object.keys(loadOptions.params).length > 0;
220
338
 
221
339
  if (bodyValue instanceof FormData) {
222
- // FormData body — send as multipart/form-data (preserves File objects).
223
- // Params are appended as a JSON string in a special field.
224
340
  if (hasParams) {
225
341
  bodyValue.set(
226
342
  "_rsc_loader_params",
@@ -233,7 +349,6 @@ function useLoaderInternal<T>(
233
349
  body: bodyValue,
234
350
  };
235
351
  } else {
236
- // JSON body — send params and body as JSON
237
352
  const bodyPayload: {
238
353
  params?: Record<string, string>;
239
354
  body?: unknown;
@@ -255,7 +370,6 @@ function useLoaderInternal<T>(
255
370
  };
256
371
  }
257
372
  } else {
258
- // GET - send params in query string
259
373
  if (
260
374
  loadOptions?.params &&
261
375
  Object.keys(loadOptions.params).length > 0
@@ -284,36 +398,45 @@ function useLoaderInternal<T>(
284
398
  }
285
399
 
286
400
  const result = payload.loaderResult;
287
- if (requestId === requestIdRef.current) {
401
+ if (shared) {
402
+ loaderStore.finishData(bucket, sharedRequestId, result);
403
+ } else if (localRequestId === localRequestIdRef.current) {
288
404
  startTransition(() => {
289
- setFetchedData(result);
405
+ setLocalFetchedData({ has: true, value: result });
406
+ setLocalIsLoading(false);
290
407
  });
291
408
  }
292
409
  return result;
293
410
  } catch (e) {
294
411
  const err = e instanceof Error ? e : new Error(String(e));
295
- if (requestId === requestIdRef.current) {
296
- setError(err);
412
+ if (shared) {
413
+ loaderStore.finishError(bucket, sharedRequestId, err);
414
+ } else if (localRequestId === localRequestIdRef.current) {
415
+ setLocalError(err);
416
+ setLocalIsLoading(false);
297
417
  }
298
418
  if (throwOnError) {
299
419
  throw err;
300
420
  }
301
- // When throwOnError is false, return the latest data snapshot (previous
302
- // successful value or undefined). Caller should check error state.
303
421
  return dataRef.current as T;
304
422
  } finally {
305
- if (requestId === requestIdRef.current) {
306
- setIsLoading(false);
423
+ if (shared) {
424
+ loaderStore.setLoading(bucket, sharedRequestId, false);
307
425
  }
308
426
  }
309
427
  },
310
428
  [throwOnError],
311
429
  );
312
430
 
313
- // Throw during render if there's an error and throwOnError is true
314
- // This allows ErrorBoundaries to catch async errors from load()
315
- if (error && throwOnError) {
316
- throw error;
431
+ if (throwOnError) {
432
+ if (localError) throw localError;
433
+ if (
434
+ sharedSnapshot.error &&
435
+ lastSharedRequestIdRef.current !== null &&
436
+ sharedSnapshot.requestId === lastSharedRequestIdRef.current
437
+ ) {
438
+ throw sharedSnapshot.error;
439
+ }
317
440
  }
318
441
 
319
442
  return {
@@ -357,7 +480,7 @@ function useLoaderInternal<T>(
357
480
  export function useLoader<T>(
358
481
  loader: LoaderDefinition<T>,
359
482
  options?: UseLoaderOptions,
360
- ): UseLoaderResult<T> {
483
+ ): UseLoaderResult<Rango.FlightSerialize<T>> {
361
484
  const result = useLoaderInternal(loader, options);
362
485
 
363
486
  // Strict mode: throw if data is not in context
@@ -369,7 +492,7 @@ export function useLoader<T>(
369
492
  );
370
493
  }
371
494
 
372
- return result as UseLoaderResult<T>;
495
+ return result as UseLoaderResult<Rango.FlightSerialize<T>>;
373
496
  }
374
497
 
375
498
  /**
@@ -421,6 +544,68 @@ export function useLoader<T>(
421
544
  export function useFetchLoader<T>(
422
545
  loader: LoaderDefinition<T>,
423
546
  options?: UseLoaderOptions,
424
- ): UseFetchLoaderResult<T> {
425
- return useLoaderInternal(loader, options);
547
+ ): UseFetchLoaderResult<Rango.FlightSerialize<T>> {
548
+ return useLoaderInternal(loader, options) as UseFetchLoaderResult<
549
+ Rango.FlightSerialize<T>
550
+ >;
551
+ }
552
+
553
+ /**
554
+ * Get a stable function that refreshes loaders by cross-loader group tag.
555
+ *
556
+ * The returned `refresh(groups)` takes one group name or an array of names and
557
+ * re-runs every currently-mounted read tagged with ANY of them, with a plain GET
558
+ * against the current route URL. This is the cross-loader counterpart to the
559
+ * single-loader `key`: use it to refresh a set of DIFFERENT loaders together
560
+ * (e.g. profile + orders after an account switch). Members are tagged via
561
+ * `useLoader(Loader, { refreshGroup })` / `useFetchLoader(Loader, { refreshGroup })`,
562
+ * where `refreshGroup` is one name or several.
563
+ *
564
+ * Passing the group(s) to the returned function rather than to the hook lets a
565
+ * single `useRefreshLoaders()` instance refresh different groups depending on
566
+ * context, and lets one call refresh several groups at once — their members are
567
+ * unioned and deduped, so a loader tagged into two of the named groups is fetched
568
+ * exactly once.
569
+ *
570
+ * Group refresh never render-throws: a failing member surfaces its error via
571
+ * that read's `error` state, and the returned promise rejects with an
572
+ * `AggregateError` of the failures so the caller can handle them at the await
573
+ * site. Each loader is refreshed in place — no params, no body, no mutations.
574
+ *
575
+ * @example
576
+ * ```tsx
577
+ * "use client";
578
+ * import { useLoader, useRefreshLoaders } from "rsc-router/client";
579
+ *
580
+ * function Profile() {
581
+ * const { data } = useLoader(ProfileLoader, { key: userId, refreshGroup: "account" });
582
+ * return <span>{data.name}</span>;
583
+ * }
584
+ * function Orders() {
585
+ * // Tagged into two groups: refreshed by "account" (the whole set) or "orders".
586
+ * const { data } = useLoader(OrdersLoader, {
587
+ * key: userId,
588
+ * refreshGroup: ["account", "orders"],
589
+ * });
590
+ * return <span>{data.count} orders</span>;
591
+ * }
592
+ * function RefreshButtons() {
593
+ * const refresh = useRefreshLoaders();
594
+ * return (
595
+ * <>
596
+ * <button onClick={() => refresh("account")}>Refresh account</button>
597
+ * <button onClick={() => refresh("orders")}>Refresh orders</button>
598
+ * <button onClick={() => refresh(["account", "orders"])}>Refresh both</button>
599
+ * </>
600
+ * );
601
+ * }
602
+ * ```
603
+ */
604
+ export function useRefreshLoaders(): (
605
+ groups: string | string[],
606
+ ) => Promise<void> {
607
+ return useCallback(
608
+ (groups: string | string[]) => loaderStore.refreshGroups(groups),
609
+ [],
610
+ );
426
611
  }
package/src/vite/debug.ts CHANGED
@@ -35,6 +35,7 @@ export const NS = {
35
35
  build: "rango:build",
36
36
  dev: "rango:dev",
37
37
  transform: "rango:transform",
38
+ chunks: "rango:chunks",
38
39
  } as const;
39
40
 
40
41
  // Back-compat: the legacy INTERNAL_RANGO_DEBUG env var enabled per-site
@@ -9,6 +9,7 @@ import { resolve } from "node:path";
9
9
  import { readFileSync, writeFileSync, existsSync } from "node:fs";
10
10
  import { evictHandlerCode } from "../utils/bundle-analysis.js";
11
11
  import { copyStagedBuildAssets } from "../utils/prerender-utils.js";
12
+ import { jsonParseExpression } from "../utils/manifest-utils.js";
12
13
  import type { DiscoveryState } from "./state.js";
13
14
 
14
15
  /**
@@ -71,12 +72,12 @@ export function postprocessBundle(state: DiscoveryState): void {
71
72
  writeFileSync(chunkPath, result.code);
72
73
  const savedKB = (result.savedBytes / 1024).toFixed(1);
73
74
  console.log(
74
- `[rsc-router] Evicted ${target.label} (${savedKB} KB saved): ${info.fileName}`,
75
+ `[rango] Evicted ${target.label} (${savedKB} KB saved): ${info.fileName}`,
75
76
  );
76
77
  }
77
78
  } catch (replaceErr: any) {
78
79
  console.warn(
79
- `[rsc-router] Failed to evict ${target.label}: ${replaceErr.message}`,
80
+ `[rango] Failed to evict ${target.label}: ${replaceErr.message}`,
80
81
  );
81
82
  }
82
83
  }
@@ -104,7 +105,7 @@ export function postprocessBundle(state: DiscoveryState): void {
104
105
  }
105
106
 
106
107
  const manifestCode = [
107
- `const m=JSON.parse('${JSON.stringify(manifestMap).replace(/'/g, "\\'")}');`,
108
+ `const m=${jsonParseExpression(manifestMap)};`,
108
109
  `export function loadPrerenderAsset(s){return import(s)}`,
109
110
  `export default m;`,
110
111
  "",
@@ -121,11 +122,11 @@ export function postprocessBundle(state: DiscoveryState): void {
121
122
 
122
123
  const totalKB = (totalBytes / 1024).toFixed(1);
123
124
  console.log(
124
- `[rsc-router] Wrote prerender assets (${totalKB} KB total, ${Object.keys(state.prerenderManifestEntries!).length} entries)`,
125
+ `[rango] Wrote prerender assets (${totalKB} KB total, ${Object.keys(state.prerenderManifestEntries!).length} entries)`,
125
126
  );
126
127
  } catch (err: any) {
127
128
  throw new Error(
128
- `[rsc-router] Failed to write prerender assets: ${err.message}`,
129
+ `[rango] Failed to write prerender assets: ${err.message}`,
129
130
  );
130
131
  }
131
132
  }
@@ -169,11 +170,11 @@ export function postprocessBundle(state: DiscoveryState): void {
169
170
 
170
171
  const totalKB = (totalBytes / 1024).toFixed(1);
171
172
  console.log(
172
- `[rsc-router] Wrote static assets (${totalKB} KB total, ${Object.keys(state.staticManifestEntries!).length} entries)`,
173
+ `[rango] Wrote static assets (${totalKB} KB total, ${Object.keys(state.staticManifestEntries!).length} entries)`,
173
174
  );
174
175
  } catch (err: any) {
175
176
  throw new Error(
176
- `[rsc-router] Failed to write static assets: ${err.message}`,
177
+ `[rango] Failed to write static assets: ${err.message}`,
177
178
  );
178
179
  }
179
180
  }