@rangojs/router 0.0.0-experimental.ae6e7825 → 0.0.0-experimental.b02a2fec

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 (104) hide show
  1. package/README.md +76 -18
  2. package/dist/bin/rango.js +130 -47
  3. package/dist/vite/index.js +688 -433
  4. package/dist/vite/index.js.bak +5448 -0
  5. package/package.json +1 -1
  6. package/skills/links/SKILL.md +3 -1
  7. package/skills/middleware/SKILL.md +2 -0
  8. package/skills/prerender/SKILL.md +110 -68
  9. package/skills/router-setup/SKILL.md +35 -0
  10. package/src/__internal.ts +1 -1
  11. package/src/browser/app-version.ts +14 -0
  12. package/src/browser/navigation-bridge.ts +19 -4
  13. package/src/browser/navigation-client.ts +64 -63
  14. package/src/browser/navigation-store.ts +43 -8
  15. package/src/browser/partial-update.ts +27 -5
  16. package/src/browser/prefetch/fetch.ts +8 -2
  17. package/src/browser/react/Link.tsx +44 -8
  18. package/src/browser/react/NavigationProvider.tsx +8 -1
  19. package/src/browser/react/context.ts +7 -2
  20. package/src/browser/react/use-handle.ts +9 -58
  21. package/src/browser/react/use-router.ts +21 -8
  22. package/src/browser/rsc-router.tsx +28 -22
  23. package/src/browser/scroll-restoration.ts +10 -8
  24. package/src/browser/server-action-bridge.ts +8 -18
  25. package/src/browser/types.ts +21 -15
  26. package/src/build/generate-manifest.ts +6 -6
  27. package/src/build/generate-route-types.ts +3 -0
  28. package/src/build/route-types/include-resolution.ts +8 -1
  29. package/src/build/route-types/router-processing.ts +211 -72
  30. package/src/build/route-types/scan-filter.ts +8 -1
  31. package/src/client.tsx +2 -56
  32. package/src/deps/browser.ts +0 -1
  33. package/src/handle.ts +40 -0
  34. package/src/index.rsc.ts +3 -1
  35. package/src/index.ts +12 -0
  36. package/src/prerender/store.ts +5 -4
  37. package/src/prerender.ts +138 -77
  38. package/src/reverse.ts +22 -1
  39. package/src/route-definition/dsl-helpers.ts +42 -19
  40. package/src/route-definition/helpers-types.ts +4 -1
  41. package/src/route-definition/index.ts +3 -0
  42. package/src/route-definition/redirect.ts +9 -1
  43. package/src/route-definition/resolve-handler-use.ts +149 -0
  44. package/src/route-types.ts +11 -0
  45. package/src/router/content-negotiation.ts +100 -1
  46. package/src/router/handler-context.ts +48 -15
  47. package/src/router/intercept-resolution.ts +9 -4
  48. package/src/router/loader-resolution.ts +150 -21
  49. package/src/router/match-api.ts +124 -189
  50. package/src/router/match-middleware/cache-lookup.ts +28 -8
  51. package/src/router/match-middleware/segment-resolution.ts +53 -0
  52. package/src/router/match-result.ts +82 -4
  53. package/src/router/middleware-types.ts +0 -6
  54. package/src/router/middleware.ts +0 -3
  55. package/src/router/navigation-snapshot.ts +182 -0
  56. package/src/router/prerender-match.ts +110 -10
  57. package/src/router/preview-match.ts +30 -102
  58. package/src/router/request-classification.ts +310 -0
  59. package/src/router/route-snapshot.ts +245 -0
  60. package/src/router/router-interfaces.ts +36 -4
  61. package/src/router/router-options.ts +37 -11
  62. package/src/router/segment-resolution/fresh.ts +70 -5
  63. package/src/router/segment-resolution/revalidation.ts +87 -9
  64. package/src/router.ts +53 -5
  65. package/src/rsc/handler.ts +472 -398
  66. package/src/rsc/loader-fetch.ts +18 -3
  67. package/src/rsc/manifest-init.ts +5 -1
  68. package/src/rsc/progressive-enhancement.ts +12 -3
  69. package/src/rsc/rsc-rendering.ts +8 -3
  70. package/src/rsc/server-action.ts +8 -2
  71. package/src/rsc/ssr-setup.ts +2 -2
  72. package/src/rsc/types.ts +6 -7
  73. package/src/server/context.ts +39 -2
  74. package/src/server/handle-store.ts +19 -0
  75. package/src/server/loader-registry.ts +9 -8
  76. package/src/server/request-context.ts +131 -16
  77. package/src/ssr/index.tsx +4 -13
  78. package/src/static-handler.ts +18 -6
  79. package/src/types/cache-types.ts +4 -4
  80. package/src/types/handler-context.ts +17 -11
  81. package/src/types/loader-types.ts +32 -5
  82. package/src/types/route-entry.ts +1 -1
  83. package/src/types/segments.ts +1 -0
  84. package/src/urls/path-helper-types.ts +9 -2
  85. package/src/urls/path-helper.ts +47 -12
  86. package/src/urls/pattern-types.ts +12 -0
  87. package/src/urls/response-types.ts +16 -6
  88. package/src/use-loader.tsx +77 -5
  89. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  90. package/src/vite/discovery/discover-routers.ts +5 -1
  91. package/src/vite/discovery/prerender-collection.ts +128 -74
  92. package/src/vite/discovery/state.ts +13 -4
  93. package/src/vite/index.ts +4 -0
  94. package/src/vite/plugin-types.ts +60 -5
  95. package/src/vite/plugins/expose-id-utils.ts +12 -0
  96. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  97. package/src/vite/plugins/expose-internal-ids.ts +257 -40
  98. package/src/vite/plugins/performance-tracks.ts +57 -280
  99. package/src/vite/plugins/refresh-cmd.ts +88 -26
  100. package/src/vite/rango.ts +17 -11
  101. package/src/vite/router-discovery.ts +178 -37
  102. package/src/vite/utils/prerender-utils.ts +18 -0
  103. package/src/vite/utils/shared-utils.ts +3 -2
  104. package/src/browser/debug-channel.ts +0 -112
