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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/router.d.ts CHANGED
@@ -120,7 +120,7 @@ export interface Router {
120
120
  * Get/create a fetcher for the given key
121
121
  * @param key
122
122
  */
123
- getFetcher<TData = any>(key?: string): Fetcher<TData>;
123
+ getFetcher<TData = any>(key: string): Fetcher<TData>;
124
124
  /**
125
125
  * @internal
126
126
  * PRIVATE - DO NOT USE
@@ -128,7 +128,7 @@ export interface Router {
128
128
  * Delete the fetcher for a given key
129
129
  * @param key
130
130
  */
131
- deleteFetcher(key?: string): void;
131
+ deleteFetcher(key: string): void;
132
132
  /**
133
133
  * @internal
134
134
  * PRIVATE - DO NOT USE
@@ -246,6 +246,7 @@ export type HydrationState = Partial<Pick<RouterState, "loaderData" | "actionDat
246
246
  * Future flags to toggle new feature behavior
247
247
  */
248
248
  export interface FutureConfig {
249
+ v7_fetcherPersist: boolean;
249
250
  v7_normalizeFormMethod: boolean;
250
251
  v7_prependBasename: boolean;
251
252
  }
@@ -303,6 +304,7 @@ type ViewTransitionOpts = {
303
304
  */
304
305
  export interface RouterSubscriber {
305
306
  (state: RouterState, opts: {
307
+ deletedFetchers: string[];
306
308
  unstable_viewTransitionOpts?: ViewTransitionOpts;
307
309
  }): void;
308
310
  }
package/dist/router.js CHANGED
@@ -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
  *
@@ -768,20 +768,29 @@ function matchPath(pattern, pathname) {
768
768
  end: true
769
769
  };
770
770
  }
771
- let [matcher, paramNames] = compilePath(pattern.path, pattern.caseSensitive, pattern.end);
771
+ let [matcher, compiledParams] = compilePath(pattern.path, pattern.caseSensitive, pattern.end);
772
772
  let match = pathname.match(matcher);
773
773
  if (!match) return null;
774
774
  let matchedPathname = match[0];
775
775
  let pathnameBase = matchedPathname.replace(/(.)\/+$/, "$1");
776
776
  let captureGroups = match.slice(1);
777
- let params = paramNames.reduce((memo, paramName, index) => {
777
+ let params = compiledParams.reduce((memo, _ref, index) => {
778
+ let {
779
+ paramName,
780
+ isOptional
781
+ } = _ref;
778
782
  // We need to compute the pathnameBase here using the raw splat value
779
783
  // instead of using params["*"] later because it will be decoded then
780
784
  if (paramName === "*") {
781
785
  let splatValue = captureGroups[index] || "";
782
786
  pathnameBase = matchedPathname.slice(0, matchedPathname.length - splatValue.length).replace(/(.)\/+$/, "$1");
783
787
  }
784
- memo[paramName] = safelyDecodeURIComponent(captureGroups[index] || "", paramName);
788
+ const value = captureGroups[index];
789
+ if (isOptional && !value) {
790
+ memo[paramName] = undefined;
791
+ } else {
792
+ memo[paramName] = safelyDecodeURIComponent(value || "", paramName);
793
+ }
785
794
  return memo;
786
795
  }, {});
787
796
  return {
@@ -799,16 +808,21 @@ function compilePath(path, caseSensitive, end) {
799
808
  end = true;
800
809
  }
801
810
  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(/\*$/, "/*") + "\"."));
802
- let paramNames = [];
811
+ let params = [];
803
812
  let regexpSource = "^" + path.replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
804
813
  .replace(/^\/*/, "/") // Make sure it has a leading /
805
- .replace(/[\\.*+^$?{}|()[\]]/g, "\\$&") // Escape special regex chars
806
- .replace(/\/:(\w+)/g, (_, paramName) => {
807
- paramNames.push(paramName);
808
- return "/([^\\/]+)";
814
+ .replace(/[\\.*+^${}|()[\]]/g, "\\$&") // Escape special regex chars
815
+ .replace(/\/:(\w+)(\?)?/g, (_, paramName, isOptional) => {
816
+ params.push({
817
+ paramName,
818
+ isOptional: isOptional != null
819
+ });
820
+ return isOptional ? "/?([^\\/]+)?" : "/([^\\/]+)";
809
821
  });
810
822
  if (path.endsWith("*")) {
811
- paramNames.push("*");
823
+ params.push({
824
+ paramName: "*"
825
+ });
812
826
  regexpSource += path === "*" || path === "/*" ? "(.*)$" // Already matched the initial /, just match the rest
813
827
  : "(?:\\/(.+)|\\/*)$"; // Don't include the / in params["*"]
814
828
  } else if (end) {
@@ -825,7 +839,7 @@ function compilePath(path, caseSensitive, end) {
825
839
  regexpSource += "(?:(?=\\/|$))";
826
840
  } else ;
827
841
  let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");
828
- return [matcher, paramNames];
842
+ return [matcher, params];
829
843
  }
830
844
  function safelyDecodeURI(value) {
831
845
  try {
@@ -1038,8 +1052,8 @@ class DeferredData {
1038
1052
  let onAbort = () => reject(new AbortedDeferredError("Deferred data aborted"));
1039
1053
  this.unlistenAbortSignal = () => this.controller.signal.removeEventListener("abort", onAbort);
1040
1054
  this.controller.signal.addEventListener("abort", onAbort);
1041
- this.data = Object.entries(data).reduce((acc, _ref) => {
1042
- let [key, value] = _ref;
1055
+ this.data = Object.entries(data).reduce((acc, _ref2) => {
1056
+ let [key, value] = _ref2;
1043
1057
  return Object.assign(acc, {
1044
1058
  [key]: this.trackPromise(key, value)
1045
1059
  });
@@ -1136,8 +1150,8 @@ class DeferredData {
1136
1150
  }
1137
1151
  get unwrappedData() {
1138
1152
  invariant(this.data !== null && this.done, "Can only unwrap data on initialized and settled deferreds");
1139
- return Object.entries(this.data).reduce((acc, _ref2) => {
1140
- let [key, value] = _ref2;
1153
+ return Object.entries(this.data).reduce((acc, _ref3) => {
1154
+ let [key, value] = _ref3;
1141
1155
  return Object.assign(acc, {
1142
1156
  [key]: unwrapTrackedPromise(value)
1143
1157
  });
@@ -1301,6 +1315,7 @@ function createRouter(init) {
1301
1315
  let basename = init.basename || "/";
1302
1316
  // Config driven behavior flags
1303
1317
  let future = _extends({
1318
+ v7_fetcherPersist: false,
1304
1319
  v7_normalizeFormMethod: false,
1305
1320
  v7_prependBasename: false
1306
1321
  }, init.future);
@@ -1403,6 +1418,11 @@ function createRouter(init) {
1403
1418
  let fetchRedirectIds = new Set();
1404
1419
  // Most recent href/match for fetcher.load calls for fetchers
1405
1420
  let fetchLoadMatches = new Map();
1421
+ // Ref-count mounted fetchers so we know when it's ok to clean them up
1422
+ let activeFetchers = new Map();
1423
+ // Fetchers that have requested a delete when using v7_fetcherPersist,
1424
+ // they'll be officially removed after they return to idle
1425
+ let deletedFetchers = new Set();
1406
1426
  // Store DeferredData instances for active route matches. When a
1407
1427
  // route loader returns defer() we stick one in here. Then, when a nested
1408
1428
  // promise resolves we update loaderData. If a new navigation starts we
@@ -1507,9 +1527,33 @@ function createRouter(init) {
1507
1527
  // Update our state and notify the calling context of the change
1508
1528
  function updateState(newState, viewTransitionOpts) {
1509
1529
  state = _extends({}, state, newState);
1530
+ // Prep fetcher cleanup so we can tell the UI which fetcher data entries
1531
+ // can be removed
1532
+ let completedFetchers = [];
1533
+ let deletedFetchersKeys = [];
1534
+ if (future.v7_fetcherPersist) {
1535
+ state.fetchers.forEach((fetcher, key) => {
1536
+ if (fetcher.state === "idle") {
1537
+ if (deletedFetchers.has(key)) {
1538
+ // Unmounted from the UI and can be totally removed
1539
+ deletedFetchersKeys.push(key);
1540
+ } else {
1541
+ // Returned to idle but still mounted in the UI, so semi-remains for
1542
+ // revalidations and such
1543
+ completedFetchers.push(key);
1544
+ }
1545
+ }
1546
+ });
1547
+ }
1510
1548
  subscribers.forEach(subscriber => subscriber(state, {
1549
+ deletedFetchers: deletedFetchersKeys,
1511
1550
  unstable_viewTransitionOpts: viewTransitionOpts
1512
1551
  }));
1552
+ // Remove idle fetchers from state since we only care about in-flight fetchers.
1553
+ if (future.v7_fetcherPersist) {
1554
+ completedFetchers.forEach(key => state.fetchers.delete(key));
1555
+ deletedFetchersKeys.forEach(key => deleteFetcher(key));
1556
+ }
1513
1557
  }
1514
1558
  // Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION
1515
1559
  // and setting state.[historyAction/location/matches] to the new route.
@@ -2027,6 +2071,14 @@ function createRouter(init) {
2027
2071
  } : {});
2028
2072
  }
2029
2073
  function getFetcher(key) {
2074
+ if (future.v7_fetcherPersist) {
2075
+ activeFetchers.set(key, (activeFetchers.get(key) || 0) + 1);
2076
+ // If this fetcher was previously marked for deletion, unmark it since we
2077
+ // have a new instance
2078
+ if (deletedFetchers.has(key)) {
2079
+ deletedFetchers.delete(key);
2080
+ }
2081
+ }
2030
2082
  return state.fetchers.get(key) || IDLE_FETCHER;
2031
2083
  }
2032
2084
  // Trigger a fetcher load/submit for the given fetcher key
@@ -2095,13 +2147,20 @@ function createRouter(init) {
2095
2147
  let originatingLoadId = incrementingLoadId;
2096
2148
  let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, manifest, mapRouteProperties, basename);
2097
2149
  if (fetchRequest.signal.aborted) {
2098
- // We can delete this so long as we weren't aborted by ou our own fetcher
2150
+ // We can delete this so long as we weren't aborted by our own fetcher
2099
2151
  // re-submit which would have put _new_ controller is in fetchControllers
2100
2152
  if (fetchControllers.get(key) === abortController) {
2101
2153
  fetchControllers.delete(key);
2102
2154
  }
2103
2155
  return;
2104
2156
  }
2157
+ if (deletedFetchers.has(key)) {
2158
+ state.fetchers.set(key, getDoneFetcher(undefined));
2159
+ updateState({
2160
+ fetchers: new Map(state.fetchers)
2161
+ });
2162
+ return;
2163
+ }
2105
2164
  if (isRedirectResult(actionResult)) {
2106
2165
  fetchControllers.delete(key);
2107
2166
  if (pendingNavigationLoadId > originatingLoadId) {
@@ -2206,7 +2265,7 @@ function createRouter(init) {
2206
2265
  let doneFetcher = getDoneFetcher(actionResult.data);
2207
2266
  state.fetchers.set(key, doneFetcher);
2208
2267
  }
2209
- let didAbortFetchLoads = abortStaleFetchLoads(loadId);
2268
+ abortStaleFetchLoads(loadId);
2210
2269
  // If we are currently in a navigation loading state and this fetcher is
2211
2270
  // more recent than the navigation, we want the newer data so abort the
2212
2271
  // navigation and complete it with the fetcher data
@@ -2223,12 +2282,11 @@ function createRouter(init) {
2223
2282
  // otherwise just update with the fetcher data, preserving any existing
2224
2283
  // loaderData for loaders that did not need to reload. We have to
2225
2284
  // manually merge here since we aren't going through completeNavigation
2226
- updateState(_extends({
2285
+ updateState({
2227
2286
  errors,
2228
- loaderData: mergeLoaderData(state.loaderData, loaderData, matches, errors)
2229
- }, didAbortFetchLoads || revalidatingFetchers.length > 0 ? {
2287
+ loaderData: mergeLoaderData(state.loaderData, loaderData, matches, errors),
2230
2288
  fetchers: new Map(state.fetchers)
2231
- } : {}));
2289
+ });
2232
2290
  isRevalidationRequired = false;
2233
2291
  }
2234
2292
  }
@@ -2262,6 +2320,13 @@ function createRouter(init) {
2262
2320
  if (fetchRequest.signal.aborted) {
2263
2321
  return;
2264
2322
  }
2323
+ if (deletedFetchers.has(key)) {
2324
+ state.fetchers.set(key, getDoneFetcher(undefined));
2325
+ updateState({
2326
+ fetchers: new Map(state.fetchers)
2327
+ });
2328
+ return;
2329
+ }
2265
2330
  // If the loader threw a redirect Response, start a new REPLACE navigation
2266
2331
  if (isRedirectResult(result)) {
2267
2332
  if (pendingNavigationLoadId > originatingLoadId) {
@@ -2281,17 +2346,7 @@ function createRouter(init) {
2281
2346
  }
2282
2347
  // Process any non-redirect errors thrown
2283
2348
  if (isErrorResult(result)) {
2284
- let boundaryMatch = findNearestBoundary(state.matches, routeId);
2285
- state.fetchers.delete(key);
2286
- // TODO: In remix, this would reset to IDLE_NAVIGATION if it was a catch -
2287
- // do we need to behave any differently with our non-redirect errors?
2288
- // What if it was a non-redirect Response?
2289
- updateState({
2290
- fetchers: new Map(state.fetchers),
2291
- errors: {
2292
- [boundaryMatch.route.id]: result.error
2293
- }
2294
- });
2349
+ setFetcherError(key, routeId, result.error);
2295
2350
  return;
2296
2351
  }
2297
2352
  invariant(!isDeferredResult(result), "Unhandled fetcher deferred data");
@@ -2456,8 +2511,25 @@ function createRouter(init) {
2456
2511
  fetchLoadMatches.delete(key);
2457
2512
  fetchReloadIds.delete(key);
2458
2513
  fetchRedirectIds.delete(key);
2514
+ deletedFetchers.delete(key);
2459
2515
  state.fetchers.delete(key);
2460
2516
  }
2517
+ function deleteFetcherAndUpdateState(key) {
2518
+ if (future.v7_fetcherPersist) {
2519
+ let count = (activeFetchers.get(key) || 0) - 1;
2520
+ if (count <= 0) {
2521
+ activeFetchers.delete(key);
2522
+ deletedFetchers.add(key);
2523
+ } else {
2524
+ activeFetchers.set(key, count);
2525
+ }
2526
+ } else {
2527
+ deleteFetcher(key);
2528
+ }
2529
+ updateState({
2530
+ fetchers: new Map(state.fetchers)
2531
+ });
2532
+ }
2461
2533
  function abortFetcher(key) {
2462
2534
  let controller = fetchControllers.get(key);
2463
2535
  invariant(controller, "Expected fetch controller: " + key);
@@ -2646,7 +2718,7 @@ function createRouter(init) {
2646
2718
  createHref: to => init.history.createHref(to),
2647
2719
  encodeLocation: to => init.history.encodeLocation(to),
2648
2720
  getFetcher,
2649
- deleteFetcher,
2721
+ deleteFetcher: deleteFetcherAndUpdateState,
2650
2722
  dispose,
2651
2723
  getBlocker,
2652
2724
  deleteBlocker,