@rangojs/router 0.0.0-experimental.8 → 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 -867
  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 -1133
  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
@@ -21,8 +21,9 @@ import type {
21
21
  import type { LoaderRevalidationResult, ActionContext } from "./types";
22
22
  import { isHandle, type Handle } from "../handle.js";
23
23
  import type { HandleStore } from "../server/handle-store.js";
24
- import { getFetchableLoader } from "../loader.rsc.js";
25
- import { getRequestContext } from "../server/request-context.js";
24
+ import { getFetchableLoader } from "../server/fetchable-loader-store.js";
25
+ import { _getRequestContext } from "../server/request-context.js";
26
+ import { debugLog } from "./logging.js";
26
27
 
27
28
  /**
28
29
  * Internal callback signature for loader error notifications.
@@ -35,7 +36,7 @@ export type LoaderErrorCallback = (
35
36
  segmentId: string;
36
37
  loaderName: string;
37
38
  handledByBoundary: boolean;
38
- }
39
+ },
39
40
  ) => void;
40
41
 
41
42
  /**
@@ -54,14 +55,14 @@ export function wrapLoaderWithErrorHandling<T>(
54
55
  segmentId: string,
55
56
  pathname: string,
56
57
  findNearestErrorBoundary: (
57
- entry: EntryData | null
58
+ entry: EntryData | null,
58
59
  ) => ReactNode | ErrorBoundaryHandler | null,
59
60
  createErrorInfo: (
60
61
  error: unknown,
61
62
  segmentId: string,
62
- segmentType: ErrorInfo["segmentType"]
63
+ segmentType: ErrorInfo["segmentType"],
63
64
  ) => ErrorInfo,
64
- onError?: LoaderErrorCallback
65
+ onError?: LoaderErrorCallback,
65
66
  ): Promise<LoaderDataResult<T>> {
66
67
  // Extract loader name from segmentId (format: "M1L0D0.loaderName")
67
68
  const loaderName = segmentId.split(".").pop() || "unknown";
@@ -72,7 +73,7 @@ export function wrapLoaderWithErrorHandling<T>(
72
73
  __loaderResult: true,
73
74
  ok: true,
74
75
  data,
75
- })
76
+ }),
76
77
  )
77
78
  .catch((error): LoaderDataResult<T> => {
78
79
  // Find nearest error boundary
@@ -111,10 +112,10 @@ export function wrapLoaderWithErrorHandling<T>(
111
112
  renderedFallback = fallback;
112
113
  }
113
114
 
114
- console.log(
115
- `[Router] Loader error wrapped with boundary fallback in ${segmentId}:`,
116
- errorInfo.message
117
- );
115
+ debugLog("loader", "loader error wrapped with boundary fallback", {
116
+ segmentId,
117
+ message: errorInfo.message,
118
+ });
118
119
 
119
120
  return {
120
121
  __loaderResult: true,
@@ -126,61 +127,103 @@ export function wrapLoaderWithErrorHandling<T>(
126
127
  }
127
128
 
128
129
  /**
129
- * Set up the use() method on handler context to access loaders and handles.
130
- *
131
- * For loaders: Lazily runs loaders, memoizes results per request.
132
- * For handles: Returns a push function bound to the current segment.
130
+ * Detect cycles in the loader dependency graph using DFS from a given node.
131
+ * Returns the cycle path (array of loader IDs forming the cycle) if one exists,
132
+ * or null if no cycle is found.
133
133
  */
134
- export function setupLoaderAccess<TEnv>(
135
- ctx: HandlerContext<any, TEnv>,
136
- loaderPromises: Map<string, Promise<any>>
137
- ): void {
138
- // Get HandleStore from request context
139
- const getHandleStore = (): HandleStore | undefined => {
140
- return getRequestContext()?._handleStore;
141
- };
134
+ function detectLoaderCycle(
135
+ from: string,
136
+ to: string,
137
+ dependsOn: Map<string, Set<string>>,
138
+ ): string[] | null {
139
+ // If `to` can reach `from` via the dependency graph, adding the edge
140
+ // from -> to creates a cycle. We search from `to` looking for `from`.
141
+ const visited = new Set<string>();
142
+ const path: string[] = [from, to];
143
+
144
+ function dfs(current: string): string[] | null {
145
+ if (current === from) {
146
+ // Found a cycle: return the path leading back to `from`
147
+ return path;
148
+ }
149
+ if (visited.has(current)) return null;
150
+ visited.add(current);
142
151
 
143
- // The use() function handles both loaders and handles
144
- ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
145
- // Handle case: return a push function
146
- if (isHandle(item)) {
147
- const handle = item;
148
- const store = getHandleStore();
149
- const segmentId = (ctx as InternalHandlerContext)._currentSegmentId;
152
+ const deps = dependsOn.get(current);
153
+ if (!deps) return null;
150
154
 
151
- if (!segmentId) {
152
- throw new Error(
153
- `Handle "${handle.$$id}" used outside of handler context. ` +
154
- `Handles must be used within route/layout handlers.`
155
- );
156
- }
155
+ for (const dep of deps) {
156
+ path.push(dep);
157
+ const cycle = dfs(dep);
158
+ if (cycle) return cycle;
159
+ path.pop();
160
+ }
161
+ return null;
162
+ }
157
163
 
158
- // Return a push function bound to this handle and segment
159
- // Accepts: value, Promise, or async callback (executed immediately)
160
- // Promises are pushed directly - RSC will serialize and stream them
161
- return (dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
162
- if (!store) return;
164
+ return dfs(to);
165
+ }
163
166
 
164
- // If it's a function, call it immediately to get the promise
165
- const valueOrPromise = typeof dataOrFn === "function"
166
- ? (dataOrFn as () => Promise<unknown>)()
167
- : dataOrFn;
167
+ /**
168
+ * Creates a memoizing loader executor with cycle detection.
169
+ * Shared by setupLoaderAccess and setupLoaderAccessSilent; only the handle
170
+ * branch differs between the two, so only the loader logic is extracted here.
171
+ *
172
+ * Returns a useLoader(loader, callerLoaderId) function that:
173
+ * - Tracks dependency edges between loaders for cycle detection
174
+ * - Throws immediately (synchronously inside an async fn) on circular deps
175
+ * - Memoizes each loader's promise so it runs at most once per request
176
+ */
177
+ function createLoaderExecutor<TEnv>(
178
+ ctx: HandlerContext<any, TEnv>,
179
+ loaderPromises: Map<string, Promise<any>>,
180
+ ): (
181
+ loader: LoaderDefinition<any, any>,
182
+ callerLoaderId: string | null,
183
+ ) => Promise<any> {
184
+ // Capture RequestContext eagerly for cookie access (ALS protection on Cloudflare)
185
+ const reqCtxRef = _getRequestContext();
186
+
187
+ // Dependency graph: loaderId -> set of loader IDs it directly depends on.
188
+ const dependsOn = new Map<string, Set<string>>();
189
+
190
+ // Loaders whose promises have not yet settled.
191
+ // A dependency on a pending loader that closes a cycle means deadlock.
192
+ const pendingLoaders = new Set<string>();
193
+
194
+ function useLoader(
195
+ loader: LoaderDefinition<any, any>,
196
+ callerLoaderId: string | null,
197
+ ): Promise<any> {
198
+ // Record the dependency edge and check for cycles before running
199
+ if (callerLoaderId !== null) {
200
+ let deps = dependsOn.get(callerLoaderId);
201
+ if (!deps) {
202
+ deps = new Set();
203
+ dependsOn.set(callerLoaderId, deps);
204
+ }
168
205
 
169
- // Push directly - promises will be serialized by RSC and streamed
170
- store.push(handle.$$id, segmentId, valueOrPromise);
171
- };
172
- }
206
+ // Only relevant when the target is still pending (would deadlock)
207
+ if (pendingLoaders.has(loader.$$id)) {
208
+ const cycle = detectLoaderCycle(callerLoaderId, loader.$$id, dependsOn);
209
+ if (cycle) {
210
+ throw new Error(
211
+ `Circular loader dependency detected: ${cycle.join(" -> ")}. ` +
212
+ `Loaders cannot depend on each other in a cycle. ` +
213
+ `Refactor to break the circular dependency.`,
214
+ );
215
+ }
216
+ }
173
217
 
174
- // Loader case: existing behavior
175
- const loader = item as LoaderDefinition<any, any>;
218
+ deps.add(loader.$$id);
219
+ }
176
220
 
177
221
  // Return cached promise if already started
178
222
  if (loaderPromises.has(loader.$$id)) {
179
- return loaderPromises.get(loader.$$id);
223
+ return loaderPromises.get(loader.$$id)!;
180
224
  }
181
225
 
182
226
  // Get loader function - either from loader object or fetchable registry
183
- // Fetchable loaders store fn in registry (not on object) to avoid client bundling issues
184
227
  let loaderFn = loader.fn;
185
228
  if (!loaderFn) {
186
229
  const fetchable = getFetchableLoader(loader.$$id);
@@ -189,122 +232,172 @@ export function setupLoaderAccess<TEnv>(
189
232
  }
190
233
  }
191
234
 
192
- // Ensure loader has a function
193
235
  if (!loaderFn) {
194
236
  throw new Error(
195
- `Loader "${loader.$$id}" has no function. This usually means the loader was defined without "use server" and the function was not included in the build.`
237
+ `Loader "${loader.$$id}" has no function. This usually means the loader was defined without "use server" and the function was not included in the build.`,
196
238
  );
197
239
  }
198
240
 
199
- // Create loader context with recursive use() support
241
+ pendingLoaders.add(loader.$$id);
242
+
243
+ const currentLoaderId = loader.$$id;
200
244
  const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
201
245
  params: ctx.params,
246
+ routeParams: (ctx.params ?? {}) as Record<string, string>,
202
247
  request: ctx.request,
203
248
  searchParams: ctx.searchParams,
249
+ search: (ctx as any).search,
204
250
  pathname: ctx.pathname,
205
251
  url: ctx.url,
206
252
  env: ctx.env,
207
253
  var: ctx.var,
208
254
  get: ctx.get,
209
255
  use: <TDep, TDepParams = any>(
210
- dep: LoaderDefinition<TDep, TDepParams>
256
+ dep: LoaderDefinition<TDep, TDepParams>,
211
257
  ): Promise<TDep> => {
212
- // Recursive call - will start dep loader if not already started
213
- return ctx.use(dep);
258
+ return useLoader(dep, currentLoaderId);
214
259
  },
215
- // Default to GET for loaders called through route handlers
216
260
  method: "GET",
217
261
  body: undefined,
262
+ reverse: ctx.reverse as LoaderContext["reverse"],
218
263
  };
219
264
 
220
- // Start loader execution with tracking
221
- const doneLoader = track(`loader:${loader.$$id}`);
265
+ const doneLoader = track(`loader:${loader.$$id}`, 2);
222
266
  const promise = Promise.resolve(
223
- loaderFn(loaderCtx as LoaderContext<any, TEnv>)
267
+ loaderFn(loaderCtx as LoaderContext<any, TEnv>),
224
268
  ).finally(() => {
269
+ pendingLoaders.delete(loader.$$id);
225
270
  doneLoader();
226
271
  });
227
272
 
228
- // Memoize for subsequent calls
229
273
  loaderPromises.set(loader.$$id, promise);
230
-
231
274
  return promise;
232
- }) as typeof ctx.use;
275
+ }
276
+
277
+ return useLoader;
233
278
  }