@@ -168,11 +168,17 @@ export async function handleLoaderFetch<TEnv>(
168
168
  loaderResult: unknown;
169
169
  }
170
170
  const loaderPayload: LoaderPayload = { loaderResult: result };
171
- const debugChannel = reqCtx._debugChannel;
172
171
  const rscStream = ctx.renderToReadableStream<LoaderPayload>(
173
172
  loaderPayload,
174
173
  {
175
- ...(debugChannel && { debugChannel }),
174
+ onError: (error: unknown) => {
175
+ ctx.callOnError(error, "rendering", {
176
+ request,
177
+ url,
178
+ env,
179
+ loaderName: loaderId,
180
+ });
181
+ },
176
182
  },
177
183
  );
178
184
 
@@ -204,7 +210,16 @@ export async function handleLoaderFetch<TEnv>(
204
210
  name: err.name,
205
211
  },
206
212
  };
207
- const rscStream = ctx.renderToReadableStream(errorPayload);
213
+ const rscStream = ctx.renderToReadableStream(errorPayload, {
214
+ onError: (error: unknown) => {
215
+ ctx.callOnError(error, "rendering", {
216
+ request,
217
+ url,
218
+ env,
219
+ loaderName: loaderId,
220
+ });
221
+ },
222
+ });
208
223
 
