@rangojs/router 0.0.0-experimental.10 → 0.0.0-experimental.100

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 (329) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +1037 -4
  3. package/dist/bin/rango.js +1619 -157
  4. package/dist/vite/index.js +5762 -2301
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +71 -63
  7. package/skills/breadcrumbs/SKILL.md +252 -0
  8. package/skills/cache-guide/SKILL.md +294 -0
  9. package/skills/caching/SKILL.md +93 -23
  10. package/skills/composability/SKILL.md +172 -0
  11. package/skills/debug-manifest/SKILL.md +12 -8
  12. package/skills/document-cache/SKILL.md +18 -16
  13. package/skills/fonts/SKILL.md +6 -4
  14. package/skills/handler-use/SKILL.md +364 -0
  15. package/skills/hooks/SKILL.md +367 -71
  16. package/skills/host-router/SKILL.md +218 -0
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +176 -8
  19. package/skills/layout/SKILL.md +124 -3
  20. package/skills/links/SKILL.md +304 -25
  21. package/skills/loader/SKILL.md +474 -47
  22. package/skills/middleware/SKILL.md +207 -37
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +15 -11
  26. package/skills/parallel/SKILL.md +272 -1
  27. package/skills/prerender/SKILL.md +467 -65
  28. package/skills/rango/SKILL.md +89 -21
  29. package/skills/response-routes/SKILL.md +152 -91
  30. package/skills/route/SKILL.md +305 -14
  31. package/skills/router-setup/SKILL.md +210 -32
  32. package/skills/server-actions/SKILL.md +739 -0
  33. package/skills/streams-and-websockets/SKILL.md +283 -0
  34. package/skills/theme/SKILL.md +9 -8
  35. package/skills/typesafety/SKILL.md +333 -86
  36. package/skills/use-cache/SKILL.md +324 -0
  37. package/skills/view-transitions/SKILL.md +212 -0
  38. package/src/__internal.ts +102 -4
  39. package/src/bin/rango.ts +312 -15
  40. package/src/browser/action-coordinator.ts +97 -0
  41. package/src/browser/action-response-classifier.ts +99 -0
  42. package/src/browser/app-shell.ts +52 -0
  43. package/src/browser/app-version.ts +14 -0
  44. package/src/browser/event-controller.ts +136 -68
  45. package/src/browser/history-state.ts +80 -0
  46. package/src/browser/intercept-utils.ts +52 -0
  47. package/src/browser/link-interceptor.ts +24 -4
  48. package/src/browser/logging.ts +55 -0
  49. package/src/browser/merge-segment-loaders.ts +20 -12
  50. package/src/browser/navigation-bridge.ts +374 -561
  51. package/src/browser/navigation-client.ts +228 -70
  52. package/src/browser/navigation-store.ts +97 -55
  53. package/src/browser/navigation-transaction.ts +297 -0
  54. package/src/browser/network-error-handler.ts +61 -0
  55. package/src/browser/partial-update.ts +376 -315
  56. package/src/browser/prefetch/cache.ts +314 -0
  57. package/src/browser/prefetch/fetch.ts +282 -0
  58. package/src/browser/prefetch/observer.ts +65 -0
  59. package/src/browser/prefetch/policy.ts +48 -0
  60. package/src/browser/prefetch/queue.ts +191 -0
  61. package/src/browser/prefetch/resource-ready.ts +77 -0
  62. package/src/browser/rango-state.ts +152 -0
  63. package/src/browser/react/Link.tsx +255 -71
  64. package/src/browser/react/NavigationProvider.tsx +152 -24
  65. package/src/browser/react/context.ts +11 -0
  66. package/src/browser/react/filter-segment-order.ts +55 -0
  67. package/src/browser/react/index.ts +15 -12
  68. package/src/browser/react/location-state-shared.ts +95 -53
  69. package/src/browser/react/location-state.ts +60 -15
  70. package/src/browser/react/mount-context.ts +6 -1
  71. package/src/browser/react/nonce-context.ts +23 -0
  72. package/src/browser/react/shallow-equal.ts +27 -0
  73. package/src/browser/react/use-action.ts +29 -51
  74. package/src/browser/react/use-client-cache.ts +5 -3
  75. package/src/browser/react/use-handle.ts +30 -120
  76. package/src/browser/react/use-link-status.ts +6 -5
  77. package/src/browser/react/use-navigation.ts +44 -65
  78. package/src/browser/react/use-params.ts +78 -0
  79. package/src/browser/react/use-pathname.ts +47 -0
  80. package/src/browser/react/use-reverse.ts +99 -0
  81. package/src/browser/react/use-router.ts +83 -0
  82. package/src/browser/react/use-search-params.ts +56 -0
  83. package/src/browser/react/use-segments.ts +85 -99
  84. package/src/browser/response-adapter.ts +73 -0
  85. package/src/browser/rsc-router.tsx +246 -64
  86. package/src/browser/scroll-restoration.ts +127 -52
  87. package/src/browser/segment-reconciler.ts +243 -0
  88. package/src/browser/segment-structure-assert.ts +16 -0
  89. package/src/browser/server-action-bridge.ts +510 -603
  90. package/src/browser/shallow.ts +6 -1
  91. package/src/browser/types.ts +158 -48
  92. package/src/browser/validate-redirect-origin.ts +29 -0
  93. package/src/build/generate-manifest.ts +84 -23
  94. package/src/build/generate-route-types.ts +39 -828
  95. package/src/build/index.ts +4 -5
  96. package/src/build/route-trie.ts +85 -32
  97. package/src/build/route-types/ast-helpers.ts +25 -0
  98. package/src/build/route-types/ast-route-extraction.ts +98 -0
  99. package/src/build/route-types/codegen.ts +102 -0
  100. package/src/build/route-types/include-resolution.ts +418 -0
  101. package/src/build/route-types/param-extraction.ts +48 -0
  102. package/src/build/route-types/per-module-writer.ts +128 -0
  103. package/src/build/route-types/router-processing.ts +618 -0
  104. package/src/build/route-types/scan-filter.ts +85 -0
  105. package/src/build/runtime-discovery.ts +231 -0
  106. package/src/cache/background-task.ts +34 -0
  107. package/src/cache/cache-key-utils.ts +44 -0
  108. package/src/cache/cache-policy.ts +125 -0
  109. package/src/cache/cache-runtime.ts +342 -0
  110. package/src/cache/cache-scope.ts +167 -307
  111. package/src/cache/cf/cf-cache-store.ts +573 -21
  112. package/src/cache/cf/index.ts +13 -3
  113. package/src/cache/document-cache.ts +116 -77
  114. package/src/cache/handle-capture.ts +81 -0
  115. package/src/cache/handle-snapshot.ts +41 -0
  116. package/src/cache/index.ts +1 -15
  117. package/src/cache/memory-segment-store.ts +191 -13
  118. package/src/cache/profile-registry.ts +73 -0
  119. package/src/cache/read-through-swr.ts +134 -0
  120. package/src/cache/segment-codec.ts +256 -0
  121. package/src/cache/taint.ts +153 -0
  122. package/src/cache/types.ts +72 -122
  123. package/src/client.rsc.tsx +6 -1
  124. package/src/client.tsx +118 -302
  125. package/src/component-utils.ts +4 -4
  126. package/src/components/DefaultDocument.tsx +5 -1
  127. package/src/context-var.ts +156 -0
  128. package/src/debug.ts +19 -9
  129. package/src/errors.ts +77 -7
  130. package/src/handle.ts +55 -10
  131. package/src/handles/MetaTags.tsx +73 -20
  132. package/src/handles/breadcrumbs.ts +66 -0
  133. package/src/handles/index.ts +1 -0
  134. package/src/handles/meta.ts +30 -13
  135. package/src/host/cookie-handler.ts +21 -15
  136. package/src/host/errors.ts +8 -8
  137. package/src/host/index.ts +4 -7
  138. package/src/host/pattern-matcher.ts +27 -27
  139. package/src/host/router.ts +61 -39
  140. package/src/host/testing.ts +8 -8
  141. package/src/host/types.ts +15 -7
  142. package/src/host/utils.ts +1 -1
  143. package/src/href-client.ts +65 -45
  144. package/src/index.rsc.ts +138 -21
  145. package/src/index.ts +206 -51
  146. package/src/internal-debug.ts +11 -0
  147. package/src/loader.rsc.ts +25 -143
  148. package/src/loader.ts +27 -10
  149. package/src/network-error-thrower.tsx +3 -1
  150. package/src/outlet-context.ts +1 -1
  151. package/src/outlet-provider.tsx +45 -0
  152. package/src/prerender/param-hash.ts +4 -2
  153. package/src/prerender/store.ts +159 -13
  154. package/src/prerender.ts +397 -29
  155. package/src/response-utils.ts +28 -0
  156. package/src/reverse.ts +231 -121
  157. package/src/root-error-boundary.tsx +41 -29
  158. package/src/route-content-wrapper.tsx +7 -4
  159. package/src/route-definition/dsl-helpers.ts +1134 -0
  160. package/src/route-definition/helper-factories.ts +200 -0
  161. package/src/route-definition/helpers-types.ts +483 -0
  162. package/src/route-definition/index.ts +55 -0
  163. package/src/route-definition/redirect.ts +101 -0
  164. package/src/route-definition/resolve-handler-use.ts +155 -0
  165. package/src/route-definition.ts +1 -1431
  166. package/src/route-map-builder.ts +162 -123
  167. package/src/route-name.ts +53 -0
  168. package/src/route-types.ts +66 -9
  169. package/src/router/content-negotiation.ts +215 -0
  170. package/src/router/debug-manifest.ts +72 -0
  171. package/src/router/error-handling.ts +9 -9
  172. package/src/router/find-match.ts +160 -0
  173. package/src/router/handler-context.ts +418 -86
  174. package/src/router/intercept-resolution.ts +35 -20
  175. package/src/router/lazy-includes.ts +237 -0
  176. package/src/router/loader-resolution.ts +359 -128
  177. package/src/router/logging.ts +251 -0
  178. package/src/router/manifest.ts +98 -32
  179. package/src/router/match-api.ts +196 -261
  180. package/src/router/match-context.ts +4 -2
  181. package/src/router/match-handlers.ts +441 -0
  182. package/src/router/match-middleware/background-revalidation.ts +108 -93
  183. package/src/router/match-middleware/cache-lookup.ts +415 -86
  184. package/src/router/match-middleware/cache-store.ts +91 -29
  185. package/src/router/match-middleware/intercept-resolution.ts +48 -21
  186. package/src/router/match-middleware/segment-resolution.ts +73 -9
  187. package/src/router/match-pipelines.ts +10 -45
  188. package/src/router/match-result.ts +154 -35
  189. package/src/router/metrics.ts +240 -15
  190. package/src/router/middleware-cookies.ts +55 -0
  191. package/src/router/middleware-types.ts +209 -0
  192. package/src/router/middleware.ts +373 -371
  193. package/src/router/navigation-snapshot.ts +182 -0
  194. package/src/router/pattern-matching.ts +292 -52
  195. package/src/router/prerender-match.ts +502 -0
  196. package/src/router/preview-match.ts +98 -0
  197. package/src/router/request-classification.ts +310 -0
  198. package/src/router/revalidation.ts +152 -39
  199. package/src/router/route-snapshot.ts +245 -0
  200. package/src/router/router-context.ts +41 -21
  201. package/src/router/router-interfaces.ts +484 -0
  202. package/src/router/router-options.ts +618 -0
  203. package/src/router/router-registry.ts +24 -0
  204. package/src/router/segment-resolution/fresh.ts +756 -0
  205. package/src/router/segment-resolution/helpers.ts +268 -0
  206. package/src/router/segment-resolution/loader-cache.ts +199 -0
  207. package/src/router/segment-resolution/revalidation.ts +1407 -0
  208. package/src/router/segment-resolution/static-store.ts +67 -0
  209. package/src/router/segment-resolution.ts +21 -1315
  210. package/src/router/segment-wrappers.ts +291 -0
  211. package/src/router/substitute-pattern-params.ts +56 -0
  212. package/src/router/telemetry-otel.ts +299 -0
  213. package/src/router/telemetry.ts +300 -0
  214. package/src/router/timeout.ts +148 -0
  215. package/src/router/trie-matching.ts +111 -39
  216. package/src/router/types.ts +17 -9
  217. package/src/router/url-params.ts +49 -0
  218. package/src/router.ts +642 -2011
  219. package/src/rsc/handler-context.ts +45 -0
  220. package/src/rsc/handler.ts +864 -1114
  221. package/src/rsc/helpers.ts +181 -19
  222. package/src/rsc/index.ts +0 -20
  223. package/src/rsc/loader-fetch.ts +229 -0
  224. package/src/rsc/manifest-init.ts +90 -0
  225. package/src/rsc/nonce.ts +14 -0
  226. package/src/rsc/origin-guard.ts +141 -0
  227. package/src/rsc/progressive-enhancement.ts +395 -0
  228. package/src/rsc/response-error.ts +37 -0
  229. package/src/rsc/response-route-handler.ts +360 -0
  230. package/src/rsc/rsc-rendering.ts +256 -0
  231. package/src/rsc/runtime-warnings.ts +42 -0
  232. package/src/rsc/server-action.ts +360 -0
  233. package/src/rsc/ssr-setup.ts +128 -0
  234. package/src/rsc/types.ts +52 -11
  235. package/src/search-params.ts +230 -0
  236. package/src/segment-content-promise.ts +67 -0
  237. package/src/segment-loader-promise.ts +122 -0
  238. package/src/segment-system.tsx +187 -38
  239. package/src/server/context.ts +333 -59
  240. package/src/server/cookie-store.ts +190 -0
  241. package/src/server/fetchable-loader-store.ts +37 -0
  242. package/src/server/handle-store.ts +113 -15
  243. package/src/server/loader-registry.ts +24 -64
  244. package/src/server/request-context.ts +603 -109
  245. package/src/server.ts +35 -155
  246. package/src/ssr/index.tsx +107 -30
  247. package/src/static-handler.ts +126 -0
  248. package/src/theme/ThemeProvider.tsx +21 -15
  249. package/src/theme/ThemeScript.tsx +5 -5
  250. package/src/theme/constants.ts +5 -2
  251. package/src/theme/index.ts +4 -14
  252. package/src/theme/theme-context.ts +4 -30
  253. package/src/theme/theme-script.ts +21 -18
  254. package/src/types/boundaries.ts +158 -0
  255. package/src/types/cache-types.ts +198 -0
  256. package/src/types/error-types.ts +192 -0
  257. package/src/types/global-namespace.ts +100 -0
  258. package/src/types/handler-context.ts +764 -0
  259. package/src/types/index.ts +88 -0
  260. package/src/types/loader-types.ts +209 -0
  261. package/src/types/request-scope.ts +126 -0
  262. package/src/types/route-config.ts +170 -0
  263. package/src/types/route-entry.ts +120 -0
  264. package/src/types/segments.ts +167 -0
  265. package/src/types.ts +1 -1757
  266. package/src/urls/include-helper.ts +207 -0
  267. package/src/urls/index.ts +53 -0
  268. package/src/urls/path-helper-types.ts +372 -0
  269. package/src/urls/path-helper.ts +364 -0
  270. package/src/urls/pattern-types.ts +107 -0
  271. package/src/urls/response-types.ts +108 -0
  272. package/src/urls/type-extraction.ts +372 -0
  273. package/src/urls/urls-function.ts +98 -0
  274. package/src/urls.ts +1 -1282
  275. package/src/use-loader.tsx +161 -81
  276. package/src/vite/debug.ts +184 -0
  277. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  278. package/src/vite/discovery/discover-routers.ts +376 -0
  279. package/src/vite/discovery/gate-state.ts +171 -0
  280. package/src/vite/discovery/prerender-collection.ts +486 -0
  281. package/src/vite/discovery/route-types-writer.ts +258 -0
  282. package/src/vite/discovery/self-gen-tracking.ts +73 -0
  283. package/src/vite/discovery/state.ts +117 -0
  284. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  285. package/src/vite/index.ts +15 -2063
  286. package/src/vite/plugin-types.ts +103 -0
  287. package/src/vite/plugins/cjs-to-esm.ts +98 -0
  288. package/src/vite/plugins/client-ref-dedup.ts +131 -0
  289. package/src/vite/plugins/client-ref-hashing.ts +117 -0
  290. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  291. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  292. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  293. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +107 -64
  294. package/src/vite/plugins/expose-id-utils.ts +299 -0
  295. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  296. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  297. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  298. package/src/vite/plugins/expose-ids/router-transform.ts +127 -0
  299. package/src/vite/plugins/expose-ids/types.ts +45 -0
  300. package/src/vite/plugins/expose-internal-ids.ts +816 -0
  301. package/src/vite/plugins/performance-tracks.ts +96 -0
  302. package/src/vite/plugins/refresh-cmd.ts +127 -0
  303. package/src/vite/plugins/use-cache-transform.ts +336 -0
  304. package/src/vite/plugins/version-injector.ts +109 -0
  305. package/src/vite/plugins/version-plugin.ts +266 -0
  306. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  307. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  308. package/src/vite/rango.ts +497 -0
  309. package/src/vite/router-discovery.ts +1423 -0
  310. package/src/vite/utils/ast-handler-extract.ts +517 -0
  311. package/src/vite/utils/banner.ts +36 -0
  312. package/src/vite/utils/bundle-analysis.ts +137 -0
  313. package/src/vite/utils/manifest-utils.ts +70 -0
  314. package/src/vite/utils/package-resolution.ts +161 -0
  315. package/src/vite/utils/prerender-utils.ts +222 -0
  316. package/src/vite/utils/shared-utils.ts +170 -0
  317. package/CLAUDE.md +0 -43
  318. package/src/browser/lru-cache.ts +0 -69
  319. package/src/browser/request-controller.ts +0 -164
  320. package/src/cache/memory-store.ts +0 -253
  321. package/src/href-context.ts +0 -33
  322. package/src/router.gen.ts +0 -6
  323. package/src/urls.gen.ts +0 -8
  324. package/src/vite/expose-handle-id.ts +0 -209
  325. package/src/vite/expose-loader-id.ts +0 -426
  326. package/src/vite/expose-location-state-id.ts +0 -177
  327. package/src/vite/expose-prerender-handler-id.ts +0 -429
  328. package/src/vite/package-resolution.ts +0 -125
  329. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -7,6 +7,7 @@
