@rangojs/router 0.0.0-experimental.20 → 0.0.0-experimental.20dbba0c

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 (189) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +172 -50
  3. package/dist/bin/rango.js +138 -50
  4. package/dist/vite/index.js +1160 -508
  5. package/dist/vite/index.js.bak +5448 -0
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +17 -16
  8. package/skills/breadcrumbs/SKILL.md +252 -0
  9. package/skills/cache-guide/SKILL.md +32 -0
  10. package/skills/caching/SKILL.md +49 -8
  11. package/skills/document-cache/SKILL.md +2 -2
  12. package/skills/handler-use/SKILL.md +362 -0
  13. package/skills/hooks/SKILL.md +61 -51
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +20 -0
  16. package/skills/layout/SKILL.md +22 -0
  17. package/skills/links/SKILL.md +91 -17
  18. package/skills/loader/SKILL.md +107 -24
  19. package/skills/middleware/SKILL.md +34 -3
  20. package/skills/migrate-nextjs/SKILL.md +560 -0
  21. package/skills/migrate-react-router/SKILL.md +765 -0
  22. package/skills/parallel/SKILL.md +185 -0
  23. package/skills/prerender/SKILL.md +112 -70
  24. package/skills/rango/SKILL.md +24 -23
  25. package/skills/response-routes/SKILL.md +8 -0
  26. package/skills/route/SKILL.md +58 -4
  27. package/skills/router-setup/SKILL.md +95 -5
  28. package/skills/streams-and-websockets/SKILL.md +283 -0
  29. package/skills/typesafety/SKILL.md +38 -24
  30. package/src/__internal.ts +92 -0
  31. package/src/browser/app-shell.ts +52 -0
  32. package/src/browser/app-version.ts +14 -0
  33. package/src/browser/event-controller.ts +5 -0
  34. package/src/browser/link-interceptor.ts +4 -0
  35. package/src/browser/navigation-bridge.ts +175 -17
  36. package/src/browser/navigation-client.ts +177 -44
  37. package/src/browser/navigation-store.ts +68 -9
  38. package/src/browser/navigation-transaction.ts +11 -9
  39. package/src/browser/partial-update.ts +113 -17
  40. package/src/browser/prefetch/cache.ts +275 -28
  41. package/src/browser/prefetch/fetch.ts +191 -46
  42. package/src/browser/prefetch/policy.ts +6 -0
  43. package/src/browser/prefetch/queue.ts +123 -20
  44. package/src/browser/prefetch/resource-ready.ts +77 -0
  45. package/src/browser/rango-state.ts +53 -13
  46. package/src/browser/react/Link.tsx +98 -14
  47. package/src/browser/react/NavigationProvider.tsx +89 -14
  48. package/src/browser/react/context.ts +7 -2
  49. package/src/browser/react/use-handle.ts +9 -58
  50. package/src/browser/react/use-navigation.ts +22 -2
  51. package/src/browser/react/use-params.ts +11 -1
  52. package/src/browser/react/use-router.ts +29 -9
  53. package/src/browser/rsc-router.tsx +177 -66
  54. package/src/browser/scroll-restoration.ts +41 -42
  55. package/src/browser/segment-reconciler.ts +36 -9
  56. package/src/browser/server-action-bridge.ts +8 -6
  57. package/src/browser/types.ts +73 -5
  58. package/src/build/generate-manifest.ts +6 -6
  59. package/src/build/generate-route-types.ts +3 -0
  60. package/src/build/route-trie.ts +67 -25
  61. package/src/build/route-types/include-resolution.ts +8 -1
  62. package/src/build/route-types/router-processing.ts +223 -74
  63. package/src/build/route-types/scan-filter.ts +8 -1
  64. package/src/cache/cache-runtime.ts +15 -11
  65. package/src/cache/cache-scope.ts +48 -7
  66. package/src/cache/cf/cf-cache-store.ts +455 -15
  67. package/src/cache/cf/index.ts +5 -1
  68. package/src/cache/document-cache.ts +17 -7
  69. package/src/cache/index.ts +1 -0
  70. package/src/cache/taint.ts +55 -0
  71. package/src/client.rsc.tsx +2 -1
  72. package/src/client.tsx +85 -276
  73. package/src/context-var.ts +72 -2
  74. package/src/debug.ts +2 -2
  75. package/src/handle.ts +40 -0
  76. package/src/handles/breadcrumbs.ts +66 -0
  77. package/src/handles/index.ts +1 -0
  78. package/src/host/index.ts +0 -3
  79. package/src/index.rsc.ts +9 -36
  80. package/src/index.ts +79 -70
  81. package/src/outlet-context.ts +1 -1
  82. package/src/prerender/store.ts +57 -15
  83. package/src/prerender.ts +138 -77
  84. package/src/response-utils.ts +28 -0
  85. package/src/reverse.ts +27 -2
  86. package/src/route-definition/dsl-helpers.ts +240 -40
  87. package/src/route-definition/helpers-types.ts +67 -19
  88. package/src/route-definition/index.ts +3 -3
  89. package/src/route-definition/redirect.ts +11 -3
  90. package/src/route-definition/resolve-handler-use.ts +155 -0
  91. package/src/route-map-builder.ts +7 -1
  92. package/src/route-types.ts +18 -0
  93. package/src/router/content-negotiation.ts +100 -1
  94. package/src/router/find-match.ts +4 -2
  95. package/src/router/handler-context.ts +129 -26
  96. package/src/router/intercept-resolution.ts +11 -4
  97. package/src/router/lazy-includes.ts +10 -7
  98. package/src/router/loader-resolution.ts +160 -22
  99. package/src/router/logging.ts +5 -2
  100. package/src/router/manifest.ts +31 -16
  101. package/src/router/match-api.ts +128 -193
  102. package/src/router/match-middleware/background-revalidation.ts +30 -2
  103. package/src/router/match-middleware/cache-lookup.ts +94 -17
  104. package/src/router/match-middleware/cache-store.ts +53 -10
  105. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  106. package/src/router/match-middleware/segment-resolution.ts +61 -5
  107. package/src/router/match-result.ts +103 -18
  108. package/src/router/metrics.ts +238 -13
  109. package/src/router/middleware-types.ts +48 -27
  110. package/src/router/middleware.ts +201 -86
  111. package/src/router/navigation-snapshot.ts +182 -0
  112. package/src/router/pattern-matching.ts +77 -11
  113. package/src/router/prerender-match.ts +114 -10
  114. package/src/router/preview-match.ts +30 -102
  115. package/src/router/request-classification.ts +310 -0
  116. package/src/router/revalidation.ts +27 -7
  117. package/src/router/route-snapshot.ts +245 -0
  118. package/src/router/router-context.ts +6 -1
  119. package/src/router/router-interfaces.ts +50 -5
  120. package/src/router/router-options.ts +50 -19
  121. package/src/router/segment-resolution/fresh.ts +215 -19
  122. package/src/router/segment-resolution/helpers.ts +30 -25
  123. package/src/router/segment-resolution/loader-cache.ts +1 -0
  124. package/src/router/segment-resolution/revalidation.ts +454 -301
  125. package/src/router/segment-wrappers.ts +2 -0
  126. package/src/router/trie-matching.ts +30 -6
  127. package/src/router/types.ts +1 -0
  128. package/src/router/url-params.ts +49 -0
  129. package/src/router.ts +89 -17
  130. package/src/rsc/handler.ts +563 -364
  131. package/src/rsc/helpers.ts +69 -41
  132. package/src/rsc/index.ts +0 -20
  133. package/src/rsc/loader-fetch.ts +23 -3
  134. package/src/rsc/manifest-init.ts +5 -1
  135. package/src/rsc/progressive-enhancement.ts +37 -10
  136. package/src/rsc/response-route-handler.ts +14 -1
  137. package/src/rsc/rsc-rendering.ts +47 -44
  138. package/src/rsc/server-action.ts +24 -10
  139. package/src/rsc/ssr-setup.ts +128 -0
  140. package/src/rsc/types.ts +11 -1
  141. package/src/search-params.ts +16 -13
  142. package/src/segment-content-promise.ts +67 -0
  143. package/src/segment-loader-promise.ts +122 -0
  144. package/src/segment-system.tsx +109 -23
  145. package/src/server/context.ts +174 -19
  146. package/src/server/handle-store.ts +19 -0
  147. package/src/server/loader-registry.ts +9 -8
  148. package/src/server/request-context.ts +218 -65
  149. package/src/server.ts +6 -0
  150. package/src/ssr/index.tsx +4 -0
  151. package/src/static-handler.ts +18 -6
  152. package/src/theme/index.ts +4 -13
  153. package/src/types/cache-types.ts +4 -4
  154. package/src/types/handler-context.ts +140 -72
  155. package/src/types/loader-types.ts +41 -15
  156. package/src/types/request-scope.ts +126 -0
  157. package/src/types/route-config.ts +17 -8
  158. package/src/types/route-entry.ts +19 -1
  159. package/src/types/segments.ts +2 -5
  160. package/src/urls/include-helper.ts +24 -14
  161. package/src/urls/path-helper-types.ts +39 -6
  162. package/src/urls/path-helper.ts +48 -13
  163. package/src/urls/pattern-types.ts +12 -0
  164. package/src/urls/response-types.ts +18 -16
  165. package/src/use-loader.tsx +77 -5
  166. package/src/vite/discovery/bundle-postprocess.ts +61 -89
  167. package/src/vite/discovery/discover-routers.ts +7 -4
  168. package/src/vite/discovery/prerender-collection.ts +162 -88
  169. package/src/vite/discovery/state.ts +17 -13
  170. package/src/vite/index.ts +8 -3
  171. package/src/vite/plugin-types.ts +51 -79
  172. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  173. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  174. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  175. package/src/vite/plugins/expose-action-id.ts +1 -3
  176. package/src/vite/plugins/expose-id-utils.ts +12 -0
  177. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  178. package/src/vite/plugins/expose-internal-ids.ts +257 -40
  179. package/src/vite/plugins/performance-tracks.ts +88 -0
  180. package/src/vite/plugins/refresh-cmd.ts +127 -0
  181. package/src/vite/plugins/version-plugin.ts +13 -1
  182. package/src/vite/rango.ts +190 -217
  183. package/src/vite/router-discovery.ts +241 -45
  184. package/src/vite/utils/banner.ts +4 -4
  185. package/src/vite/utils/package-resolution.ts +34 -1
  186. package/src/vite/utils/prerender-utils.ts +97 -5
  187. package/src/vite/utils/shared-utils.ts +3 -2
  188. package/skills/testing/SKILL.md +0 -226
  189. package/src/route-definition/route-function.ts +0 -119