234
279
 
235
280
  /**
236
- * Set up ctx.use() for proactive caching (silent mode).
237
- * Handles are silently ignored (no push to HandleStore).
238
- * Loaders work normally but with fresh memoization.
281
+ * Set up the use() method on handler context to access loaders and handles.
239
282
  *
240
- * This prevents duplicate handle data (breadcrumbs, meta) from being
241
- * pushed to the response stream during background proactive caching.
283
+ * For loaders: Lazily runs loaders, memoizes results per request.
284
+ * For handles: Returns a push function bound to the current segment.
285
+ *
286
+ * Includes cycle detection: tracks dependency edges between loaders and
287
+ * throws on circular dependencies to prevent deadlocks.
242
288
  */
243
- export function setupLoaderAccessSilent<TEnv>(
289
+ export function setupLoaderAccess<TEnv>(
244
290
  ctx: HandlerContext<any, TEnv>,
245
- loaderPromises: Map<string, Promise<any>>
291
+ loaderPromises: Map<string, Promise<any>>,
246
292
  ): void {
293
+ // Eagerly capture the HandleStore at setup time (before pipeline async ops).
294
+ // In workerd/Cloudflare, dynamic imports and fetch() in the match pipeline
295
+ // can disrupt AsyncLocalStorage, causing getRequestContext() to return
296
+ // undefined when handlers later call ctx.use(handle). Capturing early
297
+ // ensures the store reference survives ALS disruption.
298
+ const handleStoreRef = _getRequestContext()?._handleStore;
299
+
300
+ const useLoader = createLoaderExecutor(ctx, loaderPromises);
301
+
247
302
  ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
248
- // Handle case: return a no-op push function
249
303
  if (isHandle(item)) {
250
- // Silent mode - return a function that does nothing
251
- return (_dataOrFn: unknown) => {
252
- // Intentionally empty - don't push handle data during proactive caching
304
+ const handle = item;
305
+ const store = handleStoreRef;
306
+ const segmentId = (ctx as InternalHandlerContext<any, TEnv>)
307
+ ._currentSegmentId;
308
+
309
+ if (!segmentId) {
310
+ throw new Error(
311
+ `Handle "${handle.$$id}" used outside of handler context. ` +
312
+ `Handles must be used within route/layout handlers.`,
313
+ );
314
+ }
315
+
316
+ return (
317
+ dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>),
318
+ ) => {
319
+ if (!store) return;
320
+
321
+ const valueOrPromise =
322
+ typeof dataOrFn === "function"
323
+ ? (dataOrFn as () => Promise<unknown>)()
324
+ : dataOrFn;
325
+
326
+ store.push(handle.$$id, segmentId, valueOrPromise);
253
327
  };
254
328
  }
255
329
 
256
- // Loader case: same as setupLoaderAccess
257
- const loader = item as LoaderDefinition<any, any>;
330
+ return useLoader(item as LoaderDefinition<any, any>, null);
331
+ }) as typeof ctx.use;
332
+ }
258
333
 
