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