@rangojs/router 0.0.0-experimental.7 → 0.0.0-experimental.8a4d0430

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 (300) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +884 -4
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +4474 -863
  5. package/package.json +60 -51
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +50 -21
  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/hooks/SKILL.md +334 -72
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +131 -8
  16. package/skills/layout/SKILL.md +100 -3
  17. package/skills/links/SKILL.md +89 -30
  18. package/skills/loader/SKILL.md +388 -38
  19. package/skills/middleware/SKILL.md +171 -34
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +78 -1
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +85 -16
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +226 -14
  26. package/skills/router-setup/SKILL.md +123 -30
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +9 -8
  29. package/skills/typesafety/SKILL.md +318 -89
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +102 -4
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/event-controller.ts +87 -64
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/intercept-utils.ts +52 -0
  38. package/src/browser/link-interceptor.ts +24 -4
  39. package/src/browser/logging.ts +55 -0
  40. package/src/browser/merge-segment-loaders.ts +20 -12
  41. package/src/browser/navigation-bridge.ts +285 -553
  42. package/src/browser/navigation-client.ts +124 -71
  43. package/src/browser/navigation-store.ts +33 -50
  44. package/src/browser/navigation-transaction.ts +295 -0
  45. package/src/browser/network-error-handler.ts +61 -0
  46. package/src/browser/partial-update.ts +258 -308
  47. package/src/browser/prefetch/cache.ts +146 -0
  48. package/src/browser/prefetch/fetch.ts +135 -0
  49. package/src/browser/prefetch/observer.ts +65 -0
  50. package/src/browser/prefetch/policy.ts +42 -0
  51. package/src/browser/prefetch/queue.ts +88 -0
  52. package/src/browser/rango-state.ts +112 -0
  53. package/src/browser/react/Link.tsx +185 -73
  54. package/src/browser/react/NavigationProvider.tsx +51 -11
  55. package/src/browser/react/context.ts +6 -0
  56. package/src/browser/react/filter-segment-order.ts +11 -0
  57. package/src/browser/react/index.ts +12 -12
  58. package/src/browser/react/location-state-shared.ts +95 -53
  59. package/src/browser/react/location-state.ts +60 -15
  60. package/src/browser/react/mount-context.ts +6 -1
  61. package/src/browser/react/nonce-context.ts +23 -0
  62. package/src/browser/react/shallow-equal.ts +27 -0
  63. package/src/browser/react/use-action.ts +29 -51
  64. package/src/browser/react/use-client-cache.ts +5 -3
  65. package/src/browser/react/use-handle.ts +32 -79
  66. package/src/browser/react/use-href.tsx +2 -2
  67. package/src/browser/react/use-link-status.ts +6 -5
  68. package/src/browser/react/use-navigation.ts +22 -63
  69. package/src/browser/react/use-params.ts +65 -0
  70. package/src/browser/react/use-pathname.ts +47 -0
  71. package/src/browser/react/use-router.ts +63 -0
  72. package/src/browser/react/use-search-params.ts +56 -0
  73. package/src/browser/react/use-segments.ts +80 -97
  74. package/src/browser/response-adapter.ts +73 -0
  75. package/src/browser/rsc-router.tsx +107 -26
  76. package/src/browser/scroll-restoration.ts +92 -16
  77. package/src/browser/segment-reconciler.ts +216 -0
  78. package/src/browser/segment-structure-assert.ts +16 -0
  79. package/src/browser/server-action-bridge.ts +504 -599
  80. package/src/browser/shallow.ts +6 -1
  81. package/src/browser/types.ts +109 -47
  82. package/src/browser/validate-redirect-origin.ts +29 -0
  83. package/src/build/generate-manifest.ts +235 -24
  84. package/src/build/generate-route-types.ts +36 -0
  85. package/src/build/index.ts +13 -0
  86. package/src/build/route-trie.ts +265 -0
  87. package/src/build/route-types/ast-helpers.ts +25 -0
  88. package/src/build/route-types/ast-route-extraction.ts +98 -0
  89. package/src/build/route-types/codegen.ts +102 -0
  90. package/src/build/route-types/include-resolution.ts +411 -0
  91. package/src/build/route-types/param-extraction.ts +48 -0
  92. package/src/build/route-types/per-module-writer.ts +128 -0
  93. package/src/build/route-types/router-processing.ts +469 -0
  94. package/src/build/route-types/scan-filter.ts +78 -0
  95. package/src/build/runtime-discovery.ts +231 -0
  96. package/src/cache/background-task.ts +34 -0
  97. package/src/cache/cache-key-utils.ts +44 -0
  98. package/src/cache/cache-policy.ts +125 -0
  99. package/src/cache/cache-runtime.ts +338 -0
  100. package/src/cache/cache-scope.ts +120 -303
  101. package/src/cache/cf/cf-cache-store.ts +119 -7
  102. package/src/cache/cf/index.ts +8 -2
  103. package/src/cache/document-cache.ts +101 -72
  104. package/src/cache/handle-capture.ts +81 -0
  105. package/src/cache/handle-snapshot.ts +41 -0
  106. package/src/cache/index.ts +0 -15
  107. package/src/cache/memory-segment-store.ts +191 -13
  108. package/src/cache/profile-registry.ts +73 -0
  109. package/src/cache/read-through-swr.ts +134 -0
  110. package/src/cache/segment-codec.ts +256 -0
  111. package/src/cache/taint.ts +98 -0
  112. package/src/cache/types.ts +72 -122
  113. package/src/client.rsc.tsx +3 -1
  114. package/src/client.tsx +106 -126
  115. package/src/component-utils.ts +4 -4
  116. package/src/components/DefaultDocument.tsx +5 -1
  117. package/src/context-var.ts +86 -0
  118. package/src/debug.ts +17 -7
  119. package/src/errors.ts +108 -2
  120. package/src/handle.ts +15 -29
  121. package/src/handles/MetaTags.tsx +73 -20
  122. package/src/handles/breadcrumbs.ts +66 -0
  123. package/src/handles/index.ts +1 -0
  124. package/src/handles/meta.ts +30 -13
  125. package/src/host/cookie-handler.ts +21 -15
  126. package/src/host/errors.ts +8 -8
  127. package/src/host/index.ts +4 -7
  128. package/src/host/pattern-matcher.ts +27 -27
  129. package/src/host/router.ts +61 -39
  130. package/src/host/testing.ts +8 -8
  131. package/src/host/types.ts +15 -7
  132. package/src/host/utils.ts +1 -1
  133. package/src/href-client.ts +119 -29
  134. package/src/index.rsc.ts +153 -19
  135. package/src/index.ts +211 -30
  136. package/src/internal-debug.ts +11 -0
  137. package/src/loader.rsc.ts +26 -157
  138. package/src/loader.ts +27 -10
  139. package/src/network-error-thrower.tsx +3 -1
  140. package/src/outlet-provider.tsx +45 -0
  141. package/src/prerender/param-hash.ts +37 -0
  142. package/src/prerender/store.ts +185 -0
  143. package/src/prerender.ts +463 -0
  144. package/src/reverse.ts +330 -0
  145. package/src/root-error-boundary.tsx +41 -29
  146. package/src/route-content-wrapper.tsx +7 -4
  147. package/src/route-definition/dsl-helpers.ts +934 -0
  148. package/src/route-definition/helper-factories.ts +200 -0
  149. package/src/route-definition/helpers-types.ts +430 -0
  150. package/src/route-definition/index.ts +52 -0
  151. package/src/route-definition/redirect.ts +93 -0
  152. package/src/route-definition.ts +1 -1428
  153. package/src/route-map-builder.ts +211 -123
  154. package/src/route-name.ts +53 -0
  155. package/src/route-types.ts +59 -8
  156. package/src/router/content-negotiation.ts +116 -0
  157. package/src/router/debug-manifest.ts +72 -0
  158. package/src/router/error-handling.ts +9 -9
  159. package/src/router/find-match.ts +158 -0
  160. package/src/router/handler-context.ts +374 -81
  161. package/src/router/intercept-resolution.ts +395 -0
  162. package/src/router/lazy-includes.ts +234 -0
  163. package/src/router/loader-resolution.ts +215 -122
  164. package/src/router/logging.ts +248 -0
  165. package/src/router/manifest.ts +148 -35
  166. package/src/router/match-api.ts +620 -0
  167. package/src/router/match-context.ts +5 -3
  168. package/src/router/match-handlers.ts +440 -0
  169. package/src/router/match-middleware/background-revalidation.ts +80 -93
  170. package/src/router/match-middleware/cache-lookup.ts +382 -9
  171. package/src/router/match-middleware/cache-store.ts +51 -22
  172. package/src/router/match-middleware/intercept-resolution.ts +55 -17
  173. package/src/router/match-middleware/segment-resolution.ts +24 -6
  174. package/src/router/match-pipelines.ts +10 -45
  175. package/src/router/match-result.ts +34 -28
  176. package/src/router/metrics.ts +235 -15
  177. package/src/router/middleware-cookies.ts +55 -0
  178. package/src/router/middleware-types.ts +222 -0
  179. package/src/router/middleware.ts +324 -367
  180. package/src/router/pattern-matching.ts +211 -43
  181. package/src/router/prerender-match.ts +402 -0
  182. package/src/router/preview-match.ts +170 -0
  183. package/src/router/revalidation.ts +137 -38
  184. package/src/router/router-context.ts +36 -21
  185. package/src/router/router-interfaces.ts +452 -0
  186. package/src/router/router-options.ts +592 -0
  187. package/src/router/router-registry.ts +24 -0
  188. package/src/router/segment-resolution/fresh.ts +570 -0
  189. package/src/router/segment-resolution/helpers.ts +263 -0
  190. package/src/router/segment-resolution/loader-cache.ts +198 -0
  191. package/src/router/segment-resolution/revalidation.ts +1241 -0
  192. package/src/router/segment-resolution/static-store.ts +67 -0
  193. package/src/router/segment-resolution.ts +21 -0
  194. package/src/router/segment-wrappers.ts +289 -0
  195. package/src/router/telemetry-otel.ts +299 -0
  196. package/src/router/telemetry.ts +300 -0
  197. package/src/router/timeout.ts +148 -0
  198. package/src/router/trie-matching.ts +239 -0
  199. package/src/router/types.ts +77 -3
  200. package/src/router.ts +692 -4257
  201. package/src/rsc/handler-context.ts +45 -0
  202. package/src/rsc/handler.ts +764 -754
  203. package/src/rsc/helpers.ts +140 -6
  204. package/src/rsc/index.ts +0 -20
  205. package/src/rsc/loader-fetch.ts +209 -0
  206. package/src/rsc/manifest-init.ts +86 -0
  207. package/src/rsc/nonce.ts +14 -0
  208. package/src/rsc/origin-guard.ts +141 -0
  209. package/src/rsc/progressive-enhancement.ts +379 -0
  210. package/src/rsc/response-error.ts +37 -0
  211. package/src/rsc/response-route-handler.ts +347 -0
  212. package/src/rsc/rsc-rendering.ts +235 -0
  213. package/src/rsc/runtime-warnings.ts +42 -0
  214. package/src/rsc/server-action.ts +348 -0
  215. package/src/rsc/ssr-setup.ts +128 -0
  216. package/src/rsc/types.ts +38 -11
  217. package/src/search-params.ts +230 -0
  218. package/src/segment-system.tsx +25 -13
  219. package/src/server/context.ts +182 -51
  220. package/src/server/cookie-store.ts +190 -0
  221. package/src/server/fetchable-loader-store.ts +37 -0
  222. package/src/server/handle-store.ts +94 -15
  223. package/src/server/loader-registry.ts +15 -56
  224. package/src/server/request-context.ts +430 -70
  225. package/src/server.ts +35 -130
  226. package/src/ssr/index.tsx +100 -31
  227. package/src/static-handler.ts +114 -0
  228. package/src/theme/ThemeProvider.tsx +21 -15
  229. package/src/theme/ThemeScript.tsx +5 -5
  230. package/src/theme/constants.ts +5 -2
  231. package/src/theme/index.ts +4 -14
  232. package/src/theme/theme-context.ts +4 -30
  233. package/src/theme/theme-script.ts +21 -18
  234. package/src/types/boundaries.ts +158 -0
  235. package/src/types/cache-types.ts +198 -0
  236. package/src/types/error-types.ts +192 -0
  237. package/src/types/global-namespace.ts +100 -0
  238. package/src/types/handler-context.ts +687 -0
  239. package/src/types/index.ts +88 -0
  240. package/src/types/loader-types.ts +183 -0
  241. package/src/types/route-config.ts +170 -0
  242. package/src/types/route-entry.ts +102 -0
  243. package/src/types/segments.ts +148 -0
  244. package/src/types.ts +1 -1623
  245. package/src/urls/include-helper.ts +197 -0
  246. package/src/urls/index.ts +53 -0
  247. package/src/urls/path-helper-types.ts +339 -0
  248. package/src/urls/path-helper.ts +329 -0
  249. package/src/urls/pattern-types.ts +95 -0
  250. package/src/urls/response-types.ts +106 -0
  251. package/src/urls/type-extraction.ts +372 -0
  252. package/src/urls/urls-function.ts +98 -0
  253. package/src/urls.ts +1 -802
  254. package/src/use-loader.tsx +85 -77
  255. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  256. package/src/vite/discovery/discover-routers.ts +344 -0
  257. package/src/vite/discovery/prerender-collection.ts +385 -0
  258. package/src/vite/discovery/route-types-writer.ts +258 -0
  259. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  260. package/src/vite/discovery/state.ts +110 -0
  261. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  262. package/src/vite/index.ts +11 -1129
  263. package/src/vite/plugin-types.ts +131 -0
  264. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  265. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  266. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  267. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
  268. package/src/vite/plugins/expose-id-utils.ts +287 -0
  269. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  270. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  271. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  272. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  273. package/src/vite/plugins/expose-ids/types.ts +45 -0
  274. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  275. package/src/vite/plugins/refresh-cmd.ts +65 -0
  276. package/src/vite/plugins/use-cache-transform.ts +323 -0
  277. package/src/vite/plugins/version-injector.ts +83 -0
  278. package/src/vite/plugins/version-plugin.ts +254 -0
  279. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  280. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  281. package/src/vite/rango.ts +510 -0
  282. package/src/vite/router-discovery.ts +785 -0
  283. package/src/vite/utils/ast-handler-extract.ts +517 -0
  284. package/src/vite/utils/banner.ts +36 -0
  285. package/src/vite/utils/bundle-analysis.ts +137 -0
  286. package/src/vite/utils/manifest-utils.ts +70 -0
  287. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  288. package/src/vite/utils/prerender-utils.ts +189 -0
  289. package/src/vite/utils/shared-utils.ts +169 -0
  290. package/CLAUDE.md +0 -43
  291. package/src/browser/lru-cache.ts +0 -69
  292. package/src/browser/request-controller.ts +0 -164
  293. package/src/cache/memory-store.ts +0 -253
  294. package/src/href-context.ts +0 -33
  295. package/src/href.ts +0 -255
  296. package/src/server/route-manifest-cache.ts +0 -173
  297. package/src/vite/expose-handle-id.ts +0 -209
  298. package/src/vite/expose-loader-id.ts +0 -426
  299. package/src/vite/expose-location-state-id.ts +0 -177
  300. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -13,12 +13,9 @@ interface LoaderRscPayload<T = unknown> {
13
13
  }
