@remix-run/router 1.6.3 → 1.7.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/dist/router.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @remix-run/router v1.6.3
2
+ * @remix-run/router v1.7.0
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -708,26 +708,21 @@ function generatePath(originalPath, params) {
708
708
  }
709
709
  // ensure `/` is added at the beginning if the path is absolute
710
710
  const prefix = path.startsWith("/") ? "/" : "";
711
+ const stringify = p => p == null ? "" : typeof p === "string" ? p : String(p);
711
712
  const segments = path.split(/\/+/).map((segment, index, array) => {
712
713
  const isLastSegment = index === array.length - 1;
713
714
  // only apply the splat if it's the last segment
714
715
  if (isLastSegment && segment === "*") {
715
716
  const star = "*";
716
- const starParam = params[star];
717
717
  // Apply the splat
718
- return starParam;
718
+ return stringify(params[star]);
719
719
  }
720
720
  const keyMatch = segment.match(/^:(\w+)(\??)$/);
721
721
  if (keyMatch) {
722
722
  const [, key, optional] = keyMatch;
723
723
  let param = params[key];
724
- if (optional === "?") {
725
- return param == null ? "" : param;
726
- }
727
- if (param == null) {
728
- invariant(false, "Missing \":" + key + "\" param");
729
- }
730
- return param;
724
+ invariant(optional === "?" || param != null, "Missing \":" + key + "\" param");
725
+ return stringify(param);
731
726
  }
732
727
  // Remove any optional markers from optional static segments
733
728
  return segment.replace(/\?$/g, "");
@@ -1202,7 +1197,9 @@ const IDLE_NAVIGATION = {
1202
1197
  formMethod: undefined,
1203
1198
  formAction: undefined,
1204
1199
  formEncType: undefined,
1205
- formData: undefined
1200
+ formData: undefined,
1201
+ json: undefined,
1202
+ text: undefined
1206
1203
  };
1207
1204
  const IDLE_FETCHER = {
1208
1205
  state: "idle",
@@ -1210,7 +1207,9 @@ const IDLE_FETCHER = {
1210
1207
  formMethod: undefined,
1211
1208
  formAction: undefined,
1212
1209
  formEncType: undefined,
1213
- formData: undefined
1210
+ formData: undefined,
1211
+ json: undefined,
1212
+ text: undefined
1214
1213
  };
1215
1214
  const IDLE_BLOCKER = {
1216
1215
  state: "unblocked",
@@ -1404,9 +1403,10 @@ function createRouter(init) {
1404
1403
  init.history.go(delta);
1405
1404
  },
1406
1405
  reset() {
1407
- deleteBlocker(blockerKey);
1406
+ let blockers = new Map(state.blockers);
1407
+ blockers.set(blockerKey, IDLE_BLOCKER);
1408
1408
  updateState({
1409
- blockers: new Map(router.state.blockers)
1409
+ blockers
1410
1410
  });
1411
1411
  }
1412
1412
  });
@@ -1476,9 +1476,8 @@ function createRouter(init) {
1476
1476
  let loaderData = newState.loaderData ? mergeLoaderData(state.loaderData, newState.loaderData, newState.matches || [], newState.errors) : state.loaderData;
1477
1477
  // On a successful navigation we can assume we got through all blockers
1478
1478
  // so we can start fresh
1479
- for (let [key] of blockerFunctions) {
1480
- deleteBlocker(key);
1481
- }
1479
+ let blockers = new Map();
1480
+ blockerFunctions.clear();
1482
1481
  // Always respect the user flag. Otherwise don't reset on mutation
1483
1482
  // submission navigations unless they redirect
1484
1483
  let preventScrollReset = pendingPreventScrollReset === true || state.navigation.formMethod != null && isMutationMethod(state.navigation.formMethod) && ((_location$state2 = location.state) == null ? void 0 : _location$state2._isRedirect) !== true;
@@ -1486,6 +1485,11 @@ function createRouter(init) {
1486
1485
  dataRoutes = inFlightDataRoutes;
1487
1486
  inFlightDataRoutes = undefined;
1488
1487
  }
1488
+ if (isUninterruptedRevalidation) ; else if (pendingAction === Action.Pop) ; else if (pendingAction === Action.Push) {
1489
+ init.history.push(location, location.state);
1490
+ } else if (pendingAction === Action.Replace) {
1491
+ init.history.replace(location, location.state);
1492
+ }
1489
1493
  updateState(_extends({}, newState, {
1490
1494
  actionData,
1491
1495
  loaderData,
@@ -1496,13 +1500,8 @@ function createRouter(init) {
1496
1500
  revalidation: "idle",
1497
1501
  restoreScrollPosition: getSavedScrollPosition(location, newState.matches || state.matches),
1498
1502
  preventScrollReset,
1499
- blockers: new Map(state.blockers)
1503
+ blockers
1500
1504
  }));
1501
- if (isUninterruptedRevalidation) ; else if (pendingAction === Action.Pop) ; else if (pendingAction === Action.Push) {
1502
- init.history.push(location, location.state);
1503
- } else if (pendingAction === Action.Replace) {
1504
- init.history.replace(location, location.state);
1505
- }
1506
1505
  // Reset stateful navigation vars
1507
1506
  pendingAction = Action.Pop;
1508
1507
  pendingPreventScrollReset = false;
@@ -1565,9 +1564,10 @@ function createRouter(init) {
1565
1564
  navigate(to, opts);
1566
1565
  },
1567
1566
  reset() {
1568
- deleteBlocker(blockerKey);
1567
+ let blockers = new Map(state.blockers);
1568
+ blockers.set(blockerKey, IDLE_BLOCKER);
1569
1569
  updateState({
1570
- blockers: new Map(state.blockers)
1570
+ blockers
1571
1571
  });
1572
1572
  }
1573
1573
  });
@@ -1684,11 +1684,7 @@ function createRouter(init) {
1684
1684
  }
1685
1685
  pendingActionData = actionOutput.pendingActionData;
1686
1686
  pendingError = actionOutput.pendingActionError;
1687
- let navigation = _extends({
1688
- state: "loading",
1689
- location
1690
- }, opts.submission);
1691
- loadingNavigation = navigation;
1687
+ loadingNavigation = getLoadingNavigation(location, opts.submission);
1692
1688
  // Create a GET request for the loaders
1693
1689
  request = new Request(request.url, {
1694
1690
  signal: request.signal
@@ -1719,12 +1715,12 @@ function createRouter(init) {
1719
1715
  // Call the action matched by the leaf route for this navigation and handle
1720
1716
  // redirects/errors
1721
1717
  async function handleAction(request, location, submission, matches, opts) {
1718
+ if (opts === void 0) {
1719
+ opts = {};
1720
+ }
1722
1721
  interruptActiveLoads();
1723
1722
  // Put us in a submitting state
1724
- let navigation = _extends({
1725
- state: "submitting",
1726
- location
1727
- }, submission);
1723
+ let navigation = getSubmittingNavigation(location, submission);
1728
1724
  updateState({
1729
1725
  navigation
1730
1726
  });
@@ -1800,28 +1796,12 @@ function createRouter(init) {
1800
1796
  // errors, etc.
1801
1797
  async function handleLoaders(request, location, matches, overrideNavigation, submission, fetcherSubmission, replace, pendingActionData, pendingError) {
1802
1798
  // Figure out the right navigation we want to use for data loading
1803
- let loadingNavigation = overrideNavigation;
1804
- if (!loadingNavigation) {
1805
- let navigation = _extends({
1806
- state: "loading",
1807
- location,
1808
- formMethod: undefined,
1809
- formAction: undefined,
1810
- formEncType: undefined,
1811
- formData: undefined
1812
- }, submission);
1813
- loadingNavigation = navigation;
1814
- }
1799
+ let loadingNavigation = overrideNavigation || getLoadingNavigation(location, submission);
1815
1800
  // If this was a redirect from an action we don't have a "submission" but
1816
1801
  // we have it on the loading navigation so use that if available
1817
- let activeSubmission = submission || fetcherSubmission ? submission || fetcherSubmission : loadingNavigation.formMethod && loadingNavigation.formAction && loadingNavigation.formData && loadingNavigation.formEncType ? {
1818
- formMethod: loadingNavigation.formMethod,
1819
- formAction: loadingNavigation.formAction,
1820
- formData: loadingNavigation.formData,
1821
- formEncType: loadingNavigation.formEncType
1822
- } : undefined;
1802
+ let activeSubmission = submission || fetcherSubmission || getSubmissionFromNavigation(loadingNavigation);
1823
1803
  let routesToUse = inFlightDataRoutes || dataRoutes;
1824
- let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, activeSubmission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, fetchLoadMatches, routesToUse, basename, pendingActionData, pendingError);
1804
+ let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, activeSubmission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionData, pendingError);
1825
1805
  // Cancel pending deferreds for no-longer-matched routes or routes we're
1826
1806
  // about to reload. Note that if this is an action reload we would have
1827
1807
  // already cancelled all pending deferreds so this would be a no-op
@@ -1850,15 +1830,7 @@ function createRouter(init) {
1850
1830
  if (!isUninterruptedRevalidation) {
1851
1831
  revalidatingFetchers.forEach(rf => {
1852
1832
  let fetcher = state.fetchers.get(rf.key);
1853
- let revalidatingFetcher = {
1854
- state: "loading",
1855
- data: fetcher && fetcher.data,
1856
- formMethod: undefined,
1857
- formAction: undefined,
1858
- formEncType: undefined,
1859
- formData: undefined,
1860
- " _hasFetcherDoneAnything ": true
1861
- };
1833
+ let revalidatingFetcher = getLoadingFetcher(undefined, fetcher ? fetcher.data : undefined);
1862
1834
  state.fetchers.set(rf.key, revalidatingFetcher);
1863
1835
  });
1864
1836
  let actionData = pendingActionData || state.actionData;
@@ -1874,6 +1846,9 @@ function createRouter(init) {
1874
1846
  }
1875
1847
  pendingNavigationLoadId = ++incrementingLoadId;
1876
1848
  revalidatingFetchers.forEach(rf => {
1849
+ if (fetchControllers.has(rf.key)) {
1850
+ abortFetcher(rf.key);
1851
+ }
1877
1852
  if (rf.controller) {
1878
1853
  // Fetchers use an independent AbortController so that aborting a fetcher
1879
1854
  // (via deleteFetcher) does not abort the triggering navigation that
@@ -1959,8 +1934,13 @@ function createRouter(init) {
1959
1934
  }
1960
1935
  let {
1961
1936
  path,
1962
- submission
1937
+ submission,
1938
+ error
1963
1939
  } = normalizeNavigateOptions(future.v7_normalizeFormMethod, true, normalizedPath, opts);
1940
+ if (error) {
1941
+ setFetcherError(key, routeId, error);
1942
+ return;
1943
+ }
1964
1944
  let match = getTargetMatch(matches, path);
1965
1945
  pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
1966
1946
  if (submission && isMutationMethod(submission.formMethod)) {
@@ -1991,12 +1971,7 @@ function createRouter(init) {
1991
1971
  }
1992
1972
  // Put this fetcher into it's submitting state
1993
1973
  let existingFetcher = state.fetchers.get(key);
1994
- let fetcher = _extends({
1995
- state: "submitting"
1996
- }, submission, {
1997
- data: existingFetcher && existingFetcher.data,
1998
- " _hasFetcherDoneAnything ": true
1999
- });
1974
+ let fetcher = getSubmittingFetcher(submission, existingFetcher);
2000
1975
  state.fetchers.set(key, fetcher);
2001
1976
  updateState({
2002
1977
  fetchers: new Map(state.fetchers)
@@ -2017,12 +1992,7 @@ function createRouter(init) {
2017
1992
  if (isRedirectResult(actionResult)) {
2018
1993
  fetchControllers.delete(key);
2019
1994
  fetchRedirectIds.add(key);
2020
- let loadingFetcher = _extends({
2021
- state: "loading"
2022
- }, submission, {
2023
- data: undefined,
2024
- " _hasFetcherDoneAnything ": true
2025
- });
1995
+ let loadingFetcher = getLoadingFetcher(submission);
2026
1996
  state.fetchers.set(key, loadingFetcher);
2027
1997
  updateState({
2028
1998
  fetchers: new Map(state.fetchers)
@@ -2051,14 +2021,9 @@ function createRouter(init) {
2051
2021
  invariant(matches, "Didn't find any matches after fetcher action");
2052
2022
  let loadId = ++incrementingLoadId;
2053
2023
  fetchReloadIds.set(key, loadId);
2054
- let loadFetcher = _extends({
2055
- state: "loading",
2056
- data: actionResult.data
2057
- }, submission, {
2058
- " _hasFetcherDoneAnything ": true
2059
- });
2024
+ let loadFetcher = getLoadingFetcher(submission, actionResult.data);
2060
2025
  state.fetchers.set(key, loadFetcher);
2061
- let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, submission, nextLocation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, fetchLoadMatches, routesToUse, basename, {
2026
+ let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, submission, nextLocation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, {
2062
2027
  [match.route.id]: actionResult.data
2063
2028
  }, undefined // No need to send through errors since we short circuit above
2064
2029
  );
@@ -2068,16 +2033,11 @@ function createRouter(init) {
2068
2033
  revalidatingFetchers.filter(rf => rf.key !== key).forEach(rf => {
2069
2034
  let staleKey = rf.key;
2070
2035
  let existingFetcher = state.fetchers.get(staleKey);
2071
- let revalidatingFetcher = {
2072
- state: "loading",
2073
- data: existingFetcher && existingFetcher.data,
2074
- formMethod: undefined,
2075
- formAction: undefined,
2076
- formEncType: undefined,
2077
- formData: undefined,
2078
- " _hasFetcherDoneAnything ": true
2079
- };
2036
+ let revalidatingFetcher = getLoadingFetcher(undefined, existingFetcher ? existingFetcher.data : undefined);
2080
2037
  state.fetchers.set(staleKey, revalidatingFetcher);
2038
+ if (fetchControllers.has(staleKey)) {
2039
+ abortFetcher(staleKey);
2040
+ }
2081
2041
  if (rf.controller) {
2082
2042
  fetchControllers.set(staleKey, rf.controller);
2083
2043
  }
@@ -2111,15 +2071,7 @@ function createRouter(init) {
2111
2071
  // Since we let revalidations complete even if the submitting fetcher was
2112
2072
  // deleted, only put it back to idle if it hasn't been deleted
2113
2073
  if (state.fetchers.has(key)) {
2114
- let doneFetcher = {
2115
- state: "idle",
2116
- data: actionResult.data,
2117
- formMethod: undefined,
2118
- formAction: undefined,
2119
- formEncType: undefined,
2120
- formData: undefined,
2121
- " _hasFetcherDoneAnything ": true
2122
- };
2074
+ let doneFetcher = getDoneFetcher(actionResult.data);
2123
2075
  state.fetchers.set(key, doneFetcher);
2124
2076
  }
2125
2077
  let didAbortFetchLoads = abortStaleFetchLoads(loadId);
@@ -2152,16 +2104,7 @@ function createRouter(init) {
2152
2104
  async function handleFetcherLoader(key, routeId, path, match, matches, submission) {
2153
2105
  let existingFetcher = state.fetchers.get(key);
2154
2106
  // Put this fetcher into it's loading state
2155
- let loadingFetcher = _extends({
2156
- state: "loading",
2157
- formMethod: undefined,
2158
- formAction: undefined,
2159
- formEncType: undefined,
2160
- formData: undefined
2161
- }, submission, {
2162
- data: existingFetcher && existingFetcher.data,
2163
- " _hasFetcherDoneAnything ": true
2164
- });
2107
+ let loadingFetcher = getLoadingFetcher(submission, existingFetcher ? existingFetcher.data : undefined);
2165
2108
  state.fetchers.set(key, loadingFetcher);
2166
2109
  updateState({
2167
2110
  fetchers: new Map(state.fetchers)
@@ -2209,15 +2152,7 @@ function createRouter(init) {
2209
2152
  }
2210
2153
  invariant(!isDeferredResult(result), "Unhandled fetcher deferred data");
2211
2154
  // Put the fetcher back into an idle state
2212
- let doneFetcher = {
2213
- state: "idle",
2214
- data: result.data,
2215
- formMethod: undefined,
2216
- formAction: undefined,
2217
- formEncType: undefined,
2218
- formData: undefined,
2219
- " _hasFetcherDoneAnything ": true
2220
- };
2155
+ let doneFetcher = getDoneFetcher(result.data);
2221
2156
  state.fetchers.set(key, doneFetcher);
2222
2157
  updateState({
2223
2158
  fetchers: new Map(state.fetchers)
@@ -2277,26 +2212,13 @@ function createRouter(init) {
2277
2212
  let redirectHistoryAction = replace === true ? Action.Replace : Action.Push;
2278
2213
  // Use the incoming submission if provided, fallback on the active one in
2279
2214
  // state.navigation
2280
- let {
2281
- formMethod,
2282
- formAction,
2283
- formEncType,
2284
- formData
2285
- } = state.navigation;
2286
- if (!submission && formMethod && formAction && formData && formEncType) {
2287
- submission = {
2288
- formMethod,
2289
- formAction,
2290
- formEncType,
2291
- formData
2292
- };
2293
- }
2215
+ let activeSubmission = submission || getSubmissionFromNavigation(state.navigation);
2294
2216
  // If this was a 307/308 submission we want to preserve the HTTP method and
2295
2217
  // re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the
2296
2218
  // redirected location
2297
- if (redirectPreserveMethodStatusCodes.has(redirect.status) && submission && isMutationMethod(submission.formMethod)) {
2219
+ if (redirectPreserveMethodStatusCodes.has(redirect.status) && activeSubmission && isMutationMethod(activeSubmission.formMethod)) {
2298
2220
  await startNavigation(redirectHistoryAction, redirectLocation, {
2299
- submission: _extends({}, submission, {
2221
+ submission: _extends({}, activeSubmission, {
2300
2222
  formAction: redirect.location
2301
2223
  }),
2302
2224
  // Preserve this flag across redirects
@@ -2306,30 +2228,16 @@ function createRouter(init) {
2306
2228
  // For a fetch action redirect, we kick off a new loading navigation
2307
2229
  // without the fetcher submission, but we send it along for shouldRevalidate
2308
2230
  await startNavigation(redirectHistoryAction, redirectLocation, {
2309
- overrideNavigation: {
2310
- state: "loading",
2311
- location: redirectLocation,
2312
- formMethod: undefined,
2313
- formAction: undefined,
2314
- formEncType: undefined,
2315
- formData: undefined
2316
- },
2317
- fetcherSubmission: submission,
2231
+ overrideNavigation: getLoadingNavigation(redirectLocation),
2232
+ fetcherSubmission: activeSubmission,
2318
2233
  // Preserve this flag across redirects
2319
2234
  preventScrollReset: pendingPreventScrollReset
2320
2235
  });
2321
2236
  } else {
2322
- // Otherwise, we kick off a new loading navigation, preserving the
2323
- // submission info for the duration of this navigation
2237
+ // If we have a submission, we will preserve it through the redirect navigation
2238
+ let overrideNavigation = getLoadingNavigation(redirectLocation, activeSubmission);
2324
2239
  await startNavigation(redirectHistoryAction, redirectLocation, {
2325
- overrideNavigation: {
2326
- state: "loading",
2327
- location: redirectLocation,
2328
- formMethod: submission ? submission.formMethod : undefined,
2329
- formAction: submission ? submission.formAction : undefined,
2330
- formEncType: submission ? submission.formEncType : undefined,
2331
- formData: submission ? submission.formData : undefined
2332
- },
2240
+ overrideNavigation,
2333
2241
  // Preserve this flag across redirects
2334
2242
  preventScrollReset: pendingPreventScrollReset
2335
2243
  });
@@ -2407,15 +2315,7 @@ function createRouter(init) {
2407
2315
  function markFetchersDone(keys) {
2408
2316
  for (let key of keys) {
2409
2317
  let fetcher = getFetcher(key);
2410
- let doneFetcher = {
2411
- state: "idle",
2412
- data: fetcher.data,
2413
- formMethod: undefined,
2414
- formAction: undefined,
2415
- formEncType: undefined,
2416
- formData: undefined,
2417
- " _hasFetcherDoneAnything ": true
2418
- };
2318
+ let doneFetcher = getDoneFetcher(fetcher.data);
2419
2319
  state.fetchers.set(key, doneFetcher);
2420
2320
  }
2421
2321
  }
@@ -2467,9 +2367,10 @@ function createRouter(init) {
2467
2367
  // Poor mans state machine :)
2468
2368
  // https://mermaid.live/edit#pako:eNqVkc9OwzAMxl8l8nnjAYrEtDIOHEBIgwvKJTReGy3_lDpIqO27k6awMG0XcrLlnz87nwdonESogKXXBuE79rq75XZO3-yHds0RJVuv70YrPlUrCEe2HfrORS3rubqZfuhtpg5C9wk5tZ4VKcRUq88q9Z8RS0-48cE1iHJkL0ugbHuFLus9L6spZy8nX9MP2CNdomVaposqu3fGayT8T8-jJQwhepo_UtpgBQaDEUom04dZhAN1aJBDlUKJBxE1ceB2Smj0Mln-IBW5AFU2dwUiktt_2Qaq2dBfaKdEup85UV7Yd-dKjlnkabl2Pvr0DTkTreM
2469
2369
  invariant(blocker.state === "unblocked" && newBlocker.state === "blocked" || blocker.state === "blocked" && newBlocker.state === "blocked" || blocker.state === "blocked" && newBlocker.state === "proceeding" || blocker.state === "blocked" && newBlocker.state === "unblocked" || blocker.state === "proceeding" && newBlocker.state === "unblocked", "Invalid blocker state transition: " + blocker.state + " -> " + newBlocker.state);
2470
- state.blockers.set(key, newBlocker);
2370
+ let blockers = new Map(state.blockers);
2371
+ blockers.set(key, newBlocker);
2471
2372
  updateState({
2472
- blockers: new Map(state.blockers)
2373
+ blockers
2473
2374
  });
2474
2375
  }
2475
2376
  function shouldBlockNavigation(_ref2) {
@@ -2523,7 +2424,7 @@ function createRouter(init) {
2523
2424
  function enableScrollRestoration(positions, getPosition, getKey) {
2524
2425
  savedScrollPositions = positions;
2525
2426
  getScrollPosition = getPosition;
2526
- getScrollRestorationKey = getKey || (location => location.key);
2427
+ getScrollRestorationKey = getKey || null;
2527
2428
  // Perform initial hydration scroll restoration, since we miss the boat on
2528
2429
  // the initial updateState() because we've not yet rendered <ScrollRestoration/>
2529
2430
  // and therefore have no savedScrollPositions available
@@ -2542,17 +2443,22 @@ function createRouter(init) {
2542
2443
  getScrollRestorationKey = null;
2543
2444
  };
2544
2445
  }
2446
+ function getScrollKey(location, matches) {
2447
+ if (getScrollRestorationKey) {
2448
+ let key = getScrollRestorationKey(location, matches.map(m => createUseMatchesMatch(m, state.loaderData)));
2449
+ return key || location.key;
2450
+ }
2451
+ return location.key;
2452
+ }
2545
2453
  function saveScrollPosition(location, matches) {
2546
- if (savedScrollPositions && getScrollRestorationKey && getScrollPosition) {
2547
- let userMatches = matches.map(m => createUseMatchesMatch(m, state.loaderData));
2548
- let key = getScrollRestorationKey(location, userMatches) || location.key;
2454
+ if (savedScrollPositions && getScrollPosition) {
2455
+ let key = getScrollKey(location, matches);
2549
2456
  savedScrollPositions[key] = getScrollPosition();
2550
2457
  }
2551
2458
  }
2552
2459
  function getSavedScrollPosition(location, matches) {
2553
- if (savedScrollPositions && getScrollRestorationKey && getScrollPosition) {
2554
- let userMatches = matches.map(m => createUseMatchesMatch(m, state.loaderData));
2555
- let key = getScrollRestorationKey(location, userMatches) || location.key;
2460
+ if (savedScrollPositions) {
2461
+ let key = getScrollKey(location, matches);
2556
2462
  let y = savedScrollPositions[key];
2557
2463
  if (typeof y === "number") {
2558
2464
  return y;
@@ -2827,7 +2733,11 @@ function createStaticHandler(routes, opts) {
2827
2733
  error
2828
2734
  };
2829
2735
  } else {
2830
- result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, true, isRouteRequest, requestContext);
2736
+ result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, {
2737
+ isStaticRequest: true,
2738
+ isRouteRequest,
2739
+ requestContext
2740
+ });
2831
2741
  if (request.signal.aborted) {
2832
2742
  let method = isRouteRequest ? "queryRoute" : "query";
2833
2743
  throw new Error(method + "() call aborted");
@@ -2938,7 +2848,11 @@ function createStaticHandler(routes, opts) {
2938
2848
  activeDeferreds: null
2939
2849
  };
2940
2850
  }
2941
- let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename, true, isRouteRequest, requestContext))]);
2851
+ let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename, {
2852
+ isStaticRequest: true,
2853
+ isRouteRequest,
2854
+ requestContext
2855
+ }))]);
2942
2856
  if (request.signal.aborted) {
2943
2857
  let method = isRouteRequest ? "queryRoute" : "query";
2944
2858
  throw new Error(method + "() call aborted");
@@ -2982,7 +2896,7 @@ function getStaticContextFromError(routes, context, error) {
2982
2896
  return newContext;
2983
2897
  }
2984
2898
  function isSubmissionNavigation(opts) {
2985
- return opts != null && "formData" in opts;
2899
+ return opts != null && ("formData" in opts && opts.formData != null || "body" in opts && opts.body !== undefined);
2986
2900
  }
2987
2901
  function normalizeTo(location, matches, basename, prependBasename, to, fromRouteId, relative) {
2988
2902
  let contextualMatches;
@@ -3043,26 +2957,101 @@ function normalizeNavigateOptions(normalizeFormMethod, isFetcher, path, opts) {
3043
2957
  })
3044
2958
  };
3045
2959
  }
2960
+ let getInvalidBodyError = () => ({
2961
+ path,
2962
+ error: getInternalRouterError(400, {
2963
+ type: "invalid-body"
2964
+ })
2965
+ });
3046
2966
  // Create a Submission on non-GET navigations
3047
- let submission;
3048
- if (opts.formData) {
3049
- let formMethod = opts.formMethod || "get";
3050
- submission = {
3051
- formMethod: normalizeFormMethod ? formMethod.toUpperCase() : formMethod.toLowerCase(),
3052
- formAction: stripHashFromPath(path),
3053
- formEncType: opts && opts.formEncType || "application/x-www-form-urlencoded",
3054
- formData: opts.formData
3055
- };
3056
- if (isMutationMethod(submission.formMethod)) {
2967
+ let rawFormMethod = opts.formMethod || "get";
2968
+ let formMethod = normalizeFormMethod ? rawFormMethod.toUpperCase() : rawFormMethod.toLowerCase();
2969
+ let formAction = stripHashFromPath(path);
2970
+ if (opts.body !== undefined) {
2971
+ if (opts.formEncType === "text/plain") {
2972
+ // text only support POST/PUT/PATCH/DELETE submissions
2973
+ if (!isMutationMethod(formMethod)) {
2974
+ return getInvalidBodyError();
2975
+ }
2976
+ let text = typeof opts.body === "string" ? opts.body : opts.body instanceof FormData || opts.body instanceof URLSearchParams ?
2977
+ // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#plain-text-form-data
2978
+ Array.from(opts.body.entries()).reduce((acc, _ref3) => {
2979
+ let [name, value] = _ref3;
2980
+ return "" + acc + name + "=" + value + "\n";
2981
+ }, "") : String(opts.body);
3057
2982
  return {
3058
2983
  path,
3059
- submission
2984
+ submission: {
2985
+ formMethod,
2986
+ formAction,
2987
+ formEncType: opts.formEncType,
2988
+ formData: undefined,
2989
+ json: undefined,
2990
+ text
2991
+ }
3060
2992
  };
2993
+ } else if (opts.formEncType === "application/json") {
2994
+ // json only supports POST/PUT/PATCH/DELETE submissions
2995
+ if (!isMutationMethod(formMethod)) {
2996
+ return getInvalidBodyError();
2997
+ }
2998
+ try {
2999
+ let json = typeof opts.body === "string" ? JSON.parse(opts.body) : opts.body;
3000
+ return {
3001
+ path,
3002
+ submission: {
3003
+ formMethod,
3004
+ formAction,
3005
+ formEncType: opts.formEncType,
3006
+ formData: undefined,
3007
+ json,
3008
+ text: undefined
3009
+ }
3010
+ };
3011
+ } catch (e) {
3012
+ return getInvalidBodyError();
3013
+ }
3014
+ }
3015
+ }
3016
+ invariant(typeof FormData === "function", "FormData is not available in this environment");
3017
+ let searchParams;
3018
+ let formData;
3019
+ if (opts.formData) {
3020
+ searchParams = convertFormDataToSearchParams(opts.formData);
3021
+ formData = opts.formData;
3022
+ } else if (opts.body instanceof FormData) {
3023
+ searchParams = convertFormDataToSearchParams(opts.body);
3024
+ formData = opts.body;
3025
+ } else if (opts.body instanceof URLSearchParams) {
3026
+ searchParams = opts.body;
3027
+ formData = convertSearchParamsToFormData(searchParams);
3028
+ } else if (opts.body == null) {
3029
+ searchParams = new URLSearchParams();
3030
+ formData = new FormData();
3031
+ } else {
3032
+ try {
3033
+ searchParams = new URLSearchParams(opts.body);
3034
+ formData = convertSearchParamsToFormData(searchParams);
3035
+ } catch (e) {
3036
+ return getInvalidBodyError();
3061
3037
  }
3062
3038
  }
3039
+ let submission = {
3040
+ formMethod,
3041
+ formAction,
3042
+ formEncType: opts && opts.formEncType || "application/x-www-form-urlencoded",
3043
+ formData,
3044
+ json: undefined,
3045
+ text: undefined
3046
+ };
3047
+ if (isMutationMethod(submission.formMethod)) {
3048
+ return {
3049
+ path,
3050
+ submission
3051
+ };
3052
+ }
3063
3053
  // Flatten submission onto URLSearchParams for GET submissions
3064
3054
  let parsedPath = parsePath(path);
3065
- let searchParams = convertFormDataToSearchParams(opts.formData);
3066
3055
  // On GET navigation submissions we can drop the ?index param from the
3067
3056
  // resulting location since all loaders will run. But fetcher GET submissions
3068
3057
  // only run a single loader so we need to preserve any incoming ?index params
@@ -3087,7 +3076,7 @@ function getLoaderMatchesUntilBoundary(matches, boundaryId) {
3087
3076
  }
3088
3077
  return boundaryMatches;
3089
3078
  }
3090
- function getMatchesToLoad(history, state, matches, submission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, fetchLoadMatches, routesToUse, basename, pendingActionData, pendingError) {
3079
+ function getMatchesToLoad(history, state, matches, submission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionData, pendingError) {
3091
3080
  let actionResult = pendingError ? Object.values(pendingError)[0] : pendingActionData ? Object.values(pendingActionData)[0] : undefined;
3092
3081
  let currentUrl = history.createURL(state.location);
3093
3082
  let nextUrl = history.createURL(location);
@@ -3149,30 +3138,29 @@ function getMatchesToLoad(history, state, matches, submission, location, isReval
3149
3138
  });
3150
3139
  return;
3151
3140
  }
3152
- let fetcherMatch = getTargetMatch(fetcherMatches, f.path);
3153
- if (cancelledFetcherLoads.includes(key)) {
3154
- revalidatingFetchers.push({
3155
- key,
3156
- routeId: f.routeId,
3157
- path: f.path,
3158
- matches: fetcherMatches,
3159
- match: fetcherMatch,
3160
- controller: new AbortController()
3161
- });
3162
- return;
3163
- }
3164
3141
  // Revalidating fetchers are decoupled from the route matches since they
3165
- // hit a static href, so they _always_ check shouldRevalidate and the
3166
- // default is strictly if a revalidation is explicitly required (action
3167
- // submissions, useRevalidator, X-Remix-Revalidate).
3168
- let shouldRevalidate = shouldRevalidateLoader(fetcherMatch, _extends({
3142
+ // load from a static href. They only set `defaultShouldRevalidate` on
3143
+ // explicit revalidation due to submission, useRevalidator, or X-Remix-Revalidate
3144
+ //
3145
+ // They automatically revalidate without even calling shouldRevalidate if:
3146
+ // - They were cancelled
3147
+ // - They're in the middle of their first load and therefore this is still
3148
+ // an initial load and not a revalidation
3149
+ //
3150
+ // If neither of those is true, then they _always_ check shouldRevalidate
3151
+ let fetcher = state.fetchers.get(key);
3152
+ let isPerformingInitialLoad = fetcher && fetcher.state !== "idle" && fetcher.data === undefined &&
3153
+ // If a fetcher.load redirected then it'll be "loading" without any data
3154
+ // so ensure we're not processing the redirect from this fetcher
3155
+ !fetchRedirectIds.has(key);
3156
+ let fetcherMatch = getTargetMatch(fetcherMatches, f.path);
3157
+ let shouldRevalidate = cancelledFetcherLoads.includes(key) || isPerformingInitialLoad || shouldRevalidateLoader(fetcherMatch, _extends({
3169
3158
  currentUrl,
3170
3159
  currentParams: state.matches[state.matches.length - 1].params,
3171
3160
  nextUrl,
3172
3161
  nextParams: matches[matches.length - 1].params
3173
3162
  }, submission, {
3174
3163
  actionResult,
3175
- // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate
3176
3164
  defaultShouldRevalidate: isRevalidationRequired
3177
3165
  }));
3178
3166
  if (shouldRevalidate) {
@@ -3267,12 +3255,9 @@ async function loadLazyRouteModule(route, mapRouteProperties, manifest) {
3267
3255
  lazy: undefined
3268
3256
  }));
3269
3257
  }
3270
- async function callLoaderOrAction(type, request, match, matches, manifest, mapRouteProperties, basename, isStaticRequest, isRouteRequest, requestContext) {
3271
- if (isStaticRequest === void 0) {
3272
- isStaticRequest = false;
3273
- }
3274
- if (isRouteRequest === void 0) {
3275
- isRouteRequest = false;
3258
+ async function callLoaderOrAction(type, request, match, matches, manifest, mapRouteProperties, basename, opts) {
3259
+ if (opts === void 0) {
3260
+ opts = {};
3276
3261
  }
3277
3262
  let resultType;
3278
3263
  let result;
@@ -3286,7 +3271,7 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
3286
3271
  return Promise.race([handler({
3287
3272
  request,
3288
3273
  params: match.params,
3289
- context: requestContext
3274
+ context: opts.requestContext
3290
3275
  }), abortPromise]);
3291
3276
  };
3292
3277
  try {
@@ -3349,7 +3334,7 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
3349
3334
  // Support relative routing in internal redirects
3350
3335
  if (!ABSOLUTE_URL_REGEX.test(location)) {
3351
3336
  location = normalizeTo(new URL(request.url), matches.slice(0, matches.indexOf(match) + 1), basename, true, location);
3352
- } else if (!isStaticRequest) {
3337
+ } else if (!opts.isStaticRequest) {
3353
3338
  // Strip off the protocol+origin for same-origin + same-basename absolute
3354
3339
  // redirects. If this is a static request, we can let it go back to the
3355
3340
  // browser as-is
@@ -3364,7 +3349,7 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
3364
3349
  // Instead, throw the Response and let the server handle it with an HTTP
3365
3350
  // redirect. We also update the Location header in place in this flow so
3366
3351
  // basename and relative routing is taken into account
3367
- if (isStaticRequest) {
3352
+ if (opts.isStaticRequest) {
3368
3353
  result.headers.set("Location", location);
3369
3354
  throw result;
3370
3355
  }
@@ -3378,7 +3363,7 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
3378
3363
  // For SSR single-route requests, we want to hand Responses back directly
3379
3364
  // without unwrapping. We do this with the QueryRouteResponse wrapper
3380
3365
  // interface so we can know whether it was returned or thrown
3381
- if (isRouteRequest) {
3366
+ if (opts.isRouteRequest) {
3382
3367
  // eslint-disable-next-line no-throw-literal
3383
3368
  throw {
3384
3369
  type: resultType || ResultType.data,
@@ -3439,26 +3424,45 @@ function createClientSideRequest(history, location, signal, submission) {
3439
3424
  if (submission && isMutationMethod(submission.formMethod)) {
3440
3425
  let {
3441
3426
  formMethod,
3442
- formEncType,
3443
- formData
3427
+ formEncType
3444
3428
  } = submission;
3445
3429
  // Didn't think we needed this but it turns out unlike other methods, patch
3446
3430
  // won't be properly normalized to uppercase and results in a 405 error.
3447
3431
  // See: https://fetch.spec.whatwg.org/#concept-method
3448
3432
  init.method = formMethod.toUpperCase();
3449
- init.body = formEncType === "application/x-www-form-urlencoded" ? convertFormDataToSearchParams(formData) : formData;
3433
+ if (formEncType === "application/json") {
3434
+ init.headers = new Headers({
3435
+ "Content-Type": formEncType
3436
+ });
3437
+ init.body = JSON.stringify(submission.json);
3438
+ } else if (formEncType === "text/plain") {
3439
+ // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
3440
+ init.body = submission.text;
3441
+ } else if (formEncType === "application/x-www-form-urlencoded" && submission.formData) {
3442
+ // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
3443
+ init.body = convertFormDataToSearchParams(submission.formData);
3444
+ } else {
3445
+ // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
3446
+ init.body = submission.formData;
3447
+ }
3450
3448
  }
3451
- // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
3452
3449
  return new Request(url, init);
3453
3450
  }
3454
3451
  function convertFormDataToSearchParams(formData) {
3455
3452
  let searchParams = new URLSearchParams();
3456
3453
  for (let [key, value] of formData.entries()) {
3457
3454
  // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#converting-an-entry-list-to-a-list-of-name-value-pairs
3458
- searchParams.append(key, value instanceof File ? value.name : value);
3455
+ searchParams.append(key, typeof value === "string" ? value : value.name);
3459
3456
  }
3460
3457
  return searchParams;
3461
3458
  }
3459
+ function convertSearchParamsToFormData(searchParams) {
3460
+ let formData = new FormData();
3461
+ for (let [key, value] of searchParams.entries()) {
3462
+ formData.append(key, value);
3463
+ }
3464
+ return formData;
3465
+ }
3462
3466
  function processRouteLoaderData(matches, matchesToLoad, results, pendingError, activeDeferreds) {
3463
3467
  // Fill in loaderData/errors from our loaders
3464
3468
  let loaderData = {};
@@ -3564,15 +3568,7 @@ function processLoaderData(state, matches, matchesToLoad, results, pendingError,
3564
3568
  // in resolveDeferredResults
3565
3569
  invariant(false, "Unhandled fetcher deferred data");
3566
3570
  } else {
3567
- let doneFetcher = {
3568
- state: "idle",
3569
- data: result.data,
3570
- formMethod: undefined,
3571
- formAction: undefined,
3572
- formEncType: undefined,
3573
- formData: undefined,
3574
- " _hasFetcherDoneAnything ": true
3575
- };
3571
+ let doneFetcher = getDoneFetcher(result.data);
3576
3572
  state.fetchers.set(key, doneFetcher);
3577
3573
  }
3578
3574
  }
@@ -3638,6 +3634,8 @@ function getInternalRouterError(status, _temp4) {
3638
3634
  errorMessage = "You made a " + method + " request to \"" + pathname + "\" but " + ("did not provide a `loader` for route \"" + routeId + "\", ") + "so there is no way to handle the request.";
3639
3635
  } else if (type === "defer-action") {
3640
3636
  errorMessage = "defer() is not supported in actions";
3637
+ } else if (type === "invalid-body") {
3638
+ errorMessage = "Unable to encode submission body";
3641
3639
  }
3642
3640
  } else if (status === 403) {
3643
3641
  statusText = "Forbidden";
@@ -3804,6 +3802,144 @@ function getTargetMatch(matches, location) {
3804
3802
  let pathMatches = getPathContributingMatches(matches);
3805
3803
  return pathMatches[pathMatches.length - 1];
3806
3804
  }
3805
+ function getSubmissionFromNavigation(navigation) {
3806
+ let {
3807
+ formMethod,
3808
+ formAction,
3809
+ formEncType,
3810
+ text,
3811
+ formData,
3812
+ json
3813
+ } = navigation;
3814
+ if (!formMethod || !formAction || !formEncType) {
3815
+ return;
3816
+ }
3817
+ if (text != null) {
3818
+ return {
3819
+ formMethod,
3820
+ formAction,
3821
+ formEncType,
3822
+ formData: undefined,
3823
+ json: undefined,
3824
+ text
3825
+ };
3826
+ } else if (formData != null) {
3827
+ return {
3828
+ formMethod,
3829
+ formAction,
3830
+ formEncType,
3831
+ formData,
3832
+ json: undefined,
3833
+ text: undefined
3834
+ };
3835
+ } else if (json !== undefined) {
3836
+ return {
3837
+ formMethod,
3838
+ formAction,
3839
+ formEncType,
3840
+ formData: undefined,
3841
+ json,
3842
+ text: undefined
3843
+ };
3844
+ }
3845
+ }
3846
+ function getLoadingNavigation(location, submission) {
3847
+ if (submission) {
3848
+ let navigation = {
3849
+ state: "loading",
3850
+ location,
3851
+ formMethod: submission.formMethod,
3852
+ formAction: submission.formAction,
3853
+ formEncType: submission.formEncType,
3854
+ formData: submission.formData,
3855
+ json: submission.json,
3856
+ text: submission.text
3857
+ };
3858
+ return navigation;
3859
+ } else {
3860
+ let navigation = {
3861
+ state: "loading",
3862
+ location,
3863
+ formMethod: undefined,
3864
+ formAction: undefined,
3865
+ formEncType: undefined,
3866
+ formData: undefined,
3867
+ json: undefined,
3868
+ text: undefined
3869
+ };
3870
+ return navigation;
3871
+ }
3872
+ }
3873
+ function getSubmittingNavigation(location, submission) {
3874
+ let navigation = {
3875
+ state: "submitting",
3876
+ location,
3877
+ formMethod: submission.formMethod,
3878
+ formAction: submission.formAction,
3879
+ formEncType: submission.formEncType,
3880
+ formData: submission.formData,
3881
+ json: submission.json,
3882
+ text: submission.text
3883
+ };
3884
+ return navigation;
3885
+ }
3886
+ function getLoadingFetcher(submission, data) {
3887
+ if (submission) {
3888
+ let fetcher = {
3889
+ state: "loading",
3890
+ formMethod: submission.formMethod,
3891
+ formAction: submission.formAction,
3892
+ formEncType: submission.formEncType,
3893
+ formData: submission.formData,
3894
+ json: submission.json,
3895
+ text: submission.text,
3896
+ data,
3897
+ " _hasFetcherDoneAnything ": true
3898
+ };
3899
+ return fetcher;
3900
+ } else {
3901
+ let fetcher = {
3902
+ state: "loading",
3903
+ formMethod: undefined,
3904
+ formAction: undefined,
3905
+ formEncType: undefined,
3906
+ formData: undefined,
3907
+ json: undefined,
3908
+ text: undefined,
3909
+ data,
3910
+ " _hasFetcherDoneAnything ": true
3911
+ };
3912
+ return fetcher;
3913
+ }
3914
+ }
3915
+ function getSubmittingFetcher(submission, existingFetcher) {
3916
+ let fetcher = {
3917
+ state: "submitting",
3918
+ formMethod: submission.formMethod,
3919
+ formAction: submission.formAction,
3920
+ formEncType: submission.formEncType,
3921
+ formData: submission.formData,
3922
+ json: submission.json,
3923
+ text: submission.text,
3924
+ data: existingFetcher ? existingFetcher.data : undefined,
3925
+ " _hasFetcherDoneAnything ": true
3926
+ };
3927
+ return fetcher;
3928
+ }
3929
+ function getDoneFetcher(data) {
3930
+ let fetcher = {
3931
+ state: "idle",
3932
+ formMethod: undefined,
3933
+ formAction: undefined,
3934
+ formEncType: undefined,
3935
+ formData: undefined,
3936
+ json: undefined,
3937
+ text: undefined,
3938
+ data,
3939
+ " _hasFetcherDoneAnything ": true
3940
+ };
3941
+ return fetcher;
3942
+ }
3807
3943
  //#endregion
3808
3944
 
3809
3945
  export { AbortedDeferredError, Action, ErrorResponse, IDLE_BLOCKER, IDLE_FETCHER, IDLE_NAVIGATION, UNSAFE_DEFERRED_SYMBOL, DeferredData as UNSAFE_DeferredData, convertRoutesToDataRoutes as UNSAFE_convertRoutesToDataRoutes, getPathContributingMatches as UNSAFE_getPathContributingMatches, invariant as UNSAFE_invariant, warning as UNSAFE_warning, createBrowserHistory, createHashHistory, createMemoryHistory, createPath, createRouter, createStaticHandler, defer, generatePath, getStaticContextFromError, getToPathname, isDeferredData, isRouteErrorResponse, joinPaths, json, matchPath, matchRoutes, normalizePathname, parsePath, redirect, resolvePath, resolveTo, stripBasename };