@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/router.ts CHANGED
@@ -32,6 +32,7 @@ import type {
32
32
  V7_FormMethod,
33
33
  HTMLFormMethod,
34
34
  MutationFormMethod,
35
+ MapRoutePropertiesFunction,
35
36
  } from "./utils";
36
37
  import {
37
38
  ErrorResponse,
@@ -129,7 +130,7 @@ export interface Router {
129
130
  * @param to Path to navigate to
130
131
  * @param opts Navigation options (method, submission, etc.)
131
132
  */
132
- navigate(to: To, opts?: RouterNavigateOptions): Promise<void>;
133
+ navigate(to: To | null, opts?: RouterNavigateOptions): Promise<void>;
133
134
 
134
135
  /**
135
136
  * @internal
@@ -145,7 +146,7 @@ export interface Router {
145
146
  fetch(
146
147
  key: string,
147
148
  routeId: string,
148
- href: string,
149
+ href: string | null,
149
150
  opts?: RouterNavigateOptions
150
151
  ): void;
151
152
 
@@ -334,6 +335,7 @@ export type HydrationState = Partial<
334
335
  */
335
336
  export interface FutureConfig {
336
337
  v7_normalizeFormMethod: boolean;
338
+ v7_prependBasename: boolean;
337
339
  }
338
340
 
339
341
  /**
@@ -343,8 +345,12 @@ export interface RouterInit {
343
345
  routes: AgnosticRouteObject[];
344
346
  history: History;
345
347
  basename?: string;
348
+ /**
349
+ * @deprecated Use `mapRouteProperties` instead
350
+ */
346
351
  detectErrorBoundary?: DetectErrorBoundaryFunction;
347
- future?: FutureConfig;
352
+ mapRouteProperties?: MapRoutePropertiesFunction;
353
+ future?: Partial<FutureConfig>;
348
354
  hydrationData?: HydrationState;
349
355
  }
350
356
 
@@ -410,22 +416,25 @@ export interface GetScrollPositionFunction {
410
416
  (): number;
411
417
  }
412
418
 
413
- /**
414
- * Options for a navigate() call for a Link navigation
415
- */
416
- type LinkNavigateOptions = {
419
+ export type RelativeRoutingType = "route" | "path";
420
+
421
+ type BaseNavigateOptions = {
417
422
  replace?: boolean;
418
423
  state?: any;
419
424
  preventScrollReset?: boolean;
425
+ relative?: RelativeRoutingType;
426
+ fromRouteId?: string;
420
427
  };
421
428
 
429
+ /**
430
+ * Options for a navigate() call for a Link navigation
431
+ */
432
+ type LinkNavigateOptions = BaseNavigateOptions;
433
+
422
434
  /**
423
435
  * Options for a navigate() call for a Form navigation
424
436
  */
425
- type SubmissionNavigateOptions = {
426
- replace?: boolean;
427
- state?: any;
428
- preventScrollReset?: boolean;
437
+ type SubmissionNavigateOptions = BaseNavigateOptions & {
429
438
  formMethod?: HTMLFormMethod;
430
439
  formEncType?: FormEncType;
431
440
  formData: FormData;
@@ -593,6 +602,7 @@ interface RevalidatingFetcher extends FetchLoadMatch {
593
602
  key: string;
594
603
  match: AgnosticDataRouteMatch | null;
595
604
  matches: AgnosticDataRouteMatch[] | null;
605
+ controller: AbortController | null;
596
606
  }
597
607
 
598
608
  /**
@@ -657,8 +667,10 @@ const isBrowser =
657
667
  typeof window.document.createElement !== "undefined";
658
668
  const isServer = !isBrowser;
659
669
 
660
- const defaultDetectErrorBoundary = (route: AgnosticRouteObject) =>
661
- Boolean(route.hasErrorBoundary);
670
+ const defaultMapRouteProperties: MapRoutePropertiesFunction = (route) => ({
671
+ hasErrorBoundary: Boolean(route.hasErrorBoundary),
672
+ });
673
+
662
674
  //#endregion
663
675
 
664
676
  ////////////////////////////////////////////////////////////////////////////////
@@ -674,22 +686,34 @@ export function createRouter(init: RouterInit): Router {
674
686
  "You must provide a non-empty routes array to createRouter"
675
687
  );
676
688
 
677
- let detectErrorBoundary =
678
- init.detectErrorBoundary || defaultDetectErrorBoundary;
689
+ let mapRouteProperties: MapRoutePropertiesFunction;
690
+ if (init.mapRouteProperties) {
691
+ mapRouteProperties = init.mapRouteProperties;
692
+ } else if (init.detectErrorBoundary) {
693
+ // If they are still using the deprecated version, wrap it with the new API
694
+ let detectErrorBoundary = init.detectErrorBoundary;
695
+ mapRouteProperties = (route) => ({
696
+ hasErrorBoundary: detectErrorBoundary(route),
697
+ });
698
+ } else {
699
+ mapRouteProperties = defaultMapRouteProperties;
700
+ }
679
701
 
680
702
  // Routes keyed by ID
681
703
  let manifest: RouteManifest = {};
682
704
  // Routes in tree format for matching
683
705
  let dataRoutes = convertRoutesToDataRoutes(
684
706
  init.routes,
685
- detectErrorBoundary,
707
+ mapRouteProperties,
686
708
  undefined,
687
709
  manifest
688
710
  );
689
711
  let inFlightDataRoutes: AgnosticDataRouteObject[] | undefined;
712
+ let basename = init.basename || "/";
690
713
  // Config driven behavior flags
691
714
  let future: FutureConfig = {
692
715
  v7_normalizeFormMethod: false,
716
+ v7_prependBasename: false,
693
717
  ...init.future,
694
718
  };
695
719
  // Cleanup function for history
@@ -710,11 +734,7 @@ export function createRouter(init: RouterInit): Router {
710
734
  // SSR did the initial scroll restoration.
711
735
  let initialScrollRestored = init.hydrationData != null;
712
736
 
713
- let initialMatches = matchRoutes(
714
- dataRoutes,
715
- init.history.location,
716
- init.basename
717
- );
737
+ let initialMatches = matchRoutes(dataRoutes, init.history.location, basename);
718
738
  let initialErrors: RouteData | null = null;
719
739
 
720
740
  if (initialMatches == null) {
@@ -770,7 +790,7 @@ export function createRouter(init: RouterInit): Router {
770
790
 
771
791
  // Use this internal flag to force revalidation of all loaders:
772
792
  // - submissions (completed or interrupted)
773
- // - useRevalidate()
793
+ // - useRevalidator()
774
794
  // - X-Remix-Revalidate (from redirect)
775
795
  let isRevalidationRequired = false;
776
796
 
@@ -796,7 +816,7 @@ export function createRouter(init: RouterInit): Router {
796
816
  // Fetchers that triggered data reloads as a result of their actions
797
817
  let fetchReloadIds = new Map<string, number>();
798
818
 
799
- // Fetchers that triggered redirect navigations from their actions
819
+ // Fetchers that triggered redirect navigations
800
820
  let fetchRedirectIds = new Set<string>();
801
821
 
802
822
  // Most recent href/match for fetcher.load calls for fetchers
@@ -1021,7 +1041,7 @@ export function createRouter(init: RouterInit): Router {
1021
1041
  // Trigger a navigation event, which can either be a numerical POP or a PUSH
1022
1042
  // replace with an optional submission
1023
1043
  async function navigate(
1024
- to: number | To,
1044
+ to: number | To | null,
1025
1045
  opts?: RouterNavigateOptions
1026
1046
  ): Promise<void> {
1027
1047
  if (typeof to === "number") {
@@ -1029,9 +1049,19 @@ export function createRouter(init: RouterInit): Router {
1029
1049
  return;
1030
1050
  }
1031
1051
 
1032
- let { path, submission, error } = normalizeNavigateOptions(
1052
+ let normalizedPath = normalizeTo(
1053
+ state.location,
1054
+ state.matches,
1055
+ basename,
1056
+ future.v7_prependBasename,
1033
1057
  to,
1034
- future,
1058
+ opts?.fromRouteId,
1059
+ opts?.relative
1060
+ );
1061
+ let { path, submission, error } = normalizeNavigateOptions(
1062
+ future.v7_normalizeFormMethod,
1063
+ false,
1064
+ normalizedPath,
1035
1065
  opts
1036
1066
  );
1037
1067
 
@@ -1176,7 +1206,7 @@ export function createRouter(init: RouterInit): Router {
1176
1206
 
1177
1207
  let routesToUse = inFlightDataRoutes || dataRoutes;
1178
1208
  let loadingNavigation = opts && opts.overrideNavigation;
1179
- let matches = matchRoutes(routesToUse, location, init.basename);
1209
+ let matches = matchRoutes(routesToUse, location, basename);
1180
1210
 
1181
1211
  // Short circuit with a 404 on the root error boundary if we match nothing
1182
1212
  if (!matches) {
@@ -1326,8 +1356,8 @@ export function createRouter(init: RouterInit): Router {
1326
1356
  actionMatch,
1327
1357
  matches,
1328
1358
  manifest,
1329
- detectErrorBoundary,
1330
- router.basename
1359
+ mapRouteProperties,
1360
+ basename
1331
1361
  );
1332
1362
 
1333
1363
  if (request.signal.aborted) {
@@ -1436,7 +1466,7 @@ export function createRouter(init: RouterInit): Router {
1436
1466
  cancelledFetcherLoads,
1437
1467
  fetchLoadMatches,
1438
1468
  routesToUse,
1439
- init.basename,
1469
+ basename,
1440
1470
  pendingActionData,
1441
1471
  pendingError
1442
1472
  );
@@ -1452,12 +1482,14 @@ export function createRouter(init: RouterInit): Router {
1452
1482
 
1453
1483
  // Short circuit if we have no loaders to run
1454
1484
  if (matchesToLoad.length === 0 && revalidatingFetchers.length === 0) {
1485
+ let updatedFetchers = markFetchRedirectsDone();
1455
1486
  completeNavigation(location, {
1456
1487
  matches,
1457
1488
  loaderData: {},
1458
1489
  // Commit pending error if we're short circuiting
1459
1490
  errors: pendingError || null,
1460
1491
  ...(pendingActionData ? { actionData: pendingActionData } : {}),
1492
+ ...(updatedFetchers ? { fetchers: new Map(state.fetchers) } : {}),
1461
1493
  });
1462
1494
  return { shortCircuited: true };
1463
1495
  }
@@ -1495,9 +1527,24 @@ export function createRouter(init: RouterInit): Router {
1495
1527
  }
1496
1528
 
1497
1529
  pendingNavigationLoadId = ++incrementingLoadId;
1498
- revalidatingFetchers.forEach((rf) =>
1499
- fetchControllers.set(rf.key, pendingNavigationController!)
1500
- );
1530
+ revalidatingFetchers.forEach((rf) => {
1531
+ if (rf.controller) {
1532
+ // Fetchers use an independent AbortController so that aborting a fetcher
1533
+ // (via deleteFetcher) does not abort the triggering navigation that
1534
+ // triggered the revalidation
1535
+ fetchControllers.set(rf.key, rf.controller);
1536
+ }
1537
+ });
1538
+
1539
+ // Proxy navigation abort through to revalidation fetchers
1540
+ let abortPendingFetchRevalidations = () =>
1541
+ revalidatingFetchers.forEach((f) => abortFetcher(f.key));
1542
+ if (pendingNavigationController) {
1543
+ pendingNavigationController.signal.addEventListener(
1544
+ "abort",
1545
+ abortPendingFetchRevalidations
1546
+ );
1547
+ }
1501
1548
 
1502
1549
  let { results, loaderResults, fetcherResults } =
1503
1550
  await callLoadersAndMaybeResolveData(
@@ -1515,6 +1562,12 @@ export function createRouter(init: RouterInit): Router {
1515
1562
  // Clean up _after_ loaders have completed. Don't clean up if we short
1516
1563
  // circuited because fetchControllers would have been aborted and
1517
1564
  // reassigned to new controllers for the next navigation
1565
+ if (pendingNavigationController) {
1566
+ pendingNavigationController.signal.removeEventListener(
1567
+ "abort",
1568
+ abortPendingFetchRevalidations
1569
+ );
1570
+ }
1518
1571
  revalidatingFetchers.forEach((rf) => fetchControllers.delete(rf.key));
1519
1572
 
1520
1573
  // If any loaders returned a redirect Response, start a new REPLACE navigation
@@ -1548,15 +1601,15 @@ export function createRouter(init: RouterInit): Router {
1548
1601
  });
1549
1602
  });
1550
1603
 
1551
- markFetchRedirectsDone();
1604
+ let updatedFetchers = markFetchRedirectsDone();
1552
1605
  let didAbortFetchLoads = abortStaleFetchLoads(pendingNavigationLoadId);
1606
+ let shouldUpdateFetchers =
1607
+ updatedFetchers || didAbortFetchLoads || revalidatingFetchers.length > 0;
1553
1608
 
1554
1609
  return {
1555
1610
  loaderData,
1556
1611
  errors,
1557
- ...(didAbortFetchLoads || revalidatingFetchers.length > 0
1558
- ? { fetchers: new Map(state.fetchers) }
1559
- : {}),
1612
+ ...(shouldUpdateFetchers ? { fetchers: new Map(state.fetchers) } : {}),
1560
1613
  };
1561
1614
  }
1562
1615
 
@@ -1568,7 +1621,7 @@ export function createRouter(init: RouterInit): Router {
1568
1621
  function fetch(
1569
1622
  key: string,
1570
1623
  routeId: string,
1571
- href: string,
1624
+ href: string | null,
1572
1625
  opts?: RouterFetchOptions
1573
1626
  ) {
1574
1627
  if (isServer) {
@@ -1582,21 +1635,31 @@ export function createRouter(init: RouterInit): Router {
1582
1635
  if (fetchControllers.has(key)) abortFetcher(key);
1583
1636
 
1584
1637
  let routesToUse = inFlightDataRoutes || dataRoutes;
1585
- let matches = matchRoutes(routesToUse, href, init.basename);
1638
+ let normalizedPath = normalizeTo(
1639
+ state.location,
1640
+ state.matches,
1641
+ basename,
1642
+ future.v7_prependBasename,
1643
+ href,
1644
+ routeId,
1645
+ opts?.relative
1646
+ );
1647
+ let matches = matchRoutes(routesToUse, normalizedPath, basename);
1648
+
1586
1649
  if (!matches) {
1587
1650
  setFetcherError(
1588
1651
  key,
1589
1652
  routeId,
1590
- getInternalRouterError(404, { pathname: href })
1653
+ getInternalRouterError(404, { pathname: normalizedPath })
1591
1654
  );
1592
1655
  return;
1593
1656
  }
1594
1657
 
1595
1658
  let { path, submission } = normalizeNavigateOptions(
1596
- href,
1597
- future,
1598
- opts,
1599
- true
1659
+ future.v7_normalizeFormMethod,
1660
+ true,
1661
+ normalizedPath,
1662
+ opts
1600
1663
  );
1601
1664
  let match = getTargetMatch(matches, path);
1602
1665
 
@@ -1663,8 +1726,8 @@ export function createRouter(init: RouterInit): Router {
1663
1726
  match,
1664
1727
  requestMatches,
1665
1728
  manifest,
1666
- detectErrorBoundary,
1667
- router.basename
1729
+ mapRouteProperties,
1730
+ basename
1668
1731
  );
1669
1732
 
1670
1733
  if (fetchRequest.signal.aborted) {
@@ -1716,7 +1779,7 @@ export function createRouter(init: RouterInit): Router {
1716
1779
  let routesToUse = inFlightDataRoutes || dataRoutes;
1717
1780
  let matches =
1718
1781
  state.navigation.state !== "idle"
1719
- ? matchRoutes(routesToUse, state.navigation.location, init.basename)
1782
+ ? matchRoutes(routesToUse, state.navigation.location, basename)
1720
1783
  : state.matches;
1721
1784
 
1722
1785
  invariant(matches, "Didn't find any matches after fetcher action");
@@ -1743,7 +1806,7 @@ export function createRouter(init: RouterInit): Router {
1743
1806
  cancelledFetcherLoads,
1744
1807
  fetchLoadMatches,
1745
1808
  routesToUse,
1746
- init.basename,
1809
+ basename,
1747
1810
  { [match.route.id]: actionResult.data },
1748
1811
  undefined // No need to send through errors since we short circuit above
1749
1812
  );
@@ -1766,11 +1829,21 @@ export function createRouter(init: RouterInit): Router {
1766
1829
  " _hasFetcherDoneAnything ": true,
1767
1830
  };
1768
1831
  state.fetchers.set(staleKey, revalidatingFetcher);
1769
- fetchControllers.set(staleKey, abortController);
1832
+ if (rf.controller) {
1833
+ fetchControllers.set(staleKey, rf.controller);
1834
+ }
1770
1835
  });
1771
1836
 
1772
1837
  updateState({ fetchers: new Map(state.fetchers) });
1773
1838
 
1839
+ let abortPendingFetchRevalidations = () =>
1840
+ revalidatingFetchers.forEach((rf) => abortFetcher(rf.key));
1841
+
1842
+ abortController.signal.addEventListener(
1843
+ "abort",
1844
+ abortPendingFetchRevalidations
1845
+ );
1846
+
1774
1847
  let { results, loaderResults, fetcherResults } =
1775
1848
  await callLoadersAndMaybeResolveData(
1776
1849
  state.matches,
@@ -1784,6 +1857,11 @@ export function createRouter(init: RouterInit): Router {
1784
1857
  return;
1785
1858
  }
1786
1859
 
1860
+ abortController.signal.removeEventListener(
1861
+ "abort",
1862
+ abortPendingFetchRevalidations
1863
+ );
1864
+
1787
1865
  fetchReloadIds.delete(key);
1788
1866
  fetchControllers.delete(key);
1789
1867
  revalidatingFetchers.forEach((r) => fetchControllers.delete(r.key));
@@ -1891,8 +1969,8 @@ export function createRouter(init: RouterInit): Router {
1891
1969
  match,
1892
1970
  matches,
1893
1971
  manifest,
1894
- detectErrorBoundary,
1895
- router.basename
1972
+ mapRouteProperties,
1973
+ basename
1896
1974
  );
1897
1975
 
1898
1976
  // Deferred isn't supported for fetcher loads, await everything and treat it
@@ -1905,7 +1983,7 @@ export function createRouter(init: RouterInit): Router {
1905
1983
  result;
1906
1984
  }
1907
1985
 
1908
- // We can delete this so long as we weren't aborted by ou our own fetcher
1986
+ // We can delete this so long as we weren't aborted by our our own fetcher
1909
1987
  // re-load which would have put _new_ controller is in fetchControllers
1910
1988
  if (fetchControllers.get(key) === abortController) {
1911
1989
  fetchControllers.delete(key);
@@ -1917,6 +1995,7 @@ export function createRouter(init: RouterInit): Router {
1917
1995
 
1918
1996
  // If the loader threw a redirect Response, start a new REPLACE navigation
1919
1997
  if (isRedirectResult(result)) {
1998
+ fetchRedirectIds.add(key);
1920
1999
  await startRedirectNavigation(state, result);
1921
2000
  return;
1922
2001
  }
@@ -2009,8 +2088,7 @@ export function createRouter(init: RouterInit): Router {
2009
2088
  typeof window?.location !== "undefined"
2010
2089
  ) {
2011
2090
  let url = init.history.createURL(redirect.location);
2012
- let isDifferentBasename =
2013
- stripBasename(url.pathname, init.basename || "/") == null;
2091
+ let isDifferentBasename = stripBasename(url.pathname, basename) == null;
2014
2092
 
2015
2093
  if (window.location.origin !== url.origin || isDifferentBasename) {
2016
2094
  if (replace) {
@@ -2109,20 +2187,20 @@ export function createRouter(init: RouterInit): Router {
2109
2187
  match,
2110
2188
  matches,
2111
2189
  manifest,
2112
- detectErrorBoundary,
2113
- router.basename
2190
+ mapRouteProperties,
2191
+ basename
2114
2192
  )
2115
2193
  ),
2116
2194
  ...fetchersToLoad.map((f) => {
2117
- if (f.matches && f.match) {
2195
+ if (f.matches && f.match && f.controller) {
2118
2196
  return callLoaderOrAction(
2119
2197
  "loader",
2120
- createClientSideRequest(init.history, f.path, request.signal),
2198
+ createClientSideRequest(init.history, f.path, f.controller.signal),
2121
2199
  f.match,
2122
2200
  f.matches,
2123
2201
  manifest,
2124
- detectErrorBoundary,
2125
- router.basename
2202
+ mapRouteProperties,
2203
+ basename
2126
2204
  );
2127
2205
  } else {
2128
2206
  let error: ErrorResult = {
@@ -2141,7 +2219,7 @@ export function createRouter(init: RouterInit): Router {
2141
2219
  currentMatches,
2142
2220
  matchesToLoad,
2143
2221
  loaderResults,
2144
- request.signal,
2222
+ loaderResults.map(() => request.signal),
2145
2223
  false,
2146
2224
  state.loaderData
2147
2225
  ),
@@ -2149,7 +2227,7 @@ export function createRouter(init: RouterInit): Router {
2149
2227
  currentMatches,
2150
2228
  fetchersToLoad.map((f) => f.match),
2151
2229
  fetcherResults,
2152
- request.signal,
2230
+ fetchersToLoad.map((f) => (f.controller ? f.controller.signal : null)),
2153
2231
  true
2154
2232
  ),
2155
2233
  ]);
@@ -2216,17 +2294,20 @@ export function createRouter(init: RouterInit): Router {
2216
2294
  }
2217
2295
  }
2218
2296
 
2219
- function markFetchRedirectsDone(): void {
2297
+ function markFetchRedirectsDone(): boolean {
2220
2298
  let doneKeys = [];
2299
+ let updatedFetchers = false;
2221
2300
  for (let key of fetchRedirectIds) {
2222
2301
  let fetcher = state.fetchers.get(key);
2223
2302
  invariant(fetcher, `Expected fetcher: ${key}`);
2224
2303
  if (fetcher.state === "loading") {
2225
2304
  fetchRedirectIds.delete(key);
2226
2305
  doneKeys.push(key);
2306
+ updatedFetchers = true;
2227
2307
  }
2228
2308
  }
2229
2309
  markFetchersDone(doneKeys);
2310
+ return updatedFetchers;
2230
2311
  }
2231
2312
 
2232
2313
  function abortStaleFetchLoads(landedId: number): boolean {
@@ -2398,7 +2479,7 @@ export function createRouter(init: RouterInit): Router {
2398
2479
 
2399
2480
  router = {
2400
2481
  get basename() {
2401
- return init.basename;
2482
+ return basename;
2402
2483
  },
2403
2484
  get state() {
2404
2485
  return state;
@@ -2440,7 +2521,11 @@ export const UNSAFE_DEFERRED_SYMBOL = Symbol("deferred");
2440
2521
 
2441
2522
  export interface CreateStaticHandlerOptions {
2442
2523
  basename?: string;
2524
+ /**
2525
+ * @deprecated Use `mapRouteProperties` instead
2526
+ */
2443
2527
  detectErrorBoundary?: DetectErrorBoundaryFunction;
2528
+ mapRouteProperties?: MapRoutePropertiesFunction;
2444
2529
  }
2445
2530
 
2446
2531
  export function createStaticHandler(
@@ -2453,15 +2538,26 @@ export function createStaticHandler(
2453
2538
  );
2454
2539
 
2455
2540
  let manifest: RouteManifest = {};
2456
- let detectErrorBoundary =
2457
- opts?.detectErrorBoundary || defaultDetectErrorBoundary;
2541
+ let basename = (opts ? opts.basename : null) || "/";
2542
+ let mapRouteProperties: MapRoutePropertiesFunction;
2543
+ if (opts?.mapRouteProperties) {
2544
+ mapRouteProperties = opts.mapRouteProperties;
2545
+ } else if (opts?.detectErrorBoundary) {
2546
+ // If they are still using the deprecated version, wrap it with the new API
2547
+ let detectErrorBoundary = opts.detectErrorBoundary;
2548
+ mapRouteProperties = (route) => ({
2549
+ hasErrorBoundary: detectErrorBoundary(route),
2550
+ });
2551
+ } else {
2552
+ mapRouteProperties = defaultMapRouteProperties;
2553
+ }
2554
+
2458
2555
  let dataRoutes = convertRoutesToDataRoutes(
2459
2556
  routes,
2460
- detectErrorBoundary,
2557
+ mapRouteProperties,
2461
2558
  undefined,
2462
2559
  manifest
2463
2560
  );
2464
- let basename = (opts ? opts.basename : null) || "/";
2465
2561
 
2466
2562
  /**
2467
2563
  * The query() method is intended for document requests, in which we want to
@@ -2715,7 +2811,7 @@ export function createStaticHandler(
2715
2811
  actionMatch,
2716
2812
  matches,
2717
2813
  manifest,
2718
- detectErrorBoundary,
2814
+ mapRouteProperties,
2719
2815
  basename,
2720
2816
  true,
2721
2817
  isRouteRequest,
@@ -2883,7 +2979,7 @@ export function createStaticHandler(
2883
2979
  match,
2884
2980
  matches,
2885
2981
  manifest,
2886
- detectErrorBoundary,
2982
+ mapRouteProperties,
2887
2983
  basename,
2888
2984
  true,
2889
2985
  isRouteRequest,
@@ -2965,20 +3061,87 @@ function isSubmissionNavigation(
2965
3061
  return opts != null && "formData" in opts;
2966
3062
  }
2967
3063
 
3064
+ function normalizeTo(
3065
+ location: Path,
3066
+ matches: AgnosticDataRouteMatch[],
3067
+ basename: string,
3068
+ prependBasename: boolean,
3069
+ to: To | null,
3070
+ fromRouteId?: string,
3071
+ relative?: RelativeRoutingType
3072
+ ) {
3073
+ let contextualMatches: AgnosticDataRouteMatch[];
3074
+ let activeRouteMatch: AgnosticDataRouteMatch | undefined;
3075
+ if (fromRouteId != null && relative !== "path") {
3076
+ // Grab matches up to the calling route so our route-relative logic is
3077
+ // relative to the correct source route. When using relative:path,
3078
+ // fromRouteId is ignored since that is always relative to the current
3079
+ // location path
3080
+ contextualMatches = [];
3081
+ for (let match of matches) {
3082
+ contextualMatches.push(match);
3083
+ if (match.route.id === fromRouteId) {
3084
+ activeRouteMatch = match;
3085
+ break;
3086
+ }
3087
+ }
3088
+ } else {
3089
+ contextualMatches = matches;
3090
+ activeRouteMatch = matches[matches.length - 1];
3091
+ }
3092
+
3093
+ // Resolve the relative path
3094
+ let path = resolveTo(
3095
+ to ? to : ".",
3096
+ getPathContributingMatches(contextualMatches).map((m) => m.pathnameBase),
3097
+ location.pathname,
3098
+ relative === "path"
3099
+ );
3100
+
3101
+ // When `to` is not specified we inherit search/hash from the current
3102
+ // location, unlike when to="." and we just inherit the path.
3103
+ // See https://github.com/remix-run/remix/issues/927
3104
+ if (to == null) {
3105
+ path.search = location.search;
3106
+ path.hash = location.hash;
3107
+ }
3108
+
3109
+ // Add an ?index param for matched index routes if we don't already have one
3110
+ if (
3111
+ (to == null || to === "" || to === ".") &&
3112
+ activeRouteMatch &&
3113
+ activeRouteMatch.route.index &&
3114
+ !hasNakedIndexQuery(path.search)
3115
+ ) {
3116
+ path.search = path.search
3117
+ ? path.search.replace(/^\?/, "?index&")
3118
+ : "?index";
3119
+ }
3120
+
3121
+ // If we're operating within a basename, prepend it to the pathname. If
3122
+ // this is a root navigation, then just use the raw basename which allows
3123
+ // the basename to have full control over the presence of a trailing slash
3124
+ // on root actions
3125
+ if (prependBasename && basename !== "/") {
3126
+ path.pathname =
3127
+ path.pathname === "/" ? basename : joinPaths([basename, path.pathname]);
3128
+ }
3129
+
3130
+ return createPath(path);
3131
+ }
3132
+
2968
3133
  // Normalize navigation options by converting formMethod=GET formData objects to
2969
3134
  // URLSearchParams so they behave identically to links with query params
2970
3135
  function normalizeNavigateOptions(
2971
- to: To,
2972
- future: FutureConfig,
2973
- opts?: RouterNavigateOptions,
2974
- isFetcher = false
3136
+ normalizeFormMethod: boolean,
3137
+ isFetcher: boolean,
3138
+ path: string,
3139
+ opts?: RouterNavigateOptions
2975
3140
  ): {
2976
3141
  path: string;
2977
3142
  submission?: Submission;
2978
3143
  error?: ErrorResponse;
2979
3144
  } {
2980
- let path = typeof to === "string" ? to : createPath(to);
2981
-
2982
3145
  // Return location verbatim on non-submission navigations
2983
3146
  if (!opts || !isSubmissionNavigation(opts)) {
2984
3147
  return { path };
@@ -2996,7 +3159,7 @@ function normalizeNavigateOptions(
2996
3159
  if (opts.formData) {
2997
3160
  let formMethod = opts.formMethod || "get";
2998
3161
  submission = {
2999
- formMethod: future.v7_normalizeFormMethod
3162
+ formMethod: normalizeFormMethod
3000
3163
  ? (formMethod.toUpperCase() as V7_FormMethod)
3001
3164
  : (formMethod.toLowerCase() as FormMethod),
3002
3165
  formAction: stripHashFromPath(path),
@@ -3013,9 +3176,9 @@ function normalizeNavigateOptions(
3013
3176
  // Flatten submission onto URLSearchParams for GET submissions
3014
3177
  let parsedPath = parsePath(path);
3015
3178
  let searchParams = convertFormDataToSearchParams(opts.formData);
3016
- // Since fetcher GET submissions only run a single loader (as opposed to
3017
- // navigation GET submissions which run all loaders), we need to preserve
3018
- // any incoming ?index params
3179
+ // On GET navigation submissions we can drop the ?index param from the
3180
+ // resulting location since all loaders will run. But fetcher GET submissions
3181
+ // only run a single loader so we need to preserve any incoming ?index params
3019
3182
  if (isFetcher && parsedPath.search && hasNakedIndexQuery(parsedPath.search)) {
3020
3183
  searchParams.append("index", "");
3021
3184
  }
@@ -3064,14 +3227,6 @@ function getMatchesToLoad(
3064
3227
  let currentUrl = history.createURL(state.location);
3065
3228
  let nextUrl = history.createURL(location);
3066
3229
 
3067
- let defaultShouldRevalidate =
3068
- // Forced revalidation due to submission, useRevalidate, or X-Remix-Revalidate
3069
- isRevalidationRequired ||
3070
- // Clicked the same link, resubmitted a GET form
3071
- currentUrl.toString() === nextUrl.toString() ||
3072
- // Search params affect all loaders
3073
- currentUrl.search !== nextUrl.search;
3074
-
3075
3230
  // Pick navigation matches that are net-new or qualify for revalidation
3076
3231
  let boundaryId = pendingError ? Object.keys(pendingError)[0] : undefined;
3077
3232
  let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId);
@@ -3108,7 +3263,12 @@ function getMatchesToLoad(
3108
3263
  ...submission,
3109
3264
  actionResult,
3110
3265
  defaultShouldRevalidate:
3111
- defaultShouldRevalidate ||
3266
+ // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate
3267
+ isRevalidationRequired ||
3268
+ // Clicked the same link, resubmitted a GET form
3269
+ currentUrl.toString() === nextUrl.toString() ||
3270
+ // Search params affect all loaders
3271
+ currentUrl.search !== nextUrl.search ||
3112
3272
  isNewRouteInstance(currentRouteMatch, nextRouteMatch),
3113
3273
  });
3114
3274
  });
@@ -3126,7 +3286,14 @@ function getMatchesToLoad(
3126
3286
  // If the fetcher path no longer matches, push it in with null matches so
3127
3287
  // we can trigger a 404 in callLoadersAndMaybeResolveData
3128
3288
  if (!fetcherMatches) {
3129
- revalidatingFetchers.push({ key, ...f, matches: null, match: null });
3289
+ revalidatingFetchers.push({
3290
+ key,
3291
+ routeId: f.routeId,
3292
+ path: f.path,
3293
+ matches: null,
3294
+ match: null,
3295
+ controller: null,
3296
+ });
3130
3297
  return;
3131
3298
  }
3132
3299
 
@@ -3135,9 +3302,11 @@ function getMatchesToLoad(
3135
3302
  if (cancelledFetcherLoads.includes(key)) {
3136
3303
  revalidatingFetchers.push({
3137
3304
  key,
3305
+ routeId: f.routeId,
3306
+ path: f.path,
3138
3307
  matches: fetcherMatches,
3139
3308
  match: fetcherMatch,
3140
- ...f,
3309
+ controller: new AbortController(),
3141
3310
  });
3142
3311
  return;
3143
3312
  }
@@ -3153,14 +3322,17 @@ function getMatchesToLoad(
3153
3322
  nextParams: matches[matches.length - 1].params,
3154
3323
  ...submission,
3155
3324
  actionResult,
3156
- defaultShouldRevalidate,
3325
+ // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate
3326
+ defaultShouldRevalidate: isRevalidationRequired,
3157
3327
  });
3158
3328
  if (shouldRevalidate) {
3159
3329
  revalidatingFetchers.push({
3160
3330
  key,
3331
+ routeId: f.routeId,
3332
+ path: f.path,
3161
3333
  matches: fetcherMatches,
3162
3334
  match: fetcherMatch,
3163
- ...f,
3335
+ controller: new AbortController(),
3164
3336
  });
3165
3337
  }
3166
3338
  });
@@ -3224,7 +3396,7 @@ function shouldRevalidateLoader(
3224
3396
  */
3225
3397
  async function loadLazyRouteModule(
3226
3398
  route: AgnosticDataRouteObject,
3227
- detectErrorBoundary: DetectErrorBoundaryFunction,
3399
+ mapRouteProperties: MapRoutePropertiesFunction,
3228
3400
  manifest: RouteManifest
3229
3401
  ) {
3230
3402
  if (!route.lazy) {
@@ -3279,7 +3451,7 @@ async function loadLazyRouteModule(
3279
3451
  }
3280
3452
 
3281
3453
  // Mutate the route with the provided updates. Do this first so we pass
3282
- // the updated version to detectErrorBoundary
3454
+ // the updated version to mapRouteProperties
3283
3455
  Object.assign(routeToUpdate, routeUpdates);
3284
3456
 
3285
3457
  // Mutate the `hasErrorBoundary` property on the route based on the route
@@ -3287,9 +3459,10 @@ async function loadLazyRouteModule(
3287
3459
  // route again.
3288
3460
  Object.assign(routeToUpdate, {
3289
3461
  // To keep things framework agnostic, we use the provided
3290
- // `detectErrorBoundary` function to set the `hasErrorBoundary` route
3291
- // property since the logic will differ between frameworks.
3292
- hasErrorBoundary: detectErrorBoundary({ ...routeToUpdate }),
3462
+ // `mapRouteProperties` (or wrapped `detectErrorBoundary`) function to
3463
+ // set the framework-aware properties (`element`/`hasErrorBoundary`) since
3464
+ // the logic will differ between frameworks.
3465
+ ...mapRouteProperties(routeToUpdate),
3293
3466
  lazy: undefined,
3294
3467
  });
3295
3468
  }
@@ -3300,8 +3473,8 @@ async function callLoaderOrAction(
3300
3473
  match: AgnosticDataRouteMatch,
3301
3474
  matches: AgnosticDataRouteMatch[],
3302
3475
  manifest: RouteManifest,
3303
- detectErrorBoundary: DetectErrorBoundaryFunction,
3304
- basename = "/",
3476
+ mapRouteProperties: MapRoutePropertiesFunction,
3477
+ basename: string,
3305
3478
  isStaticRequest: boolean = false,
3306
3479
  isRouteRequest: boolean = false,
3307
3480
  requestContext?: unknown
@@ -3330,12 +3503,12 @@ async function callLoaderOrAction(
3330
3503
  // Run statically defined handler in parallel with lazy()
3331
3504
  let values = await Promise.all([
3332
3505
  runHandler(handler),
3333
- loadLazyRouteModule(match.route, detectErrorBoundary, manifest),
3506
+ loadLazyRouteModule(match.route, mapRouteProperties, manifest),
3334
3507
  ]);
3335
3508
  result = values[0];
3336
3509
  } else {
3337
3510
  // Load lazy route module, then run any returned handler
3338
- await loadLazyRouteModule(match.route, detectErrorBoundary, manifest);
3511
+ await loadLazyRouteModule(match.route, mapRouteProperties, manifest);
3339
3512
 
3340
3513
  handler = match.route[type];
3341
3514
  if (handler) {
@@ -3344,9 +3517,11 @@ async function callLoaderOrAction(
3344
3517
  // previously-lazy-loaded routes
3345
3518
  result = await runHandler(handler);
3346
3519
  } else if (type === "action") {
3520
+ let url = new URL(request.url);
3521
+ let pathname = url.pathname + url.search;
3347
3522
  throw getInternalRouterError(405, {
3348
3523
  method: request.method,
3349
- pathname: new URL(request.url).pathname,
3524
+ pathname,
3350
3525
  routeId: match.route.id,
3351
3526
  });
3352
3527
  } else {
@@ -3355,12 +3530,13 @@ async function callLoaderOrAction(
3355
3530
  return { type: ResultType.data, data: undefined };
3356
3531
  }
3357
3532
  }
3533
+ } else if (!handler) {
3534
+ let url = new URL(request.url);
3535
+ let pathname = url.pathname + url.search;
3536
+ throw getInternalRouterError(404, {
3537
+ pathname,
3538
+ });
3358
3539
  } else {
3359
- invariant<Function>(
3360
- handler,
3361
- `Could not find the ${type} to run on the "${match.route.id}" route`
3362
- );
3363
-
3364
3540
  result = await runHandler(handler);
3365
3541
  }
3366
3542
 
@@ -3392,28 +3568,13 @@ async function callLoaderOrAction(
3392
3568
 
3393
3569
  // Support relative routing in internal redirects
3394
3570
  if (!ABSOLUTE_URL_REGEX.test(location)) {
3395
- let activeMatches = matches.slice(0, matches.indexOf(match) + 1);
3396
- let routePathnames = getPathContributingMatches(activeMatches).map(
3397
- (match) => match.pathnameBase
3398
- );
3399
- let resolvedLocation = resolveTo(
3400
- location,
3401
- routePathnames,
3402
- new URL(request.url).pathname
3403
- );
3404
- invariant(
3405
- createPath(resolvedLocation),
3406
- `Unable to resolve redirect location: ${location}`
3571
+ location = normalizeTo(
3572
+ new URL(request.url),
3573
+ matches.slice(0, matches.indexOf(match) + 1),
3574
+ basename,
3575
+ true,
3576
+ location
3407
3577
  );
3408
-
3409
- // Prepend the basename to the redirect location if we have one
3410
- if (basename) {
3411
- let path = resolvedLocation.pathname;
3412
- resolvedLocation.pathname =
3413
- path === "/" ? basename : joinPaths([basename, path]);
3414
- }
3415
-
3416
- location = createPath(resolvedLocation);
3417
3578
  } else if (!isStaticRequest) {
3418
3579
  // Strip off the protocol+origin for same-origin + same-basename absolute
3419
3580
  // redirects. If this is a static request, we can let it go back to the
@@ -3659,7 +3820,7 @@ function processLoaderData(
3659
3820
 
3660
3821
  // Process results from our revalidating fetchers
3661
3822
  for (let index = 0; index < revalidatingFetchers.length; index++) {
3662
- let { key, match } = revalidatingFetchers[index];
3823
+ let { key, match, controller } = revalidatingFetchers[index];
3663
3824
  invariant(
3664
3825
  fetcherResults !== undefined && fetcherResults[index] !== undefined,
3665
3826
  "Did not find corresponding fetcher result"
@@ -3667,7 +3828,10 @@ function processLoaderData(
3667
3828
  let result = fetcherResults[index];
3668
3829
 
3669
3830
  // Process fetcher non-redirect errors
3670
- if (isErrorResult(result)) {
3831
+ if (controller && controller.signal.aborted) {
3832
+ // Nothing to do for aborted fetchers
3833
+ continue;
3834
+ } else if (isErrorResult(result)) {
3671
3835
  let boundaryMatch = findNearestBoundary(state.matches, match?.route.id);
3672
3836
  if (!(errors && errors[boundaryMatch.route.id])) {
3673
3837
  errors = {
@@ -3910,7 +4074,7 @@ async function resolveDeferredResults(
3910
4074
  currentMatches: AgnosticDataRouteMatch[],
3911
4075
  matchesToLoad: (AgnosticDataRouteMatch | null)[],
3912
4076
  results: DataResult[],
3913
- signal: AbortSignal,
4077
+ signals: (AbortSignal | null)[],
3914
4078
  isFetcher: boolean,
3915
4079
  currentLoaderData?: RouteData
3916
4080
  ) {
@@ -3936,6 +4100,11 @@ async function resolveDeferredResults(
3936
4100
  // Note: we do not have to touch activeDeferreds here since we race them
3937
4101
  // against the signal in resolveDeferredData and they'll get aborted
3938
4102
  // there if needed
4103
+ let signal = signals[index];
4104
+ invariant(
4105
+ signal,
4106
+ "Expected an AbortSignal for revalidating fetcher deferred result"
4107
+ );
3939
4108
  await resolveDeferredData(result, signal, isFetcher).then((result) => {
3940
4109
  if (result) {
3941
4110
  results[index] = result || results[index];