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

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 (312) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +942 -4
  3. package/dist/bin/rango.js +1689 -0
  4. package/dist/vite/index.js +4960 -935
  5. package/package.json +70 -60
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +294 -0
  8. package/skills/caching/SKILL.md +93 -23
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +12 -8
  11. package/skills/document-cache/SKILL.md +18 -16
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/handler-use/SKILL.md +362 -0
  14. package/skills/hooks/SKILL.md +334 -72
  15. package/skills/host-router/SKILL.md +218 -0
  16. package/skills/intercept/SKILL.md +151 -8
  17. package/skills/layout/SKILL.md +122 -3
  18. package/skills/links/SKILL.md +92 -31
  19. package/skills/loader/SKILL.md +404 -44
  20. package/skills/middleware/SKILL.md +205 -37
  21. package/skills/migrate-nextjs/SKILL.md +560 -0
  22. package/skills/migrate-react-router/SKILL.md +764 -0
  23. package/skills/mime-routes/SKILL.md +128 -0
  24. package/skills/parallel/SKILL.md +263 -1
  25. package/skills/prerender/SKILL.md +685 -0
  26. package/skills/rango/SKILL.md +87 -16
  27. package/skills/response-routes/SKILL.md +411 -0
  28. package/skills/route/SKILL.md +281 -14
  29. package/skills/router-setup/SKILL.md +210 -32
  30. package/skills/tailwind/SKILL.md +129 -0
  31. package/skills/theme/SKILL.md +9 -8
  32. package/skills/typesafety/SKILL.md +328 -89
  33. package/skills/use-cache/SKILL.md +324 -0
  34. package/src/__internal.ts +102 -4
  35. package/src/bin/rango.ts +321 -0
  36. package/src/browser/action-coordinator.ts +97 -0
  37. package/src/browser/action-response-classifier.ts +99 -0
  38. package/src/browser/app-version.ts +14 -0
  39. package/src/browser/event-controller.ts +92 -64
  40. package/src/browser/history-state.ts +80 -0
  41. package/src/browser/intercept-utils.ts +52 -0
  42. package/src/browser/link-interceptor.ts +24 -4
  43. package/src/browser/logging.ts +55 -0
  44. package/src/browser/merge-segment-loaders.ts +20 -12
  45. package/src/browser/navigation-bridge.ts +317 -560
  46. package/src/browser/navigation-client.ts +206 -68
  47. package/src/browser/navigation-store.ts +73 -55
  48. package/src/browser/navigation-transaction.ts +297 -0
  49. package/src/browser/network-error-handler.ts +61 -0
  50. package/src/browser/partial-update.ts +343 -316
  51. package/src/browser/prefetch/cache.ts +216 -0
  52. package/src/browser/prefetch/fetch.ts +206 -0
  53. package/src/browser/prefetch/observer.ts +65 -0
  54. package/src/browser/prefetch/policy.ts +48 -0
  55. package/src/browser/prefetch/queue.ts +160 -0
  56. package/src/browser/prefetch/resource-ready.ts +77 -0
  57. package/src/browser/rango-state.ts +112 -0
  58. package/src/browser/react/Link.tsx +253 -74
  59. package/src/browser/react/NavigationProvider.tsx +87 -11
  60. package/src/browser/react/context.ts +11 -0
  61. package/src/browser/react/filter-segment-order.ts +11 -0
  62. package/src/browser/react/index.ts +12 -12
  63. package/src/browser/react/location-state-shared.ts +95 -53
  64. package/src/browser/react/location-state.ts +60 -15
  65. package/src/browser/react/mount-context.ts +6 -1
  66. package/src/browser/react/nonce-context.ts +23 -0
  67. package/src/browser/react/shallow-equal.ts +27 -0
  68. package/src/browser/react/use-action.ts +29 -51
  69. package/src/browser/react/use-client-cache.ts +5 -3
  70. package/src/browser/react/use-handle.ts +30 -126
  71. package/src/browser/react/use-href.tsx +2 -2
  72. package/src/browser/react/use-link-status.ts +6 -5
  73. package/src/browser/react/use-navigation.ts +44 -65
  74. package/src/browser/react/use-params.ts +65 -0
  75. package/src/browser/react/use-pathname.ts +47 -0
  76. package/src/browser/react/use-router.ts +76 -0
  77. package/src/browser/react/use-search-params.ts +56 -0
  78. package/src/browser/react/use-segments.ts +80 -97
  79. package/src/browser/response-adapter.ts +73 -0
  80. package/src/browser/rsc-router.tsx +214 -58
  81. package/src/browser/scroll-restoration.ts +127 -52
  82. package/src/browser/segment-reconciler.ts +243 -0
  83. package/src/browser/segment-structure-assert.ts +16 -0
  84. package/src/browser/server-action-bridge.ts +510 -603
  85. package/src/browser/shallow.ts +6 -1
  86. package/src/browser/types.ts +141 -48
  87. package/src/browser/validate-redirect-origin.ts +29 -0
  88. package/src/build/generate-manifest.ts +235 -24
  89. package/src/build/generate-route-types.ts +39 -0
  90. package/src/build/index.ts +13 -0
  91. package/src/build/route-trie.ts +291 -0
  92. package/src/build/route-types/ast-helpers.ts +25 -0
  93. package/src/build/route-types/ast-route-extraction.ts +98 -0
  94. package/src/build/route-types/codegen.ts +102 -0
  95. package/src/build/route-types/include-resolution.ts +418 -0
  96. package/src/build/route-types/param-extraction.ts +48 -0
  97. package/src/build/route-types/per-module-writer.ts +128 -0
  98. package/src/build/route-types/router-processing.ts +618 -0
  99. package/src/build/route-types/scan-filter.ts +85 -0
  100. package/src/build/runtime-discovery.ts +231 -0
  101. package/src/cache/background-task.ts +34 -0
  102. package/src/cache/cache-key-utils.ts +44 -0
  103. package/src/cache/cache-policy.ts +125 -0
  104. package/src/cache/cache-runtime.ts +342 -0
  105. package/src/cache/cache-scope.ts +167 -309
  106. package/src/cache/cf/cf-cache-store.ts +571 -17
  107. package/src/cache/cf/index.ts +13 -3
  108. package/src/cache/document-cache.ts +116 -77
  109. package/src/cache/handle-capture.ts +81 -0
  110. package/src/cache/handle-snapshot.ts +41 -0
  111. package/src/cache/index.ts +1 -15
  112. package/src/cache/memory-segment-store.ts +191 -13
  113. package/src/cache/profile-registry.ts +73 -0
  114. package/src/cache/read-through-swr.ts +134 -0
  115. package/src/cache/segment-codec.ts +256 -0
  116. package/src/cache/taint.ts +153 -0
  117. package/src/cache/types.ts +72 -122
  118. package/src/client.rsc.tsx +3 -1
  119. package/src/client.tsx +135 -301
  120. package/src/component-utils.ts +4 -4
  121. package/src/components/DefaultDocument.tsx +5 -1
  122. package/src/context-var.ts +156 -0
  123. package/src/debug.ts +19 -9
  124. package/src/errors.ts +108 -2
  125. package/src/handle.ts +55 -29
  126. package/src/handles/MetaTags.tsx +73 -20
  127. package/src/handles/breadcrumbs.ts +66 -0
  128. package/src/handles/index.ts +1 -0
  129. package/src/handles/meta.ts +30 -13
  130. package/src/host/cookie-handler.ts +21 -15
  131. package/src/host/errors.ts +8 -8
  132. package/src/host/index.ts +4 -7
  133. package/src/host/pattern-matcher.ts +27 -27
  134. package/src/host/router.ts +61 -39
  135. package/src/host/testing.ts +8 -8
  136. package/src/host/types.ts +15 -7
  137. package/src/host/utils.ts +1 -1
  138. package/src/href-client.ts +119 -29
  139. package/src/index.rsc.ts +155 -19
  140. package/src/index.ts +251 -30
  141. package/src/internal-debug.ts +11 -0
  142. package/src/loader.rsc.ts +26 -157
  143. package/src/loader.ts +27 -10
  144. package/src/network-error-thrower.tsx +3 -1
  145. package/src/outlet-provider.tsx +45 -0
  146. package/src/prerender/param-hash.ts +37 -0
  147. package/src/prerender/store.ts +186 -0
  148. package/src/prerender.ts +524 -0
  149. package/src/reverse.ts +354 -0
  150. package/src/root-error-boundary.tsx +41 -29
  151. package/src/route-content-wrapper.tsx +7 -4
  152. package/src/route-definition/dsl-helpers.ts +1121 -0
  153. package/src/route-definition/helper-factories.ts +200 -0
  154. package/src/route-definition/helpers-types.ts +478 -0
  155. package/src/route-definition/index.ts +55 -0
  156. package/src/route-definition/redirect.ts +101 -0
  157. package/src/route-definition/resolve-handler-use.ts +149 -0
  158. package/src/route-definition.ts +1 -1428
  159. package/src/route-map-builder.ts +217 -123
  160. package/src/route-name.ts +53 -0
  161. package/src/route-types.ts +77 -8
  162. package/src/router/content-negotiation.ts +215 -0
  163. package/src/router/debug-manifest.ts +72 -0
  164. package/src/router/error-handling.ts +9 -9
  165. package/src/router/find-match.ts +160 -0
  166. package/src/router/handler-context.ts +438 -86
  167. package/src/router/intercept-resolution.ts +402 -0
  168. package/src/router/lazy-includes.ts +237 -0
  169. package/src/router/loader-resolution.ts +356 -128
  170. package/src/router/logging.ts +251 -0
  171. package/src/router/manifest.ts +163 -35
  172. package/src/router/match-api.ts +555 -0
  173. package/src/router/match-context.ts +5 -3
  174. package/src/router/match-handlers.ts +440 -0
  175. package/src/router/match-middleware/background-revalidation.ts +108 -93
  176. package/src/router/match-middleware/cache-lookup.ts +460 -10
  177. package/src/router/match-middleware/cache-store.ts +98 -26
  178. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  179. package/src/router/match-middleware/segment-resolution.ts +80 -6
  180. package/src/router/match-pipelines.ts +10 -45
  181. package/src/router/match-result.ts +135 -35
  182. package/src/router/metrics.ts +240 -15
  183. package/src/router/middleware-cookies.ts +55 -0
  184. package/src/router/middleware-types.ts +220 -0
  185. package/src/router/middleware.ts +324 -369
  186. package/src/router/navigation-snapshot.ts +182 -0
  187. package/src/router/pattern-matching.ts +211 -43
  188. package/src/router/prerender-match.ts +502 -0
  189. package/src/router/preview-match.ts +98 -0
  190. package/src/router/request-classification.ts +310 -0
  191. package/src/router/revalidation.ts +137 -38
  192. package/src/router/route-snapshot.ts +245 -0
  193. package/src/router/router-context.ts +41 -21
  194. package/src/router/router-interfaces.ts +484 -0
  195. package/src/router/router-options.ts +618 -0
  196. package/src/router/router-registry.ts +24 -0
  197. package/src/router/segment-resolution/fresh.ts +748 -0
  198. package/src/router/segment-resolution/helpers.ts +268 -0
  199. package/src/router/segment-resolution/loader-cache.ts +199 -0
  200. package/src/router/segment-resolution/revalidation.ts +1379 -0
  201. package/src/router/segment-resolution/static-store.ts +67 -0
  202. package/src/router/segment-resolution.ts +21 -0
  203. package/src/router/segment-wrappers.ts +291 -0
  204. package/src/router/telemetry-otel.ts +299 -0
  205. package/src/router/telemetry.ts +300 -0
  206. package/src/router/timeout.ts +148 -0
  207. package/src/router/trie-matching.ts +239 -0
  208. package/src/router/types.ts +78 -3
  209. package/src/router.ts +740 -4252
  210. package/src/rsc/handler-context.ts +45 -0
  211. package/src/rsc/handler.ts +907 -797
  212. package/src/rsc/helpers.ts +140 -6
  213. package/src/rsc/index.ts +0 -20
  214. package/src/rsc/loader-fetch.ts +229 -0
  215. package/src/rsc/manifest-init.ts +90 -0
  216. package/src/rsc/nonce.ts +14 -0
  217. package/src/rsc/origin-guard.ts +141 -0
  218. package/src/rsc/progressive-enhancement.ts +391 -0
  219. package/src/rsc/response-error.ts +37 -0
  220. package/src/rsc/response-route-handler.ts +347 -0
  221. package/src/rsc/rsc-rendering.ts +246 -0
  222. package/src/rsc/runtime-warnings.ts +42 -0
  223. package/src/rsc/server-action.ts +356 -0
  224. package/src/rsc/ssr-setup.ts +128 -0
  225. package/src/rsc/types.ts +46 -11
  226. package/src/search-params.ts +230 -0
  227. package/src/segment-content-promise.ts +67 -0
  228. package/src/segment-loader-promise.ts +122 -0
  229. package/src/segment-system.tsx +134 -36
  230. package/src/server/context.ts +341 -61
  231. package/src/server/cookie-store.ts +190 -0
  232. package/src/server/fetchable-loader-store.ts +37 -0
  233. package/src/server/handle-store.ts +113 -15
  234. package/src/server/loader-registry.ts +24 -64
  235. package/src/server/request-context.ts +607 -81
  236. package/src/server.ts +35 -130
  237. package/src/ssr/index.tsx +103 -30
  238. package/src/static-handler.ts +126 -0
  239. package/src/theme/ThemeProvider.tsx +21 -15
  240. package/src/theme/ThemeScript.tsx +5 -5
  241. package/src/theme/constants.ts +5 -2
  242. package/src/theme/index.ts +4 -14
  243. package/src/theme/theme-context.ts +4 -30
  244. package/src/theme/theme-script.ts +21 -18
  245. package/src/types/boundaries.ts +158 -0
  246. package/src/types/cache-types.ts +198 -0
  247. package/src/types/error-types.ts +192 -0
  248. package/src/types/global-namespace.ts +100 -0
  249. package/src/types/handler-context.ts +791 -0
  250. package/src/types/index.ts +88 -0
  251. package/src/types/loader-types.ts +210 -0
  252. package/src/types/route-config.ts +170 -0
  253. package/src/types/route-entry.ts +120 -0
  254. package/src/types/segments.ts +150 -0
  255. package/src/types.ts +1 -1623
  256. package/src/urls/include-helper.ts +207 -0
  257. package/src/urls/index.ts +53 -0
  258. package/src/urls/path-helper-types.ts +372 -0
  259. package/src/urls/path-helper.ts +364 -0
  260. package/src/urls/pattern-types.ts +107 -0
  261. package/src/urls/response-types.ts +116 -0
  262. package/src/urls/type-extraction.ts +372 -0
  263. package/src/urls/urls-function.ts +98 -0
  264. package/src/urls.ts +1 -802
  265. package/src/use-loader.tsx +161 -81
  266. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  267. package/src/vite/discovery/discover-routers.ts +348 -0
  268. package/src/vite/discovery/prerender-collection.ts +439 -0
  269. package/src/vite/discovery/route-types-writer.ts +258 -0
  270. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  271. package/src/vite/discovery/state.ts +117 -0
  272. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  273. package/src/vite/index.ts +15 -1133
  274. package/src/vite/plugin-types.ts +103 -0
  275. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  276. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  277. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  278. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  279. package/src/vite/plugins/expose-id-utils.ts +299 -0
  280. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  281. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  282. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  283. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  284. package/src/vite/plugins/expose-ids/types.ts +45 -0
  285. package/src/vite/plugins/expose-internal-ids.ts +786 -0
  286. package/src/vite/plugins/performance-tracks.ts +88 -0
  287. package/src/vite/plugins/refresh-cmd.ts +127 -0
  288. package/src/vite/plugins/use-cache-transform.ts +323 -0
  289. package/src/vite/plugins/version-injector.ts +83 -0
  290. package/src/vite/plugins/version-plugin.ts +266 -0
  291. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  292. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  293. package/src/vite/rango.ts +462 -0
  294. package/src/vite/router-discovery.ts +918 -0
  295. package/src/vite/utils/ast-handler-extract.ts +517 -0
  296. package/src/vite/utils/banner.ts +36 -0
  297. package/src/vite/utils/bundle-analysis.ts +137 -0
  298. package/src/vite/utils/manifest-utils.ts +70 -0
  299. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  300. package/src/vite/utils/prerender-utils.ts +221 -0
  301. package/src/vite/utils/shared-utils.ts +170 -0
  302. package/CLAUDE.md +0 -43
  303. package/src/browser/lru-cache.ts +0 -69
  304. package/src/browser/request-controller.ts +0 -164
  305. package/src/cache/memory-store.ts +0 -253
  306. package/src/href-context.ts +0 -33
  307. package/src/href.ts +0 -255
  308. package/src/server/route-manifest-cache.ts +0 -173
  309. package/src/vite/expose-handle-id.ts +0 -209
  310. package/src/vite/expose-loader-id.ts +0 -426
  311. package/src/vite/expose-location-state-id.ts +0 -177
  312. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -1,9 +1,71 @@
