@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26

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 (225) hide show
  1. package/README.md +294 -28
  2. package/dist/bin/rango.js +355 -47
  3. package/dist/vite/index.js +1658 -1239
  4. package/package.json +3 -3
  5. package/skills/cache-guide/SKILL.md +9 -5
  6. package/skills/caching/SKILL.md +4 -4
  7. package/skills/document-cache/SKILL.md +2 -2
  8. package/skills/hooks/SKILL.md +40 -29
  9. package/skills/host-router/SKILL.md +218 -0
  10. package/skills/intercept/SKILL.md +79 -0
  11. package/skills/layout/SKILL.md +62 -2
  12. package/skills/loader/SKILL.md +229 -15
  13. package/skills/middleware/SKILL.md +109 -30
  14. package/skills/parallel/SKILL.md +57 -2
  15. package/skills/prerender/SKILL.md +189 -19
  16. package/skills/rango/SKILL.md +1 -2
  17. package/skills/response-routes/SKILL.md +3 -3
  18. package/skills/route/SKILL.md +44 -3
  19. package/skills/router-setup/SKILL.md +80 -3
  20. package/skills/theme/SKILL.md +5 -4
  21. package/skills/typesafety/SKILL.md +59 -16
  22. package/skills/use-cache/SKILL.md +16 -2
  23. package/src/__internal.ts +1 -1
  24. package/src/bin/rango.ts +56 -19
  25. package/src/browser/action-coordinator.ts +97 -0
  26. package/src/browser/event-controller.ts +29 -48
  27. package/src/browser/history-state.ts +80 -0
  28. package/src/browser/intercept-utils.ts +1 -1
  29. package/src/browser/link-interceptor.ts +19 -3
  30. package/src/browser/merge-segment-loaders.ts +9 -2
  31. package/src/browser/navigation-bridge.ts +66 -443
  32. package/src/browser/navigation-client.ts +34 -62
  33. package/src/browser/navigation-store.ts +4 -33
  34. package/src/browser/navigation-transaction.ts +295 -0
  35. package/src/browser/partial-update.ts +103 -151
  36. package/src/browser/prefetch/cache.ts +67 -0
  37. package/src/browser/prefetch/fetch.ts +137 -0
  38. package/src/browser/prefetch/observer.ts +65 -0
  39. package/src/browser/prefetch/policy.ts +42 -0
  40. package/src/browser/prefetch/queue.ts +88 -0
  41. package/src/browser/rango-state.ts +112 -0
  42. package/src/browser/react/Link.tsx +154 -44
  43. package/src/browser/react/NavigationProvider.tsx +32 -0
  44. package/src/browser/react/context.ts +6 -0
  45. package/src/browser/react/filter-segment-order.ts +11 -0
  46. package/src/browser/react/index.ts +2 -6
  47. package/src/browser/react/location-state-shared.ts +29 -11
  48. package/src/browser/react/location-state.ts +6 -4
  49. package/src/browser/react/nonce-context.ts +23 -0
  50. package/src/browser/react/shallow-equal.ts +27 -0
  51. package/src/browser/react/use-action.ts +23 -45
  52. package/src/browser/react/use-client-cache.ts +5 -3
  53. package/src/browser/react/use-handle.ts +21 -64
  54. package/src/browser/react/use-navigation.ts +7 -32
  55. package/src/browser/react/use-params.ts +5 -34
  56. package/src/browser/react/use-pathname.ts +2 -3
  57. package/src/browser/react/use-router.ts +3 -6
  58. package/src/browser/react/use-search-params.ts +2 -1
  59. package/src/browser/react/use-segments.ts +75 -114
  60. package/src/browser/response-adapter.ts +73 -0
  61. package/src/browser/rsc-router.tsx +46 -22
  62. package/src/browser/scroll-restoration.ts +10 -7
  63. package/src/browser/server-action-bridge.ts +458 -405
  64. package/src/browser/types.ts +21 -35
  65. package/src/browser/validate-redirect-origin.ts +29 -0
  66. package/src/build/generate-manifest.ts +38 -13
  67. package/src/build/generate-route-types.ts +4 -0
  68. package/src/build/index.ts +1 -0
  69. package/src/build/route-trie.ts +19 -3
  70. package/src/build/route-types/codegen.ts +13 -4
  71. package/src/build/route-types/include-resolution.ts +13 -0
  72. package/src/build/route-types/per-module-writer.ts +15 -3
  73. package/src/build/route-types/router-processing.ts +170 -18
  74. package/src/build/runtime-discovery.ts +13 -1
  75. package/src/cache/background-task.ts +34 -0
  76. package/src/cache/cache-key-utils.ts +44 -0
  77. package/src/cache/cache-policy.ts +125 -0
  78. package/src/cache/cache-runtime.ts +136 -123
  79. package/src/cache/cache-scope.ts +76 -83
  80. package/src/cache/cf/cf-cache-store.ts +12 -7
  81. package/src/cache/document-cache.ts +93 -69
  82. package/src/cache/handle-capture.ts +81 -0
  83. package/src/cache/index.ts +0 -15
  84. package/src/cache/memory-segment-store.ts +43 -69
  85. package/src/cache/profile-registry.ts +43 -8
  86. package/src/cache/read-through-swr.ts +134 -0
  87. package/src/cache/segment-codec.ts +140 -117
  88. package/src/cache/taint.ts +30 -3
  89. package/src/cache/types.ts +1 -115
  90. package/src/client.rsc.tsx +0 -1
  91. package/src/client.tsx +53 -76
  92. package/src/errors.ts +6 -1
  93. package/src/handle.ts +1 -1
  94. package/src/handles/MetaTags.tsx +5 -2
  95. package/src/host/cookie-handler.ts +8 -3
  96. package/src/host/index.ts +0 -3
  97. package/src/host/router.ts +14 -1
  98. package/src/href-client.ts +3 -1
  99. package/src/index.rsc.ts +53 -10
  100. package/src/index.ts +73 -43
  101. package/src/loader.rsc.ts +12 -4
  102. package/src/loader.ts +8 -0
  103. package/src/prerender/store.ts +60 -18
  104. package/src/prerender.ts +76 -18
  105. package/src/reverse.ts +11 -7
  106. package/src/root-error-boundary.tsx +30 -26
  107. package/src/route-definition/dsl-helpers.ts +9 -6
  108. package/src/route-definition/index.ts +0 -3
  109. package/src/route-definition/redirect.ts +15 -3
  110. package/src/route-map-builder.ts +38 -2
  111. package/src/route-name.ts +53 -0
  112. package/src/route-types.ts +7 -0
  113. package/src/router/content-negotiation.ts +1 -1
  114. package/src/router/debug-manifest.ts +16 -3
  115. package/src/router/handler-context.ts +96 -17
  116. package/src/router/intercept-resolution.ts +6 -4
  117. package/src/router/lazy-includes.ts +4 -0
  118. package/src/router/loader-resolution.ts +6 -11
  119. package/src/router/logging.ts +100 -3
  120. package/src/router/manifest.ts +32 -3
  121. package/src/router/match-api.ts +62 -54
  122. package/src/router/match-context.ts +3 -0
  123. package/src/router/match-handlers.ts +185 -11
  124. package/src/router/match-middleware/background-revalidation.ts +65 -85
  125. package/src/router/match-middleware/cache-lookup.ts +78 -10
  126. package/src/router/match-middleware/cache-store.ts +2 -0
  127. package/src/router/match-pipelines.ts +8 -43
  128. package/src/router/match-result.ts +0 -9
  129. package/src/router/metrics.ts +233 -13
  130. package/src/router/middleware-types.ts +34 -39
  131. package/src/router/middleware.ts +290 -130
  132. package/src/router/pattern-matching.ts +61 -10
  133. package/src/router/prerender-match.ts +36 -6
  134. package/src/router/preview-match.ts +7 -1
  135. package/src/router/revalidation.ts +61 -2
  136. package/src/router/router-context.ts +15 -0
  137. package/src/router/router-interfaces.ts +158 -40
  138. package/src/router/router-options.ts +223 -1
  139. package/src/router/router-registry.ts +5 -2
  140. package/src/router/segment-resolution/fresh.ts +165 -242
  141. package/src/router/segment-resolution/helpers.ts +263 -0
  142. package/src/router/segment-resolution/loader-cache.ts +102 -98
  143. package/src/router/segment-resolution/revalidation.ts +394 -272
  144. package/src/router/segment-resolution/static-store.ts +2 -2
  145. package/src/router/segment-resolution.ts +1 -3
  146. package/src/router/segment-wrappers.ts +3 -0
  147. package/src/router/telemetry-otel.ts +299 -0
  148. package/src/router/telemetry.ts +300 -0
  149. package/src/router/timeout.ts +148 -0
  150. package/src/router/trie-matching.ts +20 -2
  151. package/src/router/types.ts +7 -1
  152. package/src/router.ts +203 -18
  153. package/src/rsc/handler-context.ts +13 -2
  154. package/src/rsc/handler.ts +489 -438
  155. package/src/rsc/helpers.ts +125 -5
  156. package/src/rsc/index.ts +0 -20
  157. package/src/rsc/loader-fetch.ts +84 -42
  158. package/src/rsc/manifest-init.ts +3 -2
  159. package/src/rsc/origin-guard.ts +141 -0
  160. package/src/rsc/progressive-enhancement.ts +245 -19
  161. package/src/rsc/response-route-handler.ts +347 -0
  162. package/src/rsc/rsc-rendering.ts +47 -43
  163. package/src/rsc/runtime-warnings.ts +42 -0
  164. package/src/rsc/server-action.ts +166 -66
  165. package/src/rsc/ssr-setup.ts +128 -0
  166. package/src/rsc/types.ts +20 -2
  167. package/src/search-params.ts +38 -23
  168. package/src/server/context.ts +61 -7
  169. package/src/server/cookie-store.ts +190 -0
  170. package/src/server/fetchable-loader-store.ts +11 -6
  171. package/src/server/handle-store.ts +84 -12
  172. package/src/server/loader-registry.ts +11 -46
  173. package/src/server/request-context.ts +275 -49
  174. package/src/server.ts +6 -0
  175. package/src/ssr/index.tsx +67 -28
  176. package/src/static-handler.ts +7 -0
  177. package/src/theme/ThemeProvider.tsx +6 -1
  178. package/src/theme/index.ts +4 -18
  179. package/src/theme/theme-context.ts +1 -28
  180. package/src/theme/theme-script.ts +2 -1
  181. package/src/types/cache-types.ts +6 -1
  182. package/src/types/error-types.ts +3 -0
  183. package/src/types/global-namespace.ts +22 -0
  184. package/src/types/handler-context.ts +103 -16
  185. package/src/types/index.ts +1 -1
  186. package/src/types/loader-types.ts +9 -6
  187. package/src/types/route-config.ts +17 -26
  188. package/src/types/route-entry.ts +28 -0
  189. package/src/types/segments.ts +0 -5
  190. package/src/urls/include-helper.ts +49 -8
  191. package/src/urls/index.ts +1 -0
  192. package/src/urls/path-helper-types.ts +30 -12
  193. package/src/urls/path-helper.ts +17 -2
  194. package/src/urls/pattern-types.ts +21 -1
  195. package/src/urls/response-types.ts +29 -7
  196. package/src/urls/type-extraction.ts +23 -15
  197. package/src/use-loader.tsx +27 -9
  198. package/src/vite/discovery/bundle-postprocess.ts +32 -52
  199. package/src/vite/discovery/discover-routers.ts +52 -26
  200. package/src/vite/discovery/prerender-collection.ts +58 -41
  201. package/src/vite/discovery/route-types-writer.ts +7 -7
  202. package/src/vite/discovery/state.ts +7 -7
  203. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  204. package/src/vite/index.ts +10 -51
  205. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  206. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  207. package/src/vite/plugins/expose-internal-ids.ts +4 -3
  208. package/src/vite/plugins/refresh-cmd.ts +65 -0
  209. package/src/vite/plugins/use-cache-transform.ts +91 -3
  210. package/src/vite/plugins/version-plugin.ts +188 -18
  211. package/src/vite/rango.ts +61 -36
  212. package/src/vite/router-discovery.ts +173 -100
  213. package/src/vite/utils/prerender-utils.ts +81 -0
  214. package/src/vite/utils/shared-utils.ts +19 -9
  215. package/skills/testing/SKILL.md +0 -226
  216. package/src/browser/lru-cache.ts +0 -61
  217. package/src/browser/react/prefetch.ts +0 -27
  218. package/src/browser/request-controller.ts +0 -164
  219. package/src/cache/memory-store.ts +0 -253
  220. package/src/href-context.ts +0 -33
  221. package/src/route-definition/route-function.ts +0 -119
  222. package/src/router.gen.ts +0 -6
  223. package/src/static-handler.gen.ts +0 -5
  224. package/src/urls.gen.ts +0 -8
  225. /package/{CLAUDE.md → AGENTS.md} +0 -0
