@rangojs/router 0.0.0-experimental.5 → 0.0.0-experimental.50

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 (301) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +884 -4
  3. package/dist/bin/rango.js +1606 -0
  4. package/dist/vite/index.js +4567 -769
  5. package/package.json +77 -58
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +85 -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 +89 -30
  18. package/skills/loader/SKILL.md +388 -38
  19. package/skills/middleware/SKILL.md +171 -34
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +204 -1
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +85 -16
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +226 -14
  26. package/skills/router-setup/SKILL.md +123 -30
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +9 -8
  29. package/skills/typesafety/SKILL.md +318 -89
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +102 -4
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/event-controller.ts +92 -64
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/intercept-utils.ts +52 -0
  38. package/src/browser/link-interceptor.ts +24 -4
  39. package/src/browser/logging.ts +55 -0
  40. package/src/browser/merge-segment-loaders.ts +20 -12
  41. package/src/browser/navigation-bridge.ts +282 -557
  42. package/src/browser/navigation-client.ts +157 -71
  43. package/src/browser/navigation-store.ts +33 -50
  44. package/src/browser/navigation-transaction.ts +297 -0
  45. package/src/browser/network-error-handler.ts +61 -0
  46. package/src/browser/partial-update.ts +303 -310
  47. package/src/browser/prefetch/cache.ts +206 -0
  48. package/src/browser/prefetch/fetch.ts +144 -0
  49. package/src/browser/prefetch/observer.ts +65 -0
  50. package/src/browser/prefetch/policy.ts +48 -0
  51. package/src/browser/prefetch/queue.ts +128 -0
  52. package/src/browser/rango-state.ts +112 -0
  53. package/src/browser/react/Link.tsx +193 -73
  54. package/src/browser/react/NavigationProvider.tsx +160 -13
  55. package/src/browser/react/context.ts +6 -0
  56. package/src/browser/react/filter-segment-order.ts +11 -0
  57. package/src/browser/react/index.ts +12 -12
  58. package/src/browser/react/location-state-shared.ts +95 -53
  59. package/src/browser/react/location-state.ts +60 -15
  60. package/src/browser/react/mount-context.ts +24 -1
  61. package/src/browser/react/nonce-context.ts +23 -0
  62. package/src/browser/react/shallow-equal.ts +27 -0
  63. package/src/browser/react/use-action.ts +29 -51
  64. package/src/browser/react/use-client-cache.ts +5 -3
  65. package/src/browser/react/use-handle.ts +32 -79
  66. package/src/browser/react/use-href.tsx +2 -2
  67. package/src/browser/react/use-link-status.ts +6 -5
  68. package/src/browser/react/use-navigation.ts +22 -63
  69. package/src/browser/react/use-params.ts +65 -0
  70. package/src/browser/react/use-pathname.ts +47 -0
  71. package/src/browser/react/use-router.ts +63 -0
  72. package/src/browser/react/use-search-params.ts +56 -0
  73. package/src/browser/react/use-segments.ts +80 -97
  74. package/src/browser/response-adapter.ts +73 -0
  75. package/src/browser/rsc-router.tsx +188 -55
  76. package/src/browser/scroll-restoration.ts +117 -44
  77. package/src/browser/segment-reconciler.ts +221 -0
  78. package/src/browser/segment-structure-assert.ts +16 -0
  79. package/src/browser/server-action-bridge.ts +504 -599
  80. package/src/browser/shallow.ts +6 -1
  81. package/src/browser/types.ts +118 -47
  82. package/src/browser/validate-redirect-origin.ts +29 -0
  83. package/src/build/generate-manifest.ts +235 -24
  84. package/src/build/generate-route-types.ts +36 -0
  85. package/src/build/index.ts +13 -0
  86. package/src/build/route-trie.ts +265 -0
  87. package/src/build/route-types/ast-helpers.ts +25 -0
  88. package/src/build/route-types/ast-route-extraction.ts +98 -0
  89. package/src/build/route-types/codegen.ts +102 -0
  90. package/src/build/route-types/include-resolution.ts +411 -0
  91. package/src/build/route-types/param-extraction.ts +48 -0
  92. package/src/build/route-types/per-module-writer.ts +128 -0
  93. package/src/build/route-types/router-processing.ts +479 -0
  94. package/src/build/route-types/scan-filter.ts +78 -0
  95. package/src/build/runtime-discovery.ts +231 -0
  96. package/src/cache/background-task.ts +34 -0
  97. package/src/cache/cache-key-utils.ts +44 -0
  98. package/src/cache/cache-policy.ts +125 -0
  99. package/src/cache/cache-runtime.ts +342 -0
  100. package/src/cache/cache-scope.ts +167 -309
  101. package/src/cache/cf/cf-cache-store.ts +571 -17
  102. package/src/cache/cf/index.ts +13 -3
  103. package/src/cache/document-cache.ts +116 -77
  104. package/src/cache/handle-capture.ts +81 -0
  105. package/src/cache/handle-snapshot.ts +41 -0
  106. package/src/cache/index.ts +1 -15
  107. package/src/cache/memory-segment-store.ts +191 -13
  108. package/src/cache/profile-registry.ts +73 -0
  109. package/src/cache/read-through-swr.ts +134 -0
  110. package/src/cache/segment-codec.ts +256 -0
  111. package/src/cache/taint.ts +98 -0
  112. package/src/cache/types.ts +72 -122
  113. package/src/client.rsc.tsx +3 -1
  114. package/src/client.tsx +106 -126
  115. package/src/component-utils.ts +4 -4
  116. package/src/components/DefaultDocument.tsx +5 -1
  117. package/src/context-var.ts +86 -0
  118. package/src/debug.ts +19 -9
  119. package/src/errors.ts +108 -2
  120. package/src/handle.ts +15 -29
  121. package/src/handles/MetaTags.tsx +73 -20
  122. package/src/handles/breadcrumbs.ts +66 -0
  123. package/src/handles/index.ts +1 -0
  124. package/src/handles/meta.ts +30 -13
  125. package/src/host/cookie-handler.ts +165 -0
  126. package/src/host/errors.ts +97 -0
  127. package/src/host/index.ts +53 -0
  128. package/src/host/pattern-matcher.ts +214 -0
  129. package/src/host/router.ts +352 -0
  130. package/src/host/testing.ts +79 -0
  131. package/src/host/types.ts +146 -0
  132. package/src/host/utils.ts +25 -0
  133. package/src/href-client.ts +119 -29
  134. package/src/index.rsc.ts +153 -19
  135. package/src/index.ts +211 -30
  136. package/src/internal-debug.ts +11 -0
  137. package/src/loader.rsc.ts +26 -147
  138. package/src/loader.ts +27 -10
  139. package/src/network-error-thrower.tsx +3 -1
  140. package/src/outlet-provider.tsx +45 -0
  141. package/src/prerender/param-hash.ts +37 -0
  142. package/src/prerender/store.ts +185 -0
  143. package/src/prerender.ts +463 -0
  144. package/src/reverse.ts +330 -0
  145. package/src/root-error-boundary.tsx +41 -29
  146. package/src/route-content-wrapper.tsx +7 -4
  147. package/src/route-definition/dsl-helpers.ts +959 -0
  148. package/src/route-definition/helper-factories.ts +200 -0
  149. package/src/route-definition/helpers-types.ts +430 -0
  150. package/src/route-definition/index.ts +52 -0
  151. package/src/route-definition/redirect.ts +93 -0
  152. package/src/route-definition.ts +1 -1428
  153. package/src/route-map-builder.ts +217 -123
  154. package/src/route-name.ts +53 -0
  155. package/src/route-types.ts +59 -8
  156. package/src/router/content-negotiation.ts +116 -0
  157. package/src/router/debug-manifest.ts +72 -0
  158. package/src/router/error-handling.ts +9 -9
  159. package/src/router/find-match.ts +160 -0
  160. package/src/router/handler-context.ts +374 -81
  161. package/src/router/intercept-resolution.ts +397 -0
  162. package/src/router/lazy-includes.ts +237 -0
  163. package/src/router/loader-resolution.ts +215 -122
  164. package/src/router/logging.ts +251 -0
  165. package/src/router/manifest.ts +154 -35
  166. package/src/router/match-api.ts +620 -0
  167. package/src/router/match-context.ts +5 -3
  168. package/src/router/match-handlers.ts +440 -0
  169. package/src/router/match-middleware/background-revalidation.ts +108 -93
  170. package/src/router/match-middleware/cache-lookup.ts +440 -10
  171. package/src/router/match-middleware/cache-store.ts +98 -26
  172. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  173. package/src/router/match-middleware/segment-resolution.ts +27 -6
  174. package/src/router/match-pipelines.ts +10 -45
  175. package/src/router/match-result.ts +55 -33
  176. package/src/router/metrics.ts +240 -15
  177. package/src/router/middleware-cookies.ts +55 -0
  178. package/src/router/middleware-types.ts +222 -0
  179. package/src/router/middleware.ts +327 -369
  180. package/src/router/pattern-matching.ts +211 -43
  181. package/src/router/prerender-match.ts +402 -0
  182. package/src/router/preview-match.ts +170 -0
  183. package/src/router/revalidation.ts +137 -38
  184. package/src/router/router-context.ts +41 -21
  185. package/src/router/router-interfaces.ts +452 -0
  186. package/src/router/router-options.ts +592 -0
  187. package/src/router/router-registry.ts +24 -0
  188. package/src/router/segment-resolution/fresh.ts +677 -0
  189. package/src/router/segment-resolution/helpers.ts +263 -0
  190. package/src/router/segment-resolution/loader-cache.ts +199 -0
  191. package/src/router/segment-resolution/revalidation.ts +1296 -0
  192. package/src/router/segment-resolution/static-store.ts +67 -0
  193. package/src/router/segment-resolution.ts +21 -0
  194. package/src/router/segment-wrappers.ts +291 -0
  195. package/src/router/telemetry-otel.ts +299 -0
  196. package/src/router/telemetry.ts +300 -0
  197. package/src/router/timeout.ts +148 -0
  198. package/src/router/trie-matching.ts +239 -0
  199. package/src/router/types.ts +77 -3
  200. package/src/router.ts +665 -4182
  201. package/src/rsc/handler-context.ts +45 -0
  202. package/src/rsc/handler.ts +764 -754
  203. package/src/rsc/helpers.ts +140 -6
  204. package/src/rsc/index.ts +0 -20
  205. package/src/rsc/loader-fetch.ts +209 -0
  206. package/src/rsc/manifest-init.ts +86 -0
  207. package/src/rsc/nonce.ts +14 -0
  208. package/src/rsc/origin-guard.ts +141 -0
  209. package/src/rsc/progressive-enhancement.ts +379 -0
  210. package/src/rsc/response-error.ts +37 -0
  211. package/src/rsc/response-route-handler.ts +347 -0
  212. package/src/rsc/rsc-rendering.ts +237 -0
  213. package/src/rsc/runtime-warnings.ts +42 -0
  214. package/src/rsc/server-action.ts +348 -0
  215. package/src/rsc/ssr-setup.ts +128 -0
  216. package/src/rsc/types.ts +38 -11
  217. package/src/search-params.ts +230 -0
  218. package/src/segment-system.tsx +172 -21
  219. package/src/server/context.ts +266 -58
  220. package/src/server/cookie-store.ts +190 -0
  221. package/src/server/fetchable-loader-store.ts +37 -0
  222. package/src/server/handle-store.ts +94 -15
  223. package/src/server/loader-registry.ts +15 -56
  224. package/src/server/request-context.ts +439 -73
  225. package/src/server.ts +35 -128
  226. package/src/ssr/index.tsx +101 -31
  227. package/src/static-handler.ts +114 -0
  228. package/src/theme/ThemeProvider.tsx +21 -15
  229. package/src/theme/ThemeScript.tsx +5 -5
  230. package/src/theme/constants.ts +5 -2
  231. package/src/theme/index.ts +4 -14
  232. package/src/theme/theme-context.ts +4 -30
  233. package/src/theme/theme-script.ts +21 -18
  234. package/src/types/boundaries.ts +158 -0
  235. package/src/types/cache-types.ts +198 -0
  236. package/src/types/error-types.ts +192 -0
  237. package/src/types/global-namespace.ts +100 -0
  238. package/src/types/handler-context.ts +773 -0
  239. package/src/types/index.ts +88 -0
  240. package/src/types/loader-types.ts +183 -0
  241. package/src/types/route-config.ts +170 -0
  242. package/src/types/route-entry.ts +109 -0
  243. package/src/types/segments.ts +150 -0
  244. package/src/types.ts +1 -1623
  245. package/src/urls/include-helper.ts +197 -0
  246. package/src/urls/index.ts +53 -0
  247. package/src/urls/path-helper-types.ts +339 -0
  248. package/src/urls/path-helper.ts +329 -0
  249. package/src/urls/pattern-types.ts +95 -0
  250. package/src/urls/response-types.ts +106 -0
  251. package/src/urls/type-extraction.ts +372 -0
  252. package/src/urls/urls-function.ts +98 -0
  253. package/src/urls.ts +1 -802
  254. package/src/use-loader.tsx +85 -77
  255. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  256. package/src/vite/discovery/discover-routers.ts +344 -0
  257. package/src/vite/discovery/prerender-collection.ts +385 -0
  258. package/src/vite/discovery/route-types-writer.ts +258 -0
  259. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  260. package/src/vite/discovery/state.ts +108 -0
  261. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  262. package/src/vite/index.ts +11 -782
  263. package/src/vite/plugin-types.ts +48 -0
  264. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  265. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  266. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  267. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  268. package/src/vite/plugins/expose-id-utils.ts +287 -0
  269. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  270. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  271. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  272. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  273. package/src/vite/plugins/expose-ids/types.ts +45 -0
  274. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  275. package/src/vite/plugins/refresh-cmd.ts +65 -0
  276. package/src/vite/plugins/use-cache-transform.ts +323 -0
  277. package/src/vite/plugins/version-injector.ts +83 -0
  278. package/src/vite/plugins/version-plugin.ts +266 -0
  279. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +27 -16
  280. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  281. package/src/vite/rango.ts +445 -0
  282. package/src/vite/router-discovery.ts +777 -0
  283. package/src/vite/utils/ast-handler-extract.ts +517 -0
  284. package/src/vite/utils/banner.ts +36 -0
  285. package/src/vite/utils/bundle-analysis.ts +137 -0
  286. package/src/vite/utils/manifest-utils.ts +70 -0
  287. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  288. package/src/vite/utils/prerender-utils.ts +189 -0
  289. package/src/vite/utils/shared-utils.ts +169 -0
  290. package/CLAUDE.md +0 -43
  291. package/src/browser/lru-cache.ts +0 -69
  292. package/src/browser/request-controller.ts +0 -164
  293. package/src/cache/memory-store.ts +0 -253
  294. package/src/href-context.ts +0 -33
  295. package/src/href.ts +0 -255
  296. package/src/server/route-manifest-cache.ts +0 -173
  297. package/src/vite/expose-handle-id.ts +0 -209
  298. package/src/vite/expose-loader-id.ts +0 -426
  299. package/src/vite/expose-location-state-id.ts +0 -177
  300. package/src/warmup/connection-warmup.tsx +0 -94
  301. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -13,13 +13,30 @@
