@remix-run/router 1.10.0 → 1.11.0-pre.1
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 +27 -0
- package/dist/router.cjs.js +109 -33
- package/dist/router.cjs.js.map +1 -1
- package/dist/router.d.ts +4 -2
- package/dist/router.js +105 -33
- package/dist/router.js.map +1 -1
- package/dist/router.umd.js +109 -33
- 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/package.json +1 -1
- package/router.ts +84 -20
- package/utils.ts +19 -15
package/package.json
CHANGED
package/router.ts
CHANGED
|
@@ -193,7 +193,7 @@ export interface Router {
|
|
|
193
193
|
* Get/create a fetcher for the given key
|
|
194
194
|
* @param key
|
|
195
195
|
*/
|
|
196
|
-
getFetcher<TData = any>(key
|
|
196
|
+
getFetcher<TData = any>(key: string): Fetcher<TData>;
|
|
197
197
|
|
|
198
198
|
/**
|
|
199
199
|
* @internal
|
|
@@ -202,7 +202,7 @@ export interface Router {
|
|
|
202
202
|
* Delete the fetcher for a given key
|
|
203
203
|
* @param key
|
|
204
204
|
*/
|
|
205
|
-
deleteFetcher(key
|
|
205
|
+
deleteFetcher(key: string): void;
|
|
206
206
|
|
|
207
207
|
/**
|
|
208
208
|
* @internal
|
|
@@ -343,6 +343,7 @@ export type HydrationState = Partial<
|
|
|
343
343
|
* Future flags to toggle new feature behavior
|
|
344
344
|
*/
|
|
345
345
|
export interface FutureConfig {
|
|
346
|
+
v7_fetcherPersist: boolean;
|
|
346
347
|
v7_normalizeFormMethod: boolean;
|
|
347
348
|
v7_prependBasename: boolean;
|
|
348
349
|
}
|
|
@@ -408,6 +409,7 @@ export interface RouterSubscriber {
|
|
|
408
409
|
(
|
|
409
410
|
state: RouterState,
|
|
410
411
|
opts: {
|
|
412
|
+
deletedFetchers: string[];
|
|
411
413
|
unstable_viewTransitionOpts?: ViewTransitionOpts;
|
|
412
414
|
}
|
|
413
415
|
): void;
|
|
@@ -763,6 +765,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
763
765
|
let basename = init.basename || "/";
|
|
764
766
|
// Config driven behavior flags
|
|
765
767
|
let future: FutureConfig = {
|
|
768
|
+
v7_fetcherPersist: false,
|
|
766
769
|
v7_normalizeFormMethod: false,
|
|
767
770
|
v7_prependBasename: false,
|
|
768
771
|
...init.future,
|
|
@@ -885,6 +888,13 @@ export function createRouter(init: RouterInit): Router {
|
|
|
885
888
|
// Most recent href/match for fetcher.load calls for fetchers
|
|
886
889
|
let fetchLoadMatches = new Map<string, FetchLoadMatch>();
|
|
887
890
|
|
|
891
|
+
// Ref-count mounted fetchers so we know when it's ok to clean them up
|
|
892
|
+
let activeFetchers = new Map<string, number>();
|
|
893
|
+
|
|
894
|
+
// Fetchers that have requested a delete when using v7_fetcherPersist,
|
|
895
|
+
// they'll be officially removed after they return to idle
|
|
896
|
+
let deletedFetchers = new Set<string>();
|
|
897
|
+
|
|
888
898
|
// Store DeferredData instances for active route matches. When a
|
|
889
899
|
// route loader returns defer() we stick one in here. Then, when a nested
|
|
890
900
|
// promise resolves we update loaderData. If a new navigation starts we
|
|
@@ -1014,9 +1024,39 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1014
1024
|
...state,
|
|
1015
1025
|
...newState,
|
|
1016
1026
|
};
|
|
1027
|
+
|
|
1028
|
+
// Prep fetcher cleanup so we can tell the UI which fetcher data entries
|
|
1029
|
+
// can be removed
|
|
1030
|
+
let completedFetchers: string[] = [];
|
|
1031
|
+
let deletedFetchersKeys: string[] = [];
|
|
1032
|
+
|
|
1033
|
+
if (future.v7_fetcherPersist) {
|
|
1034
|
+
state.fetchers.forEach((fetcher, key) => {
|
|
1035
|
+
if (fetcher.state === "idle") {
|
|
1036
|
+
if (deletedFetchers.has(key)) {
|
|
1037
|
+
// Unmounted from the UI and can be totally removed
|
|
1038
|
+
deletedFetchersKeys.push(key);
|
|
1039
|
+
} else {
|
|
1040
|
+
// Returned to idle but still mounted in the UI, so semi-remains for
|
|
1041
|
+
// revalidations and such
|
|
1042
|
+
completedFetchers.push(key);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1017
1048
|
subscribers.forEach((subscriber) =>
|
|
1018
|
-
subscriber(state, {
|
|
1049
|
+
subscriber(state, {
|
|
1050
|
+
deletedFetchers: deletedFetchersKeys,
|
|
1051
|
+
unstable_viewTransitionOpts: viewTransitionOpts,
|
|
1052
|
+
})
|
|
1019
1053
|
);
|
|
1054
|
+
|
|
1055
|
+
// Remove idle fetchers from state since we only care about in-flight fetchers.
|
|
1056
|
+
if (future.v7_fetcherPersist) {
|
|
1057
|
+
completedFetchers.forEach((key) => state.fetchers.delete(key));
|
|
1058
|
+
deletedFetchersKeys.forEach((key) => deleteFetcher(key));
|
|
1059
|
+
}
|
|
1020
1060
|
}
|
|
1021
1061
|
|
|
1022
1062
|
// Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION
|
|
@@ -1725,6 +1765,14 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1725
1765
|
}
|
|
1726
1766
|
|
|
1727
1767
|
function getFetcher<TData = any>(key: string): Fetcher<TData> {
|
|
1768
|
+
if (future.v7_fetcherPersist) {
|
|
1769
|
+
activeFetchers.set(key, (activeFetchers.get(key) || 0) + 1);
|
|
1770
|
+
// If this fetcher was previously marked for deletion, unmark it since we
|
|
1771
|
+
// have a new instance
|
|
1772
|
+
if (deletedFetchers.has(key)) {
|
|
1773
|
+
deletedFetchers.delete(key);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1728
1776
|
return state.fetchers.get(key) || IDLE_FETCHER;
|
|
1729
1777
|
}
|
|
1730
1778
|
|
|
@@ -1844,7 +1892,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1844
1892
|
);
|
|
1845
1893
|
|
|
1846
1894
|
if (fetchRequest.signal.aborted) {
|
|
1847
|
-
// We can delete this so long as we weren't aborted by
|
|
1895
|
+
// We can delete this so long as we weren't aborted by our own fetcher
|
|
1848
1896
|
// re-submit which would have put _new_ controller is in fetchControllers
|
|
1849
1897
|
if (fetchControllers.get(key) === abortController) {
|
|
1850
1898
|
fetchControllers.delete(key);
|
|
@@ -1852,6 +1900,12 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1852
1900
|
return;
|
|
1853
1901
|
}
|
|
1854
1902
|
|
|
1903
|
+
if (deletedFetchers.has(key)) {
|
|
1904
|
+
state.fetchers.set(key, getDoneFetcher(undefined));
|
|
1905
|
+
updateState({ fetchers: new Map(state.fetchers) });
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1855
1909
|
if (isRedirectResult(actionResult)) {
|
|
1856
1910
|
fetchControllers.delete(key);
|
|
1857
1911
|
if (pendingNavigationLoadId > originatingLoadId) {
|
|
@@ -2009,7 +2063,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2009
2063
|
state.fetchers.set(key, doneFetcher);
|
|
2010
2064
|
}
|
|
2011
2065
|
|
|
2012
|
-
|
|
2066
|
+
abortStaleFetchLoads(loadId);
|
|
2013
2067
|
|
|
2014
2068
|
// If we are currently in a navigation loading state and this fetcher is
|
|
2015
2069
|
// more recent than the navigation, we want the newer data so abort the
|
|
@@ -2039,9 +2093,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2039
2093
|
matches,
|
|
2040
2094
|
errors
|
|
2041
2095
|
),
|
|
2042
|
-
|
|
2043
|
-
? { fetchers: new Map(state.fetchers) }
|
|
2044
|
-
: {}),
|
|
2096
|
+
fetchers: new Map(state.fetchers),
|
|
2045
2097
|
});
|
|
2046
2098
|
isRevalidationRequired = false;
|
|
2047
2099
|
}
|
|
@@ -2105,6 +2157,12 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2105
2157
|
return;
|
|
2106
2158
|
}
|
|
2107
2159
|
|
|
2160
|
+
if (deletedFetchers.has(key)) {
|
|
2161
|
+
state.fetchers.set(key, getDoneFetcher(undefined));
|
|
2162
|
+
updateState({ fetchers: new Map(state.fetchers) });
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2108
2166
|
// If the loader threw a redirect Response, start a new REPLACE navigation
|
|
2109
2167
|
if (isRedirectResult(result)) {
|
|
2110
2168
|
if (pendingNavigationLoadId > originatingLoadId) {
|
|
@@ -2123,17 +2181,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2123
2181
|
|
|
2124
2182
|
// Process any non-redirect errors thrown
|
|
2125
2183
|
if (isErrorResult(result)) {
|
|
2126
|
-
|
|
2127
|
-
state.fetchers.delete(key);
|
|
2128
|
-
// TODO: In remix, this would reset to IDLE_NAVIGATION if it was a catch -
|
|
2129
|
-
// do we need to behave any differently with our non-redirect errors?
|
|
2130
|
-
// What if it was a non-redirect Response?
|
|
2131
|
-
updateState({
|
|
2132
|
-
fetchers: new Map(state.fetchers),
|
|
2133
|
-
errors: {
|
|
2134
|
-
[boundaryMatch.route.id]: result.error,
|
|
2135
|
-
},
|
|
2136
|
-
});
|
|
2184
|
+
setFetcherError(key, routeId, result.error);
|
|
2137
2185
|
return;
|
|
2138
2186
|
}
|
|
2139
2187
|
|
|
@@ -2376,9 +2424,25 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2376
2424
|
fetchLoadMatches.delete(key);
|
|
2377
2425
|
fetchReloadIds.delete(key);
|
|
2378
2426
|
fetchRedirectIds.delete(key);
|
|
2427
|
+
deletedFetchers.delete(key);
|
|
2379
2428
|
state.fetchers.delete(key);
|
|
2380
2429
|
}
|
|
2381
2430
|
|
|
2431
|
+
function deleteFetcherAndUpdateState(key: string): void {
|
|
2432
|
+
if (future.v7_fetcherPersist) {
|
|
2433
|
+
let count = (activeFetchers.get(key) || 0) - 1;
|
|
2434
|
+
if (count <= 0) {
|
|
2435
|
+
activeFetchers.delete(key);
|
|
2436
|
+
deletedFetchers.add(key);
|
|
2437
|
+
} else {
|
|
2438
|
+
activeFetchers.set(key, count);
|
|
2439
|
+
}
|
|
2440
|
+
} else {
|
|
2441
|
+
deleteFetcher(key);
|
|
2442
|
+
}
|
|
2443
|
+
updateState({ fetchers: new Map(state.fetchers) });
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2382
2446
|
function abortFetcher(key: string) {
|
|
2383
2447
|
let controller = fetchControllers.get(key);
|
|
2384
2448
|
invariant(controller, `Expected fetch controller: ${key}`);
|
|
@@ -2613,7 +2677,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2613
2677
|
createHref: (to: To) => init.history.createHref(to),
|
|
2614
2678
|
encodeLocation: (to: To) => init.history.encodeLocation(to),
|
|
2615
2679
|
getFetcher,
|
|
2616
|
-
deleteFetcher,
|
|
2680
|
+
deleteFetcher: deleteFetcherAndUpdateState,
|
|
2617
2681
|
dispose,
|
|
2618
2682
|
getBlocker,
|
|
2619
2683
|
deleteBlocker,
|
package/utils.ts
CHANGED
|
@@ -903,7 +903,7 @@ export function matchPath<
|
|
|
903
903
|
pattern = { path: pattern, caseSensitive: false, end: true };
|
|
904
904
|
}
|
|
905
905
|
|
|
906
|
-
let [matcher,
|
|
906
|
+
let [matcher, compiledParams] = compilePath(
|
|
907
907
|
pattern.path,
|
|
908
908
|
pattern.caseSensitive,
|
|
909
909
|
pattern.end
|
|
@@ -915,8 +915,8 @@ export function matchPath<
|
|
|
915
915
|
let matchedPathname = match[0];
|
|
916
916
|
let pathnameBase = matchedPathname.replace(/(.)\/+$/, "$1");
|
|
917
917
|
let captureGroups = match.slice(1);
|
|
918
|
-
let params: Params =
|
|
919
|
-
(memo, paramName, index) => {
|
|
918
|
+
let params: Params = compiledParams.reduce<Mutable<Params>>(
|
|
919
|
+
(memo, { paramName, isOptional }, index) => {
|
|
920
920
|
// We need to compute the pathnameBase here using the raw splat value
|
|
921
921
|
// instead of using params["*"] later because it will be decoded then
|
|
922
922
|
if (paramName === "*") {
|
|
@@ -926,10 +926,12 @@ export function matchPath<
|
|
|
926
926
|
.replace(/(.)\/+$/, "$1");
|
|
927
927
|
}
|
|
928
928
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
paramName
|
|
932
|
-
|
|
929
|
+
const value = captureGroups[index];
|
|
930
|
+
if (isOptional && !value) {
|
|
931
|
+
memo[paramName] = undefined;
|
|
932
|
+
} else {
|
|
933
|
+
memo[paramName] = safelyDecodeURIComponent(value || "", paramName);
|
|
934
|
+
}
|
|
933
935
|
return memo;
|
|
934
936
|
},
|
|
935
937
|
{}
|
|
@@ -943,11 +945,13 @@ export function matchPath<
|
|
|
943
945
|
};
|
|
944
946
|
}
|
|
945
947
|
|
|
948
|
+
type CompiledPathParam = { paramName: string; isOptional?: boolean };
|
|
949
|
+
|
|
946
950
|
function compilePath(
|
|
947
951
|
path: string,
|
|
948
952
|
caseSensitive = false,
|
|
949
953
|
end = true
|
|
950
|
-
): [RegExp,
|
|
954
|
+
): [RegExp, CompiledPathParam[]] {
|
|
951
955
|
warning(
|
|
952
956
|
path === "*" || !path.endsWith("*") || path.endsWith("/*"),
|
|
953
957
|
`Route path "${path}" will be treated as if it were ` +
|
|
@@ -956,20 +960,20 @@ function compilePath(
|
|
|
956
960
|
`please change the route path to "${path.replace(/\*$/, "/*")}".`
|
|
957
961
|
);
|
|
958
962
|
|
|
959
|
-
let
|
|
963
|
+
let params: CompiledPathParam[] = [];
|
|
960
964
|
let regexpSource =
|
|
961
965
|
"^" +
|
|
962
966
|
path
|
|
963
967
|
.replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
|
|
964
968
|
.replace(/^\/*/, "/") // Make sure it has a leading /
|
|
965
|
-
.replace(/[
|
|
966
|
-
.replace(/\/:(\w+)
|
|
967
|
-
|
|
968
|
-
return "/([^\\/]+)";
|
|
969
|
+
.replace(/[\\.*+^${}|()[\]]/g, "\\$&") // Escape special regex chars
|
|
970
|
+
.replace(/\/:(\w+)(\?)?/g, (_: string, paramName: string, isOptional) => {
|
|
971
|
+
params.push({ paramName, isOptional: isOptional != null });
|
|
972
|
+
return isOptional ? "/?([^\\/]+)?" : "/([^\\/]+)";
|
|
969
973
|
});
|
|
970
974
|
|
|
971
975
|
if (path.endsWith("*")) {
|
|
972
|
-
|
|
976
|
+
params.push({ paramName: "*" });
|
|
973
977
|
regexpSource +=
|
|
974
978
|
path === "*" || path === "/*"
|
|
975
979
|
? "(.*)$" // Already matched the initial /, just match the rest
|
|
@@ -992,7 +996,7 @@ function compilePath(
|
|
|
992
996
|
|
|
993
997
|
let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");
|
|
994
998
|
|
|
995
|
-
return [matcher,
|
|
999
|
+
return [matcher, params];
|
|
996
1000
|
}
|
|
997
1001
|
|
|
998
1002
|
function safelyDecodeURI(value: string) {
|