@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
@@ -8,8 +8,14 @@ import type { HandlerContext, InternalHandlerContext } from "../types";
8
8
  import { _getRequestContext } from "../server/request-context.js";
9
9
  import { getSearchSchema, isRouteRootScoped } from "../route-map-builder.js";
10
10
  import { parseSearchParams, serializeSearchParams } from "../search-params.js";
11
- import { contextGet, contextSet } from "../context-var.js";
12
- import { NOCACHE_SYMBOL } from "../cache/taint.js";
11
+ import {
12
+ contextGet,
13
+ contextSet,
14
+ isNonCacheable,
15
+ type ContextSetOptions,
16
+ } from "../context-var.js";
17
+ import { isInsideCacheScope } from "../server/context.js";
18
+ import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
13
19
  import { isAutoGeneratedRouteName } from "../route-name.js";
14
20
  import { PRERENDER_PASSTHROUGH } from "../prerender.js";
15
21
 
@@ -108,9 +114,9 @@ function createPrerenderPassthroughFn(
108
114
  }
109
115
  if (!isPassthroughRoute) {
110
116
  throw new Error(
111
- "ctx.passthrough() is only available on routes declared with " +
112
- "{ passthrough: true }. Remove the passthrough() call or add " +
113
- "{ passthrough: true } to the Prerender options.",
117
+ "ctx.passthrough() is only available on routes wrapped with " +
118
+ "Passthrough(). Remove the passthrough() call or wrap the " +
119
+ "Prerender definition with Passthrough(prerenderDef, liveHandler).",
114
120
  );
115
121
  }
116
122
  return PRERENDER_PASSTHROUGH;