13
13
  import { AsyncLocalStorage } from "node:async_hooks";
14
14
  import type { CookieOptions } from "../router/middleware.js";
15
15
  import type { LoaderDefinition, LoaderContext } from "../types.js";
16
+ import type { ScopedReverseFunction } from "../reverse.js";
17
+ import type {
18
+ DefaultEnv,
19
+ DefaultReverseRouteMap,
20
+ DefaultRouteName,
21
+ } from "../types/global-namespace.js";
16
22
  import type { Handle } from "../handle.js";
23
+ import { type ContextVar, contextGet, contextSet } from "../context-var.js";
17
24
  import { createHandleStore, type HandleStore } from "./handle-store.js";
18
25
  import { isHandle } from "../handle.js";
19
- import { track } from "./context.js";
26
+ import { track, type MetricsStore } from "./context.js";
27
+ import { getFetchableLoader } from "./fetchable-loader-store.js";
20
28
  import type { SegmentCacheStore } from "../cache/types.js";
21
29
  import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
22
30
  import { THEME_COOKIE } from "../theme/constants.js";
31
+ import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
32
+ import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
33
+ import {
34
+ createReverseFunction,
35
+ stripInternalParams,
36
+ } from "../router/handler-context.js";
37
+ import { getGlobalRouteMap, isRouteRootScoped } from "../route-map-builder.js";
38
+ import { invariant } from "../errors.js";
39
+ import { isAutoGeneratedRouteName } from "../route-name.js";
23
40
 
