@remix-run/router 1.0.5-pre.2 → 1.1.0-pre.0
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/CHANGELOG.md +48 -10
- package/dist/router.cjs.js +122 -39
- package/dist/router.cjs.js.map +1 -1
- package/dist/router.js +122 -39
- package/dist/router.js.map +1 -1
- package/dist/router.umd.js +122 -39
- package/dist/router.umd.js.map +1 -1
- package/dist/router.umd.min.js +2 -2
- package/dist/router.umd.min.js.map +1 -1
- package/dist/utils.d.ts +5 -5
- package/package.json +1 -1
- package/router.ts +54 -30
- package/utils.ts +95 -11
package/dist/utils.d.ts
CHANGED
|
@@ -48,8 +48,8 @@ export interface ErrorResult {
|
|
|
48
48
|
* Result from a loader or action - potentially successful or unsuccessful
|
|
49
49
|
*/
|
|
50
50
|
export declare type DataResult = SuccessResult | DeferredResult | RedirectResult | ErrorResult;
|
|
51
|
-
export declare type
|
|
52
|
-
export declare type FormMethod = "get" |
|
|
51
|
+
export declare type MutationFormMethod = "post" | "put" | "patch" | "delete";
|
|
52
|
+
export declare type FormMethod = "get" | MutationFormMethod;
|
|
53
53
|
export declare type FormEncType = "application/x-www-form-urlencoded" | "multipart/form-data";
|
|
54
54
|
/**
|
|
55
55
|
* @private
|
|
@@ -57,7 +57,7 @@ export declare type FormEncType = "application/x-www-form-urlencoded" | "multipa
|
|
|
57
57
|
* external consumption
|
|
58
58
|
*/
|
|
59
59
|
export interface Submission {
|
|
60
|
-
formMethod:
|
|
60
|
+
formMethod: FormMethod;
|
|
61
61
|
formAction: string;
|
|
62
62
|
formEncType: FormEncType;
|
|
63
63
|
formData: FormData;
|
|
@@ -158,7 +158,7 @@ export declare type AgnosticDataNonIndexRouteObject = AgnosticNonIndexRouteObjec
|
|
|
158
158
|
* A data route object, which is just a RouteObject with a required unique ID
|
|
159
159
|
*/
|
|
160
160
|
export declare type AgnosticDataRouteObject = AgnosticDataIndexRouteObject | AgnosticDataNonIndexRouteObject;
|
|
161
|
-
declare type _PathParam<Path extends string> = Path extends `${infer L}/${infer R}` ? _PathParam<L> | _PathParam<R> : Path extends
|
|
161
|
+
declare type _PathParam<Path extends string> = Path extends `${infer L}/${infer R}` ? _PathParam<L> | _PathParam<R> : Path extends `:${infer Param}` ? Param : never;
|
|
162
162
|
/**
|
|
163
163
|
* Examples:
|
|
164
164
|
* "/a/b/*" -> "*"
|
|
@@ -213,7 +213,7 @@ export declare function matchRoutes<RouteObjectType extends AgnosticRouteObject
|
|
|
213
213
|
*
|
|
214
214
|
* @see https://reactrouter.com/utils/generate-path
|
|
215
215
|
*/
|
|
216
|
-
export declare function generatePath<Path extends string>(
|
|
216
|
+
export declare function generatePath<Path extends string>(originalPath: Path, params?: {
|
|
217
217
|
[key in PathParam<Path>]: string;
|
|
218
218
|
}): string;
|
|
219
219
|
/**
|
package/package.json
CHANGED
package/router.ts
CHANGED
|
@@ -21,7 +21,7 @@ import type {
|
|
|
21
21
|
Submission,
|
|
22
22
|
SuccessResult,
|
|
23
23
|
AgnosticRouteMatch,
|
|
24
|
-
|
|
24
|
+
MutationFormMethod,
|
|
25
25
|
} from "./utils";
|
|
26
26
|
import {
|
|
27
27
|
DeferredData,
|
|
@@ -521,15 +521,20 @@ interface QueryRouteResponse {
|
|
|
521
521
|
response: Response;
|
|
522
522
|
}
|
|
523
523
|
|
|
524
|
-
const
|
|
524
|
+
const validMutationMethodsArr: MutationFormMethod[] = [
|
|
525
525
|
"post",
|
|
526
526
|
"put",
|
|
527
527
|
"patch",
|
|
528
528
|
"delete",
|
|
529
529
|
];
|
|
530
|
-
const
|
|
530
|
+
const validMutationMethods = new Set<MutationFormMethod>(
|
|
531
|
+
validMutationMethodsArr
|
|
532
|
+
);
|
|
531
533
|
|
|
532
|
-
const validRequestMethodsArr: FormMethod[] = [
|
|
534
|
+
const validRequestMethodsArr: FormMethod[] = [
|
|
535
|
+
"get",
|
|
536
|
+
...validMutationMethodsArr,
|
|
537
|
+
];
|
|
533
538
|
const validRequestMethods = new Set<FormMethod>(validRequestMethodsArr);
|
|
534
539
|
|
|
535
540
|
const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
|
|
@@ -811,7 +816,8 @@ export function createRouter(init: RouterInit): Router {
|
|
|
811
816
|
};
|
|
812
817
|
|
|
813
818
|
let historyAction =
|
|
814
|
-
(opts && opts.replace) === true ||
|
|
819
|
+
(opts && opts.replace) === true ||
|
|
820
|
+
(submission != null && isMutationMethod(submission.formMethod))
|
|
815
821
|
? HistoryAction.Replace
|
|
816
822
|
: HistoryAction.Push;
|
|
817
823
|
let preventScrollReset =
|
|
@@ -935,7 +941,11 @@ export function createRouter(init: RouterInit): Router {
|
|
|
935
941
|
pendingError = {
|
|
936
942
|
[findNearestBoundary(matches).route.id]: opts.pendingError,
|
|
937
943
|
};
|
|
938
|
-
} else if (
|
|
944
|
+
} else if (
|
|
945
|
+
opts &&
|
|
946
|
+
opts.submission &&
|
|
947
|
+
isMutationMethod(opts.submission.formMethod)
|
|
948
|
+
) {
|
|
939
949
|
// Call action if we received an action submission
|
|
940
950
|
let actionOutput = await handleAction(
|
|
941
951
|
request,
|
|
@@ -1095,6 +1105,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1095
1105
|
formAction: undefined,
|
|
1096
1106
|
formEncType: undefined,
|
|
1097
1107
|
formData: undefined,
|
|
1108
|
+
...submission,
|
|
1098
1109
|
};
|
|
1099
1110
|
loadingNavigation = navigation;
|
|
1100
1111
|
}
|
|
@@ -1259,7 +1270,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1259
1270
|
let { path, submission } = normalizeNavigateOptions(href, opts, true);
|
|
1260
1271
|
let match = getTargetMatch(matches, path);
|
|
1261
1272
|
|
|
1262
|
-
if (submission) {
|
|
1273
|
+
if (submission && isMutationMethod(submission.formMethod)) {
|
|
1263
1274
|
handleFetcherAction(key, routeId, path, match, matches, submission);
|
|
1264
1275
|
return;
|
|
1265
1276
|
}
|
|
@@ -1267,7 +1278,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1267
1278
|
// Store off the match so we can call it's shouldRevalidate on subsequent
|
|
1268
1279
|
// revalidations
|
|
1269
1280
|
fetchLoadMatches.set(key, [path, match, matches]);
|
|
1270
|
-
handleFetcherLoader(key, routeId, path, match, matches);
|
|
1281
|
+
handleFetcherLoader(key, routeId, path, match, matches, submission);
|
|
1271
1282
|
}
|
|
1272
1283
|
|
|
1273
1284
|
// Call the action for the matched fetcher.submit(), and then handle redirects,
|
|
@@ -1494,7 +1505,8 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1494
1505
|
routeId: string,
|
|
1495
1506
|
path: string,
|
|
1496
1507
|
match: AgnosticDataRouteMatch,
|
|
1497
|
-
matches: AgnosticDataRouteMatch[]
|
|
1508
|
+
matches: AgnosticDataRouteMatch[],
|
|
1509
|
+
submission?: Submission
|
|
1498
1510
|
) {
|
|
1499
1511
|
let existingFetcher = state.fetchers.get(key);
|
|
1500
1512
|
// Put this fetcher into it's loading state
|
|
@@ -1504,6 +1516,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1504
1516
|
formAction: undefined,
|
|
1505
1517
|
formEncType: undefined,
|
|
1506
1518
|
formData: undefined,
|
|
1519
|
+
...submission,
|
|
1507
1520
|
data: existingFetcher && existingFetcher.data,
|
|
1508
1521
|
};
|
|
1509
1522
|
state.fetchers.set(key, loadingFetcher);
|
|
@@ -1635,12 +1648,12 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1635
1648
|
let { formMethod, formAction, formEncType, formData } = state.navigation;
|
|
1636
1649
|
|
|
1637
1650
|
// If this was a 307/308 submission we want to preserve the HTTP method and
|
|
1638
|
-
// re-submit the POST/PUT/PATCH/DELETE as a submission navigation to the
|
|
1651
|
+
// re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the
|
|
1639
1652
|
// redirected location
|
|
1640
1653
|
if (
|
|
1641
1654
|
redirectPreserveMethodStatusCodes.has(redirect.status) &&
|
|
1642
1655
|
formMethod &&
|
|
1643
|
-
|
|
1656
|
+
isMutationMethod(formMethod) &&
|
|
1644
1657
|
formEncType &&
|
|
1645
1658
|
formData
|
|
1646
1659
|
) {
|
|
@@ -2096,7 +2109,7 @@ export function unstable_createStaticHandler(
|
|
|
2096
2109
|
);
|
|
2097
2110
|
|
|
2098
2111
|
try {
|
|
2099
|
-
if (
|
|
2112
|
+
if (isMutationMethod(request.method.toLowerCase())) {
|
|
2100
2113
|
let result = await submit(
|
|
2101
2114
|
request,
|
|
2102
2115
|
matches,
|
|
@@ -2244,7 +2257,11 @@ export function unstable_createStaticHandler(
|
|
|
2244
2257
|
}
|
|
2245
2258
|
|
|
2246
2259
|
// Create a GET request for the loaders
|
|
2247
|
-
let loaderRequest = new Request(request.url, {
|
|
2260
|
+
let loaderRequest = new Request(request.url, {
|
|
2261
|
+
headers: request.headers,
|
|
2262
|
+
redirect: request.redirect,
|
|
2263
|
+
signal: request.signal,
|
|
2264
|
+
});
|
|
2248
2265
|
let context = await loadRouteData(loaderRequest, matches, requestContext);
|
|
2249
2266
|
|
|
2250
2267
|
return {
|
|
@@ -2409,17 +2426,19 @@ function normalizeNavigateOptions(
|
|
|
2409
2426
|
}
|
|
2410
2427
|
|
|
2411
2428
|
// Create a Submission on non-GET navigations
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
formEncType
|
|
2419
|
-
|
|
2420
|
-
formData: opts.formData,
|
|
2421
|
-
},
|
|
2429
|
+
let submission: Submission | undefined;
|
|
2430
|
+
if (opts.formData) {
|
|
2431
|
+
submission = {
|
|
2432
|
+
formMethod: opts.formMethod || "get",
|
|
2433
|
+
formAction: stripHashFromPath(path),
|
|
2434
|
+
formEncType:
|
|
2435
|
+
(opts && opts.formEncType) || "application/x-www-form-urlencoded",
|
|
2436
|
+
formData: opts.formData,
|
|
2422
2437
|
};
|
|
2438
|
+
|
|
2439
|
+
if (isMutationMethod(submission.formMethod)) {
|
|
2440
|
+
return { path, submission };
|
|
2441
|
+
}
|
|
2423
2442
|
}
|
|
2424
2443
|
|
|
2425
2444
|
// Flatten submission onto URLSearchParams for GET submissions
|
|
@@ -2444,7 +2463,7 @@ function normalizeNavigateOptions(
|
|
|
2444
2463
|
};
|
|
2445
2464
|
}
|
|
2446
2465
|
|
|
2447
|
-
return { path: createPath(parsedPath) };
|
|
2466
|
+
return { path: createPath(parsedPath), submission };
|
|
2448
2467
|
}
|
|
2449
2468
|
|
|
2450
2469
|
// Filter out all routes below any caught error as they aren't going to
|
|
@@ -2767,7 +2786,7 @@ function createClientSideRequest(
|
|
|
2767
2786
|
let url = createClientSideURL(stripHashFromPath(location)).toString();
|
|
2768
2787
|
let init: RequestInit = { signal };
|
|
2769
2788
|
|
|
2770
|
-
if (submission) {
|
|
2789
|
+
if (submission && isMutationMethod(submission.formMethod)) {
|
|
2771
2790
|
let { formMethod, formEncType, formData } = submission;
|
|
2772
2791
|
init.method = formMethod.toUpperCase();
|
|
2773
2792
|
init.body =
|
|
@@ -2833,9 +2852,14 @@ function processRouteLoaderData(
|
|
|
2833
2852
|
error = Object.values(pendingError)[0];
|
|
2834
2853
|
pendingError = undefined;
|
|
2835
2854
|
}
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2855
|
+
|
|
2856
|
+
errors = errors || {};
|
|
2857
|
+
|
|
2858
|
+
// Prefer higher error values if lower errors bubble to the same boundary
|
|
2859
|
+
if (errors[boundaryMatch.route.id] == null) {
|
|
2860
|
+
errors[boundaryMatch.route.id] = error;
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2839
2863
|
// Once we find our first (highest) error, we set the status code and
|
|
2840
2864
|
// prevent deeper status codes from overriding
|
|
2841
2865
|
if (!foundError) {
|
|
@@ -3115,8 +3139,8 @@ function isValidMethod(method: string): method is FormMethod {
|
|
|
3115
3139
|
return validRequestMethods.has(method as FormMethod);
|
|
3116
3140
|
}
|
|
3117
3141
|
|
|
3118
|
-
function
|
|
3119
|
-
return
|
|
3142
|
+
function isMutationMethod(method?: string): method is MutationFormMethod {
|
|
3143
|
+
return validMutationMethods.has(method as MutationFormMethod);
|
|
3120
3144
|
}
|
|
3121
3145
|
|
|
3122
3146
|
async function resolveDeferredResults(
|
package/utils.ts
CHANGED
|
@@ -61,8 +61,8 @@ export type DataResult =
|
|
|
61
61
|
| RedirectResult
|
|
62
62
|
| ErrorResult;
|
|
63
63
|
|
|
64
|
-
export type
|
|
65
|
-
export type FormMethod = "get" |
|
|
64
|
+
export type MutationFormMethod = "post" | "put" | "patch" | "delete";
|
|
65
|
+
export type FormMethod = "get" | MutationFormMethod;
|
|
66
66
|
|
|
67
67
|
export type FormEncType =
|
|
68
68
|
| "application/x-www-form-urlencoded"
|
|
@@ -74,7 +74,7 @@ export type FormEncType =
|
|
|
74
74
|
* external consumption
|
|
75
75
|
*/
|
|
76
76
|
export interface Submission {
|
|
77
|
-
formMethod:
|
|
77
|
+
formMethod: FormMethod;
|
|
78
78
|
formAction: string;
|
|
79
79
|
formEncType: FormEncType;
|
|
80
80
|
formData: FormData;
|
|
@@ -197,7 +197,7 @@ type _PathParam<Path extends string> =
|
|
|
197
197
|
Path extends `${infer L}/${infer R}`
|
|
198
198
|
? _PathParam<L> | _PathParam<R>
|
|
199
199
|
: // find params after `:`
|
|
200
|
-
Path extends
|
|
200
|
+
Path extends `:${infer Param}`
|
|
201
201
|
? Param
|
|
202
202
|
: // otherwise, there aren't any params present
|
|
203
203
|
never;
|
|
@@ -372,9 +372,14 @@ function flattenRoutes<
|
|
|
372
372
|
parentsMeta: RouteMeta<RouteObjectType>[] = [],
|
|
373
373
|
parentPath = ""
|
|
374
374
|
): RouteBranch<RouteObjectType>[] {
|
|
375
|
-
|
|
375
|
+
let flattenRoute = (
|
|
376
|
+
route: RouteObjectType,
|
|
377
|
+
index: number,
|
|
378
|
+
relativePath?: string
|
|
379
|
+
) => {
|
|
376
380
|
let meta: RouteMeta<RouteObjectType> = {
|
|
377
|
-
relativePath:
|
|
381
|
+
relativePath:
|
|
382
|
+
relativePath === undefined ? route.path || "" : relativePath,
|
|
378
383
|
caseSensitive: route.caseSensitive === true,
|
|
379
384
|
childrenIndex: index,
|
|
380
385
|
route,
|
|
@@ -415,12 +420,75 @@ function flattenRoutes<
|
|
|
415
420
|
return;
|
|
416
421
|
}
|
|
417
422
|
|
|
418
|
-
branches.push({
|
|
423
|
+
branches.push({
|
|
424
|
+
path,
|
|
425
|
+
score: computeScore(path, route.index),
|
|
426
|
+
routesMeta,
|
|
427
|
+
});
|
|
428
|
+
};
|
|
429
|
+
routes.forEach((route, index) => {
|
|
430
|
+
// coarse-grain check for optional params
|
|
431
|
+
if (route.path === "" || !route.path?.includes("?")) {
|
|
432
|
+
flattenRoute(route, index);
|
|
433
|
+
} else {
|
|
434
|
+
for (let exploded of explodeOptionalSegments(route.path)) {
|
|
435
|
+
flattenRoute(route, index, exploded);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
419
438
|
});
|
|
420
439
|
|
|
421
440
|
return branches;
|
|
422
441
|
}
|
|
423
442
|
|
|
443
|
+
/**
|
|
444
|
+
* Computes all combinations of optional path segments for a given path,
|
|
445
|
+
* excluding combinations that are ambiguous and of lower priority.
|
|
446
|
+
*
|
|
447
|
+
* For example, `/one/:two?/three/:four?/:five?` explodes to:
|
|
448
|
+
* - `/one/three`
|
|
449
|
+
* - `/one/:two/three`
|
|
450
|
+
* - `/one/three/:four`
|
|
451
|
+
* - `/one/three/:five`
|
|
452
|
+
* - `/one/:two/three/:four`
|
|
453
|
+
* - `/one/:two/three/:five`
|
|
454
|
+
* - `/one/three/:four/:five`
|
|
455
|
+
* - `/one/:two/three/:four/:five`
|
|
456
|
+
*/
|
|
457
|
+
function explodeOptionalSegments(path: string): string[] {
|
|
458
|
+
let segments = path.split("/");
|
|
459
|
+
if (segments.length === 0) return [];
|
|
460
|
+
|
|
461
|
+
let [first, ...rest] = segments;
|
|
462
|
+
|
|
463
|
+
// Optional path segments are denoted by a trailing `?`
|
|
464
|
+
let isOptional = first.endsWith("?");
|
|
465
|
+
// Compute the corresponding required segment: `foo?` -> `foo`
|
|
466
|
+
let required = first.replace(/\?$/, "");
|
|
467
|
+
|
|
468
|
+
if (rest.length === 0) {
|
|
469
|
+
// Intepret empty string as omitting an optional segment
|
|
470
|
+
// `["one", "", "three"]` corresponds to omitting `:two` from `/one/:two?/three` -> `/one/three`
|
|
471
|
+
return isOptional ? ["", required] : [required];
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
let restExploded = explodeOptionalSegments(rest.join("/"));
|
|
475
|
+
return restExploded
|
|
476
|
+
.flatMap((subpath) => {
|
|
477
|
+
// /one + / + :two/three -> /one/:two/three
|
|
478
|
+
let requiredExploded =
|
|
479
|
+
subpath === "" ? required : required + "/" + subpath;
|
|
480
|
+
// For optional segments, return the exploded path _without_ current segment first (`subpath`)
|
|
481
|
+
// and exploded path _with_ current segment later (`subpath`)
|
|
482
|
+
// This ensures that exploded paths are emitted in priority order
|
|
483
|
+
// `/one/three/:four` will come before `/one/three/:five`
|
|
484
|
+
return isOptional ? [subpath, requiredExploded] : [requiredExploded];
|
|
485
|
+
})
|
|
486
|
+
.map((exploded) => {
|
|
487
|
+
// for absolute paths, ensure `/` instead of empty segment
|
|
488
|
+
return path.startsWith("/") && exploded === "" ? "/" : exploded;
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
424
492
|
function rankRouteBranches(branches: RouteBranch[]): void {
|
|
425
493
|
branches.sort((a, b) =>
|
|
426
494
|
a.score !== b.score
|
|
@@ -534,16 +602,32 @@ function matchRouteBranch<
|
|
|
534
602
|
* @see https://reactrouter.com/utils/generate-path
|
|
535
603
|
*/
|
|
536
604
|
export function generatePath<Path extends string>(
|
|
537
|
-
|
|
605
|
+
originalPath: Path,
|
|
538
606
|
params: {
|
|
539
607
|
[key in PathParam<Path>]: string;
|
|
540
608
|
} = {} as any
|
|
541
609
|
): string {
|
|
610
|
+
let path = originalPath;
|
|
611
|
+
if (path.endsWith("*") && path !== "*" && !path.endsWith("/*")) {
|
|
612
|
+
warning(
|
|
613
|
+
false,
|
|
614
|
+
`Route path "${path}" will be treated as if it were ` +
|
|
615
|
+
`"${path.replace(/\*$/, "/*")}" because the \`*\` character must ` +
|
|
616
|
+
`always follow a \`/\` in the pattern. To get rid of this warning, ` +
|
|
617
|
+
`please change the route path to "${path.replace(/\*$/, "/*")}".`
|
|
618
|
+
);
|
|
619
|
+
path = path.replace(/\*$/, "/*") as Path;
|
|
620
|
+
}
|
|
621
|
+
|
|
542
622
|
return path
|
|
543
|
-
.replace(
|
|
623
|
+
.replace(/^:(\w+)/g, (_, key: PathParam<Path>) => {
|
|
544
624
|
invariant(params[key] != null, `Missing ":${key}" param`);
|
|
545
625
|
return params[key]!;
|
|
546
626
|
})
|
|
627
|
+
.replace(/\/:(\w+)/g, (_, key: PathParam<Path>) => {
|
|
628
|
+
invariant(params[key] != null, `Missing ":${key}" param`);
|
|
629
|
+
return `/${params[key]!}`;
|
|
630
|
+
})
|
|
547
631
|
.replace(/(\/?)\*/, (_, prefix, __, str) => {
|
|
548
632
|
const star = "*" as PathParam<Path>;
|
|
549
633
|
|
|
@@ -682,9 +766,9 @@ function compilePath(
|
|
|
682
766
|
.replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
|
|
683
767
|
.replace(/^\/*/, "/") // Make sure it has a leading /
|
|
684
768
|
.replace(/[\\.*+^$?{}|()[\]]/g, "\\$&") // Escape special regex chars
|
|
685
|
-
.replace(
|
|
769
|
+
.replace(/\/:(\w+)/g, (_: string, paramName: string) => {
|
|
686
770
|
paramNames.push(paramName);
|
|
687
|
-
return "([^\\/]+)";
|
|
771
|
+
return "/([^\\/]+)";
|
|
688
772
|
});
|
|
689
773
|
|
|
690
774
|
if (path.endsWith("*")) {
|