@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26

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 (225) hide show
  1. package/README.md +294 -28
  2. package/dist/bin/rango.js +355 -47
  3. package/dist/vite/index.js +1658 -1239
  4. package/package.json +3 -3
  5. package/skills/cache-guide/SKILL.md +9 -5
  6. package/skills/caching/SKILL.md +4 -4
  7. package/skills/document-cache/SKILL.md +2 -2
  8. package/skills/hooks/SKILL.md +40 -29
  9. package/skills/host-router/SKILL.md +218 -0
  10. package/skills/intercept/SKILL.md +79 -0
  11. package/skills/layout/SKILL.md +62 -2
  12. package/skills/loader/SKILL.md +229 -15
  13. package/skills/middleware/SKILL.md +109 -30
  14. package/skills/parallel/SKILL.md +57 -2
  15. package/skills/prerender/SKILL.md +189 -19
  16. package/skills/rango/SKILL.md +1 -2
  17. package/skills/response-routes/SKILL.md +3 -3
  18. package/skills/route/SKILL.md +44 -3
  19. package/skills/router-setup/SKILL.md +80 -3
  20. package/skills/theme/SKILL.md +5 -4
  21. package/skills/typesafety/SKILL.md +59 -16
  22. package/skills/use-cache/SKILL.md +16 -2
  23. package/src/__internal.ts +1 -1
  24. package/src/bin/rango.ts +56 -19
  25. package/src/browser/action-coordinator.ts +97 -0
  26. package/src/browser/event-controller.ts +29 -48
  27. package/src/browser/history-state.ts +80 -0
  28. package/src/browser/intercept-utils.ts +1 -1
  29. package/src/browser/link-interceptor.ts +19 -3
  30. package/src/browser/merge-segment-loaders.ts +9 -2
  31. package/src/browser/navigation-bridge.ts +66 -443
  32. package/src/browser/navigation-client.ts +34 -62
  33. package/src/browser/navigation-store.ts +4 -33
  34. package/src/browser/navigation-transaction.ts +295 -0
  35. package/src/browser/partial-update.ts +103 -151
  36. package/src/browser/prefetch/cache.ts +67 -0
  37. package/src/browser/prefetch/fetch.ts +137 -0
  38. package/src/browser/prefetch/observer.ts +65 -0
  39. package/src/browser/prefetch/policy.ts +42 -0
  40. package/src/browser/prefetch/queue.ts +88 -0
  41. package/src/browser/rango-state.ts +112 -0
  42. package/src/browser/react/Link.tsx +154 -44
  43. package/src/browser/react/NavigationProvider.tsx +32 -0
  44. package/src/browser/react/context.ts +6 -0
  45. package/src/browser/react/filter-segment-order.ts +11 -0
  46. package/src/browser/react/index.ts +2 -6
  47. package/src/browser/react/location-state-shared.ts +29 -11
  48. package/src/browser/react/location-state.ts +6 -4
  49. package/src/browser/react/nonce-context.ts +23 -0
  50. package/src/browser/react/shallow-equal.ts +27 -0
  51. package/src/browser/react/use-action.ts +23 -45
  52. package/src/browser/react/use-client-cache.ts +5 -3
  53. package/src/browser/react/use-handle.ts +21 -64
  54. package/src/browser/react/use-navigation.ts +7 -32
  55. package/src/browser/react/use-params.ts +5 -34
  56. package/src/browser/react/use-pathname.ts +2 -3
  57. package/src/browser/react/use-router.ts +3 -6
  58. package/src/browser/react/use-search-params.ts +2 -1
  59. package/src/browser/react/use-segments.ts +75 -114
  60. package/src/browser/response-adapter.ts +73 -0
  61. package/src/browser/rsc-router.tsx +46 -22
  62. package/src/browser/scroll-restoration.ts +10 -7
  63. package/src/browser/server-action-bridge.ts +458 -405
  64. package/src/browser/types.ts +21 -35
  65. package/src/browser/validate-redirect-origin.ts +29 -0
  66. package/src/build/generate-manifest.ts +38 -13
  67. package/src/build/generate-route-types.ts +4 -0
  68. package/src/build/index.ts +1 -0
  69. package/src/build/route-trie.ts +19 -3
  70. package/src/build/route-types/codegen.ts +13 -4
  71. package/src/build/route-types/include-resolution.ts +13 -0
  72. package/src/build/route-types/per-module-writer.ts +15 -3
  73. package/src/build/route-types/router-processing.ts +170 -18
  74. package/src/build/runtime-discovery.ts +13 -1
  75. package/src/cache/background-task.ts +34 -0
  76. package/src/cache/cache-key-utils.ts +44 -0
  77. package/src/cache/cache-policy.ts +125 -0
  78. package/src/cache/cache-runtime.ts +136 -123
  79. package/src/cache/cache-scope.ts +76 -83
  80. package/src/cache/cf/cf-cache-store.ts +12 -7
  81. package/src/cache/document-cache.ts +93 -69
  82. package/src/cache/handle-capture.ts +81 -0
  83. package/src/cache/index.ts +0 -15
  84. package/src/cache/memory-segment-store.ts +43 -69
  85. package/src/cache/profile-registry.ts +43 -8
  86. package/src/cache/read-through-swr.ts +134 -0
  87. package/src/cache/segment-codec.ts +140 -117
  88. package/src/cache/taint.ts +30 -3
  89. package/src/cache/types.ts +1 -115
  90. package/src/client.rsc.tsx +0 -1
  91. package/src/client.tsx +53 -76
  92. package/src/errors.ts +6 -1
  93. package/src/handle.ts +1 -1
  94. package/src/handles/MetaTags.tsx +5 -2
  95. package/src/host/cookie-handler.ts +8 -3
  96. package/src/host/index.ts +0 -3
  97. package/src/host/router.ts +14 -1
  98. package/src/href-client.ts +3 -1
  99. package/src/index.rsc.ts +53 -10
  100. package/src/index.ts +73 -43
  101. package/src/loader.rsc.ts +12 -4
  102. package/src/loader.ts +8 -0
  103. package/src/prerender/store.ts +60 -18
  104. package/src/prerender.ts +76 -18
  105. package/src/reverse.ts +11 -7
  106. package/src/root-error-boundary.tsx +30 -26
  107. package/src/route-definition/dsl-helpers.ts +9 -6
  108. package/src/route-definition/index.ts +0 -3
  109. package/src/route-definition/redirect.ts +15 -3
  110. package/src/route-map-builder.ts +38 -2
  111. package/src/route-name.ts +53 -0
  112. package/src/route-types.ts +7 -0
  113. package/src/router/content-negotiation.ts +1 -1
  114. package/src/router/debug-manifest.ts +16 -3
  115. package/src/router/handler-context.ts +96 -17
  116. package/src/router/intercept-resolution.ts +6 -4
  117. package/src/router/lazy-includes.ts +4 -0
  118. package/src/router/loader-resolution.ts +6 -11
  119. package/src/router/logging.ts +100 -3
  120. package/src/router/manifest.ts +32 -3
  121. package/src/router/match-api.ts +62 -54
  122. package/src/router/match-context.ts +3 -0
  123. package/src/router/match-handlers.ts +185 -11
  124. package/src/router/match-middleware/background-revalidation.ts +65 -85
  125. package/src/router/match-middleware/cache-lookup.ts +78 -10
  126. package/src/router/match-middleware/cache-store.ts +2 -0
  127. package/src/router/match-pipelines.ts +8 -43
  128. package/src/router/match-result.ts +0 -9
  129. package/src/router/metrics.ts +233 -13
  130. package/src/router/middleware-types.ts +34 -39
  131. package/src/router/middleware.ts +290 -130
  132. package/src/router/pattern-matching.ts +61 -10
  133. package/src/router/prerender-match.ts +36 -6
  134. package/src/router/preview-match.ts +7 -1
  135. package/src/router/revalidation.ts +61 -2
  136. package/src/router/router-context.ts +15 -0
  137. package/src/router/router-interfaces.ts +158 -40
  138. package/src/router/router-options.ts +223 -1
  139. package/src/router/router-registry.ts +5 -2
  140. package/src/router/segment-resolution/fresh.ts +165 -242
  141. package/src/router/segment-resolution/helpers.ts +263 -0
  142. package/src/router/segment-resolution/loader-cache.ts +102 -98
  143. package/src/router/segment-resolution/revalidation.ts +394 -272
  144. package/src/router/segment-resolution/static-store.ts +2 -2
  145. package/src/router/segment-resolution.ts +1 -3
  146. package/src/router/segment-wrappers.ts +3 -0
  147. package/src/router/telemetry-otel.ts +299 -0
  148. package/src/router/telemetry.ts +300 -0
  149. package/src/router/timeout.ts +148 -0
  150. package/src/router/trie-matching.ts +20 -2
  151. package/src/router/types.ts +7 -1
  152. package/src/router.ts +203 -18
  153. package/src/rsc/handler-context.ts +13 -2
  154. package/src/rsc/handler.ts +489 -438
  155. package/src/rsc/helpers.ts +125 -5
  156. package/src/rsc/index.ts +0 -20
  157. package/src/rsc/loader-fetch.ts +84 -42
  158. package/src/rsc/manifest-init.ts +3 -2
  159. package/src/rsc/origin-guard.ts +141 -0
  160. package/src/rsc/progressive-enhancement.ts +245 -19
  161. package/src/rsc/response-route-handler.ts +347 -0
  162. package/src/rsc/rsc-rendering.ts +47 -43
  163. package/src/rsc/runtime-warnings.ts +42 -0
  164. package/src/rsc/server-action.ts +166 -66
  165. package/src/rsc/ssr-setup.ts +128 -0
  166. package/src/rsc/types.ts +20 -2
  167. package/src/search-params.ts +38 -23
  168. package/src/server/context.ts +61 -7
  169. package/src/server/cookie-store.ts +190 -0
  170. package/src/server/fetchable-loader-store.ts +11 -6
  171. package/src/server/handle-store.ts +84 -12
  172. package/src/server/loader-registry.ts +11 -46
  173. package/src/server/request-context.ts +275 -49
  174. package/src/server.ts +6 -0
  175. package/src/ssr/index.tsx +67 -28
  176. package/src/static-handler.ts +7 -0
  177. package/src/theme/ThemeProvider.tsx +6 -1
  178. package/src/theme/index.ts +4 -18
  179. package/src/theme/theme-context.ts +1 -28
  180. package/src/theme/theme-script.ts +2 -1
  181. package/src/types/cache-types.ts +6 -1
  182. package/src/types/error-types.ts +3 -0
  183. package/src/types/global-namespace.ts +22 -0
  184. package/src/types/handler-context.ts +103 -16
  185. package/src/types/index.ts +1 -1
  186. package/src/types/loader-types.ts +9 -6
  187. package/src/types/route-config.ts +17 -26
  188. package/src/types/route-entry.ts +28 -0
  189. package/src/types/segments.ts +0 -5
  190. package/src/urls/include-helper.ts +49 -8
  191. package/src/urls/index.ts +1 -0
  192. package/src/urls/path-helper-types.ts +30 -12
  193. package/src/urls/path-helper.ts +17 -2
  194. package/src/urls/pattern-types.ts +21 -1
  195. package/src/urls/response-types.ts +29 -7
  196. package/src/urls/type-extraction.ts +23 -15
  197. package/src/use-loader.tsx +27 -9
  198. package/src/vite/discovery/bundle-postprocess.ts +32 -52
  199. package/src/vite/discovery/discover-routers.ts +52 -26
  200. package/src/vite/discovery/prerender-collection.ts +58 -41
  201. package/src/vite/discovery/route-types-writer.ts +7 -7
  202. package/src/vite/discovery/state.ts +7 -7
  203. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  204. package/src/vite/index.ts +10 -51
  205. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  206. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  207. package/src/vite/plugins/expose-internal-ids.ts +4 -3
  208. package/src/vite/plugins/refresh-cmd.ts +65 -0
  209. package/src/vite/plugins/use-cache-transform.ts +91 -3
  210. package/src/vite/plugins/version-plugin.ts +188 -18
  211. package/src/vite/rango.ts +61 -36
  212. package/src/vite/router-discovery.ts +173 -100
  213. package/src/vite/utils/prerender-utils.ts +81 -0
  214. package/src/vite/utils/shared-utils.ts +19 -9
  215. package/skills/testing/SKILL.md +0 -226
  216. package/src/browser/lru-cache.ts +0 -61
  217. package/src/browser/react/prefetch.ts +0 -27
  218. package/src/browser/request-controller.ts +0 -164
  219. package/src/cache/memory-store.ts +0 -253
  220. package/src/href-context.ts +0 -33
  221. package/src/route-definition/route-function.ts +0 -119
  222. package/src/router.gen.ts +0 -6
  223. package/src/static-handler.gen.ts +0 -5
  224. package/src/urls.gen.ts +0 -8
  225. /package/{CLAUDE.md → AGENTS.md} +0 -0
