@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.
- package/README.md +61 -8
- package/dist/bin/rango.js +2 -1
- package/dist/vite/index.js +142 -62
- package/dist/vite/index.js.bak +5448 -0
- package/package.json +14 -15
- package/skills/prerender/SKILL.md +110 -68
- package/src/__internal.ts +1 -1
- package/src/build/generate-manifest.ts +3 -6
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/client.tsx +2 -56
- package/src/index.rsc.ts +3 -1
- package/src/index.ts +8 -0
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +138 -77
- package/src/route-definition/dsl-helpers.ts +42 -19
- package/src/route-definition/helpers-types.ts +4 -1
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-types.ts +11 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/handler-context.ts +20 -5
- package/src/router/match-api.ts +124 -189
- package/src/router/match-middleware/cache-lookup.ts +2 -6
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/prerender-match.ts +104 -8
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-interfaces.ts +11 -0
- package/src/router/segment-resolution/fresh.ts +44 -2
- package/src/router/segment-resolution/revalidation.ts +53 -5
- package/src/router.ts +13 -1
- package/src/rsc/handler.ts +456 -373
- package/src/rsc/ssr-setup.ts +1 -1
- package/src/server/context.ts +5 -1
- package/src/server/request-context.ts +7 -0
- package/src/static-handler.ts +18 -6
- package/src/types/handler-context.ts +12 -2
- package/src/types/route-entry.ts +1 -1
- package/src/urls/path-helper-types.ts +9 -2
- package/src/urls/path-helper.ts +47 -12
- package/src/urls/response-types.ts +16 -6
- package/src/use-loader.tsx +73 -4
- package/src/vite/discovery/bundle-postprocess.ts +30 -33
- package/src/vite/discovery/prerender-collection.ts +14 -1
- package/src/vite/discovery/state.ts +13 -4
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +60 -5
- package/src/vite/rango.ts +2 -1
- 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
|
|
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
|
|
118
|
-
"
|
|
119
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
package/src/router/match-api.ts
CHANGED
|
@@ -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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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 (!
|
|
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 (
|
|
76
|
+
if (result.type === "redirect") {
|
|
72
77
|
return {
|
|
73
78
|
type: "redirect",
|
|
74
|
-
redirectUrl:
|
|
79
|
+
redirectUrl: result.redirectTo + url.search,
|
|
75
80
|
};
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
const
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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:
|
|
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
|
|
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 (
|
|
186
|
+
if (isHmr) {
|
|
219
187
|
clearManifestCache();
|
|
220
188
|
}
|
|
221
189
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
220
|
+
if (result.type === "redirect") {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
250
223
|
|
|
251
|
-
|
|
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
|
-
|
|
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
|
-
|
|
266
|
-
|
|
229
|
+
// Backward compat: downstream middleware reads matched.pt
|
|
230
|
+
if (snapshot.isPassthrough) {
|
|
231
|
+
matched.pt = true;
|
|
267
232
|
}
|
|
268
233
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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: "
|
|
287
|
-
duration: performance.now() -
|
|
288
|
-
startTime:
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
}
|