@rangojs/router 0.0.0-experimental.0f44aca1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +899 -0
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +5214 -0
  5. package/package.json +176 -0
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +220 -0
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +112 -0
  11. package/skills/document-cache/SKILL.md +182 -0
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +704 -0
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +313 -0
  16. package/skills/layout/SKILL.md +310 -0
  17. package/skills/links/SKILL.md +239 -0
  18. package/skills/loader/SKILL.md +596 -0
  19. package/skills/middleware/SKILL.md +339 -0
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +305 -0
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +118 -0
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +385 -0
  26. package/skills/router-setup/SKILL.md +439 -0
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +79 -0
  29. package/skills/typesafety/SKILL.md +623 -0
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +273 -0
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/event-controller.ts +899 -0
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/index.ts +18 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +141 -0
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +134 -0
  42. package/src/browser/navigation-bridge.ts +645 -0
  43. package/src/browser/navigation-client.ts +215 -0
  44. package/src/browser/navigation-store.ts +806 -0
  45. package/src/browser/navigation-transaction.ts +295 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +550 -0
  48. package/src/browser/prefetch/cache.ts +146 -0
  49. package/src/browser/prefetch/fetch.ts +135 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +42 -0
  52. package/src/browser/prefetch/queue.ts +88 -0
  53. package/src/browser/rango-state.ts +112 -0
  54. package/src/browser/react/Link.tsx +360 -0
  55. package/src/browser/react/NavigationProvider.tsx +386 -0
  56. package/src/browser/react/ScrollRestoration.tsx +94 -0
  57. package/src/browser/react/context.ts +59 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +52 -0
  60. package/src/browser/react/location-state-shared.ts +162 -0
  61. package/src/browser/react/location-state.ts +107 -0
  62. package/src/browser/react/mount-context.ts +37 -0
  63. package/src/browser/react/nonce-context.ts +23 -0
  64. package/src/browser/react/shallow-equal.ts +27 -0
  65. package/src/browser/react/use-action.ts +218 -0
  66. package/src/browser/react/use-client-cache.ts +58 -0
  67. package/src/browser/react/use-handle.ts +162 -0
  68. package/src/browser/react/use-href.tsx +40 -0
  69. package/src/browser/react/use-link-status.ts +135 -0
  70. package/src/browser/react/use-mount.ts +31 -0
  71. package/src/browser/react/use-navigation.ts +99 -0
  72. package/src/browser/react/use-params.ts +65 -0
  73. package/src/browser/react/use-pathname.ts +47 -0
  74. package/src/browser/react/use-router.ts +63 -0
  75. package/src/browser/react/use-search-params.ts +56 -0
  76. package/src/browser/react/use-segments.ts +171 -0
  77. package/src/browser/response-adapter.ts +73 -0
  78. package/src/browser/rsc-router.tsx +431 -0
  79. package/src/browser/scroll-restoration.ts +400 -0
  80. package/src/browser/segment-reconciler.ts +216 -0
  81. package/src/browser/segment-structure-assert.ts +83 -0
  82. package/src/browser/server-action-bridge.ts +667 -0
  83. package/src/browser/shallow.ts +40 -0
  84. package/src/browser/types.ts +538 -0
  85. package/src/browser/validate-redirect-origin.ts +29 -0
  86. package/src/build/generate-manifest.ts +438 -0
  87. package/src/build/generate-route-types.ts +36 -0
  88. package/src/build/index.ts +35 -0
  89. package/src/build/route-trie.ts +265 -0
  90. package/src/build/route-types/ast-helpers.ts +25 -0
  91. package/src/build/route-types/ast-route-extraction.ts +98 -0
  92. package/src/build/route-types/codegen.ts +102 -0
  93. package/src/build/route-types/include-resolution.ts +411 -0
  94. package/src/build/route-types/param-extraction.ts +48 -0
  95. package/src/build/route-types/per-module-writer.ts +128 -0
  96. package/src/build/route-types/router-processing.ts +469 -0
  97. package/src/build/route-types/scan-filter.ts +78 -0
  98. package/src/build/runtime-discovery.ts +231 -0
  99. package/src/cache/background-task.ts +34 -0
  100. package/src/cache/cache-key-utils.ts +44 -0
  101. package/src/cache/cache-policy.ts +125 -0
  102. package/src/cache/cache-runtime.ts +338 -0
  103. package/src/cache/cache-scope.ts +382 -0
  104. package/src/cache/cf/cf-cache-store.ts +540 -0
  105. package/src/cache/cf/index.ts +25 -0
  106. package/src/cache/document-cache.ts +369 -0
  107. package/src/cache/handle-capture.ts +81 -0
  108. package/src/cache/handle-snapshot.ts +41 -0
  109. package/src/cache/index.ts +43 -0
  110. package/src/cache/memory-segment-store.ts +328 -0
  111. package/src/cache/profile-registry.ts +73 -0
  112. package/src/cache/read-through-swr.ts +134 -0
  113. package/src/cache/segment-codec.ts +256 -0
  114. package/src/cache/taint.ts +98 -0
  115. package/src/cache/types.ts +342 -0
  116. package/src/client.rsc.tsx +85 -0
  117. package/src/client.tsx +601 -0
  118. package/src/component-utils.ts +76 -0
  119. package/src/components/DefaultDocument.tsx +27 -0
  120. package/src/context-var.ts +86 -0
  121. package/src/debug.ts +243 -0
  122. package/src/default-error-boundary.tsx +88 -0
  123. package/src/deps/browser.ts +8 -0
  124. package/src/deps/html-stream-client.ts +2 -0
  125. package/src/deps/html-stream-server.ts +2 -0
  126. package/src/deps/rsc.ts +10 -0
  127. package/src/deps/ssr.ts +2 -0
  128. package/src/errors.ts +365 -0
  129. package/src/handle.ts +135 -0
  130. package/src/handles/MetaTags.tsx +246 -0
  131. package/src/handles/breadcrumbs.ts +66 -0
  132. package/src/handles/index.ts +7 -0
  133. package/src/handles/meta.ts +264 -0
  134. package/src/host/cookie-handler.ts +165 -0
  135. package/src/host/errors.ts +97 -0
  136. package/src/host/index.ts +53 -0
  137. package/src/host/pattern-matcher.ts +214 -0
  138. package/src/host/router.ts +352 -0
  139. package/src/host/testing.ts +79 -0
  140. package/src/host/types.ts +146 -0
  141. package/src/host/utils.ts +25 -0
  142. package/src/href-client.ts +222 -0
  143. package/src/index.rsc.ts +233 -0
  144. package/src/index.ts +277 -0
  145. package/src/internal-debug.ts +11 -0
  146. package/src/loader.rsc.ts +89 -0
  147. package/src/loader.ts +64 -0
  148. package/src/network-error-thrower.tsx +23 -0
  149. package/src/outlet-context.ts +15 -0
  150. package/src/outlet-provider.tsx +45 -0
  151. package/src/prerender/param-hash.ts +37 -0
  152. package/src/prerender/store.ts +185 -0
  153. package/src/prerender.ts +463 -0
  154. package/src/reverse.ts +330 -0
  155. package/src/root-error-boundary.tsx +289 -0
  156. package/src/route-content-wrapper.tsx +196 -0
  157. package/src/route-definition/dsl-helpers.ts +934 -0
  158. package/src/route-definition/helper-factories.ts +200 -0
  159. package/src/route-definition/helpers-types.ts +430 -0
  160. package/src/route-definition/index.ts +52 -0
  161. package/src/route-definition/redirect.ts +93 -0
  162. package/src/route-definition.ts +1 -0
  163. package/src/route-map-builder.ts +275 -0
  164. package/src/route-name.ts +53 -0
  165. package/src/route-types.ts +259 -0
  166. package/src/router/content-negotiation.ts +116 -0
  167. package/src/router/debug-manifest.ts +72 -0
  168. package/src/router/error-handling.ts +287 -0
  169. package/src/router/find-match.ts +158 -0
  170. package/src/router/handler-context.ts +451 -0
  171. package/src/router/intercept-resolution.ts +395 -0
  172. package/src/router/lazy-includes.ts +234 -0
  173. package/src/router/loader-resolution.ts +420 -0
  174. package/src/router/logging.ts +248 -0
  175. package/src/router/manifest.ts +267 -0
  176. package/src/router/match-api.ts +620 -0
  177. package/src/router/match-context.ts +266 -0
  178. package/src/router/match-handlers.ts +440 -0
  179. package/src/router/match-middleware/background-revalidation.ts +223 -0
  180. package/src/router/match-middleware/cache-lookup.ts +634 -0
  181. package/src/router/match-middleware/cache-store.ts +295 -0
  182. package/src/router/match-middleware/index.ts +81 -0
  183. package/src/router/match-middleware/intercept-resolution.ts +306 -0
  184. package/src/router/match-middleware/segment-resolution.ts +192 -0
  185. package/src/router/match-pipelines.ts +179 -0
  186. package/src/router/match-result.ts +219 -0
  187. package/src/router/metrics.ts +282 -0
  188. package/src/router/middleware-cookies.ts +55 -0
  189. package/src/router/middleware-types.ts +222 -0
  190. package/src/router/middleware.ts +748 -0
  191. package/src/router/pattern-matching.ts +563 -0
  192. package/src/router/prerender-match.ts +402 -0
  193. package/src/router/preview-match.ts +170 -0
  194. package/src/router/revalidation.ts +289 -0
  195. package/src/router/router-context.ts +316 -0
  196. package/src/router/router-interfaces.ts +452 -0
  197. package/src/router/router-options.ts +592 -0
  198. package/src/router/router-registry.ts +24 -0
  199. package/src/router/segment-resolution/fresh.ts +570 -0
  200. package/src/router/segment-resolution/helpers.ts +263 -0
  201. package/src/router/segment-resolution/loader-cache.ts +198 -0
  202. package/src/router/segment-resolution/revalidation.ts +1239 -0
  203. package/src/router/segment-resolution/static-store.ts +67 -0
  204. package/src/router/segment-resolution.ts +21 -0
  205. package/src/router/segment-wrappers.ts +289 -0
  206. package/src/router/telemetry-otel.ts +299 -0
  207. package/src/router/telemetry.ts +300 -0
  208. package/src/router/timeout.ts +148 -0
  209. package/src/router/trie-matching.ts +239 -0
  210. package/src/router/types.ts +170 -0
  211. package/src/router.ts +1002 -0
  212. package/src/rsc/handler-context.ts +45 -0
  213. package/src/rsc/handler.ts +1089 -0
  214. package/src/rsc/helpers.ts +198 -0
  215. package/src/rsc/index.ts +36 -0
  216. package/src/rsc/loader-fetch.ts +209 -0
  217. package/src/rsc/manifest-init.ts +86 -0
  218. package/src/rsc/nonce.ts +32 -0
  219. package/src/rsc/origin-guard.ts +141 -0
  220. package/src/rsc/progressive-enhancement.ts +379 -0
  221. package/src/rsc/response-error.ts +37 -0
  222. package/src/rsc/response-route-handler.ts +347 -0
  223. package/src/rsc/rsc-rendering.ts +235 -0
  224. package/src/rsc/runtime-warnings.ts +42 -0
  225. package/src/rsc/server-action.ts +348 -0
  226. package/src/rsc/ssr-setup.ts +128 -0
  227. package/src/rsc/types.ts +263 -0
  228. package/src/search-params.ts +230 -0
  229. package/src/segment-system.tsx +454 -0
  230. package/src/server/context.ts +591 -0
  231. package/src/server/cookie-store.ts +190 -0
  232. package/src/server/fetchable-loader-store.ts +37 -0
  233. package/src/server/handle-store.ts +308 -0
  234. package/src/server/loader-registry.ts +133 -0
  235. package/src/server/request-context.ts +914 -0
  236. package/src/server/root-layout.tsx +10 -0
  237. package/src/server/tsconfig.json +14 -0
  238. package/src/server.ts +51 -0
  239. package/src/ssr/index.tsx +365 -0
  240. package/src/static-handler.ts +114 -0
  241. package/src/theme/ThemeProvider.tsx +297 -0
  242. package/src/theme/ThemeScript.tsx +61 -0
  243. package/src/theme/constants.ts +62 -0
  244. package/src/theme/index.ts +48 -0
  245. package/src/theme/theme-context.ts +44 -0
  246. package/src/theme/theme-script.ts +155 -0
  247. package/src/theme/types.ts +182 -0
  248. package/src/theme/use-theme.ts +44 -0
  249. package/src/types/boundaries.ts +158 -0
  250. package/src/types/cache-types.ts +198 -0
  251. package/src/types/error-types.ts +192 -0
  252. package/src/types/global-namespace.ts +100 -0
  253. package/src/types/handler-context.ts +687 -0
  254. package/src/types/index.ts +88 -0
  255. package/src/types/loader-types.ts +183 -0
  256. package/src/types/route-config.ts +170 -0
  257. package/src/types/route-entry.ts +102 -0
  258. package/src/types/segments.ts +148 -0
  259. package/src/types.ts +1 -0
  260. package/src/urls/include-helper.ts +197 -0
  261. package/src/urls/index.ts +53 -0
  262. package/src/urls/path-helper-types.ts +339 -0
  263. package/src/urls/path-helper.ts +329 -0
  264. package/src/urls/pattern-types.ts +95 -0
  265. package/src/urls/response-types.ts +106 -0
  266. package/src/urls/type-extraction.ts +372 -0
  267. package/src/urls/urls-function.ts +98 -0
  268. package/src/urls.ts +1 -0
  269. package/src/use-loader.tsx +354 -0
  270. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  271. package/src/vite/discovery/discover-routers.ts +344 -0
  272. package/src/vite/discovery/prerender-collection.ts +385 -0
  273. package/src/vite/discovery/route-types-writer.ts +258 -0
  274. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  275. package/src/vite/discovery/state.ts +110 -0
  276. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  277. package/src/vite/index.ts +16 -0
  278. package/src/vite/plugin-types.ts +131 -0
  279. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  280. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  281. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  282. package/src/vite/plugins/expose-action-id.ts +365 -0
  283. package/src/vite/plugins/expose-id-utils.ts +287 -0
  284. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  285. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  286. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  287. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  288. package/src/vite/plugins/expose-ids/types.ts +45 -0
  289. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  290. package/src/vite/plugins/refresh-cmd.ts +65 -0
  291. package/src/vite/plugins/use-cache-transform.ts +323 -0
  292. package/src/vite/plugins/version-injector.ts +83 -0
  293. package/src/vite/plugins/version-plugin.ts +254 -0
  294. package/src/vite/plugins/version.d.ts +12 -0
  295. package/src/vite/plugins/virtual-entries.ts +123 -0
  296. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  297. package/src/vite/rango.ts +510 -0
  298. package/src/vite/router-discovery.ts +785 -0
  299. package/src/vite/utils/ast-handler-extract.ts +517 -0
  300. package/src/vite/utils/banner.ts +36 -0
  301. package/src/vite/utils/bundle-analysis.ts +137 -0
  302. package/src/vite/utils/manifest-utils.ts +70 -0
  303. package/src/vite/utils/package-resolution.ts +121 -0
  304. package/src/vite/utils/prerender-utils.ts +189 -0
  305. package/src/vite/utils/shared-utils.ts +169 -0