7
7
  import type { ReactNode } from "react";
8
8
  import { track } from "../server/context";
9
9
  import type { EntryData } from "../server/context";
10
+ import { contextGet } from "../context-var.js";
10
11
  import type {
11
12
  ResolvedSegment,
12
13
  HandlerContext,
@@ -19,10 +20,12 @@ import type {
19
20
  ErrorInfo,
20
21
  } from "../types";
21
22
  import type { LoaderRevalidationResult, ActionContext } from "./types";
22
- import { isHandle, type Handle } from "../handle.js";
23
- import type { HandleStore } from "../server/handle-store.js";
24
- import { getFetchableLoader } from "../loader.rsc.js";
25
- import { getRequestContext } from "../server/request-context.js";
23
+ import { isHandle, collectHandleData, type Handle } from "../handle.js";
24
+ import { buildHandleSnapshot } from "../server/handle-store.js";
25
+ import { getFetchableLoader } from "../server/fetchable-loader-store.js";
26
+ import { _getRequestContext } from "../server/request-context.js";
27
+ import { isInsideLoaderScope } from "../server/context.js";
28
+ import { debugLog } from "./logging.js";
26
29
 
27
30
  /**
28
31
  * Internal callback signature for loader error notifications.
@@ -35,7 +38,7 @@ export type LoaderErrorCallback = (
35
38
  segmentId: string;
36
39
  loaderName: string;
37
40
  handledByBoundary: boolean;
38
- }
41
+ },
39
42
  ) => void;
40
43
 
41
44
  /**
@@ -54,14 +57,14 @@ export function wrapLoaderWithErrorHandling<T>(
54
57
  segmentId: string,
55
58
  pathname: string,
56
59
  findNearestErrorBoundary: (
57
- entry: EntryData | null
60
+ entry: EntryData | null,
58
61
  ) => ReactNode | ErrorBoundaryHandler | null,
59
62
  createErrorInfo: (
60
63
  error: unknown,
61
64
  segmentId: string,
62
- segmentType: ErrorInfo["segmentType"]
65
+ segmentType: ErrorInfo["segmentType"],
63
66
  ) => ErrorInfo,
64
- onError?: LoaderErrorCallback
67
+ onError?: LoaderErrorCallback,
65
68
  ): Promise<LoaderDataResult<T>> {
66
69
  // Extract loader name from segmentId (format: "M1L0D0.loaderName")
67
70
  const loaderName = segmentId.split(".").pop() || "unknown";
@@ -72,7 +75,7 @@ export function wrapLoaderWithErrorHandling<T>(
72
75
  __loaderResult: true,
73
76
  ok: true,
74
77
  data,
75
- })
78
+ }),
76
79
  )
77
80
  .catch((error): LoaderDataResult<T> => {
78
81
  // Find nearest error boundary
@@ -111,10 +114,10 @@ export function wrapLoaderWithErrorHandling<T>(
111
114
  renderedFallback = fallback;
112
115
  }
113
116
 
114
- console.log(
115
- `[Router] Loader error wrapped with boundary fallback in ${segmentId}:`,
116
- errorInfo.message
117
- );
117
+ debugLog("loader", "loader error wrapped with boundary fallback", {
118
+ segmentId,
119
+ message: errorInfo.message,
120
+ });
118
121
 
119
122
  return {
120
123
  __loaderResult: true,
@@ -126,61 +129,103 @@ export function wrapLoaderWithErrorHandling<T>(
126
129
  }
127
130
 
128
131
  /**
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.
132
+ * Detect cycles in the loader dependency graph using DFS from a given node.
133
+ * Returns the cycle path (array of loader IDs forming the cycle) if one exists,
134
+ * or null if no cycle is found.
133
135
  */
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
- };
136
+ function detectLoaderCycle(
137
+ from: string,
138
+ to: string,
139
+ dependsOn: Map<string, Set<string>>,
140
+ ): string[] | null {
141
+ // If `to` can reach `from` via the dependency graph, adding the edge
142
+ // from -> to creates a cycle. We search from `to` looking for `from`.
143
+ const visited = new Set<string>();
144
+ const path: string[] = [from, to];
145
+
146
+ function dfs(current: string): string[] | null {
147
+ if (current === from) {
148
+ // Found a cycle: return the path leading back to `from`
149
+ return path;
150
+ }
151
+ if (visited.has(current)) return null;
152
+ visited.add(current);
142
153
 
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;
154
+ const deps = dependsOn.get(current);
155
+ if (!deps) return null;
150
156
 
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
- }
157
+ for (const dep of deps) {
158
+ path.push(dep);
159
+ const cycle = dfs(dep);
160
+ if (cycle) return cycle;
161
+ path.pop();
162
+ }
163
+ return null;
164
+ }
157
165
 
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;
166
+ return dfs(to);
167
+ }
163
168
 
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;
169
+ /**
170
+ * Creates a memoizing loader executor with cycle detection.
171
+ * Shared by setupLoaderAccess and setupLoaderAccessSilent; only the handle
172
+ * branch differs between the two, so only the loader logic is extracted here.
173
+ *
174
+ * Returns a useLoader(loader, callerLoaderId) function that:
175
+ * - Tracks dependency edges between loaders for cycle detection
176
+ * - Throws immediately (synchronously inside an async fn) on circular deps
177
+ * - Memoizes each loader's promise so it runs at most once per request
178
+ */
179
+ function createLoaderExecutor<TEnv>(
180
+ ctx: HandlerContext<any, TEnv>,
181
+ loaderPromises: Map<string, Promise<any>>,
182
+ ): (
183
+ loader: LoaderDefinition<any, any>,
184
+ callerLoaderId: string | null,
185
+ ) => Promise<any> {
186
+ // Capture RequestContext eagerly for cookie access (ALS protection on Cloudflare)
187
+ const reqCtxRef = _getRequestContext();
188
+
189
+ // Dependency graph: loaderId -> set of loader IDs it directly depends on.
190
+ const dependsOn = new Map<string, Set<string>>();
191
+
192
+ // Loaders whose promises have not yet settled.
193
+ // A dependency on a pending loader that closes a cycle means deadlock.
194
+ const pendingLoaders = new Set<string>();
195
+
196
+ function useLoader(
197
+ loader: LoaderDefinition<any, any>,
198
+ callerLoaderId: string | null,
199
+ ): Promise<any> {
200
+ // Record the dependency edge and check for cycles before running
201
+ if (callerLoaderId !== null) {
202
+ let deps = dependsOn.get(callerLoaderId);
203
+ if (!deps) {
204
+ deps = new Set();
205
+ dependsOn.set(callerLoaderId, deps);
206
+ }
168
207
 
169
- // Push directly - promises will be serialized by RSC and streamed
170
- store.push(handle.$$id, segmentId, valueOrPromise);
171
- };
172
- }
208
+ // Only relevant when the target is still pending (would deadlock)
209
+ if (pendingLoaders.has(loader.$$id)) {
210
+ const cycle = detectLoaderCycle(callerLoaderId, loader.$$id, dependsOn);
211
+ if (cycle) {
212
+ throw new Error(
213
+ `Circular loader dependency detected: ${cycle.join(" -> ")}. ` +
214
+ `Loaders cannot depend on each other in a cycle. ` +
215
+ `Refactor to break the circular dependency.`,
216
+ );
217
+ }
218
+ }
173
219
 
174
- // Loader case: existing behavior
175
- const loader = item as LoaderDefinition<any, any>;
220
+ deps.add(loader.$$id);
221
+ }
176
222
 
