@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.
@@ -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
  *
@@ -569,6 +569,7 @@
569
569
  * In v7, active navigation/fetcher form methods are exposed in uppercase on the
570
570
  * RouterState. This is to align with the normalization done via fetch().
571
571
  */
572
+ // Thanks https://github.com/sindresorhus/type-fest!
572
573
  /**
573
574
  * @private
574
575
  * Internal interface to pass around for action submissions, not intended for
@@ -926,28 +927,22 @@
926
927
 
927
928
  // ensure `/` is added at the beginning if the path is absolute
928
929
  const prefix = path.startsWith("/") ? "/" : "";
930
+ const stringify = p => p == null ? "" : typeof p === "string" ? p : String(p);
929
931
  const segments = path.split(/\/+/).map((segment, index, array) => {
930
932
  const isLastSegment = index === array.length - 1;
931
933
 
932
934
  // only apply the splat if it's the last segment
933
935
  if (isLastSegment && segment === "*") {
934
936
  const star = "*";
935
- const starParam = params[star];
936
-
937
937
  // Apply the splat
938
- return starParam;
938
+ return stringify(params[star]);
939
939
  }
940
940
  const keyMatch = segment.match(/^:(\w+)(\??)$/);
941
941
  if (keyMatch) {
942
942
  const [, key, optional] = keyMatch;
943
943
  let param = params[key];
944
- if (optional === "?") {
945
- return param == null ? "" : param;
946
- }
947
- if (param == null) {
948
- invariant(false, "Missing \":" + key + "\" param");
949
- }
950
- return param;
944
+ invariant(optional === "?" || param != null, "Missing \":" + key + "\" param");
945
+ return stringify(param);
951
946
  }
952
947
 
953
948
  // Remove any optional markers from optional static segments
@@ -1475,14 +1470,23 @@
1475
1470
  /**
1476
1471
  * Function signature for determining the current scroll position
1477
1472
  */
1473
+ // Allowed for any navigation or fetch
1474
+ // Only allowed for navigations
1475
+ // Only allowed for submission navigations
1478
1476
  /**
1479
- * Options for a navigate() call for a Link navigation
1477
+ * Options for a navigate() call for a normal (non-submission) navigation
1480
1478
  */
1481
1479
  /**
1482
- * Options for a navigate() call for a Form navigation
1480
+ * Options for a navigate() call for a submission navigation
1483
1481
  */
1484
1482
  /**
1485
- * Options to pass to navigate() for either a Link or Form navigation
1483
+ * Options to pass to navigate() for a navigation
1484
+ */
1485
+ /**
1486
+ * Options for a fetch() load
1487
+ */
1488
+ /**
1489
+ * Options for a fetch() submission
1486
1490
  */
