@rangojs/router 0.0.0-experimental.0f44aca1

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 (305) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +899 -0
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +5214 -0
  5. package/package.json +176 -0
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +220 -0
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +112 -0
  11. package/skills/document-cache/SKILL.md +182 -0
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +704 -0
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +313 -0
  16. package/skills/layout/SKILL.md +310 -0
  17. package/skills/links/SKILL.md +239 -0
  18. package/skills/loader/SKILL.md +596 -0
  19. package/skills/middleware/SKILL.md +339 -0
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +305 -0
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +118 -0
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +385 -0
  26. package/skills/router-setup/SKILL.md +439 -0
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +79 -0
  29. package/skills/typesafety/SKILL.md +623 -0
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +273 -0
  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 +899 -0
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/index.ts +18 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +141 -0
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +134 -0
  42. package/src/browser/navigation-bridge.ts +645 -0
  43. package/src/browser/navigation-client.ts +215 -0
  44. package/src/browser/navigation-store.ts +806 -0
  45. package/src/browser/navigation-transaction.ts +295 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +550 -0
  48. package/src/browser/prefetch/cache.ts +146 -0
  49. package/src/browser/prefetch/fetch.ts +135 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +42 -0
  52. package/src/browser/prefetch/queue.ts +88 -0
  53. package/src/browser/rango-state.ts +112 -0
  54. package/src/browser/react/Link.tsx +360 -0
  55. package/src/browser/react/NavigationProvider.tsx +386 -0
  56. package/src/browser/react/ScrollRestoration.tsx +94 -0
  57. package/src/browser/react/context.ts +59 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +52 -0
  60. package/src/browser/react/location-state-shared.ts +162 -0
  61. package/src/browser/react/location-state.ts +107 -0
  62. package/src/browser/react/mount-context.ts +37 -0
  63. package/src/browser/react/nonce-context.ts +23 -0
  64. package/src/browser/react/shallow-equal.ts +27 -0
  65. package/src/browser/react/use-action.ts +218 -0
  66. package/src/browser/react/use-client-cache.ts +58 -0
  67. package/src/browser/react/use-handle.ts +162 -0
  68. package/src/browser/react/use-href.tsx +40 -0
  69. package/src/browser/react/use-link-status.ts +135 -0
  70. package/src/browser/react/use-mount.ts +31 -0
  71. package/src/browser/react/use-navigation.ts +99 -0
  72. package/src/browser/react/use-params.ts +65 -0
  73. package/src/browser/react/use-pathname.ts +47 -0
  74. package/src/browser/react/use-router.ts +63 -0
  75. package/src/browser/react/use-search-params.ts +56 -0
  76. package/src/browser/react/use-segments.ts +171 -0
  77. package/src/browser/response-adapter.ts +73 -0
  78. package/src/browser/rsc-router.tsx +431 -0
  79. package/src/browser/scroll-restoration.ts +400 -0
  80. package/src/browser/segment-reconciler.ts +216 -0
  81. package/src/browser/segment-structure-assert.ts +83 -0
  82. package/src/browser/server-action-bridge.ts +667 -0
  83. package/src/browser/shallow.ts +40 -0
  84. package/src/browser/types.ts +538 -0
  85. package/src/browser/validate-redirect-origin.ts +29 -0
  86. package/src/build/generate-manifest.ts +438 -0
  87. package/src/build/generate-route-types.ts +36 -0
  88. package/src/build/index.ts +35 -0
  89. package/src/build/route-trie.ts +265 -0
  90. package/src/build/route-types/ast-helpers.ts +25 -0
  91. package/src/build/route-types/ast-route-extraction.ts +98 -0
  92. package/src/build/route-types/codegen.ts +102 -0
  93. package/src/build/route-types/include-resolution.ts +411 -0
  94. package/src/build/route-types/param-extraction.ts +48 -0
  95. package/src/build/route-types/per-module-writer.ts +128 -0
  96. package/src/build/route-types/router-processing.ts +469 -0
  97. package/src/build/route-types/scan-filter.ts +78 -0
  98. package/src/build/runtime-discovery.ts +231 -0
  99. package/src/cache/background-task.ts +34 -0
  100. package/src/cache/cache-key-utils.ts +44 -0
  101. package/src/cache/cache-policy.ts +125 -0
  102. package/src/cache/cache-runtime.ts +338 -0
  103. package/src/cache/cache-scope.ts +382 -0
  104. package/src/cache/cf/cf-cache-store.ts +540 -0
  105. package/src/cache/cf/index.ts +25 -0
  106. package/src/cache/document-cache.ts +369 -0
  107. package/src/cache/handle-capture.ts +81 -0
  108. package/src/cache/handle-snapshot.ts +41 -0
  109. package/src/cache/index.ts +43 -0
  110. package/src/cache/memory-segment-store.ts +328 -0
  111. package/src/cache/profile-registry.ts +73 -0
  112. package/src/cache/read-through-swr.ts +134 -0
  113. package/src/cache/segment-codec.ts +256 -0
  114. package/src/cache/taint.ts +98 -0
  115. package/src/cache/types.ts +342 -0
  116. package/src/client.rsc.tsx +85 -0
  117. package/src/client.tsx +601 -0
  118. package/src/component-utils.ts +76 -0
  119. package/src/components/DefaultDocument.tsx +27 -0
  120. package/src/context-var.ts +86 -0
  121. package/src/debug.ts +243 -0
  122. package/src/default-error-boundary.tsx +88 -0
  123. package/src/deps/browser.ts +8 -0
  124. package/src/deps/html-stream-client.ts +2 -0
  125. package/src/deps/html-stream-server.ts +2 -0
  126. package/src/deps/rsc.ts +10 -0
  127. package/src/deps/ssr.ts +2 -0
  128. package/src/errors.ts +365 -0
  129. package/src/handle.ts +135 -0
  130. package/src/handles/MetaTags.tsx +246 -0
  131. package/src/handles/breadcrumbs.ts +66 -0
  132. package/src/handles/index.ts +7 -0
  133. package/src/handles/meta.ts +264 -0
  134. package/src/host/cookie-handler.ts +165 -0
  135. package/src/host/errors.ts +97 -0
  136. package/src/host/index.ts +53 -0
  137. package/src/host/pattern-matcher.ts +214 -0
  138. package/src/host/router.ts +352 -0
  139. package/src/host/testing.ts +79 -0
  140. package/src/host/types.ts +146 -0
  141. package/src/host/utils.ts +25 -0
  142. package/src/href-client.ts +222 -0
  143. package/src/index.rsc.ts +233 -0
  144. package/src/index.ts +277 -0
  145. package/src/internal-debug.ts +11 -0
  146. package/src/loader.rsc.ts +89 -0
  147. package/src/loader.ts +64 -0
  148. package/src/network-error-thrower.tsx +23 -0
  149. package/src/outlet-context.ts +15 -0
  150. package/src/outlet-provider.tsx +45 -0
  151. package/src/prerender/param-hash.ts +37 -0
  152. package/src/prerender/store.ts +185 -0
  153. package/src/prerender.ts +463 -0
  154. package/src/reverse.ts +330 -0
  155. package/src/root-error-boundary.tsx +289 -0
  156. package/src/route-content-wrapper.tsx +196 -0
  157. package/src/route-definition/dsl-helpers.ts +934 -0
  158. package/src/route-definition/helper-factories.ts +200 -0
  159. package/src/route-definition/helpers-types.ts +430 -0
  160. package/src/route-definition/index.ts +52 -0
  161. package/src/route-definition/redirect.ts +93 -0
  162. package/src/route-definition.ts +1 -0
  163. package/src/route-map-builder.ts +275 -0
  164. package/src/route-name.ts +53 -0
  165. package/src/route-types.ts +259 -0
  166. package/src/router/content-negotiation.ts +116 -0
  167. package/src/router/debug-manifest.ts +72 -0
  168. package/src/router/error-handling.ts +287 -0
  169. package/src/router/find-match.ts +158 -0
  170. package/src/router/handler-context.ts +451 -0
  171. package/src/router/intercept-resolution.ts +395 -0
  172. package/src/router/lazy-includes.ts +234 -0
  173. package/src/router/loader-resolution.ts +420 -0
  174. package/src/router/logging.ts +248 -0
  175. package/src/router/manifest.ts +267 -0
  176. package/src/router/match-api.ts +620 -0
  177. package/src/router/match-context.ts +266 -0
  178. package/src/router/match-handlers.ts +440 -0
  179. package/src/router/match-middleware/background-revalidation.ts +223 -0
  180. package/src/router/match-middleware/cache-lookup.ts +634 -0
  181. package/src/router/match-middleware/cache-store.ts +295 -0
  182. package/src/router/match-middleware/index.ts +81 -0
  183. package/src/router/match-middleware/intercept-resolution.ts +306 -0
  184. package/src/router/match-middleware/segment-resolution.ts +192 -0
  185. package/src/router/match-pipelines.ts +179 -0
  186. package/src/router/match-result.ts +219 -0
  187. package/src/router/metrics.ts +282 -0
  188. package/src/router/middleware-cookies.ts +55 -0
  189. package/src/router/middleware-types.ts +222 -0
  190. package/src/router/middleware.ts +748 -0
  191. package/src/router/pattern-matching.ts +563 -0
  192. package/src/router/prerender-match.ts +402 -0
  193. package/src/router/preview-match.ts +170 -0
  194. package/src/router/revalidation.ts +289 -0
  195. package/src/router/router-context.ts +316 -0
  196. package/src/router/router-interfaces.ts +452 -0
  197. package/src/router/router-options.ts +592 -0
  198. package/src/router/router-registry.ts +24 -0
  199. package/src/router/segment-resolution/fresh.ts +570 -0
  200. package/src/router/segment-resolution/helpers.ts +263 -0
  201. package/src/router/segment-resolution/loader-cache.ts +198 -0
  202. package/src/router/segment-resolution/revalidation.ts +1239 -0
  203. package/src/router/segment-resolution/static-store.ts +67 -0
  204. package/src/router/segment-resolution.ts +21 -0
  205. package/src/router/segment-wrappers.ts +289 -0
  206. package/src/router/telemetry-otel.ts +299 -0
  207. package/src/router/telemetry.ts +300 -0
  208. package/src/router/timeout.ts +148 -0
  209. package/src/router/trie-matching.ts +239 -0
  210. package/src/router/types.ts +170 -0
  211. package/src/router.ts +1002 -0
  212. package/src/rsc/handler-context.ts +45 -0
  213. package/src/rsc/handler.ts +1089 -0
  214. package/src/rsc/helpers.ts +198 -0
  215. package/src/rsc/index.ts +36 -0
  216. package/src/rsc/loader-fetch.ts +209 -0
  217. package/src/rsc/manifest-init.ts +86 -0
  218. package/src/rsc/nonce.ts +32 -0
  219. package/src/rsc/origin-guard.ts +141 -0
  220. package/src/rsc/progressive-enhancement.ts +379 -0
  221. package/src/rsc/response-error.ts +37 -0
  222. package/src/rsc/response-route-handler.ts +347 -0
  223. package/src/rsc/rsc-rendering.ts +235 -0
  224. package/src/rsc/runtime-warnings.ts +42 -0
  225. package/src/rsc/server-action.ts +348 -0
  226. package/src/rsc/ssr-setup.ts +128 -0
  227. package/src/rsc/types.ts +263 -0
  228. package/src/search-params.ts +230 -0
  229. package/src/segment-system.tsx +454 -0
  230. package/src/server/context.ts +591 -0
  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 +308 -0
  234. package/src/server/loader-registry.ts +133 -0
  235. package/src/server/request-context.ts +914 -0
  236. package/src/server/root-layout.tsx +10 -0
  237. package/src/server/tsconfig.json +14 -0
  238. package/src/server.ts +51 -0
  239. package/src/ssr/index.tsx +365 -0
  240. package/src/static-handler.ts +114 -0
  241. package/src/theme/ThemeProvider.tsx +297 -0
  242. package/src/theme/ThemeScript.tsx +61 -0
  243. package/src/theme/constants.ts +62 -0
  244. package/src/theme/index.ts +48 -0
  245. package/src/theme/theme-context.ts +44 -0
  246. package/src/theme/theme-script.ts +155 -0
  247. package/src/theme/types.ts +182 -0
  248. package/src/theme/use-theme.ts +44 -0
  249. package/src/types/boundaries.ts +158 -0
  250. package/src/types/cache-types.ts +198 -0
  251. package/src/types/error-types.ts +192 -0
  252. package/src/types/global-namespace.ts +100 -0
  253. package/src/types/handler-context.ts +687 -0
  254. package/src/types/index.ts +88 -0
  255. package/src/types/loader-types.ts +183 -0
  256. package/src/types/route-config.ts +170 -0
  257. package/src/types/route-entry.ts +102 -0
  258. package/src/types/segments.ts +148 -0
  259. package/src/types.ts +1 -0
  260. package/src/urls/include-helper.ts +197 -0
  261. package/src/urls/index.ts +53 -0
  262. package/src/urls/path-helper-types.ts +339 -0
  263. package/src/urls/path-helper.ts +329 -0
  264. package/src/urls/pattern-types.ts +95 -0
  265. package/src/urls/response-types.ts +106 -0
  266. package/src/urls/type-extraction.ts +372 -0
  267. package/src/urls/urls-function.ts +98 -0
  268. package/src/urls.ts +1 -0
  269. package/src/use-loader.tsx +354 -0
  270. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  271. package/src/vite/discovery/discover-routers.ts +344 -0
  272. package/src/vite/discovery/prerender-collection.ts +385 -0
  273. package/src/vite/discovery/route-types-writer.ts +258 -0
  274. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  275. package/src/vite/discovery/state.ts +110 -0
  276. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  277. package/src/vite/index.ts +16 -0
  278. package/src/vite/plugin-types.ts +131 -0
  279. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  280. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  281. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  282. package/src/vite/plugins/expose-action-id.ts +365 -0
  283. package/src/vite/plugins/expose-id-utils.ts +287 -0
  284. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  285. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  286. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  287. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  288. package/src/vite/plugins/expose-ids/types.ts +45 -0
  289. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  290. package/src/vite/plugins/refresh-cmd.ts +65 -0
  291. package/src/vite/plugins/use-cache-transform.ts +323 -0
  292. package/src/vite/plugins/version-injector.ts +83 -0
  293. package/src/vite/plugins/version-plugin.ts +254 -0
  294. package/src/vite/plugins/version.d.ts +12 -0
  295. package/src/vite/plugins/virtual-entries.ts +123 -0
  296. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  297. package/src/vite/rango.ts +510 -0
  298. package/src/vite/router-discovery.ts +785 -0
  299. package/src/vite/utils/ast-handler-extract.ts +517 -0
  300. package/src/vite/utils/banner.ts +36 -0
  301. package/src/vite/utils/bundle-analysis.ts +137 -0
  302. package/src/vite/utils/manifest-utils.ts +70 -0
  303. package/src/vite/utils/package-resolution.ts +121 -0
  304. package/src/vite/utils/prerender-utils.ts +189 -0
  305. package/src/vite/utils/shared-utils.ts +169 -0
