@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1fa245e2

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 (160) hide show
  1. package/{CLAUDE.md → AGENTS.md} +4 -0
  2. package/README.md +122 -30
  3. package/dist/bin/rango.js +245 -63
  4. package/dist/vite/index.js +859 -418
  5. package/package.json +3 -3
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +32 -0
  8. package/skills/caching/SKILL.md +49 -8
  9. package/skills/document-cache/SKILL.md +2 -2
  10. package/skills/hooks/SKILL.md +33 -31
  11. package/skills/host-router/SKILL.md +218 -0
  12. package/skills/links/SKILL.md +3 -1
  13. package/skills/loader/SKILL.md +72 -22
  14. package/skills/middleware/SKILL.md +2 -0
  15. package/skills/parallel/SKILL.md +126 -0
  16. package/skills/prerender/SKILL.md +112 -70
  17. package/skills/rango/SKILL.md +0 -1
  18. package/skills/route/SKILL.md +34 -4
  19. package/skills/router-setup/SKILL.md +95 -5
  20. package/skills/typesafety/SKILL.md +35 -23
  21. package/src/__internal.ts +92 -0
  22. package/src/bin/rango.ts +18 -0
  23. package/src/browser/app-version.ts +14 -0
  24. package/src/browser/event-controller.ts +5 -0
  25. package/src/browser/link-interceptor.ts +4 -0
  26. package/src/browser/navigation-bridge.ts +114 -18
  27. package/src/browser/navigation-client.ts +126 -44
  28. package/src/browser/navigation-store.ts +43 -8
  29. package/src/browser/navigation-transaction.ts +11 -9
  30. package/src/browser/partial-update.ts +80 -15
  31. package/src/browser/prefetch/cache.ts +166 -27
  32. package/src/browser/prefetch/fetch.ts +52 -39
  33. package/src/browser/prefetch/policy.ts +6 -0
  34. package/src/browser/prefetch/queue.ts +92 -20
  35. package/src/browser/prefetch/resource-ready.ts +77 -0
  36. package/src/browser/react/Link.tsx +70 -14
  37. package/src/browser/react/NavigationProvider.tsx +40 -4
  38. package/src/browser/react/context.ts +7 -2
  39. package/src/browser/react/use-handle.ts +9 -58
  40. package/src/browser/react/use-router.ts +21 -8
  41. package/src/browser/rsc-router.tsx +143 -59
  42. package/src/browser/scroll-restoration.ts +41 -42
  43. package/src/browser/segment-reconciler.ts +6 -1
  44. package/src/browser/server-action-bridge.ts +454 -436
  45. package/src/browser/types.ts +60 -5
  46. package/src/build/generate-manifest.ts +6 -6
  47. package/src/build/generate-route-types.ts +5 -0
  48. package/src/build/route-trie.ts +19 -3
  49. package/src/build/route-types/include-resolution.ts +8 -1
  50. package/src/build/route-types/router-processing.ts +346 -87
  51. package/src/build/route-types/scan-filter.ts +8 -1
  52. package/src/cache/cache-runtime.ts +15 -11
  53. package/src/cache/cache-scope.ts +48 -7
  54. package/src/cache/cf/cf-cache-store.ts +453 -11
  55. package/src/cache/cf/index.ts +5 -1
  56. package/src/cache/document-cache.ts +17 -7
  57. package/src/cache/index.ts +1 -0
  58. package/src/cache/taint.ts +55 -0
  59. package/src/client.rsc.tsx +2 -1
  60. package/src/client.tsx +3 -102
  61. package/src/context-var.ts +72 -2
  62. package/src/debug.ts +2 -2
  63. package/src/handle.ts +40 -0
  64. package/src/handles/breadcrumbs.ts +66 -0
  65. package/src/handles/index.ts +1 -0
  66. package/src/host/index.ts +0 -3
  67. package/src/index.rsc.ts +8 -37
  68. package/src/index.ts +40 -66
  69. package/src/prerender/store.ts +57 -15
  70. package/src/prerender.ts +138 -77
  71. package/src/reverse.ts +22 -1
  72. package/src/route-definition/dsl-helpers.ts +73 -25
  73. package/src/route-definition/helpers-types.ts +10 -6
  74. package/src/route-definition/index.ts +3 -3
  75. package/src/route-definition/redirect.ts +11 -3
  76. package/src/route-definition/resolve-handler-use.ts +149 -0
  77. package/src/route-map-builder.ts +7 -1
  78. package/src/route-types.ts +11 -0
  79. package/src/router/content-negotiation.ts +100 -1
  80. package/src/router/find-match.ts +4 -2
  81. package/src/router/handler-context.ts +108 -25
  82. package/src/router/intercept-resolution.ts +11 -4
  83. package/src/router/lazy-includes.ts +4 -1
  84. package/src/router/loader-resolution.ts +123 -11
  85. package/src/router/logging.ts +5 -2
  86. package/src/router/manifest.ts +9 -3
  87. package/src/router/match-api.ts +125 -190
  88. package/src/router/match-middleware/background-revalidation.ts +30 -2
  89. package/src/router/match-middleware/cache-lookup.ts +88 -16
  90. package/src/router/match-middleware/cache-store.ts +53 -10
  91. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  92. package/src/router/match-middleware/segment-resolution.ts +61 -5
  93. package/src/router/match-result.ts +22 -15
  94. package/src/router/metrics.ts +238 -13
  95. package/src/router/middleware-types.ts +53 -12
  96. package/src/router/middleware.ts +172 -85
  97. package/src/router/navigation-snapshot.ts +182 -0
  98. package/src/router/pattern-matching.ts +20 -5
  99. package/src/router/prerender-match.ts +114 -10
  100. package/src/router/preview-match.ts +30 -102
  101. package/src/router/request-classification.ts +310 -0
  102. package/src/router/revalidation.ts +27 -7
  103. package/src/router/route-snapshot.ts +245 -0
  104. package/src/router/router-context.ts +6 -1
  105. package/src/router/router-interfaces.ts +50 -5
  106. package/src/router/router-options.ts +50 -19
  107. package/src/router/segment-resolution/fresh.ts +200 -19
  108. package/src/router/segment-resolution/helpers.ts +30 -25
  109. package/src/router/segment-resolution/loader-cache.ts +1 -0
  110. package/src/router/segment-resolution/revalidation.ts +429 -301
  111. package/src/router/segment-wrappers.ts +2 -0
  112. package/src/router/trie-matching.ts +20 -2
  113. package/src/router/types.ts +1 -0
  114. package/src/router.ts +88 -15
  115. package/src/rsc/handler.ts +546 -359
  116. package/src/rsc/index.ts +0 -20
  117. package/src/rsc/manifest-init.ts +5 -1
  118. package/src/rsc/progressive-enhancement.ts +25 -8
  119. package/src/rsc/rsc-rendering.ts +35 -43
  120. package/src/rsc/server-action.ts +16 -10
  121. package/src/rsc/ssr-setup.ts +128 -0
  122. package/src/rsc/types.ts +10 -1
  123. package/src/search-params.ts +16 -13
  124. package/src/segment-system.tsx +140 -4
  125. package/src/server/context.ts +148 -16
  126. package/src/server/loader-registry.ts +9 -8
  127. package/src/server/request-context.ts +182 -34
  128. package/src/server.ts +6 -0
  129. package/src/ssr/index.tsx +4 -0
  130. package/src/static-handler.ts +18 -6
  131. package/src/theme/index.ts +4 -13
  132. package/src/types/cache-types.ts +4 -4
  133. package/src/types/handler-context.ts +149 -49
  134. package/src/types/loader-types.ts +36 -9
  135. package/src/types/route-config.ts +17 -8
  136. package/src/types/route-entry.ts +8 -1
  137. package/src/types/segments.ts +2 -5
  138. package/src/urls/path-helper-types.ts +9 -2
  139. package/src/urls/path-helper.ts +48 -13
  140. package/src/urls/pattern-types.ts +12 -0
  141. package/src/urls/response-types.ts +16 -6
  142. package/src/use-loader.tsx +73 -4
  143. package/src/vite/discovery/bundle-postprocess.ts +61 -89
  144. package/src/vite/discovery/discover-routers.ts +23 -5
  145. package/src/vite/discovery/prerender-collection.ts +48 -15
  146. package/src/vite/discovery/state.ts +17 -13
  147. package/src/vite/index.ts +8 -3
  148. package/src/vite/plugin-types.ts +51 -79
  149. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  150. package/src/vite/plugins/expose-action-id.ts +1 -3
  151. package/src/vite/plugins/performance-tracks.ts +88 -0
  152. package/src/vite/plugins/refresh-cmd.ts +127 -0
  153. package/src/vite/plugins/version-plugin.ts +13 -1
  154. package/src/vite/rango.ts +174 -211
  155. package/src/vite/router-discovery.ts +169 -42
  156. package/src/vite/utils/banner.ts +3 -3
  157. package/src/vite/utils/prerender-utils.ts +78 -0
  158. package/src/vite/utils/shared-utils.ts +3 -2
  159. package/skills/testing/SKILL.md +0 -226
  160. package/src/route-definition/route-function.ts +0 -119