177
223
  // Return cached promise if already started
178
224
  if (loaderPromises.has(loader.$$id)) {
179
- return loaderPromises.get(loader.$$id);
225
+ return loaderPromises.get(loader.$$id)!;
180
226
  }
181
227
 
182
228
  // 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
229
  let loaderFn = loader.fn;
185
230
  if (!loaderFn) {
186
231
  const fetchable = getFetchableLoader(loader.$$id);
@@ -189,122 +234,308 @@ export function setupLoaderAccess<TEnv>(
189
234
  }
190
235
  }
191
236
 
192
- // Ensure loader has a function
193
237
  if (!loaderFn) {
194
238
  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.`
239
+ `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
240
  );
197
241
  }
198
242
 
199
- // Create loader context with recursive use() support
243
+ pendingLoaders.add(loader.$$id);
244
+
245
+ const currentLoaderId = loader.$$id;
246
+ const variables = (ctx as InternalHandlerContext<any, TEnv>)._variables;
247
+
248
+ // Capture whether this loader is being started from a DSL loader scope
249
+ // (runInsideLoaderScope in fresh.ts). Handler-invoked loaders are NOT
250
+ // inside loader scope. This determines whether rendered() is allowed.
251
+ const isDslLoader = isInsideLoaderScope();
252
+
253
+ let renderedResolved = false;
254
+ let renderedPromise: Promise<void> | null = null;
255
+
256
+ // Loader functions are always fresh (never cached), so they get an
257
+ // unguarded get that bypasses non-cacheable read guards. This applies
258
+ // to ALL loaders — DSL and handler-called — because the loader
259
+ // function itself always re-executes. Also handles nested deps
260
+ // (loaderA → use(loaderB)) since all share this unguarded get.
200
261
  const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
