@rangojs/router 0.0.0-experimental.b9cb8739 → 0.0.0-experimental.bf1b128c

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 (182) hide show
  1. package/README.md +126 -38
  2. package/dist/bin/rango.js +138 -50
  3. package/dist/vite/index.js +2018 -732
  4. package/dist/vite/index.js.bak +5448 -0
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +7 -5
  7. package/skills/breadcrumbs/SKILL.md +3 -1
  8. package/skills/cache-guide/SKILL.md +32 -0
  9. package/skills/caching/SKILL.md +45 -4
  10. package/skills/handler-use/SKILL.md +362 -0
  11. package/skills/hooks/SKILL.md +28 -20
  12. package/skills/intercept/SKILL.md +20 -0
  13. package/skills/layout/SKILL.md +22 -0
  14. package/skills/links/SKILL.md +91 -17
  15. package/skills/loader/SKILL.md +119 -45
  16. package/skills/middleware/SKILL.md +34 -3
  17. package/skills/migrate-nextjs/SKILL.md +560 -0
  18. package/skills/migrate-react-router/SKILL.md +765 -0
  19. package/skills/parallel/SKILL.md +192 -0
  20. package/skills/prerender/SKILL.md +110 -68
  21. package/skills/rango/SKILL.md +24 -22
  22. package/skills/response-routes/SKILL.md +8 -0
  23. package/skills/route/SKILL.md +55 -0
  24. package/skills/router-setup/SKILL.md +87 -2
  25. package/skills/streams-and-websockets/SKILL.md +283 -0
  26. package/skills/typesafety/SKILL.md +13 -1
  27. package/src/__internal.ts +1 -1
  28. package/src/browser/app-shell.ts +52 -0
  29. package/src/browser/app-version.ts +14 -0
  30. package/src/browser/event-controller.ts +5 -0
  31. package/src/browser/navigation-bridge.ts +88 -9
  32. package/src/browser/navigation-client.ts +167 -59
  33. package/src/browser/navigation-store.ts +68 -9
  34. package/src/browser/navigation-transaction.ts +11 -9
  35. package/src/browser/partial-update.ts +109 -15
  36. package/src/browser/prefetch/cache.ts +175 -15
  37. package/src/browser/prefetch/fetch.ts +180 -33
  38. package/src/browser/prefetch/queue.ts +123 -20
  39. package/src/browser/prefetch/resource-ready.ts +77 -0
  40. package/src/browser/rango-state.ts +53 -13
  41. package/src/browser/react/Link.tsx +81 -9
  42. package/src/browser/react/NavigationProvider.tsx +89 -14
  43. package/src/browser/react/context.ts +7 -2
  44. package/src/browser/react/use-handle.ts +9 -58
  45. package/src/browser/react/use-navigation.ts +22 -2
  46. package/src/browser/react/use-params.ts +11 -1
  47. package/src/browser/react/use-router.ts +29 -9
  48. package/src/browser/rsc-router.tsx +168 -65
  49. package/src/browser/scroll-restoration.ts +36 -17
  50. package/src/browser/segment-reconciler.ts +36 -9
  51. package/src/browser/server-action-bridge.ts +8 -6
  52. package/src/browser/types.ts +49 -5
  53. package/src/build/generate-manifest.ts +6 -6
  54. package/src/build/generate-route-types.ts +3 -0
  55. package/src/build/route-trie.ts +50 -24
  56. package/src/build/route-types/include-resolution.ts +8 -1
  57. package/src/build/route-types/router-processing.ts +223 -74
  58. package/src/build/route-types/scan-filter.ts +8 -1
  59. package/src/cache/cache-runtime.ts +15 -11
  60. package/src/cache/cache-scope.ts +48 -7
  61. package/src/cache/cf/cf-cache-store.ts +455 -15
  62. package/src/cache/cf/index.ts +5 -1
  63. package/src/cache/document-cache.ts +17 -7
  64. package/src/cache/index.ts +1 -0
  65. package/src/cache/taint.ts +55 -0
  66. package/src/client.tsx +84 -230
  67. package/src/context-var.ts +72 -2
  68. package/src/debug.ts +2 -2
  69. package/src/handle.ts +40 -0
  70. package/src/index.rsc.ts +6 -1
  71. package/src/index.ts +49 -6
  72. package/src/outlet-context.ts +1 -1
  73. package/src/prerender/store.ts +5 -4
  74. package/src/prerender.ts +138 -77
  75. package/src/response-utils.ts +28 -0
  76. package/src/reverse.ts +27 -2
  77. package/src/route-definition/dsl-helpers.ts +240 -40
  78. package/src/route-definition/helpers-types.ts +73 -20
  79. package/src/route-definition/index.ts +3 -0
  80. package/src/route-definition/redirect.ts +11 -3
  81. package/src/route-definition/resolve-handler-use.ts +155 -0
  82. package/src/route-map-builder.ts +7 -1
  83. package/src/route-types.ts +18 -0
  84. package/src/router/content-negotiation.ts +100 -1
  85. package/src/router/find-match.ts +4 -2
  86. package/src/router/handler-context.ts +101 -25
  87. package/src/router/intercept-resolution.ts +11 -4
  88. package/src/router/lazy-includes.ts +10 -7
  89. package/src/router/loader-resolution.ts +159 -21
  90. package/src/router/logging.ts +5 -2
  91. package/src/router/manifest.ts +31 -16
  92. package/src/router/match-api.ts +127 -192
  93. package/src/router/match-middleware/background-revalidation.ts +30 -2
  94. package/src/router/match-middleware/cache-lookup.ts +94 -17
  95. package/src/router/match-middleware/cache-store.ts +53 -10
  96. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  97. package/src/router/match-middleware/segment-resolution.ts +61 -5
  98. package/src/router/match-result.ts +104 -10
  99. package/src/router/metrics.ts +6 -1
  100. package/src/router/middleware-types.ts +8 -30
  101. package/src/router/middleware.ts +58 -13
  102. package/src/router/navigation-snapshot.ts +182 -0
  103. package/src/router/pattern-matching.ts +60 -9
  104. package/src/router/prerender-match.ts +110 -10
  105. package/src/router/preview-match.ts +30 -102
  106. package/src/router/request-classification.ts +310 -0
  107. package/src/router/revalidation.ts +15 -1
  108. package/src/router/route-snapshot.ts +245 -0
  109. package/src/router/router-context.ts +6 -1
  110. package/src/router/router-interfaces.ts +36 -4
  111. package/src/router/router-options.ts +37 -11
  112. package/src/router/segment-resolution/fresh.ts +198 -20
  113. package/src/router/segment-resolution/helpers.ts +29 -24
  114. package/src/router/segment-resolution/loader-cache.ts +1 -0
  115. package/src/router/segment-resolution/revalidation.ts +452 -307
  116. package/src/router/segment-wrappers.ts +2 -0
  117. package/src/router/trie-matching.ts +10 -4
  118. package/src/router/types.ts +1 -0
  119. package/src/router/url-params.ts +49 -0
  120. package/src/router.ts +60 -8
  121. package/src/rsc/handler.ts +478 -374
  122. package/src/rsc/helpers.ts +69 -41
  123. package/src/rsc/loader-fetch.ts +23 -3
  124. package/src/rsc/manifest-init.ts +5 -1
  125. package/src/rsc/progressive-enhancement.ts +16 -2
  126. package/src/rsc/response-route-handler.ts +14 -1
  127. package/src/rsc/rsc-rendering.ts +17 -1
  128. package/src/rsc/server-action.ts +10 -0
  129. package/src/rsc/ssr-setup.ts +2 -2
  130. package/src/rsc/types.ts +9 -1
  131. package/src/segment-content-promise.ts +67 -0
  132. package/src/segment-loader-promise.ts +122 -0
  133. package/src/segment-system.tsx +109 -23
  134. package/src/server/context.ts +166 -17
  135. package/src/server/handle-store.ts +19 -0
  136. package/src/server/loader-registry.ts +9 -8
  137. package/src/server/request-context.ts +194 -60
  138. package/src/ssr/index.tsx +4 -0
  139. package/src/static-handler.ts +18 -6
  140. package/src/types/cache-types.ts +4 -4
  141. package/src/types/handler-context.ts +145 -68
  142. package/src/types/loader-types.ts +41 -15
  143. package/src/types/request-scope.ts +126 -0
  144. package/src/types/route-entry.ts +19 -1
  145. package/src/types/segments.ts +2 -0
  146. package/src/urls/include-helper.ts +24 -14
  147. package/src/urls/path-helper-types.ts +39 -6
  148. package/src/urls/path-helper.ts +48 -13
  149. package/src/urls/pattern-types.ts +12 -0
  150. package/src/urls/response-types.ts +18 -16
  151. package/src/use-loader.tsx +77 -5
  152. package/src/vite/debug.ts +184 -0
  153. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  154. package/src/vite/discovery/discover-routers.ts +36 -4
  155. package/src/vite/discovery/gate-state.ts +171 -0
  156. package/src/vite/discovery/prerender-collection.ts +175 -74
  157. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  158. package/src/vite/discovery/state.ts +13 -6
  159. package/src/vite/index.ts +4 -0
  160. package/src/vite/plugin-types.ts +51 -79
  161. package/src/vite/plugins/cjs-to-esm.ts +5 -0
  162. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  163. package/src/vite/plugins/client-ref-hashing.ts +16 -4
  164. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  165. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  166. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  167. package/src/vite/plugins/expose-action-id.ts +53 -31
  168. package/src/vite/plugins/expose-id-utils.ts +12 -0
  169. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  170. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  171. package/src/vite/plugins/expose-internal-ids.ts +563 -316
  172. package/src/vite/plugins/performance-tracks.ts +96 -0
  173. package/src/vite/plugins/refresh-cmd.ts +88 -26
  174. package/src/vite/plugins/use-cache-transform.ts +56 -43
  175. package/src/vite/plugins/version-injector.ts +37 -11
  176. package/src/vite/plugins/version-plugin.ts +13 -1
  177. package/src/vite/rango.ts +204 -217
  178. package/src/vite/router-discovery.ts +732 -94
  179. package/src/vite/utils/banner.ts +4 -4
  180. package/src/vite/utils/package-resolution.ts +41 -1
  181. package/src/vite/utils/prerender-utils.ts +37 -5
  182. package/src/vite/utils/shared-utils.ts +3 -2
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Request Classification
3
+ *
4
+ * Replaces the implicit "preview then match again" model with a clean
5
+ * two-stage architecture:
6
+ *
7
+ * 1. Classification — classifyRequest() produces a RequestPlan that answers
8
+ * all routing questions once: target route, request mode, route middleware,
9
+ * response-route info, negotiation state.
10
+ *
11
+ * 2. Execution — executeRequest() dispatches on the plan to the appropriate
12
+ * handler (response route, loader fetch, full render, partial render,
13
+ * action revalidation, PE render).
14
+ *
15
+ * Builds on RouteSnapshot from route-snapshot.ts.
16
+ */
17
+
18
+ import { RouteNotFoundError } from "../errors.js";
19
+ import type { EntryData } from "../server/context.js";
20
+ import type { CollectedMiddleware } from "./middleware-types.js";
21
+ import type { RouteMatchResult } from "./pattern-matching.js";
22
+ import { negotiateRoute } from "./content-negotiation.js";
23
+ import { stripInternalParams } from "./handler-context.js";
24
+ import { resolveRoute, type RouteSnapshot } from "./route-snapshot.js";
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // RequestPlan — discriminated union
28
+ // ---------------------------------------------------------------------------
29
+
30
+ interface RedirectPlan<TEnv = any> {
31
+ mode: "redirect";
32
+ route: RouteSnapshot<TEnv>;
33
+ redirectUrl: string;
34
+ }
35
+
36
+ interface VersionMismatchPlan<TEnv = any> {
37
+ mode: "version-mismatch";
38
+ /** May be undefined when version mismatch is detected before route resolution */
39
+ route?: RouteSnapshot<TEnv>;
40
+ reloadUrl: string;
41
+ }
42
+
43
+ interface ResponseRoutePlan<TEnv = any> {
44
+ mode: "response";
45
+ route: RouteSnapshot<TEnv>;
46
+ handler: Function;
47
+ responseType: string;
48
+ negotiated: boolean;
49
+ manifestEntry: EntryData;
50
+ routeMiddleware: CollectedMiddleware[];
51
+ }
52
+
53
+ interface LoaderFetchPlan<TEnv = any> {
54
+ mode: "loader";
55
+ route: RouteSnapshot<TEnv>;
56
+ }
57
+
58
+ interface PeRenderPlan<TEnv = any> {
59
+ mode: "pe-render";
60
+ route: RouteSnapshot<TEnv>;
61
+ }
62
+
63
+ interface ActionPlan<TEnv = any> {
64
+ mode: "action";
65
+ route: RouteSnapshot<TEnv>;
66
+ actionId: string;
67
+ negotiated: boolean;
68
+ }
69
+
70
+ interface FullRenderPlan<TEnv = any> {
71
+ mode: "full-render";
72
+ route: RouteSnapshot<TEnv>;
73
+ negotiated: boolean;
74
+ }
75
+
76
+ interface PartialRenderPlan<TEnv = any> {
77
+ mode: "partial-render";
78
+ route: RouteSnapshot<TEnv>;
79
+ negotiated: boolean;
80
+ }
81
+
82
+ /**
83
+ * The output of request classification. A discriminated union where each
84
+ * variant carries exactly the fields needed for its execution path.
85
+ */
86
+ export type RequestPlan<TEnv = any> =
87
+ | RedirectPlan<TEnv>
88
+ | VersionMismatchPlan<TEnv>
89
+ | ResponseRoutePlan<TEnv>
90
+ | LoaderFetchPlan<TEnv>
91
+ | PeRenderPlan<TEnv>
92
+ | ActionPlan<TEnv>
93
+ | FullRenderPlan<TEnv>
94
+ | PartialRenderPlan<TEnv>;
95
+
96
+ /**
97
+ * Plans that have passed the terminal-check gate (version-mismatch handled)
98
+ * and are ready for execution. Always have a `route` field.
99
+ */
100
+ export type ExecutableRequestPlan<TEnv = any> = Exclude<
101
+ RequestPlan<TEnv>,
102
+ VersionMismatchPlan<TEnv>
103
+ >;
104
+
105
+ /**
106
+ * Re-export individual plan types for consumers that need to narrow.
107
+ */
108
+ export type {
109
+ RedirectPlan,
110
+ VersionMismatchPlan,
111
+ ResponseRoutePlan,
112
+ LoaderFetchPlan,
113
+ PeRenderPlan,
114
+ ActionPlan,
115
+ FullRenderPlan,
116
+ PartialRenderPlan,
117
+ };
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // classifyRequest — the single authoritative classification step
121
+ // ---------------------------------------------------------------------------
122
+
123
+ export interface ClassifyRequestDeps<TEnv = any> {
124
+ findMatch: (pathname: string) => RouteMatchResult<TEnv> | null;
125
+ routerVersion: string;
126
+ routerId: string;
127
+ }
128
+
129
+ /**
130
+ * Classify an incoming request into a RequestPlan.
131
+ *
132
+ * This is the single source of truth for request mode detection. It replaces
133
+ * the scattered previewMatch + isAction/isLoaderFetch/isPartial checks in
134
+ * handler.ts.
135
+ *
136
+ * Classification order:
137
+ * 1. Route resolution (findMatch + loadManifest via resolveRoute lite)
138
+ * 2. Redirect detection
139
+ * 3. Version mismatch
140
+ * 4. Response route + content negotiation
141
+ * 5. Mode detection from headers/params
142
+ */
143
+ export async function classifyRequest<TEnv = any>(
144
+ request: Request,
145
+ url: URL,
146
+ deps: ClassifyRequestDeps<TEnv>,
147
+ ): Promise<RequestPlan<TEnv>> {
148
+ const pathname = url.pathname;
149
+ const isAction =
150
+ request.headers.has("rsc-action") || url.searchParams.has("_rsc_action");
151
+
152
+ // Version mismatch — runs BEFORE route resolution so stale clients
153
+ // requesting removed routes get a reload, not a 404.
154
+ const clientVersion = url.searchParams.get("_rsc_v");
155
+ if (
156
+ deps.routerVersion &&
157
+ clientVersion &&
158
+ clientVersion !== deps.routerVersion
159
+ ) {
160
+ // Strip internal _rsc_* params so the browser reloads to a clean URL
161
+ let reloadUrl = stripInternalParams(url).toString();
162
+ if (isAction) {
163
+ const referer = request.headers.get("referer");
164
+ if (referer) {
165
+ try {
166
+ const refererUrl = new URL(referer);
167
+ if (refererUrl.origin === url.origin) {
168
+ reloadUrl = referer;
169
+ }
170
+ } catch {
171
+ // Malformed referer, fall back to stripped url
172
+ }
173
+ }
174
+ }
175
+
176
+ return {
177
+ mode: "version-mismatch",
178
+ reloadUrl,
179
+ };
180
+ }
181
+
182
+ // No metricsStore — classification is a lightweight gating step.
183
+ // Route-matching and manifest-loading metrics belong in the match path
184
+ // (createMatchContextForFull/Partial) which runs the authoritative resolution.
185
+ const result = await resolveRoute<TEnv>(pathname, {
186
+ findMatch: deps.findMatch,
187
+ lite: true,
188
+ });
189
+
190
+ if (!result) {
191
+ throw new RouteNotFoundError(`No route matched for ${pathname}`, {
192
+ cause: { pathname, method: request.method },
193
+ });
194
+ }
195
+
196
+ // Redirect
197
+ if (result.type === "redirect") {
198
+ const snapshot: RouteSnapshot<TEnv> = {
199
+ matched: result as any,
200
+ manifestEntry: null as any,
201
+ entries: [],
202
+ routeKey: "",
203
+ localRouteName: "",
204
+ params: {},
205
+ routeMiddleware: [],
206
+ cacheScope: null,
207
+ isPassthrough: false,
208
+ };
209
+ return {
210
+ mode: "redirect",
211
+ route: snapshot,
212
+ redirectUrl: result.redirectTo + url.search,
213
+ };
214
+ }
215
+
216
+ const snapshot = result.snapshot;
217
+
218
+ // Response route — non-RSC short-circuit (JSON, streaming, etc.)
219
+ const responseResult = await classifyResponseRoute(
220
+ request,
221
+ pathname,
222
+ snapshot,
223
+ );
224
+ if (responseResult) {
225
+ return responseResult;
226
+ }
227
+
228
+ // Mode detection from request signals
229
+ const actionId =
230
+ request.headers.get("rsc-action") || url.searchParams.get("_rsc_action");
231
+ const isLoaderFetch = url.searchParams.has("_rsc_loader");
232
+
233
+ const hasVariants =
234
+ snapshot.matched.negotiateVariants &&
235
+ snapshot.matched.negotiateVariants.length > 0;
236
+ const negotiated = !!hasVariants;
237
+
238
+ if (isAction && actionId) {
239
+ return { mode: "action", route: snapshot, actionId, negotiated };
240
+ }
241
+
242
+ if (isLoaderFetch) {
243
+ return { mode: "loader", route: snapshot };
244
+ }
245
+
246
+ // PE detection: POST with form content-type, but not a server action
247
+ const contentType = request.headers.get("content-type") || "";
248
+ const isFormSubmission =
249
+ contentType.includes("multipart/form-data") ||
250
+ contentType.includes("application/x-www-form-urlencoded");
251
+ if (request.method === "POST" && !isAction && isFormSubmission) {
252
+ return { mode: "pe-render", route: snapshot };
253
+ }
254
+
255
+ // App switch: client's routerId doesn't match this router
256
+ const clientRouterId = url.searchParams.get("_rsc_rid");
257
+ const isAppSwitch = !!(clientRouterId && clientRouterId !== deps.routerId);
258
+ const isPartial = url.searchParams.has("_rsc_partial") && !isAppSwitch;
259
+
260
+ if (isPartial) {
261
+ return { mode: "partial-render", route: snapshot, negotiated };
262
+ }
263
+
264
+ return { mode: "full-render", route: snapshot, negotiated };
265
+ }
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // Content negotiation for response routes
269
+ // ---------------------------------------------------------------------------
270
+
271
+ /**
272
+ * Check if the route is a response route and perform content negotiation
273
+ * if negotiate variants exist. Returns a ResponseRoutePlan if the route
274
+ * is a response route, null otherwise (RSC route).
275
+ */
276
+ async function classifyResponseRoute<TEnv>(
277
+ request: Request,
278
+ pathname: string,
279
+ snapshot: RouteSnapshot<TEnv>,
280
+ ): Promise<ResponseRoutePlan<TEnv> | null> {
281
+ const { manifestEntry, responseType } = snapshot;
282
+
283
+ const negotiation = await negotiateRoute(request, pathname, snapshot);
284
+ if (negotiation) {
285
+ return {
286
+ mode: "response",
287
+ route: snapshot,
288
+ ...negotiation,
289
+ };
290
+ }
291
+
292
+ // Non-negotiated response route (no variants, or RSC won negotiation)
293
+ if (responseType) {
294
+ const handler =
295
+ manifestEntry.type === "route" ? manifestEntry.handler : undefined;
296
+ if (handler) {
297
+ return {
298
+ mode: "response",
299
+ route: snapshot,
300
+ handler,
301
+ responseType,
302
+ negotiated: false,
303
+ manifestEntry,
304
+ routeMiddleware: snapshot.routeMiddleware,
305
+ };
306
+ }
307
+ }
308
+
309
+ return null;
310
+ }
@@ -59,6 +59,14 @@ interface EvaluateRevalidationOptions<TEnv> {
59
59
  stale?: boolean;
60
60
  /** Trace source hint for the revalidation trace */
61
61
  traceSource?: RevalidationTraceEntry["source"];
62
+ /**
63
+ * Override the segment-type-derived default. When set, the value is used as
64
+ * the seed `defaultShouldRevalidate` passed to user revalidate fns and the
65
+ * reason flows into the trace. Callers use this when client-knowledge
66
+ * (e.g. parallel slot not in clientSegmentIds) should dictate the seed
67
+ * instead of the params/method-based heuristic.
68
+ */
69
+ defaultOverride?: { value: boolean; reason: string };
62
70
  }