@@ -15,21 +15,31 @@ import type { CookieOptions } from "../router/middleware.js";
15
15
  import type { LoaderDefinition, LoaderContext } from "../types.js";
16
16
  import type { ScopedReverseFunction } from "../reverse.js";
17
17
  import type {
18
+ DefaultEnv,
18
19
  DefaultReverseRouteMap,
19
20
  DefaultRouteName,
20
21
  } from "../types/global-namespace.js";
21
22
  import type { Handle } from "../handle.js";
22
- import { type ContextVar, contextGet, contextSet } from "../context-var.js";
23
+ import {
24
+ type ContextVar,
25
+ contextGet,
26
+ contextSet,
27
+ isNonCacheable,
28
+ } from "../context-var.js";
23
29
  import { createHandleStore, type HandleStore } from "./handle-store.js";
24
30
  import { isHandle } from "../handle.js";
25
- import { track } from "./context.js";
31
+ import { track, type MetricsStore } from "./context.js";
26
32
  import { getFetchableLoader } from "./fetchable-loader-store.js";
27
33
  import type { SegmentCacheStore } from "../cache/types.js";
28
34
  import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
29
35
  import { THEME_COOKIE } from "../theme/constants.js";
30
36
  import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
31
37
  import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
32
- import { createReverseFunction } from "../router/handler-context.js";
38
+ import { isInsideCacheScope } from "./context.js";
39
+ import {
40
+ createReverseFunction,
41
+ stripInternalParams,
42
+ } from "../router/handler-context.js";
33
43
  import { getGlobalRouteMap, isRouteRootScoped } from "../route-map-builder.js";