201
262
  params: ctx.params,
263
+ routeParams: (ctx.params ?? {}) as Record<string, string>,
202
264
  request: ctx.request,
203
265
  searchParams: ctx.searchParams,
266
+ search: (ctx as any).search,
204
267
  pathname: ctx.pathname,
205
268
  url: ctx.url,
269
+ originalUrl: ctx.originalUrl,
206
270
  env: ctx.env,
207
- var: ctx.var,
208
- get: ctx.get,
209
- use: <TDep, TDepParams = any>(
210
- dep: LoaderDefinition<TDep, TDepParams>
211
- ): Promise<TDep> => {
212
- // Recursive call - will start dep loader if not already started
213
- return ctx.use(dep);
214
- },
215
- // Default to GET for loaders called through route handlers
271
+ waitUntil: ctx.waitUntil.bind(ctx),
272
+ executionContext: ctx.executionContext,
273
+ get: ((keyOrVar: any) =>
274
+ contextGet(variables, keyOrVar)) as typeof ctx.get,
275
+ use: ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
276
+ if (isHandle(item)) {
277
+ if (!renderedResolved) {
278
+ throw new Error(
279
+ `ctx.use(handle) in a loader requires "await ctx.rendered()" first. ` +
280
+ `Handle "${item.$$id}" cannot be read until the render tree has settled.`,
281
+ );
282
+ }
283
+ const reqCtx = reqCtxRef ?? _getRequestContext();
284
+ if (!reqCtx) {
285
+ throw new Error(
286
+ `ctx.use(handle) failed: request context not available.`,
287
+ );
288
+ }
289
+ const segmentOrder = reqCtx._renderBarrierSegmentOrder ?? [];
290
+ const snapshot =
291
+ reqCtx._renderBarrierHandleSnapshot ??
292
+ buildHandleSnapshot(reqCtx._handleStore, segmentOrder);
293
+ return collectHandleData(item, snapshot, segmentOrder);
294
+ }
295
+
296
+ // Loader case
297
+ return useLoader(item as LoaderDefinition<any, any>, currentLoaderId);
298
+ }) as LoaderContext["use"],
216
299
  method: "GET",