209
224
  return createResponseWithMergedHeaders(rscStream, {
210
225
  status: 500,
@@ -31,7 +31,11 @@ export async function buildRouterTrieFromUrlpatterns(
31
31
  ): Promise<void> {
32
32
  const { generateManifestFull } =
33
33
  await import("../build/generate-manifest.js");
34
- const generated = generateManifestFull(router.urlpatterns);
34
+ const generated = generateManifestFull(
35
+ router.urlpatterns,
36
+ undefined,
37
+ router.basename ? { urlPrefix: router.basename } : undefined,
38
+ );
35
39
  if (
36
40
  generated._routeAncestry &&
37
41
  Object.keys(generated._routeAncestry).length > 0
@@ -243,6 +243,8 @@ export async function handleProgressiveEnhancement<TEnv>(
243
243
  const payload: RscPayload = {
244
244
  metadata: {
245
245
  pathname: url.pathname,
246
+ routerId: ctx.router.id,
247
+ basename: ctx.router.basename,
246
248
  segments: match.segments,
247
249
  matched: match.matched,
248
250
  diff: match.diff,
@@ -257,9 +259,10 @@ export async function handleProgressiveEnhancement<TEnv>(
257
259
  formState: actionResult,
258
260
  };
259
261
 
260
- const debugChannel = requireRequestContext()._debugChannel;
261
262
  const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
262
- ...(debugChannel && { debugChannel }),
263
+ onError: (error: unknown) => {
264
+ ctx.callOnError(error, "rendering", { request, url, env });
265
+ },
263
266
  });
264
267
  // metricsStore=undefined is safe: the handler already stashed the early
265
268
  // SSR setup promise on request variables, so getSSRSetup returns it
@@ -345,6 +348,8 @@ async function renderPeErrorBoundary<TEnv>(
345
348
  const payload: RscPayload = {
346
349
  metadata: {
347
350
  pathname: url.pathname,
351
+ routerId: ctx.router.id,
352
+ basename: ctx.router.basename,
348
353
  segments: errorResult.segments,
349
354
  matched: errorResult.matched,
350
355
  diff: errorResult.diff,
@@ -359,7 +364,11 @@ async function renderPeErrorBoundary<TEnv>(
359
364
  },
360
365
  };
361
366
 
362
- const rscStream = ctx.renderToReadableStream<RscPayload>(payload);
367
+ const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
368
+ onError: (error: unknown) => {
369
+ ctx.callOnError(error, "rendering", { request, url, env });
370
+ },
371
+ });
363
372
  // metricsStore=undefined is safe: the handler already stashed the early
364
373
  // SSR setup promise on request variables, so getSSRSetup returns it
365
374
  // without falling back to a fresh startSSRSetup.
@@ -54,6 +54,8 @@ export async function handleRscRendering<TEnv>(
54
54
  payload = {
55
55
  metadata: {
56
56
  pathname: url.pathname,
57
+ routerId: ctx.router.id,
58
+ basename: ctx.router.basename,
57
59
  segments: match.segments,
58
60
  matched: match.matched,
59
61
  diff: match.diff,
@@ -75,6 +77,7 @@ export async function handleRscRendering<TEnv>(
75
77
  payload = {
76
78
  metadata: {
77
79
  pathname: url.pathname,
80
+ routerId: ctx.router.id,
78
81
  segments: result.segments,
79
82
  matched: result.matched,
80
83
  diff: result.diff,
@@ -136,6 +139,8 @@ export async function handleRscRendering<TEnv>(
136
139
 
137
140
  metadata: {
138
141
  pathname: url.pathname,
142
+ routerId: ctx.router.id,
143
+ basename: ctx.router.basename,
139
144
  segments: match.segments,
140
145
  matched: match.matched,
141
146
  diff: match.diff,
@@ -168,9 +173,10 @@ export async function handleRscRendering<TEnv>(
168
173
 
169
174
  // Serialize to RSC stream
170
175
  const rscSerializeStart = performance.now();
171
- const debugChannel = reqCtx._debugChannel;
172
176
  const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
173
- ...(debugChannel && { debugChannel }),
177
+ onError: (error: unknown) => {
178
+ ctx.callOnError(error, "rendering", { request, url, env });
179
+ },
174
180
  });
175
181
  const rscSerializeDur = performance.now() - rscSerializeStart;
176
182
  // This measures synchronous stream creation, not end-to-end stream consumption.
@@ -227,7 +233,6 @@ export async function handleRscRendering<TEnv>(
227
233
  const htmlStream = await ssrModule.renderHTML(rscStream, {
228
234
  nonce,
229
235
  streamMode,
230
- debugId: reqCtx._debugId,
231
236
  });
232
237
  const ssrRenderDur = performance.now() - ssrRenderStart;
233
238
  appendMetric(metricsStore, "ssr-render-html", ssrRenderStart, ssrRenderDur);
@@ -208,6 +208,7 @@ export async function executeServerAction<TEnv>(
208
208
  const payload: RscPayload = {
209
209
  metadata: {
210
210
  pathname: url.pathname,
211
+ routerId: ctx.router.id,
211
212
  segments: errorResult.segments,
212
213
  isPartial: true,
213
214
  matched: errorResult.matched,
@@ -223,10 +224,11 @@ export async function executeServerAction<TEnv>(
223
224
  // location state is a success-only semantic. Error boundary responses
224
225
  // update the error UI but should not mutate browser history state.
225
226
 
226
- const debugChannel = requireRequestContext()._debugChannel;
227
227
  const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
228
228
  temporaryReferences,
229
- ...(debugChannel && { debugChannel }),
229
+ onError: (error: unknown) => {
230
+ ctx.callOnError(error, "rendering", { request, url, env });
231
+ },
230
232
  });
231
233
 
232
234
  return createResponseWithMergedHeaders(rscStream, {
@@ -316,6 +318,7 @@ export async function revalidateAfterAction<TEnv>(
316
318
  const payload: RscPayload = {
317
319
  metadata: {
318
320
  pathname: url.pathname,
321
+ routerId: ctx.router.id,
319
322
  segments: matchResult.segments,
320
323
  isPartial: true,
321
324
  matched: matchResult.matched,
@@ -332,6 +335,9 @@ export async function revalidateAfterAction<TEnv>(
332
335
  const renderStart = performance.now();
333
336
  const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
334
337
  temporaryReferences,
338
+ onError: (error: unknown) => {
339
+ ctx.callOnError(error, "rendering", { request, url, env });
340
+ },
335
341
  });
336
342
  const rscSerializeDur = performance.now() - renderStart;
337
343
  // This measures synchronous stream creation, not end-to-end stream consumption.
@@ -77,7 +77,7 @@ export function getSSRSetup<TEnv>(
77
77
  url: URL,
78
78
  metricsStore: MetricsStore | undefined,
79
79
  ): Promise<SSRSetup> {
80
- const early = _getRequestContext()?.var?.[SSR_SETUP_VAR] as
80
+ const early = _getRequestContext()?._variables?.[SSR_SETUP_VAR] as
81
81
  | Promise<SSRSetup>
82
82
  | undefined;
83
83
  if (early) return early;
@@ -98,7 +98,7 @@ export function getSSRSetup<TEnv>(
98
98
  * the isRscRequest decision in rsc-rendering.ts.
99
99
  *
100
100
  * Note: response/mime routes are excluded by the caller — this function
101
- * runs after previewMatch() classifies the route type.
101
+ * runs after classifyRequest() determines the request mode.
102
102
  */
103
103
  export function mayNeedSSR(request: Request, url: URL): boolean {
104
104
  if (
package/src/rsc/types.ts CHANGED
@@ -19,6 +19,9 @@ export interface RscPayload {
19
19
  metadata?: {
20
20
  pathname: string;
21
21
  segments: ResolvedSegment[];
22
+ /** Router instance ID. When this changes between navigations, the client
23
+ * discards cached segments and does a full tree replacement (app switch). */
24
+ routerId?: string;
22
25
  isPartial?: boolean;
23
26
  isError?: boolean;
24
27
  matched?: string[];
@@ -38,6 +41,8 @@ export interface RscPayload {
38
41
  themeConfig?: ResolvedThemeConfig | null;
39
42
  /** Initial theme from cookie (for SSR hydration) */
40
43
  initialTheme?: Theme;
44
+ /** URL prefix for all routes (from createRouter({ basename })). */
45
+ basename?: string;
41
46
  /** Whether connection warmup is enabled */
42
47
  warmupEnabled?: boolean;
43
48
  /** Server-side redirect with optional state (for partial requests) */
@@ -65,10 +70,7 @@ export interface RSCDependencies {
65
70
  payload: T,
66
71
  options?: {
67
72
  temporaryReferences?: unknown;
68
- debugChannel?: {
69
- readable?: ReadableStream;
70
- writable?: WritableStream;
71
- };
73
+ onError?: (error: unknown) => string | void;
72
74
  },
73
75
  ) => ReadableStream<Uint8Array>;
74
76
 
@@ -130,9 +132,6 @@ export interface SSRRenderOptions {
130
132
  * - `"allReady"` — await `stream.allReady` before returning.
131
133
  */
132
134
  streamMode?: import("../router/router-options.js").SSRStreamMode;
133
-
134
- /** @internal Dev-only: debug channel ID for React Performance Tracks */
135
- debugId?: string;
136
135
  }
137
136
 
138
137
  /**
@@ -191,8 +191,12 @@ export type EntryData =
191
191
  /** Original PrerenderHandlerDefinition (for build-time getParams access) */
192
192
  prerenderDef?: {
193
193
  getParams?: (ctx: any) => Promise<any[]> | any[];
194
- options?: { passthrough?: boolean };
194
+ options?: { concurrency?: number };
195
195
  };
196
+ /** Set when route is wrapped with Passthrough() — has a separate live handler */
197
+ isPassthrough?: true;
198
+ /** Live handler for runtime fallback (only set on Passthrough routes) */
199
+ liveHandler?: Handler<any, any, any>;
196
200
  /** Set when handler is a Static definition (build-time only) */
197
201
  isStaticPrerender?: true;
198
202
  /** Static handler $$id for build-time store lookup */
@@ -670,11 +674,44 @@ export function track(label: string, depth?: number): () => void {
670
674
  };
671
675
  }
672
676
 
677
+ /**
678
+ * Separate ALS for tracking loader execution scope.
679
+ * Uses a dedicated ALS (not RSCRouterContext) to avoid issues with
680
+ * nested RSCRouterContext.run() calls in Vite's module runner.
681
+ */
682
+ const LOADER_SCOPE_KEY = Symbol.for("rangojs-router:loader-scope");
683
+ const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
684
+ globalThis as any
685
+ )[LOADER_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
686
+
673
687
  /**
674
688
  * Check if the current execution is inside a cache() DSL boundary.
675
689
  * Returns false inside loader execution — loaders are always fresh
676
690
  * (never cached), so non-cacheable reads are safe.
677
691
  */
678
692
  export function isInsideCacheScope(): boolean {
679
- return RSCRouterContext.getStore()?.insideCacheScope === true;
693
+ if (RSCRouterContext.getStore()?.insideCacheScope !== true) return false;
694
+ // Loaders are always fresh — even inside a cache() boundary, the loader
695
+ // function re-executes on every request. Skip the guard when running
696
+ // inside a loader.
697
+ if (loaderScopeALS.getStore()?.active) return false;
698
+ return true;
699
+ }
700
+
701
+ /**
702
+ * Check if the current execution is inside a DSL loader scope
703
+ * (wrapped by runInsideLoaderScope). Used by rendered() barrier
704
+ * to distinguish DSL loaders from handler-invoked loaders.
705
+ */
706
+ export function isInsideLoaderScope(): boolean {
707
+ return loaderScopeALS.getStore()?.active === true;
708
+ }
709
+
710
+ /**
711
+ * Run `fn` inside a loader scope. While active, cache-scope guards
712
+ * are bypassed because loaders are always fresh (never cached) and
713
+ * their side effects (setCookie, header, etc.) are safe.
714
+ */
715
+ export function runInsideLoaderScope<T>(fn: () => T): T {
716
+ return loaderScopeALS.run({ active: true }, fn);
680
717
  }
@@ -13,6 +13,25 @@
13
13
  */
14
14
  export type HandleData = Record<string, Record<string, unknown[]>>;
15
15
 
16
+ /**
17
+ * Build a HandleData snapshot from a HandleStore using segment ordering.
18
+ * Reads data directly from the store for each segment in order.
19
+ */
20
+ export function buildHandleSnapshot(
21
+ handleStore: HandleStore,
22
+ segmentOrder: string[],
23
+ ): HandleData {
24
+ const data: HandleData = {};
25
+ for (const segmentId of segmentOrder) {
26
+ const segData = handleStore.getDataForSegment(segmentId);
27
+ for (const handleName in segData) {
28
+ if (!data[handleName]) data[handleName] = {};
29
+ data[handleName][segmentId] = segData[handleName];
30
+ }
31
+ }
32
+ return data;
33
+ }
34
+
16
35
  function createLateHandlePushError(
17
36
  handleName: string,
18
37
  segmentId: string,
@@ -44,20 +44,21 @@ export function setLoaderImports(
44
44
  export async function getLoaderLazy(
45
45
  id: string,
46
46
  ): Promise<LoaderRegistryEntry | undefined> {
47
- // Check if already cached in main registry
48
- const existing = loaderRegistry.get(id);
49
- if (existing) {
50
- return existing;
51
- }
52
-
53
- // Check the fetchable loader registry (populated by createLoader)
47
+ // Always check fetchableLoaderRegistry first it's the source of truth.
48
+ // createLoader() updates it during module re-evaluation (HMR), so checking
49
+ // here ensures we pick up the fresh function after a loader file change.
54
50
  const fetchable = getFetchableLoader(id);
55
51
  if (fetchable) {
56
- // Cache in main registry for future requests
57
52
  loaderRegistry.set(id, fetchable);
58
53
  return fetchable;
59
54
  }
60
55
 
56
+ // Fall back to local cache (populated by previous lazy imports in production)
57
+ const existing = loaderRegistry.get(id);
58
+ if (existing) {
59
+ return existing;
60
+ }
61
+
61
62
  // Try to lazy load from the import map (production mode)
62
63
  if (lazyLoaderImports && lazyLoaderImports.size > 0) {
63
64
  const lazyImport = lazyLoaderImports.get(id);
@@ -26,7 +26,12 @@ import {
26
26
  contextSet,
27
27
  isNonCacheable,
28
28
  } from "../context-var.js";
29
- import { createHandleStore, type HandleStore } from "./handle-store.js";
29
+ import {
30
+ createHandleStore,
31
+ buildHandleSnapshot,
32
+ type HandleStore,
33
+ type HandleData,
34
+ } from "./handle-store.js";
30
35
  import { isHandle } from "../handle.js";
31
36
  import { track, type MetricsStore } from "./context.js";
32
37
  import { getFetchableLoader } from "./fetchable-loader-store.js";
@@ -69,8 +74,8 @@ export interface RequestContext<
69
74
  pathname: string;
70
75
  /** URL search params (with internal `_rsc*` params stripped, same as `url.searchParams`) */
71
76
  searchParams: URLSearchParams;
72
- /** Variables set by middleware (same as ctx.var) */
73
- var: Record<string, any>;
77
+ /** @internal Shared variable backing store for ctx.get()/ctx.set(). */
78
+ _variables: Record<string, any>;
74
79
  /** Get a variable set by middleware */
75
80
  get: {
76
81
  <T>(contextVar: ContextVar<T>): T | undefined;
@@ -271,6 +276,54 @@ export interface RequestContext<
271
276
  /** @internal Previous route key (from the navigation source), used for revalidation */
272
277
  _prevRouteKey?: string;
273
278
 
279
+ /**
280
+ * @internal Render barrier for experimental `rendered()` API.
281
+ * Resolves when all non-loader segments have settled and handle data
282
+ * is available. Used by DSL loaders that call `ctx.rendered()`.
283
+ */
284
+ _renderBarrier: Promise<void>;
285
+
286
+ /**
287
+ * @internal Resolve the render barrier. Accepts resolved segments, filters
288
+ * out loaders, and captures non-loader segment IDs as the handle ordering.
289
+ * Called after segment resolution (fresh) or handle replay (cache/prerender).
290
+ */
291
+ _resolveRenderBarrier: (
292
+ segments: Array<{ type: string; id: string }>,
293
+ ) => void;
294
+
295
+ /**
296
+ * @internal Segment order at barrier resolution time, used by loader
297
+ * ctx.use(handle) to collect handle data in correct order.
298
+ */
299
+ _renderBarrierSegmentOrder?: string[];
300
+
301
+ /**
302
+ * @internal Set to true when the matched entry tree contains any `loading()`
303
+ * entries (streaming). Used by rendered() to fail fast.
304
+ */
305
+ _treeHasStreaming?: boolean;
306
+
307
+ /**
308
+ * @internal Loader IDs that have called rendered() and are waiting for the
309
+ * barrier. Used to detect deadlocks when a handler tries to await the same
310
+ * loader via ctx.use(Loader).
311
+ */
312
+ _renderBarrierWaiters?: Set<string>;
313
+
314
+ /**
315
+ * @internal Loader IDs that handlers have started awaiting via ctx.use().
316
+ * Used for bidirectional deadlock detection: if a loader later calls
317
+ * rendered() and a handler already awaits it, we can detect the deadlock.
318
+ */
319
+ _handlerLoaderDeps?: Set<string>;
320
+
321
+ /**
322
+ * @internal Cached HandleData snapshot built at barrier resolution time.
323
+ * Avoids rebuilding the snapshot on every loader ctx.use(handle) call.
324
+ */
325
+ _renderBarrierHandleSnapshot?: HandleData;
326
+
274
327
  /** @internal Per-request error dedup set for onError reporting */
275
328
  _reportedErrors: WeakSet<object>;
276
329
 
@@ -288,14 +341,14 @@ export interface RequestContext<
288
341
  /** @internal Request-scoped performance metrics store */
289
342
  _metricsStore?: MetricsStore;
290
343
 
291
- /** @internal Dev-only: debug channel for React Performance Tracks */
292
- _debugChannel?: {
293
- readable: ReadableStream;
294
- writable: WritableStream;
295
- };
344
+ /** @internal Router basename for this request (used by redirect()) */
345
+ _basename?: string;
296
346
 
297
- /** @internal Dev-only: debug channel ID for React Performance Tracks */
298
- _debugId?: string;
347
+ /**
348
+ * @internal RouteSnapshot from classifyRequest, reused by match/matchPartial
349
+ * to avoid a second resolveRoute call. Cleared on HMR invalidation.
350
+ */
351
+ _classifiedRoute?: import("../router/route-snapshot.js").RouteSnapshot;
299
352
  }
300
353
 
301
354
  /**
@@ -322,12 +375,20 @@ export type PublicRequestContext<
322
375
  | "_routeName"
323
376
  | "_prevRouteKey"
324
377
  | "_reportedErrors"
378
+ | "_renderBarrier"
379
+ | "_resolveRenderBarrier"
380
+ | "_renderBarrierSegmentOrder"
381
+ | "_treeHasStreaming"
382
+ | "_renderBarrierWaiters"
383
+ | "_handlerLoaderDeps"
384
+ | "_renderBarrierHandleSnapshot"
325
385
  | "_reportBackgroundError"
326
386
  | "_debugPerformance"
327
387
  | "_metricsStore"
328
- | "_debugChannel"
329
- | "_debugId"
388
+ | "_basename"
330
389
  | "_setStatus"
390
+ | "_variables"
391
+ | "_classifiedRoute"
331
392
  | "res"
332
393
  >;
333
394
 
@@ -602,7 +663,7 @@ export function createRequestContext<TEnv>(
602
663
  originalUrl: new URL(request.url),
603
664
  pathname: url.pathname,
604
665
  searchParams: cleanUrl.searchParams,
605
- var: variables,
666
+ _variables: variables,
606
667
  get: ((keyOrVar: any) => {
607
668
  if (isNonCacheable(variables, keyOrVar) && isInsideCacheScope()) {
608
669
  throw new Error(
@@ -749,9 +810,58 @@ export function createRequestContext<TEnv>(
749
810
  _reportedErrors: new WeakSet<object>(),
750
811
  _metricsStore: undefined,
751
812
 
813
+ // Render barrier: deferred promise resolved after non-loader segments settle.
814
+ _renderBarrier: null as any, // set below
815
+ _resolveRenderBarrier: null as any, // set below
816
+ _renderBarrierSegmentOrder: undefined,
817
+
752
818
  reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
753
819
  };
754
820
 
821
+ // Lazy render barrier: only allocate the Promise when a loader actually
822
+ // calls rendered(). Requests that don't use rendered() pay zero cost.
823
+ let barrierResolved = false;
824
+ let resolveBarrier: (() => void) | undefined;
825
+ ctx._renderBarrier = null as any; // lazy — created on first access
826
+ ctx._resolveRenderBarrier = (
827
+ segments: Array<{ type: string; id: string }>,
828
+ ) => {
829
+ if (barrierResolved) return;
830
+ barrierResolved = true;
831
+ const segOrder = segments
832
+ .filter((s) => s.type !== "loader")
833
+ .map((s) => s.id);
834
+ ctx._renderBarrierSegmentOrder = segOrder;
835
+ // Build and cache handle snapshot so loader ctx.use(handle) calls
836
+ // don't rebuild it on every invocation.
837
+ ctx._renderBarrierHandleSnapshot = buildHandleSnapshot(
838
+ handleStore,
839
+ segOrder,
840
+ );
841
+ ctx._renderBarrierWaiters = undefined;
842
+ ctx._handlerLoaderDeps = undefined;
843
+ if (resolveBarrier) resolveBarrier();
844
+ };
845
+ Object.defineProperty(ctx, "_renderBarrier", {
846
+ get() {
847
+ // Barrier already resolved (cache/prerender hit) or first lazy access.
848
+ // Either way, replace the getter with a concrete value to avoid
849
+ // repeated Promise.resolve() allocations on subsequent reads.
850
+ const p = barrierResolved
851
+ ? Promise.resolve()
852
+ : new Promise<void>((resolve) => {
853
+ resolveBarrier = resolve;
854
+ });
855
+ Object.defineProperty(ctx, "_renderBarrier", {
856
+ value: p,
857
+ writable: false,
858
+ configurable: false,
859
+ });
860
+ return p;
861
+ },
862
+ configurable: true,
863
+ });
864
+
755
865
  // Now create use() with access to ctx
756
866
  ctx.use = createUseFunction({
757
867
  handleStore,
@@ -934,14 +1044,13 @@ export function createUseFunction<TEnv>(
934
1044
  pathname: ctx.pathname,
935
1045
  url: ctx.url,
936
1046
  env: ctx.env as any,
937
- var: ctx.var as any,
938
1047
  get: ctx.get as any,
939
- use: <TDep, TDepParams = any>(
1048
+ use: (<TDep, TDepParams = any>(
940
1049
  dep: LoaderDefinition<TDep, TDepParams>,
941
1050
  ): Promise<TDep> => {
942
1051
  // Recursive call - will start dep loader if not already started
943
1052
  return ctx.use(dep);
944
- },
1053
+ }) as LoaderContext["use"],
945
1054
  method: "GET",
946
1055
  body: undefined,
947
1056
  reverse: createReverseFunction(
@@ -950,6 +1059,12 @@ export function createUseFunction<TEnv>(
950
1059
  ctx.params as Record<string, string>,
951
1060
  ctx._routeName ? isRouteRootScoped(ctx._routeName) : undefined,
952
1061
  ),
1062
+ rendered: () => {
1063
+ throw new Error(
1064
+ `ctx.rendered() is only available in DSL loaders (registered via loader() in urls()). ` +
1065
+ `It cannot be used from request-context loaders or server actions.`,
1066
+ );
1067
+ },
953
1068
  };
954
1069
 
955
1070
  const doneLoader = track(`loader:${loader.$$id}`, 2);
package/src/ssr/index.tsx CHANGED
@@ -64,9 +64,6 @@ export interface SSRRenderOptions {
64
64
  * - `"allReady"` — await `stream.allReady` before returning.
65
65
  */
66
66
  streamMode?: import("../router/router-options.js").SSRStreamMode;
67
-
68
- /** @internal Dev-only: debug channel ID for React Performance Tracks */
69
- debugId?: string;
70
67
  }
71
68
 
72
69
  /**
@@ -132,6 +129,7 @@ interface RscPayload {
132
129
  matched?: string[];
133
130
  pathname?: string;
134
131
  params?: Record<string, string>;
132
+ basename?: string;
135
133
  themeConfig?: ResolvedThemeConfig | null;
136
134
  initialTheme?: Theme;
137
135
  version?: string;
@@ -264,6 +262,7 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
264
262
  function SsrRoot() {
265
263
  payload ??= createFromReadableStream<RscPayload>(rscStream1);
266
264
  const resolved = React.use(payload);
265
+
267
266
  const themeConfig = resolved.metadata?.themeConfig ?? null;
268
267
  const pathname = resolved.metadata?.pathname ?? "/";
269
268
 
@@ -289,6 +288,7 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
289
288
  navigate: async () => {},
290
289
  refresh: async () => {},
291
290
  version: resolved.metadata?.version,
291
+ basename: resolved.metadata?.basename,
292
292
  };
293
293
 
294
294
  // Build content tree from segments.
@@ -332,16 +332,7 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
332
332
  }
333
333
 
334
334
  // Get bootstrap script content
335
- let bootstrapScriptContent = await loadBootstrapScriptContent();
336
-
337
- // Dev-only: inject debugId for React Performance Tracks.
338
- // The client reads this during hydration to create the matching WS channel.
339
- const debugId = options?.debugId;
340
- if (debugId) {
341
- bootstrapScriptContent =
342
- `globalThis.__RANGO_DEBUG_ID__=${JSON.stringify(debugId)};` +
343
- bootstrapScriptContent;
344
- }
335
+ const bootstrapScriptContent = await loadBootstrapScriptContent();
345
336
 
346
337
  // Render React tree to HTML stream
347
338
  // Pass formState for useActionState progressive enhancement if provided