63
71
 
64
72
  /**
@@ -81,6 +89,7 @@ export async function evaluateRevalidation<TEnv>(
81
89
  actionContext,
82
90
  stale,
83
91
  traceSource,
92
+ defaultOverride,
84
93
  } = options;
85
94
  const nextParams = segment.params || {};
86
95
  const paramsChanged = !paramsEqual(nextParams, prevParams);
@@ -110,7 +119,12 @@ export async function evaluateRevalidation<TEnv>(
110
119
  let defaultShouldRevalidate: boolean;
111
120
  let defaultReason: string;
112
121
 
113
- if (request.method === "POST") {
122
+ if (defaultOverride) {
123
+ // Caller injected the seed (e.g. parallel slot not in clientSegmentIds).
124
+ // Skip the type-derived heuristic — caller knows better in this context.
125
+ defaultShouldRevalidate = defaultOverride.value;
126
+ defaultReason = defaultOverride.reason;
127
+ } else if (request.method === "POST") {
114
128
  // Actions: revalidate segments that belong to the route, skip parent chain
115
129
  if (segment.type === "route") {
116
130
  // Route segment always revalidates on actions
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Route Snapshot
3
+ *
4
+ * Pure data type representing the fully-resolved state of a single route match.
5
+ * Consolidates the duplicated findMatch + loadManifest + collectRouteMiddleware +
6
+ * cacheScope derivation that previously lived separately in preview-match.ts
7
+ * and match-api.ts.
8
+ *
9
+ * resolveRoute() is the factory: given a pathname and dependencies, it returns
10
+ * a RouteSnapshot (or redirect/null). Consumers (createMatchContextForFull,
11
+ * createMatchContextForPartial, previewMatch) read snapshot fields instead of
12
+ * re-deriving them.
13
+ */
14
+
15
+ import type { CacheScope } from "../cache/cache-scope.js";
16
+ import { createCacheScope } from "../cache/cache-scope.js";
17
+ import type { EntryData, MetricsStore } from "../server/context.js";
18
+ import { loadManifest } from "./manifest.js";
19
+ import { collectRouteMiddleware } from "./middleware.js";
20
+ import type { CollectedMiddleware } from "./middleware-types.js";
21
+ import { traverseBack } from "./pattern-matching.js";
22
+ import type { RouteMatchResult } from "./pattern-matching.js";
23
+
24
+ /**
25
+ * Immutable snapshot of a resolved route match.
26
+ *
27
+ * Contains everything derivable from (pathname, findMatch, loadManifest)
28
+ * without request context, navigation state, or intercept logic.
29
+ */
30
+ export interface RouteSnapshot<TEnv = any> {
31
+ /** Raw match result from the trie/pattern matcher */
32
+ matched: RouteMatchResult<TEnv>;
33
+ /** Resolved manifest entry (with loaded handler, loader, etc.) */
34
+ manifestEntry: EntryData;
35
+ /** All entries in the route chain (from traverseBack) */
36
+ entries: EntryData[];
37
+ /** Canonical route key (e.g. "blog.detail") */
38
+ routeKey: string;
39
+ /** Last segment of a dotted route key (e.g. "detail" from "blog.detail") */
40
+ localRouteName: string;
41
+ /** Extracted route params */
42
+ params: Record<string, string>;
43
+ /** Collected route-level middleware from the entry tree */
44
+ routeMiddleware: CollectedMiddleware[];
45
+ /** Merged cache scope from the entry chain */
46
+ cacheScope: CacheScope | null;
47
+ /** Whether the matched route is a passthrough route */
48
+ isPassthrough: boolean;
49
+ /** Response type for non-RSC routes (e.g. "application/json") */
50
+ responseType?: string;
51
+ }
52
+
53
+ export type ResolveRouteResult<TEnv = any> =
54
+ | { type: "match"; snapshot: RouteSnapshot<TEnv> }
55
+ | { type: "redirect"; redirectTo: string }
56
+ | null;
57
+
58
+ export interface ResolveRouteDeps<TEnv = any> {
59
+ findMatch: (pathname: string) => RouteMatchResult<TEnv> | null;
60
+ metricsStore?: MetricsStore;
61
+ isSSR?: boolean;
62
+ /**
63
+ * When true, skip entries array and cacheScope chain construction.
64
+ * Used by previewMatch which only needs matched, manifestEntry,
65
+ * routeMiddleware, and responseType — avoids an extra traverseBack
66
+ * allocation and cacheScope composition on the hot classification path.
67
+ */
68
+ lite?: boolean;
69
+ /**
70
+ * When true, skip pushing the "route-matching" metric internally.
71
+ * Used by createMatchContextForPartial on the fresh path (no snapshot
72
+ * reuse) so it can measure current + prev + intercept-source findMatch
73
+ * calls under one combined "route-matching" metric. On the reuse path,
74
+ * the partial path emits "route-matching:nav" for the prev +
75
+ * intercept-source lookups only (current-route resolution was done
76
+ * during classification without metrics).
77
+ */
78
+ skipRouteMatchMetric?: boolean;
79
+ }
80
+
81
+ /**
82
+ * Resolve a pathname into a RouteSnapshot.
83
+ *
84
+ * This is the single source of truth for route derivation. It performs:
85
+ * 1. findMatch(pathname)
86
+ * 2. Redirect check
87
+ * 3. loadManifest
88
+ * 4. Passthrough detection
89
+ * 5. collectRouteMiddleware
90
+ * 6. Cache scope chain
91
+ * 7. responseType + localRouteName extraction
92
+ *
93
+ * Metrics timing is preserved identically to the previous inline code.
94
+ */
95
+ export async function resolveRoute<TEnv = any>(
96
+ pathname: string,
97
+ deps: ResolveRouteDeps<TEnv>,
98
+ ): Promise<ResolveRouteResult<TEnv>> {
99
+ const {
100
+ metricsStore,
101
+ isSSR = false,
102
+ lite = false,
103
+ skipRouteMatchMetric = false,
104
+ } = deps;
105
+
106
+ const routeMatchStart =
107
+ metricsStore && !skipRouteMatchMetric ? performance.now() : 0;
108
+ const matched = deps.findMatch(pathname);
109
+ if (metricsStore && !skipRouteMatchMetric) {
110
+ metricsStore.metrics.push({
111
+ label: "route-matching",
112
+ duration: performance.now() - routeMatchStart,
113
+ startTime: routeMatchStart - metricsStore.requestStart,
114
+ });
115
+ }
116
+
117
+ if (!matched) {
118
+ return null;
119
+ }
120
+
121
+ if (matched.redirectTo) {
122
+ return { type: "redirect", redirectTo: matched.redirectTo };
123
+ }
124
+
125
+ const manifestStart = metricsStore ? performance.now() : 0;
126
+ const manifestEntry = await loadManifest(
127
+ matched.entry,
128
+ matched.routeKey,
129
+ pathname,
130
+ metricsStore,
131
+ isSSR,
132
+ );
133
+ if (metricsStore) {
134
+ metricsStore.metrics.push({
135
+ label: "manifest-loading",
136
+ duration: performance.now() - manifestStart,
137
+ startTime: manifestStart - metricsStore.requestStart,
138
+ });
139
+ }
140
+
141
+ const isPassthrough =
142
+ manifestEntry.type === "route" && manifestEntry.isPassthrough === true;
143
+
144
+ let entries: EntryData[];
145
+ let cacheScope: CacheScope | null = null;
146
+ if (lite) {
147
+ entries = [];
148
+ } else {
149
+ ({ entries, cacheScope } = buildEntriesAndCacheScope(manifestEntry));
150
+ }
151
+
152
+ const routeMiddleware = collectRouteMiddleware(
153
+ lite ? traverseBack(manifestEntry) : entries,
154
+ matched.params,
155
+ );
156
+
157
+ const responseType =
158
+ matched.responseType ||
159
+ (manifestEntry.type === "route" ? manifestEntry.responseType : undefined);
160
+
161
+ const localRouteName = matched.routeKey.includes(".")
162
+ ? matched.routeKey.split(".").pop()!
163
+ : matched.routeKey;
164
+
165
+ return {
166
+ type: "match",
167
+ snapshot: {
168
+ matched,
169
+ manifestEntry,
170
+ entries,
171
+ routeKey: matched.routeKey,
172
+ localRouteName,
173
+ params: matched.params,
174
+ routeMiddleware,
175
+ cacheScope,
176
+ isPassthrough,
177
+ responseType,
178
+ },
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Fill in the entries and cacheScope fields on a lite snapshot.
184
+ *
185
+ * When classifyRequest produces a lite snapshot (entries=[], cacheScope=null),
186
+ * this function computes the missing fields from manifestEntry without
187
+ * re-running findMatch, loadManifest, or collectRouteMiddleware.
188
+ *
189
+ * If the snapshot already has entries, returns it as-is.
190
+ */
191
+ export function ensureFullRouteSnapshot<TEnv = any>(
192
+ snapshot: RouteSnapshot<TEnv>,
193
+ ): RouteSnapshot<TEnv> {
194
+ if (snapshot.entries.length > 0) {
195
+ return snapshot;
196
+ }
197
+
198
+ const { entries, cacheScope } = buildEntriesAndCacheScope(
199
+ snapshot.manifestEntry,
200
+ );
201
+ return { ...snapshot, entries, cacheScope };
202
+ }
203
+
204
+ /**
205
+ * Materialize the entry chain and derive the merged cache scope.
206
+ * Shared by resolveRoute (non-lite) and ensureFullRouteSnapshot.
207
+ */
208
+ function buildEntriesAndCacheScope(manifestEntry: EntryData): {
209
+ entries: EntryData[];
210
+ cacheScope: CacheScope | null;
211
+ } {
212
+ const entries = [...traverseBack(manifestEntry)];
213
+ let cacheScope: CacheScope | null = null;
214
+ for (const entry of entries) {
215
+ if (entry.cache) {
216
+ cacheScope = createCacheScope(entry.cache, cacheScope);
217
+ }
218
+ }
219
+ return { entries, cacheScope };
220
+ }
221
+
222
+ /**
223
+ * Test helper: create a RouteSnapshot with sensible defaults and overrides.
224
+ */
225
+ export function createRouteSnapshot<TEnv = any>(
226
+ overrides?: Partial<RouteSnapshot<TEnv>>,
227
+ ): RouteSnapshot<TEnv> {
228
+ return {
229
+ matched: {
230
+ entry: {} as any,
231
+ routeKey: "test",
232
+ params: {},
233
+ optionalParams: new Set(),
234
+ } as RouteMatchResult<TEnv>,
235
+ manifestEntry: { type: "route", shortCode: "R0", parent: null } as any,
236
+ entries: [],
237
+ routeKey: "test",
238
+ localRouteName: "test",
239
+ params: {},
240
+ routeMiddleware: [],
241
+ cacheScope: null,
242
+ isPassthrough: false,
243
+ ...overrides,
244
+ };
245
+ }
@@ -138,6 +138,7 @@ export interface RouterContext<TEnv = any> {
138
138
  interceptResult: InterceptResult | null,
139
139
  localRouteName: string,
140
140
  pathname: string,
141
+ stale?: boolean,
141
142
  ) => Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }>;
142
143
 
143
144
  // Generator-based segment resolution (for pipeline)
@@ -188,7 +189,10 @@ export interface RouterContext<TEnv = any> {
188
189
  | "cache-hit"
189
190
  | "loader"
190
191
  | "parallel"
191
- | "orphan-layout";
192
+ | "orphan-layout"
193
+ | "route-handler"
194
+ | "layout-handler"
195
+ | "intercept-loader";
192
196
  }) => Promise<boolean>;
193
197
 
194
198
  // Request context
@@ -206,6 +210,7 @@ export interface RouterContext<TEnv = any> {
206
210
  params: Record<string, string>,
207
211
  handlerContext: HandlerContext<any, TEnv>,
208
212
  loaderPromises: Map<string, Promise<any>>,
213
+ options?: { skipLoaders?: boolean },
209
214
  ) => Promise<ResolvedSegment[]>;
210
215
 
211
216
  // Generator-based simple resolution