@@ -20,17 +20,33 @@ import type {
20
20
  DefaultRouteName,
21
21
  } from "../types/global-namespace.js";
22
22
  import type { Handle } from "../handle.js";
23
- import { type ContextVar, contextGet, contextSet } from "../context-var.js";
24
- import { createHandleStore, type HandleStore } from "./handle-store.js";
23
+ import {
24
+ type ContextVar,
25
+ contextGet,
26
+ contextSet,
27
+ isNonCacheable,
28
+ } from "../context-var.js";
29
+ import {
30
+ createHandleStore,
31
+ buildHandleSnapshot,
32
+ type HandleStore,
33
+ type HandleData,
34
+ } from "./handle-store.js";
25
35
  import { isHandle } from "../handle.js";
26
- import { track } from "./context.js";
36
+ import { track, type MetricsStore } from "./context.js";
27
37
  import { getFetchableLoader } from "./fetchable-loader-store.js";
28
38
  import type { SegmentCacheStore } from "../cache/types.js";
29
39
  import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
40
+ import type { ExecutionContext, RequestScope } from "../types/request-scope.js";
41
+ import { fireAndForgetWaitUntil } from "../types/request-scope.js";
30
42
  import { THEME_COOKIE } from "../theme/constants.js";
31
43
  import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
