@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.
@@ -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
  *
@@ -576,7 +576,7 @@ function isIndexRoute(route) {
576
576
  // solely with AgnosticDataRouteObject's within the Router
577
577
 
578
578
 
579
- function convertRoutesToDataRoutes(routes, detectErrorBoundary, parentPath, manifest) {
579
+ function convertRoutesToDataRoutes(routes, mapRouteProperties, parentPath, manifest) {
580
580
  if (parentPath === void 0) {
581
581
  parentPath = [];
582
582
  }
@@ -592,24 +592,22 @@ function convertRoutesToDataRoutes(routes, detectErrorBoundary, parentPath, mani
592
592
  invariant(!manifest[id], "Found a route id collision on id \"" + id + "\". Route " + "id's must be globally unique within Data Router usages");
593
593
 
594
594
  if (isIndexRoute(route)) {
595
- let indexRoute = _extends({}, route, {
596
- hasErrorBoundary: detectErrorBoundary(route),
595
+ let indexRoute = _extends({}, route, mapRouteProperties(route), {
597
596
  id
598
597
  });
599
598
 
600
599
  manifest[id] = indexRoute;
601
600
  return indexRoute;
602
601
  } else {
603
- let pathOrLayoutRoute = _extends({}, route, {
602
+ let pathOrLayoutRoute = _extends({}, route, mapRouteProperties(route), {
604
603
  id,
605
- hasErrorBoundary: detectErrorBoundary(route),
606
604
  children: undefined
607
605
  });
608
606
 
609
607
  manifest[id] = pathOrLayoutRoute;
610
608
 
611
609
  if (route.children) {
612
- pathOrLayoutRoute.children = convertRoutesToDataRoutes(route.children, detectErrorBoundary, treePath, manifest);
610
+ pathOrLayoutRoute.children = convertRoutesToDataRoutes(route.children, mapRouteProperties, treePath, manifest);
613
611
  }
614
612
 
615
613
  return pathOrLayoutRoute;
@@ -1471,7 +1469,9 @@ const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
1471
1469
  const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined";
1472
1470
  const isServer = !isBrowser;
1473
1471
 
1474
- const defaultDetectErrorBoundary = route => Boolean(route.hasErrorBoundary); //#endregion
1472
+ const defaultMapRouteProperties = route => ({
1473
+ hasErrorBoundary: Boolean(route.hasErrorBoundary)
1474
+ }); //#endregion
1475
1475
  ////////////////////////////////////////////////////////////////////////////////
1476
1476
  //#region createRouter
1477
1477
  ////////////////////////////////////////////////////////////////////////////////
@@ -1483,15 +1483,31 @@ const defaultDetectErrorBoundary = route => Boolean(route.hasErrorBoundary); //#
1483
1483
 
1484
1484
  function createRouter(init) {
1485
1485
  invariant(init.routes.length > 0, "You must provide a non-empty routes array to createRouter");
1486
- let detectErrorBoundary = init.detectErrorBoundary || defaultDetectErrorBoundary; // Routes keyed by ID
1486
+ let mapRouteProperties;
1487
+
1488
+ if (init.mapRouteProperties) {
1489
+ mapRouteProperties = init.mapRouteProperties;
1490
+ } else if (init.detectErrorBoundary) {
1491
+ // If they are still using the deprecated version, wrap it with the new API
1492
+ let detectErrorBoundary = init.detectErrorBoundary;
1493
+
1494
+ mapRouteProperties = route => ({
1495
+ hasErrorBoundary: detectErrorBoundary(route)
1496
+ });
1497
+ } else {
1498
+ mapRouteProperties = defaultMapRouteProperties;
1499
+ } // Routes keyed by ID
1500
+
1487
1501
 
1488
1502
  let manifest = {}; // Routes in tree format for matching
1489
1503
 
1490
- let dataRoutes = convertRoutesToDataRoutes(init.routes, detectErrorBoundary, undefined, manifest);
1491
- let inFlightDataRoutes; // Config driven behavior flags
1504
+ let dataRoutes = convertRoutesToDataRoutes(init.routes, mapRouteProperties, undefined, manifest);
1505
+ let inFlightDataRoutes;
1506
+ let basename = init.basename || "/"; // Config driven behavior flags
1492
1507
 
1493
1508
  let future = _extends({
1494
- v7_normalizeFormMethod: false
1509
+ v7_normalizeFormMethod: false,
1510
+ v7_prependBasename: false
1495
1511
  }, init.future); // Cleanup function for history
1496
1512
 
1497
1513
 
@@ -1511,7 +1527,7 @@ function createRouter(init) {
1511
1527
  // SSR did the initial scroll restoration.
1512
1528
 
1513
1529
  let initialScrollRestored = init.hydrationData != null;
1514
- let initialMatches = matchRoutes(dataRoutes, init.history.location, init.basename);
1530
+ let initialMatches = matchRoutes(dataRoutes, init.history.location, basename);
1515
1531
  let initialErrors = null;
1516
1532
 
1517
1533
  if (initialMatches == null) {
@@ -1563,7 +1579,7 @@ function createRouter(init) {
1563
1579
 
1564
1580
  let isUninterruptedRevalidation = false; // Use this internal flag to force revalidation of all loaders:
1565
1581
  // - submissions (completed or interrupted)
1566
- // - useRevalidate()
1582
+ // - useRevalidator()
1567
1583
  // - X-Remix-Revalidate (from redirect)
1568
1584
 
1569
1585
  let isRevalidationRequired = false; // Use this internal array to capture routes that require revalidation due
@@ -1582,7 +1598,7 @@ function createRouter(init) {
1582
1598
 
1583
1599
  let pendingNavigationLoadId = -1; // Fetchers that triggered data reloads as a result of their actions
1584
1600
 
1585
- let fetchReloadIds = new Map(); // Fetchers that triggered redirect navigations from their actions
1601
+ let fetchReloadIds = new Map(); // Fetchers that triggered redirect navigations
1586
1602
 
1587
1603
  let fetchRedirectIds = new Set(); // Most recent href/match for fetcher.load calls for fetchers
1588
1604
 
@@ -1779,11 +1795,12 @@ function createRouter(init) {
1779
1795
  return;
1780
1796
  }
1781
1797
 
1798
+ let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, to, opts == null ? void 0 : opts.fromRouteId, opts == null ? void 0 : opts.relative);
1782
1799
  let {
1783
1800
  path,
1784
1801
  submission,
1785
1802
  error
1786
- } = normalizeNavigateOptions(to, future, opts);
1803
+ } = normalizeNavigateOptions(future.v7_normalizeFormMethod, false, normalizedPath, opts);
1787
1804
  let currentLocation = state.location;
1788
1805
  let nextLocation = createLocation(state.location, path, opts && opts.state); // When using navigate as a PUSH/REPLACE we aren't reading an already-encoded
1789
1806
  // URL from window.location, so we need to encode it here so the behavior
@@ -1899,7 +1916,7 @@ function createRouter(init) {
1899
1916
  pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
1900
1917
  let routesToUse = inFlightDataRoutes || dataRoutes;
1901
1918
  let loadingNavigation = opts && opts.overrideNavigation;
1902
- let matches = matchRoutes(routesToUse, location, init.basename); // Short circuit with a 404 on the root error boundary if we match nothing
1919
+ let matches = matchRoutes(routesToUse, location, basename); // Short circuit with a 404 on the root error boundary if we match nothing
1903
1920
 
1904
1921
  if (!matches) {
1905
1922
  let error = getInternalRouterError(404, {
@@ -2022,7 +2039,7 @@ function createRouter(init) {
2022
2039
  })
2023
2040
  };
2024
2041
  } else {
2025
- result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, detectErrorBoundary, router.basename);
2042
+ result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename);
2026
2043
 
2027
2044
  if (request.signal.aborted) {
2028
2045
  return {
@@ -2114,13 +2131,14 @@ function createRouter(init) {
2114
2131
  formEncType: loadingNavigation.formEncType
2115
2132
  } : undefined;
2116
2133
  let routesToUse = inFlightDataRoutes || dataRoutes;
2117
- 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
2134
+ 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
2118
2135
  // about to reload. Note that if this is an action reload we would have
2119
2136
  // already cancelled all pending deferreds so this would be a no-op
2120
2137
 
2121
2138
  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
2122
2139
 
2123
2140
  if (matchesToLoad.length === 0 && revalidatingFetchers.length === 0) {
2141
+ let updatedFetchers = markFetchRedirectsDone();
2124
2142
  completeNavigation(location, _extends({
2125
2143
  matches,
2126
2144
  loaderData: {},
@@ -2128,6 +2146,8 @@ function createRouter(init) {
2128
2146
  errors: pendingError || null
2129
2147
  }, pendingActionData ? {
2130
2148
  actionData: pendingActionData
2149
+ } : {}, updatedFetchers ? {
2150
+ fetchers: new Map(state.fetchers)
2131
2151
  } : {}));
2132
2152
  return {
2133
2153
  shortCircuited: true
@@ -2165,7 +2185,21 @@ function createRouter(init) {
2165
2185
  }
2166
2186
 
2167
2187
  pendingNavigationLoadId = ++incrementingLoadId;
2168
- revalidatingFetchers.forEach(rf => fetchControllers.set(rf.key, pendingNavigationController));
2188
+ revalidatingFetchers.forEach(rf => {
2189
+ if (rf.controller) {
2190
+ // Fetchers use an independent AbortController so that aborting a fetcher
2191
+ // (via deleteFetcher) does not abort the triggering navigation that
2192
+ // triggered the revalidation
2193
+ fetchControllers.set(rf.key, rf.controller);
2194
+ }
2195
+ }); // Proxy navigation abort through to revalidation fetchers
2196
+
2197
+ let abortPendingFetchRevalidations = () => revalidatingFetchers.forEach(f => abortFetcher(f.key));
2198
+
2199
+ if (pendingNavigationController) {
2200
+ pendingNavigationController.signal.addEventListener("abort", abortPendingFetchRevalidations);
2201
+ }
2202
+
2169
2203
  let {
2170
2204
  results,
2171
2205
  loaderResults,
@@ -2181,6 +2215,10 @@ function createRouter(init) {
2181
2215
  // reassigned to new controllers for the next navigation
2182
2216
 
2183
2217
 
2218
+ if (pendingNavigationController) {
2219
+ pendingNavigationController.signal.removeEventListener("abort", abortPendingFetchRevalidations);
2220
+ }
2221
+
2184
2222
  revalidatingFetchers.forEach(rf => fetchControllers.delete(rf.key)); // If any loaders returned a redirect Response, start a new REPLACE navigation
2185
2223
 
2186
2224
  let redirect = findRedirect(results);
@@ -2210,12 +2248,13 @@ function createRouter(init) {
2210
2248
  }
2211
2249
  });
2212
2250
  });
2213
- markFetchRedirectsDone();
2251
+ let updatedFetchers = markFetchRedirectsDone();
2214
2252
  let didAbortFetchLoads = abortStaleFetchLoads(pendingNavigationLoadId);
2253
+ let shouldUpdateFetchers = updatedFetchers || didAbortFetchLoads || revalidatingFetchers.length > 0;
2215
2254
  return _extends({
2216
2255
  loaderData,
2217
2256
  errors
2218
- }, didAbortFetchLoads || revalidatingFetchers.length > 0 ? {
2257
+ }, shouldUpdateFetchers ? {
2219
2258
  fetchers: new Map(state.fetchers)
2220
2259
  } : {});
2221
2260
  }
@@ -2232,11 +2271,12 @@ function createRouter(init) {
2232
2271
 
2233
2272
  if (fetchControllers.has(key)) abortFetcher(key);
2234
2273
  let routesToUse = inFlightDataRoutes || dataRoutes;
2235
- let matches = matchRoutes(routesToUse, href, init.basename);
2274
+ let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, href, routeId, opts == null ? void 0 : opts.relative);
2275
+ let matches = matchRoutes(routesToUse, normalizedPath, basename);
2236
2276
 
2237
2277
  if (!matches) {
2238
2278
  setFetcherError(key, routeId, getInternalRouterError(404, {
2239
- pathname: href
2279
+ pathname: normalizedPath
2240
2280
  }));
2241
2281
  return;
2242
2282
  }
@@ -2244,7 +2284,7 @@ function createRouter(init) {
2244
2284
  let {
2245
2285
  path,
2246
2286
  submission
2247
- } = normalizeNavigateOptions(href, future, opts, true);
2287
+ } = normalizeNavigateOptions(future.v7_normalizeFormMethod, true, normalizedPath, opts);
2248
2288
  let match = getTargetMatch(matches, path);
2249
2289
  pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
2250
2290
 
@@ -2296,7 +2336,7 @@ function createRouter(init) {
2296
2336
  let abortController = new AbortController();
2297
2337
  let fetchRequest = createClientSideRequest(init.history, path, abortController.signal, submission);
2298
2338
  fetchControllers.set(key, abortController);
2299
- let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, manifest, detectErrorBoundary, router.basename);
2339
+ let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, manifest, mapRouteProperties, basename);
2300
2340
 
2301
2341
  if (fetchRequest.signal.aborted) {
2302
2342
  // We can delete this so long as we weren't aborted by ou our own fetcher
@@ -2346,7 +2386,7 @@ function createRouter(init) {
2346
2386
  let nextLocation = state.navigation.location || state.location;
2347
2387
  let revalidationRequest = createClientSideRequest(init.history, nextLocation, abortController.signal);
2348
2388
  let routesToUse = inFlightDataRoutes || dataRoutes;
2349
- let matches = state.navigation.state !== "idle" ? matchRoutes(routesToUse, state.navigation.location, init.basename) : state.matches;
2389
+ let matches = state.navigation.state !== "idle" ? matchRoutes(routesToUse, state.navigation.location, basename) : state.matches;
2350
2390
  invariant(matches, "Didn't find any matches after fetcher action");
2351
2391
  let loadId = ++incrementingLoadId;
2352
2392
  fetchReloadIds.set(key, loadId);
@@ -2359,7 +2399,7 @@ function createRouter(init) {
2359
2399
  });
2360
2400
 
2361
2401
  state.fetchers.set(key, loadFetcher);
2362
- let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, submission, nextLocation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, fetchLoadMatches, routesToUse, init.basename, {
2402
+ let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, submission, nextLocation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, fetchLoadMatches, routesToUse, basename, {
2363
2403
  [match.route.id]: actionResult.data
2364
2404
  }, undefined // No need to send through errors since we short circuit above
2365
2405
  ); // Put all revalidating fetchers into the loading state, except for the
@@ -2379,11 +2419,18 @@ function createRouter(init) {
2379
2419
  " _hasFetcherDoneAnything ": true
2380
2420
  };
2381
2421
  state.fetchers.set(staleKey, revalidatingFetcher);
2382
- fetchControllers.set(staleKey, abortController);
2422
+
2423
+ if (rf.controller) {
2424
+ fetchControllers.set(staleKey, rf.controller);
2425
+ }
2383
2426
  });
2384
2427
  updateState({
2385
2428
  fetchers: new Map(state.fetchers)
2386
2429
  });
2430
+
2431
+ let abortPendingFetchRevalidations = () => revalidatingFetchers.forEach(rf => abortFetcher(rf.key));
2432
+
2433
+ abortController.signal.addEventListener("abort", abortPendingFetchRevalidations);
2387
2434
  let {
2388
2435
  results,
2389
2436
  loaderResults,
@@ -2394,6 +2441,7 @@ function createRouter(init) {
2394
2441
  return;
2395
2442
  }
2396
2443
 
2444
+ abortController.signal.removeEventListener("abort", abortPendingFetchRevalidations);
2397
2445
  fetchReloadIds.delete(key);
2398
2446
  fetchControllers.delete(key);
2399
2447
  revalidatingFetchers.forEach(r => fetchControllers.delete(r.key));
@@ -2468,14 +2516,14 @@ function createRouter(init) {
2468
2516
  let abortController = new AbortController();
2469
2517
  let fetchRequest = createClientSideRequest(init.history, path, abortController.signal);
2470
2518
  fetchControllers.set(key, abortController);
2471
- let result = await callLoaderOrAction("loader", fetchRequest, match, matches, manifest, detectErrorBoundary, router.basename); // Deferred isn't supported for fetcher loads, await everything and treat it
2519
+ let result = await callLoaderOrAction("loader", fetchRequest, match, matches, manifest, mapRouteProperties, basename); // Deferred isn't supported for fetcher loads, await everything and treat it
2472
2520
  // as a normal load. resolveDeferredData will return undefined if this
2473
2521
  // fetcher gets aborted, so we just leave result untouched and short circuit
2474
2522
  // below if that happens
2475
2523
 
2476
2524
  if (isDeferredResult(result)) {
2477
2525
  result = (await resolveDeferredData(result, fetchRequest.signal, true)) || result;
2478
- } // We can delete this so long as we weren't aborted by ou our own fetcher
2526
+ } // We can delete this so long as we weren't aborted by our our own fetcher
2479
2527
  // re-load which would have put _new_ controller is in fetchControllers
2480
2528
 
2481
2529
 
@@ -2489,6 +2537,7 @@ function createRouter(init) {
2489
2537
 
2490
2538
 
2491
2539
  if (isRedirectResult(result)) {
2540
+ fetchRedirectIds.add(key);
2492
2541
  await startRedirectNavigation(state, result);
2493
2542
  return;
2494
2543
  } // Process any non-redirect errors thrown
@@ -2569,7 +2618,7 @@ function createRouter(init) {
2569
2618
 
2570
2619
  if (ABSOLUTE_URL_REGEX.test(redirect.location) && isBrowser && typeof ((_window = window) == null ? void 0 : _window.location) !== "undefined") {
2571
2620
  let url = init.history.createURL(redirect.location);
2572
- let isDifferentBasename = stripBasename(url.pathname, init.basename || "/") == null;
2621
+ let isDifferentBasename = stripBasename(url.pathname, basename) == null;
2573
2622
 
2574
2623
  if (window.location.origin !== url.origin || isDifferentBasename) {
2575
2624
  if (replace) {
@@ -2653,9 +2702,9 @@ function createRouter(init) {
2653
2702
  // Call all navigation loaders and revalidating fetcher loaders in parallel,
2654
2703
  // then slice off the results into separate arrays so we can handle them
2655
2704
  // accordingly
2656
- let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, detectErrorBoundary, router.basename)), ...fetchersToLoad.map(f => {
2657
- if (f.matches && f.match) {
2658
- return callLoaderOrAction("loader", createClientSideRequest(init.history, f.path, request.signal), f.match, f.matches, manifest, detectErrorBoundary, router.basename);
2705
+ let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename)), ...fetchersToLoad.map(f => {
2706
+ if (f.matches && f.match && f.controller) {
2707
+ return callLoaderOrAction("loader", createClientSideRequest(init.history, f.path, f.controller.signal), f.match, f.matches, manifest, mapRouteProperties, basename);
2659
2708
  } else {
2660
2709
  let error = {
2661
2710
  type: ResultType.error,
@@ -2668,7 +2717,7 @@ function createRouter(init) {
2668
2717
  })]);
2669
2718
  let loaderResults = results.slice(0, matchesToLoad.length);
2670
2719
  let fetcherResults = results.slice(matchesToLoad.length);
2671
- await Promise.all([resolveDeferredResults(currentMatches, matchesToLoad, loaderResults, request.signal, false, state.loaderData), resolveDeferredResults(currentMatches, fetchersToLoad.map(f => f.match), fetcherResults, request.signal, true)]);
2720
+ 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)]);
2672
2721
  return {
2673
2722
  results,
2674
2723
  loaderResults,
@@ -2735,6 +2784,7 @@ function createRouter(init) {
2735
2784
 
2736
2785
  function markFetchRedirectsDone() {
2737
2786
  let doneKeys = [];
2787
+ let updatedFetchers = false;
2738
2788
 
2739
2789
  for (let key of fetchRedirectIds) {
2740
2790
  let fetcher = state.fetchers.get(key);
@@ -2743,10 +2793,12 @@ function createRouter(init) {
2743
2793
  if (fetcher.state === "loading") {
2744
2794
  fetchRedirectIds.delete(key);
2745
2795
  doneKeys.push(key);
2796
+ updatedFetchers = true;
2746
2797
  }
2747
2798
  }
2748
2799
 
2749
2800
  markFetchersDone(doneKeys);
2801
+ return updatedFetchers;
2750
2802
  }
2751
2803
 
2752
2804
  function abortStaleFetchLoads(landedId) {
@@ -2906,7 +2958,7 @@ function createRouter(init) {
2906
2958
 
2907
2959
  router = {
2908
2960
  get basename() {
2909
- return init.basename;
2961
+ return basename;
2910
2962
  },
2911
2963
 
2912
2964
  get state() {
@@ -2948,9 +3000,23 @@ const UNSAFE_DEFERRED_SYMBOL = Symbol("deferred");
2948
3000
  function createStaticHandler(routes, opts) {
2949
3001
  invariant(routes.length > 0, "You must provide a non-empty routes array to createStaticHandler");
2950
3002
  let manifest = {};
2951
- let detectErrorBoundary = (opts == null ? void 0 : opts.detectErrorBoundary) || defaultDetectErrorBoundary;
2952
- let dataRoutes = convertRoutesToDataRoutes(routes, detectErrorBoundary, undefined, manifest);
2953
3003
  let basename = (opts ? opts.basename : null) || "/";
3004
+ let mapRouteProperties;
3005
+
3006
+ if (opts != null && opts.mapRouteProperties) {
3007
+ mapRouteProperties = opts.mapRouteProperties;
3008
+ } else if (opts != null && opts.detectErrorBoundary) {
3009
+ // If they are still using the deprecated version, wrap it with the new API
3010
+ let detectErrorBoundary = opts.detectErrorBoundary;
3011
+
3012
+ mapRouteProperties = route => ({
3013
+ hasErrorBoundary: detectErrorBoundary(route)
3014
+ });
3015
+ } else {
3016
+ mapRouteProperties = defaultMapRouteProperties;
3017
+ }
3018
+
3019
+ let dataRoutes = convertRoutesToDataRoutes(routes, mapRouteProperties, undefined, manifest);
2954
3020
  /**
2955
3021
  * The query() method is intended for document requests, in which we want to
2956
3022
  * call an optional action and potentially multiple loaders for all nested
@@ -3187,7 +3253,7 @@ function createStaticHandler(routes, opts) {
3187
3253
  error
3188
3254
  };
3189
3255
  } else {
3190
- result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, detectErrorBoundary, basename, true, isRouteRequest, requestContext);
3256
+ result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, true, isRouteRequest, requestContext);
3191
3257
 
3192
3258
  if (request.signal.aborted) {
3193
3259
  let method = isRouteRequest ? "queryRoute" : "query";
@@ -3310,7 +3376,7 @@ function createStaticHandler(routes, opts) {
3310
3376
  };
3311
3377
  }
3312
3378
 
3313
- let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, detectErrorBoundary, basename, true, isRouteRequest, requestContext))]);
3379
+ let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename, true, isRouteRequest, requestContext))]);
3314
3380
 
3315
3381
  if (request.signal.aborted) {
3316
3382
  let method = isRouteRequest ? "queryRoute" : "query";
@@ -3361,17 +3427,62 @@ function getStaticContextFromError(routes, context, error) {
3361
3427
 
3362
3428
  function isSubmissionNavigation(opts) {
3363
3429
  return opts != null && "formData" in opts;
3364
- } // Normalize navigation options by converting formMethod=GET formData objects to
3365
- // URLSearchParams so they behave identically to links with query params
3430
+ }
3366
3431
 
3432
+ function normalizeTo(location, matches, basename, prependBasename, to, fromRouteId, relative) {
3433
+ let contextualMatches;
3434
+ let activeRouteMatch;
3367
3435
 
3368
- function normalizeNavigateOptions(to, future, opts, isFetcher) {
3369
- if (isFetcher === void 0) {
3370
- isFetcher = false;
3436
+ if (fromRouteId != null && relative !== "path") {
3437
+ // Grab matches up to the calling route so our route-relative logic is
3438
+ // relative to the correct source route. When using relative:path,
3439
+ // fromRouteId is ignored since that is always relative to the current
3440
+ // location path
3441
+ contextualMatches = [];
3442
+
3443
+ for (let match of matches) {
3444
+ contextualMatches.push(match);
3445
+
3446
+ if (match.route.id === fromRouteId) {
3447
+ activeRouteMatch = match;
3448
+ break;
3449
+ }
3450
+ }
3451
+ } else {
3452
+ contextualMatches = matches;
3453
+ activeRouteMatch = matches[matches.length - 1];
3454
+ } // Resolve the relative path
3455
+
3456
+
3457
+ 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
3458
+ // location, unlike when to="." and we just inherit the path.
3459
+ // See https://github.com/remix-run/remix/issues/927
3460
+
3461
+ if (to == null) {
3462
+ path.search = location.search;
3463
+ path.hash = location.hash;
3464
+ } // Add an ?index param for matched index routes if we don't already have one
3465
+
3466
+
3467
+ if ((to == null || to === "" || to === ".") && activeRouteMatch && activeRouteMatch.route.index && !hasNakedIndexQuery(path.search)) {
3468
+ path.search = path.search ? path.search.replace(/^\?/, "?index&") : "?index";
3469
+ } // If we're operating within a basename, prepend it to the pathname. If
3470
+ // this is a root navigation, then just use the raw basename which allows
3471
+ // the basename to have full control over the presence of a trailing slash
3472
+ // on root actions
3473
+
3474
+
3475
+ if (prependBasename && basename !== "/") {
3476
+ path.pathname = path.pathname === "/" ? basename : joinPaths([basename, path.pathname]);
3371
3477
  }
3372
3478
 
3373
- let path = typeof to === "string" ? to : createPath(to); // Return location verbatim on non-submission navigations
3479
+ return createPath(path);
3480
+ } // Normalize navigation options by converting formMethod=GET formData objects to
3481
+ // URLSearchParams so they behave identically to links with query params
3482
+
3374
3483
 
3484
+ function normalizeNavigateOptions(normalizeFormMethod, isFetcher, path, opts) {
3485
+ // Return location verbatim on non-submission navigations
3375
3486
  if (!opts || !isSubmissionNavigation(opts)) {
3376
3487
  return {
3377
3488
  path
@@ -3393,7 +3504,7 @@ function normalizeNavigateOptions(to, future, opts, isFetcher) {
3393
3504
  if (opts.formData) {
3394
3505
  let formMethod = opts.formMethod || "get";
3395
3506
  submission = {
3396
- formMethod: future.v7_normalizeFormMethod ? formMethod.toUpperCase() : formMethod.toLowerCase(),
3507
+ formMethod: normalizeFormMethod ? formMethod.toUpperCase() : formMethod.toLowerCase(),
3397
3508
  formAction: stripHashFromPath(path),
3398
3509
  formEncType: opts && opts.formEncType || "application/x-www-form-urlencoded",
3399
3510
  formData: opts.formData
@@ -3409,9 +3520,9 @@ function normalizeNavigateOptions(to, future, opts, isFetcher) {
3409
3520
 
3410
3521
 
3411
3522
  let parsedPath = parsePath(path);
3412
- let searchParams = convertFormDataToSearchParams(opts.formData); // Since fetcher GET submissions only run a single loader (as opposed to
3413
- // navigation GET submissions which run all loaders), we need to preserve
3414
- // any incoming ?index params
3523
+ let searchParams = convertFormDataToSearchParams(opts.formData); // On GET navigation submissions we can drop the ?index param from the
3524
+ // resulting location since all loaders will run. But fetcher GET submissions
3525
+ // only run a single loader so we need to preserve any incoming ?index params
3415
3526
 
3416
3527
  if (isFetcher && parsedPath.search && hasNakedIndexQuery(parsedPath.search)) {
3417
3528
  searchParams.append("index", "");
@@ -3443,11 +3554,7 @@ function getLoaderMatchesUntilBoundary(matches, boundaryId) {
3443
3554
  function getMatchesToLoad(history, state, matches, submission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, fetchLoadMatches, routesToUse, basename, pendingActionData, pendingError) {
3444
3555
  let actionResult = pendingError ? Object.values(pendingError)[0] : pendingActionData ? Object.values(pendingActionData)[0] : undefined;
3445
3556
  let currentUrl = history.createURL(state.location);
3446
- let nextUrl = history.createURL(location);
3447
- let defaultShouldRevalidate = // Forced revalidation due to submission, useRevalidate, or X-Remix-Revalidate
3448
- isRevalidationRequired || // Clicked the same link, resubmitted a GET form
3449
- currentUrl.toString() === nextUrl.toString() || // Search params affect all loaders
3450
- currentUrl.search !== nextUrl.search; // Pick navigation matches that are net-new or qualify for revalidation
3557
+ let nextUrl = history.createURL(location); // Pick navigation matches that are net-new or qualify for revalidation
3451
3558
 
3452
3559
  let boundaryId = pendingError ? Object.keys(pendingError)[0] : undefined;
3453
3560
  let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId);
@@ -3479,7 +3586,10 @@ function getMatchesToLoad(history, state, matches, submission, location, isReval
3479
3586
  nextParams: nextRouteMatch.params
3480
3587
  }, submission, {
3481
3588
  actionResult,
3482
- defaultShouldRevalidate: defaultShouldRevalidate || isNewRouteInstance(currentRouteMatch, nextRouteMatch)
3589
+ defaultShouldRevalidate: // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate
3590
+ isRevalidationRequired || // Clicked the same link, resubmitted a GET form
3591
+ currentUrl.toString() === nextUrl.toString() || // Search params affect all loaders
3592
+ currentUrl.search !== nextUrl.search || isNewRouteInstance(currentRouteMatch, nextRouteMatch)
3483
3593
  }));
3484
3594
  }); // Pick fetcher.loads that need to be revalidated
3485
3595
 
@@ -3494,23 +3604,28 @@ function getMatchesToLoad(history, state, matches, submission, location, isReval
3494
3604
  // we can trigger a 404 in callLoadersAndMaybeResolveData
3495
3605
 
3496
3606
  if (!fetcherMatches) {
3497
- revalidatingFetchers.push(_extends({
3498
- key
3499
- }, f, {
3607
+ revalidatingFetchers.push({
3608
+ key,
3609
+ routeId: f.routeId,
3610
+ path: f.path,
3500
3611
  matches: null,
3501
- match: null
3502
- }));
3612
+ match: null,
3613
+ controller: null
3614
+ });
3503
3615
  return;
3504
3616
  }
3505
3617
 
3506
3618
  let fetcherMatch = getTargetMatch(fetcherMatches, f.path);
3507
3619
 
3508
3620
  if (cancelledFetcherLoads.includes(key)) {
3509
- revalidatingFetchers.push(_extends({
3621
+ revalidatingFetchers.push({
3510
3622
  key,
3623
+ routeId: f.routeId,
3624
+ path: f.path,
3511
3625
  matches: fetcherMatches,
3512
- match: fetcherMatch
3513
- }, f));
3626
+ match: fetcherMatch,
3627
+ controller: new AbortController()
3628
+ });
3514
3629
  return;
3515
3630
  } // Revalidating fetchers are decoupled from the route matches since they
3516
3631
  // hit a static href, so they _always_ check shouldRevalidate and the
@@ -3525,15 +3640,19 @@ function getMatchesToLoad(history, state, matches, submission, location, isReval
3525
3640
  nextParams: matches[matches.length - 1].params
3526
3641
  }, submission, {
3527
3642
  actionResult,
3528
- defaultShouldRevalidate
3643
+ // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate
3644
+ defaultShouldRevalidate: isRevalidationRequired
3529
3645
  }));
3530
3646
 
3531
3647
  if (shouldRevalidate) {
3532
- revalidatingFetchers.push(_extends({
3648
+ revalidatingFetchers.push({
3533
3649
  key,
3650
+ routeId: f.routeId,
3651
+ path: f.path,
3534
3652
  matches: fetcherMatches,
3535
- match: fetcherMatch
3536
- }, f));
3653
+ match: fetcherMatch,
3654
+ controller: new AbortController()
3655
+ });
3537
3656
  }
3538
3657
  });
3539
3658
  return [navigationMatches, revalidatingFetchers];
@@ -3577,7 +3696,7 @@ function shouldRevalidateLoader(loaderMatch, arg) {
3577
3696
  */
3578
3697
 
3579
3698
 
3580
- async function loadLazyRouteModule(route, detectErrorBoundary, manifest) {
3699
+ async function loadLazyRouteModule(route, mapRouteProperties, manifest) {
3581
3700
  if (!route.lazy) {
3582
3701
  return;
3583
3702
  }
@@ -3613,27 +3732,19 @@ async function loadLazyRouteModule(route, detectErrorBoundary, manifest) {
3613
3732
  routeUpdates[lazyRouteProperty] = lazyRoute[lazyRouteProperty];
3614
3733
  }
3615
3734
  } // Mutate the route with the provided updates. Do this first so we pass
3616
- // the updated version to detectErrorBoundary
3735
+ // the updated version to mapRouteProperties
3617
3736
 
3618
3737
 
3619
3738
  Object.assign(routeToUpdate, routeUpdates); // Mutate the `hasErrorBoundary` property on the route based on the route
3620
3739
  // updates and remove the `lazy` function so we don't resolve the lazy
3621
3740
  // route again.
3622
3741
 
3623
- Object.assign(routeToUpdate, {
3624
- // To keep things framework agnostic, we use the provided
3625
- // `detectErrorBoundary` function to set the `hasErrorBoundary` route
3626
- // property since the logic will differ between frameworks.
3627
- hasErrorBoundary: detectErrorBoundary(_extends({}, routeToUpdate)),
3742
+ Object.assign(routeToUpdate, _extends({}, mapRouteProperties(routeToUpdate), {
3628
3743
  lazy: undefined
3629
- });
3744
+ }));
3630
3745
  }
3631
3746
 
3632
- async function callLoaderOrAction(type, request, match, matches, manifest, detectErrorBoundary, basename, isStaticRequest, isRouteRequest, requestContext) {
3633
- if (basename === void 0) {
3634
- basename = "/";
3635
- }
3636
-
3747
+ async function callLoaderOrAction(type, request, match, matches, manifest, mapRouteProperties, basename, isStaticRequest, isRouteRequest, requestContext) {
3637
3748
  if (isStaticRequest === void 0) {
3638
3749
  isStaticRequest = false;
3639
3750
  }
@@ -3667,11 +3778,11 @@ async function callLoaderOrAction(type, request, match, matches, manifest, detec
3667
3778
  if (match.route.lazy) {
3668
3779
  if (handler) {
3669
3780
  // Run statically defined handler in parallel with lazy()
3670
- let values = await Promise.all([runHandler(handler), loadLazyRouteModule(match.route, detectErrorBoundary, manifest)]);
3781
+ let values = await Promise.all([runHandler(handler), loadLazyRouteModule(match.route, mapRouteProperties, manifest)]);
3671
3782
  result = values[0];
3672
3783
  } else {
3673
3784
  // Load lazy route module, then run any returned handler
3674
- await loadLazyRouteModule(match.route, detectErrorBoundary, manifest);
3785
+ await loadLazyRouteModule(match.route, mapRouteProperties, manifest);
3675
3786
  handler = match.route[type];
3676
3787
 
3677
3788
  if (handler) {
@@ -3680,9 +3791,11 @@ async function callLoaderOrAction(type, request, match, matches, manifest, detec
3680
3791
  // previously-lazy-loaded routes
3681
3792
  result = await runHandler(handler);
3682
3793
  } else if (type === "action") {
3794
+ let url = new URL(request.url);
3795
+ let pathname = url.pathname + url.search;
3683
3796
  throw getInternalRouterError(405, {
3684
3797
  method: request.method,
3685
- pathname: new URL(request.url).pathname,
3798
+ pathname,
3686
3799
  routeId: match.route.id
3687
3800
  });
3688
3801
  } else {
@@ -3694,8 +3807,13 @@ async function callLoaderOrAction(type, request, match, matches, manifest, detec
3694
3807
  };
3695
3808
  }
3696
3809
  }
3810
+ } else if (!handler) {
3811
+ let url = new URL(request.url);
3812
+ let pathname = url.pathname + url.search;
3813
+ throw getInternalRouterError(404, {
3814
+ pathname
3815
+ });
3697
3816
  } else {
3698
- invariant(handler, "Could not find the " + type + " to run on the \"" + match.route.id + "\" route");
3699
3817
  result = await runHandler(handler);
3700
3818
  }
3701
3819
 
@@ -3717,17 +3835,7 @@ async function callLoaderOrAction(type, request, match, matches, manifest, detec
3717
3835
  invariant(location, "Redirects returned/thrown from loaders/actions must have a Location header"); // Support relative routing in internal redirects
3718
3836
 
3719
3837
  if (!ABSOLUTE_URL_REGEX.test(location)) {
3720
- let activeMatches = matches.slice(0, matches.indexOf(match) + 1);
3721
- let routePathnames = getPathContributingMatches(activeMatches).map(match => match.pathnameBase);
3722
- let resolvedLocation = resolveTo(location, routePathnames, new URL(request.url).pathname);
3723
- invariant(createPath(resolvedLocation), "Unable to resolve redirect location: " + location); // Prepend the basename to the redirect location if we have one
3724
-
3725
- if (basename) {
3726
- let path = resolvedLocation.pathname;
3727
- resolvedLocation.pathname = path === "/" ? basename : joinPaths([basename, path]);
3728
- }
3729
-
3730
- location = createPath(resolvedLocation);
3838
+ location = normalizeTo(new URL(request.url), matches.slice(0, matches.indexOf(match) + 1), basename, true, location);
3731
3839
  } else if (!isStaticRequest) {
3732
3840
  // Strip off the protocol+origin for same-origin + same-basename absolute
3733
3841
  // redirects. If this is a static request, we can let it go back to the
@@ -3943,12 +4051,16 @@ function processLoaderData(state, matches, matchesToLoad, results, pendingError,
3943
4051
  for (let index = 0; index < revalidatingFetchers.length; index++) {
3944
4052
  let {
3945
4053
  key,
3946
- match
4054
+ match,
4055
+ controller
3947
4056
  } = revalidatingFetchers[index];
3948
4057
  invariant(fetcherResults !== undefined && fetcherResults[index] !== undefined, "Did not find corresponding fetcher result");
3949
4058
  let result = fetcherResults[index]; // Process fetcher non-redirect errors
3950
4059
 
3951
- if (isErrorResult(result)) {
4060
+ if (controller && controller.signal.aborted) {
4061
+ // Nothing to do for aborted fetchers
4062
+ continue;
4063
+ } else if (isErrorResult(result)) {
3952
4064
  let boundaryMatch = findNearestBoundary(state.matches, match == null ? void 0 : match.route.id);
3953
4065
 
3954
4066
  if (!(errors && errors[boundaryMatch.route.id])) {
@@ -4137,7 +4249,7 @@ function isMutationMethod(method) {
4137
4249
  return validMutationMethods.has(method.toLowerCase());
4138
4250
  }
4139
4251
 
4140
- async function resolveDeferredResults(currentMatches, matchesToLoad, results, signal, isFetcher, currentLoaderData) {
4252
+ async function resolveDeferredResults(currentMatches, matchesToLoad, results, signals, isFetcher, currentLoaderData) {
4141
4253
  for (let index = 0; index < results.length; index++) {
4142
4254
  let result = results[index];
4143
4255
  let match = matchesToLoad[index]; // If we don't have a match, then we can have a deferred result to do
@@ -4155,6 +4267,8 @@ async function resolveDeferredResults(currentMatches, matchesToLoad, results, si
4155
4267
  // Note: we do not have to touch activeDeferreds here since we race them
4156
4268
  // against the signal in resolveDeferredData and they'll get aborted
4157
4269
  // there if needed
4270
+ let signal = signals[index];
4271
+ invariant(signal, "Expected an AbortSignal for revalidating fetcher deferred result");
4158
4272
  await resolveDeferredData(result, signal, isFetcher).then(result => {
4159
4273
  if (result) {
4160
4274
  results[index] = result || results[index];