24
41
  /**
25
42
  * Unified request context available via getRequestContext()
@@ -28,46 +45,61 @@ import { THEME_COOKIE } from "../theme/constants.js";
28
45
  * Use this when you need access to request data outside of route handlers.
29
46
  */
30
47
  export interface RequestContext<
31
- TEnv = unknown,
48
+ TEnv = DefaultEnv,
32
49
  TParams = Record<string, string>,
33
50
  > {
34
51
  /** Platform bindings (Cloudflare env, etc.) */
35
52
  env: TEnv;
36
53
  /** Original HTTP request */
37
54
  request: Request;
38
- /** Parsed URL (system params like _rsc* are NOT filtered here) */
55
+ /** Parsed URL (with internal `_rsc*` params stripped) */
39
56
  url: URL;
57
+ /**
58
+ * The original request URL with all parameters intact, including
59
+ * internal `_rsc*` transport params.
60
+ */
61
+ originalUrl: URL;
40
62
  /** URL pathname */
41
63
  pathname: string;
42
- /** URL search params (system params like _rsc* are NOT filtered here) */
64
+ /** URL search params (with internal `_rsc*` params stripped, same as `url.searchParams`) */
43
65
  searchParams: URLSearchParams;
44
66
  /** Variables set by middleware (same as ctx.var) */
45
67
  var: Record<string, any>;
46
68
  /** Get a variable set by middleware */
47
- get: <K extends string>(key: K) => any;
69
+ get: {
70
+ <T>(contextVar: ContextVar<T>): T | undefined;
71
+ <K extends string>(key: K): any;
72
+ };
48
73
  /** Set a variable (shared with middleware and handlers) */
49
- set: <K extends string>(key: K, value: any) => void;
74
+ set: {
75
+ <T>(contextVar: ContextVar<T>, value: T): void;
76
+ <K extends string>(key: K, value: any): void;
77
+ };
50
78
  /**
51
79
  * Route params (populated after route matching)
52
80
  * Initially empty, then set to matched params
53
81
  */
54
82
  params: TParams;
55
- /**
56
- * Stub response for setting headers/cookies
57
- * Headers set here are merged into the final response
58
- */
59
- res: Response;
83
+ /** @internal Stub response for collecting headers/cookies. Use ctx.headers or ctx.header() instead. */
84
+ readonly res: Response;
60
85
 
61
- /** Get a cookie value from the request */
86
+ /** @internal Get a cookie value (effective: request + response mutations). Use cookies().get() instead. */
62
87
  cookie(name: string): string | undefined;
63
- /** Get all cookies from the request */
88
+ /** @internal Get all cookies (effective merged view). Use cookies().getAll() instead. */
64
89
  cookies(): Record<string, string>;
65
- /** Set a cookie on the response */
90
+ /** @internal Set a cookie on the response. Use cookies().set() instead. */
66
91
  setCookie(name: string, value: string, options?: CookieOptions): void;
67
- /** Delete a cookie */
68
- deleteCookie(name: string, options?: Pick<CookieOptions, "domain" | "path">): void;
92
+ /** @internal Delete a cookie. Use cookies().delete() instead. */
93
+ deleteCookie(
94
+ name: string,
95
+ options?: Pick<CookieOptions, "domain" | "path">,
96
+ ): void;
69
97
  /** Set a response header */
70
98
  header(name: string, value: string): void;
99
+ /** Set the response status code */
100
+ setStatus(status: number): void;
101
+ /** @internal Set status bypassing cache-exec guard (for framework error handling) */
102
+ _setStatus(status: number): void;
71
103
 
72
104
  /**
73
105
  * Access loader data or push handle data.
@@ -89,10 +121,12 @@ export interface RequestContext<
89
121
  * ```
90
122
  */
91
123
  use: {
92
- <T, TLoaderParams = any>(loader: LoaderDefinition<T, TLoaderParams>): Promise<T>;
93
- <TData, TAccumulated = TData[]>(handle: Handle<TData, TAccumulated>): (
94
- data: TData | Promise<TData> | (() => Promise<TData>)
95
- ) => void;
124
+ <T, TLoaderParams = any>(
125
+ loader: LoaderDefinition<T, TLoaderParams>,
126
+ ): Promise<T>;
127
+ <TData, TAccumulated = TData[]>(
128
+ handle: Handle<TData, TAccumulated>,
129
+ ): (data: TData | Promise<TData> | (() => Promise<TData>)) => void;
96
130
  };
97
131
 
98
132
  /** HTTP method (GET, POST, PUT, PATCH, DELETE, etc.) */
@@ -104,6 +138,12 @@ export interface RequestContext<
104
138
  /** @internal Cache store for segment caching (optional, used by CacheScope) */
105
139
  _cacheStore?: SegmentCacheStore;
106
140
 
141
+ /** @internal Cache profiles for "use cache" profile resolution (per-router) */
142
+ _cacheProfiles?: Record<
143
+ string,
144
+ import("../cache/profile-registry.js").CacheProfile
145
+ >;
146
+
107
147
  /**
108
148
  * Schedule work to run after the response is sent.
109
149
  * On Cloudflare Workers, uses ctx.waitUntil().
@@ -177,8 +217,99 @@ export interface RequestContext<
177
217
 
178
218
  /** @internal Theme configuration (null if theme not enabled) */
179
219
  _themeConfig?: ResolvedThemeConfig | null;
220
+
221
+ /**
222
+ * Attach location state entries to the current response.
223
+ *
224
+ * For partial (SPA) requests, the state is included in the RSC payload
225
+ * metadata and merged into history.pushState on the client. For redirect
226
+ * responses, the state travels through the redirect payload so the target
227
+ * page can read it via useLocationState.
228
+ *
229
+ * Multiple calls accumulate entries.
230
+ *
231
+ * @example
232
+ * ```typescript
233
+ * ctx.setLocationState(Flash({ text: "Item saved!" }));
234
+ * ```
235
+ */
236
+ setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void;
237
+
238
+ /** @internal Accumulated location state entries */
239
+ _locationState?: LocationStateEntry[];
240
+
241
+ /**
242
+ * The matched route name, if the route has an explicit name.
243
+ * Undefined before route matching or for unnamed routes.
244
+ * Includes the namespace prefix from include() (e.g., "blog.post").
245
+ */
246
+ routeName?: DefaultRouteName;
247
+
248
+ /**
249
+ * Generate URLs from route names.
250
+ * Uses the global route map. After route matching, scoped (`.name`) resolution
251
+ * works within the matched include() scope.
252
+ */
253
+ reverse: ScopedReverseFunction<
254
+ Record<string, string>,
255
+ DefaultReverseRouteMap
256
+ >;
257
+
258
+ /** @internal Route name from route matching, used for scoped reverse resolution */
259
+ _routeName?: string;
260
+
261
+ /** @internal Previous route key (from the navigation source), used for revalidation */
262
+ _prevRouteKey?: string;
263
+
264
+ /** @internal Per-request error dedup set for onError reporting */
265
+ _reportedErrors: WeakSet<object>;
266
+
267
+ /**
268
+ * @internal Report a non-fatal background error through the router's
269
+ * onError callback. Wired by the RSC handler / router during request
270
+ * creation. Cache-runtime and other subsystems call this to surface
271
+ * errors without failing the response.
272
+ */
273
+ _reportBackgroundError?: (error: unknown, category: string) => void;
274
+
275
+ /** @internal Per-request debug performance override (set via ctx.debugPerformance()) */
276
+ _debugPerformance?: boolean;
277
+
278
+ /** @internal Request-scoped performance metrics store */
279
+ _metricsStore?: MetricsStore;
180
280
  }
