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

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 (84) hide show
  1. package/README.md +46 -12
  2. package/dist/bin/rango.js +109 -15
  3. package/dist/vite/index.js +323 -121
  4. package/package.json +15 -16
  5. package/skills/breadcrumbs/SKILL.md +250 -0
  6. package/skills/caching/SKILL.md +4 -4
  7. package/skills/document-cache/SKILL.md +2 -2
  8. package/skills/hooks/SKILL.md +33 -31
  9. package/skills/host-router/SKILL.md +218 -0
  10. package/skills/loader/SKILL.md +55 -15
  11. package/skills/prerender/SKILL.md +2 -2
  12. package/skills/rango/SKILL.md +0 -1
  13. package/skills/route/SKILL.md +3 -4
  14. package/skills/router-setup/SKILL.md +8 -3
  15. package/skills/typesafety/SKILL.md +25 -23
  16. package/src/__internal.ts +92 -0
  17. package/src/bin/rango.ts +18 -0
  18. package/src/browser/link-interceptor.ts +4 -0
  19. package/src/browser/navigation-bridge.ts +95 -5
  20. package/src/browser/navigation-client.ts +97 -72
  21. package/src/browser/prefetch/cache.ts +112 -25
  22. package/src/browser/prefetch/fetch.ts +28 -30
  23. package/src/browser/prefetch/policy.ts +6 -0
  24. package/src/browser/react/Link.tsx +19 -7
  25. package/src/browser/rsc-router.tsx +11 -2
  26. package/src/browser/server-action-bridge.ts +448 -432
  27. package/src/browser/types.ts +24 -0
  28. package/src/build/generate-route-types.ts +2 -0
  29. package/src/build/route-trie.ts +19 -3
  30. package/src/build/route-types/router-processing.ts +125 -15
  31. package/src/client.rsc.tsx +2 -1
  32. package/src/client.tsx +1 -46
  33. package/src/handles/breadcrumbs.ts +66 -0
  34. package/src/handles/index.ts +1 -0
  35. package/src/host/index.ts +0 -3
  36. package/src/index.rsc.ts +5 -36
  37. package/src/index.ts +32 -66
  38. package/src/prerender/store.ts +56 -15
  39. package/src/route-definition/index.ts +0 -3
  40. package/src/router/handler-context.ts +30 -3
  41. package/src/router/loader-resolution.ts +1 -1
  42. package/src/router/match-api.ts +1 -1
  43. package/src/router/match-result.ts +0 -9
  44. package/src/router/metrics.ts +233 -13
  45. package/src/router/middleware-types.ts +53 -10
  46. package/src/router/middleware.ts +170 -81
  47. package/src/router/pattern-matching.ts +20 -5
  48. package/src/router/prerender-match.ts +4 -0
  49. package/src/router/revalidation.ts +27 -7
  50. package/src/router/router-interfaces.ts +14 -1
  51. package/src/router/router-options.ts +13 -8
  52. package/src/router/segment-resolution/fresh.ts +18 -0
  53. package/src/router/segment-resolution/helpers.ts +1 -1
  54. package/src/router/segment-resolution/revalidation.ts +22 -9
  55. package/src/router/trie-matching.ts +20 -2
  56. package/src/router.ts +29 -9
  57. package/src/rsc/handler.ts +106 -11
  58. package/src/rsc/index.ts +0 -20
  59. package/src/rsc/progressive-enhancement.ts +21 -8
  60. package/src/rsc/rsc-rendering.ts +30 -43
  61. package/src/rsc/server-action.ts +14 -10
  62. package/src/rsc/ssr-setup.ts +128 -0
  63. package/src/rsc/types.ts +2 -0
  64. package/src/search-params.ts +16 -13
  65. package/src/server/context.ts +8 -2
  66. package/src/server/request-context.ts +38 -16
  67. package/src/server.ts +6 -0
  68. package/src/theme/index.ts +4 -13
  69. package/src/types/handler-context.ts +12 -16
  70. package/src/types/route-config.ts +17 -8
  71. package/src/types/segments.ts +0 -5
  72. package/src/vite/discovery/bundle-postprocess.ts +31 -56
  73. package/src/vite/discovery/discover-routers.ts +18 -4
  74. package/src/vite/discovery/prerender-collection.ts +34 -14
  75. package/src/vite/discovery/state.ts +4 -7
  76. package/src/vite/index.ts +4 -3
  77. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  78. package/src/vite/plugins/refresh-cmd.ts +65 -0
  79. package/src/vite/rango.ts +11 -0
  80. package/src/vite/router-discovery.ts +16 -0
  81. package/src/vite/utils/prerender-utils.ts +60 -0
  82. package/skills/testing/SKILL.md +0 -226
  83. package/src/route-definition/route-function.ts +0 -119
  84. /package/{CLAUDE.md → AGENTS.md} +0 -0