@@ -16,6 +16,7 @@ export interface ParsedSegment {
16
16
  value: string; // static text, param name, or "*"
17
17
  optional: boolean;
18
18
  constraint?: string[]; // enum values like ["en", "gb"]
19
+ suffix?: string; // literal text after param in same segment (e.g., ".html")
19
20
  }
20
21
 
21
22
  /**
@@ -39,11 +40,21 @@ export function parsePattern(pattern: string): ParsedSegment[] {
39
40
  // - :param(a|b)?
40
41
  // - *
41
42
  const segmentRegex =
42
- /\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)?|(\*)|([^/]+))/g;
43
+ /\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)?([^/]*)|(\*)|([^/]+))/g;
43
44
 
44
45
  let match;
45
46
  while ((match = segmentRegex.exec(pattern)) !== null) {
46
- const [, , paramName, , constraint, optional, wildcard, staticText] = match;
47
+ const [
48
+ ,
49
+ ,
50
+ paramName,
51
+ ,
52
+ constraint,
53
+ optional,
54
+ suffix,
55
+ wildcard,
56
+ staticText,
57
+ ] = match;
47
58
 
48
59
  if (wildcard) {
49
60
  segments.push({ type: "wildcard", value: "*", optional: false });
@@ -53,6 +64,7 @@ export function parsePattern(pattern: string): ParsedSegment[] {
53
64
  value: paramName,
54
65
  optional: optional === "?",
55
66
  constraint: constraint ? constraint.split("|") : undefined,
67
+ suffix: suffix || undefined,
56
68
  });
57
69
  } else if (staticText) {
58
70
  segments.push({ type: "static", value: staticText, optional: false });
@@ -139,16 +151,19 @@ export function compilePattern(pattern: string): CompiledPattern {
139
151
  regexPattern += "/(.*)";
140
152
  } else if (segment.type === "param") {
141
153
  paramNames.push(segment.value);
154
+ const suffixPattern = segment.suffix ? escapeRegex(segment.suffix) : "";
142
155
  const valuePattern = segment.constraint
143
- ? `(${segment.constraint.join("|")})`
144
- : "([^/]+)";
156
+ ? `(${segment.constraint.map(escapeRegex).join("|")})`
157
+ : segment.suffix
158
+ ? "([^/]+?)"
159
+ : "([^/]+)";
145
160
 
146
161
  if (segment.optional) {
147
162
  optionalParams.add(segment.value);
148
163
  // Optional: make the whole /segment optional
149
- regexPattern += `(?:/${valuePattern})?`;
164
+ regexPattern += `(?:/${valuePattern}${suffixPattern})?`;
150
165
  } else {
151
- regexPattern += `/${valuePattern}`;
166
+ regexPattern += `/${valuePattern}${suffixPattern}`;
152
167
  }
153
168
  } else {
154
169
  // Static segment
@@ -388,6 +403,9 @@ export function findMatch<TEnv>(
388
403
  const prFlag = entry.prerenderRouteKeys?.has(routeKey)
389
404
  ? { pr: true as const }
390
405
  : {};
406
+ const ptFlag = entry.passthroughRouteKeys?.has(routeKey)
407
+ ? { pt: true as const }
408
+ : {};
391
409
 
392
410
  // Try exact match first
393
411
  const match = regex.exec(pathname);
@@ -419,6 +437,7 @@ export function findMatch<TEnv>(
419
437
  optionalParams,
420
438
  redirectTo: pathname + "/",
421
439
  ...prFlag,
440
+ ...ptFlag,
422
441
  };
423
442
  } else if (trailingSlashMode === "never" && pathnameHasTrailingSlash) {
424
443
  // Mode says never have trailing slash, but pathname has it
@@ -429,10 +448,18 @@ export function findMatch<TEnv>(
429
448
  optionalParams,
430
449
  redirectTo: pathname.slice(0, -1),
431
450
  ...prFlag,
451
+ ...ptFlag,
432
452
  };
433
453
  }
434
454
 
435
- return { entry, routeKey, params, optionalParams, ...prFlag };
455
+ return {
456
+ entry,
457
+ routeKey,
458
+ params,
459
+ optionalParams,
460
+ ...prFlag,
461
+ ...ptFlag,
462
+ };
436
463
  }
437
464
 
438
465
  // Try alternate pathname (opposite trailing slash)
@@ -446,7 +473,14 @@ export function findMatch<TEnv>(
446
473
  // Determine redirect behavior based on mode
447
474
  if (trailingSlashMode === "ignore") {
448
475
  // Match without redirect
449
- return { entry, routeKey, params, optionalParams, ...prFlag };
476
+ return {
477
+ entry,
478
+ routeKey,
479
+ params,
480
+ optionalParams,
481
+ ...prFlag,
482
+ ...ptFlag,
483
+ };
450
484
  } else if (trailingSlashMode === "never") {
451
485
  // Redirect to no trailing slash
452
486
  if (pathnameHasTrailingSlash) {
@@ -457,9 +491,17 @@ export function findMatch<TEnv>(
457
491
  optionalParams,
458
492
  redirectTo: alternatePathname,
459
493
  ...prFlag,
494
+ ...ptFlag,
460
495
  };
461
496
  }
462
- return { entry, routeKey, params, optionalParams, ...prFlag };
497
+ return {
498
+ entry,
499
+ routeKey,
500
+ params,
501
+ optionalParams,
502
+ ...prFlag,
503
+ ...ptFlag,
504
+ };
463
505
  } else if (trailingSlashMode === "always") {
464
506
  // Redirect to with trailing slash
465
507
  if (!pathnameHasTrailingSlash) {
@@ -470,9 +512,17 @@ export function findMatch<TEnv>(
470
512
  optionalParams,
471
513
  redirectTo: alternatePathname,
472
514
  ...prFlag,
515
+ ...ptFlag,
473
516
  };
474
517
  }
475
- return { entry, routeKey, params, optionalParams, ...prFlag };
518
+ return {
519
+ entry,
520
+ routeKey,
521
+ params,
522
+ optionalParams,
523
+ ...prFlag,
524
+ ...ptFlag,
525
+ };
476
526
  } else {
477
527
  // No explicit mode - use pattern-based detection
478
528
  // Redirect to canonical form (what the pattern defines)
@@ -486,6 +536,7 @@ export function findMatch<TEnv>(
486
536
  optionalParams,
487
537
  redirectTo: canonicalPath,
488
538
  ...prFlag,
539
+ ...ptFlag,
489
540
  };
490
541
  }
491
542
  }
@@ -11,6 +11,8 @@ import {
11
11
  createStaticContext,
12
12
  createReverseFunction,
13
13
  } from "./handler-context.js";
14
+ import { isPrerenderPassthrough } from "../prerender.js";
15
+ import { isRouteRootScoped } from "../route-map-builder.js";
14
16
  import { setupBuildUse } from "./loader-resolution.js";
15
17
  import { loadManifest } from "./manifest.js";
16
18
  import { traverseBack } from "./pattern-matching.js";
@@ -51,6 +53,7 @@ export async function matchForPrerender<TEnv = any>(
51
53
  params: Record<string, string>,
52
54
  deps: PrerenderMatchDeps<TEnv>,
53
55
  buildVars?: Record<string, any>,
56
+ isPassthroughRoute?: boolean,
54
57
  ): Promise<{
55
58
  segments: SerializedSegmentData[];
56
59
  handles: Record<string, SegmentHandleData>;
@@ -58,6 +61,7 @@ export async function matchForPrerender<TEnv = any>(
58
61
  params: Record<string, string>;
59
62
  interceptSegments?: SerializedSegmentData[];
60
63
  interceptHandles?: Record<string, SegmentHandleData>;
64
+ passthrough?: true;
61
65
  } | null> {
62
66
  // 1. Find the matching route entry
63
67
  const matched = deps.findMatch(pathname);
@@ -65,6 +69,7 @@ export async function matchForPrerender<TEnv = any>(
65
69
 
66
70
  // Use params from trie match if available, fall back to provided params
67
71
  const matchedParams = matched.params ?? params;
72
+ const matchedPassthroughRoute = isPassthroughRoute ?? matched.pt === true;
68
73
 
69
74
  // Build RouterContext for loadManifest/traverseBack
70
75
  const routerCtx = deps.buildRouterContext();
@@ -110,6 +115,7 @@ export async function matchForPrerender<TEnv = any>(
110
115
  setCookie: () => {},
111
116
  deleteCookie: () => {},
112
117
  header: () => {},
118
+ setStatus: () => {},
113
119
  use: (() => {
114
120
  throw new Error("use() not available during pre-rendering");
115
121
  }) as any,
@@ -120,10 +126,12 @@ export async function matchForPrerender<TEnv = any>(
120
126
  _onResponseCallbacks: [],
121
127
  setLocationState() {},
122
128
  _locationState: undefined,
129
+ _reportedErrors: new WeakSet<object>(),
123
130
  reverse: createReverseFunction(
124
131
  deps.mergedRouteMap,
125
132
  matched.routeKey,
126
133
  matchedParams,
134
+ matched.routeKey ? isRouteRootScoped(matched.routeKey) : undefined,
127
135
  ),
128
136
  };
129
137
 
@@ -137,6 +145,7 @@ export async function matchForPrerender<TEnv = any>(
137
145
  deps.mergedRouteMap,
138
146
  matched.routeKey,
139
147
  variables,
148
+ matchedPassthroughRoute,
140
149
  );
141
150
 
142
151
  // 7. Wire use() for handles only (loaders throw)
@@ -153,17 +162,31 @@ export async function matchForPrerender<TEnv = any>(
153
162
  { skipLoaders: true },
154
163
  );
155
164
 
156
- // 9. Filter out any loader segments (belt-and-suspenders)
165
+ // 9. Detect passthrough sentinel: handler returned ctx.passthrough()
166
+ for (const seg of allSegments) {
167
+ if (isPrerenderPassthrough(seg.component)) {
168
+ return {
169
+ segments: [],
170
+ handles: {},
171
+ routeName: matched.routeKey,
172
+ params: matchedParams,
173
+ passthrough: true as const,
174
+ };
175
+ }
176
+ }
177
+
178
+ // 10. Filter out any loader segments (belt-and-suspenders)
157
179
  const nonLoaderSegments = allSegments.filter((s) => s.type !== "loader");
158
180
 
159
- // 10. Wait for handles to settle
181
+ // 11. Wait for handles to settle
182
+ handleStore.seal();
160
183
  await handleStore.settled;
161
184
 
162
- // 11. Serialize segments using the cache serializer
185
+ // 12. Serialize segments using the cache serializer
163
186
  const { serializeSegments } = await import("../cache/segment-codec.js");
164
187
  const serializedSegments = await serializeSegments(nonLoaderSegments);
165
188
 
166
- // 12. Collect handle data per segment (skip segments with no handle data)
189
+ // 13. Collect handle data per segment (skip segments with no handle data)
167
190
  const handles: Record<string, SegmentHandleData> = {};
168
191
  for (const seg of nonLoaderSegments) {
169
192
  const segHandles = handleStore.getDataForSegment(seg.id);
@@ -175,7 +198,7 @@ export async function matchForPrerender<TEnv = any>(
175
198
  // Use the trie-level route key (e.g., "docs", "docs.article")
176
199
  const routeName = matched.routeKey;
177
200
 
178
- // 13. Resolve intercept segments for this route (if any ancestor defines
201
+ // 14. Resolve intercept segments for this route (if any ancestor defines
179
202
  // an intercept targeting this route). At build time we skip when()
180
203
  // evaluation -- we pre-render all intercepts unconditionally and let
181
204
  // runtime matching decide which to serve.
@@ -320,6 +343,7 @@ export async function renderStaticSegment<TEnv = any>(
320
343
  setCookie: () => {},
321
344
  deleteCookie: () => {},
322
345
  header: () => {},
346
+ setStatus: () => {},
323
347
  use: (() => {
324
348
  throw new Error("use() not available during static pre-rendering");
325
349
  }) as any,
@@ -330,7 +354,13 @@ export async function renderStaticSegment<TEnv = any>(
330
354
  _onResponseCallbacks: [],
331
355
  setLocationState() {},
332
356
  _locationState: undefined,
333
- reverse: createReverseFunction(mergedRouteMap, routeName, {}),
357
+ _reportedErrors: new WeakSet<object>(),
358
+ reverse: createReverseFunction(
359
+ mergedRouteMap,
360
+ routeName,
361
+ {},
362
+ routeName ? isRouteRootScoped(routeName) : undefined,
363
+ ),
334
364
  };
335
365
 
336
366
  return runWithRequestContext(minimalRequestContext, async () => {
@@ -122,9 +122,15 @@ export async function previewMatch<TEnv = any>(
122
122
  undefined,
123
123
  false,
124
124
  );
125
+ // Recompute middleware from the selected variant's entry tree
126
+ // since different variants can have different middleware chains.
127
+ const variantMiddleware = collectRouteMiddleware(
128
+ traverseBack(negotiateEntry),
129
+ matched.params,
130
+ );
125
131
  return {
126
132
  routeMiddleware:
127
- routeMiddleware.length > 0 ? routeMiddleware : undefined,
133
+ variantMiddleware.length > 0 ? variantMiddleware : undefined,
128
134
  responseType: variant.responseType,
129
135
  handler:
130
136
  negotiateEntry.type === "route"
@@ -6,7 +6,14 @@
6
6
 
7
7
  import type { ResolvedSegment, HandlerContext } from "../types";
8
8
  import type { ActionContext } from "./types";
9
- import { debugLog } from "./logging.js";
9
+ import {
10
+ debugLog,
11
+ pushRevalidationTraceEntry,
12
+ isTraceActive,
13
+ } from "./logging.js";
14
+ import type { RevalidationTraceEntry } from "./logging.js";
15
+ import { _getRequestContext } from "../server/request-context.js";
16
+ import { isAutoGeneratedRouteName } from "../route-name.js";
10
17
 
11
18
  function paramsEqual(
12
19
  a: Record<string, string>,
@@ -50,6 +57,8 @@ interface EvaluateRevalidationOptions<TEnv> {
50
57
  actionContext?: ActionContext;
51
58
  /** If true, this is a stale cache revalidation request */