181
281
 
282
+ /**
283
+ * Public view of RequestContext, without internal methods and fields.
284
+ *
285
+ * This is the type exported to library consumers. Internal code should
286
+ * use the full RequestContext interface directly.
287
+ */
288
+ export type PublicRequestContext<
289
+ TEnv = DefaultEnv,
290
+ TParams = Record<string, string>,
291
+ > = Omit<
292
+ RequestContext<TEnv, TParams>,
293
+ | "cookie"
294
+ | "cookies"
295
+ | "setCookie"
296
+ | "deleteCookie"
297
+ | "_handleStore"
298
+ | "_cacheStore"
299
+ | "_cacheProfiles"
300
+ | "_onResponseCallbacks"
301
+ | "_themeConfig"
302
+ | "_locationState"
303
+ | "_routeName"
304
+ | "_prevRouteKey"
305
+ | "_reportedErrors"
306
+ | "_reportBackgroundError"
307
+ | "_debugPerformance"
308
+ | "_metricsStore"
309
+ | "_setStatus"
310
+ | "res"
311
+ >;
312
+
182
313
  // AsyncLocalStorage instance for request context
183
314
  const requestContextStorage = new AsyncLocalStorage<RequestContext<any>>();
184
315
 
@@ -188,16 +319,33 @@ const requestContextStorage = new AsyncLocalStorage<RequestContext<any>>();
188
319
  */