@@ -160,9 +166,24 @@ export function createReverseFunction(
160
166
  : hrefParams;
161
167
 
162
168
  // Substitute params (strip constraint and optional syntax: :param(a|b)? -> value)
169
+ // Optional params (:param?) are omitted when not provided
163
170
  if (effectiveParams) {
171
+ let hadOmittedOptional = false;
172
+ // First pass: optional params (trailing ?)
164
173
  result = result.replace(
165
- /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?\??/g,
174
+ /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(\?)/g,
175
+ (_, key) => {
176
+ const value = effectiveParams[key];
177
+ if (value === undefined) {
178
+ hadOmittedOptional = true;
179
+ return "";
180
+ }
181
+ return encodeURIComponent(value);
182
+ },
183
+ );
184
+ // Second pass: required params (no trailing ?)
185
+ result = result.replace(
186
+ /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(?!\?)/g,
166
187
  (_, key) => {
167
188
  const value = effectiveParams[key];
168
189
  if (value === undefined) {
@@ -171,6 +192,13 @@ export function createReverseFunction(
171
192
  return encodeURIComponent(value);
172
193
  },
173
194
  );
195
+ // Clean up slashes only when an optional param was actually omitted,
196
+ // so intentional trailing-slash patterns like "/blog/" are preserved.
197
+ if (hadOmittedOptional) {
198
+ const hadTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
199
+ result = result.replace(/\/\/+/g, "/").replace(/\/+$/, "") || "/";
200
+ if (hadTrailingSlash && !result.endsWith("/")) result += "/";
201
+ }
174
202
  }
175
203
 
176
204
  // Append search params as query string
@@ -201,7 +229,7 @@ export function createHandlerContext<TEnv>(
201
229
  // Get variables from request context - this is the unified context
202
230
  // shared between middleware and route handlers
203
231
  const requestContext = _getRequestContext();
204
- const variables: any = requestContext?.var ?? {};
232
+ const variables: any = requestContext?._variables ?? {};
205
233
 
206
234
  // If route has a search schema, parse URLSearchParams into typed object
207
235
  const searchSchema = routeName ? getSearchSchema(routeName) : undefined;
@@ -213,25 +241,66 @@ export function createHandlerContext<TEnv>(
213
241
  const stubResponse =
214
242
  requestContext?.res ?? new Response(null, { status: 200 });
215
243
 
216
- const ctx: InternalHandlerContext<any, TEnv> = {
244
+ // Guard mutating Headers methods so they throw inside "use cache" or cache() scope.
245
+ // Uses lazy `ctx` reference (assigned below) — only the specific handler ctx
246
+ // is stamped by cache-runtime, not the shared request context.
247
+ const MUTATING_HEADERS_METHODS = new Set(["set", "append", "delete"]);
248
+ let ctx: InternalHandlerContext<any, TEnv>;
249
+ const guardedHeaders = new Proxy(stubResponse.headers, {
250
+ get(target, prop, receiver) {
251
+ const value = Reflect.get(target, prop, receiver);
252
+ if (typeof value === "function") {
253
+ if (MUTATING_HEADERS_METHODS.has(prop as string)) {
254
+ return (...args: any[]) => {
255
+ assertNotInsideCacheExec(ctx, "headers");
256
+ if (isInsideCacheScope()) {
257
+ throw new Error(
258
+ `ctx.headers.${String(prop)}() cannot be called inside a cache() boundary. ` +
259
+ `On cache hit the handler is skipped, so this side effect would be lost. ` +
260
+ `Move header mutations to a middleware or layout outside the cache() scope.`,
261
+ );
262
+ }
263
+ return value.apply(target, args);
264
+ };
265
+ }
266
+ return value.bind(target);
267
+ }
268
+ return value;
269
+ },
270
+ });
271
+
272
+ ctx = {
217
273
  params,
218
274
  build: false,
275
+ dev: false,
219
276
  request,
220
277
  searchParams,
221
278
  search: searchSchema ? resolvedSearchParams : {},
222
279
  pathname,
223
280
  url,
281
+ originalUrl: new URL(request.url),
224
282
  env: bindings,
225
- var: variables,
226
- get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as HandlerContext<
227
- any,
228
- TEnv
229
- >["get"],
230
- set: ((keyOrVar: any, value: any) => {
231
- contextSet(variables, keyOrVar, value);
283
+ _variables: variables,
284
+ get: ((keyOrVar: any) => {
285
+ // Read-time guard: non-cacheable var inside cache() → throw.
286
+ // Works for both ContextVar tokens and string keys.
287
+ if (isNonCacheable(variables, keyOrVar) && isInsideCacheScope()) {
288
+ throw new Error(
289
+ `ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
290
+ `The variable was created with { cache: false } or set with { cache: false }, ` +
291
+ `and its value would be stale on cache hit. Move the read outside the cached scope.`,
292
+ );
293
+ }
294
+ return contextGet(variables, keyOrVar);
295
+ }) as HandlerContext<any, TEnv>["get"],
296
+ set: ((keyOrVar: any, value: any, options?: ContextSetOptions) => {
297
+ assertNotInsideCacheExec(ctx, "set");
298
+ // Write is dumb: store value + non-cacheable metadata.
299
+ // Enforcement happens at read time via ctx.get().
300
+ contextSet(variables, keyOrVar, value, options);
232
301
  }) as HandlerContext<any, TEnv>["set"],
233
302
  res: stubResponse, // Stub response for setting headers
234
- headers: stubResponse.headers, // Shorthand for res.headers
303
+ headers: guardedHeaders, // Guarded shorthand for res.headers
235
304
  // Placeholder use() - will be replaced with actual implementation during request
236
305
  use: () => {
237
306
  throw new Error("ctx.use() called before loaders were initialized");
@@ -274,7 +343,7 @@ export function createHandlerContext<TEnv>(
274
343
  *
275
344
  * Returns an InternalHandlerContext where params, pathname, url, searchParams,
276
345
  * search, reverse, and use(handle) work. Request-time properties
277
- * (request, env, headers, cookies, var, get, set, res) throw with a clear error.
346
+ * (request, env, headers, cookies, get, set, res) throw with a clear error.
278
347
  */
279
348
  export function createPrerenderContext<TEnv>(
280
349
  params: Record<string, string>,
@@ -283,6 +352,8 @@ export function createPrerenderContext<TEnv>(
283
352
  routeName?: string,
284
353
  buildVars?: Record<string, any>,
285
354
  isPassthroughRoute?: boolean,
355
+ buildEnv?: TEnv,
356
+ devMode?: boolean,
286
357
  ): InternalHandlerContext<any, TEnv> {
287
358
  const syntheticUrl = new URL(`http://prerender${pathname}`);
288
359
  const variables = buildVars ?? {};
@@ -297,6 +368,7 @@ export function createPrerenderContext<TEnv>(
297
368
  return {
298
369
  params,
299
370
  build: true,
371
+ dev: devMode ?? false,
300
372
  get request(): Request {
301
373
  return throwUnavailable("request");
302
374
  },
@@ -304,12 +376,15 @@ export function createPrerenderContext<TEnv>(
304
376
  search: {},
305
377
  pathname,
306
378
  url: syntheticUrl,
379
+ originalUrl: syntheticUrl,
307
380
  get env(): TEnv {
308
- return throwUnavailable("env");
309
- },
310
- get var(): any {
311
- return throwUnavailable("var");
381
+ if (buildEnv !== undefined) return buildEnv;
382
+ throw new Error(
383
+ "ctx.env is not available during pre-rendering. " +
384
+ "Configure buildEnv in your rango() plugin options to enable build-time env access.",
385
+ );
312
386
  },
387
+ _variables: variables,
313
388
  get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
314
389
  set: ((keyOrVar: any, value: any) => {
315
390
  contextSet(variables, keyOrVar, value);
@@ -355,6 +430,8 @@ export function createPrerenderContext<TEnv>(
355
430
  export function createStaticContext<TEnv>(
356
431
  routeMap: Record<string, string>,
357
432
  routeName?: string,
433
+ buildEnv?: TEnv,
434
+ devMode?: boolean,
358
435
  ): InternalHandlerContext<any, TEnv> {
359
436
  const variables: Record<string, any> = {};
360
437
 
@@ -370,6 +447,7 @@ export function createStaticContext<TEnv>(
370
447
  return throwUnavailable("params");
371
448
  },
372
449
  build: true,
450
+ dev: devMode ?? false,
373
451
  get request(): Request {
374
452
  return throwUnavailable("request");
375
453
  },
@@ -385,12 +463,17 @@ export function createStaticContext<TEnv>(
385
463
  get url(): URL {
386
464
  return throwUnavailable("url");
387
465
  },
388
- get env(): TEnv {
389
- return throwUnavailable("env");
466
+ get originalUrl(): URL {
467
+ return throwUnavailable("originalUrl");
390
468
  },
391
- get var(): any {
392
- return throwUnavailable("var");
469
+ get env(): TEnv {
470
+ if (buildEnv !== undefined) return buildEnv;
471
+ throw new Error(
472
+ "ctx.env is not available in Static() handlers. " +
473
+ "Configure buildEnv in your rango() plugin options to enable build-time env access.",
474
+ );
393
475
  },
476
+ _variables: variables,
394
477
  get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
395
478
  set: ((keyOrVar: any, value: any) => {
396
479
  contextSet(variables, keyOrVar, value);
@@ -11,7 +11,11 @@ import type {
11
11
  InterceptEntry,
12
12
  InterceptSelectorContext,
13
13
  } from "../server/context";
14
- import type { HandlerContext, ResolvedSegment } from "../types";
14
+ import type {
15
+ HandlerContext,
16
+ InternalHandlerContext,
17
+ ResolvedSegment,
18
+ } from "../types";
15
19
  import { evaluateRevalidation } from "./revalidation.js";
16
20
  import { getRequestContext } from "../server/request-context.js";
17
21
  import { executeInterceptMiddleware } from "./middleware.js";
@@ -20,6 +24,7 @@ import { getGlobalRouteMap } from "../route-map-builder.js";
20
24
  import { handleHandlerResult } from "./segment-resolution.js";
21
25
  import type { SegmentResolutionDeps } from "./types.js";
22
26
  import { debugLog } from "./logging.js";
27
+ import { runInsideLoaderScope } from "../server/context.js";
23
28
 
24
29
  /**
25
30
  * Check if an intercept's when conditions are satisfied.
@@ -133,7 +138,7 @@ export async function resolveInterceptEntry<TEnv>(
133
138
  context.request,
134
139
  context.env,
135
140
  params,
136
- context.var as Record<string, any>,
141
+ (context as InternalHandlerContext<any, TEnv>)._variables,
137
142
  requestCtx.res,
138
143
  createReverseFunction(getGlobalRouteMap()),
139
144
  );
@@ -188,6 +193,7 @@ export async function resolveInterceptEntry<TEnv>(
188
193
  context,
189
194
  actionContext,
190
195
  stale,
196
+ traceSource: "intercept-loader",
191
197
  });
192
198
 
193
199
  if (!shouldRevalidate) {
@@ -206,7 +212,7 @@ export async function resolveInterceptEntry<TEnv>(
206
212
  loaderIds.push(loader.$$id);
207
213
  loaderPromises.push(
208
214
  deps.wrapLoaderPromise(
209
- context.use(loader),
215
+ runInsideLoaderScope(() => context.use(loader)),
210
216
  parentEntry,
211
217
  segmentId,
212
218
  context.pathname,
@@ -355,6 +361,7 @@ export async function resolveInterceptLoadersOnly<TEnv>(
355
361
  context,
356
362
  actionContext,
357
363
  stale,
364
+ traceSource: "intercept-loader",
358
365
  });
359
366
 
360
367
  if (!shouldRevalidate) {
@@ -372,7 +379,7 @@ export async function resolveInterceptLoadersOnly<TEnv>(
372
379
  loaderIds.push(loader.$$id);
373
380
  loaderPromises.push(
374
381
  deps.wrapLoaderPromise(
375
- context.use(loader),
382
+ runInsideLoaderScope(() => context.use(loader)),
376
383
  parentEntry,
377
384
  segmentId,
378
385
  context.pathname,
@@ -4,6 +4,7 @@ import {
4
4
  EntryData,
5
5
  RSCRouterContext,
6
6
  runWithPrefixes,
7
+ getIsolatedLazyParent,
7
8
  } from "../server/context";
8
9
  import type { UrlPatterns } from "../urls.js";
9
10
  import type { AllUseItems, IncludeItem } from "../route-types.js";
@@ -14,6 +15,7 @@ export interface LazyEvalDeps<TEnv = any> {
14
15
  mergedRouteMap: Record<string, string>;
15
16
  nextMountIndex: () => number;
16
17
  getPrecomputedByPrefix: () => Map<string, Record<string, string>> | null;
18
+ routerId?: string;
17
19
  }
18
20
 
19
21
  // Detect lazy includes in handler result and create placeholder entries
@@ -137,7 +139,7 @@ export function evaluateLazyEntry<TEnv = any>(
137
139
  patternsByPrefix,
138
140
  trailingSlash: trailingSlashMap,
139
141
  namespace: "lazy",
140
- parent: (lazyContext?.parent as EntryData | null) ?? null,
142
+ parent: getIsolatedLazyParent(lazyContext?.parent as EntryData | null),
141
143
  counters: lazyCounters,
142
144
  cacheProfiles: (lazyContext as any)?.cacheProfiles,
143
145
  rootScoped: (lazyContext as any)?.rootScoped,
@@ -200,6 +202,7 @@ export function evaluateLazyEntry<TEnv = any>(
200
202
  trailingSlash: entry.trailingSlash,
201
203
  handler: (lazyInclude.patterns as UrlPatterns<TEnv>).handler,
202
204
  mountIndex: deps.nextMountIndex(),
205
+ routerId: deps.routerId,
203
206
  // Lazy evaluation fields
204
207
  lazy: true,
205
208
  lazyPatterns: lazyInclude.patterns,
@@ -7,6 +7,7 @@
7
7
  import type { ReactNode } from "react";
8
8
  import { track } from "../server/context";
9
9
  import type { EntryData } from "../server/context";
10
+ import { contextGet } from "../context-var.js";
10
11
  import type {
11
12
  ResolvedSegment,
12
13
  HandlerContext,
@@ -19,10 +20,11 @@ import type {
19
20
  ErrorInfo,
20
21
  } from "../types";
21
22
  import type { LoaderRevalidationResult, ActionContext } from "./types";
22
- import { isHandle, type Handle } from "../handle.js";
23
- import type { HandleStore } from "../server/handle-store.js";
23
+ import { isHandle, collectHandleData, type Handle } from "../handle.js";
24
+ import type { HandleStore, HandleData } from "../server/handle-store.js";
24
25
  import { getFetchableLoader } from "../server/fetchable-loader-store.js";
25
26
  import { _getRequestContext } from "../server/request-context.js";
27
+ import { isInsideLoaderScope } from "../server/context.js";
26
28
  import { debugLog } from "./logging.js";
27
29
 
28
30
  /**
@@ -241,6 +243,21 @@ function createLoaderExecutor<TEnv>(
241
243
  pendingLoaders.add(loader.$$id);
242
244
 
243
245
  const currentLoaderId = loader.$$id;
246
+ const variables = (ctx as InternalHandlerContext<any, TEnv>)._variables;
247
+
248
+ // Capture whether this loader is being started from a DSL loader scope
249
+ // (runInsideLoaderScope in fresh.ts). Handler-invoked loaders are NOT
250
+ // inside loader scope. This determines whether rendered() is allowed.
251
+ const isDslLoader = isInsideLoaderScope();
252
+
253
+ let renderedResolved = false;
254
+ let renderedPromise: Promise<void> | null = null;
255
+
256
+ // Loader functions are always fresh (never cached), so they get an
257
+ // unguarded get that bypasses non-cacheable read guards. This applies
258
+ // to ALL loaders — DSL and handler-called — because the loader
259
+ // function itself always re-executes. Also handles nested deps
260
+ // (loaderA → use(loaderB)) since all share this unguarded get.
244
261
  const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
245
262
  params: ctx.params,
246
263
  routeParams: (ctx.params ?? {}) as Record<string, string>,
@@ -250,19 +267,79 @@ function createLoaderExecutor<TEnv>(
250
267
  pathname: ctx.pathname,
251
268
  url: ctx.url,
252
269
  env: ctx.env,
253
- var: ctx.var,
254
- get: ctx.get,
255
- use: <TDep, TDepParams = any>(
256
- dep: LoaderDefinition<TDep, TDepParams>,
257
- ): Promise<TDep> => {
258
- return useLoader(dep, currentLoaderId);
259
- },
270
+ get: ((keyOrVar: any) =>
271
+ contextGet(variables, keyOrVar)) as typeof ctx.get,
272
+ use: ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
273
+ if (isHandle(item)) {
274
+ if (!renderedResolved) {
275
+ throw new Error(
276
+ `ctx.use(handle) in a loader requires "await ctx.rendered()" first. ` +
277
+ `Handle "${item.$$id}" cannot be read until the render tree has settled.`,
278
+ );
279
+ }
280
+ const reqCtx = reqCtxRef ?? _getRequestContext();
281
+ if (!reqCtx) {
282
+ throw new Error(
283
+ `ctx.use(handle) failed: request context not available.`,
284
+ );
285
+ }
286
+ const segmentOrder = reqCtx._renderBarrierSegmentOrder ?? [];
287
+ const snapshot = buildHandleSnapshot(
288
+ reqCtx._handleStore,
289
+ segmentOrder,
290
+ );
291
+ return collectHandleData(item, snapshot, segmentOrder);
292
+ }
293
+
294
+ // Loader case
295
+ return useLoader(item as LoaderDefinition<any, any>, currentLoaderId);
296
+ }) as LoaderContext["use"],
260
297
  method: "GET",
261
298
  body: undefined,
262
299
  reverse: ctx.reverse as LoaderContext["reverse"],
300
+ rendered: (): Promise<void> => {
301
+ // Guard: only DSL loaders may use rendered()
302
+ if (!isDslLoader) {
303
+ throw new Error(
304
+ `ctx.rendered() is only available in DSL loaders (registered via loader() in urls()). ` +
305
+ `Handler-invoked loaders (ctx.use(Loader) inside a handler) cannot use rendered().`,
306
+ );
307
+ }
308
+
309
+ // Guard: reject streaming trees
310
+ const reqCtx = reqCtxRef ?? _getRequestContext();
311
+ if (reqCtx?._treeHasStreaming) {
312
+ throw new Error(
313
+ `ctx.rendered() is not supported when the matched route tree uses loading(). ` +
314
+ `Streaming handlers may not have settled when rendered() resolves. ` +
315
+ `Remove loading() from the route tree or restructure to avoid rendered().`,
316
+ );
317
+ }
318
+
319
+ if (renderedPromise) return renderedPromise;
320
+
321
+ if (!reqCtx) {
322
+ throw new Error(
323
+ `ctx.rendered() failed: request context not available.`,
324
+ );
325
+ }
326
+
327
+ // Register this loader as waiting for the barrier so that
328
+ // setupLoaderAccess can detect deadlocks when a handler
329
+ // tries to await the same loader via ctx.use().
330
+ if (!reqCtx._renderBarrierWaiters) {
331
+ reqCtx._renderBarrierWaiters = new Set();
332
+ }
333
+ reqCtx._renderBarrierWaiters.add(currentLoaderId);
334
+
335
+ renderedPromise = reqCtx._renderBarrier.then(() => {
336
+ renderedResolved = true;
337
+ });
338
+ return renderedPromise;
339
+ },
263
340
  };
264
341
 
265
- const doneLoader = track(`loader:${loader.$$id}`);
342
+ const doneLoader = track(`loader:${loader.$$id}`, 2);
266
343
  const promise = Promise.resolve(
267
344
  loaderFn(loaderCtx as LoaderContext<any, TEnv>),
268
345
  ).finally(() => {
@@ -277,6 +354,25 @@ function createLoaderExecutor<TEnv>(
277
354
  return useLoader;
278
355
  }
279
356
 
357
+ /**
358
+ * Build a HandleData snapshot from the HandleStore using segment ordering.
359
+ * Reads data directly from the store for each segment in order.
360
+ */
361
+ function buildHandleSnapshot(
362
+ handleStore: HandleStore,
363
+ segmentOrder: string[],
364
+ ): HandleData {
365
+ const data: HandleData = {};
366
+ for (const segmentId of segmentOrder) {
367
+ const segData = handleStore.getDataForSegment(segmentId);
368
+ for (const handleName in segData) {
369
+ if (!data[handleName]) data[handleName] = {};
370
+ data[handleName][segmentId] = segData[handleName];
371
+ }
372
+ }
373
+ return data;
374
+ }
375
+
280
376
  /**
281
377
  * Set up the use() method on handler context to access loaders and handles.
282
378
  *
@@ -327,7 +423,23 @@ export function setupLoaderAccess<TEnv>(
327
423
  };
328
424
  }
329
425
 
330
- return useLoader(item as LoaderDefinition<any, any>, null);
426
+ // Deadlock guard: if a HANDLER awaits a loader that called rendered(),
427
+ // the handler blocks segment resolution which blocks the barrier.
428
+ // Skip this check when inside a DSL loader scope (resolveLoaderData
429
+ // also calls ctx.use() but that's DSL-to-DSL, not handler-to-loader).
430
+ const loader = item as LoaderDefinition<any, any>;
431
+ if (loaderPromises.has(loader.$$id) && !isInsideLoaderScope()) {
432
+ const reqCtx = _getRequestContext();
433
+ if (reqCtx?._renderBarrierWaiters?.has(loader.$$id)) {
434
+ throw new Error(
435
+ `Deadlock: handler is awaiting loader "${loader.$$id}" which called ctx.rendered(). ` +
436
+ `The loader is waiting for segment resolution, but the handler blocks resolution. ` +
437
+ `Move the data dependency to a loader-to-loader pattern instead.`,
438
+ );
439
+ }
440
+ }
441
+
442
+ return useLoader(loader, null);
331
443
  }) as typeof ctx.use;
332
444
  }
333
445
 
@@ -12,7 +12,10 @@ export interface RevalidationTraceEntry {
12
12
  | "cache-hit"
13
13
  | "loader"
14
14
  | "parallel"
15
- | "orphan-layout";
15
+ | "orphan-layout"
16
+ | "route-handler"
17
+ | "layout-handler"
18
+ | "intercept-loader";
16
19
  defaultShouldRevalidate: boolean;
17
20
  finalShouldRevalidate: boolean;
18
21
  reason: string;
@@ -71,7 +74,7 @@ function getHeaderRequestId(request: Request): string | null {
71
74
  return trimmed.length > 0 ? trimmed : null;
72
75
  }
73
76
 
74
- function getOrCreateRequestId(request: Request): string {
77
+ export function getOrCreateRequestId(request: Request): string {
75
78
  const existing = requestIds.get(request);
76
79
  if (existing) return existing;
77
80
 
@@ -9,6 +9,7 @@ import { createRouteHelpers } from "../route-definition";
9
9
  import {
10
10
  getContext,
11
11
  runWithPrefixes,
12
+ getIsolatedLazyParent,
12
13
  type EntryData,
13
14
  type MetricsStore,
14
15
  } from "../server/context";
@@ -65,7 +66,9 @@ export async function loadManifest(
65
66
  const mountIndex = entry.mountIndex;
66
67
 
67
68
  // Check module-level cache (persists across requests within same isolate)
68
- const cacheKey = `${VERSION}:${mountIndex ?? ""}:${routeKey}:${isSSR ? 1 : 0}`;
69
+ // Include routerId so multi-router setups (host routing) don't share cached
70
+ // EntryData across routers with overlapping mountIndex + routeKey combinations.
71
+ const cacheKey = `${VERSION}:${entry.routerId ?? ""}:${mountIndex ?? ""}:${routeKey}:${isSSR ? 1 : 0}`;
69
72
  const cached = manifestModuleCache.get(cacheKey);
70
73
  if (cached) {
71
74
  const cacheStart = performance.now();
@@ -112,8 +115,11 @@ export async function loadManifest(
112
115
  // This ensures routes are registered under the correct layout hierarchy
113
116
  const lazyContext =
114
117
  entry.lazy && entry.lazyPatterns ? entry.lazyContext : null;
115
- const parentForContext =
116
- (lazyContext?.parent as EntryData | null) ?? Store.parent;
118
+ const parentForContext = lazyContext
119
+ ? getIsolatedLazyParent(
120
+ (lazyContext.parent as EntryData | null) ?? Store.parent,
121
+ )
122
+ : Store.parent;
117
123
 
118
124
  // For lazy entries, merge captured counters from include() so the
119
125
  // handler's entries get shortCode indices after sibling entries that