34
44
  import { invariant } from "../errors.js";
35
45
  import { isAutoGeneratedRouteName } from "../route-name.js";
@@ -41,21 +51,26 @@ import { isAutoGeneratedRouteName } from "../route-name.js";
41
51
  * Use this when you need access to request data outside of route handlers.
42
52
  */
43
53
  export interface RequestContext<
44
- TEnv = unknown,
54
+ TEnv = DefaultEnv,
45
55
  TParams = Record<string, string>,
46
56
  > {
47
57
  /** Platform bindings (Cloudflare env, etc.) */
48
58
  env: TEnv;
49
59
  /** Original HTTP request */
50
60
  request: Request;
51
- /** Parsed URL (system params like _rsc* are NOT filtered here) */
61
+ /** Parsed URL (with internal `_rsc*` params stripped) */
52
62
  url: URL;
63
+ /**
64
+ * The original request URL with all parameters intact, including
65
+ * internal `_rsc*` transport params.
66
+ */
67
+ originalUrl: URL;
53
68
  /** URL pathname */
54
69
  pathname: string;
55
- /** URL search params (system params like _rsc* are NOT filtered here) */
70
+ /** URL search params (with internal `_rsc*` params stripped, same as `url.searchParams`) */
56
71
  searchParams: URLSearchParams;
57
- /** Variables set by middleware (same as ctx.var) */
58
- var: Record<string, any>;
72
+ /** @internal Shared variable backing store for ctx.get()/ctx.set(). */
73
+ _variables: Record<string, any>;
59
74
  /** Get a variable set by middleware */
60
75
  get: {
61
76
  <T>(contextVar: ContextVar<T>): T | undefined;
@@ -63,20 +78,19 @@ export interface RequestContext<
63
78
  };
64
79
  /** Set a variable (shared with middleware and handlers) */
65
80
  set: {
66
- <T>(contextVar: ContextVar<T>, value: T): void;
67
- <K extends string>(key: K, value: any): void;
81
+ <T>(
82
+ contextVar: ContextVar<T>,
83
+ value: T,
84
+ options?: { cache?: boolean },
85
+ ): void;
86
+ <K extends string>(key: K, value: any, options?: { cache?: boolean }): void;
68
87
  };
69
88
  /**
70
89
  * Route params (populated after route matching)
71
90
  * Initially empty, then set to matched params
72
91
  */
73
92
  params: TParams;
74
- /**
75
- * Stub response for setting headers/cookies (read-only).
76
- * Headers set here are merged into the final response.
77
- * Use header() or setStatus() to mutate response headers/status.
78
- * Use cookies().set()/cookies().delete() for cookie mutations.
79
- */
93
+ /** @internal Stub response for collecting headers/cookies. Use ctx.headers or ctx.header() instead. */
80
94
  readonly res: Response;
81
95
 
82
96
  /** @internal Get a cookie value (effective: request + response mutations). Use cookies().get() instead. */
@@ -94,6 +108,8 @@ export interface RequestContext<
94
108
  header(name: string, value: string): void;
95
109
  /** Set the response status code */
96
110
  setStatus(status: number): void;
111
+ /** @internal Set status bypassing cache-exec guard (for framework error handling) */
112
+ _setStatus(status: number): void;
97
113
 
98
114
  /**
99
115
  * Access loader data or push handle data.
@@ -255,6 +271,41 @@ export interface RequestContext<
255
271
  /** @internal Previous route key (from the navigation source), used for revalidation */
256
272
  _prevRouteKey?: string;
257
273
 
274
+ /**
275
+ * @internal Render barrier for experimental `rendered()` API.
276
+ * Resolves when all non-loader segments have settled and handle data
277
+ * is available. Used by DSL loaders that call `ctx.rendered()`.
278
+ */
279
+ _renderBarrier: Promise<void>;
280
+
281
+ /**
282
+ * @internal Resolve the render barrier. Accepts resolved segments, filters
283
+ * out loaders, and captures non-loader segment IDs as the handle ordering.
284
+ * Called after segment resolution (fresh) or handle replay (cache/prerender).
285
+ */
286
+ _resolveRenderBarrier: (
287
+ segments: Array<{ type: string; id: string }>,
288
+ ) => void;
289
+
290
+ /**
291
+ * @internal Segment order at barrier resolution time, used by loader
292
+ * ctx.use(handle) to collect handle data in correct order.
293
+ */
294
+ _renderBarrierSegmentOrder?: string[];
295
+
296
+ /**
297
+ * @internal Set to true when the matched entry tree contains any `loading()`
298
+ * entries (streaming). Used by rendered() to fail fast.
299
+ */
300
+ _treeHasStreaming?: boolean;
301
+
302
+ /**
303
+ * @internal Loader IDs that have called rendered() and are waiting for the
304
+ * barrier. Used to detect deadlocks when a handler tries to await the same
305
+ * loader via ctx.use(Loader).
306
+ */
307
+ _renderBarrierWaiters?: Set<string>;
308
+
258
309
  /** @internal Per-request error dedup set for onError reporting */
259
310
  _reportedErrors: WeakSet<object>;
260
311
 
@@ -265,6 +316,21 @@ export interface RequestContext<
265
316
  * errors without failing the response.
266
317
  */
267
318
  _reportBackgroundError?: (error: unknown, category: string) => void;
319
+
320
+ /** @internal Per-request debug performance override (set via ctx.debugPerformance()) */
321
+ _debugPerformance?: boolean;
322
+
323
+ /** @internal Request-scoped performance metrics store */
324
+ _metricsStore?: MetricsStore;
325
+
326
+ /** @internal Router basename for this request (used by redirect()) */
327
+ _basename?: string;
328
+
329
+ /**
330
+ * @internal RouteSnapshot from classifyRequest, reused by match/matchPartial
331
+ * to avoid a second resolveRoute call. Cleared on HMR invalidation.
332
+ */
333
+ _classifiedRoute?: import("../router/route-snapshot.js").RouteSnapshot;
268
334
  }