189
320
  export function runWithRequestContext<TEnv, T>(
190
321
  context: RequestContext<TEnv>,
191
- fn: () => T
322
+ fn: () => T,
192
323
  ): T {
193
324
  return requestContextStorage.run(context, fn);
194
325
  }
195
326
 
196
327
  /**
197
328
  * Get the current request context
198
- * Returns undefined if not running within a request context
329
+ * Throws if called outside of a request context
199
330
  */
200
- export function getRequestContext<TEnv = unknown>():
331
+ export function getRequestContext<TEnv = DefaultEnv>(): RequestContext<TEnv> {
332
+ const ctx = requestContextStorage.getStore() as
333
+ | RequestContext<TEnv>
334
+ | undefined;
335
+ invariant(
336
+ ctx,
337
+ "getRequestContext() called outside of a request context. " +
338
+ "This function must be called from within a route handler, loader, middleware, " +
339
+ "server action, or server component.",
340
+ );
341
+ return ctx;
342
+ }
343
+
344
+ /**
345
+ * @internal Get the request context without throwing — for internal code that
346
+ * may run outside a request context (cache stores, optional handle lookups, etc.)
347
+ */
348
+ export function _getRequestContext<TEnv = DefaultEnv>():
201
349
  | RequestContext<TEnv>
