@rangojs/router 0.0.0-experimental.3 → 0.0.0-experimental.30

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