32
44
  import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
33
- import { createReverseFunction } from "../router/handler-context.js";
45
+ import { isInsideCacheScope } from "./context.js";
46
+ import {
47
+ createReverseFunction,
48
+ stripInternalParams,
49
+ } from "../router/handler-context.js";
34
50
  import { getGlobalRouteMap, isRouteRootScoped } from "../route-map-builder.js";
35
51
  import { invariant } from "../errors.js";
36
52
  import { isAutoGeneratedRouteName } from "../route-name.js";
@@ -44,19 +60,9 @@ import { isAutoGeneratedRouteName } from "../route-name.js";
44
60
  export interface RequestContext<
45
61
  TEnv = DefaultEnv,
46
62
  TParams = Record<string, string>,
47
- > {
48
- /** Platform bindings (Cloudflare env, etc.) */
49
- env: TEnv;
50
- /** Original HTTP request */
51
- request: Request;
52
- /** Parsed URL (system params like _rsc* are NOT filtered here) */
53
- url: URL;
54
- /** URL pathname */
55
- pathname: string;
56
- /** URL search params (system params like _rsc* are NOT filtered here) */
57
- searchParams: URLSearchParams;
58
- /** Variables set by middleware (same as ctx.var) */
59
- var: Record<string, any>;
63
+ > extends RequestScope<TEnv> {
64
+ /** @internal Shared variable backing store for ctx.get()/ctx.set(). */
65
+ _variables: Record<string, any>;
60
66
  /** Get a variable set by middleware */
61
67
  get: {
62
68
  <T>(contextVar: ContextVar<T>): T | undefined;
@@ -64,20 +70,19 @@ export interface RequestContext<
64
70
  };
65
71
  /** Set a variable (shared with middleware and handlers) */
66
72
  set: {
67
- <T>(contextVar: ContextVar<T>, value: T): void;
68
- <K extends string>(key: K, value: any): void;
73
+ <T>(
74
+ contextVar: ContextVar<T>,
75
+ value: T,
76
+ options?: { cache?: boolean },
77
+ ): void;
78
+ <K extends string>(key: K, value: any, options?: { cache?: boolean }): void;
69
79
  };
70
80
  /**
71
81
  * Route params (populated after route matching)
72
82
  * Initially empty, then set to matched params
73
83
  */
74
84
  params: TParams;
75
- /**
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.
80
- */
85
+ /** @internal Stub response for collecting headers/cookies. Use ctx.headers or ctx.header() instead. */
81
86
  readonly res: Response;
82
87
 
83
88
  /** @internal Get a cookie value (effective: request + response mutations). Use cookies().get() instead. */
@@ -95,6 +100,8 @@ export interface RequestContext<
95
100
  header(name: string, value: string): void;
96
101
  /** Set the response status code */
97
102
  setStatus(status: number): void;
103
+ /** @internal Set status bypassing cache-exec guard (for framework error handling) */
104
+ _setStatus(status: number): void;
98
105
 
99
106
  /**
100
107
  * Access loader data or push handle data.
@@ -139,20 +146,6 @@ export interface RequestContext<
139
146
  import("../cache/profile-registry.js").CacheProfile
140
147
  >;
141
148
 
142
- /**
143
- * Schedule work to run after the response is sent.
144
- * On Cloudflare Workers, uses ctx.waitUntil().
145
- * On Node.js, runs as fire-and-forget.
146
- *
147
- * @example
148
- * ```typescript
149
- * ctx.waitUntil(async () => {
150
- * await cacheStore.set(key, data, ttl);
151
- * });
152
- * ```
153
- */
154
- waitUntil(fn: () => Promise<void>): void;
155
-
156
149
  /**
157
150
  * Register a callback to run when the response is created.
158
151
  * Callbacks are sync and receive the response. They can:
@@ -256,6 +249,54 @@ export interface RequestContext<
256
249
  /** @internal Previous route key (from the navigation source), used for revalidation */
257
250
  _prevRouteKey?: string;
258
251
 
252
+ /**
253
+ * @internal Render barrier for experimental `rendered()` API.
254
+ * Resolves when all non-loader segments have settled and handle data
255
+ * is available. Used by DSL loaders that call `ctx.rendered()`.
256
+ */
257
+ _renderBarrier: Promise<void>;
258
+
259
+ /**
260
+ * @internal Resolve the render barrier. Accepts resolved segments, filters
261
+ * out loaders, and captures non-loader segment IDs as the handle ordering.
262
+ * Called after segment resolution (fresh) or handle replay (cache/prerender).
263
+ */
264
+ _resolveRenderBarrier: (
265
+ segments: Array<{ type: string; id: string }>,
266
+ ) => void;
267
+
268
+ /**
269
+ * @internal Segment order at barrier resolution time, used by loader
270
+ * ctx.use(handle) to collect handle data in correct order.
271
+ */
272
+ _renderBarrierSegmentOrder?: string[];
273
+
274
+ /**
275
+ * @internal Set to true when the matched entry tree contains any `loading()`
276
+ * entries (streaming). Used by rendered() to fail fast.
277
+ */
278
+ _treeHasStreaming?: boolean;
279
+
280
+ /**
281
+ * @internal Loader IDs that have called rendered() and are waiting for the
282
+ * barrier. Used to detect deadlocks when a handler tries to await the same
283
+ * loader via ctx.use(Loader).
284
+ */
285
+ _renderBarrierWaiters?: Set<string>;
286
+
287
+ /**
288
+ * @internal Loader IDs that handlers have started awaiting via ctx.use().
289
+ * Used for bidirectional deadlock detection: if a loader later calls
290
+ * rendered() and a handler already awaits it, we can detect the deadlock.
291
+ */
292
+ _handlerLoaderDeps?: Set<string>;
293
+
294
+ /**
295
+ * @internal Cached HandleData snapshot built at barrier resolution time.
296
+ * Avoids rebuilding the snapshot on every loader ctx.use(handle) call.
297
+ */
298
+ _renderBarrierHandleSnapshot?: HandleData;
299
+
259
300
  /** @internal Per-request error dedup set for onError reporting */
260
301
  _reportedErrors: WeakSet<object>;
261
302
 
@@ -266,6 +307,21 @@ export interface RequestContext<
266
307
  * errors without failing the response.
267
308
  */
268
309
  _reportBackgroundError?: (error: unknown, category: string) => void;
310
+
311
+ /** @internal Per-request debug performance override (set via ctx.debugPerformance()) */
312
+ _debugPerformance?: boolean;
313
+
314
+ /** @internal Request-scoped performance metrics store */
315
+ _metricsStore?: MetricsStore;
316
+
317
+ /** @internal Router basename for this request (used by redirect()) */
318
+ _basename?: string;
319
+
320
+ /**
321
+ * @internal RouteSnapshot from classifyRequest, reused by match/matchPartial
322
+ * to avoid a second resolveRoute call. Cleared on HMR invalidation.
323
+ */
324
+ _classifiedRoute?: import("../router/route-snapshot.js").RouteSnapshot;
269
325
  }
270
326
 
271
327
  /**
@@ -292,7 +348,21 @@ export type PublicRequestContext<
292
348
  | "_routeName"
293
349
  | "_prevRouteKey"
294
350
  | "_reportedErrors"
351
+ | "_renderBarrier"
352
+ | "_resolveRenderBarrier"
353
+ | "_renderBarrierSegmentOrder"
354
+ | "_treeHasStreaming"
355
+ | "_renderBarrierWaiters"
356
+ | "_handlerLoaderDeps"
357
+ | "_renderBarrierHandleSnapshot"
295
358
  | "_reportBackgroundError"
359
+ | "_debugPerformance"
360
+ | "_metricsStore"
361
+ | "_basename"
362
+ | "_setStatus"
363
+ | "_variables"
364
+ | "_classifiedRoute"
365
+ | "res"
296
366
  >;
297
367
 
298
368
  // AsyncLocalStorage instance for request context
@@ -401,13 +471,7 @@ export function requireRequestContext<
401
471
  return getRequestContext<TEnv>();
402
472
  }
403
473
 
404
- /**
405
- * Cloudflare Workers ExecutionContext (subset we need)
406
- */
407
- export interface ExecutionContext {
408
- waitUntil(promise: Promise<any>): void;
409
- passThroughOnException(): void;
410
- }
474
+ export type { ExecutionContext };
411
475
 
412
476
  /**
413
477
  * Options for creating a request context
@@ -491,6 +555,18 @@ export function createRequestContext<TEnv>(
491
555
  responseCookieCache = null;
492
556
  };
493
557
 
558
+ // Guard: throw if a response-level side effect is called inside a cache() scope.
559
+ // Uses ALS to detect the scope (set during segment resolution).
560
+ function assertNotInsideCacheScopeALS(methodName: string): void {
561
+ if (isInsideCacheScope()) {
562
+ throw new Error(
563
+ `ctx.${methodName}() cannot be called inside a cache() boundary. ` +
564
+ `On cache hit the handler is skipped, so this side effect would be lost. ` +
565
+ `Move ctx.${methodName}() to a middleware or layout outside the cache() scope.`,
566
+ );
567
+ }
568
+ }
569
+
494
570
  // Effective cookie read: response stub Set-Cookie wins, then original header.
495
571
  // The stub IS the source of truth for same-request mutations.
496
572
  const effectiveCookie = (name: string): string | undefined => {
@@ -543,19 +619,31 @@ export function createRequestContext<TEnv>(
543
619
  invalidateResponseCookieCache();
544
620
  };
545
621
 
622
+ // Strip internal _rsc* params so userland sees a clean URL.
623
+ const cleanUrl = stripInternalParams(url);
624
+
546
625
  // Build the context object first (without use), then add use
547
626
  const ctx: RequestContext<TEnv> = {
548
627
  env,
549
628
  request,
550
- url,
629
+ url: cleanUrl,
630
+ originalUrl: new URL(request.url),
551
631
  pathname: url.pathname,
552
- searchParams: url.searchParams,
553
- var: variables,
554
- get: ((keyOrVar: any) =>
555
- contextGet(variables, keyOrVar)) as RequestContext<TEnv>["get"],
556
- set: ((keyOrVar: any, value: any) => {
632
+ searchParams: cleanUrl.searchParams,
633
+ _variables: variables,
634
+ get: ((keyOrVar: any) => {
635
+ if (isNonCacheable(variables, keyOrVar) && isInsideCacheScope()) {
636
+ throw new Error(
637
+ `ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
638
+ `The variable was created with { cache: false } or set with { cache: false }, ` +
639
+ `and its value would be stale on cache hit. Move the read outside the cached scope.`,
640
+ );
641
+ }
642
+ return contextGet(variables, keyOrVar);
643
+ }) as RequestContext<TEnv>["get"],
644
+ set: ((keyOrVar: any, value: any, options?: any) => {
557
645
  assertNotInsideCacheExec(ctx, "set");
558
- contextSet(variables, keyOrVar, value);
646
+ contextSet(variables, keyOrVar, value, options);
559
647
  }) as RequestContext<TEnv>["set"],
560
648
  params: {} as Record<string, string>,
561
649
 
@@ -593,6 +681,7 @@ export function createRequestContext<TEnv>(
593
681
 
594
682
  setCookie(name: string, value: string, options?: CookieOptions): void {
595
683
  assertNotInsideCacheExec(ctx, "setCookie");
684
+ assertNotInsideCacheScopeALS("setCookie");
596
685
  stubResponse.headers.append(
597
686
  "Set-Cookie",
598
687
  serializeCookieValue(name, value, options),
@@ -605,6 +694,7 @@ export function createRequestContext<TEnv>(
605
694
  options?: Pick<CookieOptions, "domain" | "path">,
606
695
  ): void {
607
696
  assertNotInsideCacheExec(ctx, "deleteCookie");
697
+ assertNotInsideCacheScopeALS("deleteCookie");
608
698
  stubResponse.headers.append(
609
699
  "Set-Cookie",
610
700
  serializeCookieValue(name, "", { ...options, maxAge: 0 }),
@@ -614,13 +704,20 @@ export function createRequestContext<TEnv>(
614
704
 
615
705
  header(name: string, value: string): void {
616
706
  assertNotInsideCacheExec(ctx, "header");
707
+ assertNotInsideCacheScopeALS("header");
617
708
  stubResponse.headers.set(name, value);
618
709
  },
619
710
 
620
711
  setStatus(status: number): void {
621
712
  assertNotInsideCacheExec(ctx, "setStatus");
622
- // Response.status is read-only, so we must create a new Response.
623
- // Headers are passed by reference — no cookie cache invalidation needed.
713
+ assertNotInsideCacheScopeALS("setStatus");
714
+ stubResponse = new Response(null, {
715
+ status,
716
+ headers: stubResponse.headers,
717
+ });
718
+ },
719
+
720
+ _setStatus(status: number): void {
624
721
  stubResponse = new Response(null, {
625
722
  status,
626
723
  headers: stubResponse.headers,
@@ -638,20 +735,19 @@ export function createRequestContext<TEnv>(
638
735
 
639
736
  waitUntil(fn: () => Promise<void>): void {
640
737
  if (executionContext?.waitUntil) {
641
- // Cloudflare Workers: use native waitUntil
642
738
  executionContext.waitUntil(fn());
643
739
  } else {
644
- // Node.js / dev: fire-and-forget with error logging
645
- fn().catch((err) =>
646
- console.error("[waitUntil] Background task failed:", err),
647
- );
740
+ fireAndForgetWaitUntil(fn);
648
741
  }
649
742
  },
650
743
 
744
+ executionContext,
745
+
651
746
  _onResponseCallbacks: [],
652
747
 
653
748
  onResponse(callback: (response: Response) => Response): void {
654
749
  assertNotInsideCacheExec(ctx, "onResponse");
750
+ assertNotInsideCacheScopeALS("onResponse");
655
751
  this._onResponseCallbacks.push(callback);
656
752
  },
657
753
 
@@ -677,10 +773,60 @@ export function createRequestContext<TEnv>(
677
773
  _locationState: undefined,
678
774
 
679
775
  _reportedErrors: new WeakSet<object>(),
776
+ _metricsStore: undefined,
777
+
778
+ // Render barrier: deferred promise resolved after non-loader segments settle.
779
+ _renderBarrier: null as any, // set below
780
+ _resolveRenderBarrier: null as any, // set below
781
+ _renderBarrierSegmentOrder: undefined,
680
782
 
681
783
  reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
682
784
  };
683
785
 
786
+ // Lazy render barrier: only allocate the Promise when a loader actually
787
+ // calls rendered(). Requests that don't use rendered() pay zero cost.
788
+ let barrierResolved = false;
789
+ let resolveBarrier: (() => void) | undefined;
790
+ ctx._renderBarrier = null as any; // lazy — created on first access
791
+ ctx._resolveRenderBarrier = (
792
+ segments: Array<{ type: string; id: string }>,
793
+ ) => {
794
+ if (barrierResolved) return;
795
+ barrierResolved = true;
796
+ const segOrder = segments
797
+ .filter((s) => s.type !== "loader")
798
+ .map((s) => s.id);
799
+ ctx._renderBarrierSegmentOrder = segOrder;
800
+ // Build and cache handle snapshot so loader ctx.use(handle) calls
801
+ // don't rebuild it on every invocation.
802
+ ctx._renderBarrierHandleSnapshot = buildHandleSnapshot(
803
+ handleStore,
804
+ segOrder,
805
+ );
806
+ ctx._renderBarrierWaiters = undefined;
807
+ ctx._handlerLoaderDeps = undefined;
808
+ if (resolveBarrier) resolveBarrier();
809
+ };
810
+ Object.defineProperty(ctx, "_renderBarrier", {
811
+ get() {
812
+ // Barrier already resolved (cache/prerender hit) or first lazy access.
813
+ // Either way, replace the getter with a concrete value to avoid
814
+ // repeated Promise.resolve() allocations on subsequent reads.
815
+ const p = barrierResolved
816
+ ? Promise.resolve()
817
+ : new Promise<void>((resolve) => {
818
+ resolveBarrier = resolve;
819
+ });
820
+ Object.defineProperty(ctx, "_renderBarrier", {
821
+ value: p,
822
+ writable: false,
823
+ configurable: false,
824
+ });
825
+ return p;
826
+ },
827
+ configurable: true,
828
+ });
829
+
684
830
  // Now create use() with access to ctx
685
831
  ctx.use = createUseFunction({
686
832
  handleStore,
@@ -862,15 +1008,17 @@ export function createUseFunction<TEnv>(
862
1008
  search: (ctx as any).search ?? {},
863
1009
  pathname: ctx.pathname,
864
1010
  url: ctx.url,
1011
+ originalUrl: ctx.originalUrl,
865
1012
  env: ctx.env as any,
866
- var: ctx.var as any,
1013
+ waitUntil: ctx.waitUntil.bind(ctx),
1014
+ executionContext: ctx.executionContext,
867
1015
  get: ctx.get as any,
868
- use: <TDep, TDepParams = any>(
1016
+ use: (<TDep, TDepParams = any>(
869
1017
  dep: LoaderDefinition<TDep, TDepParams>,
870
1018
  ): Promise<TDep> => {
871
1019
  // Recursive call - will start dep loader if not already started
872
1020
  return ctx.use(dep);
873
- },
1021
+ }) as LoaderContext["use"],
874
1022
  method: "GET",
875
1023
  body: undefined,
876
1024
  reverse: createReverseFunction(
@@ -879,10 +1027,15 @@ export function createUseFunction<TEnv>(
879
1027
  ctx.params as Record<string, string>,
880
1028
  ctx._routeName ? isRouteRootScoped(ctx._routeName) : undefined,
881
1029
  ),
1030
+ rendered: () => {
1031
+ throw new Error(
1032
+ `ctx.rendered() is only available in DSL loaders (registered via loader() in urls()). ` +
1033
+ `It cannot be used from request-context loaders or server actions.`,
1034
+ );
1035
+ },
882
1036
  };
883
1037
 
884
- // Start loader execution with tracking
885
- const doneLoader = track(`loader:${loader.$$id}`);
1038
+ const doneLoader = track(`loader:${loader.$$id}`, 2);
886
1039
  const promise = Promise.resolve(loaderFn(loaderCtx)).finally(() => {
887
1040
  doneLoader();
888
1041
  });
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,
package/src/ssr/index.tsx CHANGED
@@ -129,6 +129,7 @@ interface RscPayload {
129
129
  matched?: string[];
130
130
  pathname?: string;
131
131
  params?: Record<string, string>;
132
+ basename?: string;
132
133
  themeConfig?: ResolvedThemeConfig | null;
133
134
  initialTheme?: Theme;
134
135
  version?: string;
@@ -168,6 +169,7 @@ function createSsrEventController(opts: {
168
169
  const state: DerivedNavigationState = {
169
170
  state: "idle",
170
171
  isStreaming: false,
172
+ isNavigating: false,
171
173
  location,
172
174
  pendingUrl: null,
173
175
  inflightActions: [],
@@ -260,6 +262,7 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
260
262
  function SsrRoot() {
261
263
  payload ??= createFromReadableStream<RscPayload>(rscStream1);
262
264
  const resolved = React.use(payload);
265
+
263
266
  const themeConfig = resolved.metadata?.themeConfig ?? null;
264
267
  const pathname = resolved.metadata?.pathname ?? "/";
265
268
 
@@ -285,6 +288,7 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
285
288
  navigate: async () => {},
286
289
  refresh: async () => {},
287
290
  version: resolved.metadata?.version,
291
+ basename: resolved.metadata?.basename,
288
292
  };
289
293
 
290
294
  // Build content tree from segments.
@@ -32,11 +32,21 @@
32
32
  */
33
33
  import type { ReactNode } from "react";
34
34
  import type { Handler } from "./types.js";
35
- import type { PrerenderOptions, StaticBuildContext } from "./prerender.js";
35
+ import type { StaticBuildContext } from "./prerender.js";
36
+ import type { UseItems, HandlerUseItem } from "./route-types.js";
36
37
  import { isCachedFunction } from "./cache/taint.js";
37
38
 
38
39
  // -- Types ------------------------------------------------------------------
39
40
 
41
+ export interface StaticHandlerOptions {
42
+ /**
43
+ * Keep handler in server bundle for live fallback (default: false).
44
+ * false: handler replaced with stub, source-only APIs excluded from bundle.
45
+ * true: handler stays in bundle, renders live at request time.
46
+ */
47
+ passthrough?: boolean;
48
+ }
49
+
40
50
  export interface StaticHandlerDefinition<
41
51
  TParams extends Record<string, any> = any,
42
52
  > {
@@ -46,14 +56,16 @@ export interface StaticHandlerDefinition<
46
56
  /** In dev mode, the actual handler function that layout/path/parallel can call. */
47
57
  handler: Handler<TParams>;
48
58
  /** Static handler options (passthrough support). */
49
- options?: PrerenderOptions;
59
+ options?: StaticHandlerOptions;
60
+ /** Composable default DSL items merged when the handler is mounted. */
61
+ use?: () => UseItems<HandlerUseItem>;
50
62
  }
51
63
 
52
64
  // -- Function ---------------------------------------------------------------
53
65
 
54
66
  export function Static<TParams extends Record<string, any> = {}>(
55
67
  handler: (ctx: StaticBuildContext) => ReactNode | Promise<ReactNode>,
56
- options?: PrerenderOptions,
68
+ options?: StaticHandlerOptions,
57
69
  __injectedId?: string,
58
70
  ): StaticHandlerDefinition<TParams>;
59
71
 
@@ -61,7 +73,7 @@ export function Static<TParams extends Record<string, any> = {}>(
61
73
 
62
74
  export function Static<TParams extends Record<string, any>>(
63
75
  handler: Function,
64
- optionsOrId?: PrerenderOptions | string,
76
+ optionsOrId?: StaticHandlerOptions | string,
65
77
  maybeId?: string,
66
78
  ): StaticHandlerDefinition<TParams> {
67
79
  if (isCachedFunction(handler)) {
@@ -72,13 +84,13 @@ export function Static<TParams extends Record<string, any>>(
72
84
  );
73
85
  }
74
86
 
75
- let options: PrerenderOptions | undefined;
87
+ let options: StaticHandlerOptions | undefined;
76
88
  let id: string;
77
89
 
78
90
  if (typeof optionsOrId === "string") {
79
91
  id = optionsOrId;
80
92
  } else {
81
- options = optionsOrId as PrerenderOptions | undefined;
93
+ options = optionsOrId as StaticHandlerOptions | undefined;
82
94
  id = maybeId ?? "";
83
95
  }
84
96
 
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Theme module exports for @rangojs/router/theme
3
3
  *
4
- * This module provides theme management for rsc-router:
4
+ * This module provides the public theme API:
5
5
  * - useTheme: Hook for accessing theme state in client components
6
6
  * - ThemeProvider: Component for manual theme provider setup (typically not needed)
7
+ * - ThemeScript: FOUC-prevention script component for document/head usage
7
8
  * - Types for theme configuration
8
9
  *
9
10
  * @example
@@ -43,15 +44,5 @@ export type {
43
44
  ThemeContextValue,
44
45
  } from "./types.js";
45
46
 
46
- // Constants (for advanced use cases)
47
- export {
48
- THEME_DEFAULTS,
49
- THEME_COOKIE,
50
- resolveThemeConfig,
51
- } from "./constants.js";
52
-
53
- // Script generation (for advanced SSR use cases)
54
- export { generateThemeScript, getNonceAttribute } from "./theme-script.js";
55
-
56
- // Context (for advanced use cases)
57
- export { ThemeContext, useThemeContext } from "./theme-context.js";
47
+ // Constants
48
+ export { THEME_DEFAULTS, THEME_COOKIE } from "./constants.js";
@@ -5,8 +5,8 @@
5
5
  * during cache key generation (before middleware runs).
6
6
  *
7
7
  * Note: While the full RequestContext is passed, middleware-set variables
8
- * (ctx.var, ctx.get()) may not be populated yet since cache lookup
9
- * happens before middleware execution.
8
+ * read via `ctx.get()` may not be populated yet since cache lookup happens
9
+ * before middleware execution.
10
10
  */
11
11
  export type { RequestContext as CacheContext } from "../server/request-context.js";
12
12
 
@@ -101,7 +101,7 @@ export interface CacheOptions<TEnv = unknown> {
101
101
  * Return false to skip cache for this request (always fetch fresh).
102
102
  *
103
103
  * Has access to full RequestContext including env, request, params, cookies, etc.
104
- * Note: Middleware-set variables (ctx.var) may not be populated yet.
104
+ * Note: Middleware-set variables read via `ctx.get()` may not be populated yet.
105
105
  *
106
106
  * @example
107
107
  * ```typescript
@@ -123,7 +123,7 @@ export interface CacheOptions<TEnv = unknown> {
123
123
  * Bypasses default key generation AND store's keyGenerator.
124
124
  *
125
125
  * Has access to full RequestContext including env, request, params, cookies, etc.
126
- * Note: Middleware-set variables (ctx.var) may not be populated yet.
126
+ * Note: Middleware-set variables read via `ctx.get()` may not be populated yet.
127
127
  *
128
128
  * @example
129
129
  * ```typescript