@@ -0,0 +1,420 @@
1
+ /**
2
+ * Router Loader Resolution
3
+ *
4
+ * Loader execution, memoization, and error handling utilities.
5
+ */
6
+
7
+ import type { ReactNode } from "react";
8
+ import { track } from "../server/context";
9
+ import type { EntryData } from "../server/context";
10
+ import type {
11
+ ResolvedSegment,
12
+ HandlerContext,
13
+ InternalHandlerContext,
14
+ LoaderDefinition,
15
+ LoaderContext,
16
+ LoaderDataResult,
17
+ ErrorBoundaryHandler,
18
+ ErrorBoundaryFallbackProps,
19
+ ErrorInfo,
20
+ } from "../types";
21
+ 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 "../server/fetchable-loader-store.js";
25
+ import { _getRequestContext } from "../server/request-context.js";
26
+ import { debugLog } from "./logging.js";
27
+
28
+ /**
29
+ * Internal callback signature for loader error notifications.
30
+ * This is a simplified callback for internal use in wrapLoaderWithErrorHandling.
31
+ * The caller (wrapLoaderPromise in router.ts) bridges this to the full OnErrorCallback.
32
+ */
33
+ export type LoaderErrorCallback = (
34
+ error: unknown,
35
+ context: {
36
+ segmentId: string;
37
+ loaderName: string;
38
+ handledByBoundary: boolean;
39
+ },
40
+ ) => void;
41
+
42
+ /**
43
+ * Wrap a loader promise with error handling for deferred client-side resolution.
44
+ * Catches errors and converts them to LoaderDataResult objects that include
45
+ * error info and pre-rendered fallback UI when an error boundary is available.
46
+ *
47
+ * @param onError - Optional callback invoked when loader errors occur.
48
+ * This has a simplified signature for internal use - the caller (typically
49
+ * wrapLoaderPromise in router.ts) is responsible for bridging to the full
50
+ * OnErrorCallback with complete request context (request, url, env, etc.).
51
+ */
52
+ export function wrapLoaderWithErrorHandling<T>(
53
+ promise: Promise<T>,
54
+ entry: EntryData,
55
+ segmentId: string,
56
+ pathname: string,
57
+ findNearestErrorBoundary: (
58
+ entry: EntryData | null,
59
+ ) => ReactNode | ErrorBoundaryHandler | null,
60
+ createErrorInfo: (
61
+ error: unknown,
62
+ segmentId: string,
63
+ segmentType: ErrorInfo["segmentType"],
64
+ ) => ErrorInfo,
65
+ onError?: LoaderErrorCallback,
66
+ ): Promise<LoaderDataResult<T>> {
67
+ // Extract loader name from segmentId (format: "M1L0D0.loaderName")
68
+ const loaderName = segmentId.split(".").pop() || "unknown";
69
+
70
+ return Promise.resolve(promise)
71
+ .then(
72
+ (data): LoaderDataResult<T> => ({
73
+ __loaderResult: true,
74
+ ok: true,
75
+ data,
76
+ }),
77
+ )
78
+ .catch((error): LoaderDataResult<T> => {
79
+ // Find nearest error boundary
80
+ const fallback = findNearestErrorBoundary(entry);
81
+
82
+ // Create error info
83
+ const errorInfo = createErrorInfo(error, segmentId, "loader");
84
+
85
+ // Invoke onError callback if provided
86
+ onError?.(error, {
87
+ segmentId,
88
+ loaderName,
89
+ handledByBoundary: !!fallback,
90
+ });
91
+
92
+ if (!fallback) {
93
+ // No error boundary - return error result without fallback
94
+ // Client will throw this error
95
+ return {
96
+ __loaderResult: true,
97
+ ok: false,
98
+ error: errorInfo,
99
+ fallback: null,
100
+ };
101
+ }
102
+
103
+ // Render fallback on server
104
+ let renderedFallback: ReactNode;
105
+ if (typeof fallback === "function") {
106
+ // ErrorBoundaryHandler - call with error info
107
+ const props: ErrorBoundaryFallbackProps = {
108
+ error: errorInfo,
109
+ };
110
+ renderedFallback = fallback(props);
111
+ } else {
112
+ renderedFallback = fallback;
113
+ }
114
+
115
+ debugLog("loader", "loader error wrapped with boundary fallback", {
116
+ segmentId,
117
+ message: errorInfo.message,
118
+ });
119
+
120
+ return {
121
+ __loaderResult: true,
122
+ ok: false,
123
+ error: errorInfo,
124
+ fallback: renderedFallback,
125
+ };
126
+ });
127
+ }
128
+
129
+ /**
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
+ */
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);
151
+
152
+ const deps = dependsOn.get(current);
153
+ if (!deps) return null;
154
+
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
+ }
163
+
164
+ return dfs(to);
165
+ }
166
+
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
+ }
205
+
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
+ }
217
+
218
+ deps.add(loader.$$id);
219
+ }
220
+
221
+ // Return cached promise if already started
222
+ if (loaderPromises.has(loader.$$id)) {
223
+ return loaderPromises.get(loader.$$id)!;
224
+ }
225
+
226
+ // Get loader function - either from loader object or fetchable registry
227
+ let loaderFn = loader.fn;
228
+ if (!loaderFn) {
229
+ const fetchable = getFetchableLoader(loader.$$id);
230
+ if (fetchable) {
231
+ loaderFn = fetchable.fn;
232
+ }
233
+ }
234
+
235
+ if (!loaderFn) {
236
+ throw new Error(
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.`,
238
+ );
239
+ }
240
+
241
+ pendingLoaders.add(loader.$$id);
242
+
243
+ const currentLoaderId = loader.$$id;
244
+ const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
245
+ params: ctx.params,
246
+ routeParams: (ctx.params ?? {}) as Record<string, string>,
247
+ request: ctx.request,
248
+ searchParams: ctx.searchParams,
249
+ search: (ctx as any).search,
250
+ pathname: ctx.pathname,
251
+ url: ctx.url,
252
+ env: ctx.env,
253
+ var: ctx.var,
254
+ get: ctx.get,
255
+ use: <TDep, TDepParams = any>(
256
+ dep: LoaderDefinition<TDep, TDepParams>,
257
+ ): Promise<TDep> => {
258
+ return useLoader(dep, currentLoaderId);
259
+ },
260
+ method: "GET",
261
+ body: undefined,
262
+ reverse: ctx.reverse as LoaderContext["reverse"],
263
+ };
264
+
265
+ const doneLoader = track(`loader:${loader.$$id}`, 2);
266
+ const promise = Promise.resolve(
267
+ loaderFn(loaderCtx as LoaderContext<any, TEnv>),
268
+ ).finally(() => {
269
+ pendingLoaders.delete(loader.$$id);
270
+ doneLoader();
271
+ });
272
+
273
+ loaderPromises.set(loader.$$id, promise);
274
+ return promise;
275
+ }
276
+
277
+ return useLoader;
278
+ }
279
+
280
+ /**
281
+ * Set up the use() method on handler context to access loaders and handles.
282
+ *
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.
288
+ */
289
+ export function setupLoaderAccess<TEnv>(
290
+ ctx: HandlerContext<any, TEnv>,
291
+ loaderPromises: Map<string, Promise<any>>,
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
+
302
+ ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
303
+ if (isHandle(item)) {
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);
327
+ };
328
+ }
329
+
330
+ return useLoader(item as LoaderDefinition<any, any>, null);
331
+ }) as typeof ctx.use;
332
+ }
333
+
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;
341
+
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
+ );
355
+ }
356
+
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
+ };
369
+ }
370
+
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
+ }
379
+
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);
393
+
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);
401
+ }) as typeof ctx.use;
402
+ }
403
+
404
+ /**
405
+ * Conditional execution based on revalidation
406
+ * Evaluates revalidation logic lazily, then executes appropriate callback
407
+ *
408
+ * @param shouldRevalidate - Async function that determines if revalidation is needed
409
+ * @param onRevalidate - Callback executed if revalidation returns true
410
+ * @param onSkip - Callback executed if revalidation returns false
411
+ * @returns Result from either onRevalidate or onSkip
412
+ */
413
+ export async function revalidate<T>(
414
+ shouldRevalidate: () => Promise<boolean>,
415
+ onRevalidate: () => Promise<T>,
416
+ onSkip: () => T,
417
+ ): Promise<T> {
418
+ const needsRevalidation = await shouldRevalidate();
419
+ return needsRevalidation ? await onRevalidate() : onSkip();
420
+ }
@@ -0,0 +1,248 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
3
+
4
+ // -- Revalidation trace types --
5
+
6
+ export interface RevalidationTraceEntry {
7
+ segmentId: string;
8
+ segmentType: string;
9
+ belongsToRoute: boolean;
10
+ source:
11
+ | "segment-resolution"
12
+ | "cache-hit"
13
+ | "loader"
14
+ | "parallel"
15
+ | "orphan-layout";
16
+ defaultShouldRevalidate: boolean;
17
+ finalShouldRevalidate: boolean;
18
+ reason: string;
19
+ customRevalidators?: number;
20
+ }
21
+
22
+ export interface RevalidationTraceMeta {
23
+ method: string;
24
+ prevUrl: string;
25
+ nextUrl: string;
26
+ routeKey: string;
27
+ isAction: boolean;
28
+ stale?: boolean;
29
+ }
30
+
31
+ export interface RevalidationTrace {
32
+ meta: RevalidationTraceMeta;
33
+ entries: RevalidationTraceEntry[];
34
+ }
35
+
36
+ // -- Log context --
37
+
38
+ interface RouterLogContext {
39
+ requestId: string;
40
+ transactionId: string;
41
+ depth: number;
42
+ revalidationTrace?: RevalidationTrace;
43
+ }
44
+
45
+ interface RouterLogOptions {
46
+ request: Request;
47
+ transaction: string;
48
+ }
49
+
50
+ interface LogDetails {
51
+ [key: string]: unknown;
52
+ }
53
+
54
+ const routerLogContext = new AsyncLocalStorage<RouterLogContext>();
55
+ const requestIds = new WeakMap<Request, string>();
56
+
57
+ let requestCounter = 0;
58
+ let transactionCounter = 0;
59
+
60
+ function nextId(prefix: string, counter: number): string {
61
+ return `${prefix}${counter.toString(36)}`;
62
+ }
63
+
64
+ function getHeaderRequestId(request: Request): string | null {
65
+ const candidate =
66
+ request.headers.get("x-rsc-router-request-id") ??
67
+ request.headers.get("x-request-id") ??
68
+ request.headers.get("cf-ray");
69
+ if (!candidate) return null;
70
+ const trimmed = candidate.trim();
71
+ return trimmed.length > 0 ? trimmed : null;
72
+ }
73
+
74
+ function getOrCreateRequestId(request: Request): string {
75
+ const existing = requestIds.get(request);
76
+ if (existing) return existing;
77
+
78
+ const fromHeaders = getHeaderRequestId(request);
79
+ if (fromHeaders) {
80
+ requestIds.set(request, fromHeaders);
81
+ return fromHeaders;
82
+ }
83
+
84
+ requestCounter += 1;
85
+ const generated = nextId("req-", requestCounter);
86
+ requestIds.set(request, generated);
87
+ return generated;
88
+ }
89
+
90
+ export function runWithRouterLogContext<T>(
91
+ options: RouterLogOptions,
92
+ fn: () => T,
93
+ ): T {
94
+ if (!INTERNAL_RANGO_DEBUG) {
95
+ return fn();
96
+ }
97
+
98
+ const requestId = getOrCreateRequestId(options.request);
99
+ transactionCounter += 1;
100
+ const transactionId = `${options.transaction}-${nextId("tx-", transactionCounter)}`;
101
+
102
+ return routerLogContext.run(
103
+ {
104
+ requestId,
105
+ transactionId,
106
+ depth: 0,
107
+ },
108
+ fn,
109
+ );
110
+ }
111
+
112
+ export function withRouterLogScope<T>(
113
+ label: string,
114
+ fn: () => Promise<T>,
115
+ ): Promise<T>;
116
+ export function withRouterLogScope<T>(label: string, fn: () => T): T;
117
+ export function withRouterLogScope<T>(
118
+ label: string,
119
+ fn: () => Promise<T> | T,
120
+ ): Promise<T> | T {
121
+ const ctx = routerLogContext.getStore();
122
+ if (!INTERNAL_RANGO_DEBUG || !ctx) {
123
+ return fn();
124
+ }
125
+
126
+ debugLog(label, "start");
127
+
128
+ return routerLogContext.run({ ...ctx, depth: ctx.depth + 1 }, () => {
129
+ try {
130
+ const result = fn();
131
+ if (result && typeof (result as Promise<T>).then === "function") {
132
+ return (result as Promise<T>).then(
133
+ (value) => {
134
+ debugLog(label, "end");
135
+ return value;
136
+ },
137
+ (error) => {
138
+ debugLog(label, "error", { error: String(error) });
139
+ throw error;
140
+ },
141
+ );
142
+ }
143
+ debugLog(label, "end");
144
+ return result;
145
+ } catch (error) {
146
+ debugLog(label, "error", { error: String(error) });
147
+ throw error;
148
+ }
149
+ });
150
+ }
151
+
152
+ export function isRouterDebugEnabled(): boolean {
153
+ return INTERNAL_RANGO_DEBUG && !!routerLogContext.getStore();
154
+ }
155
+
156
+ function formatPrefix(scope: string): string {
157
+ const ctx = routerLogContext.getStore();
158
+ if (!ctx) return `[Router][${scope}]`;
159
+ const indent = " ".repeat(ctx.depth);
160
+ return `[Router][req:${ctx.requestId}][tx:${ctx.transactionId}] ${indent}[${scope}]`;
161
+ }
162
+
163
+ export function debugLog(
164
+ scope: string,
165
+ message: string,
166
+ details?: LogDetails,
167
+ ): void {
168
+ if (!isRouterDebugEnabled()) return;
169
+
170
+ const prefix = formatPrefix(scope);
171
+ if (details) {
172
+ console.log(`${prefix} ${message}`, details);
173
+ return;
174
+ }
175
+
176
+ console.log(`${prefix} ${message}`);
177
+ }
178
+
179
+ export function debugWarn(
180
+ scope: string,
181
+ message: string,
182
+ details?: LogDetails,
183
+ ): void {
184
+ if (!isRouterDebugEnabled()) return;
185
+
186
+ const prefix = formatPrefix(scope);
187
+ if (details) {
188
+ console.warn(`${prefix} ${message}`, details);
189
+ return;
190
+ }
191
+
192
+ console.warn(`${prefix} ${message}`);
193
+ }
194
+
195
+ // -- Revalidation trace helpers --
196
+
197
+ export function isTraceActive(): boolean {
198
+ if (!INTERNAL_RANGO_DEBUG) return false;
199
+ const ctx = routerLogContext.getStore();
200
+ return !!ctx?.revalidationTrace;
201
+ }
202
+
203
+ export function startRevalidationTrace(meta: RevalidationTraceMeta): void {
204
+ const ctx = routerLogContext.getStore();
205
+ if (!ctx || !INTERNAL_RANGO_DEBUG) return;
206
+ ctx.revalidationTrace = { meta, entries: [] };
207
+ }
208
+
209
+ export function pushRevalidationTraceEntry(
210
+ entry: RevalidationTraceEntry,
211
+ ): void {
212
+ const ctx = routerLogContext.getStore();
213
+ if (!ctx?.revalidationTrace) return;
214
+ ctx.revalidationTrace.entries.push(entry);
215
+ }
216
+
217
+ export function flushRevalidationTrace(): RevalidationTrace | null {
218
+ const ctx = routerLogContext.getStore();
219
+ if (!ctx?.revalidationTrace) return null;
220
+ const trace = ctx.revalidationTrace;
221
+ ctx.revalidationTrace = undefined;
222
+
223
+ if (trace.entries.length === 0) return trace;
224
+
225
+ const revalidated = trace.entries.filter((e) => e.finalShouldRevalidate);
226
+ const skipped = trace.entries.filter((e) => !e.finalShouldRevalidate);
227
+
228
+ debugLog("revalidation-trace", "flush", {
229
+ method: trace.meta.method,
230
+ routeKey: trace.meta.routeKey,
231
+ isAction: trace.meta.isAction,
232
+ stale: trace.meta.stale,
233
+ prevUrl: trace.meta.prevUrl,
234
+ nextUrl: trace.meta.nextUrl,
235
+ total: trace.entries.length,
236
+ revalidated: revalidated.length,
237
+ skipped: skipped.length,
238
+ entries: trace.entries.map((e) => ({
239
+ segmentId: e.segmentId,
240
+ type: e.segmentType,
241
+ source: e.source,
242
+ revalidate: e.finalShouldRevalidate,
243
+ reason: e.reason,
244
+ })),
245
+ });
246
+
247
+ return trace;
248
+ }