14
14
 
15
15
  /**
16
- * Load function type with form action support
16
+ * Load function type for fetching loader data from the client
17
17
  */
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
- };
18
+ export type LoadFunction<T> = (options?: LoadOptions) => Promise<T>;
22
19
 
23
20
  /**
24
21
  * Result type for useLoader hook (strict - data is required)
@@ -69,7 +66,7 @@ export interface UseLoaderOptions {
69
66
  */
70
67
  function useLoaderInternal<T>(
71
68
  loader: LoaderDefinition<T>,
72
- options?: UseLoaderOptions
69
+ options?: UseLoaderOptions,
73
70
  ): UseFetchLoaderResult<T> {
74
71
  const context = useContext(OutletContext);
75
72
 
@@ -91,6 +88,7 @@ function useLoaderInternal<T>(
91
88
  const [fetchedData, setFetchedData] = useState<T | undefined>(undefined);
92
89
  const [isLoading, setIsLoading] = useState(false);
93
90
  const [error, setError] = useState<Error | null>(null);
91
+ const requestIdRef = useRef(0);
94
92
 
95
93
  // Track context data changes to reset fetched data on navigation
96
94
  const prevContextDataRef = useRef(contextData);
@@ -108,13 +106,25 @@ function useLoaderInternal<T>(
108
106
 
109
107
  const throwOnError = options?.throwOnError ?? true;
110
108
 
111
- // Load function for fetching data
109
+ // Refs for values used inside load() that should NOT cause callback identity
110
+ // churn. loader.$$id can change if a reusable component receives a different
111
+ // loader without remounting; data changes on every navigation. Refs keep the
112
+ // callback stable while always reading the latest values.
113
+ const loaderIdRef = useRef(loader.$$id);
114
+ loaderIdRef.current = loader.$$id;
115
+ const dataRef = useRef(data);
116
+ dataRef.current = data;
117
+
118
+ // Load function for fetching data via the ?_rsc_loader endpoint.
119
+ // Supports GET (data fetching) and POST/PUT/PATCH/DELETE (mutations).
112
120
  const load = useCallback(
113
121
  async (loadOptions?: LoadOptions): Promise<T> => {
122
+ const requestId = ++requestIdRef.current;
123
+ const loaderId = loaderIdRef.current;
114
124
  // Verify the loader has $$id
115
- if (!loader.$$id) {
125
+ if (!loaderId) {
116
126
  throw new Error(
117
- `Loader is missing $$id. Make sure the exposeLoaderId Vite plugin is enabled.`
127
+ `Loader is missing $$id. Make sure the exposeLoaderId Vite plugin is enabled.`,
118
128
  );
119
129
  }
120
130
 
@@ -122,8 +132,8 @@ function useLoaderInternal<T>(
122
132
  setError(null);
123
133
 
124
134
  try {
125
- const url = new URL(window.location.pathname, window.location.origin);
126
- url.searchParams.set("_rsc_loader", loader.$$id);
135
+ const url = new URL(window.location.href);
136
+ url.searchParams.set("_rsc_loader", loaderId);
127
137
 
128
138
  const method = loadOptions?.method ?? "GET";
129
139
  const isBodyMethod = method !== "GET";
@@ -131,29 +141,58 @@ function useLoaderInternal<T>(
131
141
  let fetchOptions: RequestInit;
132
142
 
133
143
  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;
144
+ const bodyValue =
145
+ "body" in (loadOptions ?? {})
146
+ ? (loadOptions as any).body
147
+ : undefined;
148
+ const hasParams =
149
+ loadOptions?.params && Object.keys(loadOptions.params).length > 0;
150
+
151
+ if (bodyValue instanceof FormData) {
152
+ // FormData body — send as multipart/form-data (preserves File objects).
153
+ // Params are appended as a JSON string in a special field.
154
+ if (hasParams) {
155
+ bodyValue.set(
156
+ "_rsc_loader_params",
157
+ JSON.stringify(loadOptions!.params),
158
+ );
159
+ }
160
+ fetchOptions = {
161
+ method,
162
+ headers: { Accept: "text/x-component" },
163
+ body: bodyValue,
164
+ };
165
+ } else {
166
+ // JSON body — send params and body as JSON
167
+ const bodyPayload: {
168
+ params?: Record<string, string>;
169
+ body?: unknown;
170
+ } = {};
171
+ if (hasParams) {
172
+ bodyPayload.params = loadOptions!.params;
173
+ }
174
+ if (bodyValue !== undefined) {
175
+ bodyPayload.body = bodyValue;
176
+ }
177
+
178
+ fetchOptions = {
179
+ method,
180
+ headers: {
181
+ Accept: "text/x-component",
182
+ "Content-Type": "application/json",
183
+ },
184
+ body: JSON.stringify(bodyPayload),
185
+ };
138
186
  }
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
187
  } else {
152
188
  // GET - send params in query string
153
- if (loadOptions?.params && Object.keys(loadOptions.params).length > 0) {
189
+ if (
190
+ loadOptions?.params &&
191
+ Object.keys(loadOptions.params).length > 0
192
+ ) {
154
193
  url.searchParams.set(
155
194
  "_rsc_loader_params",
156
- JSON.stringify(loadOptions.params)
195
+ JSON.stringify(loadOptions.params),
157
196
  );
158
197
  }
159
198
 
@@ -175,61 +214,30 @@ function useLoaderInternal<T>(
175
214
  }
176
215
 
177
216
  const result = payload.loaderResult;
178
- setFetchedData(result);
217
+ if (requestId === requestIdRef.current) {
218
+ setFetchedData(result);
219
+ }
179
220
  return result;
180
221
  } catch (e) {
181
222
  const err = e instanceof Error ? e : new Error(String(e));
182
- setError(err);
183
- if (throwOnError) {
184
- throw err;
223
+ if (requestId === requestIdRef.current) {
224
+ setError(err);
185
225
  }
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
226
  if (throwOnError) {
220
227
  throw err;
221
228
  }
229
+ // When throwOnError is false, return the latest data snapshot (previous
230
+ // successful value or undefined). Caller should check error state.
231
+ return dataRef.current as T;
222
232
  } finally {
223
- setIsLoading(false);
233
+ if (requestId === requestIdRef.current) {
234
+ setIsLoading(false);
235
+ }
224
236
  }
225
237
  },
226
- [throwOnError]
238
+ [throwOnError],
227
239
  );
228
240
 
229
- // Attach action to load function
230
- const loadWithAction = load as LoadFunction<T>;
231
- loadWithAction.action = action;
232
-
233
241
  // Throw during render if there's an error and throwOnError is true
234
242
  // This allows ErrorBoundaries to catch async errors from load()
235
243
  if (error && throwOnError) {
@@ -240,8 +248,8 @@ function useLoaderInternal<T>(
240
248
  data,
241
249
  isLoading,
242
250
  error,
243
- load: loadWithAction,
244
- refetch: loadWithAction,
251
+ load,
252
+ refetch: load,
245
253
  };
246
254
  }
247
255
 
@@ -276,7 +284,7 @@ function useLoaderInternal<T>(
276
284
  */
277
285
  export function useLoader<T>(
278
286
  loader: LoaderDefinition<T>,
279
- options?: UseLoaderOptions
287
+ options?: UseLoaderOptions,
280
288
  ): UseLoaderResult<T> {
281
289
  const result = useLoaderInternal(loader, options);
282
290
 
@@ -285,7 +293,7 @@ export function useLoader<T>(
285
293
  throw new Error(
286
294
  `useLoader: Loader "${loader.$$id}" data not found in context. ` +
287
295
  `Make sure the loader is registered on the route with loader(). ` +
288
- `If you need on-demand fetching, use useFetchLoader() instead.`
296
+ `If you need on-demand fetching, use useFetchLoader() instead.`,
289
297
  );
290
298
  }
291
299
 
@@ -340,7 +348,7 @@ export function useLoader<T>(
340
348
  */
341
349
  export function useFetchLoader<T>(
342
350
  loader: LoaderDefinition<T>,
343
- options?: UseLoaderOptions
351
+ options?: UseLoaderOptions,
344
352
  ): UseFetchLoaderResult<T> {
345
353
  return useLoaderInternal(loader, options);
346
354
  }
@@ -0,0 +1,184 @@
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 __prerender-handlers and __static-handlers chunks.
35
+ // handlerChunkInfo/staticHandlerChunkInfo are populated by generateBundle
36
+ // after the production RSC build. In Vite 6 multi-environment builds, the
37
+ // RSC build runs twice (analysis + production). Chunk info is only available
38
+ // after the production pass, so we run eviction whenever it becomes available.
39
+ const evictionTargets: Array<{
40
+ info: typeof state.handlerChunkInfo;
41
+ fnName: string;
42
+ brand: string;
43
+ label: string;
44
+ }> = [
45
+ {
46
+ info: state.handlerChunkInfo,
47
+ fnName: "Prerender",
48
+ brand: "prerenderHandler",
49
+ label: "handler code from RSC bundle",
50
+ },
51
+ {
52
+ info: state.staticHandlerChunkInfo,
53
+ fnName: "Static",
54
+ brand: "staticHandler",
55
+ label: "static handler code",
56
+ },
57
+ ];
58
+
59
+ for (const target of evictionTargets) {
60
+ if (!target.info) continue;
61
+ const chunkPath = resolve(
62
+ state.projectRoot,
63
+ "dist/rsc",
64
+ target.info.fileName,
65
+ );
66
+ try {
67
+ const code = readFileSync(chunkPath, "utf-8");
68
+ const result = evictHandlerCode(
69
+ code,
70
+ target.info.exports,
71
+ target.fnName,
72
+ target.brand,
73
+ );
74
+ if (result) {
75
+ writeFileSync(chunkPath, result.code);
76
+ const savedKB = (result.savedBytes / 1024).toFixed(1);
77
+ console.log(
78
+ `[rsc-router] Evicted ${target.label} (${savedKB} KB saved): ${target.info.fileName}`,
79
+ );
80
+ }
81
+ } catch (replaceErr: any) {
82
+ console.warn(
83
+ `[rsc-router] Failed to evict ${target.label}: ${replaceErr.message}`,
84
+ );
85
+ }
86
+ }
87
+ state.handlerChunkInfo = null;
88
+ state.staticHandlerChunkInfo = null;
89
+
90
+ // 2. Write prerender data as separate importable asset modules
91
+ // and inject a lazy manifest loader into the RSC entry.
92
+ if (hasPrerenderData && existsSync(rscEntryPath)) {
93
+ const rscCode = readFileSync(rscEntryPath, "utf-8");
94
+ // Check for the specific injection marker to avoid double-injection.
95
+ if (!rscCode.includes("__prerender-manifest.js")) {
96
+ try {
97
+ let totalBytes = copyStagedBuildAssets(
98
+ state.projectRoot,
99
+ Object.values(state.prerenderManifestEntries!),
100
+ );
101
+
102
+ const manifestMap: Record<string, string> = {};
103
+ for (const [key, assetFileName] of Object.entries(
104
+ state.prerenderManifestEntries!,
105
+ )) {
106
+ manifestMap[key] = `./assets/${assetFileName}`;
107
+ }
108
+
109
+ const manifestCode = [
110
+ `const m=JSON.parse('${JSON.stringify(manifestMap).replace(/'/g, "\\'")}');`,
111
+ `export function loadPrerenderAsset(s){return import(s)}`,
112
+ `export default m;`,
113
+ "",
114
+ ].join("\n");
115
+ const manifestPath = resolve(
116
+ state.projectRoot,
117
+ "dist/rsc/__prerender-manifest.js",
118
+ );
119
+ writeFileSync(manifestPath, manifestCode);
120
+ totalBytes += Buffer.byteLength(manifestCode);
121
+
122
+ const injection = `globalThis.__loadPrerenderManifestModule = () => import("./__prerender-manifest.js");\n`;
123
+ writeFileSync(rscEntryPath, injection + rscCode);
124
+
125
+ const totalKB = (totalBytes / 1024).toFixed(1);
126
+ console.log(
127
+ `[rsc-router] Wrote prerender assets (${totalKB} KB total, ${Object.keys(state.prerenderManifestEntries!).length} entries)`,
128
+ );
129
+ } catch (err: any) {
130
+ throw new Error(
131
+ `[rsc-router] Failed to write prerender assets: ${err.message}`,
132
+ );
133
+ }
134
+ }
135
+ }
136
+
137
+ // 3. Write static handler data as separate importable asset modules
138
+ // and inject a __STATIC_MANIFEST import into the RSC entry.
139
+ if (hasStaticData && existsSync(rscEntryPath)) {
140
+ const rscCode = readFileSync(rscEntryPath, "utf-8");
141
+ if (!rscCode.includes("__STATIC_MANIFEST")) {
142
+ try {
143
+ const manifestEntries: string[] = [];
144
+ let totalBytes = copyStagedBuildAssets(
145
+ state.projectRoot,
146
+ Object.values(state.staticManifestEntries!),
147
+ );
148
+
149
+ for (const [handlerId, assetFileName] of Object.entries(
150
+ state.staticManifestEntries!,
151
+ )) {
152
+ manifestEntries.push(
153
+ `${JSON.stringify(handlerId)}:()=>import("./assets/${assetFileName}")`,
154
+ );
155
+ }
156
+
157
+ // Set the global inside the manifest module so it is assigned
158
+ // during module evaluation (before dependent modules like
159
+ // segment-resolution.ts run their top-level initializers).
160
+ const manifestCode = `const m={${manifestEntries.join(",")}};globalThis.__STATIC_MANIFEST=m;export default m;\n`;
161
+ const manifestPath = resolve(
162
+ state.projectRoot,
163
+ "dist/rsc/__static-manifest.js",
164
+ );
165
+ writeFileSync(manifestPath, manifestCode);
166
+ totalBytes += Buffer.byteLength(manifestCode);
167
+
168
+ // The import ensures the manifest module is evaluated early.
169
+ // The global is already set inside the module itself.
170
+ const injection = `import "./__static-manifest.js";\n`;
171
+ writeFileSync(rscEntryPath, injection + rscCode);
172
+
173
+ const totalKB = (totalBytes / 1024).toFixed(1);
174
+ console.log(
175
+ `[rsc-router] Wrote static assets (${totalKB} KB total, ${Object.keys(state.staticManifestEntries!).length} entries)`,
176
+ );
177
+ } catch (err: any) {
178
+ throw new Error(
179
+ `[rsc-router] Failed to write static assets: ${err.message}`,
180
+ );
181
+ }
182
+ }
183
+ }
184
+ }