217
300
  body: undefined,
301
+ reverse: ctx.reverse as LoaderContext["reverse"],
302
+ rendered: (): Promise<void> => {
303
+ // Guard: only DSL loaders may use rendered()
304
+ if (!isDslLoader) {
305
+ throw new Error(
306
+ `ctx.rendered() is only available in DSL loaders (registered via loader() in urls()). ` +
307
+ `Handler-invoked loaders (ctx.use(Loader) inside a handler) cannot use rendered().`,
308
+ );
309
+ }
310
+
311
+ // Guard: reject streaming trees
312
+ const reqCtx = reqCtxRef ?? _getRequestContext();
313
+ if (reqCtx?._treeHasStreaming) {
314
+ throw new Error(
315
+ `ctx.rendered() is not supported when the matched route tree uses loading(). ` +
316
+ `Streaming handlers may not have settled when rendered() resolves. ` +
317
+ `Remove loading() from the route tree or restructure to avoid rendered().`,
318
+ );
319
+ }
320
+
321
+ if (renderedPromise) return renderedPromise;
322
+
323
+ if (!reqCtx) {
324
+ throw new Error(
325
+ `ctx.rendered() failed: request context not available.`,
326
+ );
327
+ }
328
+
329
+ // Bidirectional deadlock check: if a handler already started
330
+ // awaiting this loader, calling rendered() would deadlock.
331
+ if (reqCtx._handlerLoaderDeps?.has(currentLoaderId)) {
332
+ throw new Error(
333
+ `Deadlock: loader "${currentLoaderId}" called ctx.rendered() but a handler ` +
334
+ `is already awaiting this loader via ctx.use(). The handler blocks ` +
335
+ `segment resolution, which blocks the barrier, which blocks this loader. ` +
336
+ `Move the data dependency to a loader-to-loader pattern instead.`,
337
+ );
338
+ }
339
+
340
+ // Register this loader as waiting for the barrier so that
341
+ // setupLoaderAccess can detect deadlocks when a handler
342
+ // tries to await the same loader via ctx.use().
343
+ if (!reqCtx._renderBarrierWaiters) {
344
+ reqCtx._renderBarrierWaiters = new Set();
345
+ }
346
+ reqCtx._renderBarrierWaiters.add(currentLoaderId);
347
+
348
+ renderedPromise = reqCtx._renderBarrier.then(() => {
349
+ renderedResolved = true;
350
+ });
351
+ return renderedPromise;
352
+ },
218
353
  };
