@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
@@ -1,10 +1,45 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
3
3
 
4
+ // -- Revalidation trace types --
5
+
6
+ export interface RevalidationTraceEntry {
7
+ segmentId: string;
8
+ segmentType: string;
9
+ belongsToRoute: boolean;
10
+ source:
11
+ | "segment-resolution"
12
+ | "cache-hit"
13
+ | "loader"
14
+ | "parallel"
15
+ | "orphan-layout";
16
+ defaultShouldRevalidate: boolean;
17
+ finalShouldRevalidate: boolean;
18
+ reason: string;
19
+ customRevalidators?: number;
20
+ }
21
+
22
+ export interface RevalidationTraceMeta {
23
+ method: string;
24
+ prevUrl: string;
25
+ nextUrl: string;
26
+ routeKey: string;
27
+ isAction: boolean;
28
+ stale?: boolean;
29
+ }
30
+
31
+ export interface RevalidationTrace {
32
+ meta: RevalidationTraceMeta;
33
+ entries: RevalidationTraceEntry[];
34
+ }
35
+
36
+ // -- Log context --
37
+
4
38
  interface RouterLogContext {
5
39
  requestId: string;
6
40
  transactionId: string;
7
41
  depth: number;
42
+ revalidationTrace?: RevalidationTrace;
8
43
  }
9
44
 