1487
1491
  /**
1488
1492
  * Options to pass to fetch()
@@ -1517,7 +1521,9 @@
1517
1521
  formMethod: undefined,
1518
1522
  formAction: undefined,
1519
1523
  formEncType: undefined,
1520
- formData: undefined
1524
+ formData: undefined,
1525
+ json: undefined,
1526
+ text: undefined
1521
1527
  };
1522
1528
  const IDLE_FETCHER = {
1523
1529
  state: "idle",
@@ -1525,7 +1531,9 @@
1525
1531
  formMethod: undefined,
1526
1532
  formAction: undefined,
1527
1533
  formEncType: undefined,
1528
- formData: undefined
1534
+ formData: undefined,
1535
+ json: undefined,
1536
+ text: undefined
1529
1537
  };
1530
1538
  const IDLE_BLOCKER = {
1531
1539
  state: "unblocked",
@@ -1741,9 +1749,10 @@
1741
1749
  init.history.go(delta);
1742
1750
  },
1743
1751
  reset() {
1744
- deleteBlocker(blockerKey);
1752
+ let blockers = new Map(state.blockers);
1753
+ blockers.set(blockerKey, IDLE_BLOCKER);
1745
1754
  updateState({
1746
- blockers: new Map(router.state.blockers)
1755
+ blockers
1747
1756
  });
1748
1757
  }
1749
1758
  });
@@ -1820,9 +1829,8 @@
1820
1829
 
1821
1830
  // On a successful navigation we can assume we got through all blockers
1822
1831
  // so we can start fresh
1823
- for (let [key] of blockerFunctions) {
1824
- deleteBlocker(key);
1825
- }
1832
+ let blockers = new Map();
1833
+ blockerFunctions.clear();
1826
1834
 
1827
1835
  // Always respect the user flag. Otherwise don't reset on mutation
1828
1836
  // submission navigations unless they redirect
@@ -1831,6 +1839,11 @@
1831
1839
  dataRoutes = inFlightDataRoutes;
1832
1840
  inFlightDataRoutes = undefined;
1833
1841
  }
1842
+ if (isUninterruptedRevalidation) ; else if (pendingAction === exports.Action.Pop) ; else if (pendingAction === exports.Action.Push) {
1843
+ init.history.push(location, location.state);
1844
+ } else if (pendingAction === exports.Action.Replace) {
1845
+ init.history.replace(location, location.state);
1846
+ }
1834
1847
  updateState(_extends({}, newState, {
1835
1848
  // matches, errors, fetchers go through as-is
1836
1849
  actionData,
@@ -1842,13 +1855,8 @@
1842
1855
  revalidation: "idle",
1843
1856
  restoreScrollPosition: getSavedScrollPosition(location, newState.matches || state.matches),
1844
1857
  preventScrollReset,
1845
- blockers: new Map(state.blockers)
1858
+ blockers
1846
1859
  }));
1847
- if (isUninterruptedRevalidation) ; else if (pendingAction === exports.Action.Pop) ; else if (pendingAction === exports.Action.Push) {
1848
- init.history.push(location, location.state);
1849
- } else if (pendingAction === exports.Action.Replace) {
1850
- init.history.replace(location, location.state);
1851
- }
1852
1860
 
1853
1861
  // Reset stateful navigation vars
1854
1862
  pendingAction = exports.Action.Pop;
@@ -1914,9 +1922,10 @@
1914
1922
  navigate(to, opts);
1915
1923
  },
1916
1924
  reset() {
1917
- deleteBlocker(blockerKey);
1925
+ let blockers = new Map(state.blockers);
1926
+ blockers.set(blockerKey, IDLE_BLOCKER);
1918
1927
  updateState({
1919
- blockers: new Map(state.blockers)
1928
+ blockers
1920
1929
  });
1921
1930
  }
1922
1931
  });
@@ -2042,11 +2051,7 @@
2042
2051
  }
2043
2052
  pendingActionData = actionOutput.pendingActionData;
2044
2053
  pendingError = actionOutput.pendingActionError;
2045
- let navigation = _extends({
2046
- state: "loading",
2047
- location
2048
- }, opts.submission);
2049
- loadingNavigation = navigation;
2054
+ loadingNavigation = getLoadingNavigation(location, opts.submission);
2050
2055
 
2051
2056
  // Create a GET request for the loaders
2052
2057
  request = new Request(request.url, {
@@ -2081,13 +2086,13 @@
2081
2086
  // Call the action matched by the leaf route for this navigation and handle
2082
2087
  // redirects/errors
2083
2088
  async function handleAction(request, location, submission, matches, opts) {
2089
+ if (opts === void 0) {
2090
+ opts = {};
2091
+ }
2084
2092
  interruptActiveLoads();
2085
2093
 
2086
2094
  // Put us in a submitting state
2087
- let navigation = _extends({
2088
- state: "submitting",
2089
- location
2090
- }, submission);
2095
+ let navigation = getSubmittingNavigation(location, submission);
2091
2096
  updateState({
2092
2097
  navigation
2093
2098
  });
@@ -2166,29 +2171,13 @@
2166
2171
  // errors, etc.
2167
2172
  async function handleLoaders(request, location, matches, overrideNavigation, submission, fetcherSubmission, replace, pendingActionData, pendingError) {
2168
2173
  // Figure out the right navigation we want to use for data loading
2169
- let loadingNavigation = overrideNavigation;
2170
- if (!loadingNavigation) {
2171
- let navigation = _extends({
2172
- state: "loading",
2173
- location,
2174
- formMethod: undefined,
2175
- formAction: undefined,
2176
- formEncType: undefined,
2177
- formData: undefined
2178
- }, submission);
2179
- loadingNavigation = navigation;
2180
- }
2174
+ let loadingNavigation = overrideNavigation || getLoadingNavigation(location, submission);
2181
2175
 
2182
2176
  // If this was a redirect from an action we don't have a "submission" but
2183
2177
  // we have it on the loading navigation so use that if available
2184
- let activeSubmission = submission || fetcherSubmission ? submission || fetcherSubmission : loadingNavigation.formMethod && loadingNavigation.formAction && loadingNavigation.formData && loadingNavigation.formEncType ? {
2185
- formMethod: loadingNavigation.formMethod,
2186
- formAction: loadingNavigation.formAction,
2187
- formData: loadingNavigation.formData,
2188
- formEncType: loadingNavigation.formEncType
2189
- } : undefined;
2178
+ let activeSubmission = submission || fetcherSubmission || getSubmissionFromNavigation(loadingNavigation);
2190
2179
  let routesToUse = inFlightDataRoutes || dataRoutes;
2191
- let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, activeSubmission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, fetchLoadMatches, routesToUse, basename, pendingActionData, pendingError);
2180
+ let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, activeSubmission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionData, pendingError);
2192
2181
 
2193
2182
  // Cancel pending deferreds for no-longer-matched routes or routes we're
2194
2183
  // about to reload. Note that if this is an action reload we would have
@@ -2220,15 +2209,7 @@
2220
2209
  if (!isUninterruptedRevalidation) {
2221
2210
  revalidatingFetchers.forEach(rf => {
2222
2211
  let fetcher = state.fetchers.get(rf.key);
2223
- let revalidatingFetcher = {
2224
- state: "loading",
2225
- data: fetcher && fetcher.data,
2226
- formMethod: undefined,
2227
- formAction: undefined,
2228
- formEncType: undefined,
2229
- formData: undefined,
2230
- " _hasFetcherDoneAnything ": true
2231
- };
2212
+ let revalidatingFetcher = getLoadingFetcher(undefined, fetcher ? fetcher.data : undefined);
2232
2213
  state.fetchers.set(rf.key, revalidatingFetcher);
2233
2214
  });
2234
2215
  let actionData = pendingActionData || state.actionData;
@@ -2244,6 +2225,9 @@
2244
2225
  }
2245
2226
  pendingNavigationLoadId = ++incrementingLoadId;
2246
2227
  revalidatingFetchers.forEach(rf => {
2228
+ if (fetchControllers.has(rf.key)) {
2229
+ abortFetcher(rf.key);
2230
+ }
2247
2231
  if (rf.controller) {
2248
2232
  // Fetchers use an independent AbortController so that aborting a fetcher
2249
2233
  // (via deleteFetcher) does not abort the triggering navigation that
@@ -2335,8 +2319,13 @@
2335
2319
  }
2336
2320
  let {
2337
2321
  path,
2338
- submission
2322
+ submission,
2323
+ error
2339
2324
  } = normalizeNavigateOptions(future.v7_normalizeFormMethod, true, normalizedPath, opts);
2325
+ if (error) {
2326
+ setFetcherError(key, routeId, error);
2327
+ return;
2328
+ }
2340
2329
  let match = getTargetMatch(matches, path);
2341
2330
  pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
2342
2331
  if (submission && isMutationMethod(submission.formMethod)) {
@@ -2370,12 +2359,7 @@
2370
2359
 
2371
2360
  // Put this fetcher into it's submitting state
2372
2361
  let existingFetcher = state.fetchers.get(key);
2373
- let fetcher = _extends({
2374
- state: "submitting"
2375
- }, submission, {
2376
- data: existingFetcher && existingFetcher.data,
2377
- " _hasFetcherDoneAnything ": true
2378
- });
2362
+ let fetcher = getSubmittingFetcher(submission, existingFetcher);
2379
2363
  state.fetchers.set(key, fetcher);
2380
2364
  updateState({
2381
2365
  fetchers: new Map(state.fetchers)
@@ -2397,12 +2381,7 @@
2397
2381
  if (isRedirectResult(actionResult)) {
2398
2382
  fetchControllers.delete(key);
2399
2383
  fetchRedirectIds.add(key);
2400
- let loadingFetcher = _extends({
2401
- state: "loading"
2402
- }, submission, {
2403
- data: undefined,
2404
- " _hasFetcherDoneAnything ": true
2405
- });
2384
+ let loadingFetcher = getLoadingFetcher(submission);
2406
2385
  state.fetchers.set(key, loadingFetcher);
2407
2386
  updateState({
2408
2387
  fetchers: new Map(state.fetchers)
@@ -2433,14 +2412,9 @@
2433
2412
  invariant(matches, "Didn't find any matches after fetcher action");
2434
2413
  let loadId = ++incrementingLoadId;
2435
2414
  fetchReloadIds.set(key, loadId);
2436
- let loadFetcher = _extends({
2437
- state: "loading",
2438
- data: actionResult.data
2439
- }, submission, {
2440
- " _hasFetcherDoneAnything ": true
2441
- });
2415
+ let loadFetcher = getLoadingFetcher(submission, actionResult.data);
2442
2416
  state.fetchers.set(key, loadFetcher);
2443
- let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, submission, nextLocation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, fetchLoadMatches, routesToUse, basename, {
2417
+ let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, submission, nextLocation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, {
2444
2418
  [match.route.id]: actionResult.data
2445
2419
  }, undefined // No need to send through errors since we short circuit above
2446
2420
  );
@@ -2451,16 +2425,11 @@
2451
2425
  revalidatingFetchers.filter(rf => rf.key !== key).forEach(rf => {
2452
2426
  let staleKey = rf.key;
2453
2427
  let existingFetcher = state.fetchers.get(staleKey);
2454
- let revalidatingFetcher = {
2455
- state: "loading",
2456
- data: existingFetcher && existingFetcher.data,
2457
- formMethod: undefined,
2458
- formAction: undefined,
2459
- formEncType: undefined,
2460
- formData: undefined,
2461
- " _hasFetcherDoneAnything ": true
2462
- };
2428
+ let revalidatingFetcher = getLoadingFetcher(undefined, existingFetcher ? existingFetcher.data : undefined);
2463
2429
  state.fetchers.set(staleKey, revalidatingFetcher);
2430
+ if (fetchControllers.has(staleKey)) {
2431
+ abortFetcher(staleKey);
2432
+ }
2464
2433
  if (rf.controller) {
2465
2434
  fetchControllers.set(staleKey, rf.controller);
2466
2435
  }
@@ -2496,15 +2465,7 @@
2496
2465
  // Since we let revalidations complete even if the submitting fetcher was
2497
2466
  // deleted, only put it back to idle if it hasn't been deleted
2498
2467
  if (state.fetchers.has(key)) {
2499
- let doneFetcher = {
2500
- state: "idle",
2501
- data: actionResult.data,
2502
- formMethod: undefined,
2503
- formAction: undefined,
2504
- formEncType: undefined,
2505
- formData: undefined,
2506
- " _hasFetcherDoneAnything ": true
2507
- };
2468
+ let doneFetcher = getDoneFetcher(actionResult.data);
2508
2469
  state.fetchers.set(key, doneFetcher);
2509
2470
  }
2510
2471
  let didAbortFetchLoads = abortStaleFetchLoads(loadId);
@@ -2539,16 +2500,7 @@
2539
2500
  async function handleFetcherLoader(key, routeId, path, match, matches, submission) {
2540
2501
  let existingFetcher = state.fetchers.get(key);
2541
2502
  // Put this fetcher into it's loading state
2542
- let loadingFetcher = _extends({
2543
- state: "loading",
2544
- formMethod: undefined,
2545
- formAction: undefined,
2546
- formEncType: undefined,
2547
- formData: undefined
2548
- }, submission, {
2549
- data: existingFetcher && existingFetcher.data,
2550
- " _hasFetcherDoneAnything ": true
2551
- });
2503
+ let loadingFetcher = getLoadingFetcher(submission, existingFetcher ? existingFetcher.data : undefined);
2552
2504
  state.fetchers.set(key, loadingFetcher);
2553
2505
  updateState({
2554
2506
  fetchers: new Map(state.fetchers)
@@ -2602,15 +2554,7 @@
2602
2554
  invariant(!isDeferredResult(result), "Unhandled fetcher deferred data");
2603
2555
 
2604
2556
  // Put the fetcher back into an idle state
2605
- let doneFetcher = {
2606
- state: "idle",
2607
- data: result.data,
2608
- formMethod: undefined,
2609
- formAction: undefined,
2610
- formEncType: undefined,
2611
- formData: undefined,
2612
- " _hasFetcherDoneAnything ": true
2613
- };
2557
+ let doneFetcher = getDoneFetcher(result.data);
2614
2558
  state.fetchers.set(key, doneFetcher);
2615
2559
  updateState({
2616
2560
  fetchers: new Map(state.fetchers)
@@ -2673,27 +2617,14 @@
2673
2617
 
2674
2618
  // Use the incoming submission if provided, fallback on the active one in
2675
2619
  // state.navigation
2676
- let {
2677
- formMethod,
2678
- formAction,
2679
- formEncType,
2680
- formData
2681
- } = state.navigation;
2682
- if (!submission && formMethod && formAction && formData && formEncType) {
2683
- submission = {
2684
- formMethod,
2685
- formAction,
2686
- formEncType,
2687
- formData
2688
- };
2689
- }
2620
+ let activeSubmission = submission || getSubmissionFromNavigation(state.navigation);
2690
2621
 
2691
2622
  // If this was a 307/308 submission we want to preserve the HTTP method and
2692
2623
  // re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the
2693
2624
  // redirected location
2694
- if (redirectPreserveMethodStatusCodes.has(redirect.status) && submission && isMutationMethod(submission.formMethod)) {
2625
+ if (redirectPreserveMethodStatusCodes.has(redirect.status) && activeSubmission && isMutationMethod(activeSubmission.formMethod)) {
2695
2626
  await startNavigation(redirectHistoryAction, redirectLocation, {
2696
- submission: _extends({}, submission, {
2627
+ submission: _extends({}, activeSubmission, {
2697
2628
  formAction: redirect.location
2698
2629
  }),
2699
2630
  // Preserve this flag across redirects
@@ -2703,30 +2634,16 @@
2703
2634
  // For a fetch action redirect, we kick off a new loading navigation
2704
2635
  // without the fetcher submission, but we send it along for shouldRevalidate
2705
2636
  await startNavigation(redirectHistoryAction, redirectLocation, {
2706
- overrideNavigation: {
2707
- state: "loading",
2708
- location: redirectLocation,
2709
- formMethod: undefined,
2710
- formAction: undefined,
2711
- formEncType: undefined,
2712
- formData: undefined
2713
- },
2714
- fetcherSubmission: submission,
2637
+ overrideNavigation: getLoadingNavigation(redirectLocation),
2638
+ fetcherSubmission: activeSubmission,
2715
2639
  // Preserve this flag across redirects
2716
2640
  preventScrollReset: pendingPreventScrollReset
2717
2641
  });
2718
2642
  } else {
2719
- // Otherwise, we kick off a new loading navigation, preserving the
2720
- // submission info for the duration of this navigation
2643
+ // If we have a submission, we will preserve it through the redirect navigation
2644
+ let overrideNavigation = getLoadingNavigation(redirectLocation, activeSubmission);
2721
2645
  await startNavigation(redirectHistoryAction, redirectLocation, {
2722
- overrideNavigation: {
2723
- state: "loading",
2724
- location: redirectLocation,
2725
- formMethod: submission ? submission.formMethod : undefined,
2726
- formAction: submission ? submission.formAction : undefined,
2727
- formEncType: submission ? submission.formEncType : undefined,
2728
- formData: submission ? submission.formData : undefined
2729
- },
2646
+ overrideNavigation,
2730
2647
  // Preserve this flag across redirects
2731
2648
  preventScrollReset: pendingPreventScrollReset
2732
2649
  });
@@ -2806,15 +2723,7 @@
2806
2723
  function markFetchersDone(keys) {
2807
2724
  for (let key of keys) {
2808
2725
  let fetcher = getFetcher(key);
2809
- let doneFetcher = {
2810
- state: "idle",
2811
- data: fetcher.data,
2812
- formMethod: undefined,
2813
- formAction: undefined,
2814
- formEncType: undefined,
2815
- formData: undefined,
2816
- " _hasFetcherDoneAnything ": true
2817
- };
2726
+ let doneFetcher = getDoneFetcher(fetcher.data);
2818
2727
  state.fetchers.set(key, doneFetcher);
2819
2728
  }
2820
2729
  }
@@ -2868,9 +2777,10 @@
2868
2777
  // Poor mans state machine :)
2869
2778
  // https://mermaid.live/edit#pako:eNqVkc9OwzAMxl8l8nnjAYrEtDIOHEBIgwvKJTReGy3_lDpIqO27k6awMG0XcrLlnz87nwdonESogKXXBuE79rq75XZO3-yHds0RJVuv70YrPlUrCEe2HfrORS3rubqZfuhtpg5C9wk5tZ4VKcRUq88q9Z8RS0-48cE1iHJkL0ugbHuFLus9L6spZy8nX9MP2CNdomVaposqu3fGayT8T8-jJQwhepo_UtpgBQaDEUom04dZhAN1aJBDlUKJBxE1ceB2Smj0Mln-IBW5AFU2dwUiktt_2Qaq2dBfaKdEup85UV7Yd-dKjlnkabl2Pvr0DTkTreM
2870
2779
  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);
2871
- state.blockers.set(key, newBlocker);
2780
+ let blockers = new Map(state.blockers);
2781
+ blockers.set(key, newBlocker);
2872
2782
  updateState({
2873
- blockers: new Map(state.blockers)
2783
+ blockers
2874
2784
  });
2875
2785
  }
2876
2786
  function shouldBlockNavigation(_ref2) {
@@ -2927,7 +2837,7 @@
2927
2837
  function enableScrollRestoration(positions, getPosition, getKey) {
2928
2838
  savedScrollPositions = positions;
2929
2839
  getScrollPosition = getPosition;
2930
- getScrollRestorationKey = getKey || (location => location.key);
2840
+ getScrollRestorationKey = getKey || null;
2931
2841
 
2932
2842
  // Perform initial hydration scroll restoration, since we miss the boat on
2933
2843
  // the initial updateState() because we've not yet rendered <ScrollRestoration/>
@@ -2947,17 +2857,22 @@
2947
2857
  getScrollRestorationKey = null;
2948
2858
  };
2949
2859
  }
2860
+ function getScrollKey(location, matches) {
2861
+ if (getScrollRestorationKey) {
2862
+ let key = getScrollRestorationKey(location, matches.map(m => createUseMatchesMatch(m, state.loaderData)));
2863
+ return key || location.key;
2864
+ }
2865
+ return location.key;
2866
+ }
2950
2867
  function saveScrollPosition(location, matches) {
2951
- if (savedScrollPositions && getScrollRestorationKey && getScrollPosition) {
2952
- let userMatches = matches.map(m => createUseMatchesMatch(m, state.loaderData));
2953
- let key = getScrollRestorationKey(location, userMatches) || location.key;
2868
+ if (savedScrollPositions && getScrollPosition) {
2869
+ let key = getScrollKey(location, matches);
2954
2870
  savedScrollPositions[key] = getScrollPosition();
2955
2871
  }
2956
2872
  }
2957
2873
  function getSavedScrollPosition(location, matches) {
2958
- if (savedScrollPositions && getScrollRestorationKey && getScrollPosition) {
2959
- let userMatches = matches.map(m => createUseMatchesMatch(m, state.loaderData));
2960
- let key = getScrollRestorationKey(location, userMatches) || location.key;
2874
+ if (savedScrollPositions) {
2875
+ let key = getScrollKey(location, matches);
2961
2876
  let y = savedScrollPositions[key];
2962
2877
  if (typeof y === "number") {
2963
2878
  return y;
@@ -3240,7 +3155,11 @@
3240
3155
  error
3241
3156
  };
3242
3157
  } else {
3243
- result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, true, isRouteRequest, requestContext);
3158
+ result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, {
3159
+ isStaticRequest: true,
3160
+ isRouteRequest,
3161
+ requestContext
3162
+ });
3244
3163
  if (request.signal.aborted) {
3245
3164
  let method = isRouteRequest ? "queryRoute" : "query";
3246
3165
  throw new Error(method + "() call aborted");
@@ -3355,7 +3274,11 @@
3355
3274
  activeDeferreds: null
3356
3275
  };
3357
3276
  }
3358
- let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename, true, isRouteRequest, requestContext))]);
3277
+ let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename, {
3278
+ isStaticRequest: true,
3279
+ isRouteRequest,
3280
+ requestContext
3281
+ }))]);
3359
3282
  if (request.signal.aborted) {
3360
3283
  let method = isRouteRequest ? "queryRoute" : "query";
3361
3284
  throw new Error(method + "() call aborted");
@@ -3404,7 +3327,7 @@
3404
3327
  return newContext;
3405
3328
  }
3406
3329
  function isSubmissionNavigation(opts) {
3407
- return opts != null && "formData" in opts;
3330
+ return opts != null && ("formData" in opts && opts.formData != null || "body" in opts && opts.body !== undefined);
3408
3331
  }
3409
3332
  function normalizeTo(location, matches, basename, prependBasename, to, fromRouteId, relative) {
3410
3333
  let contextualMatches;
@@ -3470,28 +3393,103 @@
3470
3393
  })
3471
3394
  };
3472
3395
  }
3396
+ let getInvalidBodyError = () => ({
3397
+ path,
3398
+ error: getInternalRouterError(400, {
3399
+ type: "invalid-body"
3400
+ })
3401
+ });
3473
3402
 
3474
3403
  // Create a Submission on non-GET navigations
3475
- let submission;
3476
- if (opts.formData) {
3477
- let formMethod = opts.formMethod || "get";
3478
- submission = {
3479
- formMethod: normalizeFormMethod ? formMethod.toUpperCase() : formMethod.toLowerCase(),
3480
- formAction: stripHashFromPath(path),
3481
- formEncType: opts && opts.formEncType || "application/x-www-form-urlencoded",
3482
- formData: opts.formData
3483
- };
3484
- if (isMutationMethod(submission.formMethod)) {
3404
+ let rawFormMethod = opts.formMethod || "get";
3405
+ let formMethod = normalizeFormMethod ? rawFormMethod.toUpperCase() : rawFormMethod.toLowerCase();
3406
+ let formAction = stripHashFromPath(path);
3407
+ if (opts.body !== undefined) {
3408
+ if (opts.formEncType === "text/plain") {
3409
+ // text only support POST/PUT/PATCH/DELETE submissions
3410
+ if (!isMutationMethod(formMethod)) {
3411
+ return getInvalidBodyError();
3412
+ }
3413
+ let text = typeof opts.body === "string" ? opts.body : opts.body instanceof FormData || opts.body instanceof URLSearchParams ?
3414
+ // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#plain-text-form-data
3415
+ Array.from(opts.body.entries()).reduce((acc, _ref3) => {
3416
+ let [name, value] = _ref3;
3417
+ return "" + acc + name + "=" + value + "\n";
3418
+ }, "") : String(opts.body);
3485
3419
  return {
3486
3420
  path,
3487
- submission
3421
+ submission: {
3422
+ formMethod,
3423
+ formAction,
3424
+ formEncType: opts.formEncType,
3425
+ formData: undefined,
3426
+ json: undefined,
3427
+ text
3428
+ }
3488
3429
  };
3430
+ } else if (opts.formEncType === "application/json") {
3431
+ // json only supports POST/PUT/PATCH/DELETE submissions
3432
+ if (!isMutationMethod(formMethod)) {
3433
+ return getInvalidBodyError();
3434
+ }
3435
+ try {
3436
+ let json = typeof opts.body === "string" ? JSON.parse(opts.body) : opts.body;
3437
+ return {
3438
+ path,
3439
+ submission: {
3440
+ formMethod,
3441
+ formAction,
3442
+ formEncType: opts.formEncType,
3443
+ formData: undefined,
3444
+ json,
3445
+ text: undefined
3446
+ }
3447
+ };
3448
+ } catch (e) {
3449
+ return getInvalidBodyError();
3450
+ }
3451
+ }
3452
+ }
3453
+ invariant(typeof FormData === "function", "FormData is not available in this environment");
3454
+ let searchParams;
3455
+ let formData;
3456
+ if (opts.formData) {
3457
+ searchParams = convertFormDataToSearchParams(opts.formData);
3458
+ formData = opts.formData;
3459
+ } else if (opts.body instanceof FormData) {
3460
+ searchParams = convertFormDataToSearchParams(opts.body);
3461
+ formData = opts.body;
3462
+ } else if (opts.body instanceof URLSearchParams) {
3463
+ searchParams = opts.body;
3464
+ formData = convertSearchParamsToFormData(searchParams);
3465
+ } else if (opts.body == null) {
3466
+ searchParams = new URLSearchParams();
3467
+ formData = new FormData();
3468
+ } else {
3469
+ try {
3470
+ searchParams = new URLSearchParams(opts.body);
3471
+ formData = convertSearchParamsToFormData(searchParams);
3472
+ } catch (e) {
3473
+ return getInvalidBodyError();
3489
3474
  }
3490
3475
  }
3476
+ let submission = {
3477
+ formMethod,
3478
+ formAction,
3479
+ formEncType: opts && opts.formEncType || "application/x-www-form-urlencoded",
3480
+ formData,
3481
+ json: undefined,
3482
+ text: undefined
3483
+ };
3484
+ if (isMutationMethod(submission.formMethod)) {
3485
+ return {
3486
+ path,
3487
+ submission
3488
+ };
3489
+ }
3491
3490
 
3492
3491
  // Flatten submission onto URLSearchParams for GET submissions
3493
3492
  let parsedPath = parsePath(path);
3494
- let searchParams = convertFormDataToSearchParams(opts.formData);
3495
3493
  // On GET navigation submissions we can drop the ?index param from the
3496
3494
  // resulting location since all loaders will run. But fetcher GET submissions
3497
3495
  // only run a single loader so we need to preserve any incoming ?index params
@@ -3517,7 +3515,7 @@
3517
3515
  }
3518
3516
  return boundaryMatches;
3519
3517
  }
3520
- function getMatchesToLoad(history, state, matches, submission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, fetchLoadMatches, routesToUse, basename, pendingActionData, pendingError) {
3518
+ function getMatchesToLoad(history, state, matches, submission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionData, pendingError) {
3521
3519
  let actionResult = pendingError ? Object.values(pendingError)[0] : pendingActionData ? Object.values(pendingActionData)[0] : undefined;
3522
3520
  let currentUrl = history.createURL(state.location);
3523
3521
  let nextUrl = history.createURL(location);
@@ -3584,31 +3582,30 @@
3584
3582
  });
3585
3583
  return;
3586
3584
  }
3587
- let fetcherMatch = getTargetMatch(fetcherMatches, f.path);
3588
- if (cancelledFetcherLoads.includes(key)) {
3589
- revalidatingFetchers.push({
3590
- key,
3591
- routeId: f.routeId,
3592
- path: f.path,
3593
- matches: fetcherMatches,
3594
- match: fetcherMatch,
3595
- controller: new AbortController()
3596
- });
3597
- return;
3598
- }
3599
3585
 
3600
3586
  // Revalidating fetchers are decoupled from the route matches since they
3601
- // hit a static href, so they _always_ check shouldRevalidate and the
3602
- // default is strictly if a revalidation is explicitly required (action
3603
- // submissions, useRevalidator, X-Remix-Revalidate).
3604
- let shouldRevalidate = shouldRevalidateLoader(fetcherMatch, _extends({
3587
+ // load from a static href. They only set `defaultShouldRevalidate` on
3588
+ // explicit revalidation due to submission, useRevalidator, or X-Remix-Revalidate
3589
+ //
3590
+ // They automatically revalidate without even calling shouldRevalidate if:
3591
+ // - They were cancelled
3592
+ // - They're in the middle of their first load and therefore this is still
3593
+ // an initial load and not a revalidation
3594
+ //
3595
+ // If neither of those is true, then they _always_ check shouldRevalidate
3596
+ let fetcher = state.fetchers.get(key);
3597
+ let isPerformingInitialLoad = fetcher && fetcher.state !== "idle" && fetcher.data === undefined &&
3598
+ // If a fetcher.load redirected then it'll be "loading" without any data
3599
+ // so ensure we're not processing the redirect from this fetcher
3600
+ !fetchRedirectIds.has(key);
3601
+ let fetcherMatch = getTargetMatch(fetcherMatches, f.path);
3602
+ let shouldRevalidate = cancelledFetcherLoads.includes(key) || isPerformingInitialLoad || shouldRevalidateLoader(fetcherMatch, _extends({
3605
3603
  currentUrl,
3606
3604
  currentParams: state.matches[state.matches.length - 1].params,
3607
3605
  nextUrl,
3608
3606
  nextParams: matches[matches.length - 1].params
3609
3607
  }, submission, {
3610
3608
  actionResult,
3611
- // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate
3612
3609
  defaultShouldRevalidate: isRevalidationRequired
3613
3610
  }));
3614
3611
  if (shouldRevalidate) {
@@ -3710,12 +3707,9 @@
3710
3707
  lazy: undefined
3711
3708
  }));
3712
3709
  }
3713
- async function callLoaderOrAction(type, request, match, matches, manifest, mapRouteProperties, basename, isStaticRequest, isRouteRequest, requestContext) {
3714
- if (isStaticRequest === void 0) {
3715
- isStaticRequest = false;
3716
- }
3717
- if (isRouteRequest === void 0) {
3718
- isRouteRequest = false;
3710
+ async function callLoaderOrAction(type, request, match, matches, manifest, mapRouteProperties, basename, opts) {
3711
+ if (opts === void 0) {
3712
+ opts = {};
3719
3713
  }
3720
3714
  let resultType;
3721
3715
  let result;
@@ -3729,7 +3723,7 @@
3729
3723
  return Promise.race([handler({
3730
3724
  request,
3731
3725
  params: match.params,
3732
- context: requestContext
3726
+ context: opts.requestContext
3733
3727
  }), abortPromise]);
3734
3728
  };
3735
3729
  try {
@@ -3794,7 +3788,7 @@
3794
3788
  // Support relative routing in internal redirects
3795
3789
  if (!ABSOLUTE_URL_REGEX.test(location)) {
3796
3790
  location = normalizeTo(new URL(request.url), matches.slice(0, matches.indexOf(match) + 1), basename, true, location);
3797
- } else if (!isStaticRequest) {
3791
+ } else if (!opts.isStaticRequest) {
3798
3792
  // Strip off the protocol+origin for same-origin + same-basename absolute
3799
3793
  // redirects. If this is a static request, we can let it go back to the
3800
3794
  // browser as-is
@@ -3810,7 +3804,7 @@
3810
3804
  // Instead, throw the Response and let the server handle it with an HTTP
3811
3805
  // redirect. We also update the Location header in place in this flow so
3812
3806
  // basename and relative routing is taken into account
3813
- if (isStaticRequest) {
3807
+ if (opts.isStaticRequest) {
3814
3808
  result.headers.set("Location", location);
3815
3809
  throw result;
3816
3810
  }
@@ -3825,7 +3819,7 @@
3825
3819
  // For SSR single-route requests, we want to hand Responses back directly
3826
3820
  // without unwrapping. We do this with the QueryRouteResponse wrapper
3827
3821
  // interface so we can know whether it was returned or thrown
3828
- if (isRouteRequest) {
3822
+ if (opts.isRouteRequest) {
3829
3823
  // eslint-disable-next-line no-throw-literal
3830
3824
  throw {
3831
3825
  type: resultType || ResultType.data,
@@ -3887,27 +3881,45 @@
3887
3881
  if (submission && isMutationMethod(submission.formMethod)) {
3888
3882
  let {
3889
3883
  formMethod,
3890
- formEncType,
3891
- formData
3884
+ formEncType
3892
3885
  } = submission;
3893
3886
  // Didn't think we needed this but it turns out unlike other methods, patch
3894
3887
  // won't be properly normalized to uppercase and results in a 405 error.
3895
3888
  // See: https://fetch.spec.whatwg.org/#concept-method
3896
3889
  init.method = formMethod.toUpperCase();
3897
- init.body = formEncType === "application/x-www-form-urlencoded" ? convertFormDataToSearchParams(formData) : formData;
3890
+ if (formEncType === "application/json") {
3891
+ init.headers = new Headers({
3892
+ "Content-Type": formEncType
3893
+ });
3894
+ init.body = JSON.stringify(submission.json);
3895
+ } else if (formEncType === "text/plain") {
3896
+ // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
3897
+ init.body = submission.text;
3898
+ } else if (formEncType === "application/x-www-form-urlencoded" && submission.formData) {
3899
+ // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
3900
+ init.body = convertFormDataToSearchParams(submission.formData);
3901
+ } else {
3902
+ // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
3903
+ init.body = submission.formData;
3904
+ }
3898
3905
  }
3899
-
3900
- // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
3901
3906
  return new Request(url, init);
3902
3907
  }
3903
3908
  function convertFormDataToSearchParams(formData) {
3904
3909
  let searchParams = new URLSearchParams();
3905
3910
  for (let [key, value] of formData.entries()) {
3906
3911
  // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#converting-an-entry-list-to-a-list-of-name-value-pairs
3907
- searchParams.append(key, value instanceof File ? value.name : value);
3912
+ searchParams.append(key, typeof value === "string" ? value : value.name);
3908
3913
  }
3909
3914
  return searchParams;
3910
3915
  }
3916
+ function convertSearchParamsToFormData(searchParams) {
3917
+ let formData = new FormData();
3918
+ for (let [key, value] of searchParams.entries()) {
3919
+ formData.append(key, value);
3920
+ }
3921
+ return formData;
3922
+ }
3911
3923
  function processRouteLoaderData(matches, matchesToLoad, results, pendingError, activeDeferreds) {
3912
3924
  // Fill in loaderData/errors from our loaders
3913
3925
  let loaderData = {};
@@ -4021,15 +4033,7 @@
4021
4033
  // in resolveDeferredResults
4022
4034
  invariant(false, "Unhandled fetcher deferred data");
4023
4035
  } else {
4024
- let doneFetcher = {
4025
- state: "idle",
4026
- data: result.data,
4027
- formMethod: undefined,
4028
- formAction: undefined,
4029
- formEncType: undefined,
4030
- formData: undefined,
4031
- " _hasFetcherDoneAnything ": true
4032
- };
4036
+ let doneFetcher = getDoneFetcher(result.data);
4033
4037
  state.fetchers.set(key, doneFetcher);
4034
4038
  }
4035
4039
  }
@@ -4096,6 +4100,8 @@
4096
4100
  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.";
4097
4101
  } else if (type === "defer-action") {
4098
4102
  errorMessage = "defer() is not supported in actions";
4103
+ } else if (type === "invalid-body") {
4104
+ errorMessage = "Unable to encode submission body";
4099
4105
  }
4100
4106
  } else if (status === 403) {
4101
4107
  statusText = "Forbidden";
@@ -4265,6 +4271,144 @@
4265
4271
  let pathMatches = getPathContributingMatches(matches);
4266
4272
  return pathMatches[pathMatches.length - 1];
4267
4273
  }
4274
+ function getSubmissionFromNavigation(navigation) {
4275
+ let {
4276
+ formMethod,
4277
+ formAction,
4278
+ formEncType,
4279
+ text,
4280
+ formData,
4281
+ json
4282
+ } = navigation;
4283
+ if (!formMethod || !formAction || !formEncType) {
4284
+ return;
4285
+ }
4286
+ if (text != null) {
4287
+ return {
4288
+ formMethod,
4289
+ formAction,
4290
+ formEncType,
4291
+ formData: undefined,
4292
+ json: undefined,
4293
+ text
4294
+ };
4295
+ } else if (formData != null) {
4296
+ return {
4297
+ formMethod,
4298
+ formAction,
4299
+ formEncType,
4300
+ formData,
4301
+ json: undefined,
4302
+ text: undefined
4303
+ };
4304
+ } else if (json !== undefined) {
4305
+ return {
4306
+ formMethod,
4307
+ formAction,
4308
+ formEncType,
4309
+ formData: undefined,
4310
+ json,
4311
+ text: undefined
4312
+ };
4313
+ }
4314
+ }
4315
+ function getLoadingNavigation(location, submission) {
4316
+ if (submission) {
4317
+ let navigation = {
4318
+ state: "loading",
4319
+ location,
4320
+ formMethod: submission.formMethod,
4321
+ formAction: submission.formAction,
4322
+ formEncType: submission.formEncType,
4323
+ formData: submission.formData,
4324
+ json: submission.json,
4325
+ text: submission.text
4326
+ };
4327
+ return navigation;
4328
+ } else {
4329
+ let navigation = {
4330
+ state: "loading",
4331
+ location,
4332
+ formMethod: undefined,
4333
+ formAction: undefined,
4334
+ formEncType: undefined,
4335
+ formData: undefined,
4336
+ json: undefined,
4337
+ text: undefined
4338
+ };
4339
+ return navigation;
4340
+ }
4341
+ }
4342
+ function getSubmittingNavigation(location, submission) {
4343
+ let navigation = {
4344
+ state: "submitting",
4345
+ location,
4346
+ formMethod: submission.formMethod,
4347
+ formAction: submission.formAction,
4348
+ formEncType: submission.formEncType,
4349
+ formData: submission.formData,
4350
+ json: submission.json,
4351
+ text: submission.text
4352
+ };
4353
+ return navigation;
4354
+ }
4355
+ function getLoadingFetcher(submission, data) {
4356
+ if (submission) {
4357
+ let fetcher = {
4358
+ state: "loading",
4359
+ formMethod: submission.formMethod,
4360
+ formAction: submission.formAction,
4361
+ formEncType: submission.formEncType,
4362
+ formData: submission.formData,
4363
+ json: submission.json,
4364
+ text: submission.text,
4365
+ data,
4366
+ " _hasFetcherDoneAnything ": true
4367
+ };
4368
+ return fetcher;
4369
+ } else {
4370
+ let fetcher = {
4371
+ state: "loading",
4372
+ formMethod: undefined,
4373
+ formAction: undefined,
4374
+ formEncType: undefined,
4375
+ formData: undefined,
4376
+ json: undefined,
4377
+ text: undefined,
4378
+ data,
4379
+ " _hasFetcherDoneAnything ": true
4380
+ };
4381
+ return fetcher;
4382
+ }
4383
+ }
4384
+ function getSubmittingFetcher(submission, existingFetcher) {
4385
+ let fetcher = {
4386
+ state: "submitting",
4387
+ formMethod: submission.formMethod,
4388
+ formAction: submission.formAction,
4389
+ formEncType: submission.formEncType,
4390
+ formData: submission.formData,
4391
+ json: submission.json,
4392
+ text: submission.text,
4393
+ data: existingFetcher ? existingFetcher.data : undefined,
4394
+ " _hasFetcherDoneAnything ": true
4395
+ };
4396
+ return fetcher;
4397
+ }
4398
+ function getDoneFetcher(data) {
4399
+ let fetcher = {
4400
+ state: "idle",
4401
+ formMethod: undefined,
4402
+ formAction: undefined,
4403
+ formEncType: undefined,
4404
+ formData: undefined,
4405
+ json: undefined,
4406
+ text: undefined,
4407
+ data,
4408
+ " _hasFetcherDoneAnything ": true
4409
+ };
4410
+ return fetcher;
4411
+ }
4268
4412
  //#endregion
4269
4413
 
4270
4414
  exports.AbortedDeferredError = AbortedDeferredError;