@remix-run/router 1.0.5 → 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 +49 -0
- 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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# `@remix-run/router`
|
|
2
2
|
|
|
3
|
+
## 1.1.0-pre.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Support for optional path segments ([#9650](https://github.com/remix-run/react-router/pull/9650))
|
|
8
|
+
- You can now denote optional path segments with a `?` as the last character in a path segment
|
|
9
|
+
- Optional params examples
|
|
10
|
+
- `:lang?/about` will get expanded and match:
|
|
11
|
+
- `/:lang/about`
|
|
12
|
+
- `/about`
|
|
13
|
+
- `/multistep/:widget1?/widget2?/widget3?` will get expanded and match:
|
|
14
|
+
- `/multistep/:widget1/:widget2/:widget3`
|
|
15
|
+
- `/multistep/:widget1/:widget2`
|
|
16
|
+
- `/multistep/:widget1`
|
|
17
|
+
- `/multistep`
|
|
18
|
+
- Optional static segment example
|
|
19
|
+
- `/fr?/about` will get expanded and match:
|
|
20
|
+
- `/fr/about`
|
|
21
|
+
- `/about`
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- Stop incorrectly matching on partial named parameters, i.e. `<Route path="prefix-:param">`, to align with how splat parameters work. If you were previously relying on this behavior then it's recommended to extract the static portion of the path at the `useParams` call site: ([#9506](https://github.com/remix-run/react-router/pull/9506))
|
|
26
|
+
|
|
27
|
+
```jsx
|
|
28
|
+
// Old behavior at URL /prefix-123
|
|
29
|
+
<Route path="prefix-:id" element={<Comp /> }>
|
|
30
|
+
|
|
31
|
+
function Comp() {
|
|
32
|
+
let params = useParams(); // { id: '123' }
|
|
33
|
+
let id = params.id; // "123"
|
|
34
|
+
...
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// New behavior at URL /prefix-123
|
|
38
|
+
<Route path=":id" element={<Comp /> }>
|
|
39
|
+
|
|
40
|
+
function Comp() {
|
|
41
|
+
let params = useParams(); // { id: 'prefix-123' }
|
|
42
|
+
let id = params.id.replace(/^prefix-/, ''); // "123"
|
|
43
|
+
...
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- Fix requests sent to revalidating loaders so they reflect a GET request ([#9660](https://github.com/remix-run/react-router/pull/9660))
|
|
48
|
+
- Persist `headers` on `loader` `request`'s after SSR document `action` request ([#9721](https://github.com/remix-run/react-router/pull/9721))
|
|
49
|
+
- `GET` forms now expose a submission on the loading navigation ([#9695](https://github.com/remix-run/react-router/pull/9695))
|
|
50
|
+
- Fix error boundary tracking for multiple errors bubbling to the same boundary ([#9702](https://github.com/remix-run/react-router/pull/9702))
|
|
51
|
+
|
|
3
52
|
## 1.0.5
|
|
4
53
|
|
|
5
54
|
### Patch Changes
|
package/dist/router.cjs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @remix-run/router v1.0.
|
|
2
|
+
* @remix-run/router v1.1.0-pre.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Remix Software Inc.
|
|
5
5
|
*
|
|
@@ -611,9 +611,9 @@ function flattenRoutes(routes, branches, parentsMeta, parentPath) {
|
|
|
611
611
|
parentPath = "";
|
|
612
612
|
}
|
|
613
613
|
|
|
614
|
-
|
|
614
|
+
let flattenRoute = (route, index, relativePath) => {
|
|
615
615
|
let meta = {
|
|
616
|
-
relativePath: route.path || "",
|
|
616
|
+
relativePath: relativePath === undefined ? route.path || "" : relativePath,
|
|
617
617
|
caseSensitive: route.caseSensitive === true,
|
|
618
618
|
childrenIndex: index,
|
|
619
619
|
route
|
|
@@ -647,9 +647,67 @@ function flattenRoutes(routes, branches, parentsMeta, parentPath) {
|
|
|
647
647
|
score: computeScore(path, route.index),
|
|
648
648
|
routesMeta
|
|
649
649
|
});
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
routes.forEach((route, index) => {
|
|
653
|
+
var _route$path;
|
|
654
|
+
|
|
655
|
+
// coarse-grain check for optional params
|
|
656
|
+
if (route.path === "" || !((_route$path = route.path) != null && _route$path.includes("?"))) {
|
|
657
|
+
flattenRoute(route, index);
|
|
658
|
+
} else {
|
|
659
|
+
for (let exploded of explodeOptionalSegments(route.path)) {
|
|
660
|
+
flattenRoute(route, index, exploded);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
650
663
|
});
|
|
651
664
|
return branches;
|
|
652
665
|
}
|
|
666
|
+
/**
|
|
667
|
+
* Computes all combinations of optional path segments for a given path,
|
|
668
|
+
* excluding combinations that are ambiguous and of lower priority.
|
|
669
|
+
*
|
|
670
|
+
* For example, `/one/:two?/three/:four?/:five?` explodes to:
|
|
671
|
+
* - `/one/three`
|
|
672
|
+
* - `/one/:two/three`
|
|
673
|
+
* - `/one/three/:four`
|
|
674
|
+
* - `/one/three/:five`
|
|
675
|
+
* - `/one/:two/three/:four`
|
|
676
|
+
* - `/one/:two/three/:five`
|
|
677
|
+
* - `/one/three/:four/:five`
|
|
678
|
+
* - `/one/:two/three/:four/:five`
|
|
679
|
+
*/
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
function explodeOptionalSegments(path) {
|
|
683
|
+
let segments = path.split("/");
|
|
684
|
+
if (segments.length === 0) return [];
|
|
685
|
+
let [first, ...rest] = segments; // Optional path segments are denoted by a trailing `?`
|
|
686
|
+
|
|
687
|
+
let isOptional = first.endsWith("?"); // Compute the corresponding required segment: `foo?` -> `foo`
|
|
688
|
+
|
|
689
|
+
let required = first.replace(/\?$/, "");
|
|
690
|
+
|
|
691
|
+
if (rest.length === 0) {
|
|
692
|
+
// Intepret empty string as omitting an optional segment
|
|
693
|
+
// `["one", "", "three"]` corresponds to omitting `:two` from `/one/:two?/three` -> `/one/three`
|
|
694
|
+
return isOptional ? ["", required] : [required];
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
let restExploded = explodeOptionalSegments(rest.join("/"));
|
|
698
|
+
return restExploded.flatMap(subpath => {
|
|
699
|
+
// /one + / + :two/three -> /one/:two/three
|
|
700
|
+
let requiredExploded = subpath === "" ? required : required + "/" + subpath; // For optional segments, return the exploded path _without_ current segment first (`subpath`)
|
|
701
|
+
// and exploded path _with_ current segment later (`subpath`)
|
|
702
|
+
// This ensures that exploded paths are emitted in priority order
|
|
703
|
+
// `/one/three/:four` will come before `/one/three/:five`
|
|
704
|
+
|
|
705
|
+
return isOptional ? [subpath, requiredExploded] : [requiredExploded];
|
|
706
|
+
}).map(exploded => {
|
|
707
|
+
// for absolute paths, ensure `/` instead of empty segment
|
|
708
|
+
return path.startsWith("/") && exploded === "" ? "/" : exploded;
|
|
709
|
+
});
|
|
710
|
+
}
|
|
653
711
|
|
|
654
712
|
function rankRouteBranches(branches) {
|
|
655
713
|
branches.sort((a, b) => a.score !== b.score ? b.score - a.score // Higher score first
|
|
@@ -733,14 +791,24 @@ function matchRouteBranch(branch, pathname) {
|
|
|
733
791
|
*/
|
|
734
792
|
|
|
735
793
|
|
|
736
|
-
function generatePath(
|
|
794
|
+
function generatePath(originalPath, params) {
|
|
737
795
|
if (params === void 0) {
|
|
738
796
|
params = {};
|
|
739
797
|
}
|
|
740
798
|
|
|
741
|
-
|
|
799
|
+
let path = originalPath;
|
|
800
|
+
|
|
801
|
+
if (path.endsWith("*") && path !== "*" && !path.endsWith("/*")) {
|
|
802
|
+
warning(false, "Route path \"" + path + "\" will be treated as if it were " + ("\"" + path.replace(/\*$/, "/*") + "\" because the `*` character must ") + "always follow a `/` in the pattern. To get rid of this warning, " + ("please change the route path to \"" + path.replace(/\*$/, "/*") + "\"."));
|
|
803
|
+
path = path.replace(/\*$/, "/*");
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return path.replace(/^:(\w+)/g, (_, key) => {
|
|
742
807
|
invariant(params[key] != null, "Missing \":" + key + "\" param");
|
|
743
808
|
return params[key];
|
|
809
|
+
}).replace(/\/:(\w+)/g, (_, key) => {
|
|
810
|
+
invariant(params[key] != null, "Missing \":" + key + "\" param");
|
|
811
|
+
return "/" + params[key];
|
|
744
812
|
}).replace(/(\/?)\*/, (_, prefix, __, str) => {
|
|
745
813
|
const star = "*";
|
|
746
814
|
|
|
@@ -812,9 +880,9 @@ function compilePath(path, caseSensitive, end) {
|
|
|
812
880
|
let regexpSource = "^" + path.replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
|
|
813
881
|
.replace(/^\/*/, "/") // Make sure it has a leading /
|
|
814
882
|
.replace(/[\\.*+^$?{}|()[\]]/g, "\\$&") // Escape special regex chars
|
|
815
|
-
.replace(
|
|
883
|
+
.replace(/\/:(\w+)/g, (_, paramName) => {
|
|
816
884
|
paramNames.push(paramName);
|
|
817
|
-
return "([^\\/]+)";
|
|
885
|
+
return "/([^\\/]+)";
|
|
818
886
|
});
|
|
819
887
|
|
|
820
888
|
if (path.endsWith("*")) {
|
|
@@ -1295,9 +1363,9 @@ function isRouteErrorResponse(e) {
|
|
|
1295
1363
|
* A Router instance manages all navigation and data loading/mutations
|
|
1296
1364
|
*/
|
|
1297
1365
|
|
|
1298
|
-
const
|
|
1299
|
-
const
|
|
1300
|
-
const validRequestMethodsArr = ["get", ...
|
|
1366
|
+
const validMutationMethodsArr = ["post", "put", "patch", "delete"];
|
|
1367
|
+
const validMutationMethods = new Set(validMutationMethodsArr);
|
|
1368
|
+
const validRequestMethodsArr = ["get", ...validMutationMethodsArr];
|
|
1301
1369
|
const validRequestMethods = new Set(validRequestMethodsArr);
|
|
1302
1370
|
const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
|
|
1303
1371
|
const redirectPreserveMethodStatusCodes = new Set([307, 308]);
|
|
@@ -1534,7 +1602,7 @@ function createRouter(init) {
|
|
|
1534
1602
|
// without having to touch history
|
|
1535
1603
|
|
|
1536
1604
|
location = _extends({}, location, init.history.encodeLocation(location));
|
|
1537
|
-
let historyAction = (opts && opts.replace) === true || submission != null ? exports.Action.Replace : exports.Action.Push;
|
|
1605
|
+
let historyAction = (opts && opts.replace) === true || submission != null && isMutationMethod(submission.formMethod) ? exports.Action.Replace : exports.Action.Push;
|
|
1538
1606
|
let preventScrollReset = opts && "preventScrollReset" in opts ? opts.preventScrollReset === true : undefined;
|
|
1539
1607
|
return await startNavigation(historyAction, location, {
|
|
1540
1608
|
submission,
|
|
@@ -1638,7 +1706,7 @@ function createRouter(init) {
|
|
|
1638
1706
|
pendingError = {
|
|
1639
1707
|
[findNearestBoundary(matches).route.id]: opts.pendingError
|
|
1640
1708
|
};
|
|
1641
|
-
} else if (opts && opts.submission) {
|
|
1709
|
+
} else if (opts && opts.submission && isMutationMethod(opts.submission.formMethod)) {
|
|
1642
1710
|
// Call action if we received an action submission
|
|
1643
1711
|
let actionOutput = await handleAction(request, location, opts.submission, matches, {
|
|
1644
1712
|
replace: opts.replace
|
|
@@ -1765,14 +1833,15 @@ function createRouter(init) {
|
|
|
1765
1833
|
let loadingNavigation = overrideNavigation;
|
|
1766
1834
|
|
|
1767
1835
|
if (!loadingNavigation) {
|
|
1768
|
-
let navigation = {
|
|
1836
|
+
let navigation = _extends({
|
|
1769
1837
|
state: "loading",
|
|
1770
1838
|
location,
|
|
1771
1839
|
formMethod: undefined,
|
|
1772
1840
|
formAction: undefined,
|
|
1773
1841
|
formEncType: undefined,
|
|
1774
1842
|
formData: undefined
|
|
1775
|
-
};
|
|
1843
|
+
}, submission);
|
|
1844
|
+
|
|
1776
1845
|
loadingNavigation = navigation;
|
|
1777
1846
|
}
|
|
1778
1847
|
|
|
@@ -1907,7 +1976,7 @@ function createRouter(init) {
|
|
|
1907
1976
|
} = normalizeNavigateOptions(href, opts, true);
|
|
1908
1977
|
let match = getTargetMatch(matches, path);
|
|
1909
1978
|
|
|
1910
|
-
if (submission) {
|
|
1979
|
+
if (submission && isMutationMethod(submission.formMethod)) {
|
|
1911
1980
|
handleFetcherAction(key, routeId, path, match, matches, submission);
|
|
1912
1981
|
return;
|
|
1913
1982
|
} // Store off the match so we can call it's shouldRevalidate on subsequent
|
|
@@ -1915,7 +1984,7 @@ function createRouter(init) {
|
|
|
1915
1984
|
|
|
1916
1985
|
|
|
1917
1986
|
fetchLoadMatches.set(key, [path, match, matches]);
|
|
1918
|
-
handleFetcherLoader(key, routeId, path, match, matches);
|
|
1987
|
+
handleFetcherLoader(key, routeId, path, match, matches, submission);
|
|
1919
1988
|
} // Call the action for the matched fetcher.submit(), and then handle redirects,
|
|
1920
1989
|
// errors, and revalidation
|
|
1921
1990
|
|
|
@@ -2096,17 +2165,19 @@ function createRouter(init) {
|
|
|
2096
2165
|
} // Call the matched loader for fetcher.load(), handling redirects, errors, etc.
|
|
2097
2166
|
|
|
2098
2167
|
|
|
2099
|
-
async function handleFetcherLoader(key, routeId, path, match, matches) {
|
|
2168
|
+
async function handleFetcherLoader(key, routeId, path, match, matches, submission) {
|
|
2100
2169
|
let existingFetcher = state.fetchers.get(key); // Put this fetcher into it's loading state
|
|
2101
2170
|
|
|
2102
|
-
let loadingFetcher = {
|
|
2171
|
+
let loadingFetcher = _extends({
|
|
2103
2172
|
state: "loading",
|
|
2104
2173
|
formMethod: undefined,
|
|
2105
2174
|
formAction: undefined,
|
|
2106
2175
|
formEncType: undefined,
|
|
2107
|
-
formData: undefined
|
|
2176
|
+
formData: undefined
|
|
2177
|
+
}, submission, {
|
|
2108
2178
|
data: existingFetcher && existingFetcher.data
|
|
2109
|
-
};
|
|
2179
|
+
});
|
|
2180
|
+
|
|
2110
2181
|
state.fetchers.set(key, loadingFetcher);
|
|
2111
2182
|
updateState({
|
|
2112
2183
|
fetchers: new Map(state.fetchers)
|
|
@@ -2226,10 +2297,10 @@ function createRouter(init) {
|
|
|
2226
2297
|
formEncType,
|
|
2227
2298
|
formData
|
|
2228
2299
|
} = state.navigation; // If this was a 307/308 submission we want to preserve the HTTP method and
|
|
2229
|
-
// re-submit the POST/PUT/PATCH/DELETE as a submission navigation to the
|
|
2300
|
+
// re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the
|
|
2230
2301
|
// redirected location
|
|
2231
2302
|
|
|
2232
|
-
if (redirectPreserveMethodStatusCodes.has(redirect.status) && formMethod &&
|
|
2303
|
+
if (redirectPreserveMethodStatusCodes.has(redirect.status) && formMethod && isMutationMethod(formMethod) && formEncType && formData) {
|
|
2233
2304
|
await startNavigation(redirectHistoryAction, redirectLocation, {
|
|
2234
2305
|
submission: {
|
|
2235
2306
|
formMethod,
|
|
@@ -2640,7 +2711,7 @@ function unstable_createStaticHandler(routes, opts) {
|
|
|
2640
2711
|
invariant(request.signal, "query()/queryRoute() requests must contain an AbortController signal");
|
|
2641
2712
|
|
|
2642
2713
|
try {
|
|
2643
|
-
if (
|
|
2714
|
+
if (isMutationMethod(request.method.toLowerCase())) {
|
|
2644
2715
|
let result = await submit(request, matches, routeMatch || getTargetMatch(matches, location), requestContext, routeMatch != null);
|
|
2645
2716
|
return result;
|
|
2646
2717
|
}
|
|
@@ -2757,6 +2828,8 @@ function unstable_createStaticHandler(routes, opts) {
|
|
|
2757
2828
|
|
|
2758
2829
|
|
|
2759
2830
|
let loaderRequest = new Request(request.url, {
|
|
2831
|
+
headers: request.headers,
|
|
2832
|
+
redirect: request.redirect,
|
|
2760
2833
|
signal: request.signal
|
|
2761
2834
|
});
|
|
2762
2835
|
let context = await loadRouteData(loaderRequest, matches, requestContext);
|
|
@@ -2872,16 +2945,22 @@ function normalizeNavigateOptions(to, opts, isFetcher) {
|
|
|
2872
2945
|
} // Create a Submission on non-GET navigations
|
|
2873
2946
|
|
|
2874
2947
|
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
}
|
|
2948
|
+
let submission;
|
|
2949
|
+
|
|
2950
|
+
if (opts.formData) {
|
|
2951
|
+
submission = {
|
|
2952
|
+
formMethod: opts.formMethod || "get",
|
|
2953
|
+
formAction: stripHashFromPath(path),
|
|
2954
|
+
formEncType: opts && opts.formEncType || "application/x-www-form-urlencoded",
|
|
2955
|
+
formData: opts.formData
|
|
2884
2956
|
};
|
|
2957
|
+
|
|
2958
|
+
if (isMutationMethod(submission.formMethod)) {
|
|
2959
|
+
return {
|
|
2960
|
+
path,
|
|
2961
|
+
submission
|
|
2962
|
+
};
|
|
2963
|
+
}
|
|
2885
2964
|
} // Flatten submission onto URLSearchParams for GET submissions
|
|
2886
2965
|
|
|
2887
2966
|
|
|
@@ -2905,7 +2984,8 @@ function normalizeNavigateOptions(to, opts, isFetcher) {
|
|
|
2905
2984
|
}
|
|
2906
2985
|
|
|
2907
2986
|
return {
|
|
2908
|
-
path: createPath(parsedPath)
|
|
2987
|
+
path: createPath(parsedPath),
|
|
2988
|
+
submission
|
|
2909
2989
|
};
|
|
2910
2990
|
} // Filter out all routes below any caught error as they aren't going to
|
|
2911
2991
|
// render so we don't need to load them
|
|
@@ -3149,7 +3229,7 @@ function createClientSideRequest(location, signal, submission) {
|
|
|
3149
3229
|
signal
|
|
3150
3230
|
};
|
|
3151
3231
|
|
|
3152
|
-
if (submission) {
|
|
3232
|
+
if (submission && isMutationMethod(submission.formMethod)) {
|
|
3153
3233
|
let {
|
|
3154
3234
|
formMethod,
|
|
3155
3235
|
formEncType,
|
|
@@ -3199,11 +3279,14 @@ function processRouteLoaderData(matches, matchesToLoad, results, pendingError, a
|
|
|
3199
3279
|
pendingError = undefined;
|
|
3200
3280
|
}
|
|
3201
3281
|
|
|
3202
|
-
errors =
|
|
3203
|
-
|
|
3204
|
-
|
|
3282
|
+
errors = errors || {}; // Prefer higher error values if lower errors bubble to the same boundary
|
|
3283
|
+
|
|
3284
|
+
if (errors[boundaryMatch.route.id] == null) {
|
|
3285
|
+
errors[boundaryMatch.route.id] = error;
|
|
3286
|
+
} // Once we find our first (highest) error, we set the status code and
|
|
3205
3287
|
// prevent deeper status codes from overriding
|
|
3206
3288
|
|
|
3289
|
+
|
|
3207
3290
|
if (!foundError) {
|
|
3208
3291
|
foundError = true;
|
|
3209
3292
|
statusCode = isRouteErrorResponse(result.error) ? result.error.status : 500;
|
|
@@ -3419,8 +3502,8 @@ function isValidMethod(method) {
|
|
|
3419
3502
|
return validRequestMethods.has(method);
|
|
3420
3503
|
}
|
|
3421
3504
|
|
|
3422
|
-
function
|
|
3423
|
-
return
|
|
3505
|
+
function isMutationMethod(method) {
|
|
3506
|
+
return validMutationMethods.has(method);
|
|
3424
3507
|
}
|
|
3425
3508
|
|
|
3426
3509
|
async function resolveDeferredResults(currentMatches, matchesToLoad, results, signal, isFetcher, currentLoaderData) {
|