@rangojs/router 0.0.0-experimental.62 → 0.0.0-experimental.64

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 (50) hide show
  1. package/README.md +61 -8
  2. package/dist/bin/rango.js +2 -1
  3. package/dist/vite/index.js +142 -62
  4. package/dist/vite/index.js.bak +5448 -0
  5. package/package.json +14 -15
  6. package/skills/prerender/SKILL.md +110 -68
  7. package/src/__internal.ts +1 -1
  8. package/src/build/generate-manifest.ts +3 -6
  9. package/src/build/route-types/scan-filter.ts +8 -1
  10. package/src/client.tsx +2 -56
  11. package/src/index.rsc.ts +3 -1
  12. package/src/index.ts +8 -0
  13. package/src/prerender/store.ts +5 -4
  14. package/src/prerender.ts +138 -77
  15. package/src/route-definition/dsl-helpers.ts +42 -19
  16. package/src/route-definition/helpers-types.ts +4 -1
  17. package/src/route-definition/index.ts +3 -0
  18. package/src/route-definition/resolve-handler-use.ts +149 -0
  19. package/src/route-types.ts +11 -0
  20. package/src/router/content-negotiation.ts +100 -1
  21. package/src/router/handler-context.ts +20 -5
  22. package/src/router/match-api.ts +124 -189
  23. package/src/router/match-middleware/cache-lookup.ts +2 -6
  24. package/src/router/navigation-snapshot.ts +182 -0
  25. package/src/router/prerender-match.ts +104 -8
  26. package/src/router/preview-match.ts +30 -102
  27. package/src/router/request-classification.ts +310 -0
  28. package/src/router/route-snapshot.ts +245 -0
  29. package/src/router/router-interfaces.ts +11 -0
  30. package/src/router/segment-resolution/fresh.ts +44 -2
  31. package/src/router/segment-resolution/revalidation.ts +53 -5
  32. package/src/router.ts +13 -1
  33. package/src/rsc/handler.ts +456 -373
  34. package/src/rsc/ssr-setup.ts +1 -1
  35. package/src/server/context.ts +5 -1
  36. package/src/server/request-context.ts +7 -0
  37. package/src/static-handler.ts +18 -6
  38. package/src/types/handler-context.ts +12 -2
  39. package/src/types/route-entry.ts +1 -1
  40. package/src/urls/path-helper-types.ts +9 -2
  41. package/src/urls/path-helper.ts +47 -12
  42. package/src/urls/response-types.ts +16 -6
  43. package/src/use-loader.tsx +73 -4
  44. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  45. package/src/vite/discovery/prerender-collection.ts +14 -1
  46. package/src/vite/discovery/state.ts +13 -4
  47. package/src/vite/index.ts +4 -0
  48. package/src/vite/plugin-types.ts +60 -5
  49. package/src/vite/rango.ts +2 -1
  50. package/src/vite/router-discovery.ts +153 -34
@@ -2,10 +2,18 @@
2
2
  * Content Negotiation Utilities
3
3
  *
4
4
  * Pure functions for HTTP Accept header parsing and response type matching.
5
- * Used by createRouter's previewMatch for content negotiation between
5
+ * Used by previewMatch and classifyRequest for content negotiation between
6
6
  * RSC routes and response routes (JSON, text, image, stream, etc.).
7
7
  */
8
8
 
9
+ import type { EntryData } from "../server/context.js";
10
+ import type { CollectedMiddleware } from "./middleware-types.js";
11
+ import { collectRouteMiddleware } from "./middleware.js";
12
+ import { loadManifest } from "./manifest.js";
13
+ import { traverseBack } from "./pattern-matching.js";
14
+ import type { RouteMatchResult } from "./pattern-matching.js";
15
+ import type { RouteSnapshot } from "./route-snapshot.js";
16
+
9
17
  // Response type -> MIME type used for Accept header matching
