@rangojs/router 0.0.0-experimental.7 → 0.0.0-experimental.71

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