@remix-run/router 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/router.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @remix-run/router v1.5.0
2
+ * @remix-run/router v1.6.0
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -534,7 +534,7 @@ function isIndexRoute(route) {
534
534
  // solely with AgnosticDataRouteObject's within the Router
535
535
 
536
536
 
537
- function convertRoutesToDataRoutes(routes, detectErrorBoundary, parentPath, manifest) {
537
+ function convertRoutesToDataRoutes(routes, mapRouteProperties, parentPath, manifest) {
538
538
  if (parentPath === void 0) {
539
539
  parentPath = [];
540
540
  }
@@ -550,24 +550,22 @@ function convertRoutesToDataRoutes(routes, detectErrorBoundary, parentPath, mani
550
550
  invariant(!manifest[id], "Found a route id collision on id \"" + id + "\". Route " + "id's must be globally unique within Data Router usages");
551
551
 
552
552
  if (isIndexRoute(route)) {
553
- let indexRoute = _extends({}, route, {
554
- hasErrorBoundary: detectErrorBoundary(route),
553
+ let indexRoute = _extends({}, route, mapRouteProperties(route), {
555
554
  id
556
555
  });
557
556
 
558
557
  manifest[id] = indexRoute;
559
558
  return indexRoute;
560
559
  } else {
561
- let pathOrLayoutRoute = _extends({}, route, {
560
+ let pathOrLayoutRoute = _extends({}, route, mapRouteProperties(route), {
562
561
  id,
563
- hasErrorBoundary: detectErrorBoundary(route),
564
562
  children: undefined
565
563
  });
566
564
 
567
565
  manifest[id] = pathOrLayoutRoute;
568
566
 
569
567
  if (route.children) {
570
- pathOrLayoutRoute.children = convertRoutesToDataRoutes(route.children, detectErrorBoundary, treePath, manifest);
568
+ pathOrLayoutRoute.children = convertRoutesToDataRoutes(route.children, mapRouteProperties, treePath, manifest);
571
569
  }
572
570
 
573
571
  return pathOrLayoutRoute;
@@ -1419,7 +1417,9 @@ const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
1419
1417
  const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined";
1420
1418
  const isServer = !isBrowser;
1421
1419
 
1422
- const defaultDetectErrorBoundary = route => Boolean(route.hasErrorBoundary); //#endregion
1420
+ const defaultMapRouteProperties = route => ({
1421
+ hasErrorBoundary: Boolean(route.hasErrorBoundary)
1422
+ }); //#endregion
1423
1423
  ////////////////////////////////////////////////////////////////////////////////
1424
1424
  //#region createRouter
1425
1425
  ////////////////////////////////////////////////////////////////////////////////
@@ -1431,15 +1431,31 @@ const defaultDetectErrorBoundary = route => Boolean(route.hasErrorBoundary); //#
1431
1431
 
1432
1432
  function createRouter(init) {
1433
1433
  invariant(init.routes.length > 0, "You must provide a non-empty routes array to createRouter");
1434
- let detectErrorBoundary = init.detectErrorBoundary || defaultDetectErrorBoundary; // Routes keyed by ID
1434
+ let mapRouteProperties;
1435
+
1436
+ if (init.mapRouteProperties) {
1437
+ mapRouteProperties = init.mapRouteProperties;
1438
+ } else if (init.detectErrorBoundary) {
1439
+ // If they are still using the deprecated version, wrap it with the new API
1440
+ let detectErrorBoundary = init.detectErrorBoundary;
1441
+
1442
+ mapRouteProperties = route => ({
1443
+ hasErrorBoundary: detectErrorBoundary(route)
1444
+ });
1445
+ } else {
1446
+ mapRouteProperties = defaultMapRouteProperties;
1447
+ } // Routes keyed by ID
1448
+
1435
1449
 
1436
1450
  let manifest = {}; // Routes in tree format for matching
1437
1451
 
1438
- let dataRoutes = convertRoutesToDataRoutes(init.routes, detectErrorBoundary, undefined, manifest);
1439
- let inFlightDataRoutes; // Config driven behavior flags
1452
+ let dataRoutes = convertRoutesToDataRoutes(init.routes, mapRouteProperties, undefined, manifest);
1453
+ let inFlightDataRoutes;
1454
+ let basename = init.basename || "/"; // Config driven behavior flags
1440
1455
 
1441
1456
  let future = _extends({
1442
- v7_normalizeFormMethod: false
1457
+ v7_normalizeFormMethod: false,
1458
+ v7_prependBasename: false
1443
1459
  }, init.future); // Cleanup function for history
1444
1460
 
1445
1461
 
@@ -1459,7 +1475,7 @@ function createRouter(init) {
1459
1475
  // SSR did the initial scroll restoration.
1460
1476
 
1461
1477
  let initialScrollRestored = init.hydrationData != null;
1462
- let initialMatches = matchRoutes(dataRoutes, init.history.location, init.basename);
1478
+ let initialMatches = matchRoutes(dataRoutes, init.history.location, basename);
1463
1479
  let initialErrors = null;
1464
1480
 
1465
1481
  if (initialMatches == null) {
@@ -1511,7 +1527,7 @@ function createRouter(init) {
1511
1527
 
1512
1528
  let isUninterruptedRevalidation = false; // Use this internal flag to force revalidation of all loaders:
1513
1529
  // - submissions (completed or interrupted)
1514
- // - useRevalidate()
1530
+ // - useRevalidator()
1515
1531
  // - X-Remix-Revalidate (from redirect)
1516
1532
 
1517
1533
  let isRevalidationRequired = false; // Use this internal array to capture routes that require revalidation due
@@ -1530,7 +1546,7 @@ function createRouter(init) {
1530
1546
 
1531
1547
  let pendingNavigationLoadId = -1; // Fetchers that triggered data reloads as a result of their actions
1532
1548
 
1533
- let fetchReloadIds = new Map(); // Fetchers that triggered redirect navigations from their actions
1549
+ let fetchReloadIds = new Map(); // Fetchers that triggered redirect navigations
1534
1550
 
1535
1551
  let fetchRedirectIds = new Set(); // Most recent href/match for fetcher.load calls for fetchers
1536
1552
 
@@ -1726,11 +1742,12 @@ function createRouter(init) {
1726
1742
  return;
1727
1743
  }
1728
1744
 
1745
+ let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, to, opts == null ? void 0 : opts.fromRouteId, opts == null ? void 0 : opts.relative);
1729
1746
  let {
1730
1747
  path,
1731
1748
  submission,
1732
1749
  error
1733
- } = normalizeNavigateOptions(to, future, opts);
1750
+ } = normalizeNavigateOptions(future.v7_normalizeFormMethod, false, normalizedPath, opts);
1734
1751
  let currentLocation = state.location;
1735
1752
  let nextLocation = createLocation(state.location, path, opts && opts.state); // When using navigate as a PUSH/REPLACE we aren't reading an already-encoded
1736
1753
  // URL from window.location, so we need to encode it here so the behavior
@@ -1846,7 +1863,7 @@ function createRouter(init) {
1846
1863
  pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
1847
1864
  let routesToUse = inFlightDataRoutes || dataRoutes;
1848
1865
  let loadingNavigation = opts && opts.overrideNavigation;
1849
- let matches = matchRoutes(routesToUse, location, init.basename); // Short circuit with a 404 on the root error boundary if we match nothing
1866
+ let matches = matchRoutes(routesToUse, location, basename); // Short circuit with a 404 on the root error boundary if we match nothing
1850
1867
 
1851
1868
  if (!matches) {
1852
1869
  let error = getInternalRouterError(404, {
@@ -1969,7 +1986,7 @@ function createRouter(init) {
1969
1986
  })
1970
1987
  };
1971
1988
  } else {
1972
- result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, detectErrorBoundary, router.basename);
1989
+ result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename);
1973
1990
 
1974
1991
  if (request.signal.aborted) {
1975
1992
  return {
@@ -2061,13 +2078,14 @@ function createRouter(init) {
2061
2078
  formEncType: loadingNavigation.formEncType
2062
2079
  } : undefined;
2063
2080
  let routesToUse = inFlightDataRoutes || dataRoutes;
2064
- let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, activeSubmission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, fetchLoadMatches, routesToUse, init.basename, pendingActionData, pendingError); // Cancel pending deferreds for no-longer-matched routes or routes we're
2081
+ let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, activeSubmission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, fetchLoadMatches, routesToUse, basename, pendingActionData, pendingError); // Cancel pending deferreds for no-longer-matched routes or routes we're
2065
2082
  // about to reload. Note that if this is an action reload we would have
2066
2083
  // already cancelled all pending deferreds so this would be a no-op
2067
2084
 
2068
2085
  cancelActiveDeferreds(routeId => !(matches && matches.some(m => m.route.id === routeId)) || matchesToLoad && matchesToLoad.some(m => m.route.id === routeId)); // Short circuit if we have no loaders to run
2069
2086
 
2070
2087
  if (matchesToLoad.length === 0 && revalidatingFetchers.length === 0) {
2088
+ let updatedFetchers = markFetchRedirectsDone();
2071
2089
  completeNavigation(location, _extends({
2072
2090
  matches,
2073
2091
  loaderData: {},
@@ -2075,6 +2093,8 @@ function createRouter(init) {
2075
2093
  errors: pendingError || null
2076
2094
  }, pendingActionData ? {
2077
2095
  actionData: pendingActionData
2096
+ } : {}, updatedFetchers ? {
2097
+ fetchers: new Map(state.fetchers)
2078
2098
  } : {}));
2079
2099
  return {
2080
2100
  shortCircuited: true
@@ -2112,7 +2132,21 @@ function createRouter(init) {
2112
2132
  }
2113
2133
 
2114
2134
  pendingNavigationLoadId = ++incrementingLoadId;
2115
- revalidatingFetchers.forEach(rf => fetchControllers.set(rf.key, pendingNavigationController));
2135
+ revalidatingFetchers.forEach(rf => {
2136
+ if (rf.controller) {
2137
+ // Fetchers use an independent AbortController so that aborting a fetcher
2138
+ // (via deleteFetcher) does not abort the triggering navigation that
2139
+ // triggered the revalidation
2140
+ fetchControllers.set(rf.key, rf.controller);
2141
+ }
2142
+ }); // Proxy navigation abort through to revalidation fetchers
2143
+
2144
+ let abortPendingFetchRevalidations = () => revalidatingFetchers.forEach(f => abortFetcher(f.key));
2145
+
2146
+ if (pendingNavigationController) {
2147
+ pendingNavigationController.signal.addEventListener("abort", abortPendingFetchRevalidations);
2148
+ }
2149
+
2116
2150
  let {
2117
2151
  results,
2118
2152
  loaderResults,
@@ -2128,6 +2162,10 @@ function createRouter(init) {
2128
2162
  // reassigned to new controllers for the next navigation
2129
2163
 
2130
2164
 
2165
+ if (pendingNavigationController) {
2166
+ pendingNavigationController.signal.removeEventListener("abort", abortPendingFetchRevalidations);
2167
+ }
2168
+
2131
2169
  revalidatingFetchers.forEach(rf => fetchControllers.delete(rf.key)); // If any loaders returned a redirect Response, start a new REPLACE navigation
2132
2170
 
2133
2171
  let redirect = findRedirect(results);
@@ -2157,12 +2195,13 @@ function createRouter(init) {
2157
2195
  }
2158
2196
  });
2159
2197
  });
2160
- markFetchRedirectsDone();
2198
+ let updatedFetchers = markFetchRedirectsDone();
2161
2199
  let didAbortFetchLoads = abortStaleFetchLoads(pendingNavigationLoadId);
2200
+ let shouldUpdateFetchers = updatedFetchers || didAbortFetchLoads || revalidatingFetchers.length > 0;
2162
2201
  return _extends({
2163
2202
  loaderData,
2164
2203
  errors
2165
- }, didAbortFetchLoads || revalidatingFetchers.length > 0 ? {
2204
+ }, shouldUpdateFetchers ? {
2166
2205
  fetchers: new Map(state.fetchers)
2167
2206
  } : {});
2168
2207
  }
@@ -2179,11 +2218,12 @@ function createRouter(init) {
2179
2218
 
2180
2219
  if (fetchControllers.has(key)) abortFetcher(key);
2181
2220
  let routesToUse = inFlightDataRoutes || dataRoutes;
2182
- let matches = matchRoutes(routesToUse, href, init.basename);
2221
+ let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, href, routeId, opts == null ? void 0 : opts.relative);
2222
+ let matches = matchRoutes(routesToUse, normalizedPath, basename);
2183
2223
 
2184
2224
  if (!matches) {
2185
2225
  setFetcherError(key, routeId, getInternalRouterError(404, {
2186
- pathname: href
2226
+ pathname: normalizedPath
2187
2227
  }));
2188
2228
  return;
2189
2229
  }
@@ -2191,7 +2231,7 @@ function createRouter(init) {
2191
2231
  let {
2192
2232
  path,
2193
2233
  submission
2194
- } = normalizeNavigateOptions(href, future, opts, true);
2234
+ } = normalizeNavigateOptions(future.v7_normalizeFormMethod, true, normalizedPath, opts);
2195
2235
  let match = getTargetMatch(matches, path);
2196
2236
  pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
2197
2237
 
@@ -2243,7 +2283,7 @@ function createRouter(init) {
2243
2283
  let abortController = new AbortController();
2244
2284
  let fetchRequest = createClientSideRequest(init.history, path, abortController.signal, submission);
2245
2285
  fetchControllers.set(key, abortController);
2246
- let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, manifest, detectErrorBoundary, router.basename);
2286
+ let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, manifest, mapRouteProperties, basename);
2247
2287
 
2248
2288
  if (fetchRequest.signal.aborted) {
2249
2289
  // We can delete this so long as we weren't aborted by ou our own fetcher
@@ -2293,7 +2333,7 @@ function createRouter(init) {
2293
2333
  let nextLocation = state.navigation.location || state.location;
2294
2334
  let revalidationRequest = createClientSideRequest(init.history, nextLocation, abortController.signal);
2295
2335
  let routesToUse = inFlightDataRoutes || dataRoutes;
2296
- let matches = state.navigation.state !== "idle" ? matchRoutes(routesToUse, state.navigation.location, init.basename) : state.matches;
2336
+ let matches = state.navigation.state !== "idle" ? matchRoutes(routesToUse, state.navigation.location, basename) : state.matches;
2297
2337
  invariant(matches, "Didn't find any matches after fetcher action");
2298
2338
  let loadId = ++incrementingLoadId;
2299
2339
  fetchReloadIds.set(key, loadId);
@@ -2306,7 +2346,7 @@ function createRouter(init) {
2306
2346
  });
2307
2347
 
2308
2348
  state.fetchers.set(key, loadFetcher);
2309
- let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, submission, nextLocation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, fetchLoadMatches, routesToUse, init.basename, {
2349
+ let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, submission, nextLocation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, fetchLoadMatches, routesToUse, basename, {
2310
2350
  [match.route.id]: actionResult.data
2311
2351
  }, undefined // No need to send through errors since we short circuit above
2312
2352
  ); // Put all revalidating fetchers into the loading state, except for the
@@ -2326,11 +2366,18 @@ function createRouter(init) {
2326
2366
  " _hasFetcherDoneAnything ": true
2327
2367
  };
2328
2368
  state.fetchers.set(staleKey, revalidatingFetcher);
2329
- fetchControllers.set(staleKey, abortController);
2369
+
2370
+ if (rf.controller) {
2371
+ fetchControllers.set(staleKey, rf.controller);
2372
+ }
2330
2373
  });
2331
2374
  updateState({
2332
2375
  fetchers: new Map(state.fetchers)
2333
2376
  });
2377
+
2378
+ let abortPendingFetchRevalidations = () => revalidatingFetchers.forEach(rf => abortFetcher(rf.key));
2379
+
2380
+ abortController.signal.addEventListener("abort", abortPendingFetchRevalidations);
2334
2381
  let {
2335
2382
  results,
2336
2383
  loaderResults,
@@ -2341,6 +2388,7 @@ function createRouter(init) {
2341
2388
  return;
2342
2389
  }
2343
2390
 
2391
+ abortController.signal.removeEventListener("abort", abortPendingFetchRevalidations);
2344
2392
  fetchReloadIds.delete(key);
2345
2393
  fetchControllers.delete(key);
2346
2394
  revalidatingFetchers.forEach(r => fetchControllers.delete(r.key));
@@ -2415,14 +2463,14 @@ function createRouter(init) {
2415
2463
  let abortController = new AbortController();
2416
2464
  let fetchRequest = createClientSideRequest(init.history, path, abortController.signal);
2417
2465
  fetchControllers.set(key, abortController);
2418
- let result = await callLoaderOrAction("loader", fetchRequest, match, matches, manifest, detectErrorBoundary, router.basename); // Deferred isn't supported for fetcher loads, await everything and treat it
2466
+ let result = await callLoaderOrAction("loader", fetchRequest, match, matches, manifest, mapRouteProperties, basename); // Deferred isn't supported for fetcher loads, await everything and treat it
2419
2467
  // as a normal load. resolveDeferredData will return undefined if this
2420
2468
  // fetcher gets aborted, so we just leave result untouched and short circuit
2421
2469
  // below if that happens
2422
2470
 
2423
2471
  if (isDeferredResult(result)) {
2424
2472
  result = (await resolveDeferredData(result, fetchRequest.signal, true)) || result;
2425
- } // We can delete this so long as we weren't aborted by ou our own fetcher
2473
+ } // We can delete this so long as we weren't aborted by our our own fetcher
2426
2474
  // re-load which would have put _new_ controller is in fetchControllers
2427
2475
 
2428
2476
 
@@ -2436,6 +2484,7 @@ function createRouter(init) {
2436
2484
 
2437
2485
 
2438
2486
  if (isRedirectResult(result)) {
2487
+ fetchRedirectIds.add(key);
2439
2488
  await startRedirectNavigation(state, result);
2440
2489
  return;
2441
2490
  } // Process any non-redirect errors thrown
@@ -2516,7 +2565,7 @@ function createRouter(init) {
2516
2565
 
2517
2566
  if (ABSOLUTE_URL_REGEX.test(redirect.location) && isBrowser && typeof ((_window = window) == null ? void 0 : _window.location) !== "undefined") {
2518
2567
  let url = init.history.createURL(redirect.location);
2519
- let isDifferentBasename = stripBasename(url.pathname, init.basename || "/") == null;
2568
+ let isDifferentBasename = stripBasename(url.pathname, basename) == null;
2520
2569
 
2521
2570
  if (window.location.origin !== url.origin || isDifferentBasename) {
2522
2571
  if (replace) {
@@ -2600,9 +2649,9 @@ function createRouter(init) {
2600
2649
  // Call all navigation loaders and revalidating fetcher loaders in parallel,
2601
2650
  // then slice off the results into separate arrays so we can handle them
2602
2651
  // accordingly
2603
- let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, detectErrorBoundary, router.basename)), ...fetchersToLoad.map(f => {
2604
- if (f.matches && f.match) {
2605
- return callLoaderOrAction("loader", createClientSideRequest(init.history, f.path, request.signal), f.match, f.matches, manifest, detectErrorBoundary, router.basename);
2652
+ let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename)), ...fetchersToLoad.map(f => {
2653
+ if (f.matches && f.match && f.controller) {
2654
+ return callLoaderOrAction("loader", createClientSideRequest(init.history, f.path, f.controller.signal), f.match, f.matches, manifest, mapRouteProperties, basename);
2606
2655
  } else {
2607
2656
  let error = {
2608
2657
  type: ResultType.error,
@@ -2615,7 +2664,7 @@ function createRouter(init) {
2615
2664
  })]);
2616
2665
  let loaderResults = results.slice(0, matchesToLoad.length);
2617
2666
  let fetcherResults = results.slice(matchesToLoad.length);
2618
- await Promise.all([resolveDeferredResults(currentMatches, matchesToLoad, loaderResults, request.signal, false, state.loaderData), resolveDeferredResults(currentMatches, fetchersToLoad.map(f => f.match), fetcherResults, request.signal, true)]);
2667
+ await Promise.all([resolveDeferredResults(currentMatches, matchesToLoad, loaderResults, loaderResults.map(() => request.signal), false, state.loaderData), resolveDeferredResults(currentMatches, fetchersToLoad.map(f => f.match), fetcherResults, fetchersToLoad.map(f => f.controller ? f.controller.signal : null), true)]);
2619
2668
  return {
2620
2669
  results,
2621
2670
  loaderResults,
@@ -2682,6 +2731,7 @@ function createRouter(init) {
2682
2731
 
2683
2732
  function markFetchRedirectsDone() {
2684
2733
  let doneKeys = [];
2734
+ let updatedFetchers = false;
2685
2735
 
2686
2736
  for (let key of fetchRedirectIds) {
2687
2737
  let fetcher = state.fetchers.get(key);
@@ -2690,10 +2740,12 @@ function createRouter(init) {
2690
2740
  if (fetcher.state === "loading") {
2691
2741
  fetchRedirectIds.delete(key);
2692
2742
  doneKeys.push(key);
2743
+ updatedFetchers = true;
2693
2744
  }
2694
2745
  }
2695
2746
 
2696
2747
  markFetchersDone(doneKeys);
2748
+ return updatedFetchers;
2697
2749
  }
2698
2750
 
2699
2751
  function abortStaleFetchLoads(landedId) {
@@ -2853,7 +2905,7 @@ function createRouter(init) {
2853
2905
 
2854
2906
  router = {
2855
2907
  get basename() {
2856
- return init.basename;
2908
+ return basename;
2857
2909
  },
2858
2910
 
2859
2911
  get state() {
@@ -2895,9 +2947,23 @@ const UNSAFE_DEFERRED_SYMBOL = Symbol("deferred");
2895
2947
  function createStaticHandler(routes, opts) {
2896
2948
  invariant(routes.length > 0, "You must provide a non-empty routes array to createStaticHandler");
2897
2949
  let manifest = {};
2898
- let detectErrorBoundary = (opts == null ? void 0 : opts.detectErrorBoundary) || defaultDetectErrorBoundary;
2899
- let dataRoutes = convertRoutesToDataRoutes(routes, detectErrorBoundary, undefined, manifest);
2900
2950
  let basename = (opts ? opts.basename : null) || "/";
2951
+ let mapRouteProperties;
2952
+
2953
+ if (opts != null && opts.mapRouteProperties) {
2954
+ mapRouteProperties = opts.mapRouteProperties;
2955
+ } else if (opts != null && opts.detectErrorBoundary) {
2956
+ // If they are still using the deprecated version, wrap it with the new API
2957
+ let detectErrorBoundary = opts.detectErrorBoundary;
2958
+
2959
+ mapRouteProperties = route => ({
2960
+ hasErrorBoundary: detectErrorBoundary(route)
2961
+ });
2962
+ } else {
2963
+ mapRouteProperties = defaultMapRouteProperties;
2964
+ }
2965
+
2966
+ let dataRoutes = convertRoutesToDataRoutes(routes, mapRouteProperties, undefined, manifest);
2901
2967
  /**
2902
2968
  * The query() method is intended for document requests, in which we want to
2903
2969
  * call an optional action and potentially multiple loaders for all nested
@@ -3134,7 +3200,7 @@ function createStaticHandler(routes, opts) {
3134
3200
  error
3135
3201
  };
3136
3202
  } else {
3137
- result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, detectErrorBoundary, basename, true, isRouteRequest, requestContext);
3203
+ result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, true, isRouteRequest, requestContext);
3138
3204
 
3139
3205
  if (request.signal.aborted) {
3140
3206
  let method = isRouteRequest ? "queryRoute" : "query";
@@ -3257,7 +3323,7 @@ function createStaticHandler(routes, opts) {
3257
3323
  };
3258
3324
  }
3259
3325
 
3260
- let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, detectErrorBoundary, basename, true, isRouteRequest, requestContext))]);
3326
+ let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename, true, isRouteRequest, requestContext))]);
3261
3327
 
3262
3328
  if (request.signal.aborted) {
3263
3329
  let method = isRouteRequest ? "queryRoute" : "query";
@@ -3308,17 +3374,62 @@ function getStaticContextFromError(routes, context, error) {
3308
3374
 
3309
3375
  function isSubmissionNavigation(opts) {
3310
3376
  return opts != null && "formData" in opts;
3311
- } // Normalize navigation options by converting formMethod=GET formData objects to
3312
- // URLSearchParams so they behave identically to links with query params
3377
+ }
3313
3378
 
3379
+ function normalizeTo(location, matches, basename, prependBasename, to, fromRouteId, relative) {
3380
+ let contextualMatches;
3381
+ let activeRouteMatch;
3314
3382
 
3315
- function normalizeNavigateOptions(to, future, opts, isFetcher) {
3316
- if (isFetcher === void 0) {
3317
- isFetcher = false;
3383
+ if (fromRouteId != null && relative !== "path") {
3384
+ // Grab matches up to the calling route so our route-relative logic is
3385
+ // relative to the correct source route. When using relative:path,
3386
+ // fromRouteId is ignored since that is always relative to the current
3387
+ // location path
3388
+ contextualMatches = [];
3389
+
3390
+ for (let match of matches) {
3391
+ contextualMatches.push(match);
3392
+
3393
+ if (match.route.id === fromRouteId) {
3394
+ activeRouteMatch = match;
3395
+ break;
3396
+ }
3397
+ }
3398
+ } else {
3399
+ contextualMatches = matches;
3400
+ activeRouteMatch = matches[matches.length - 1];
3401
+ } // Resolve the relative path
3402
+
3403
+
3404
+ let path = resolveTo(to ? to : ".", getPathContributingMatches(contextualMatches).map(m => m.pathnameBase), location.pathname, relative === "path"); // When `to` is not specified we inherit search/hash from the current
3405
+ // location, unlike when to="." and we just inherit the path.
3406
+ // See https://github.com/remix-run/remix/issues/927
3407
+
3408
+ if (to == null) {
3409
+ path.search = location.search;
3410
+ path.hash = location.hash;
3411
+ } // Add an ?index param for matched index routes if we don't already have one
3412
+
3413
+
3414
+ if ((to == null || to === "" || to === ".") && activeRouteMatch && activeRouteMatch.route.index && !hasNakedIndexQuery(path.search)) {
3415
+ path.search = path.search ? path.search.replace(/^\?/, "?index&") : "?index";
3416
+ } // If we're operating within a basename, prepend it to the pathname. If
3417
+ // this is a root navigation, then just use the raw basename which allows
3418
+ // the basename to have full control over the presence of a trailing slash
3419
+ // on root actions
3420
+
3421
+
3422
+ if (prependBasename && basename !== "/") {
3423
+ path.pathname = path.pathname === "/" ? basename : joinPaths([basename, path.pathname]);
3318
3424
  }
3319
3425
 
3320
- let path = typeof to === "string" ? to : createPath(to); // Return location verbatim on non-submission navigations
3426
+ return createPath(path);
3427
+ } // Normalize navigation options by converting formMethod=GET formData objects to
3428
+ // URLSearchParams so they behave identically to links with query params
3429
+
3321
3430
 
3431
+ function normalizeNavigateOptions(normalizeFormMethod, isFetcher, path, opts) {
3432
+ // Return location verbatim on non-submission navigations
3322
3433
  if (!opts || !isSubmissionNavigation(opts)) {
3323
3434
  return {
3324
3435
  path
@@ -3340,7 +3451,7 @@ function normalizeNavigateOptions(to, future, opts, isFetcher) {
3340
3451
  if (opts.formData) {
3341
3452
  let formMethod = opts.formMethod || "get";
3342
3453
  submission = {
3343
- formMethod: future.v7_normalizeFormMethod ? formMethod.toUpperCase() : formMethod.toLowerCase(),
3454
+ formMethod: normalizeFormMethod ? formMethod.toUpperCase() : formMethod.toLowerCase(),
3344
3455
  formAction: stripHashFromPath(path),
3345
3456
  formEncType: opts && opts.formEncType || "application/x-www-form-urlencoded",
3346
3457
  formData: opts.formData
@@ -3356,9 +3467,9 @@ function normalizeNavigateOptions(to, future, opts, isFetcher) {
3356
3467
 
3357
3468
 
3358
3469
  let parsedPath = parsePath(path);
3359
- let searchParams = convertFormDataToSearchParams(opts.formData); // Since fetcher GET submissions only run a single loader (as opposed to
3360
- // navigation GET submissions which run all loaders), we need to preserve
3361
- // any incoming ?index params
3470
+ let searchParams = convertFormDataToSearchParams(opts.formData); // On GET navigation submissions we can drop the ?index param from the
3471
+ // resulting location since all loaders will run. But fetcher GET submissions
3472
+ // only run a single loader so we need to preserve any incoming ?index params
3362
3473
 
3363
3474
  if (isFetcher && parsedPath.search && hasNakedIndexQuery(parsedPath.search)) {
3364
3475
  searchParams.append("index", "");
@@ -3390,11 +3501,7 @@ function getLoaderMatchesUntilBoundary(matches, boundaryId) {
3390
3501
  function getMatchesToLoad(history, state, matches, submission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, fetchLoadMatches, routesToUse, basename, pendingActionData, pendingError) {
3391
3502
  let actionResult = pendingError ? Object.values(pendingError)[0] : pendingActionData ? Object.values(pendingActionData)[0] : undefined;
3392
3503
  let currentUrl = history.createURL(state.location);
3393
- let nextUrl = history.createURL(location);
3394
- let defaultShouldRevalidate = // Forced revalidation due to submission, useRevalidate, or X-Remix-Revalidate
3395
- isRevalidationRequired || // Clicked the same link, resubmitted a GET form
3396
- currentUrl.toString() === nextUrl.toString() || // Search params affect all loaders
3397
- currentUrl.search !== nextUrl.search; // Pick navigation matches that are net-new or qualify for revalidation
3504
+ let nextUrl = history.createURL(location); // Pick navigation matches that are net-new or qualify for revalidation
3398
3505
 
3399
3506
  let boundaryId = pendingError ? Object.keys(pendingError)[0] : undefined;
3400
3507
  let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId);
@@ -3426,7 +3533,10 @@ function getMatchesToLoad(history, state, matches, submission, location, isReval
3426
3533
  nextParams: nextRouteMatch.params
3427
3534
  }, submission, {
3428
3535
  actionResult,
3429
- defaultShouldRevalidate: defaultShouldRevalidate || isNewRouteInstance(currentRouteMatch, nextRouteMatch)
3536
+ defaultShouldRevalidate: // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate
3537
+ isRevalidationRequired || // Clicked the same link, resubmitted a GET form
3538
+ currentUrl.toString() === nextUrl.toString() || // Search params affect all loaders
3539
+ currentUrl.search !== nextUrl.search || isNewRouteInstance(currentRouteMatch, nextRouteMatch)
3430
3540
  }));
3431
3541
  }); // Pick fetcher.loads that need to be revalidated
3432
3542
 
@@ -3441,23 +3551,28 @@ function getMatchesToLoad(history, state, matches, submission, location, isReval
3441
3551
  // we can trigger a 404 in callLoadersAndMaybeResolveData
3442
3552
 
3443
3553
  if (!fetcherMatches) {
3444
- revalidatingFetchers.push(_extends({
3445
- key
3446
- }, f, {
3554
+ revalidatingFetchers.push({
3555
+ key,
3556
+ routeId: f.routeId,
3557
+ path: f.path,
3447
3558
  matches: null,
3448
- match: null
3449
- }));
3559
+ match: null,
3560
+ controller: null
3561
+ });
3450
3562
  return;
3451
3563
  }
3452
3564
 
3453
3565
  let fetcherMatch = getTargetMatch(fetcherMatches, f.path);
3454
3566
 
3455
3567
  if (cancelledFetcherLoads.includes(key)) {
3456
- revalidatingFetchers.push(_extends({
3568
+ revalidatingFetchers.push({
3457
3569
  key,
3570
+ routeId: f.routeId,
3571
+ path: f.path,
3458
3572
  matches: fetcherMatches,
3459
- match: fetcherMatch
3460
- }, f));
3573
+ match: fetcherMatch,
3574
+ controller: new AbortController()
3575
+ });
3461
3576
  return;
3462
3577
  } // Revalidating fetchers are decoupled from the route matches since they
3463
3578
  // hit a static href, so they _always_ check shouldRevalidate and the
@@ -3472,15 +3587,19 @@ function getMatchesToLoad(history, state, matches, submission, location, isReval
3472
3587
  nextParams: matches[matches.length - 1].params
3473
3588
  }, submission, {
3474
3589
  actionResult,
3475
- defaultShouldRevalidate
3590
+ // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate
3591
+ defaultShouldRevalidate: isRevalidationRequired
3476
3592
  }));
3477
3593
 
3478
3594
  if (shouldRevalidate) {
3479
- revalidatingFetchers.push(_extends({
3595
+ revalidatingFetchers.push({
3480
3596
  key,
3597
+ routeId: f.routeId,
3598
+ path: f.path,
3481
3599
  matches: fetcherMatches,
3482
- match: fetcherMatch
3483
- }, f));
3600
+ match: fetcherMatch,
3601
+ controller: new AbortController()
3602
+ });
3484
3603
  }
3485
3604
  });
3486
3605
  return [navigationMatches, revalidatingFetchers];
@@ -3524,7 +3643,7 @@ function shouldRevalidateLoader(loaderMatch, arg) {
3524
3643
  */
3525
3644
 
3526
3645
 
3527
- async function loadLazyRouteModule(route, detectErrorBoundary, manifest) {
3646
+ async function loadLazyRouteModule(route, mapRouteProperties, manifest) {
3528
3647
  if (!route.lazy) {
3529
3648
  return;
3530
3649
  }
@@ -3560,27 +3679,19 @@ async function loadLazyRouteModule(route, detectErrorBoundary, manifest) {
3560
3679
  routeUpdates[lazyRouteProperty] = lazyRoute[lazyRouteProperty];
3561
3680
  }
3562
3681
  } // Mutate the route with the provided updates. Do this first so we pass
3563
- // the updated version to detectErrorBoundary
3682
+ // the updated version to mapRouteProperties
3564
3683
 
3565
3684
 
3566
3685
  Object.assign(routeToUpdate, routeUpdates); // Mutate the `hasErrorBoundary` property on the route based on the route
3567
3686
  // updates and remove the `lazy` function so we don't resolve the lazy
3568
3687
  // route again.
3569
3688
 
3570
- Object.assign(routeToUpdate, {
3571
- // To keep things framework agnostic, we use the provided
3572
- // `detectErrorBoundary` function to set the `hasErrorBoundary` route
3573
- // property since the logic will differ between frameworks.
3574
- hasErrorBoundary: detectErrorBoundary(_extends({}, routeToUpdate)),
3689
+ Object.assign(routeToUpdate, _extends({}, mapRouteProperties(routeToUpdate), {
3575
3690
  lazy: undefined
3576
- });
3691
+ }));
3577
3692
  }
3578
3693
 
3579
- async function callLoaderOrAction(type, request, match, matches, manifest, detectErrorBoundary, basename, isStaticRequest, isRouteRequest, requestContext) {
3580
- if (basename === void 0) {
3581
- basename = "/";
3582
- }
3583
-
3694
+ async function callLoaderOrAction(type, request, match, matches, manifest, mapRouteProperties, basename, isStaticRequest, isRouteRequest, requestContext) {
3584
3695
  if (isStaticRequest === void 0) {
3585
3696
  isStaticRequest = false;
3586
3697
  }
@@ -3614,11 +3725,11 @@ async function callLoaderOrAction(type, request, match, matches, manifest, detec
3614
3725
  if (match.route.lazy) {
3615
3726
  if (handler) {
3616
3727
  // Run statically defined handler in parallel with lazy()
3617
- let values = await Promise.all([runHandler(handler), loadLazyRouteModule(match.route, detectErrorBoundary, manifest)]);
3728
+ let values = await Promise.all([runHandler(handler), loadLazyRouteModule(match.route, mapRouteProperties, manifest)]);
3618
3729
  result = values[0];
3619
3730
  } else {
3620
3731
  // Load lazy route module, then run any returned handler
3621
- await loadLazyRouteModule(match.route, detectErrorBoundary, manifest);
3732
+ await loadLazyRouteModule(match.route, mapRouteProperties, manifest);
3622
3733
  handler = match.route[type];
3623
3734
 
3624
3735
  if (handler) {
@@ -3627,9 +3738,11 @@ async function callLoaderOrAction(type, request, match, matches, manifest, detec
3627
3738
  // previously-lazy-loaded routes
3628
3739
  result = await runHandler(handler);
3629
3740
  } else if (type === "action") {
3741
+ let url = new URL(request.url);
3742
+ let pathname = url.pathname + url.search;
3630
3743
  throw getInternalRouterError(405, {
3631
3744
  method: request.method,
3632
- pathname: new URL(request.url).pathname,
3745
+ pathname,
3633
3746
  routeId: match.route.id
3634
3747
  });
3635
3748
  } else {
@@ -3641,8 +3754,13 @@ async function callLoaderOrAction(type, request, match, matches, manifest, detec
3641
3754
  };
3642
3755
  }
3643
3756
  }
3757
+ } else if (!handler) {
3758
+ let url = new URL(request.url);
3759
+ let pathname = url.pathname + url.search;
3760
+ throw getInternalRouterError(404, {
3761
+ pathname
3762
+ });
3644
3763
  } else {
3645
- invariant(handler, "Could not find the " + type + " to run on the \"" + match.route.id + "\" route");
3646
3764
  result = await runHandler(handler);
3647
3765
  }
3648
3766
 
@@ -3664,17 +3782,7 @@ async function callLoaderOrAction(type, request, match, matches, manifest, detec
3664
3782
  invariant(location, "Redirects returned/thrown from loaders/actions must have a Location header"); // Support relative routing in internal redirects
3665
3783
 
3666
3784
  if (!ABSOLUTE_URL_REGEX.test(location)) {
3667
- let activeMatches = matches.slice(0, matches.indexOf(match) + 1);
3668
- let routePathnames = getPathContributingMatches(activeMatches).map(match => match.pathnameBase);
3669
- let resolvedLocation = resolveTo(location, routePathnames, new URL(request.url).pathname);
3670
- invariant(createPath(resolvedLocation), "Unable to resolve redirect location: " + location); // Prepend the basename to the redirect location if we have one
3671
-
3672
- if (basename) {
3673
- let path = resolvedLocation.pathname;
3674
- resolvedLocation.pathname = path === "/" ? basename : joinPaths([basename, path]);
3675
- }
3676
-
3677
- location = createPath(resolvedLocation);
3785
+ location = normalizeTo(new URL(request.url), matches.slice(0, matches.indexOf(match) + 1), basename, true, location);
3678
3786
  } else if (!isStaticRequest) {
3679
3787
  // Strip off the protocol+origin for same-origin + same-basename absolute
3680
3788
  // redirects. If this is a static request, we can let it go back to the
@@ -3890,12 +3998,16 @@ function processLoaderData(state, matches, matchesToLoad, results, pendingError,
3890
3998
  for (let index = 0; index < revalidatingFetchers.length; index++) {
3891
3999
  let {
3892
4000
  key,
3893
- match
4001
+ match,
4002
+ controller
3894
4003
  } = revalidatingFetchers[index];
3895
4004
  invariant(fetcherResults !== undefined && fetcherResults[index] !== undefined, "Did not find corresponding fetcher result");
3896
4005
  let result = fetcherResults[index]; // Process fetcher non-redirect errors
3897
4006
 
3898
- if (isErrorResult(result)) {
4007
+ if (controller && controller.signal.aborted) {
4008
+ // Nothing to do for aborted fetchers
4009
+ continue;
4010
+ } else if (isErrorResult(result)) {
3899
4011
  let boundaryMatch = findNearestBoundary(state.matches, match == null ? void 0 : match.route.id);
3900
4012
 
3901
4013
  if (!(errors && errors[boundaryMatch.route.id])) {
@@ -4084,7 +4196,7 @@ function isMutationMethod(method) {
4084
4196
  return validMutationMethods.has(method.toLowerCase());
4085
4197
  }
4086
4198
 
4087
- async function resolveDeferredResults(currentMatches, matchesToLoad, results, signal, isFetcher, currentLoaderData) {
4199
+ async function resolveDeferredResults(currentMatches, matchesToLoad, results, signals, isFetcher, currentLoaderData) {
4088
4200
  for (let index = 0; index < results.length; index++) {
4089
4201
  let result = results[index];
4090
4202
  let match = matchesToLoad[index]; // If we don't have a match, then we can have a deferred result to do
@@ -4102,6 +4214,8 @@ async function resolveDeferredResults(currentMatches, matchesToLoad, results, si
4102
4214
  // Note: we do not have to touch activeDeferreds here since we race them
4103
4215
  // against the signal in resolveDeferredData and they'll get aborted
4104
4216
  // there if needed
4217
+ let signal = signals[index];
4218
+ invariant(signal, "Expected an AbortSignal for revalidating fetcher deferred result");
4105
4219
  await resolveDeferredData(result, signal, isFetcher).then(result => {
4106
4220
  if (result) {
4107
4221
  results[index] = result || results[index];