@rangojs/router 0.0.0-experimental.63 → 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/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/src/client.tsx +2 -56
- package/src/route-definition/dsl-helpers.ts +5 -1
- package/src/route-definition/helpers-types.ts +4 -1
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/match-api.ts +124 -183
- package/src/router/navigation-snapshot.ts +182 -0
- 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 +7 -0
- package/src/router/segment-resolution/fresh.ts +37 -0
- package/src/router/segment-resolution/revalidation.ts +43 -0
- package/src/router.ts +4 -0
- package/src/rsc/handler.ts +456 -373
- package/src/rsc/ssr-setup.ts +1 -1
- package/src/server/request-context.ts +7 -0
- package/src/urls/path-helper-types.ts +4 -1
- package/src/use-loader.tsx +73 -4
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { traverseBack } from "./pattern-matching.js";
|
|
3
|
-
import { collectRouteMiddleware } from "./middleware.js";
|
|
4
|
-
import {
|
|
5
|
-
parseAcceptTypes,
|
|
6
|
-
RSC_RESPONSE_TYPE,
|
|
7
|
-
pickNegotiateVariant,
|
|
8
|
-
} from "./content-negotiation.js";
|
|
1
|
+
import { negotiateRoute } from "./content-negotiation.js";
|
|
9
2
|
import { runWithRouterLogContext, withRouterLogScope } from "./logging.js";
|
|
10
3
|
import type { EntryData } from "../server/context";
|
|
11
4
|
import type { RouteMatchResult } from "./pattern-matching.js";
|
|
12
5
|
import type { MiddlewareFn } from "./middleware.js";
|
|
6
|
+
import { resolveRoute } from "./route-snapshot.js";
|
|
13
7
|
|
|
14
8
|
export interface PreviewMatchDeps<TEnv = any> {
|
|
15
9
|
findMatch: (pathname: string) => RouteMatchResult<TEnv> | null;
|
|
@@ -42,110 +36,44 @@ export async function previewMatch<TEnv = any>(
|
|
|
42
36
|
const url = new URL(request.url);
|
|
43
37
|
const pathname = url.pathname;
|
|
44
38
|
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
// Route resolution via snapshot (lite mode: skip entries/cacheScope
|
|
40
|
+
// since previewMatch only needs matched, manifestEntry, routeMiddleware,
|
|
41
|
+
// and responseType)
|
|
42
|
+
const result = await resolveRoute<TEnv>(pathname, {
|
|
43
|
+
findMatch: deps.findMatch,
|
|
44
|
+
lite: true,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (!result) {
|
|
48
48
|
return null;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// Skip redirect check - will be handled in full match
|
|
52
|
-
if (
|
|
52
|
+
if (result.type === "redirect") {
|
|
53
53
|
return { routeMiddleware: undefined };
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
const manifestEntry
|
|
58
|
-
|
|
59
|
-
matched.routeKey,
|
|
60
|
-
pathname,
|
|
61
|
-
undefined, // No metrics store for preview
|
|
62
|
-
false, // isSSR - doesn't matter for preview
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
// Collect route-level middleware from entry tree
|
|
66
|
-
// Includes middleware from orphan layouts (inline layouts within routes)
|
|
67
|
-
const routeMiddleware = collectRouteMiddleware(
|
|
68
|
-
traverseBack(manifestEntry),
|
|
69
|
-
matched.params,
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
// Check for response type (from trie match or manifest entry)
|
|
73
|
-
const responseType =
|
|
74
|
-
matched.responseType ||
|
|
75
|
-
(manifestEntry.type === "route"
|
|
76
|
-
? manifestEntry.responseType
|
|
77
|
-
: undefined);
|
|
78
|
-
|
|
79
|
-
// Content negotiation: when negotiate variants exist, pick the best
|
|
80
|
-
// handler based on the Accept header. Uses q-values and client order
|
|
81
|
-
// as tiebreaker (matching Express/Hono behavior). RSC routes participate
|
|
82
|
-
// as text/html candidates so browsers naturally get HTML without
|
|
83
|
-
// special-casing.
|
|
84
|
-
if (matched.negotiateVariants && matched.negotiateVariants.length > 0) {
|
|
85
|
-
const acceptEntries = parseAcceptTypes(
|
|
86
|
-
request.headers.get("accept") || "",
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
// Build candidate list preserving definition order.
|
|
90
|
-
// For wildcard (*/*) and no-Accept fallback, the first candidate wins.
|
|
91
|
-
const variants = matched.negotiateVariants;
|
|
92
|
-
let candidates: Array<{ routeKey: string; responseType: string }>;
|
|
93
|
-
if (responseType) {
|
|
94
|
-
// Primary is response-type — include it as a candidate
|
|
95
|
-
candidates = [
|
|
96
|
-
...variants,
|
|
97
|
-
{ routeKey: matched.routeKey, responseType },
|
|
98
|
-
];
|
|
99
|
-
} else {
|
|
100
|
-
// Primary is RSC — insert as text/html candidate in definition order
|
|
101
|
-
const rscCandidate = {
|
|
102
|
-
routeKey: matched.routeKey,
|
|
103
|
-
responseType: RSC_RESPONSE_TYPE,
|
|
104
|
-
};
|
|
105
|
-
candidates = matched.rscFirst
|
|
106
|
-
? [rscCandidate, ...variants]
|
|
107
|
-
: [...variants, rscCandidate];
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const variant = pickNegotiateVariant(acceptEntries, candidates);
|
|
56
|
+
const snapshot = result.snapshot;
|
|
57
|
+
const { matched, manifestEntry, routeMiddleware, responseType } =
|
|
58
|
+
snapshot;
|
|
111
59
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// since different variants can have different middleware chains.
|
|
127
|
-
const variantMiddleware = collectRouteMiddleware(
|
|
128
|
-
traverseBack(negotiateEntry),
|
|
129
|
-
matched.params,
|
|
130
|
-
);
|
|
131
|
-
return {
|
|
132
|
-
routeMiddleware:
|
|
133
|
-
variantMiddleware.length > 0 ? variantMiddleware : undefined,
|
|
134
|
-
responseType: variant.responseType,
|
|
135
|
-
handler:
|
|
136
|
-
negotiateEntry.type === "route"
|
|
137
|
-
? negotiateEntry.handler
|
|
138
|
-
: undefined,
|
|
139
|
-
params: matched.params,
|
|
140
|
-
negotiated: true,
|
|
141
|
-
manifestEntry: negotiateEntry,
|
|
142
|
-
routeKey: matched.routeKey,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
60
|
+
const negotiation = await negotiateRoute(request, pathname, snapshot);
|
|
61
|
+
if (negotiation) {
|
|
62
|
+
return {
|
|
63
|
+
routeMiddleware:
|
|
64
|
+
negotiation.routeMiddleware.length > 0
|
|
65
|
+
? negotiation.routeMiddleware
|
|
66
|
+
: undefined,
|
|
67
|
+
responseType: negotiation.responseType,
|
|
68
|
+
handler: negotiation.handler,
|
|
69
|
+
params: matched.params,
|
|
70
|
+
negotiated: true,
|
|
71
|
+
manifestEntry: negotiation.manifestEntry,
|
|
72
|
+
routeKey: matched.routeKey,
|
|
73
|
+
};
|
|
145
74
|
}
|
|
146
75
|
|
|
147
|
-
//
|
|
148
|
-
// negotiated so the handler sets Vary: Accept on the response.
|
|
76
|
+
// No negotiation or RSC won — return default route info
|
|
149
77
|
const hasVariants =
|
|
150
78
|
matched.negotiateVariants && matched.negotiateVariants.length > 0;
|
|
151
79
|
return {
|
|
@@ -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
|
+
}
|