269
335
 
270
336
  /**
@@ -274,7 +340,7 @@ export interface RequestContext<
274
340
  * use the full RequestContext interface directly.
275
341
  */
276
342
  export type PublicRequestContext<
277
- TEnv = unknown,
343
+ TEnv = DefaultEnv,
278
344
  TParams = Record<string, string>,
279
345
  > = Omit<
280
346
  RequestContext<TEnv, TParams>,
@@ -291,7 +357,19 @@ export type PublicRequestContext<
291
357
  | "_routeName"
292
358
  | "_prevRouteKey"
293
359
  | "_reportedErrors"
360
+ | "_renderBarrier"
361
+ | "_resolveRenderBarrier"
362
+ | "_renderBarrierSegmentOrder"
363
+ | "_treeHasStreaming"
364
+ | "_renderBarrierWaiters"
294
365
  | "_reportBackgroundError"
366
+ | "_debugPerformance"
367
+ | "_metricsStore"
368
+ | "_basename"
369
+ | "_setStatus"
370
+ | "_variables"
371
+ | "_classifiedRoute"
372
+ | "res"
295
373
  >;
296
374
 
297
375
  // AsyncLocalStorage instance for request context
@@ -312,7 +390,7 @@ export function runWithRequestContext<TEnv, T>(
312
390
  * Get the current request context
313
391
  * Throws if called outside of a request context
314
392
  */
315
- export function getRequestContext<TEnv = unknown>(): RequestContext<TEnv> {
393
+ export function getRequestContext<TEnv = DefaultEnv>(): RequestContext<TEnv> {
316
394
  const ctx = requestContextStorage.getStore() as
317
395
  | RequestContext<TEnv>
318
396
  | undefined;
@@ -329,7 +407,7 @@ export function getRequestContext<TEnv = unknown>(): RequestContext<TEnv> {
329
407
  * @internal Get the request context without throwing — for internal code that
330
408
  * may run outside a request context (cache stores, optional handle lookups, etc.)
331
409
  */
332
- export function _getRequestContext<TEnv = unknown>():
410
+ export function _getRequestContext<TEnv = DefaultEnv>():
333
411
  | RequestContext<TEnv>
334
412
  | undefined {
335
413
  return requestContextStorage.getStore() as RequestContext<TEnv> | undefined;
@@ -394,7 +472,9 @@ export function getLocationState(): LocationStateEntry[] | undefined {
394
472
  * Get the current request context, throwing if not available
395
473
  * @deprecated Use getRequestContext() directly — it now throws if outside context
396
474
  */
397
- export function requireRequestContext<TEnv = unknown>(): RequestContext<TEnv> {
475
+ export function requireRequestContext<
476
+ TEnv = DefaultEnv,
477
+ >(): RequestContext<TEnv> {
398
478
  return getRequestContext<TEnv>();
399
479
  }
400
480
 
@@ -488,6 +568,18 @@ export function createRequestContext<TEnv>(
488
568
  responseCookieCache = null;
489
569
  };
490
570
 
571
+ // Guard: throw if a response-level side effect is called inside a cache() scope.
572
+ // Uses ALS to detect the scope (set during segment resolution).
573
+ function assertNotInsideCacheScopeALS(methodName: string): void {
574
+ if (isInsideCacheScope()) {
575
+ throw new Error(
576
+ `ctx.${methodName}() cannot be called inside a cache() boundary. ` +
577
+ `On cache hit the handler is skipped, so this side effect would be lost. ` +
578
+ `Move ctx.${methodName}() to a middleware or layout outside the cache() scope.`,
579
+ );
580
+ }
581
+ }
582
+
491
583
  // Effective cookie read: response stub Set-Cookie wins, then original header.
492
584
  // The stub IS the source of truth for same-request mutations.
493
585
  const effectiveCookie = (name: string): string | undefined => {
@@ -540,19 +632,31 @@ export function createRequestContext<TEnv>(
540
632
  invalidateResponseCookieCache();
541
633
  };
542
634
 
635
+ // Strip internal _rsc* params so userland sees a clean URL.
636
+ const cleanUrl = stripInternalParams(url);
637
+
543
638
  // Build the context object first (without use), then add use
544
639
  const ctx: RequestContext<TEnv> = {
545
640
  env,
546
641
  request,
547
- url,
642
+ url: cleanUrl,
643
+ originalUrl: new URL(request.url),
548
644
  pathname: url.pathname,
549
- searchParams: url.searchParams,
550
- var: variables,
551
- get: ((keyOrVar: any) =>
552
- contextGet(variables, keyOrVar)) as RequestContext<TEnv>["get"],
553
- set: ((keyOrVar: any, value: any) => {
645
+ searchParams: cleanUrl.searchParams,
646
+ _variables: variables,
647
+ get: ((keyOrVar: any) => {
648
+ if (isNonCacheable(variables, keyOrVar) && isInsideCacheScope()) {
649
+ throw new Error(
650
+ `ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
651
+ `The variable was created with { cache: false } or set with { cache: false }, ` +
652
+ `and its value would be stale on cache hit. Move the read outside the cached scope.`,
653
+ );
654
+ }
655
+ return contextGet(variables, keyOrVar);
656
+ }) as RequestContext<TEnv>["get"],
657
+ set: ((keyOrVar: any, value: any, options?: any) => {
554
658
  assertNotInsideCacheExec(ctx, "set");
555
- contextSet(variables, keyOrVar, value);
659
+ contextSet(variables, keyOrVar, value, options);
556
660
  }) as RequestContext<TEnv>["set"],
557
661
  params: {} as Record<string, string>,
558
662
 
@@ -590,6 +694,7 @@ export function createRequestContext<TEnv>(
590
694
 
591
695
  setCookie(name: string, value: string, options?: CookieOptions): void {
592
696
  assertNotInsideCacheExec(ctx, "setCookie");
697
+ assertNotInsideCacheScopeALS("setCookie");
593
698
  stubResponse.headers.append(
594
699
  "Set-Cookie",
595
700
  serializeCookieValue(name, value, options),
@@ -602,6 +707,7 @@ export function createRequestContext<TEnv>(
602
707
  options?: Pick<CookieOptions, "domain" | "path">,
603
708
  ): void {
604
709
  assertNotInsideCacheExec(ctx, "deleteCookie");
710
+ assertNotInsideCacheScopeALS("deleteCookie");
605
711
  stubResponse.headers.append(
606
712
  "Set-Cookie",
607
713
  serializeCookieValue(name, "", { ...options, maxAge: 0 }),
@@ -611,13 +717,20 @@ export function createRequestContext<TEnv>(
611
717
 
612
718
  header(name: string, value: string): void {
613
719
  assertNotInsideCacheExec(ctx, "header");
720
+ assertNotInsideCacheScopeALS("header");
614
721
  stubResponse.headers.set(name, value);
615
722
  },
616
723
 
617
724
  setStatus(status: number): void {
618
725
  assertNotInsideCacheExec(ctx, "setStatus");
619
- // Response.status is read-only, so we must create a new Response.
620
- // Headers are passed by reference — no cookie cache invalidation needed.
726
+ assertNotInsideCacheScopeALS("setStatus");
727
+ stubResponse = new Response(null, {
728
+ status,
729
+ headers: stubResponse.headers,
730
+ });
731
+ },
732
+
733
+ _setStatus(status: number): void {
621
734
  stubResponse = new Response(null, {
622
735
  status,
623
736
  headers: stubResponse.headers,
@@ -649,6 +762,7 @@ export function createRequestContext<TEnv>(
649
762
 
650
763
  onResponse(callback: (response: Response) => Response): void {
651
764
  assertNotInsideCacheExec(ctx, "onResponse");
765
+ assertNotInsideCacheScopeALS("onResponse");
652
766
  this._onResponseCallbacks.push(callback);
653
767
  },
654
768
 
@@ -674,10 +788,40 @@ export function createRequestContext<TEnv>(
674
788
  _locationState: undefined,
675
789
 
676
790
  _reportedErrors: new WeakSet<object>(),
791
+ _metricsStore: undefined,
792
+
793
+ // Render barrier: deferred promise resolved after non-loader segments settle.
794
+ _renderBarrier: null as any, // set below
795
+ _resolveRenderBarrier: null as any, // set below
796
+ _renderBarrierSegmentOrder: undefined,
677
797
 
678
798
  reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
679
799
  };
680
800
 
801
+ // Create deferred render barrier. Phase 1: non-streaming only, so all handlers
802
+ // complete synchronously during resolveAllSegments. The barrier is a simple
803
+ // deferred promise resolved after segment resolution (or after handle replay
804
+ // on cache/prerender paths). No HandleStore sealing here — that stays in the
805
+ // existing lifecycle (rsc-rendering.ts, cache-scope.ts, etc.).
806
+ let barrierResolved = false;
807
+ let resolveBarrier: () => void;
808
+ ctx._renderBarrier = new Promise<void>((resolve) => {
809
+ resolveBarrier = resolve;
810
+ });
811
+ ctx._resolveRenderBarrier = (
812
+ segments: Array<{ type: string; id: string }>,
813
+ ) => {
814
+ if (barrierResolved) return;
815
+ barrierResolved = true;
816
+ ctx._renderBarrierSegmentOrder = segments
817
+ .filter((s) => s.type !== "loader")
818
+ .map((s) => s.id);
819
+ // Clear deadlock detection set — once the barrier resolves, the loaders
820
+ // waiting on it will settle and the deadlock window is closed.
821
+ ctx._renderBarrierWaiters = undefined;
822
+ resolveBarrier();
823
+ };
824
+
681
825
  // Now create use() with access to ctx
682
826
  ctx.use = createUseFunction({
683
827
  handleStore,
@@ -860,14 +1004,13 @@ export function createUseFunction<TEnv>(
860
1004
  pathname: ctx.pathname,
861
1005
  url: ctx.url,
862
1006
  env: ctx.env as any,
863
- var: ctx.var as any,
864
1007
  get: ctx.get as any,
865
- use: <TDep, TDepParams = any>(
1008
+ use: (<TDep, TDepParams = any>(
866
1009
  dep: LoaderDefinition<TDep, TDepParams>,
867
1010
  ): Promise<TDep> => {
868
1011
  // Recursive call - will start dep loader if not already started
869
1012
  return ctx.use(dep);
870
- },
1013
+ }) as LoaderContext["use"],
871
1014
  method: "GET",
872
1015
  body: undefined,
873
1016
  reverse: createReverseFunction(
@@ -876,10 +1019,15 @@ export function createUseFunction<TEnv>(
876
1019
  ctx.params as Record<string, string>,
877
1020
  ctx._routeName ? isRouteRootScoped(ctx._routeName) : undefined,
878
1021
  ),
1022
+ rendered: () => {
1023
+ throw new Error(
1024
+ `ctx.rendered() is only available in DSL loaders (registered via loader() in urls()). ` +
1025
+ `It cannot be used from request-context loaders or server actions.`,
1026
+ );
1027
+ },
879
1028
  };
880
1029
 
881
- // Start loader execution with tracking
882
- const doneLoader = track(`loader:${loader.$$id}`);
1030
+ const doneLoader = track(`loader:${loader.$$id}`, 2);
883
1031
  const promise = Promise.resolve(loaderFn(loaderCtx)).finally(() => {
884
1032
  doneLoader();
885
1033
  });
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