@remix-run/router 1.10.0 → 1.11.0-pre.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @remix-run/router v1.10.0
2
+ * @remix-run/router v1.11.0-pre.1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -1019,20 +1019,29 @@
1019
1019
  end: true
1020
1020
  };
1021
1021
  }
1022
- let [matcher, paramNames] = compilePath(pattern.path, pattern.caseSensitive, pattern.end);
1022
+ let [matcher, compiledParams] = compilePath(pattern.path, pattern.caseSensitive, pattern.end);
1023
1023
  let match = pathname.match(matcher);
1024
1024
  if (!match) return null;
1025
1025
  let matchedPathname = match[0];
1026
1026
  let pathnameBase = matchedPathname.replace(/(.)\/+$/, "$1");
1027
1027
  let captureGroups = match.slice(1);
1028
- let params = paramNames.reduce((memo, paramName, index) => {
1028
+ let params = compiledParams.reduce((memo, _ref, index) => {
1029
+ let {
1030
+ paramName,
1031
+ isOptional
1032
+ } = _ref;
1029
1033
  // We need to compute the pathnameBase here using the raw splat value
1030
1034
  // instead of using params["*"] later because it will be decoded then
1031
1035
  if (paramName === "*") {
1032
1036
  let splatValue = captureGroups[index] || "";
1033
1037
  pathnameBase = matchedPathname.slice(0, matchedPathname.length - splatValue.length).replace(/(.)\/+$/, "$1");
1034
1038
  }
1035
- memo[paramName] = safelyDecodeURIComponent(captureGroups[index] || "", paramName);
1039
+ const value = captureGroups[index];
1040
+ if (isOptional && !value) {
1041
+ memo[paramName] = undefined;
1042
+ } else {
1043
+ memo[paramName] = safelyDecodeURIComponent(value || "", paramName);
1044
+ }
1036
1045
  return memo;
1037
1046
  }, {});
1038
1047
  return {
@@ -1050,16 +1059,21 @@
1050
1059
  end = true;
1051
1060
  }
1052
1061
  warning(path === "*" || !path.endsWith("*") || path.endsWith("/*"), "Route path \"" + path + "\" will be treated as if it were " + ("\"" + path.replace(/\*$/, "/*") + "\" because the `*` character must ") + "always follow a `/` in the pattern. To get rid of this warning, " + ("please change the route path to \"" + path.replace(/\*$/, "/*") + "\"."));
1053
- let paramNames = [];
1062
+ let params = [];
1054
1063
  let regexpSource = "^" + path.replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
1055
1064
  .replace(/^\/*/, "/") // Make sure it has a leading /
1056
- .replace(/[\\.*+^$?{}|()[\]]/g, "\\$&") // Escape special regex chars
1057
- .replace(/\/:(\w+)/g, (_, paramName) => {
1058
- paramNames.push(paramName);
1059
- return "/([^\\/]+)";
1065
+ .replace(/[\\.*+^${}|()[\]]/g, "\\$&") // Escape special regex chars
1066
+ .replace(/\/:(\w+)(\?)?/g, (_, paramName, isOptional) => {
1067
+ params.push({
1068
+ paramName,
1069
+ isOptional: isOptional != null
1070
+ });
1071
+ return isOptional ? "/?([^\\/]+)?" : "/([^\\/]+)";
1060
1072
  });
1061
1073
  if (path.endsWith("*")) {
1062
- paramNames.push("*");
1074
+ params.push({
1075
+ paramName: "*"
1076
+ });
1063
1077
  regexpSource += path === "*" || path === "/*" ? "(.*)$" // Already matched the initial /, just match the rest
1064
1078
  : "(?:\\/(.+)|\\/*)$"; // Don't include the / in params["*"]
1065
1079
  } else if (end) {
@@ -1076,7 +1090,7 @@
1076
1090
  regexpSource += "(?:(?=\\/|$))";
1077
1091
  } else ;
1078
1092
  let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");
1079
- return [matcher, paramNames];
1093
+ return [matcher, params];
1080
1094
  }
1081
1095
  function safelyDecodeURI(value) {
1082
1096
  try {
@@ -1304,8 +1318,8 @@
1304
1318
  let onAbort = () => reject(new AbortedDeferredError("Deferred data aborted"));
1305
1319
  this.unlistenAbortSignal = () => this.controller.signal.removeEventListener("abort", onAbort);
1306
1320
  this.controller.signal.addEventListener("abort", onAbort);
1307
- this.data = Object.entries(data).reduce((acc, _ref) => {
1308
- let [key, value] = _ref;
1321
+ this.data = Object.entries(data).reduce((acc, _ref2) => {
1322
+ let [key, value] = _ref2;
1309
1323
  return Object.assign(acc, {
1310
1324
  [key]: this.trackPromise(key, value)
1311
1325
  });
@@ -1405,8 +1419,8 @@
1405
1419
  }
1406
1420
  get unwrappedData() {
1407
1421
  invariant(this.data !== null && this.done, "Can only unwrap data on initialized and settled deferreds");
1408
- return Object.entries(this.data).reduce((acc, _ref2) => {
1409
- let [key, value] = _ref2;
1422
+ return Object.entries(this.data).reduce((acc, _ref3) => {
1423
+ let [key, value] = _ref3;
1410
1424
  return Object.assign(acc, {
1411
1425
  [key]: unwrapTrackedPromise(value)
1412
1426
  });
@@ -1651,6 +1665,7 @@
1651
1665
  let basename = init.basename || "/";
1652
1666
  // Config driven behavior flags
1653
1667
  let future = _extends({
1668
+ v7_fetcherPersist: false,
1654
1669
  v7_normalizeFormMethod: false,
1655
1670
  v7_prependBasename: false
1656
1671
  }, init.future);
@@ -1770,6 +1785,13 @@
1770
1785
  // Most recent href/match for fetcher.load calls for fetchers
1771
1786
  let fetchLoadMatches = new Map();
1772
1787
 
1788
+ // Ref-count mounted fetchers so we know when it's ok to clean them up
1789
+ let activeFetchers = new Map();
1790
+
1791
+ // Fetchers that have requested a delete when using v7_fetcherPersist,
1792
+ // they'll be officially removed after they return to idle
1793
+ let deletedFetchers = new Set();
1794
+
1773
1795
  // Store DeferredData instances for active route matches. When a
1774
1796
  // route loader returns defer() we stick one in here. Then, when a nested
1775
1797
  // promise resolves we update loaderData. If a new navigation starts we
@@ -1882,9 +1904,35 @@
1882
1904
  // Update our state and notify the calling context of the change
1883
1905
  function updateState(newState, viewTransitionOpts) {
1884
1906
  state = _extends({}, state, newState);
1907
+
1908
+ // Prep fetcher cleanup so we can tell the UI which fetcher data entries
1909
+ // can be removed
1910
+ let completedFetchers = [];
1911
+ let deletedFetchersKeys = [];
1912
+ if (future.v7_fetcherPersist) {
1913
+ state.fetchers.forEach((fetcher, key) => {
1914
+ if (fetcher.state === "idle") {
1915
+ if (deletedFetchers.has(key)) {
1916
+ // Unmounted from the UI and can be totally removed
1917
+ deletedFetchersKeys.push(key);
1918
+ } else {
1919
+ // Returned to idle but still mounted in the UI, so semi-remains for
1920
+ // revalidations and such
1921
+ completedFetchers.push(key);
1922
+ }
1923
+ }
1924
+ });
1925
+ }
1885
1926
  subscribers.forEach(subscriber => subscriber(state, {
1927
+ deletedFetchers: deletedFetchersKeys,
1886
1928
  unstable_viewTransitionOpts: viewTransitionOpts
1887
1929
  }));
1930
+
1931
+ // Remove idle fetchers from state since we only care about in-flight fetchers.
1932
+ if (future.v7_fetcherPersist) {
1933
+ completedFetchers.forEach(key => state.fetchers.delete(key));
1934
+ deletedFetchersKeys.forEach(key => deleteFetcher(key));
1935
+ }
1888
1936
  }
1889
1937
 
1890
1938
  // Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION
@@ -2437,6 +2485,14 @@
2437
2485
  } : {});
2438
2486
  }
2439
2487
  function getFetcher(key) {
2488
+ if (future.v7_fetcherPersist) {
2489
+ activeFetchers.set(key, (activeFetchers.get(key) || 0) + 1);
2490
+ // If this fetcher was previously marked for deletion, unmark it since we
2491
+ // have a new instance
2492
+ if (deletedFetchers.has(key)) {
2493
+ deletedFetchers.delete(key);
2494
+ }
2495
+ }
2440
2496
  return state.fetchers.get(key) || IDLE_FETCHER;
2441
2497
  }
2442
2498
 
@@ -2510,13 +2566,20 @@
2510
2566
  let originatingLoadId = incrementingLoadId;
2511
2567
  let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, manifest, mapRouteProperties, basename);
2512
2568
  if (fetchRequest.signal.aborted) {
2513
- // We can delete this so long as we weren't aborted by ou our own fetcher
2569
+ // We can delete this so long as we weren't aborted by our own fetcher
2514
2570
  // re-submit which would have put _new_ controller is in fetchControllers
2515
2571
  if (fetchControllers.get(key) === abortController) {
2516
2572
  fetchControllers.delete(key);
2517
2573
  }
2518
2574
  return;
2519
2575
  }
2576
+ if (deletedFetchers.has(key)) {
2577
+ state.fetchers.set(key, getDoneFetcher(undefined));
2578
+ updateState({
2579
+ fetchers: new Map(state.fetchers)
2580
+ });
2581
+ return;
2582
+ }
2520
2583
  if (isRedirectResult(actionResult)) {
2521
2584
  fetchControllers.delete(key);
2522
2585
  if (pendingNavigationLoadId > originatingLoadId) {
@@ -2626,7 +2689,7 @@
2626
2689
  let doneFetcher = getDoneFetcher(actionResult.data);
2627
2690
  state.fetchers.set(key, doneFetcher);
2628
2691
  }
2629
- let didAbortFetchLoads = abortStaleFetchLoads(loadId);
2692
+ abortStaleFetchLoads(loadId);
2630
2693
 
2631
2694
  // If we are currently in a navigation loading state and this fetcher is
2632
2695
  // more recent than the navigation, we want the newer data so abort the
@@ -2644,12 +2707,11 @@
2644
2707
  // otherwise just update with the fetcher data, preserving any existing
2645
2708
  // loaderData for loaders that did not need to reload. We have to
2646
2709
  // manually merge here since we aren't going through completeNavigation
2647
- updateState(_extends({
2710
+ updateState({
2648
2711
  errors,
2649
- loaderData: mergeLoaderData(state.loaderData, loaderData, matches, errors)
2650
- }, didAbortFetchLoads || revalidatingFetchers.length > 0 ? {
2712
+ loaderData: mergeLoaderData(state.loaderData, loaderData, matches, errors),
2651
2713
  fetchers: new Map(state.fetchers)
2652
- } : {}));
2714
+ });
2653
2715
  isRevalidationRequired = false;
2654
2716
  }
2655
2717
  }
@@ -2687,6 +2749,13 @@
2687
2749
  if (fetchRequest.signal.aborted) {
2688
2750
  return;
2689
2751
  }
2752
+ if (deletedFetchers.has(key)) {
2753
+ state.fetchers.set(key, getDoneFetcher(undefined));
2754
+ updateState({
2755
+ fetchers: new Map(state.fetchers)
2756
+ });
2757
+ return;
2758
+ }
2690
2759
 
2691
2760
  // If the loader threw a redirect Response, start a new REPLACE navigation
2692
2761
  if (isRedirectResult(result)) {
@@ -2708,17 +2777,7 @@
2708
2777
 
2709
2778
  // Process any non-redirect errors thrown
2710
2779
  if (isErrorResult(result)) {
2711
- let boundaryMatch = findNearestBoundary(state.matches, routeId);
2712
- state.fetchers.delete(key);
2713
- // TODO: In remix, this would reset to IDLE_NAVIGATION if it was a catch -
2714
- // do we need to behave any differently with our non-redirect errors?
2715
- // What if it was a non-redirect Response?
2716
- updateState({
2717
- fetchers: new Map(state.fetchers),
2718
- errors: {
2719
- [boundaryMatch.route.id]: result.error
2720
- }
2721
- });
2780
+ setFetcherError(key, routeId, result.error);
2722
2781
  return;
2723
2782
  }
2724
2783
  invariant(!isDeferredResult(result), "Unhandled fetcher deferred data");
@@ -2890,8 +2949,25 @@
2890
2949
  fetchLoadMatches.delete(key);
2891
2950
  fetchReloadIds.delete(key);
2892
2951
  fetchRedirectIds.delete(key);
2952
+ deletedFetchers.delete(key);
2893
2953
  state.fetchers.delete(key);
2894
2954
  }
2955
+ function deleteFetcherAndUpdateState(key) {
2956
+ if (future.v7_fetcherPersist) {
2957
+ let count = (activeFetchers.get(key) || 0) - 1;
2958
+ if (count <= 0) {
2959
+ activeFetchers.delete(key);
2960
+ deletedFetchers.add(key);
2961
+ } else {
2962
+ activeFetchers.set(key, count);
2963
+ }
2964
+ } else {
2965
+ deleteFetcher(key);
2966
+ }
2967
+ updateState({
2968
+ fetchers: new Map(state.fetchers)
2969
+ });
2970
+ }
2895
2971
  function abortFetcher(key) {
2896
2972
  let controller = fetchControllers.get(key);
2897
2973
  invariant(controller, "Expected fetch controller: " + key);
@@ -3086,7 +3162,7 @@
3086
3162
  createHref: to => init.history.createHref(to),
3087
3163
  encodeLocation: to => init.history.encodeLocation(to),
3088
3164
  getFetcher,
3089
- deleteFetcher,
3165
+ deleteFetcher: deleteFetcherAndUpdateState,
3090
3166
  dispose,
3091
3167
  getBlocker,
3092
3168
  deleteBlocker,