1
1
  "use client";
2
2
 
3
- import { useCallback, useContext, useEffect, useRef, useState } from "react";
3
+ import {
4
+ isValidElement,
5
+ startTransition,
6
+ useCallback,
7
+ useContext,
8
+ useEffect,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ type ReactNode,
13
+ } from "react";
4
14
  import { OutletContext, type OutletContextValue } from "./outlet-context.js";
5
15
  import type { LoaderDefinition, LoadOptions } from "./types.js";
6
16
 
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
+ */
27
+ const NOT_FOUND = Symbol("not-found");
28
+
29
+ function extractContentLoaderData(
30
+ node: ReactNode,
31
+ loaderId: string,
32
+ ): unknown | typeof NOT_FOUND {
33
+ if (!isValidElement(node)) return NOT_FOUND;
34
+ const props = node.props as Record<string, any> | undefined;
35
+ if (!props) return NOT_FOUND;
36
+
37
+ // Direct OutletProvider with loaderData
38
+ if (props.loaderData && loaderId in props.loaderData) {
39
+ return props.loaderData[loaderId];
40
+ }
41
+
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
+ if (
47
+ props.loaderIds &&
48
+ Array.isArray(props.loaderIds) &&
49
+ props.loaderDataPromise &&
50
+ !(props.loaderDataPromise instanceof Promise)
51
+ ) {
52
+ const idx = (props.loaderIds as string[]).indexOf(loaderId);
53
+ if (idx !== -1) {
54
+ const data = (props.loaderDataPromise as any[])[idx];
55
+ // loaderDataPromise entries may be { ok, data } result objects
56
+ if (data && typeof data === "object" && "ok" in data) {
57
+ return data.ok ? data.data : NOT_FOUND;
58
+ }
59
+ return data;
60
+ }
61
+ }
62
+
63
+ // Traverse into wrapper elements (MountContextProvider, ViewTransition,
64
+ // Suspense wrappers, etc.)
65
+ if (props.children) return extractContentLoaderData(props.children, loaderId);
66
+ return NOT_FOUND;
67
+ }
68
+
7
69
  /**
8
70
  * Payload returned by loader RSC requests
9
71
  */