52
59
  stale?: boolean;
60
+ /** Trace source hint for the revalidation trace */
61
+ traceSource?: RevalidationTraceEntry["source"];
53
62
  }
54
63
 
55
64
  /**
@@ -71,28 +80,54 @@ export async function evaluateRevalidation<TEnv>(
71
80
  context,
72
81
  actionContext,
73
82
  stale,
83
+ traceSource,
74
84
  } = options;
75
85
  const nextParams = segment.params || {};
76
86
  const paramsChanged = !paramsEqual(nextParams, prevParams);
77
87
 
88
+ // Trace helper: push a structured entry to the request-scoped trace buffer.
89
+ // Guarded by isTraceActive() so object construction is skipped in production.
90
+ function pushTrace(
91
+ defaultVal: boolean,
92
+ finalVal: boolean,
93
+ reason: string,
94
+ ): void {
95
+ if (!isTraceActive()) return;
96
+ pushRevalidationTraceEntry({
97
+ segmentId: segment.id,
98
+ segmentType: segment.type,
99
+ belongsToRoute: segment.belongsToRoute ?? false,
100
+ source: traceSource ?? "segment-resolution",
101
+ defaultShouldRevalidate: defaultVal,
102
+ finalShouldRevalidate: finalVal,
103
+ reason,
104
+ customRevalidators: revalidations.length || undefined,
105
+ });
106
+ }
107
+
78
108
  // Calculate default revalidation based on segment type and request method
79
109
  let defaultShouldRevalidate: boolean;
110
+ let defaultReason: string;
80
111
 
81
112
  if (request.method === "POST") {
82
113
  // Actions: revalidate segments that belong to the route, skip parent chain
83
114
  if (segment.type === "route") {
84
115
  // Route segment always revalidates on actions
85
116
  defaultShouldRevalidate = true;
117
+ defaultReason = "action:route-segment";
86
118
  } else if (segment.type === "loader") {
87
119
  // Loaders always revalidate on actions - they often contain action-sensitive data
88
120
  // (e.g., cart count after add-to-cart action)
89
121
  defaultShouldRevalidate = true;
122
+ defaultReason = "action:loader-segment";
90
123
  } else if (segment.belongsToRoute) {
91
124
  // Segment belongs to route (orphan layouts/parallels) - revalidate
92
125
  defaultShouldRevalidate = true;
126
+ defaultReason = "action:belongs-to-route";
93
127
  } else {
94
128
  // Parent chain segment (shared layouts/parallels) - don't revalidate
95
129
  defaultShouldRevalidate = false;
130
+ defaultReason = "action:parent-chain-skip";
96
131
  }
97
132
  } else {
98
133
  // Navigation (GET): Conservative defaults to minimize unnecessary revalidations
@@ -102,6 +137,9 @@ export async function evaluateRevalidation<TEnv>(
102
137
  // Route segments revalidate when params change
103
138
  // Routes are the primary param-dependent content and always need updates
104
139
  defaultShouldRevalidate = paramsChanged;
140
+ defaultReason = paramsChanged
141
+ ? "nav:params-changed"
142
+ : "nav:params-unchanged";
105
143
  if (paramsChanged) {
106
144
  debugLog("revalidation", "route params changed, revalidating", {
107
145
  segmentId: segment.id,
@@ -112,6 +150,7 @@ export async function evaluateRevalidation<TEnv>(
112
150
  // Cannot assume these segments depend on params without explicit declaration
113
151
  // Use custom revalidation functions to opt-in when needed
114
152
  defaultShouldRevalidate = false;
153
+ defaultReason = "nav:non-route-skip";
115
154
  debugLog("revalidation", "non-route segment skipped by default", {
116
155
  segmentId: segment.id,
117
156
  segmentType: segment.type,
@@ -132,6 +171,7 @@ export async function evaluateRevalidation<TEnv>(
132
171
  segmentId: segment.id,
133
172
  });
134
173
  }
174
+ pushTrace(defaultShouldRevalidate, defaultShouldRevalidate, defaultReason);
135
175
  return defaultShouldRevalidate;
136
176
  }
137
177
 
@@ -142,6 +182,16 @@ export async function evaluateRevalidation<TEnv>(
142
182
  // Execute revalidation functions with soft/hard decision pattern
143
183
  let currentSuggestion = defaultShouldRevalidate;
144
184
 
185
+ // Compute public route names (filtered: undefined for auto-generated routes)
186
+ const toRouteName =
187
+ routeKey && !isAutoGeneratedRouteName(routeKey) ? routeKey : undefined;
188
+ const reqCtx = _getRequestContext();
189
+ const prevRouteKey = reqCtx?._prevRouteKey;
190
+ const fromRouteName =
191
+ prevRouteKey && !isAutoGeneratedRouteName(prevRouteKey)
192
+ ? prevRouteKey
193
+ : undefined;
194
+
145
195
  for (const { name, fn } of revalidations) {
146
196
  const result = fn({
147
197
  currentParams: prevSegment?.params || prevParams, // Use segment params if available, else route params
@@ -160,7 +210,9 @@ export async function evaluateRevalidation<TEnv>(
160
210
  actionResult: actionContext?.actionResult,
161
211
  formData: actionContext?.formData,
162
212
  method: request.method, // GET for navigation, POST for actions
163
- routeName: routeKey, // User-friendly route name (e.g., "products.detail")
213
+ routeName: toRouteName, // Navigation target route name (filtered)
214
+ fromRouteName, // Navigation source route name (filtered)
215
+ toRouteName, // Navigation target route name (filtered)
164
216
  // Stale cache context (only true for background revalidation after stale cache render)
165
217
  stale,
166
218
  });
@@ -176,6 +228,7 @@ export async function evaluateRevalidation<TEnv>(
176
228
  revalidator: name,
177
229
  revalidate: result,
178
230
  });
231
+ pushTrace(defaultShouldRevalidate, result, `hard:${name}`);
179
232
  return result;
180
233
  } else if (
181
234
  result &&
@@ -206,5 +259,11 @@ export async function evaluateRevalidation<TEnv>(
206
259
  segmentId: segment.id,
207
260
  revalidate: currentSuggestion,
208
261
  });
262
+ const softNames = revalidations.map((r) => r.name).join(",");
263
+ pushTrace(
264
+ defaultShouldRevalidate,
265
+ currentSuggestion,
266
+ `soft-chain:${softNames}`,
267
+ );
209
268
  return currentSuggestion;
210
269
  }
@@ -18,6 +18,7 @@ import type {
18
18
  ShouldRevalidateFn,
19
19
  } from "../types.js";
20
20
  import type { RouteMatchResult } from "./pattern-matching.js";
21
+ import type { TelemetrySink } from "./telemetry.js";
21
22
 
22
23
  /**
23
24
  * Revalidation context passed to segment resolution
@@ -79,6 +80,7 @@ export interface RouterContext<TEnv = any> {
79
80
  routeMap?: Record<string, string>,
80
81
  routeName?: string,
81
82
  responseType?: string,
83
+ isPassthroughRoute?: boolean,
82
84
  ) => HandlerContext<any, TEnv>;
83
85
 
84
86
  // Loader setup
@@ -181,6 +183,12 @@ export interface RouterContext<TEnv = any> {
181
183
  context: HandlerContext<any, TEnv>;
182
184
  actionContext?: any;
183
185
  stale?: boolean;
186
+ traceSource?:
187
+ | "segment-resolution"
188
+ | "cache-hit"
189
+ | "loader"
190
+ | "parallel"
191
+ | "orphan-layout";
184
192
  }) => Promise<boolean>;
185
193
 
186
194
  // Request context
@@ -234,6 +242,7 @@ export interface RouterContext<TEnv = any> {
234
242
  nextUrl: URL,
235
243
  routeKey: string,
236
244
  actionContext?: any,
245
+ stale?: boolean,
237
246
  ) => Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }>;
238
247
 
239
248
  // Entry revalidation map
@@ -241,6 +250,12 @@ export interface RouterContext<TEnv = any> {
241
250
  entries: EntryData[],
242
251
  ) => Map<string, { revalidate: ShouldRevalidateFn[] }>;
243
252
 
253
+ // Telemetry sink (optional, no-op when undefined)
254
+ telemetry?: TelemetrySink;
255
+
256
+ // Request ID for telemetry span correlation (set per-request in match handlers)
257
+ requestId?: string;
258
+
244
259
  // Intercept loaders only (for cache hit + intercept scenarios)
245
260
  resolveInterceptLoadersOnly?: (
246
261
  intercept: InterceptEntry,