259
- // Return cached promise if already started
260
- if (loaderPromises.has(loader.$$id)) {
261
- return loaderPromises.get(loader.$$id);
262
- }
334
+ /**
335
+ * Set up ctx.use() for pre-rendering (build-time).
336
+ * Handles push to HandleStore; loaders throw with a clear error.
337
+ */
338
+ export function setupBuildUse<TEnv>(ctx: HandlerContext<any, TEnv>): void {
339
+ // Eagerly capture the HandleStore (same ALS protection as setupLoaderAccess).
340
+ const handleStoreRef = _getRequestContext()?._handleStore;
263
341
 
264
- // Get loader function
265
- let loaderFn = loader.fn;
266
- if (!loaderFn) {
267
- const fetchable = getFetchableLoader(loader.$$id);
268
- if (fetchable) {
269
- loaderFn = fetchable.fn;
342
+ ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
343
+ // Handle case: return a push function bound to the current segment
344
+ if (isHandle(item)) {
345
+ const handle = item;
346
+ const store = handleStoreRef;
347
+ const segmentId = (ctx as InternalHandlerContext<any, TEnv>)
348
+ ._currentSegmentId;
349
+
350
+ if (!segmentId) {
351
+ throw new Error(
352
+ `Handle "${handle.$$id}" used outside of handler context. ` +
353
+ `Handles must be used within route/layout handlers.`,
354
+ );
270
355
  }
271
- }
272
356
 
273
- if (!loaderFn) {
274
- throw new Error(
275
- `Loader "${loader.$$id}" has no function. This usually means the loader was defined without "use server" and the function was not included in the build.`
276
- );
357
+ return (
358
+ dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>),
359
+ ) => {
360
+ if (!store) return;
361
+
362
+ const valueOrPromise =
363
+ typeof dataOrFn === "function"
364
+ ? (dataOrFn as () => Promise<unknown>)()
365
+ : dataOrFn;
366
+
367
+ store.push(handle.$$id, segmentId, valueOrPromise);
368
+ };
277
369
  }
278
370
 
279
- // Create loader context with recursive use() support
280
- const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
281
- params: ctx.params,
282
- request: ctx.request,
283
- searchParams: ctx.searchParams,
284
- pathname: ctx.pathname,
285
- url: ctx.url,
286
- env: ctx.env,
287
- var: ctx.var,
288
- get: ctx.get,
289
- use: <TDep, TDepParams = any>(
290
- dep: LoaderDefinition<TDep, TDepParams>
291
- ): Promise<TDep> => {
292
- return ctx.use(dep);
293
- },
294
- method: "GET",
295
- body: undefined,
296
- };
371
+ // Loader case: not available during pre-rendering
372
+ throw new Error(
373
+ "Loaders are not available during pre-rendering. " +
374
+ "Use them on parent layouts with cache() for request-time data, " +
375
+ "or use a passthrough prerender handler.",
376
+ );
377
+ }) as typeof ctx.use;
378
+ }
297
379
 