@@ -13,12 +75,9 @@ interface LoaderRscPayload<T = unknown> {
13
75
  }
14
76
 
15
77
  /**
16
- * Load function type with form action support
78
+ * Load function type for fetching loader data from the client
17
79
  */
18
- export type LoadFunction<T> = ((options?: LoadOptions) => Promise<T>) & {
19
- /** Form action for progressive enhancement - can be passed to form action prop */
20
- action: (formData: FormData) => Promise<void>;
21
- };
80
+ export type LoadFunction<T> = (options?: LoadOptions) => Promise<T>;
22
81
 
23
82
  /**
24
83
  * Result type for useLoader hook (strict - data is required)
@@ -69,28 +128,37 @@ export interface UseLoaderOptions {
69
128
  */
70
129
  function useLoaderInternal<T>(
71
130
  loader: LoaderDefinition<T>,
72
- options?: UseLoaderOptions
131
+ options?: UseLoaderOptions,
73
132
  ): UseFetchLoaderResult<T> {
74
133
  const context = useContext(OutletContext);
75
134
 
76
135
  // Get data from context (SSR/navigation)
77
- const getContextData = useCallback((): T | undefined => {
136
+ const contextData = useMemo((): T | undefined => {
78
137
  let current: OutletContextValue | null | undefined = context;
79
138
  while (current) {
80
139
  if (current.loaderData && loader.$$id in current.loaderData) {
81
140
  return current.loaderData[loader.$$id] as T;
82
141
  }
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
+ const contentData = extractContentLoaderData(
146
+ current.content,
147
+ loader.$$id,
148
+ );
149
+ if (contentData !== NOT_FOUND) {
150
+ return contentData as T;
151
+ }
83
152
  current = current.parent;
84
153
  }
85
154
  return undefined;
86
155
  }, [context, loader.$$id]);
87
156
 
88
- const contextData = getContextData();
89
-
90
157
  // Local state for fetched data (from load() calls)
91
158
  const [fetchedData, setFetchedData] = useState<T | undefined>(undefined);
92
159
  const [isLoading, setIsLoading] = useState(false);
93
160
  const [error, setError] = useState<Error | null>(null);
161
+ const requestIdRef = useRef(0);
94
162
 
95
163
  // Track context data changes to reset fetched data on navigation
96
164
  const prevContextDataRef = useRef(contextData);
@@ -108,13 +176,25 @@ function useLoaderInternal<T>(
108
176
 
109
177
  const throwOnError = options?.throwOnError ?? true;
110
178
 
111
- // Load function for fetching data
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;
185
+ const dataRef = useRef(data);
186
+ dataRef.current = data;
187
+
188
+ // Load function for fetching data via the ?_rsc_loader endpoint.
189
+ // Supports GET (data fetching) and POST/PUT/PATCH/DELETE (mutations).
112
190
  const load = useCallback(
113
191
  async (loadOptions?: LoadOptions): Promise<T> => {
192
+ const requestId = ++requestIdRef.current;
193
+ const loaderId = loaderIdRef.current;
114
194
  // Verify the loader has $$id
115
- if (!loader.$$id) {
195
+ if (!loaderId) {
116
196
  throw new Error(
117
- `Loader is missing $$id. Make sure the exposeLoaderId Vite plugin is enabled.`
197
+ `Loader is missing $$id. Make sure the exposeLoaderId Vite plugin is enabled.`,
118
198
  );
119
199
  }
120
200
 
@@ -122,8 +202,8 @@ function useLoaderInternal<T>(
122
202
  setError(null);
123
203
 
124
204
  try {
125
- const url = new URL(window.location.pathname, window.location.origin);
126
- url.searchParams.set("_rsc_loader", loader.$$id);
205
+ const url = new URL(window.location.href);
206
+ url.searchParams.set("_rsc_loader", loaderId);
127
207
 
128
208
  const method = loadOptions?.method ?? "GET";
129
209
  const isBodyMethod = method !== "GET";
@@ -131,29 +211,58 @@ function useLoaderInternal<T>(
131
211
  let fetchOptions: RequestInit;
132
212
 
133
213
  if (isBodyMethod) {
134
- // POST/PUT/PATCH/DELETE - send params and body as JSON
135
- const bodyPayload: { params?: Record<string, string>; body?: unknown } = {};
136
- if (loadOptions?.params && Object.keys(loadOptions.params).length > 0) {
137
- bodyPayload.params = loadOptions.params;
214
+ const bodyValue =
215
+ "body" in (loadOptions ?? {})
216
+ ? (loadOptions as any).body
217
+ : undefined;
218
+ const hasParams =
219
+ loadOptions?.params && Object.keys(loadOptions.params).length > 0;
220
+
221
+ 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
+ if (hasParams) {
225
+ bodyValue.set(
226
+ "_rsc_loader_params",
227
+ JSON.stringify(loadOptions!.params),
228
+ );
229
+ }
230
+ fetchOptions = {
231
+ method,
232
+ headers: { Accept: "text/x-component" },
233
+ body: bodyValue,
234
+ };
235
+ } else {
236
+ // JSON body — send params and body as JSON
237
+ const bodyPayload: {
238
+ params?: Record<string, string>;
239
+ body?: unknown;
240
+ } = {};
241
+ if (hasParams) {
242
+ bodyPayload.params = loadOptions!.params;
243
+ }
244
+ if (bodyValue !== undefined) {
245
+ bodyPayload.body = bodyValue;
246
+ }
247
+
248
+ fetchOptions = {
249
+ method,
250
+ headers: {
251
+ Accept: "text/x-component",
252
+ "Content-Type": "application/json",
253
+ },
254
+ body: JSON.stringify(bodyPayload),
255
+ };
138
256
  }
139
- if ("body" in (loadOptions ?? {}) && (loadOptions as any).body !== undefined) {
140
- bodyPayload.body = (loadOptions as any).body;
141
- }
142
-
143
- fetchOptions = {
144
- method,
145
- headers: {
146
- Accept: "text/x-component",
147
- "Content-Type": "application/json",
148
- },
149
- body: JSON.stringify(bodyPayload),
150
- };
151
257
  } else {
152
258
  // GET - send params in query string
153
- if (loadOptions?.params && Object.keys(loadOptions.params).length > 0) {
259
+ if (
260
+ loadOptions?.params &&
261
+ Object.keys(loadOptions.params).length > 0
262
+ ) {
154
263
  url.searchParams.set(
155
264
  "_rsc_loader_params",
156
- JSON.stringify(loadOptions.params)
265
+ JSON.stringify(loadOptions.params),
157
266
  );
158
267
  }
159
268
 
@@ -175,61 +284,32 @@ function useLoaderInternal<T>(
175
284
  }
176
285
 
177
286
  const result = payload.loaderResult;
178
- setFetchedData(result);
287
+ if (requestId === requestIdRef.current) {
288
+ startTransition(() => {
289
+ setFetchedData(result);
290
+ });
291
+ }
179
292
  return result;
180
293
  } catch (e) {
181
294
  const err = e instanceof Error ? e : new Error(String(e));
182
- setError(err);
183
- if (throwOnError) {
184
- throw err;
295
+ if (requestId === requestIdRef.current) {
296
+ setError(err);
185
297
  }
186
- // When throwOnError is false, return the current data (previous successful
187
- // value or undefined). Caller should check error state for error handling.
188
- return data as T;
189
- } finally {
190
- setIsLoading(false);
191
- }
192
- },
193
- [throwOnError]
194
- );
195
-
196
- // Form action for progressive enhancement
197
- // This wrapper is for useFetchLoader's load.action - it manages state internally
198
- // and doesn't use React's useActionState. For true PE, use loader.action directly
199
- // with useActionState.
200
- const action = useCallback(
201
- async (formData: FormData): Promise<void> => {
202
- if (!loader.action) {
203
- throw new Error(
204
- `Loader "${loader.$$id}" does not have an action. ` +
205
- `Make sure the loader is created with fetchable: true.`
206
- );
207
- }
208
-
209
- setIsLoading(true);
210
- setError(null);
211
-
212
- try {
213
- // Pass null as prevState - this wrapper manages state internally
214
- const result = await loader.action(null, formData);
215
- setFetchedData(result);
216
- } catch (e) {
217
- const err = e instanceof Error ? e : new Error(String(e));
218
- setError(err);
219
298
  if (throwOnError) {
220
299
  throw err;
221
300
  }
301
+ // When throwOnError is false, return the latest data snapshot (previous
302
+ // successful value or undefined). Caller should check error state.
303
+ return dataRef.current as T;
222
304
  } finally {
223
- setIsLoading(false);
305
+ if (requestId === requestIdRef.current) {
306
+ setIsLoading(false);
307
+ }
224
308
  }
225
309
  },
226
- [throwOnError]
310
+ [throwOnError],
227
311
  );
228
312
 
229
- // Attach action to load function
230
- const loadWithAction = load as LoadFunction<T>;
231
- loadWithAction.action = action;
232
-
233
313
  // Throw during render if there's an error and throwOnError is true
234
314
  // This allows ErrorBoundaries to catch async errors from load()
235
315
  if (error && throwOnError) {
@@ -240,8 +320,8 @@ function useLoaderInternal<T>(
240
320
  data,
241
321
  isLoading,
242
322
  error,
243
- load: loadWithAction,
244
- refetch: loadWithAction,
323
+ load,
324
+ refetch: load,
245
325
  };
246
326
  }
247
327
 
@@ -276,7 +356,7 @@ function useLoaderInternal<T>(
276
356
  */
277
357
  export function useLoader<T>(
278
358
  loader: LoaderDefinition<T>,
279
- options?: UseLoaderOptions
359
+ options?: UseLoaderOptions,
280
360
  ): UseLoaderResult<T> {
281
361
  const result = useLoaderInternal(loader, options);
282
362
 
@@ -285,7 +365,7 @@ export function useLoader<T>(
285
365
  throw new Error(
286
366
  `useLoader: Loader "${loader.$$id}" data not found in context. ` +
287
367
  `Make sure the loader is registered on the route with loader(). ` +
288
- `If you need on-demand fetching, use useFetchLoader() instead.`
368
+ `If you need on-demand fetching, use useFetchLoader() instead.`,
289
369
  );
290
370
  }
291
371
 
@@ -340,7 +420,7 @@ export function useLoader<T>(
340
420
  */
341
421
  export function useFetchLoader<T>(
342
422
  loader: LoaderDefinition<T>,
343
- options?: UseLoaderOptions
423
+ options?: UseLoaderOptions,
344
424
  ): UseFetchLoaderResult<T> {
345
425
  return useLoaderInternal(loader, options);
346
426
  }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Bundle Post-Processing
3
+ *
4
+ * Handles handler code eviction from prerender/static chunks and
5
+ * injection of collected prerender/static data into the RSC entry bundle.
6
+ */
7
+
8
+ import { resolve } from "node:path";
9
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
10
+ import { evictHandlerCode } from "../utils/bundle-analysis.js";
11
+ import { copyStagedBuildAssets } from "../utils/prerender-utils.js";
12
+ import type { DiscoveryState } from "./state.js";
13
+
14
+ /**
15
+ * Post-process the RSC bundle: evict handler code and inject
16
+ * prerender/static data as importable asset modules.
17
+ */
18
+ export function postprocessBundle(state: DiscoveryState): void {
19
+ const hasPrerenderData =
20
+ state.prerenderManifestEntries &&
21
+ Object.keys(state.prerenderManifestEntries).length > 0;
22
+ const hasStaticData =
23
+ state.staticManifestEntries &&
24
+ Object.keys(state.staticManifestEntries).length > 0;
25
+ if (!hasPrerenderData && !hasStaticData) return;
26
+
27
+ // Find RSC entry (recorded in generateBundle, fallback to dist/rsc/index.js)
28
+ const rscEntryPath = resolve(
29
+ state.projectRoot,
30
+ "dist/rsc",
31
+ state.rscEntryFileName ?? "index.js",
32
+ );
33
+
34
+ // 1. Evict handler code from whichever chunks contain handler exports.
35
+ // handlerChunkInfoMap/staticHandlerChunkInfoMap are populated by generateBundle
36
+ // after the production RSC build. In Vite 6 multi-environment builds, the
37
+ // RSC build runs twice (analysis + production). The maps are cleared at the
38
+ // start of each generateBundle pass so only production data is used here.
39
+ const evictionTargets: Array<{
40
+ infos: Iterable<import("./state.js").ChunkInfo>;
41
+ fnName: string;
42
+ brand: string;
43
+ label: string;
44
+ }> = [
45
+ {
46
+ infos: state.handlerChunkInfoMap.values(),
47
+ fnName: "Prerender",
48
+ brand: "prerenderHandler",
49
+ label: "handler code from RSC bundle",
50
+ },
51
+ {
52
+ infos: state.staticHandlerChunkInfoMap.values(),
53
+ fnName: "Static",
54
+ brand: "staticHandler",
55
+ label: "static handler code",
56
+ },
57
+ ];
58
+
59
+ for (const target of evictionTargets) {
60
+ for (const info of target.infos) {
61
+ const chunkPath = resolve(state.projectRoot, "dist/rsc", info.fileName);
62
+ try {
63
+ const code = readFileSync(chunkPath, "utf-8");
64
+ const result = evictHandlerCode(
65
+ code,
66
+ info.exports,
67
+ target.fnName,
68
+ target.brand,
69
+ );
70
+ if (result) {
71
+ writeFileSync(chunkPath, result.code);
72
+ const savedKB = (result.savedBytes / 1024).toFixed(1);
73
+ console.log(
74
+ `[rsc-router] Evicted ${target.label} (${savedKB} KB saved): ${info.fileName}`,
75
+ );
76
+ }
77
+ } catch (replaceErr: any) {
78
+ console.warn(
79
+ `[rsc-router] Failed to evict ${target.label}: ${replaceErr.message}`,
80
+ );
81
+ }
82
+ }
83
+ }
84
+ state.handlerChunkInfoMap.clear();
85
+ state.staticHandlerChunkInfoMap.clear();
86
+
87
+ // 2. Write prerender data as separate importable asset modules
88
+ // and inject a lazy manifest loader into the RSC entry.
89
+ if (hasPrerenderData && existsSync(rscEntryPath)) {
90
+ const rscCode = readFileSync(rscEntryPath, "utf-8");
91
+ // Check for the specific injection marker to avoid double-injection.
92
+ if (!rscCode.includes("__prerender-manifest.js")) {
93
+ try {
94
+ let totalBytes = copyStagedBuildAssets(
95
+ state.projectRoot,
96
+ Object.values(state.prerenderManifestEntries!),
97
+ );
98
+
99
+ const manifestMap: Record<string, string> = {};
100
+ for (const [key, assetFileName] of Object.entries(
101
+ state.prerenderManifestEntries!,
102
+ )) {
103
+ manifestMap[key] = `./assets/${assetFileName}`;
104
+ }
105
+
106
+ const manifestCode = [
107
+ `const m=JSON.parse('${JSON.stringify(manifestMap).replace(/'/g, "\\'")}');`,
108
+ `export function loadPrerenderAsset(s){return import(s)}`,
109
+ `export default m;`,
110
+ "",
111
+ ].join("\n");
112
+ const manifestPath = resolve(
113
+ state.projectRoot,
114
+ "dist/rsc/__prerender-manifest.js",
115
+ );
116
+ writeFileSync(manifestPath, manifestCode);
117
+ totalBytes += Buffer.byteLength(manifestCode);
118
+
119
+ const injection = `globalThis.__loadPrerenderManifestModule = () => import("./__prerender-manifest.js");\n`;
120
+ writeFileSync(rscEntryPath, injection + rscCode);
121
+
122
+ const totalKB = (totalBytes / 1024).toFixed(1);
123
+ console.log(
124
+ `[rsc-router] Wrote prerender assets (${totalKB} KB total, ${Object.keys(state.prerenderManifestEntries!).length} entries)`,
125
+ );
126
+ } catch (err: any) {
127
+ throw new Error(
128
+ `[rsc-router] Failed to write prerender assets: ${err.message}`,
129
+ );
130
+ }
131
+ }
132
+ }
133
+
134
+ // 3. Write static handler data as separate importable asset modules
135
+ // and inject a __STATIC_MANIFEST import into the RSC entry.
136
+ if (hasStaticData && existsSync(rscEntryPath)) {
137
+ const rscCode = readFileSync(rscEntryPath, "utf-8");
138
+ if (!rscCode.includes("__static-manifest.js")) {
139
+ try {
140
+ const manifestEntries: string[] = [];
141
+ let totalBytes = copyStagedBuildAssets(
142
+ state.projectRoot,
143
+ Object.values(state.staticManifestEntries!),
144
+ );
145
+
146
+ for (const [handlerId, assetFileName] of Object.entries(
147
+ state.staticManifestEntries!,
148
+ )) {
149
+ manifestEntries.push(
150
+ `${JSON.stringify(handlerId)}:()=>import("./assets/${assetFileName}")`,
151
+ );
152
+ }
153
+
154
+ // Set the global inside the manifest module so it is assigned
155
+ // during module evaluation (before dependent modules like
156
+ // segment-resolution.ts run their top-level initializers).
157
+ const manifestCode = `const m={${manifestEntries.join(",")}};globalThis.__STATIC_MANIFEST=m;export default m;\n`;
158
+ const manifestPath = resolve(
159
+ state.projectRoot,
160
+ "dist/rsc/__static-manifest.js",
161
+ );
162
+ writeFileSync(manifestPath, manifestCode);
163
+ totalBytes += Buffer.byteLength(manifestCode);
164
+
165
+ // The import ensures the manifest module is evaluated early.
166
+ // The global is already set inside the module itself.
167
+ const injection = `import "./__static-manifest.js";\n`;
168
+ writeFileSync(rscEntryPath, injection + rscCode);
169
+
170
+ const totalKB = (totalBytes / 1024).toFixed(1);
171
+ console.log(
172
+ `[rsc-router] Wrote static assets (${totalKB} KB total, ${Object.keys(state.staticManifestEntries!).length} entries)`,
173
+ );
174
+ } catch (err: any) {
175
+ throw new Error(
176
+ `[rsc-router] Failed to write static assets: ${err.message}`,
177
+ );
178
+ }
179
+ }
180
+ }
181
+ }