202
350
  | undefined {
203
351
  return requestContextStorage.getStore() as RequestContext<TEnv> | undefined;
@@ -205,28 +353,67 @@ export function getRequestContext<TEnv = unknown>():
205
353
 
206
354
  /**
207
355
  * Update params on the current request context
208
- * Called after route matching to populate route params
356
+ * Called after route matching to populate route params and route name
209
357
  */
210
- export function setRequestContextParams(params: Record<string, string>): void {
358
+ export function setRequestContextParams(
359
+ params: Record<string, string>,
360
+ routeName?: string,
361
+ ): void {
211
362
  const ctx = requestContextStorage.getStore();
212
363
  if (ctx) {
213
364
  ctx.params = params;
365
+ if (routeName !== undefined) {
366
+ ctx._routeName = routeName;
367
+ ctx.routeName = (
368
+ routeName && !isAutoGeneratedRouteName(routeName)
369
+ ? routeName
370
+ : undefined
371
+ ) as DefaultRouteName | undefined;
372
+ }
373
+ // Update reverse with scoped resolution now that route is known
374
+ ctx.reverse = createReverseFunction(
375
+ getGlobalRouteMap(),
376
+ routeName,
377
+ params,
378
+ routeName ? isRouteRootScoped(routeName) : undefined,
379
+ );
214
380
  }
215
381
  }
216
382
 
217
383
  /**
218
- * Get the current request context, throwing if not available
219
- * Use this when context is required (e.g., in loader actions)
384
+ * Store the previous route key on the request context.
385
+ * Called during partial-match context creation to make the navigation source
386
+ * route key available for revalidation and intercept evaluation.
387
+ * @internal
220
388
  */
221
- export function requireRequestContext<TEnv = unknown>(): RequestContext<TEnv> {
222
- const ctx = getRequestContext<TEnv>();
223
- if (!ctx) {
224
- throw new Error(
225
- "Request context not available. This function must be called from within a server action " +
226
- "executed through the RSC handler."
227
- );
389
+ export function setRequestContextPrevRouteKey(
390
+ prevRouteKey: string | undefined,
391
+ ): void {
392
+ const ctx = requestContextStorage.getStore();
393
+ if (ctx && prevRouteKey !== undefined) {
394
+ ctx._prevRouteKey = prevRouteKey;
228
395
  }
229
- return ctx;
396
+ }
397
+
398
+ /**
399
+ * Get accumulated location state entries from the current request context.
400
+ * Returns undefined if no state has been set.
401
+ *
402
+ * @internal Used by the RSC handler to include state in payload metadata.
403
+ */
404
+ export function getLocationState(): LocationStateEntry[] | undefined {
405
+ const ctx = getRequestContext();
406
+ return ctx?._locationState;
407
+ }
408
+
409
+ /**
410
+ * Get the current request context, throwing if not available
411
+ * @deprecated Use getRequestContext() directly — it now throws if outside context
412
+ */
413
+ export function requireRequestContext<
414
+ TEnv = DefaultEnv,
415
+ >(): RequestContext<TEnv> {
416
+ return getRequestContext<TEnv>();
230
417
  }
231
418
 
232
419
  /**
@@ -245,8 +432,15 @@ export interface CreateRequestContextOptions<TEnv> {
245
432
  request: Request;
246
433
  url: URL;
247
434
  variables: Record<string, any>;
435
+ /** Optional initial response stub headers/status to seed effective cookie reads */
436
+ initialResponse?: Response;
248
437
  /** Optional cache store for segment caching (used by CacheScope) */
249
438
  cacheStore?: SegmentCacheStore;
439
+ /** Optional cache profiles for "use cache" resolution (per-router) */
440
+ cacheProfiles?: Record<
441
+ string,
442
+ import("../cache/profile-registry.js").CacheProfile
443
+ >;
250
444
  /** Optional Cloudflare execution context for waitUntil support */
251
445
  executionContext?: ExecutionContext;
252
446
  /** Optional theme configuration (enables ctx.theme and ctx.setTheme) */
@@ -262,20 +456,37 @@ export interface CreateRequestContextOptions<TEnv> {
262
456
  * - Passed to handlers as ctx
263
457
  */
264
458
  export function createRequestContext<TEnv>(
265
- options: CreateRequestContextOptions<TEnv>
459
+ options: CreateRequestContextOptions<TEnv>,
266
460
  ): RequestContext<TEnv> {
267
- const { env, request, url, variables, cacheStore, executionContext, themeConfig } = options;
461
+ const {
462
+ env,
463
+ request,
464
+ url,
465
+ variables,
466
+ initialResponse,
467
+ cacheStore,
468
+ cacheProfiles,
469
+ executionContext,
470
+ themeConfig,
471
+ } = options;
268
472
  const cookieHeader = request.headers.get("Cookie");
269
473
  let parsedCookies: Record<string, string> | null = null;
270
474
 
271
- // Create stub response for collecting headers/cookies
272
- const stubResponse = new Response(null, { status: 200 });
475
+ // Create stub response for collecting headers/cookies.
476
+ // All cookie/header mutations go here; cookie reads derive from it.
477
+ let stubResponse = initialResponse
478
+ ? new Response(null, {
479
+ status: initialResponse.status,
480
+ statusText: initialResponse.statusText,
481
+ headers: new Headers(initialResponse.headers),
482
+ })
483
+ : new Response(null, { status: 200 });
273
484
 
274
485
  // Create handle store and loader memoization for this request
275
486
  const handleStore = createHandleStore();
276
487
  const loaderPromises = new Map<string, Promise<any>>();
277
488
 
278
- // Lazy parse cookies
489
+ // Lazy parse cookies from the original Cookie header
279
490
  const getParsedCookies = (): Record<string, string> => {
280
491
  if (!parsedCookies) {
281
492
  parsedCookies = parseCookiesFromHeader(cookieHeader);
@@ -283,11 +494,35 @@ export function createRequestContext<TEnv>(
283
494
  return parsedCookies;
284
495
  };
285
496
 
497
+ // Cached response cookie mutations — invalidated on setCookie/deleteCookie/setTheme
498
+ let responseCookieCache: Map<string, string | null> | null = null;
499
+ const getResponseCookies = (): Map<string, string | null> => {
500
+ if (!responseCookieCache) {
501
+ responseCookieCache = parseResponseCookies(stubResponse);
502
+ }
503
+ return responseCookieCache;
504
+ };
505
+ const invalidateResponseCookieCache = () => {
506
+ responseCookieCache = null;
507
+ };
508
+
509
+ // Effective cookie read: response stub Set-Cookie wins, then original header.
510
+ // The stub IS the source of truth for same-request mutations.
511
+ const effectiveCookie = (name: string): string | undefined => {
512
+ const mutations = getResponseCookies();
513
+ if (mutations.has(name)) {
514
+ const v = mutations.get(name);
515
+ return v === null ? undefined : v;
516
+ }
517
+ return getParsedCookies()[name];
518
+ };
519
+
286
520
  // Theme helpers (only used when themeConfig is provided)
287
521
  const getTheme = (): Theme | undefined => {
288
522
  if (!themeConfig) return undefined;
289
523
 
290
- const stored = getParsedCookies()[themeConfig.storageKey];
524
+ // Use overlay-aware read so setTheme() in the same request is reflected
525
+ const stored = effectiveCookie(themeConfig.storageKey);
291
526
  if (stored) {
292
527
  // Validate stored value
293
528
  if (stored === "system" && themeConfig.enableSystem) {
@@ -305,65 +540,117 @@ export function createRequestContext<TEnv>(
305
540
 
306
541
  // Validate theme value
307
542
  if (theme !== "system" && !themeConfig.themes.includes(theme)) {
308
- console.warn(`[Theme] Invalid theme value: "${theme}". Valid values: system, ${themeConfig.themes.join(", ")}`);
543
+ console.warn(
544
+ `[Theme] Invalid theme value: "${theme}". Valid values: system, ${themeConfig.themes.join(", ")}`,
545
+ );
309
546
  return;
310
547
  }
311
548
 
312
- // Set cookie
549
+ // Write to stub — effectiveCookie() will pick it up on next read
313
550
  stubResponse.headers.append(
314
551
  "Set-Cookie",
315
552
  serializeCookieValue(themeConfig.storageKey, theme, {
316
553
  path: THEME_COOKIE.path,
317
554
  maxAge: THEME_COOKIE.maxAge,
318
555
  sameSite: THEME_COOKIE.sameSite,
319
- })
556
+ }),
320
557
  );
558
+ invalidateResponseCookieCache();
321
559
  };
322
560
 
561
+ // Strip internal _rsc* params so userland sees a clean URL.
562
+ const cleanUrl = stripInternalParams(url);
563
+
323
564
  // Build the context object first (without use), then add use
324
565
  const ctx: RequestContext<TEnv> = {
325
566
  env,
326
567
  request,
327
- url,
568
+ url: cleanUrl,
569
+ originalUrl: new URL(request.url),
328
570
  pathname: url.pathname,
329
- searchParams: url.searchParams,
571
+ searchParams: cleanUrl.searchParams,
330
572
  var: variables,
331
- get: <K extends string>(key: K) => variables[key],
332
- set: <K extends string>(key: K, value: any) => {
333
- variables[key] = value;
334
- },
573
+ get: ((keyOrVar: any) =>
574
+ contextGet(variables, keyOrVar)) as RequestContext<TEnv>["get"],
575
+ set: ((keyOrVar: any, value: any) => {
576
+ assertNotInsideCacheExec(ctx, "set");
577
+ contextSet(variables, keyOrVar, value);
578
+ }) as RequestContext<TEnv>["set"],
335
579
  params: {} as Record<string, string>,
336
- res: stubResponse,
580
+
581
+ get res(): Response {
582
+ return stubResponse;
583
+ },
584
+ set res(_: Response) {
585
+ throw new Error(
586
+ "ctx.res is read-only. Use ctx.header() to set response headers, or cookies() for cookie mutations.",
587
+ );
588
+ },
337
589
 
338
590
  cookie(name: string): string | undefined {
339
- return getParsedCookies()[name];
591
+ return effectiveCookie(name);
340
592
  },
341
593
 
342
594
  cookies(): Record<string, string> {
343
- return { ...getParsedCookies() };
595
+ const parsed = getParsedCookies();
596
+ const mutations = getResponseCookies();
597
+ if (mutations.size === 0) return { ...parsed };
598
+ // Build result without delete (avoids V8 dictionary-mode de-opt)
599
+ const deleted = new Set<string>();
600
+ for (const [k, v] of mutations) {
601
+ if (v === null) deleted.add(k);
602
+ }
603
+ const result: Record<string, string> = {};
604
+ for (const key of Object.keys(parsed)) {
605
+ if (!deleted.has(key)) result[key] = parsed[key];
606
+ }
607
+ for (const [k, v] of mutations) {
608
+ if (v !== null) result[k] = v;
609
+ }
610
+ return result;
344
611
  },
345
612
 
346
613
  setCookie(name: string, value: string, options?: CookieOptions): void {
614
+ assertNotInsideCacheExec(ctx, "setCookie");
347
615
  stubResponse.headers.append(
348
616
  "Set-Cookie",
349
- serializeCookieValue(name, value, options)
617
+ serializeCookieValue(name, value, options),
350
618
  );
619
+ invalidateResponseCookieCache();
351
620
  },
352
621
 
353
622
  deleteCookie(
354
623
  name: string,
355
- options?: Pick<CookieOptions, "domain" | "path">
624
+ options?: Pick<CookieOptions, "domain" | "path">,
356
625
  ): void {
626
+ assertNotInsideCacheExec(ctx, "deleteCookie");
357
627
  stubResponse.headers.append(
358
628
  "Set-Cookie",
359
- serializeCookieValue(name, "", { ...options, maxAge: 0 })
629
+ serializeCookieValue(name, "", { ...options, maxAge: 0 }),
360
630
  );
631
+ invalidateResponseCookieCache();
361
632
  },
362
633
 
363
634
  header(name: string, value: string): void {
635
+ assertNotInsideCacheExec(ctx, "header");
364
636
  stubResponse.headers.set(name, value);
365
637
  },
366
638
 
639
+ setStatus(status: number): void {
640
+ assertNotInsideCacheExec(ctx, "setStatus");
641
+ stubResponse = new Response(null, {
642
+ status,
643
+ headers: stubResponse.headers,
644
+ });
645
+ },
646
+
647
+ _setStatus(status: number): void {
648
+ stubResponse = new Response(null, {
649
+ status,
650
+ headers: stubResponse.headers,
651
+ });
652
+ },
653
+
367
654
  // Placeholder - will be replaced below
368
655
  use: null as any,
369
656
 
@@ -371,6 +658,7 @@ export function createRequestContext<TEnv>(
371
658
 
372
659
  _handleStore: handleStore,
373
660
  _cacheStore: cacheStore,
661
+ _cacheProfiles: cacheProfiles,
374
662
 
375
663
  waitUntil(fn: () => Promise<void>): void {
376
664
  if (executionContext?.waitUntil) {
@@ -378,20 +666,44 @@ export function createRequestContext<TEnv>(
378
666
  executionContext.waitUntil(fn());
379
667
  } else {
380
668
  // Node.js / dev: fire-and-forget with error logging
381
- fn().catch((err) => console.error("[waitUntil] Background task failed:", err));
669
+ fn().catch((err) =>
670
+ console.error("[waitUntil] Background task failed:", err),
671
+ );
382
672
  }
383
673
  },
384
674
 
385
675
  _onResponseCallbacks: [],
386
676
 
387
677
  onResponse(callback: (response: Response) => Response): void {
678
+ assertNotInsideCacheExec(ctx, "onResponse");
388
679
  this._onResponseCallbacks.push(callback);
389
680
  },
390
681
 
391
682
  // Theme properties (only set when themeConfig is provided)
392
- theme: themeConfig ? getTheme() : undefined,
393
- setTheme: themeConfig ? setTheme : undefined,
683
+ get theme() {
684
+ return themeConfig ? getTheme() : undefined;
685
+ },
686
+ setTheme: themeConfig
687
+ ? (theme: Theme) => {
688
+ assertNotInsideCacheExec(ctx, "setTheme");
689
+ setTheme(theme);
690
+ }
691
+ : undefined,
394
692
  _themeConfig: themeConfig,
693
+
694
+ setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void {
695
+ assertNotInsideCacheExec(ctx, "setLocationState");
696
+ const arr = Array.isArray(entries) ? entries : [entries];
697
+ this._locationState = this._locationState
698
+ ? [...this._locationState, ...arr]
699
+ : arr;
700
+ },
701
+ _locationState: undefined,
702
+
703
+ _reportedErrors: new WeakSet<object>(),
704
+ _metricsStore: undefined,
705
+
706
+ reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
395
707
  };
396
708
 
397
709
  // Now create use() with access to ctx
@@ -401,14 +713,53 @@ export function createRequestContext<TEnv>(
401
713
  getContext: () => ctx,
402
714
  });
403
715
 
716
+ // Brand with taint symbol so "use cache" excludes ctx from cache keys
717
+ (ctx as any)[NOCACHE_SYMBOL] = true;
404
718
  return ctx;
405
719
  }
406
720
 
721
+ /**
722
+ * Parse Set-Cookie headers from a response into effective cookie state.
723
+ * Returns a map of cookie name -> value (string) or name -> null (deleted).
724
+ * Last-write-wins: later Set-Cookie entries for the same name overwrite earlier ones.
725
+ * Max-Age=0 is treated as a delete.
726
+ */
727
+ const MAX_AGE_ZERO_RE = /;\s*Max-Age\s*=\s*0/i;
728
+
729
+ function parseResponseCookies(response: Response): Map<string, string | null> {
730
+ const result = new Map<string, string | null>();
731
+ const setCookies = response.headers.getSetCookie();
732
+
733
+ for (const header of setCookies) {
734
+ // First segment before ';' is the name=value pair
735
+ const semiIdx = header.indexOf(";");
736
+ const pair = semiIdx === -1 ? header : header.substring(0, semiIdx);
737
+ const eqIdx = pair.indexOf("=");
738
+ if (eqIdx === -1) continue;
739
+
740
+ let name: string;
741
+ let value: string;
742
+ try {
743
+ name = decodeURIComponent(pair.substring(0, eqIdx).trim());
744
+ value = decodeURIComponent(pair.substring(eqIdx + 1).trim());
745
+ } catch {
746
+ // Malformed encoding — skip this entry
747
+ continue;
748
+ }
749
+
750
+ // Max-Age=0 means the cookie is being deleted
751
+ const isDeleted = MAX_AGE_ZERO_RE.test(header);
752
+ result.set(name, isDeleted ? null : value);
753
+ }
754
+
755
+ return result;
756
+ }
757
+
407
758
  /**
408
759
  * Parse cookies from Cookie header
409
760
  */
410
761
  function parseCookiesFromHeader(
411
- cookieHeader: string | null
762
+ cookieHeader: string | null,
412
763
  ): Record<string, string> {
413
764
  if (!cookieHeader) return {};
414
765
 
@@ -418,7 +769,13 @@ function parseCookiesFromHeader(
418
769
  for (const pair of pairs) {
419
770
  const [name, ...rest] = pair.trim().split("=");
420
771
  if (name) {
421
- cookies[name] = decodeURIComponent(rest.join("="));
772
+ const raw = rest.join("=");
773
+ try {
774
+ cookies[name] = decodeURIComponent(raw);
775
+ } catch {
776
+ // Malformed percent-encoded value (e.g. %zz, %2) - fall back to raw value
777
+ cookies[name] = raw;
778
+ }
422
779
  }
423
780
  }
424
781
 
@@ -431,7 +788,7 @@ function parseCookiesFromHeader(
431
788
  function serializeCookieValue(
432
789
  name: string,
433
790
  value: string,
434
- options: CookieOptions = {}
791
+ options: CookieOptions = {},
435
792
  ): string {
436
793
  let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
437
794
 
@@ -463,7 +820,7 @@ export interface CreateUseFunctionOptions<TEnv> {
463
820
  * - For handles: returns a push function to add handle data
464
821
  */
465
822
  export function createUseFunction<TEnv>(
466
- options: CreateUseFunctionOptions<TEnv>
823
+ options: CreateUseFunctionOptions<TEnv>,
467
824
  ): RequestContext["use"] {
468
825
  const { handleStore, loaderPromises, getContext } = options;
469
826
 
@@ -477,16 +834,19 @@ export function createUseFunction<TEnv>(
477
834
  if (!segmentId) {
478
835
  throw new Error(
479
836
  `Handle "${handle.$$id}" used outside of handler context. ` +
480
- `Handles must be used within route/layout handlers.`
837
+ `Handles must be used within route/layout handlers.`,
481
838
  );
482
839
  }
483
840
 
484
841
  // Return a push function bound to this handle and segment
485
- return (dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
842
+ return (
843
+ dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>),
844
+ ) => {
486
845
  // If it's a function, call it immediately to get the promise
487
- const valueOrPromise = typeof dataOrFn === "function"
488
- ? (dataOrFn as () => Promise<unknown>)()
489
- : dataOrFn;
846
+ const valueOrPromise =
847
+ typeof dataOrFn === "function"
848
+ ? (dataOrFn as () => Promise<unknown>)()
849
+ : dataOrFn;
490
850
 
491
851
  // Push directly - promises will be serialized by RSC and streamed
492
852
  handleStore.push(handle.$$id, segmentId, valueOrPromise);
@@ -504,8 +864,6 @@ export function createUseFunction<TEnv>(
504
864
  // Get loader function - either from loader object or fetchable registry
505
865
  let loaderFn = loader.fn;
506
866
  if (!loaderFn) {
507
- // Lazy import to avoid circular dependency
508
- const { getFetchableLoader } = require("../loader.rsc.js");
509
867
  const fetchable = getFetchableLoader(loader.$$id);
510
868
  if (fetchable) {
511
869
  loaderFn = fetchable.fn;
@@ -514,7 +872,7 @@ export function createUseFunction<TEnv>(
514
872
 
515
873
  if (!loaderFn) {
516
874
  throw new Error(
517
- `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.`
875
+ `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.`,
518
876
  );
519
877
  }
520
878
 
@@ -523,25 +881,33 @@ export function createUseFunction<TEnv>(
523
881
  // Create loader context with recursive use() support
524
882
  const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
525
883
  params: ctx.params,
884
+ routeParams: (ctx.params ?? {}) as Record<string, string>,
526
885
  request: ctx.request,
527
886
  searchParams: ctx.searchParams,
887
+ search: (ctx as any).search ?? {},
528
888
  pathname: ctx.pathname,
529
889
  url: ctx.url,
530
890
  env: ctx.env as any,
531
891
  var: ctx.var as any,
532
892
  get: ctx.get as any,
533
893
  use: <TDep, TDepParams = any>(
534
- dep: LoaderDefinition<TDep, TDepParams>
894
+ dep: LoaderDefinition<TDep, TDepParams>,
535
895
  ): Promise<TDep> => {
536
896
  // Recursive call - will start dep loader if not already started
537
897
  return ctx.use(dep);
538
898
  },
539
899
  method: "GET",
540
900
  body: undefined,
901
+ reverse: createReverseFunction(
902
+ getGlobalRouteMap(),
903
+ ctx._routeName,
904
+ ctx.params as Record<string, string>,
905
+ ctx._routeName ? isRouteRootScoped(ctx._routeName) : undefined,
906
+ ),
541
907
  };
542
908
 
543
909
  // Start loader execution with tracking
544
- const doneLoader = track(`loader:${loader.$$id}`);
910
+ const doneLoader = track(`loader:${loader.$$id}`, 2);
545
911
  const promise = Promise.resolve(loaderFn(loaderCtx)).finally(() => {
546
912
  doneLoader();
547
913
  });