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