@@ -6,13 +6,10 @@
6
6
  */
7
7
 
8
8
  import type { LoaderFn } from "../types.js";
9
- import type { MiddlewareFn } from "../router/middleware.js";
10
- import { getFetchableLoader } from "./fetchable-loader-store.js";
11
-
12
- interface RegisteredLoader {
13
- fn: LoaderFn<any, any, any>;
14
- middleware: MiddlewareFn[];
15
- }
9
+ import {
10
+ getFetchableLoader,
11
+ type LoaderRegistryEntry,
12
+ } from "./fetchable-loader-store.js";
16
13
 
17
14
  // Server-side cache - maps loader $$id to function and middleware
18
15
  // This is a CACHE populated by getLoaderLazy() when loaders are first accessed.
@@ -21,7 +18,7 @@ interface RegisteredLoader {
21
18
  // 1. Avoid repeated lookups/imports for the same loader
22
19
  // 2. Support lazy loading in production (loaders imported on-demand)
23
20
  // 3. Provide a stable reference for the RSC handler
24
- const loaderRegistry = new Map<string, RegisteredLoader>();
21
+ const loaderRegistry = new Map<string, LoaderRegistryEntry>();
25
22
 
26
23
  // Lazy import map - set by the loader manifest
27
24
  // Maps loader $$id to a function that imports the loader module
@@ -37,28 +34,6 @@ export function setLoaderImports(
37
34
  lazyLoaderImports = new Map(Object.entries(imports));
38
35
  }
39
36
 
40
- /**
41
- * Register a fetchable loader by $$id
42
- * Called by createLoader when fetchable option is provided
43
- */
44
- export function registerLoader(
45
- id: string,
46
- fn: LoaderFn<any, any, any>,
47
- middleware: MiddlewareFn[] = [],
48
- ): void {
49
- // Always update the registry entry. During HMR, the module is re-executed
50
- // with the new loader function, so we must replace the stale reference.
51
- loaderRegistry.set(id, { fn, middleware });
52
- }
53
-
54
- /**
55
- * Get a registered loader by $$id (synchronous)
56
- * Returns undefined if loader is not registered
57
- */
58
- export function getLoader(id: string): RegisteredLoader | undefined {
59
- return loaderRegistry.get(id);
60
- }
61
-
62
37
  /**
63
38
  * Get a loader by $$id, loading it lazily if needed
64
39
  * This is the primary method for the RSC handler to get loaders
@@ -68,7 +43,7 @@ export function getLoader(id: string): RegisteredLoader | undefined {
68
43
  */
69
44
  export async function getLoaderLazy(
70
45
  id: string,
71
- ): Promise<RegisteredLoader | undefined> {
46
+ ): Promise<LoaderRegistryEntry | undefined> {
72
47
  // Check if already cached in main registry
73
48
  const existing = loaderRegistry.get(id);
74
49
  if (existing) {
@@ -128,20 +103,6 @@ export async function getLoaderLazy(
128
103
  return undefined;
129
104
  }
130
105
 
131
- /**
132
- * Check if a loader is registered by $$id
133
- */
134
- export function hasLoader(id: string): boolean {
135
- return loaderRegistry.has(id) || getFetchableLoader(id) !== undefined;
136
- }
137
-
138
- /**
139
- * Get all registered loader IDs (for debugging)
140
- */
141
- export function getRegisteredLoaderIds(): string[] {
142
- return Array.from(loaderRegistry.keys());
143
- }
144
-
145
106
  /**
146
107
  * Register a loader by its $$id (injected by Vite plugin)
147
108
  * This is called during module loading to cache loaders
@@ -163,6 +124,10 @@ export function registerLoaderById(loader: {
163
124
 
164
125
  // Fall back to using fn from the loader object (non-fetchable loaders)
165
126
  if (loader.fn) {
166
- loaderRegistry.set(loader.$$id, { fn: loader.fn, middleware: [] });
127
+ loaderRegistry.set(loader.$$id, {
128
+ fn: loader.fn,
129
+ middleware: [],
130
+ fetchable: false,
131
+ });
167
132
  }
168
133
  }
@@ -13,11 +13,17 @@
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";
17
23
  import { type ContextVar, contextGet, contextSet } from "../context-var.js";
18
24
  import { createHandleStore, type HandleStore } from "./handle-store.js";
19
25
  import { isHandle } from "../handle.js";
20
- import { track } from "./context.js";
26
+ import { track, type MetricsStore } from "./context.js";
21
27
  import { getFetchableLoader } from "./fetchable-loader-store.js";
22
28
  import type { SegmentCacheStore } from "../cache/types.js";
23
29
  import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
@@ -25,7 +31,9 @@ import { THEME_COOKIE } from "../theme/constants.js";
25
31
  import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
26
32
  import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
27
33
  import { createReverseFunction } from "../router/handler-context.js";
28
- import { getGlobalRouteMap } from "../route-map-builder.js";
34
+ import { getGlobalRouteMap, isRouteRootScoped } from "../route-map-builder.js";
35
+ import { invariant } from "../errors.js";
36
+ import { isAutoGeneratedRouteName } from "../route-name.js";
29
37
 
30
38
  /**
31
39
  * Unified request context available via getRequestContext()
@@ -34,7 +42,7 @@ import { getGlobalRouteMap } from "../route-map-builder.js";
34
42
  * Use this when you need access to request data outside of route handlers.
35
43
  */
36
44
  export interface RequestContext<
37
- TEnv = unknown,
45
+ TEnv = DefaultEnv,
38
46
  TParams = Record<string, string>,
39
47
  > {
40
48
  /** Platform bindings (Cloudflare env, etc.) */
@@ -65,24 +73,28 @@ export interface RequestContext<
65
73
  */
66
74
  params: TParams;
67
75
  /**
68
- * Stub response for setting headers/cookies
69
- * Headers set here are merged into the final response
76
+ * Stub response for setting headers/cookies (read-only).
77
+ * Headers set here are merged into the final response.
78
+ * Use header() or setStatus() to mutate response headers/status.
79
+ * Use cookies().set()/cookies().delete() for cookie mutations.
70
80
  */
71
- res: Response;
81
+ readonly res: Response;
72
82
 
73
- /** Get a cookie value from the request */
83
+ /** @internal Get a cookie value (effective: request + response mutations). Use cookies().get() instead. */
74
84
  cookie(name: string): string | undefined;
75
- /** Get all cookies from the request */
85
+ /** @internal Get all cookies (effective merged view). Use cookies().getAll() instead. */
76
86
  cookies(): Record<string, string>;
77
- /** Set a cookie on the response */
87
+ /** @internal Set a cookie on the response. Use cookies().set() instead. */
78
88
  setCookie(name: string, value: string, options?: CookieOptions): void;
79
- /** Delete a cookie */
89
+ /** @internal Delete a cookie. Use cookies().delete() instead. */
80
90
  deleteCookie(
81
91
  name: string,
82
92
  options?: Pick<CookieOptions, "domain" | "path">,
83
93
  ): void;
84
94
  /** Set a response header */
85
95
  header(name: string, value: string): void;
96
+ /** Set the response status code */
97
+ setStatus(status: number): void;
86
98
 
87
99
  /**
88
100
  * Access loader data or push handle data.
@@ -121,6 +133,12 @@ export interface RequestContext<
121
133
  /** @internal Cache store for segment caching (optional, used by CacheScope) */
122
134
  _cacheStore?: SegmentCacheStore;
123
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
+
124
142
  /**
125
143
  * Schedule work to run after the response is sent.
126
144
  * On Cloudflare Workers, uses ctx.waitUntil().
@@ -207,29 +225,84 @@ export interface RequestContext<
207
225
  *
208
226
  * @example
209
227
  * ```typescript
210
- * ctx.setLocationState([Flash({ text: "Item saved!" })]);
228
+ * ctx.setLocationState(Flash({ text: "Item saved!" }));
211
229
  * ```
212
230
  */
213
- setLocationState(entries: LocationStateEntry[]): void;
231
+ setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void;
214
232
 
215
233
  /** @internal Accumulated location state entries */
216
234
  _locationState?: LocationStateEntry[];
217
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
+
218
243
  /**
219
244
  * Generate URLs from route names.
220
245
  * Uses the global route map. After route matching, scoped (`.name`) resolution
221
246
  * works within the matched include() scope.
222
247
  */
223
- reverse(
224
- name: string,
225
- params?: Record<string, string>,
226
- search?: Record<string, unknown>,
227
- ): string;
248
+ reverse: ScopedReverseFunction<
249
+ Record<string, string>,
250
+ DefaultReverseRouteMap
251
+ >;
228
252
 
229
253
  /** @internal Route name from route matching, used for scoped reverse resolution */
230
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;
231
275
  }
232
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
+ >;
305
+
233
306
  // AsyncLocalStorage instance for request context
234
307
  const requestContextStorage = new AsyncLocalStorage<RequestContext<any>>();
235
308
 
@@ -246,9 +319,26 @@ export function runWithRequestContext<TEnv, T>(
246
319
 
247
320
  /**
248
321
  * Get the current request context
249
- * Returns undefined if not running within a request context
322
+ * Throws if called outside of a request context
323
+ */
324
+ export function getRequestContext<TEnv = DefaultEnv>(): RequestContext<TEnv> {
325
+ const ctx = requestContextStorage.getStore() as
326
+ | RequestContext<TEnv>
327
+ | undefined;
328
+ invariant(
329
+ ctx,
330
+ "getRequestContext() called outside of a request context. " +
331
+ "This function must be called from within a route handler, loader, middleware, " +
332
+ "server action, or server component.",
333
+ );
334
+ return ctx;
335
+ }
336
+
337
+ /**
338
+ * @internal Get the request context without throwing — for internal code that
339
+ * may run outside a request context (cache stores, optional handle lookups, etc.)
250
340
  */
251
- export function getRequestContext<TEnv = unknown>():
341
+ export function _getRequestContext<TEnv = DefaultEnv>():
252
342
  | RequestContext<TEnv>
253
343
  | undefined {
254
344
  return requestContextStorage.getStore() as RequestContext<TEnv> | undefined;
@@ -267,9 +357,34 @@ export function setRequestContextParams(
267
357
  ctx.params = params;
268
358
  if (routeName !== undefined) {
269
359
  ctx._routeName = routeName;
360
+ ctx.routeName = (
361
+ routeName && !isAutoGeneratedRouteName(routeName)
362
+ ? routeName
363
+ : undefined
364
+ ) as DefaultRouteName | undefined;
270
365
  }
271
366
  // Update reverse with scoped resolution now that route is known
272
- ctx.reverse = createReverseFunction(getGlobalRouteMap(), routeName, params);
367
+ ctx.reverse = createReverseFunction(
368
+ getGlobalRouteMap(),
369
+ routeName,
370
+ params,
371
+ routeName ? isRouteRootScoped(routeName) : undefined,
372
+ );
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Store the previous route key on the request context.
378
+ * Called during partial-match context creation to make the navigation source
379
+ * route key available for revalidation and intercept evaluation.
380
+ * @internal
381
+ */
382
+ export function setRequestContextPrevRouteKey(
383
+ prevRouteKey: string | undefined,
384
+ ): void {
385
+ const ctx = requestContextStorage.getStore();
386
+ if (ctx && prevRouteKey !== undefined) {
387
+ ctx._prevRouteKey = prevRouteKey;
273
388
  }
274
389
  }
275
390
 
@@ -286,17 +401,12 @@ export function getLocationState(): LocationStateEntry[] | undefined {
286
401
 
287
402
  /**
288
403
  * Get the current request context, throwing if not available
289
- * Use this when context is required (e.g., in loader actions)
404
+ * @deprecated Use getRequestContext() directly it now throws if outside context
290
405
  */
291
- export function requireRequestContext<TEnv = unknown>(): RequestContext<TEnv> {
292
- const ctx = getRequestContext<TEnv>();
293
- if (!ctx) {
294
- throw new Error(
295
- "Request context not available. This function must be called from within a server action " +
296
- "executed through the RSC handler.",
297
- );
298
- }
299
- return ctx;
406
+ export function requireRequestContext<
407
+ TEnv = DefaultEnv,
408
+ >(): RequestContext<TEnv> {
409
+ return getRequestContext<TEnv>();
300
410
  }
301
411
 
302
412
  /**
@@ -315,8 +425,15 @@ export interface CreateRequestContextOptions<TEnv> {
315
425
  request: Request;
316
426
  url: URL;
317
427
  variables: Record<string, any>;
428
+ /** Optional initial response stub headers/status to seed effective cookie reads */
429
+ initialResponse?: Response;
318
430
  /** Optional cache store for segment caching (used by CacheScope) */
319
431
  cacheStore?: SegmentCacheStore;
432
+ /** Optional cache profiles for "use cache" resolution (per-router) */
433
+ cacheProfiles?: Record<
434
+ string,
435
+ import("../cache/profile-registry.js").CacheProfile
436
+ >;
320
437
  /** Optional Cloudflare execution context for waitUntil support */
321
438
  executionContext?: ExecutionContext;
322
439
  /** Optional theme configuration (enables ctx.theme and ctx.setTheme) */
@@ -339,21 +456,30 @@ export function createRequestContext<TEnv>(
339
456
  request,
340
457
  url,
341
458
  variables,
459
+ initialResponse,
342
460
  cacheStore,
461
+ cacheProfiles,
343
462
  executionContext,
344
463
  themeConfig,
345
464
  } = options;
346
465
  const cookieHeader = request.headers.get("Cookie");
347
466
  let parsedCookies: Record<string, string> | null = null;
348
467
 
349
- // Create stub response for collecting headers/cookies
350
- const stubResponse = new Response(null, { status: 200 });
468
+ // Create stub response for collecting headers/cookies.
469
+ // All cookie/header mutations go here; cookie reads derive from it.
470
+ let stubResponse = initialResponse
471
+ ? new Response(null, {
472
+ status: initialResponse.status,
473
+ statusText: initialResponse.statusText,
474
+ headers: new Headers(initialResponse.headers),
475
+ })
476
+ : new Response(null, { status: 200 });
351
477
 
352
478
  // Create handle store and loader memoization for this request
353
479
  const handleStore = createHandleStore();
354
480
  const loaderPromises = new Map<string, Promise<any>>();
355
481
 
356
- // Lazy parse cookies
482
+ // Lazy parse cookies from the original Cookie header
357
483
  const getParsedCookies = (): Record<string, string> => {
358
484
  if (!parsedCookies) {
359
485
  parsedCookies = parseCookiesFromHeader(cookieHeader);
@@ -361,11 +487,35 @@ export function createRequestContext<TEnv>(
361
487
  return parsedCookies;
362
488
  };
363
489
 
490
+ // Cached response cookie mutations — invalidated on setCookie/deleteCookie/setTheme
491
+ let responseCookieCache: Map<string, string | null> | null = null;
492
+ const getResponseCookies = (): Map<string, string | null> => {
493
+ if (!responseCookieCache) {
494
+ responseCookieCache = parseResponseCookies(stubResponse);
495
+ }
496
+ return responseCookieCache;
497
+ };
498
+ const invalidateResponseCookieCache = () => {
499
+ responseCookieCache = null;
500
+ };
501
+
502
+ // Effective cookie read: response stub Set-Cookie wins, then original header.
503
+ // The stub IS the source of truth for same-request mutations.
504
+ const effectiveCookie = (name: string): string | undefined => {
505
+ const mutations = getResponseCookies();
506
+ if (mutations.has(name)) {
507
+ const v = mutations.get(name);
508
+ return v === null ? undefined : v;
509
+ }
510
+ return getParsedCookies()[name];
511
+ };
512
+
364
513
  // Theme helpers (only used when themeConfig is provided)
365
514
  const getTheme = (): Theme | undefined => {
366
515
  if (!themeConfig) return undefined;
367
516
 
368
- const stored = getParsedCookies()[themeConfig.storageKey];
517
+ // Use overlay-aware read so setTheme() in the same request is reflected
518
+ const stored = effectiveCookie(themeConfig.storageKey);
369
519
  if (stored) {
370
520
  // Validate stored value
371
521
  if (stored === "system" && themeConfig.enableSystem) {
@@ -389,7 +539,7 @@ export function createRequestContext<TEnv>(
389
539
  return;
390
540
  }
391
541
 
392
- // Set cookie
542
+ // Write to stub — effectiveCookie() will pick it up on next read
393
543
  stubResponse.headers.append(
394
544
  "Set-Cookie",
395
545
  serializeCookieValue(themeConfig.storageKey, theme, {
@@ -398,6 +548,7 @@ export function createRequestContext<TEnv>(
398
548
  sameSite: THEME_COOKIE.sameSite,
399
549
  }),
400
550
  );
551
+ invalidateResponseCookieCache();
401
552
  };
402
553
 
403
554
  // Build the context object first (without use), then add use
@@ -415,14 +566,37 @@ export function createRequestContext<TEnv>(
415
566
  contextSet(variables, keyOrVar, value);
416
567
  }) as RequestContext<TEnv>["set"],
417
568
  params: {} as Record<string, string>,
418
- res: stubResponse,
569
+
570
+ get res(): Response {
571
+ return stubResponse;
572
+ },
573
+ set res(_: Response) {
574
+ throw new Error(
575
+ "ctx.res is read-only. Use ctx.header() to set response headers, or cookies() for cookie mutations.",
576
+ );
577
+ },
419
578
 
420
579
  cookie(name: string): string | undefined {
421
- return getParsedCookies()[name];
580
+ return effectiveCookie(name);
422
581
  },
423
582
 
424
583
  cookies(): Record<string, string> {
425
- return { ...getParsedCookies() };
584
+ const parsed = getParsedCookies();
585
+ const mutations = getResponseCookies();
586
+ if (mutations.size === 0) return { ...parsed };
587
+ // Build result without delete (avoids V8 dictionary-mode de-opt)
588
+ const deleted = new Set<string>();
589
+ for (const [k, v] of mutations) {
590
+ if (v === null) deleted.add(k);
591
+ }
592
+ const result: Record<string, string> = {};
593
+ for (const key of Object.keys(parsed)) {
594
+ if (!deleted.has(key)) result[key] = parsed[key];
595
+ }
596
+ for (const [k, v] of mutations) {
597
+ if (v !== null) result[k] = v;
598
+ }
599
+ return result;
426
600
  },
427
601
 
428
602
  setCookie(name: string, value: string, options?: CookieOptions): void {
@@ -431,6 +605,7 @@ export function createRequestContext<TEnv>(
431
605
  "Set-Cookie",
432
606
  serializeCookieValue(name, value, options),
433
607
  );
608
+ invalidateResponseCookieCache();
434
609
  },
435
610
 
436
611
  deleteCookie(
@@ -442,6 +617,7 @@ export function createRequestContext<TEnv>(
442
617
  "Set-Cookie",
443
618
  serializeCookieValue(name, "", { ...options, maxAge: 0 }),
444
619
  );
620
+ invalidateResponseCookieCache();
445
621
  },
446
622
 
447
623
  header(name: string, value: string): void {
@@ -449,6 +625,16 @@ export function createRequestContext<TEnv>(
449
625
  stubResponse.headers.set(name, value);
450
626
  },
451
627
 
628
+ setStatus(status: number): void {
629
+ assertNotInsideCacheExec(ctx, "setStatus");
630
+ // Response.status is read-only, so we must create a new Response.
631
+ // Headers are passed by reference — no cookie cache invalidation needed.
632
+ stubResponse = new Response(null, {
633
+ status,
634
+ headers: stubResponse.headers,
635
+ });
636
+ },
637
+
452
638
  // Placeholder - will be replaced below
453
639
  use: null as any,
454
640
 
@@ -456,6 +642,7 @@ export function createRequestContext<TEnv>(
456
642
 
457
643
  _handleStore: handleStore,
458
644
  _cacheStore: cacheStore,
645
+ _cacheProfiles: cacheProfiles,
459
646
 
460
647
  waitUntil(fn: () => Promise<void>): void {
461
648
  if (executionContext?.waitUntil) {
@@ -477,7 +664,9 @@ export function createRequestContext<TEnv>(
477
664
  },
478
665
 
479
666
  // Theme properties (only set when themeConfig is provided)
480
- theme: themeConfig ? getTheme() : undefined,
667
+ get theme() {
668
+ return themeConfig ? getTheme() : undefined;
669
+ },
481
670
  setTheme: themeConfig
482
671
  ? (theme: Theme) => {
483
672
  assertNotInsideCacheExec(ctx, "setTheme");
@@ -486,14 +675,18 @@ export function createRequestContext<TEnv>(
486
675
  : undefined,
487
676
  _themeConfig: themeConfig,
488
677
 
489
- setLocationState(entries: LocationStateEntry[]): void {
678
+ setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void {
490
679
  assertNotInsideCacheExec(ctx, "setLocationState");
680
+ const arr = Array.isArray(entries) ? entries : [entries];
491
681
  this._locationState = this._locationState
492
- ? [...this._locationState, ...entries]
493
- : entries;
682
+ ? [...this._locationState, ...arr]
683
+ : arr;
494
684
  },
495
685
  _locationState: undefined,
496
686
 
687
+ _reportedErrors: new WeakSet<object>(),
688
+ _metricsStore: undefined,
689
+
497
690
  reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
498
691
  };
499
692
 
@@ -509,6 +702,43 @@ export function createRequestContext<TEnv>(
509
702
  return ctx;
510
703
  }
511
704
 
705
+ /**
706
+ * Parse Set-Cookie headers from a response into effective cookie state.
707
+ * Returns a map of cookie name -> value (string) or name -> null (deleted).
708
+ * Last-write-wins: later Set-Cookie entries for the same name overwrite earlier ones.
709
+ * Max-Age=0 is treated as a delete.
710
+ */
711
+ const MAX_AGE_ZERO_RE = /;\s*Max-Age\s*=\s*0/i;
712
+
713
+ function parseResponseCookies(response: Response): Map<string, string | null> {
714
+ const result = new Map<string, string | null>();
715
+ const setCookies = response.headers.getSetCookie();
716
+
717
+ for (const header of setCookies) {
718
+ // First segment before ';' is the name=value pair
719
+ const semiIdx = header.indexOf(";");
720
+ const pair = semiIdx === -1 ? header : header.substring(0, semiIdx);
721
+ const eqIdx = pair.indexOf("=");
722
+ if (eqIdx === -1) continue;
723
+
724
+ let name: string;
725
+ let value: string;
726
+ try {
727
+ name = decodeURIComponent(pair.substring(0, eqIdx).trim());
728
+ value = decodeURIComponent(pair.substring(eqIdx + 1).trim());
729
+ } catch {
730
+ // Malformed encoding — skip this entry
731
+ continue;
732
+ }
733
+
734
+ // Max-Age=0 means the cookie is being deleted
735
+ const isDeleted = MAX_AGE_ZERO_RE.test(header);
736
+ result.set(name, isDeleted ? null : value);
737
+ }
738
+
739
+ return result;
740
+ }
741
+
512
742
  /**
513
743
  * Parse cookies from Cookie header
514
744
  */
@@ -635,6 +865,7 @@ export function createUseFunction<TEnv>(
635
865
  // Create loader context with recursive use() support
636
866
  const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
637
867
  params: ctx.params,
868
+ routeParams: (ctx.params ?? {}) as Record<string, string>,
638
869
  request: ctx.request,
639
870
  searchParams: ctx.searchParams,
640
871
  search: (ctx as any).search ?? {},
@@ -643,12 +874,6 @@ export function createUseFunction<TEnv>(
643
874
  env: ctx.env as any,
644
875
  var: ctx.var as any,
645
876
  get: ctx.get as any,
646
- cookie(name: string) {
647
- return ctx.cookie(name);
648
- },
649
- cookies() {
650
- return ctx.cookies();
651
- },
652
877
  use: <TDep, TDepParams = any>(
653
878
  dep: LoaderDefinition<TDep, TDepParams>,
654
879
  ): Promise<TDep> => {
@@ -661,11 +886,12 @@ export function createUseFunction<TEnv>(
661
886
  getGlobalRouteMap(),
662
887
  ctx._routeName,
663
888
  ctx.params as Record<string, string>,
889
+ ctx._routeName ? isRouteRootScoped(ctx._routeName) : undefined,
664
890
  ),
665
891
  };
666
892
 
667
893
  // Start loader execution with tracking
668
- const doneLoader = track(`loader:${loader.$$id}`);
894
+ const doneLoader = track(`loader:${loader.$$id}`, 2);
669
895
  const promise = Promise.resolve(loaderFn(loaderCtx)).finally(() => {
670
896
  doneLoader();
671
897
  });
package/src/server.ts CHANGED
@@ -11,6 +11,12 @@
11
11
  // Router registry (used by Vite plugin for build-time discovery)
12
12
  export { RSC_ROUTER_BRAND, RouterRegistry } from "./router.js";
13
13
 
14
+ // Host router registry (used by Vite plugin for host-router lazy discovery)
15
+ export {
16
+ HostRouterRegistry,
17
+ type HostRouterRegistryEntry,
18
+ } from "./host/router.js";
19
+
14
20
  // Route map builder (Vite plugin injects these via virtual modules)
15
21
  export {
16
22
  registerRouteMap,