219
354
 
220
- // Start loader execution with tracking
221
- const doneLoader = track(`loader:${loader.$$id}`);
355
+ const doneLoader = track(`loader:${loader.$$id}`, 2);
222
356
  const promise = Promise.resolve(
223
- loaderFn(loaderCtx as LoaderContext<any, TEnv>)
357
+ loaderFn(loaderCtx as LoaderContext<any, TEnv>),
224
358
  ).finally(() => {
359
+ pendingLoaders.delete(loader.$$id);
225
360
  doneLoader();
226
361
  });
227
362
 
228
- // Memoize for subsequent calls
229
363
  loaderPromises.set(loader.$$id, promise);
230
-
231
364
  return promise;
232
- }) as typeof ctx.use;
365
+ }
366
+
367
+ return useLoader;
233
368
  }
234
369
 
235
370
  /**
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.
371
+ * Set up the use() method on handler context to access loaders and handles.
239
372
  *
240
- * This prevents duplicate handle data (breadcrumbs, meta) from being
241
- * pushed to the response stream during background proactive caching.
373
+ * For loaders: Lazily runs loaders, memoizes results per request.
374
+ * For handles: Returns a push function bound to the current segment.
375
+ *
376
+ * Includes cycle detection: tracks dependency edges between loaders and
377
+ * throws on circular dependencies to prevent deadlocks.
242
378
  */