10
18
  export const RESPONSE_TYPE_MIME: Record<string, string> = {
11
19
  json: "application/json",
@@ -114,3 +122,94 @@ export function pickNegotiateVariant(
114
122
  // No match -- use first candidate as default
115
123
  return candidates[0]!;
116
124
  }
125
+
126
+ /**
127
+ * Result of content negotiation for a route with negotiate variants.
128
+ */
129
+ export interface NegotiationResult {
130
+ /** The winning response type */
131
+ responseType: string;
132
+ /** Handler function for the winning variant */
133
+ handler: Function;
134
+ /** Manifest entry for the winning variant (may differ from primary) */
135
+ manifestEntry: EntryData;
136
+ /** Route middleware for the winning variant */
137
+ routeMiddleware: CollectedMiddleware[];
138
+ /** Always true — negotiation occurred */
139
+ negotiated: true;
140
+ }
141
+
142
+ /**
143
+ * Perform content negotiation for a route with negotiate variants.
144
+ *
145
+ * Returns a NegotiationResult when a response route wins negotiation.
146
+ * Returns null when RSC wins or no negotiation is needed.
147
+ *
148
+ * Shared by previewMatch and classifyRequest to avoid duplicating
149
+ * the candidate-building and variant-loading logic.
150
+ */
151
+ export async function negotiateRoute(
152
+ request: Request,
153
+ pathname: string,
154
+ snapshot: RouteSnapshot,
155
+ ): Promise<NegotiationResult | null> {
156
+ const { matched, manifestEntry, routeMiddleware, responseType } = snapshot;
157
+ if (!matched.negotiateVariants || matched.negotiateVariants.length === 0) {
158
+ return null;
159
+ }
160
+
161
+ const acceptEntries = parseAcceptTypes(request.headers.get("accept") || "");
162
+
163
+ // Build candidate list preserving definition order.
164
+ const variants = matched.negotiateVariants;
165
+ let candidates: Array<{ routeKey: string; responseType: string }>;
166
+ if (responseType) {
167
+ candidates = [...variants, { routeKey: matched.routeKey, responseType }];
168
+ } else {
169
+ const rscCandidate = {
170
+ routeKey: matched.routeKey,
171
+ responseType: RSC_RESPONSE_TYPE,
172
+ };
173
+ candidates = matched.rscFirst
174
+ ? [rscCandidate, ...variants]
175
+ : [...variants, rscCandidate];
176
+ }
177
+
178
+ const variant = pickNegotiateVariant(acceptEntries, candidates);
179
+
180
+ // RSC won negotiation
181
+ if (variant.responseType === RSC_RESPONSE_TYPE) {
182
+ return null;
183
+ }
184
+
185
+ // Primary response-type won — use existing manifest entry and middleware
186
+ if (responseType && variant.routeKey === matched.routeKey) {
187
+ return {
188
+ responseType,
189
+ handler: manifestEntry.handler as Function,
190
+ manifestEntry,
191
+ routeMiddleware,
192
+ negotiated: true,
193
+ };
194
+ }
195
+
196
+ // Different variant won — load its manifest entry
197
+ const negotiateEntry = await loadManifest(
198
+ matched.entry,
199
+ variant.routeKey,
200
+ pathname,
201
+ undefined,
202
+ false,
203
+ );
204
+ const variantMiddleware = collectRouteMiddleware(
205
+ traverseBack(negotiateEntry),
206
+ matched.params,
207
+ );
208
+ return {
209
+ responseType: variant.responseType,
210
+ handler: negotiateEntry.handler as Function,
211
+ manifestEntry: negotiateEntry,
212
+ routeMiddleware: variantMiddleware,
213
+ negotiated: true,
214
+ };
215
+ }
@@ -114,9 +114,9 @@ function createPrerenderPassthroughFn(
114
114
  }
115
115
  if (!isPassthroughRoute) {
116
116
  throw new Error(
117
- "ctx.passthrough() is only available on routes declared with " +
118
- "{ passthrough: true }. Remove the passthrough() call or add " +
119
- "{ passthrough: true } to the Prerender options.",
117
+ "ctx.passthrough() is only available on routes wrapped with " +
118
+ "Passthrough(). Remove the passthrough() call or wrap the " +
119
+ "Prerender definition with Passthrough(prerenderDef, liveHandler).",
120
120
  );
121
121
  }
122
122
  return PRERENDER_PASSTHROUGH;
@@ -272,6 +272,7 @@ export function createHandlerContext<TEnv>(
272
272
  ctx = {
273
273
  params,
274
274
  build: false,
275
+ dev: false,
275
276
  request,
276
277
  searchParams,
277
278
  search: searchSchema ? resolvedSearchParams : {},
@@ -351,6 +352,8 @@ export function createPrerenderContext<TEnv>(
351
352
  routeName?: string,
352
353
  buildVars?: Record<string, any>,
353
354
  isPassthroughRoute?: boolean,
355
+ buildEnv?: TEnv,
356
+ devMode?: boolean,
354
357
  ): InternalHandlerContext<any, TEnv> {
355
358
  const syntheticUrl = new URL(`http://prerender${pathname}`);
356
359
  const variables = buildVars ?? {};
@@ -365,6 +368,7 @@ export function createPrerenderContext<TEnv>(
365
368
  return {
366
369
  params,
367
370
  build: true,
371
+ dev: devMode ?? false,
368
372
  get request(): Request {
369
373
  return throwUnavailable("request");
370
374
  },
@@ -374,7 +378,11 @@ export function createPrerenderContext<TEnv>(
374
378
  url: syntheticUrl,
375
379
  originalUrl: syntheticUrl,
376
380
  get env(): TEnv {
377
- return throwUnavailable("env");
381
+ if (buildEnv !== undefined) return buildEnv;
382
+ throw new Error(
383
+ "ctx.env is not available during pre-rendering. " +
384
+ "Configure buildEnv in your rango() plugin options to enable build-time env access.",
385
+ );
378
386
  },
379
387
  _variables: variables,
380
388
  get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
@@ -422,6 +430,8 @@ export function createPrerenderContext<TEnv>(
422
430
  export function createStaticContext<TEnv>(
423
431
  routeMap: Record<string, string>,
424
432
  routeName?: string,
433
+ buildEnv?: TEnv,
434
+ devMode?: boolean,
425
435
  ): InternalHandlerContext<any, TEnv> {
426
436
  const variables: Record<string, any> = {};
427
437
 
@@ -437,6 +447,7 @@ export function createStaticContext<TEnv>(
437
447
  return throwUnavailable("params");
438
448
  },
439
449
  build: true,
450
+ dev: devMode ?? false,
440
451
  get request(): Request {
441
452
  return throwUnavailable("request");
442
453
  },
@@ -456,7 +467,11 @@ export function createStaticContext<TEnv>(
456
467
  return throwUnavailable("originalUrl");
457
468
  },
458
469
  get env(): TEnv {
459
- return throwUnavailable("env");
470
+ if (buildEnv !== undefined) return buildEnv;
471
+ throw new Error(
472
+ "ctx.env is not available in Static() handlers. " +
473
+ "Configure buildEnv in your rango() plugin options to enable build-time env access.",
474
+ );
460
475
  },
461
476
  _variables: variables,
462
477
  get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
@@ -36,7 +36,14 @@ import {
36
36
  setRequestContextPrevRouteKey,
37
37
  } from "../server/request-context.js";
38
38
  import { isAutoGeneratedRouteName } from "../route-name.js";
39
+ import type { DefaultRouteName } from "../types/global-namespace.js";
39
40
  import { debugLog, debugWarn } from "./logging.js";
41
+ import {
42
+ resolveRoute,
43
+ ensureFullRouteSnapshot,
44
+ type RouteSnapshot,
45
+ } from "./route-snapshot.js";
46
+ import { resolveNavigation } from "./navigation-snapshot.js";
40
47
 
41
48
  /**
42
49
  * Create match context for full requests (document/SSR).
@@ -52,57 +59,36 @@ export async function createMatchContextForFull<TEnv>(
52
59
 
53
60
  const metricsStore = deps.getMetricsStore();
54
61
 
55
- const routeMatchStart = metricsStore ? performance.now() : 0;
56
- const matched = deps.findMatch(pathname, metricsStore);
57
- if (metricsStore) {
58
- metricsStore.metrics.push({
59
- label: "route-matching",
60
- duration: performance.now() - routeMatchStart,
61
- startTime: routeMatchStart - metricsStore.requestStart,
62
- });
63
- }
62
+ // Full renders always resolve fresh with isSSR: true because loadManifest
63
+ // keys its cache on isSSR and stamps Store.isSSR for downstream behavior.
64
+ const result = await resolveRoute<TEnv>(pathname, {
65
+ findMatch: (p) => deps.findMatch(p, metricsStore),
66
+ metricsStore,
67
+ isSSR: true,
68
+ });
64
69
 
65
- if (!matched) {
70
+ if (!result) {
66
71
  throw new RouteNotFoundError(`No route matched for ${pathname}`, {
67
72
  cause: { pathname, method: request.method },
68
73
  });
69
74
  }
70
75
 
71
- if (matched.redirectTo) {
76
+ if (result.type === "redirect") {
72
77
  return {
73
78
  type: "redirect",
74
- redirectUrl: matched.redirectTo + url.search,
79
+ redirectUrl: result.redirectTo + url.search,
75
80
  };
76
81
  }
77
82
 
78
- const manifestStart = metricsStore ? performance.now() : 0;
79
- const manifestEntry = await loadManifest(
80
- matched.entry,
81
- matched.routeKey,
82
- pathname,
83
- metricsStore,
84
- true,
85
- );
86
- if (metricsStore) {
87
- metricsStore.metrics.push({
88
- label: "manifest-loading",
89
- duration: performance.now() - manifestStart,
90
- startTime: manifestStart - metricsStore.requestStart,
91
- });
92
- }
83
+ const snapshot = result.snapshot;
93
84
 
94
- if (
95
- manifestEntry.type === "route" &&
96
- manifestEntry.prerenderDef?.options?.passthrough === true
97
- ) {
85
+ const { matched } = snapshot;
86
+
87
+ // Backward compat: downstream middleware reads matched.pt
88
+ if (snapshot.isPassthrough) {
98
89
  matched.pt = true;
99
90
  }
100
91
 
101
- const routeMiddleware = collectRouteMiddleware(
102
- traverseBack(manifestEntry),
103
- matched.params,
104
- );
105
-
106
92
  // Clean URL without internal _rsc* params for userland access
107
93
  const cleanUrl = stripInternalParams(url);
108
94
 
@@ -134,14 +120,6 @@ export async function createMatchContextForFull<TEnv>(
134
120
  Store.metrics = metricsStore;
135
121
  }
136
122
 
137
- const entries = [...traverseBack(manifestEntry)];
138
- let cacheScope: CacheScope | null = null;
139
- for (const entry of entries) {
140
- if (entry.cache) {
141
- cacheScope = createCacheScope(entry.cache, cacheScope);
142
- }
143
- }
144
-
145
123
  return {
146
124
  request,
147
125
  url: cleanUrl,
@@ -154,12 +132,10 @@ export async function createMatchContextForFull<TEnv>(
154
132
  prevParams: {},
155
133
  prevMatch: null,
156
134
  matched,
157
- manifestEntry,
158
- entries,
135
+ manifestEntry: snapshot.manifestEntry,
136
+ entries: snapshot.entries,
159
137
  routeKey: matched.routeKey,
160
- localRouteName: matched.routeKey.includes(".")
161
- ? matched.routeKey.split(".").pop()!
162
- : matched.routeKey,
138
+ localRouteName: snapshot.localRouteName,
163
139
  handlerContext,
164
140
  loaderPromises,
165
141
  routeMap: deps.getRouteMap(),
@@ -175,16 +151,16 @@ export async function createMatchContextForFull<TEnv>(
175
151
  segments: { path: [], ids: [] },
176
152
  toRouteName:
177
153
  matched.routeKey && !isAutoGeneratedRouteName(matched.routeKey)
178
- ? matched.routeKey
154
+ ? (matched.routeKey as DefaultRouteName)
179
155
  : undefined,
180
156
  },
181
157
  isSameRouteNavigation: false,
182
158
  interceptResult: null,
183
- cacheScope,
159
+ cacheScope: snapshot.cacheScope,
184
160
  isIntercept: false,
185
161
  actionContext: undefined,
186
162
  isAction: false,
187
- routeMiddleware,
163
+ routeMiddleware: snapshot.routeMiddleware,
188
164
  isFullMatch: true,
189
165
  };
190
166
  }
@@ -204,103 +180,85 @@ export async function createMatchContextForPartial<TEnv>(
204
180
 
205
181
  const metricsStore = deps.getMetricsStore();
206
182
 
207
- const clientSegmentIds =
208
- url.searchParams.get("_rsc_segments")?.split(",").filter(Boolean) || [];
209
- const stale = url.searchParams.get("_rsc_stale") === "true";
210
- const previousUrl =
211
- request.headers.get("X-RSC-Router-Client-Path") ||
212
- request.headers.get("Referer");
213
- const interceptSourceUrl = request.headers.get(
214
- "X-RSC-Router-Intercept-Source",
215
- );
183
+ const isHmr = !!request.headers.get("X-RSC-HMR");
216
184
 
217
185
  // HMR: clear manifest cache so stale handler references are discarded
218
- if (request.headers.get("X-RSC-HMR")) {
186
+ if (isHmr) {
219
187
  clearManifestCache();
220
188
  }
221
189
 
222
- if (!previousUrl) {
223
- return null;
224
- }
225
-
226
- let prevUrl: URL;
227
- try {
228
- prevUrl = new URL(previousUrl, url.origin);
229
- } catch {
230
- return null;
231
- }
190
+ // Reuse the classified snapshot when available and not invalidated by HMR.
191
+ // classifyRequest already called resolveRoute(lite) with isSSR=false, which
192
+ // matches the partial path. On HMR, discard to pick up manifest changes.
193
+ const classifiedRoute = isHmr
194
+ ? undefined
195
+ : getRequestContext()?._classifiedRoute;
196
+
197
+ // Time route matching. On the reuse path, only nav findMatch calls are new
198
+ // (current-route findMatch and manifest-loading were already timed during
199
+ // classifyRequest via its metricsStore). On the fresh path, all findMatch
200
+ // calls are measured together.
201
+ const routeMatchStart = metricsStore ? performance.now() : 0;
232
202
 
233
- let interceptContextUrl: URL;
234
- try {
235
- interceptContextUrl = interceptSourceUrl
236
- ? new URL(interceptSourceUrl, url.origin)
237
- : prevUrl;
238
- } catch {
239
- interceptContextUrl = prevUrl;
240
- }
203
+ let snapshot: RouteSnapshot<TEnv>;
204
+ if (classifiedRoute && classifiedRoute.manifestEntry) {
205
+ snapshot = ensureFullRouteSnapshot(classifiedRoute);
206
+ } else {
207
+ const result = await resolveRoute<TEnv>(pathname, {
208
+ findMatch: (p) => deps.findMatch(p, metricsStore),
209
+ metricsStore,
210
+ isSSR: false,
211
+ skipRouteMatchMetric: true,
212
+ });
241
213
 
242
- const routeMatchStart = metricsStore ? performance.now() : 0;
243
- const prevMatch = deps.findMatch(prevUrl.pathname);
244
- const prevParams = prevMatch?.params || {};
245
- const interceptContextMatch = interceptSourceUrl
246
- ? deps.findMatch(interceptContextUrl.pathname)
247
- : prevMatch;
214
+ if (!result) {
215
+ throw new RouteNotFoundError(`No route matched for ${pathname}`, {
216
+ cause: { pathname, method: request.method },
217
+ });
218
+ }
248
219
 
249
- const matched = deps.findMatch(pathname, metricsStore);
220
+ if (result.type === "redirect") {
221
+ return null;
222
+ }
250
223
 
251
- if (metricsStore) {
252
- metricsStore.metrics.push({
253
- label: "route-matching",
254
- duration: performance.now() - routeMatchStart,
255
- startTime: routeMatchStart - metricsStore.requestStart,
256
- });
224
+ snapshot = result.snapshot;
257
225
  }
258
226
 
259
- if (!matched) {
260
- throw new RouteNotFoundError(`No route matched for ${pathname}`, {
261
- cause: { pathname, method: request.method, previousUrl },
262
- });
263
- }
227
+ const { matched } = snapshot;
264
228
 
265
- if (matched.redirectTo) {
266
- return null;
229
+ // Backward compat: downstream middleware reads matched.pt
230
+ if (snapshot.isPassthrough) {
231
+ matched.pt = true;
267
232
  }
268
233
 
269
- if (prevMatch && prevMatch.entry !== matched.entry && !matched.pr) {
270
- debugLog("matchPartial", "route group changed", {
271
- from: prevMatch.routeKey,
272
- to: matched.routeKey,
273
- });
234
+ // Navigation state (prev + intercept-source findMatch calls)
235
+ const nav = resolveNavigation(request, url, matched.routeKey, {
236
+ findMatch: deps.findMatch,
237
+ });
238
+ if (!nav) {
239
+ return null;
274
240
  }
275
241
 
276
- const manifestStart = metricsStore ? performance.now() : 0;
277
- const manifestEntry = await loadManifest(
278
- matched.entry,
279
- matched.routeKey,
280
- pathname,
281
- metricsStore,
282
- false,
283
- );
242
+ // Push route-matching metric. On the fresh path this covers all findMatch
243
+ // calls (current + prev + intercept-source). On the reuse path, current-route
244
+ // findMatch was already timed during classification, so this only covers
245
+ // the nav lookups (prev + intercept-source).
284
246
  if (metricsStore) {
247
+ const isReuse = !!classifiedRoute;
285
248
  metricsStore.metrics.push({
286
- label: "manifest-loading",
287
- duration: performance.now() - manifestStart,
288
- startTime: manifestStart - metricsStore.requestStart,
249
+ label: isReuse ? "route-matching:nav" : "route-matching",
250
+ duration: performance.now() - routeMatchStart,
251
+ startTime: routeMatchStart - metricsStore.requestStart,
289
252
  });
290
253
  }
291
254
 
292
- if (
293
- manifestEntry.type === "route" &&
294
- manifestEntry.prerenderDef?.options?.passthrough === true
295
- ) {
296
- matched.pt = true;
255
+ if (nav.prevMatch && nav.prevMatch.entry !== matched.entry && !matched.pr) {
256
+ debugLog("matchPartial", "route group changed", {
257
+ from: nav.prevMatch.routeKey,
258
+ to: matched.routeKey,
259
+ });
297
260
  }
298
261
 
299
- const routeMiddleware = collectRouteMiddleware(
300
- traverseBack(manifestEntry),
301
- matched.params,
302
- );
303
-
304
262
  // Clean URL without internal _rsc* params for userland access
305
263
  const cleanUrl = stripInternalParams(url);
306
264
 
@@ -317,9 +275,8 @@ export async function createMatchContextForPartial<TEnv>(
317
275
  matched.pt === true,
318
276
  );
319
277
 
320
- const clientSegmentSet = new Set(clientSegmentIds);
321
278
  debugLog("matchPartial", "client segments", {
322
- segments: Array.from(clientSegmentSet),
279
+ segments: Array.from(nav.clientSegmentSet),
323
280
  });
324
281
 
325
282
  const loaderPromises = new Map<string, Promise<any>>();
@@ -337,100 +294,78 @@ export async function createMatchContextForPartial<TEnv>(
337
294
  Store.metrics = metricsStore;
338
295
  }
339
296
 
340
- const isSameRouteNavigation = !!(
341
- interceptContextMatch && interceptContextMatch.routeKey === matched.routeKey
342
- );
343
-
344
- if (interceptSourceUrl) {
297
+ if (nav.hasInterceptSource) {
345
298
  debugLog("matchPartial.intercept", "intercept context detected", {
346
299
  currentUrl: pathname,
347
- interceptSource: interceptSourceUrl,
348
- contextRoute: interceptContextMatch?.routeKey,
300
+ interceptSource: nav.interceptContextUrl.href,
301
+ contextRoute: nav.interceptContextMatch?.routeKey,
349
302
  currentRoute: matched.routeKey,
350
- sameRouteNavigation: isSameRouteNavigation,
303
+ sameRouteNavigation: nav.isSameRouteNavigation,
351
304
  });
352
305
  }
353
306
 
354
- const localRouteName = matched.routeKey.includes(".")
355
- ? matched.routeKey.split(".").pop()!
356
- : matched.routeKey;
357
-
358
- const filteredSegmentIds = clientSegmentIds.filter((id) => {
359
- if (id.includes(".@")) return false;
360
- if (/D\d+\./.test(id)) return false;
361
- return true;
362
- });
363
- const effectiveFromUrl = interceptSourceUrl ? interceptContextUrl : prevUrl;
364
- const effectiveFromMatch = interceptSourceUrl
365
- ? interceptContextMatch
366
- : prevMatch;
367
-
368
307
  // Store previous route key on the request context for revalidation
369
308
  // fromRouteName. Uses effectiveFromMatch so intercept-source navigations
370
309
  // see the intercept origin route, not the plain previous URL route.
371
- setRequestContextPrevRouteKey(effectiveFromMatch?.routeKey);
310
+ setRequestContextPrevRouteKey(nav.effectiveFromMatch?.routeKey);
372
311
 
373
312
  const interceptSelectorContext: InterceptSelectorContext = {
374
- from: effectiveFromUrl,
313
+ from: nav.effectiveFromUrl,
375
314
  to: cleanUrl,
376
315
  params: matched.params,
377
316
  request,
378
317
  env,
379
318
  segments: {
380
- path: effectiveFromUrl.pathname.split("/").filter(Boolean),
381
- ids: filteredSegmentIds,
319
+ path: nav.effectiveFromUrl.pathname.split("/").filter(Boolean),
320
+ ids: nav.filteredSegmentIds,
382
321
  },
383
322
  fromRouteName:
384
- effectiveFromMatch?.routeKey &&
385
- !isAutoGeneratedRouteName(effectiveFromMatch.routeKey)
386
- ? effectiveFromMatch.routeKey
323
+ nav.effectiveFromMatch?.routeKey &&
324
+ !isAutoGeneratedRouteName(nav.effectiveFromMatch.routeKey)
325
+ ? (nav.effectiveFromMatch.routeKey as DefaultRouteName)
387
326
  : undefined,
388
327
  toRouteName:
389
328
  matched.routeKey && !isAutoGeneratedRouteName(matched.routeKey)
390
- ? matched.routeKey
329
+ ? (matched.routeKey as DefaultRouteName)
391
330
  : undefined,
392
331
  };
393
332
  const isAction = !!actionContext;
394
333
 
395
- const clientHasInterceptSegments = [...clientSegmentSet].some((id) =>
334
+ const clientHasInterceptSegments = [...nav.clientSegmentSet].some((id) =>
396
335
  id.includes(".@"),
397
336
  );
398
337
  const skipInterceptForAction = isAction && !clientHasInterceptSegments;
399
338
  const interceptResult =
400
- isSameRouteNavigation || skipInterceptForAction
339
+ nav.isSameRouteNavigation || skipInterceptForAction
401
340
  ? null
402
341
  : findInterceptForRoute(
403
342
  matched.routeKey,
404
- manifestEntry.parent,
343
+ snapshot.manifestEntry.parent,
405
344
  interceptSelectorContext,
406
345
  isAction,
407
346
  ) ||
408
- (localRouteName !== matched.routeKey
347
+ (snapshot.localRouteName !== matched.routeKey
409
348
  ? findInterceptForRoute(
410
- localRouteName,
411
- manifestEntry.parent,
349
+ snapshot.localRouteName,
350
+ snapshot.manifestEntry.parent,
412
351
  interceptSelectorContext,
413
352
  isAction,
414
353
  )
415
354
  : null);
416
355
 
356
+ // Make a mutable copy of clientSegmentSet so we can delete entries
357
+ // for same-route navigation forcing
358
+ const clientSegmentSet = new Set(nav.clientSegmentSet);
359
+
417
360
  if (
418
- isSameRouteNavigation &&
419
- manifestEntry.type === "route" &&
420
- interceptSourceUrl
361
+ nav.isSameRouteNavigation &&
362
+ snapshot.manifestEntry.type === "route" &&
363
+ nav.hasInterceptSource
421
364
  ) {
422
365
  debugLog("matchPartial.intercept", "forcing route segment render", {
423
- segmentId: manifestEntry.shortCode,
366
+ segmentId: snapshot.manifestEntry.shortCode,
424
367
  });
425
- clientSegmentSet.delete(manifestEntry.shortCode);
426
- }
427
-
428
- const entries = [...traverseBack(manifestEntry)];
429
- let cacheScope: CacheScope | null = null;
430
- for (const entry of entries) {
431
- if (entry.cache) {
432
- cacheScope = createCacheScope(entry.cache, cacheScope);
433
- }
368
+ clientSegmentSet.delete(snapshot.manifestEntry.shortCode);
434
369
  }
435
370
 
436
371
  const isIntercept = !!interceptResult;
@@ -440,31 +375,31 @@ export async function createMatchContextForPartial<TEnv>(
440
375
  url: cleanUrl,
441
376
  pathname,
442
377
  env,
443
- clientSegmentIds,
378
+ clientSegmentIds: nav.clientSegmentIds,
444
379
  clientSegmentSet,
445
- stale,
446
- prevUrl,
447
- prevParams,
448
- prevMatch,
380
+ stale: nav.stale,
381
+ prevUrl: nav.prevUrl,
382
+ prevParams: nav.prevParams,
383
+ prevMatch: nav.prevMatch,
449
384
  matched,
450
- manifestEntry,
451
- entries,
385
+ manifestEntry: snapshot.manifestEntry,
386
+ entries: snapshot.entries,
452
387
  routeKey: matched.routeKey,
453
- localRouteName,
388
+ localRouteName: snapshot.localRouteName,
454
389
  handlerContext,
455
390
  loaderPromises,
456
391
  routeMap: deps.getRouteMap(),
457
392
  metricsStore,
458
393
  Store,
459
- interceptContextMatch,
394
+ interceptContextMatch: nav.interceptContextMatch,
460
395
  interceptSelectorContext,
461
- isSameRouteNavigation,
396
+ isSameRouteNavigation: nav.isSameRouteNavigation,
462
397
  interceptResult,
463
- cacheScope,
398
+ cacheScope: snapshot.cacheScope,
464
399
  isIntercept,
465
400
  actionContext,
466
401
  isAction,
467
- routeMiddleware,
402
+ routeMiddleware: snapshot.routeMiddleware,
468
403
  isFullMatch: false,
469
404
  };
470
405
  }