298
- // Start loader execution with tracking
299
- const doneLoader = track(`loader:${loader.$$id}`);
300
- const promise = Promise.resolve(
301
- loaderFn(loaderCtx as LoaderContext<any, TEnv>)
302
- ).finally(() => {
303
- doneLoader();
304
- });
380
+ /**
381
+ * Set up ctx.use() for proactive caching (silent mode).
382
+ * Handles are silently ignored (no push to HandleStore).
383
+ * Loaders work normally but with fresh memoization and cycle detection.
384
+ *
385
+ * This prevents duplicate handle data (breadcrumbs, meta) from being
386
+ * pushed to the response stream during background proactive caching.
387
+ */
388
+ export function setupLoaderAccessSilent<TEnv>(
389
+ ctx: HandlerContext<any, TEnv>,
390
+ loaderPromises: Map<string, Promise<any>>,
391
+ ): void {
392
+ const useLoader = createLoaderExecutor(ctx, loaderPromises);
305
393
 
306
- loaderPromises.set(loader.$$id, promise);
307
- return promise;
394
+ ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
395
+ if (isHandle(item)) {
396
+ // Silent mode - return a no-op so handle data is not pushed during caching
397
+ return (_dataOrFn: unknown) => {};
398
+ }
399
+
400
+ return useLoader(item as LoaderDefinition<any, any>, null);
308
401
  }) as typeof ctx.use;
309
402
  }
310
403
 
@@ -320,7 +413,7 @@ export function setupLoaderAccessSilent<TEnv>(
320
413
  export async function revalidate<T>(
321
414
  shouldRevalidate: () => Promise<boolean>,
322
415
  onRevalidate: () => Promise<T>,
323
- onSkip: () => T
416
+ onSkip: () => T,
324
417
  ): Promise<T> {
325
418
  const needsRevalidation = await shouldRevalidate();
326
419
  return needsRevalidation ? await onRevalidate() : onSkip();