243
- export function setupLoaderAccessSilent<TEnv>(
379
+ export function setupLoaderAccess<TEnv>(
244
380
  ctx: HandlerContext<any, TEnv>,
245
- loaderPromises: Map<string, Promise<any>>
381
+ loaderPromises: Map<string, Promise<any>>,
246
382
  ): void {
383
+ // Eagerly capture the request context and HandleStore at setup time
384
+ // (before pipeline async ops). In workerd/Cloudflare, dynamic imports and
385
+ // fetch() in the match pipeline can disrupt AsyncLocalStorage, causing
386
+ // getRequestContext() to return undefined when handlers later call
387
+ // ctx.use(handle). Capturing early ensures references survive ALS disruption.
388
+ const reqCtxRef = _getRequestContext();
389
+ const handleStoreRef = reqCtxRef?._handleStore;
390
+
391
+ const useLoader = createLoaderExecutor(ctx, loaderPromises);
392
+
393
+ // Track whether we're inside a handle push callback. Loaders started
394
+ // from push callbacks (e.g. push(async () => ctx.use(Loader))) do NOT
395
+ // block segment resolution, so they must not be registered as handler
396
+ // dependencies for deadlock detection.
397
+ let insideHandlePush = false;
398
+
247
399
  ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
248
- // Handle case: return a no-op push function
249
400
  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
401
+ const handle = item;
402
+ const store = handleStoreRef;
403
+ const segmentId = (ctx as InternalHandlerContext<any, TEnv>)
404
+ ._currentSegmentId;
405
+
406
+ if (!segmentId) {
407
+ throw new Error(
408
+ `Handle "${handle.$$id}" used outside of handler context. ` +
409
+ `Handles must be used within route/layout handlers.`,
410
+ );
411
+ }
412
+
413
+ return (
414
+ dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>),
415
+ ) => {
416
+ if (!store) return;
417
+
418
+ if (typeof dataOrFn === "function") {
419
+ // Mark scope so ctx.use(loader) calls inside the callback
420
+ // are not registered as handler-to-loader deps.
421
+ insideHandlePush = true;
422
+ try {
423
+ const result = (dataOrFn as () => Promise<unknown>)();
424
+ store.push(handle.$$id, segmentId, result);
425
+ } finally {
426
+ insideHandlePush = false;
427
+ }
428
+ return;
429
+ }
430
+
431
+ store.push(handle.$$id, segmentId, dataOrFn);
253
432
  };
254
433
  }
255
434
 
256
- // Loader case: same as setupLoaderAccess
435
+ // Deadlock guard and handler-to-loader dependency tracking.
436
+ // Skip when inside a DSL loader scope (resolveLoaderData also calls
437
+ // ctx.use() but that's DSL-to-DSL, not handler-to-loader) or when
438
+ // inside a handle push callback (push callbacks don't block segment
439
+ // resolution so they can't cause rendered() deadlocks).
257
440
  const loader = item as LoaderDefinition<any, any>;