@@ -12,6 +12,8 @@ import {
12
12
  getLocationState,
13
13
  } from "../server/request-context.js";
14
14
  import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
15
+ import { appendMetric } from "../router/metrics.js";
16
+ import { getSSRSetup } from "./ssr-setup.js";
15
17
  import type { RscPayload } from "./types.js";
16
18
  import {
17
19
  createResponseWithMergedHeaders,
@@ -28,13 +30,9 @@ export async function handleRscRendering<TEnv>(
28
30
  handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
29
31
  nonce: string | undefined,
30
32
  ): Promise<Response> {
31
- // Retrieve handler-level timing from variables
32
33
  const reqCtx = requireRequestContext();
33
- const handlerTimingArr: string[] = reqCtx.var.__handlerTiming || [];
34
- const handlerStart: number = reqCtx.var.__handlerStart || 0;
35
34
 
36
35
  let payload: RscPayload;
37
- let serverTiming: string | undefined;
38
36
  let hasInterceptSlots = false;
39
37
 
40
38
  if (isPartial) {
@@ -53,8 +51,6 @@ export async function handleRscRendering<TEnv>(
53
51
  return createSimpleRedirectResponse(match.redirect);
54
52
  }
55
53
 
56
- serverTiming = match.serverTiming;
57
-
58
54
  payload = {
59
55
  metadata: {
60
56
  pathname: url.pathname,
@@ -66,13 +62,14 @@ export async function handleRscRendering<TEnv>(
66
62
  rootLayout: ctx.router.rootLayout,
67
63
  handles: handleStore.stream(),
68
64
  version: ctx.version,
65
+ prefetchCacheTTL: ctx.router.prefetchCacheTTL,
69
66
  themeConfig: ctx.router.themeConfig,
70
67
  initialTheme: reqCtx.theme,
71
68
  },
72
69
  };
73
70
  } else {
74
71
  setRequestContextParams(result.params, result.routeName);
75
- serverTiming = result.serverTiming;
72
+
76
73
  hasInterceptSlots = !!result.slots;
77
74
 
78
75
  payload = {
@@ -86,6 +83,7 @@ export async function handleRscRendering<TEnv>(
86
83
  slots: result.slots,
87
84
  handles: handleStore.stream(),
88
85
  version: ctx.version,
86
+ prefetchCacheTTL: ctx.router.prefetchCacheTTL,
89
87
  },
90
88
  };
91
89
  }
@@ -132,8 +130,6 @@ export async function handleRscRendering<TEnv>(
132
130
  { headers: { "Content-Type": "application/json" } },
133
131
  );
134
132
  } else {
135
- serverTiming = match.serverTiming;
136
-
137
133
  payload = {
138
134
  // Initial SSR can reconstruct the tree from segments + rootLayout,
139
135
  // so we omit root to avoid sending the same structure twice.
@@ -148,6 +144,7 @@ export async function handleRscRendering<TEnv>(
148
144
  rootLayout: ctx.router.rootLayout,
149
145
  handles: handleStore.stream(),
150
146
  version: ctx.version,
147
+ prefetchCacheTTL: ctx.router.prefetchCacheTTL,
151
148
  themeConfig: ctx.router.themeConfig,
152
149
  initialTheme: reqCtx.theme,
153
150
  },
@@ -166,10 +163,20 @@ export async function handleRscRendering<TEnv>(
166
163
  }
167
164
  }
168
165
 
166
+ const metricsStore = reqCtx._metricsStore;
167
+ const renderStart = performance.now();
168
+
169
169
  // Serialize to RSC stream
170
170
  const rscSerializeStart = performance.now();
171
171
  const rscStream = ctx.renderToReadableStream<RscPayload>(payload);
172
172
  const rscSerializeDur = performance.now() - rscSerializeStart;
173
+ // This measures synchronous stream creation, not end-to-end stream consumption.
174
+ appendMetric(
175
+ metricsStore,
176
+ "rsc-serialize",
177
+ rscSerializeStart,
178
+ rscSerializeDur,
179
+ );
173
180
 
174
181
  // Determine if this is an RSC request or HTML request.
175
182
  // Partial requests (_rsc_partial) are always RSC -- they come from client-side
@@ -181,15 +188,9 @@ export async function handleRscRendering<TEnv>(
181
188
  !url.searchParams.has("__html")) ||
182
189
  url.searchParams.has("__rsc");
183
190
 
184
- // Build complete Server-Timing: handler phases + match/manifest + RSC serialize
185
- const timingParts: string[] = [...handlerTimingArr];
186
- if (serverTiming) {
187
- timingParts.push(serverTiming);
188
- }
189
- timingParts.push(`rsc-serialize;dur=${rscSerializeDur.toFixed(2)}`);
190
-
191
191
  if (isRscRequest) {
192
- const fullTiming = timingParts.join(", ");
192
+ const renderDur = performance.now() - renderStart;
193
+ appendMetric(metricsStore, "render:total", renderStart, renderDur);
193
194
  const rscHeaders: Record<string, string> = {
194
195
  "content-type": "text/x-component;charset=utf-8",
195
196
  vary: "accept, X-Rango-State, X-RSC-Router-Client-Path",
@@ -205,22 +206,19 @@ export async function handleRscRendering<TEnv>(
205
206
  rscHeaders["cache-control"] = cc;
206
207
  }
207
208
  }
208
- if (fullTiming) {
209
- rscHeaders["Server-Timing"] = fullTiming;
210
- }
211
209
  return createResponseWithMergedHeaders(rscStream, {
212
210
  headers: rscHeaders,
213
211
  });
214
212
  }
215
213
 
216
- // Delegate to SSR for HTML response
217
- const ssrSetupStart = performance.now();
218
- const [ssrModule, streamMode] = await Promise.all([
219
- ctx.loadSSRModule(),
220
- ctx.resolveStreamMode(request, env, url),
221
- ]);
222
- const ssrSetupDur = performance.now() - ssrSetupStart;
223
- timingParts.push(`ssr-setup;dur=${ssrSetupDur.toFixed(2)}`);
214
+ // Delegate to SSR for HTML response (reuse early setup if available)
215
+ const [ssrModule, streamMode] = await getSSRSetup(
216
+ ctx,
217
+ request,
218
+ env,
219
+ url,
220
+ metricsStore,
221
+ );
224
222
 
225
223
  const ssrRenderStart = performance.now();
226
224
  const htmlStream = await ssrModule.renderHTML(rscStream, {
@@ -228,23 +226,12 @@ export async function handleRscRendering<TEnv>(
228
226
  streamMode,
229
227
  });
230
228
  const ssrRenderDur = performance.now() - ssrRenderStart;
231
- timingParts.push(`ssr-render-html;dur=${ssrRenderDur.toFixed(2)}`);
229
+ appendMetric(metricsStore, "ssr-render-html", ssrRenderStart, ssrRenderDur);
232
230
 
233
- // Add total handler duration
234
- if (handlerStart) {
235
- const totalHandler = performance.now() - handlerStart;
236
- timingParts.push(`handler-total;dur=${totalHandler.toFixed(2)}`);
237
- }
238
-
239
- const fullTiming = timingParts.join(", ");
240
- const htmlHeaders: Record<string, string> = {
241
- "content-type": "text/html;charset=utf-8",
242
- };
243
- if (fullTiming) {
244
- htmlHeaders["Server-Timing"] = fullTiming;
245
- }
231
+ const renderDur = performance.now() - renderStart;
232
+ appendMetric(metricsStore, "render:total", renderStart, renderDur);
246
233
 
247
234
  return createResponseWithMergedHeaders(htmlStream, {
248
- headers: htmlHeaders,
235
+ headers: { "content-type": "text/html;charset=utf-8" },
249
236
  });
250
237
  }
@@ -21,6 +21,7 @@ import {
21
21
  getLocationState,
22
22
  } from "../server/request-context.js";
23
23
  import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
24
+ import { appendMetric } from "../router/metrics.js";
24
25
  import type { RscPayload } from "./types.js";
25
26
  import {
26
27
  hasBodyContent,
@@ -274,6 +275,8 @@ export async function revalidateAfterAction<TEnv>(
274
275
  ): Promise<Response> {
275
276
  const { returnValue, actionStatus, temporaryReferences, actionContext } =
276
277
  continuation;
278
+ const reqCtx = requireRequestContext();
279
+ const metricsStore = reqCtx._metricsStore;
277
280
 
278
281
  const matchResult = await ctx.router.matchPartial(
279
282
  request,
@@ -308,8 +311,6 @@ export async function revalidateAfterAction<TEnv>(
308
311
  // Return updated segments
309
312
  setRequestContextParams(matchResult.params, matchResult.routeName);
310
313
 
311
- const serverTiming = matchResult.serverTiming;
312
-
313
314
  const payload: RscPayload = {
314
315
  metadata: {
315
316
  pathname: url.pathname,
@@ -326,19 +327,22 @@ export async function revalidateAfterAction<TEnv>(
326
327
 
327
328
  attachLocationState(payload);
328
329
 
330
+ const renderStart = performance.now();
329
331
  const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
330
332
  temporaryReferences,
331
333
  });
332
-
333
- const actionHeaders: Record<string, string> = {
334
- "content-type": "text/x-component;charset=utf-8",
335
- };
336
- if (serverTiming) {
337
- actionHeaders["Server-Timing"] = serverTiming;
338
- }
334
+ const rscSerializeDur = performance.now() - renderStart;
335
+ // This measures synchronous stream creation, not end-to-end stream consumption.
336
+ appendMetric(metricsStore, "rsc-serialize", renderStart, rscSerializeDur);
337
+ appendMetric(
338
+ metricsStore,
339
+ "render:total",
340
+ renderStart,
341
+ performance.now() - renderStart,
342
+ );
339
343
 
340
344
  return createResponseWithMergedHeaders(rscStream, {
341
345
  status: actionStatus,
342
- headers: actionHeaders,
346
+ headers: { "content-type": "text/x-component;charset=utf-8" },
343
347
  });
344
348
  }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * SSR Setup Utilities
3
+ *
4
+ * Manages early kickoff and retrieval of SSR module loading and stream mode
5
+ * resolution. Both operations are request-scoped but independent of route
6
+ * matching, so they can run in parallel with segment resolution.
7
+ */
8
+
9
+ import type { HandlerContext } from "./handler-context.js";
10
+ import type { SSRModule } from "./types.js";
11
+ import type { SSRStreamMode } from "../router/router-options.js";
12
+ import type { MetricsStore } from "../server/context.js";
13
+ import { appendMetric } from "../router/metrics.js";
14
+ import { _getRequestContext } from "../server/request-context.js";
15
+
16
+ export type SSRSetup = readonly [SSRModule, SSRStreamMode];
17
+
18
+ /**
19
+ * Key used to stash the early SSR setup promise on request variables.
20
+ * Read back via `getSSRSetup`.
21
+ */
22
+ export const SSR_SETUP_VAR = "__ssrSetup";
23
+
24
+ /**
25
+ * Start loading the SSR module and resolving the stream mode in parallel.
26
+ * When a `getMetricsStore` getter is provided, records individual
27
+ * `ssr:module-load` and `ssr:stream-mode` metrics (the getter is called
28
+ * lazily so stores created after kickoff are still captured). Without a
29
+ * getter the promises run bare — no `.then()` microtasks, no
30
+ * `performance.now()` calls — keeping the non-debug hot path lean.
31
+ */
32
+ export function startSSRSetup<TEnv>(
33
+ ctx: HandlerContext<TEnv>,
34
+ request: Request,
35
+ env: TEnv,
36
+ url: URL,
37
+ getMetricsStore?: () => MetricsStore | undefined,
38
+ ): Promise<SSRSetup> {
39
+ if (!getMetricsStore) {
40
+ return Promise.all([
41
+ ctx.loadSSRModule(),
42
+ ctx.resolveStreamMode(request, env, url),
43
+ ]);
44
+ }
45
+ const start = performance.now();
46
+ return Promise.all([
47
+ ctx.loadSSRModule().then((mod) => {
48
+ appendMetric(
49
+ getMetricsStore(),
50
+ "ssr:module-load",
51
+ start,
52
+ performance.now() - start,
53
+ );
54
+ return mod;
55
+ }),
56
+ ctx.resolveStreamMode(request, env, url).then((mode) => {
57
+ appendMetric(
58
+ getMetricsStore(),
59
+ "ssr:stream-mode",
60
+ start,
61
+ performance.now() - start,
62
+ );
63
+ return mode;
64
+ }),
65
+ ]);
66
+ }
67
+
68
+ /**
69
+ * Retrieve the SSR setup result. Returns the early-kicked-off promise
70
+ * when available (stashed on request variables), otherwise starts a
71
+ * fresh setup.
72
+ */
73
+ export function getSSRSetup<TEnv>(
74
+ ctx: HandlerContext<TEnv>,
75
+ request: Request,
76
+ env: TEnv,
77
+ url: URL,
78
+ metricsStore: MetricsStore | undefined,
79
+ ): Promise<SSRSetup> {
80
+ const early = _getRequestContext()?.var?.[SSR_SETUP_VAR] as
81
+ | Promise<SSRSetup>
82
+ | undefined;
83
+ if (early) return early;
84
+ return startSSRSetup(
85
+ ctx,
86
+ request,
87
+ env,
88
+ url,
89
+ metricsStore ? () => metricsStore : undefined,
90
+ );
91
+ }
92
+
93
+ /**
94
+ * Classify whether a request may require SSR (HTML rendering).
95
+ *
96
+ * Returns false for requests that are definitively RSC-only, loader fetches,
97
+ * prerender collection, or Accept-based RSC (no text/html). This mirrors
98
+ * the isRscRequest decision in rsc-rendering.ts.
99
+ *
100
+ * Note: response/mime routes are excluded by the caller — this function
101
+ * runs after previewMatch() classifies the route type.
102
+ */
103
+ export function mayNeedSSR(request: Request, url: URL): boolean {
104
+ if (
105
+ url.searchParams.has("_rsc_partial") ||
106
+ url.searchParams.has("_rsc_action") ||
107
+ request.headers.has("rsc-action") ||
108
+ url.searchParams.has("_rsc_loader") ||
109
+ url.searchParams.has("__rsc") ||
110
+ url.searchParams.has("__prerender_collect")
111
+ ) {
112
+ return false;
113
+ }
114
+
115
+ // Mirror the Accept-based RSC decision from rsc-rendering.ts:
116
+ // if Accept is present and does not include text/html (and no __html override),
117
+ // the response will be RSC, not HTML.
118
+ const accept = request.headers.get("accept");
119
+ if (
120
+ accept &&
121
+ !accept.includes("text/html") &&
122
+ !url.searchParams.has("__html")
123
+ ) {
124
+ return false;
125
+ }
126
+
127
+ return true;
128
+ }
package/src/rsc/types.ts CHANGED
@@ -32,6 +32,8 @@ export interface RscPayload {
32
32
  handles?: AsyncGenerator<HandleData, void, unknown>;
33
33
  /** RSC version string for cache invalidation */
34
34
  version?: string;
35
+ /** TTL in milliseconds for the client-side in-memory prefetch cache */
36
+ prefetchCacheTTL?: number;
35
37
  /** Theme configuration for FOUC prevention */
36
38
  themeConfig?: ResolvedThemeConfig | null;
37
39
  /** Initial theme from cookie (for SSR hydration) */
@@ -55,14 +55,22 @@ type Simplify<T> = { [K in keyof T]: T[K] };
55
55
  /**
56
56
  * Resolve a SearchSchema to its typed object.
57
57
  *
58
+ * Both required and optional params resolve to `T | undefined` at the handler
59
+ * level. The required/optional distinction is a consumer-facing contract
60
+ * (e.g., for href() and reverse() autocomplete) — it tells callers which
61
+ * params the route expects, but the handler must still check for undefined
62
+ * since the framework cannot trust the client to send all required params.
63
+ *
58
64
  * @example
59
65
  * type S = { q: "string"; page: "number?"; sort: "string?" };
60
66
  * type R = ResolveSearchSchema<S>;
61
- * // { q: string; page?: number; sort?: string }
67
+ * // { q: string | undefined; page?: number; sort?: string }
62
68
  */
63
69
  export type ResolveSearchSchema<T extends SearchSchema> = Simplify<
64
70
  {
65
- [K in RequiredKeys<T> & string]: ResolveBaseType<BaseType<T[K]>>;
71
+ [K in RequiredKeys<T> & string]:
72
+ | ResolveBaseType<BaseType<T[K]>>
73
+ | undefined;
66
74
  } & {
67
75
  [K in OptionalKeys<T> & string]?: ResolveBaseType<BaseType<T[K]>>;
68
76
  }
@@ -166,7 +174,9 @@ type ExtractParamsFromPattern<T extends string> =
166
174
  * - `"number"` / `"number?"` - coerced via `Number()`; NaN treated as missing
167
175
  * - `"boolean"` / `"boolean?"` - `"true"` / `"1"` -> true, `"false"` / `"0"` / `""` -> false
168
176
  *
169
- * Missing required params are set to their zero value (empty string / 0 / false).
177
+ * Missing params (both required and optional) are omitted from the result
178
+ * (undefined). The required/optional distinction is a consumer-facing contract
179
+ * only — the handler must check for undefined.
170
180
  */
171
181
  export function parseSearchParams<T extends SearchSchema>(
172
182
  searchParams: URLSearchParams,
@@ -180,13 +190,7 @@ export function parseSearchParams<T extends SearchSchema>(
180
190
  const raw = searchParams.get(key);
181
191
 
182
192
  if (raw === null) {
183
- if (!isOptional) {
184
- // Required param missing: use zero value
185
- if (baseType === "string") result[key] = "";
186
- else if (baseType === "number") result[key] = 0;
187
- else if (baseType === "boolean") result[key] = false;
188
- }
189
- // Optional params are omitted (undefined)
193
+ // Missing params are omitted (undefined) regardless of required/optional
190
194
  continue;
191
195
  }
192
196
 
@@ -194,11 +198,10 @@ export function parseSearchParams<T extends SearchSchema>(
194
198
  result[key] = raw;
195
199
  } else if (baseType === "number") {
196
200
  const num = Number(raw);
197
- if (Number.isNaN(num)) {
198
- if (!isOptional) result[key] = 0;
199
- } else {
201
+ if (!Number.isNaN(num)) {
200
202
  result[key] = num;
201
203
  }
204
+ // NaN treated as missing (undefined)
202
205
  } else if (baseType === "boolean") {
203
206
  result[key] = raw === "true" || raw === "1";
204
207
  }
@@ -26,6 +26,7 @@ export interface PerformanceMetric {
26
26
  label: string; // e.g., "route-matching", "loader:UserLoader"
27
27
  duration: number; // milliseconds
28
28
  startTime: number; // relative to request start
29
+ depth?: number; // nesting level for hierarchical display (0 = top-level)
29
30
  }
30
31
 
31
32
  /**
@@ -567,7 +568,7 @@ export type { HelperContext };
567
568
  * done(); // Records duration
568
569
  * ```
569
570
  */
570
- export function track(label: string): () => void {
571
+ export function track(label: string, depth?: number): () => void {
571
572
  const store = RSCRouterContext.getStore();
572
573
 
573
574
  // No-op if context unavailable or metrics not enabled
@@ -580,6 +581,11 @@ export function track(label: string): () => void {
580
581
  return () => {
581
582
  const duration =
582
583
  performance.now() - store.metrics!.requestStart - startTime;
583
- store.metrics!.metrics.push({ label, duration, startTime });
584
+ store.metrics!.metrics.push({
585
+ label,
586
+ duration,
587
+ startTime,
588
+ ...(depth != null ? { depth } : {}),
589
+ });
584
590
  };
585
591
  }
@@ -15,6 +15,7 @@ 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";
@@ -22,7 +23,7 @@ import type { Handle } from "../handle.js";
22
23
  import { type ContextVar, contextGet, contextSet } from "../context-var.js";
23
24
  import { createHandleStore, type HandleStore } from "./handle-store.js";
24
25
  import { isHandle } from "../handle.js";
25
- import { track } from "./context.js";
26
+ import { track, type MetricsStore } from "./context.js";
26
27
  import { getFetchableLoader } from "./fetchable-loader-store.js";
27
28
  import type { SegmentCacheStore } from "../cache/types.js";
28
29
  import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
@@ -41,15 +42,20 @@ import { isAutoGeneratedRouteName } from "../route-name.js";
41
42
  * Use this when you need access to request data outside of route handlers.
42
43
  */
43
44
  export interface RequestContext<
44
- TEnv = unknown,
45
+ TEnv = DefaultEnv,
45
46
  TParams = Record<string, string>,
46
47
  > {
47
48
  /** Platform bindings (Cloudflare env, etc.) */
48
49
  env: TEnv;
49
50
  /** Original HTTP request */
50
51
  request: Request;
51
- /** Parsed URL (system params like _rsc* are NOT filtered here) */
52
+ /** Parsed URL (with internal `_rsc*` params stripped) */
52
53
  url: URL;
54
+ /**
55
+ * The original request URL with all parameters intact, including
56
+ * internal `_rsc*` transport params.
57
+ */
58
+ originalUrl: URL;
53
59
  /** URL pathname */
54
60
  pathname: string;
55
61
  /** URL search params (system params like _rsc* are NOT filtered here) */
@@ -71,12 +77,7 @@ export interface RequestContext<
71
77
  * Initially empty, then set to matched params
72
78
  */
73
79
  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
- */
80
+ /** @internal Stub response for collecting headers/cookies. Use ctx.headers or ctx.header() instead. */
80
81
  readonly res: Response;
81
82
 
82
83
  /** @internal Get a cookie value (effective: request + response mutations). Use cookies().get() instead. */
@@ -94,6 +95,8 @@ export interface RequestContext<
94
95
  header(name: string, value: string): void;
95
96
  /** Set the response status code */
96
97
  setStatus(status: number): void;
98
+ /** @internal Set status bypassing cache-exec guard (for framework error handling) */
99
+ _setStatus(status: number): void;
97
100
 
98
101
  /**
99
102
  * Access loader data or push handle data.
@@ -265,6 +268,12 @@ export interface RequestContext<
265
268
  * errors without failing the response.
266
269
  */
267
270
  _reportBackgroundError?: (error: unknown, category: string) => void;
271
+
272
+ /** @internal Per-request debug performance override (set via ctx.debugPerformance()) */
273
+ _debugPerformance?: boolean;
274
+
275
+ /** @internal Request-scoped performance metrics store */
276
+ _metricsStore?: MetricsStore;
268
277
  }
269
278
 
270
279
  /**
@@ -274,7 +283,7 @@ export interface RequestContext<
274
283
  * use the full RequestContext interface directly.
275
284
  */
276
285
  export type PublicRequestContext<
277
- TEnv = unknown,
286
+ TEnv = DefaultEnv,
278
287
  TParams = Record<string, string>,
279
288
  > = Omit<
280
289
  RequestContext<TEnv, TParams>,
@@ -292,6 +301,10 @@ export type PublicRequestContext<
292
301
  | "_prevRouteKey"
293
302
  | "_reportedErrors"
294
303
  | "_reportBackgroundError"
304
+ | "_debugPerformance"
305
+ | "_metricsStore"
306
+ | "_setStatus"
307
+ | "res"
295
308
  >;
296
309
 
297
310
  // AsyncLocalStorage instance for request context
@@ -312,7 +325,7 @@ export function runWithRequestContext<TEnv, T>(
312
325
  * Get the current request context
313
326
  * Throws if called outside of a request context
314
327
  */
315
- export function getRequestContext<TEnv = unknown>(): RequestContext<TEnv> {
328
+ export function getRequestContext<TEnv = DefaultEnv>(): RequestContext<TEnv> {
316
329
  const ctx = requestContextStorage.getStore() as
317
330
  | RequestContext<TEnv>
318
331
  | undefined;
@@ -329,7 +342,7 @@ export function getRequestContext<TEnv = unknown>(): RequestContext<TEnv> {
329
342
  * @internal Get the request context without throwing — for internal code that
330
343
  * may run outside a request context (cache stores, optional handle lookups, etc.)
331
344
  */
332
- export function _getRequestContext<TEnv = unknown>():
345
+ export function _getRequestContext<TEnv = DefaultEnv>():
333
346
  | RequestContext<TEnv>
334
347
  | undefined {
335
348
  return requestContextStorage.getStore() as RequestContext<TEnv> | undefined;
@@ -394,7 +407,9 @@ export function getLocationState(): LocationStateEntry[] | undefined {
394
407
  * Get the current request context, throwing if not available
395
408
  * @deprecated Use getRequestContext() directly — it now throws if outside context
396
409
  */
397
- export function requireRequestContext<TEnv = unknown>(): RequestContext<TEnv> {
410
+ export function requireRequestContext<
411
+ TEnv = DefaultEnv,
412
+ >(): RequestContext<TEnv> {
398
413
  return getRequestContext<TEnv>();
399
414
  }
400
415
 
@@ -545,6 +560,7 @@ export function createRequestContext<TEnv>(
545
560
  env,
546
561
  request,
547
562
  url,
563
+ originalUrl: new URL(request.url),
548
564
  pathname: url.pathname,
549
565
  searchParams: url.searchParams,
550
566
  var: variables,
@@ -616,8 +632,13 @@ export function createRequestContext<TEnv>(
616
632
 
617
633
  setStatus(status: number): void {
618
634
  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.
635
+ stubResponse = new Response(null, {
636
+ status,
637
+ headers: stubResponse.headers,
638
+ });
639
+ },
640
+
641
+ _setStatus(status: number): void {
621
642
  stubResponse = new Response(null, {
622
643
  status,
623
644
  headers: stubResponse.headers,
@@ -674,6 +695,7 @@ export function createRequestContext<TEnv>(
674
695
  _locationState: undefined,
675
696
 
676
697
  _reportedErrors: new WeakSet<object>(),
698
+ _metricsStore: undefined,
677
699
 
678
700
  reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
679
701
  };
@@ -879,7 +901,7 @@ export function createUseFunction<TEnv>(
879
901
  };
880
902
 
881
903
  // Start loader execution with tracking
882
- const doneLoader = track(`loader:${loader.$$id}`);
904
+ const doneLoader = track(`loader:${loader.$$id}`, 2);
883
905
  const promise = Promise.resolve(loaderFn(loaderCtx)).finally(() => {
884
906
  doneLoader();
885
907
  });
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,
@@ -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";