@@ -0,0 +1,354 @@
1
+ "use client";
2
+
3
+ import { useCallback, useContext, useEffect, useRef, useState } from "react";
4
+ import { OutletContext, type OutletContextValue } from "./outlet-context.js";
5
+ import type { LoaderDefinition, LoadOptions } from "./types.js";
6
+
7
+ /**
8
+ * Payload returned by loader RSC requests
9
+ */
10
+ interface LoaderRscPayload<T = unknown> {
11
+ loaderResult: T;
12
+ loaderError?: { message: string; name: string };
13
+ }
14
+
15
+ /**
16
+ * Load function type for fetching loader data from the client
17
+ */
18
+ export type LoadFunction<T> = (options?: LoadOptions) => Promise<T>;
19
+
20
+ /**
21
+ * Result type for useLoader hook (strict - data is required)
22
+ */
23
+ export interface UseLoaderResult<T> {
24
+ /** The loaded data - guaranteed to exist when loader is registered on route */
25
+ data: T;
26
+ /** True while a load() is in progress */
27
+ isLoading: boolean;
28
+ /** Error from the most recent load attempt, null if successful */
29
+ error: Error | null;
30
+ /** Function to trigger a fetch (only works if loader is fetchable) */
31
+ load: LoadFunction<T>;
32
+ /** Alias for load */
33
+ refetch: LoadFunction<T>;
34
+ }
35
+
36
+ /**
37
+ * Result type for useFetchLoader hook (flexible - data is optional)
38
+ */
39
+ export interface UseFetchLoaderResult<T> {
40
+ /** The loaded data - may be undefined if not yet fetched or not in context */
41
+ data: T | undefined;
42
+ /** True while a load() is in progress */
43
+ isLoading: boolean;
44
+ /** Error from the most recent load attempt, null if successful */
45
+ error: Error | null;
46
+ /** Function to trigger a fetch (only works if loader is fetchable) */
47
+ load: LoadFunction<T>;
48
+ /** Alias for load */
49
+ refetch: LoadFunction<T>;
50
+ }
51
+
52
+ /**
53
+ * Options for useLoader hook
54
+ */
55
+ export interface UseLoaderOptions {
56
+ /**
57
+ * If true (default), errors from load() will be thrown to the nearest error boundary.
58
+ * If false, errors are only captured in the `error` state.
59
+ * @default true
60
+ */
61
+ throwOnError?: boolean;
62
+ }
63
+
64
+ /**
65
+ * Internal hook implementation shared by useLoader and useFetchLoader
66
+ */
67
+ function useLoaderInternal<T>(
68
+ loader: LoaderDefinition<T>,
69
+ options?: UseLoaderOptions,
70
+ ): UseFetchLoaderResult<T> {
71
+ const context = useContext(OutletContext);
72
+
73
+ // Get data from context (SSR/navigation)
74
+ const getContextData = useCallback((): T | undefined => {
75
+ let current: OutletContextValue | null | undefined = context;
76
+ while (current) {
77
+ if (current.loaderData && loader.$$id in current.loaderData) {
78
+ return current.loaderData[loader.$$id] as T;
79
+ }
80
+ current = current.parent;
81
+ }
82
+ return undefined;
83
+ }, [context, loader.$$id]);
84
+
85
+ const contextData = getContextData();
86
+
87
+ // Local state for fetched data (from load() calls)
88
+ const [fetchedData, setFetchedData] = useState<T | undefined>(undefined);
89
+ const [isLoading, setIsLoading] = useState(false);
90
+ const [error, setError] = useState<Error | null>(null);
91
+ const requestIdRef = useRef(0);
92
+
93
+ // Track context data changes to reset fetched data on navigation
94
+ const prevContextDataRef = useRef(contextData);
95
+ useEffect(() => {
96
+ if (prevContextDataRef.current !== contextData) {
97
+ // Navigation happened, clear fetched data so context takes precedence
98
+ setFetchedData(undefined);
99
+ setError(null);
100
+ prevContextDataRef.current = contextData;
101
+ }
102
+ }, [contextData]);
103
+
104
+ // Data priority: fetched data (if any) > context data
105
+ const data = fetchedData ?? contextData;
106
+
107
+ const throwOnError = options?.throwOnError ?? true;
108
+
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).
120
+ const load = useCallback(
121
+ async (loadOptions?: LoadOptions): Promise<T> => {
122
+ const requestId = ++requestIdRef.current;
123
+ const loaderId = loaderIdRef.current;
124
+ // Verify the loader has $$id
125
+ if (!loaderId) {
126
+ throw new Error(
127
+ `Loader is missing $$id. Make sure the exposeLoaderId Vite plugin is enabled.`,
128
+ );
129
+ }
130
+
131
+ setIsLoading(true);
132
+ setError(null);
133
+
134
+ try {
135
+ const url = new URL(window.location.href);
136
+ url.searchParams.set("_rsc_loader", loaderId);
137
+
138
+ const method = loadOptions?.method ?? "GET";
139
+ const isBodyMethod = method !== "GET";
140
+
141
+ let fetchOptions: RequestInit;
142
+
143
+ if (isBodyMethod) {
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
+ };
186
+ }
187
+ } else {
188
+ // GET - send params in query string
189
+ if (
190
+ loadOptions?.params &&
191
+ Object.keys(loadOptions.params).length > 0
192
+ ) {
193
+ url.searchParams.set(
194
+ "_rsc_loader_params",
195
+ JSON.stringify(loadOptions.params),
196
+ );
197
+ }
198
+
199
+ fetchOptions = {
200
+ method: "GET",
201
+ headers: {
202
+ Accept: "text/x-component",
203
+ },
204
+ };
205
+ }
206
+
207
+ const response = fetch(url.toString(), fetchOptions);
208
+
209
+ const { createFromFetch } = await import("./deps/browser.js");
210
+ const payload = await createFromFetch<LoaderRscPayload<T>>(response);
211
+
212
+ if (payload.loaderError) {
213
+ throw new Error(payload.loaderError.message);
214
+ }
215
+
216
+ const result = payload.loaderResult;
217
+ if (requestId === requestIdRef.current) {
218
+ setFetchedData(result);
219
+ }
220
+ return result;
221
+ } catch (e) {
222
+ const err = e instanceof Error ? e : new Error(String(e));
223
+ if (requestId === requestIdRef.current) {
224
+ setError(err);
225
+ }
226
+ if (throwOnError) {
227
+ throw err;
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;
232
+ } finally {
233
+ if (requestId === requestIdRef.current) {
234
+ setIsLoading(false);
235
+ }
236
+ }
237
+ },
238
+ [throwOnError],
239
+ );
240
+
241
+ // Throw during render if there's an error and throwOnError is true
242
+ // This allows ErrorBoundaries to catch async errors from load()
243
+ if (error && throwOnError) {
244
+ throw error;
245
+ }
246
+
247
+ return {
248
+ data,
249
+ isLoading,
250
+ error,
251
+ load,
252
+ refetch: load,
253
+ };
254
+ }
255
+
256
+ /**
257
+ * Hook to access loader data from route context (strict version)
258
+ *
259
+ * Use this when the loader is registered on the route via `loader()`.
260
+ * The data is guaranteed to exist - throws an error if not found.
261
+ *
262
+ * For on-demand fetching or when loader might not be in context,
263
+ * use `useFetchLoader` instead.
264
+ *
265
+ * @param loader - The loader definition (must be registered on route)
266
+ * @param options - Optional configuration
267
+ * @returns Object with data (guaranteed), isLoading, error, load, and refetch
268
+ * @throws Error if loader data is not found in context
269
+ *
270
+ * @example Basic usage - accessing route loader data
271
+ * ```tsx
272
+ * "use client";
273
+ * import { useLoader } from "rsc-router/client";
274
+ * import { CartLoader } from "../loaders/cart";
275
+ *
276
+ * // In route definition: loader(CartLoader)
277
+ *
278
+ * export function CartIcon() {
279
+ * const { data } = useLoader(CartLoader);
280
+ * // data is guaranteed to be CartData, not CartData | undefined
281
+ * return <span>Cart ({data.items.length})</span>;
282
+ * }
283
+ * ```
284
+ */
285
+ export function useLoader<T>(
286
+ loader: LoaderDefinition<T>,
287
+ options?: UseLoaderOptions,
288
+ ): UseLoaderResult<T> {
289
+ const result = useLoaderInternal(loader, options);
290
+
291
+ // Strict mode: throw if data is not in context
292
+ if (result.data === undefined) {
293
+ throw new Error(
294
+ `useLoader: Loader "${loader.$$id}" data not found in context. ` +
295
+ `Make sure the loader is registered on the route with loader(). ` +
296
+ `If you need on-demand fetching, use useFetchLoader() instead.`,
297
+ );
298
+ }
299
+
300
+ return result as UseLoaderResult<T>;
301
+ }
302
+
303
+ /**
304
+ * Hook to access loader data with optional fetching (flexible version)
305
+ *
306
+ * Use this when:
307
+ * - The loader might not be registered on the route
308
+ * - You want to fetch data on-demand from the client
309
+ * - You're building a reusable component that doesn't assume route context
310
+ *
311
+ * If the loader IS registered on the route, it will still get the initial
312
+ * data from context - you just have to handle the `undefined` case in types.
313
+ *
314
+ * @param loader - The loader definition
315
+ * @param options - Optional configuration
316
+ * @returns Object with data (may be undefined), isLoading, error, load, and refetch
317
+ *
318
+ * @example On-demand fetching
319
+ * ```tsx
320
+ * "use client";
321
+ * import { useFetchLoader } from "rsc-router/client";
322
+ * import { SearchLoader } from "../loaders/search";
323
+ *
324
+ * export function SearchResults() {
325
+ * const { data, load, isLoading } = useFetchLoader(SearchLoader);
326
+ *
327
+ * const handleSearch = async (query: string) => {
328
+ * await load({ params: { query } });
329
+ * };
330
+ *
331
+ * return (
332
+ * <div>
333
+ * <button onClick={() => handleSearch("test")}>Search</button>
334
+ * {isLoading && <span>Loading...</span>}
335
+ * {data?.results.map(r => <div key={r.id}>{r.name}</div>)}
336
+ * </div>
337
+ * );
338
+ * }
339
+ * ```
340
+ *
341
+ * @example With route context (hybrid usage)
342
+ * ```tsx
343
+ * // Loader registered on route: loader(UserLoader)
344
+ * // useFetchLoader still works - gets initial data from context
345
+ * const { data, load } = useFetchLoader(UserLoader);
346
+ * // data is UserData | undefined (even though it will have initial value)
347
+ * ```
348
+ */
349
+ export function useFetchLoader<T>(
350
+ loader: LoaderDefinition<T>,
351
+ options?: UseLoaderOptions,
352
+ ): UseFetchLoaderResult<T> {
353
+ return useLoaderInternal(loader, options);
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
+ }