258
-
259
- // Return cached promise if already started
260
- if (loaderPromises.has(loader.$$id)) {
261
- return loaderPromises.get(loader.$$id);
441
+ if (!isInsideLoaderScope() && !insideHandlePush) {
442
+ const reqCtx = reqCtxRef ?? _getRequestContext();
443
+ if (reqCtx) {
444
+ // Direction 1: handler awaits loader that already called rendered()
445
+ if (
446
+ loaderPromises.has(loader.$$id) &&
447
+ reqCtx._renderBarrierWaiters?.has(loader.$$id)
448
+ ) {
449
+ throw new Error(
450
+ `Deadlock: handler is awaiting loader "${loader.$$id}" which called ctx.rendered(). ` +
451
+ `The loader is waiting for segment resolution, but the handler blocks resolution. ` +
452
+ `Move the data dependency to a loader-to-loader pattern instead.`,
453
+ );
454
+ }
455
+ // Direction 2: track dep so rendered() can detect the deadlock
456
+ // if the loader calls it later. Skip when the barrier has already
457
+ // resolved — no deadlock is possible (rendered() resolves immediately).
458
+ // _renderBarrierSegmentOrder is undefined before resolution, string[]
459
+ // after. This also prevents false positives from handle push callbacks
460
+ // that resume after their first await (post-barrier-resolution).
461
+ if (reqCtx._renderBarrierSegmentOrder === undefined) {
462
+ if (!reqCtx._handlerLoaderDeps) reqCtx._handlerLoaderDeps = new Set();
463
+ reqCtx._handlerLoaderDeps.add(loader.$$id);
464
+ }
465
+ }
262
466
  }
263
467
 
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;
468
+ return useLoader(loader, null);
469
+ }) as typeof ctx.use;
470
+ }
471
+
472
+ /**
473
+ * Set up ctx.use() for pre-rendering (build-time).
474
+ * Handles push to HandleStore; loaders throw with a clear error.
475
+ */
476
+ export function setupBuildUse<TEnv>(ctx: HandlerContext<any, TEnv>): void {
477
+ // Eagerly capture the HandleStore (same ALS protection as setupLoaderAccess).
478
+ const handleStoreRef = _getRequestContext()?._handleStore;
479
+
480
+ ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
481
+ // Handle case: return a push function bound to the current segment
482
+ if (isHandle(item)) {
483
+ const handle = item;
484
+ const store = handleStoreRef;
485
+ const segmentId = (ctx as InternalHandlerContext<any, TEnv>)
486
+ ._currentSegmentId;
487
+
488
+ if (!segmentId) {
489
+ throw new Error(
490
+ `Handle "${handle.$$id}" used outside of handler context. ` +
491
+ `Handles must be used within route/layout handlers.`,
492
+ );
270
493
  }
271
- }
272
494
 
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
- );
495
+ return (
496
+ dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>),
497
+ ) => {
498
+ if (!store) return;
499
+
500
+ const valueOrPromise =
501
+ typeof dataOrFn === "function"
502
+ ? (dataOrFn as () => Promise<unknown>)()
503
+ : dataOrFn;
504
+
505
+ store.push(handle.$$id, segmentId, valueOrPromise);
506
+ };
277
507
  }
278
508
 
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
- };
509
+ // Loader case: not available during pre-rendering
510
+ throw new Error(
511
+ "Loaders are not available during pre-rendering. " +
512
+ "Use them on parent layouts with cache() for request-time data, " +
513
+ "or use a passthrough prerender handler.",
514
+ );
515
+ }) as typeof ctx.use;
516
+ }
297
517
 
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
- });
518
+ /**
519
+ * Set up ctx.use() for proactive caching (silent mode).
520
+ * Handles are silently ignored (no push to HandleStore).
521
+ * Loaders work normally but with fresh memoization and cycle detection.
522
+ *
523
+ * This prevents duplicate handle data (breadcrumbs, meta) from being
524
+ * pushed to the response stream during background proactive caching.
525
+ */
526
+ export function setupLoaderAccessSilent<TEnv>(
527
+ ctx: HandlerContext<any, TEnv>,
528
+ loaderPromises: Map<string, Promise<any>>,
529
+ ): void {
530
+ const useLoader = createLoaderExecutor(ctx, loaderPromises);
305
531
 
306
- loaderPromises.set(loader.$$id, promise);
307
- return promise;
532
+ ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
533
+ if (isHandle(item)) {
534
+ // Silent mode - return a no-op so handle data is not pushed during caching
535
+ return (_dataOrFn: unknown) => {};
536
+ }
537
+
538
+ return useLoader(item as LoaderDefinition<any, any>, null);
308
539
  }) as typeof ctx.use;
309
540
  }
310
541
 
@@ -320,7 +551,7 @@ export function setupLoaderAccessSilent<TEnv>(
320
551
  export async function revalidate<T>(
321
552
  shouldRevalidate: () => Promise<boolean>,
322
553
  onRevalidate: () => Promise<T>,
323
- onSkip: () => T
554
+ onSkip: () => T,
324
555
  ): Promise<T> {
325
556
  const needsRevalidation = await shouldRevalidate();
326
557
  return needsRevalidation ? await onRevalidate() : onSkip();