10
45
  interface RouterLogOptions {
@@ -94,9 +129,16 @@ export function withRouterLogScope<T>(
94
129
  try {
95
130
  const result = fn();
96
131
  if (result && typeof (result as Promise<T>).then === "function") {
97
- return (result as Promise<T>).finally(() => {
98
- debugLog(label, "end");
99
- });
132
+ return (result as Promise<T>).then(
133
+ (value) => {
134
+ debugLog(label, "end");
135
+ return value;
136
+ },
137
+ (error) => {
138
+ debugLog(label, "error", { error: String(error) });
139
+ throw error;
140
+ },
141
+ );
100
142
  }
101
143
  debugLog(label, "end");
102
144
  return result;
@@ -149,3 +191,58 @@ export function debugWarn(
149
191
 
150
192
  console.warn(`${prefix} ${message}`);
151
193
  }
194
+
195
+ // -- Revalidation trace helpers --
196
+
197
+ export function isTraceActive(): boolean {
198
+ if (!INTERNAL_RANGO_DEBUG) return false;
199
+ const ctx = routerLogContext.getStore();
200
+ return !!ctx?.revalidationTrace;
201
+ }
202
+
203
+ export function startRevalidationTrace(meta: RevalidationTraceMeta): void {
204
+ const ctx = routerLogContext.getStore();
205
+ if (!ctx || !INTERNAL_RANGO_DEBUG) return;
206
+ ctx.revalidationTrace = { meta, entries: [] };
207
+ }
208
+
209
+ export function pushRevalidationTraceEntry(
210
+ entry: RevalidationTraceEntry,
211
+ ): void {
212
+ const ctx = routerLogContext.getStore();
213
+ if (!ctx?.revalidationTrace) return;
214
+ ctx.revalidationTrace.entries.push(entry);
215
+ }
216
+
217
+ export function flushRevalidationTrace(): RevalidationTrace | null {
218
+ const ctx = routerLogContext.getStore();
219
+ if (!ctx?.revalidationTrace) return null;
220
+ const trace = ctx.revalidationTrace;
221
+ ctx.revalidationTrace = undefined;
222
+
223
+ if (trace.entries.length === 0) return trace;
224
+
225
+ const revalidated = trace.entries.filter((e) => e.finalShouldRevalidate);
226
+ const skipped = trace.entries.filter((e) => !e.finalShouldRevalidate);
227
+
228
+ debugLog("revalidation-trace", "flush", {
229
+ method: trace.meta.method,
230
+ routeKey: trace.meta.routeKey,
231
+ isAction: trace.meta.isAction,
232
+ stale: trace.meta.stale,
233
+ prevUrl: trace.meta.prevUrl,
234
+ nextUrl: trace.meta.nextUrl,
235
+ total: trace.entries.length,
236
+ revalidated: revalidated.length,
237
+ skipped: skipped.length,
238
+ entries: trace.entries.map((e) => ({
239
+ segmentId: e.segmentId,
240
+ type: e.segmentType,
241
+ source: e.source,
242
+ revalidate: e.finalShouldRevalidate,
243
+ reason: e.reason,
244
+ })),
245
+ });
246
+
247
+ return trace;
248
+ }
@@ -127,6 +127,22 @@ export async function loadManifest(
127
127
  }
128
128
  }
129
129
 
130
+ // Propagate cache profiles for DSL-time cache("profileName") resolution.
131
+ // Non-lazy entries carry profiles directly; lazy entries carry them
132
+ // in the captured lazyContext from include() time.
133
+ const entryProfiles =
134
+ entry.cacheProfiles ?? (lazyContext as any)?.cacheProfiles;
135
+ if (entryProfiles) {
136
+ Store.cacheProfiles = entryProfiles;
137
+ }
138
+
139
+ // Propagate rootScoped from lazyContext so that routes inside
140
+ // nested { name: "sub" } under { name: "" } keep inherited root scope
141
+ // when the manifest is rebuilt on each request.
142
+ if (lazyContext && (lazyContext as any).rootScoped !== undefined) {
143
+ Store.rootScoped = (lazyContext as any).rootScoped;
144
+ }
145
+
130
146
  const handlerExecStart = performance.now();
131
147
  const useItems = await getContext().runWithStore(
132
148
  Store,
@@ -178,15 +194,28 @@ export async function loadManifest(
178
194
  "default" in load
179
195
  ) {
180
196
  // Promise<{ default: () => Array }> - e.g., dynamic import
181
- // Lazy-loaded handlers may need helpers (passed as optional arg)
197
+ if (typeof load.default !== "function") {
198
+ throw new Error(
199
+ `[@rangojs/router] Unsupported async handler: { default } must be a function, ` +
200
+ `got ${typeof load.default}. Use () => import('./urls') for lazy loading.`,
201
+ );
202
+ }
182
203
  return (load.default as (h?: any) => any)(helpers);
183
204
  }
184
205
  if (typeof load === "function") {
185
206
  // Promise<() => Array>
186
207
  return (load as (h?: any) => any)(helpers);
187
208
  }
188
- // Promise<Array> - direct array from async handler
189
- return load;
209
+ // Reject unsupported async handler results. Supported shapes are:
210
+ // Promise<{ default: fn }> — dynamic import
211
+ // Promise<fn> — lazy function
212
+ // Direct Promise<Array> is not supported; use a function wrapper.
213
+ throw new Error(
214
+ `[@rangojs/router] Unsupported async handler result (${typeof load}). ` +
215
+ `Lazy route handlers must resolve to a function or { default: fn }, ` +
216
+ `not a direct array. Wrap your handler: () => import('./urls') or ` +
217
+ `() => Promise.resolve((h) => [...])`,
218
+ );
190
219
  }
191
220
 
192
221
  // Inline handler - routes were registered with correct parent inside layout
@@ -31,7 +31,11 @@ import type { ErrorBoundaryHandler, ErrorInfo, MatchResult } from "../types";
31
31
  import type { ReactNode } from "react";
32
32
  import type { MatchContext } from "./match-context.js";
33
33
  import type { MatchApiDeps, ActionContext } from "./types.js";
34
- import { getRequestContext } from "../server/request-context.js";
34
+ import {
35
+ getRequestContext,
36
+ setRequestContextPrevRouteKey,
37
+ } from "../server/request-context.js";
38
+ import { isAutoGeneratedRouteName } from "../route-name.js";
35
39
  import { debugLog, debugWarn } from "./logging.js";
36
40
 
37
41
  /**
@@ -87,6 +91,13 @@ export async function createMatchContextForFull<TEnv>(
87
91
  });
88
92
  }
89
93
 
94
+ if (
95
+ manifestEntry.type === "route" &&
96
+ manifestEntry.prerenderDef?.options?.passthrough === true
97
+ ) {
98
+ matched.pt = true;
99
+ }
100
+
90
101
  const routeMiddleware = collectRouteMiddleware(
91
102
  traverseBack(manifestEntry),
92
103
  matched.params,
@@ -105,6 +116,7 @@ export async function createMatchContextForFull<TEnv>(
105
116
  deps.getRouteMap(),
106
117
  matched.routeKey,
107
118
  matched.responseType,
119
+ matched.pt === true,
108
120
  );
109
121
 
110
122
  const loaderPromises = new Map<string, Promise<any>>();
@@ -161,6 +173,10 @@ export async function createMatchContextForFull<TEnv>(
161
173
  request,
162
174
  env,
163
175
  segments: { path: [], ids: [] },
176
+ toRouteName:
177
+ matched.routeKey && !isAutoGeneratedRouteName(matched.routeKey)
178
+ ? matched.routeKey
179
+ : undefined,
164
180
  },
165
181
  isSameRouteNavigation: false,
166
182
  interceptResult: null,
@@ -207,10 +223,21 @@ export async function createMatchContextForPartial<TEnv>(
207
223
  return null;
208
224
  }
209
225
 
210
- const prevUrl = new URL(previousUrl, url.origin);
211
- const interceptContextUrl = interceptSourceUrl
212
- ? new URL(interceptSourceUrl, url.origin)
213
- : prevUrl;
226
+ let prevUrl: URL;
227
+ try {
228
+ prevUrl = new URL(previousUrl, url.origin);
229
+ } catch {
230
+ return null;
231
+ }
232
+
233
+ let interceptContextUrl: URL;
234
+ try {
235
+ interceptContextUrl = interceptSourceUrl
236
+ ? new URL(interceptSourceUrl, url.origin)
237
+ : prevUrl;
238
+ } catch {
239
+ interceptContextUrl = prevUrl;
240
+ }
214
241
 
215
242
  const routeMatchStart = metricsStore ? performance.now() : 0;
216
243
  const prevMatch = deps.findMatch(prevUrl.pathname);
@@ -262,6 +289,13 @@ export async function createMatchContextForPartial<TEnv>(
262
289
  });
263
290
  }
264
291
 
292
+ if (
293
+ manifestEntry.type === "route" &&
294
+ manifestEntry.prerenderDef?.options?.passthrough === true
295
+ ) {
296
+ matched.pt = true;
297
+ }
298
+
265
299
  const routeMiddleware = collectRouteMiddleware(
266
300
  traverseBack(manifestEntry),
267
301
  matched.params,
@@ -280,6 +314,7 @@ export async function createMatchContextForPartial<TEnv>(
280
314
  deps.getRouteMap(),
281
315
  matched.routeKey,
282
316
  matched.responseType,
317
+ matched.pt === true,
283
318
  );
284
319
 
285
320
  const clientSegmentSet = new Set(clientSegmentIds);
@@ -325,16 +360,35 @@ export async function createMatchContextForPartial<TEnv>(
325
360
  if (/D\d+\./.test(id)) return false;
326
361
  return true;
327
362
  });
363
+ const effectiveFromUrl = interceptSourceUrl ? interceptContextUrl : prevUrl;
364
+ const effectiveFromMatch = interceptSourceUrl
365
+ ? interceptContextMatch
366
+ : prevMatch;
367
+
368
+ // Store previous route key on the request context for revalidation
369
+ // fromRouteName. Uses effectiveFromMatch so intercept-source navigations
370
+ // see the intercept origin route, not the plain previous URL route.
371
+ setRequestContextPrevRouteKey(effectiveFromMatch?.routeKey);
372
+
328
373
  const interceptSelectorContext: InterceptSelectorContext = {
329
- from: prevUrl,
374
+ from: effectiveFromUrl,
330
375
  to: cleanUrl,
331
376
  params: matched.params,
332
377
  request,
333
378
  env,
334
379
  segments: {
335
- path: prevUrl.pathname.split("/").filter(Boolean),
380
+ path: effectiveFromUrl.pathname.split("/").filter(Boolean),
336
381
  ids: filteredSegmentIds,
337
382
  },
383
+ fromRouteName:
384
+ effectiveFromMatch?.routeKey &&
385
+ !isAutoGeneratedRouteName(effectiveFromMatch.routeKey)
386
+ ? effectiveFromMatch.routeKey
387
+ : undefined,
388
+ toRouteName:
389
+ matched.routeKey && !isAutoGeneratedRouteName(matched.routeKey)
390
+ ? matched.routeKey
391
+ : undefined,
338
392
  };
339
393
  const isAction = !!actionContext;
340
394
 
@@ -537,10 +591,7 @@ export async function matchError<TEnv>(
537
591
 
538
592
  const reqCtx = getRequestContext();
539
593
  if (reqCtx) {
540
- reqCtx.res = new Response(null, {
541
- status: 500,
542
- headers: reqCtx.res.headers,
543
- });
594
+ reqCtx.setStatus(500);
544
595
  }
545
596
 
546
597
  const effectiveFallback = fallback || DefaultErrorFallback;
@@ -567,46 +618,3 @@ export async function matchError<TEnv>(
567
618
  params: matched.params,
568
619
  };
569
620
  }
570
-
571
- /**
572
- * Preview match - returns route middleware without segment resolution.
573
- */
574
- export async function previewMatch<TEnv>(
575
- request: Request,
576
- context: TEnv,
577
- deps: MatchApiDeps<TEnv>,
578
- ): Promise<{
579
- routeMiddleware?: Array<{
580
- handler: import("./middleware.js").MiddlewareFn;
581
- params: Record<string, string>;
582
- }>;
583
- } | null> {
584
- const url = new URL(request.url);
585
- const pathname = url.pathname;
586
-
587
- const matched = deps.findMatch(pathname);
588
- if (!matched) {
589
- return null;
590
- }
591
-
592
- if (matched.redirectTo) {
593
- return { routeMiddleware: undefined };
594
- }
595
-
596
- const manifestEntry = await loadManifest(
597
- matched.entry,
598
- matched.routeKey,
599
- pathname,
600
- undefined,
601
- false,
602
- );
603
-
604
- const routeMiddleware = collectRouteMiddleware(
605
- traverseBack(manifestEntry),
606
- matched.params,
607
- );
608
-
609
- return {
610
- routeMiddleware: routeMiddleware.length > 0 ? routeMiddleware : undefined,
611
- };
612
- }
@@ -210,6 +210,9 @@ export interface MatchPipelineState {
210
210
  // Whether cache should be revalidated (SWR)
211
211
  shouldRevalidate?: boolean;
212
212
 
213
+ // Source of cache hit ("runtime" or "prerender")
214
+ cacheSource?: "runtime" | "prerender";
215
+
213
216
  // Resolved segments from pipeline
214
217
  segments: ResolvedSegment[];
215
218
  matchedIds: string[];
@@ -22,9 +22,21 @@ import {
22
22
  matchError as _matchError,
23
23
  } from "./match-api.js";
24
24
  import { previewMatch as _previewMatch } from "./preview-match.js";
25
- import { runWithRouterLogContext, withRouterLogScope } from "./logging.js";
25
+ import {
26
+ runWithRouterLogContext,
27
+ withRouterLogScope,
28
+ isRouterDebugEnabled,
29
+ startRevalidationTrace,
30
+ flushRevalidationTrace,
31
+ } from "./logging.js";
26
32
  import type { ErrorBoundaryHandler, NotFoundBoundaryHandler } from "../types";
27
33
  import type { MiddlewareFn } from "./middleware.js";
34
+ import {
35
+ type TelemetrySink,
36
+ safeEmit,
37
+ resolveSink,
38
+ getRequestId,
39
+ } from "./telemetry.js";
28
40
 
29
41
  export interface MatchHandlerDeps<TEnv = any> {
30
42
  buildRouterContext: () => RouterContext<TEnv>;
@@ -38,6 +50,7 @@ export interface MatchHandlerDeps<TEnv = any> {
38
50
  selectorContext: InterceptSelectorContext | null,
39
51
  isAction: boolean,
40
52
  ) => { intercept: InterceptEntry; entry: EntryData } | null;
53
+ telemetry?: TelemetrySink;
41
54
  }
42
55
 
43
56
  export interface MatchHandlers<TEnv = any> {
@@ -98,6 +111,8 @@ export function createMatchHandlers<TEnv = any>(
98
111
  defaultErrorBoundary,
99
112
  findInterceptForRoute,
100
113
  } = deps;
114
+ const hasTelemetry = !!deps.telemetry;
115
+ const telemetry = resolveSink(deps.telemetry);
101
116
 
102
117
  async function createMatchContextForFull(
103
118
  request: Request,
@@ -140,13 +155,43 @@ export function createMatchHandlers<TEnv = any>(
140
155
  * - background-revalidation: SWR revalidation
141
156
  */
142
157
  async function match(request: Request, env: TEnv): Promise<MatchResult> {
143
- return runWithRouterLogContext({ request, transaction: "match" }, () =>
144
- runWithRouterContext(buildRouterContext(), async () =>
158
+ const requestId = hasTelemetry ? getRequestId(request) : undefined;
159
+ return runWithRouterLogContext({ request, transaction: "match" }, () => {
160
+ const routerCtx = buildRouterContext();
161
+ routerCtx.requestId = requestId;
162
+ return runWithRouterContext(routerCtx, async () =>
145
163
  withRouterLogScope("match", async () => {
164
+ const matchStart = performance.now();
165
+ const pathname = new URL(request.url).pathname;
166
+ if (hasTelemetry) {
167
+ safeEmit(telemetry, {
168
+ type: "request.start",
169
+ timestamp: matchStart,
170
+ requestId,
171
+ method: request.method,
172
+ pathname,
173
+ transaction: "match",
174
+ isPartial: false,
175
+ });
176
+ }
177
+
146
178
  const result = await createMatchContextForFull(request, env);
147
179
 
148
180
  // Handle redirect case
149
181
  if ("type" in result && result.type === "redirect") {
182
+ if (hasTelemetry) {
183
+ safeEmit(telemetry, {
184
+ type: "request.end",
185
+ timestamp: performance.now(),
186
+ requestId,
187
+ method: request.method,
188
+ pathname,
189
+ transaction: "match",
190
+ durationMs: performance.now() - matchStart,
191
+ segmentCount: 0,
192
+ cacheHit: false,
193
+ });
194
+ }
150
195
  return {
151
196
  segments: [],
152
197
  matched: [],
@@ -161,8 +206,47 @@ export function createMatchHandlers<TEnv = any>(
161
206
  try {
162
207
  const state = createPipelineState();
163
208
  const pipeline = createMatchPartialPipeline(ctx, state);
164
- return await collectMatchResult(pipeline, ctx, state);
209
+ const matchResult = await collectMatchResult(pipeline, ctx, state);
210
+ if (hasTelemetry) {
211
+ safeEmit(telemetry, {
212
+ type: "cache.decision",
213
+ timestamp: performance.now(),
214
+ requestId,
215
+ pathname,
216
+ routeKey: ctx.routeKey,
217
+ hit: state.cacheHit,
218
+ shouldRevalidate: !!state.shouldRevalidate,
219
+ source: state.cacheSource,
220
+ });
221
+ safeEmit(telemetry, {
222
+ type: "request.end",
223
+ timestamp: performance.now(),
224
+ requestId,
225
+ method: request.method,
226
+ pathname,
227
+ transaction: "match",
228
+ durationMs: performance.now() - matchStart,
229
+ segmentCount: matchResult.segments.length,
230
+ cacheHit: state.cacheHit,
231
+ });
232
+ }
233
+ return matchResult;
165
234
  } catch (error) {
235
+ if (hasTelemetry) {
236
+ const errorObj =
237
+ error instanceof Error ? error : new Error(String(error));
238
+ safeEmit(telemetry, {
239
+ type: "request.error",
240
+ timestamp: performance.now(),
241
+ requestId,
242
+ method: request.method,
243
+ pathname,
244
+ transaction: "match",
245
+ error: errorObj,
246
+ phase: error instanceof Response ? "redirect" : "routing",
247
+ durationMs: performance.now() - matchStart,
248
+ });
249
+ }
166
250
  if (error instanceof Response) throw error;
167
251
  // Report unhandled errors during full match pipeline
168
252
  callOnError(error, "routing", {
@@ -175,8 +259,8 @@ export function createMatchHandlers<TEnv = any>(
175
259
  throw sanitizeError(error);
176
260
  }
177
261
  }),
178
- ),
179
- );
262
+ );
263
+ });
180
264
  }
181
265
 
182
266
  async function matchError(
@@ -214,23 +298,112 @@ export function createMatchHandlers<TEnv = any>(
214
298
  context: TEnv,
215
299
  actionContext?: ActionContext,
216
300
  ): Promise<MatchResult | null> {
301
+ const partialRequestId = hasTelemetry ? getRequestId(request) : undefined;
217
302
  return runWithRouterLogContext(
218
303
  { request, transaction: "matchPartial" },
219
- () =>
220
- runWithRouterContext(buildRouterContext(), async () =>
304
+ () => {
305
+ const routerCtx = buildRouterContext();
306
+ routerCtx.requestId = partialRequestId;
307
+ return runWithRouterContext(routerCtx, async () =>
221
308
  withRouterLogScope("matchPartial", async () => {
309
+ const matchStart = performance.now();
310
+ const pathname = new URL(request.url).pathname;
311
+ if (hasTelemetry) {
312
+ safeEmit(telemetry, {
313
+ type: "request.start",
314
+ timestamp: matchStart,
315
+ requestId: partialRequestId,
316
+ method: request.method,
317
+ pathname,
318
+ transaction: "matchPartial",
319
+ isPartial: true,
320
+ });
321
+ }
322
+
222
323
  const ctx = await createMatchContextForPartial(
223
324
  request,
224
325
  context,
225
326
  actionContext,
226
327
  );
227
- if (!ctx) return null;
328
+ if (!ctx) {
329
+ if (hasTelemetry) {
330
+ safeEmit(telemetry, {
331
+ type: "request.end",
332
+ timestamp: performance.now(),
333
+ requestId: partialRequestId,
334
+ method: request.method,
335
+ pathname,
336
+ transaction: "matchPartial",
337
+ durationMs: performance.now() - matchStart,
338
+ segmentCount: 0,
339
+ cacheHit: false,
340
+ });
341
+ }
342
+ return null;
343
+ }
344
+
345
+ if (isRouterDebugEnabled()) {
346
+ startRevalidationTrace({
347
+ method: request.method,
348
+ prevUrl: ctx.prevUrl.href,
349
+ nextUrl: ctx.url.href,
350
+ routeKey: ctx.routeKey,
351
+ isAction: !!actionContext,
352
+ stale: ctx.stale || undefined,
353
+ });
354
+ }
228
355
 
229
356
  try {
230
357
  const state = createPipelineState();
231
358
  const pipeline = createMatchPartialPipeline(ctx, state);
232
- return await collectMatchResult(pipeline, ctx, state);
359
+ const matchResult = await collectMatchResult(
360
+ pipeline,
361
+ ctx,
362
+ state,
363
+ );
364
+ flushRevalidationTrace();
365
+ if (hasTelemetry) {
366
+ safeEmit(telemetry, {
367
+ type: "cache.decision",
368
+ timestamp: performance.now(),
369
+ requestId: partialRequestId,
370
+ pathname,
371
+ routeKey: ctx.routeKey,
372
+ hit: state.cacheHit,
373
+ shouldRevalidate: !!state.shouldRevalidate,
374
+ source: state.cacheSource,
375
+ });
376
+ safeEmit(telemetry, {
377
+ type: "request.end",
378
+ timestamp: performance.now(),
379
+ requestId: partialRequestId,
380
+ method: request.method,
381
+ pathname,
382
+ transaction: "matchPartial",
383
+ durationMs: performance.now() - matchStart,
384
+ segmentCount: matchResult.segments.length,
385
+ cacheHit: state.cacheHit,
386
+ });
387
+ }
388
+ return matchResult;
233
389
  } catch (error) {
390
+ flushRevalidationTrace();
391
+ if (hasTelemetry) {
392
+ const errorObj =
393
+ error instanceof Error ? error : new Error(String(error));
394
+ const phase = actionContext ? "action" : "revalidation";
395
+ safeEmit(telemetry, {
396
+ type: "request.error",
397
+ timestamp: performance.now(),
398
+ requestId: partialRequestId,
399
+ method: request.method,
400
+ pathname,
401
+ transaction: "matchPartial",
402
+ error: errorObj,
403
+ phase: error instanceof Response ? "redirect" : phase,
404
+ durationMs: performance.now() - matchStart,
405
+ });
406
+ }
234
407
  if (error instanceof Response) throw error;
235
408
  // Report unhandled errors during partial match pipeline
236
409
  callOnError(error, actionContext ? "action" : "revalidation", {
@@ -244,7 +417,8 @@ export function createMatchHandlers<TEnv = any>(
244
417
  throw sanitizeError(error);
245
418
  }
246
419
  }),
247
- ),
420
+ );
421
+ },
248
422
  );
249
423
  }
250
424