@remix-run/router 1.0.2 → 1.0.3-pre.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/router.ts CHANGED
@@ -1,8 +1,9 @@
1
- import type { History, Location, Path, To } from "./history";
1
+ import type { History, Location, To } from "./history";
2
2
  import {
3
3
  Action as HistoryAction,
4
4
  createLocation,
5
5
  createPath,
6
+ createURL,
6
7
  parsePath,
7
8
  } from "./history";
8
9
  import type {
@@ -25,9 +26,12 @@ import {
25
26
  ErrorResponse,
26
27
  ResultType,
27
28
  convertRoutesToDataRoutes,
29
+ getPathContributingMatches,
28
30
  invariant,
29
31
  isRouteErrorResponse,
32
+ joinPaths,
30
33
  matchRoutes,
34
+ resolveTo,
31
35
  } from "./utils";
32
36
 
33
37
  ////////////////////////////////////////////////////////////////////////////////
@@ -469,14 +473,35 @@ interface HandleLoadersResult extends ShortCircuitable {
469
473
  }
470
474
 
471
475
  /**
472
- * Tuple of [key, href, DataRouterMatch] for a revalidating fetcher.load()
476
+ * Tuple of [key, href, DataRouteMatch, DataRouteMatch[]] for a revalidating
477
+ * fetcher.load()
473
478
  */
474
- type RevalidatingFetcher = [string, string, AgnosticDataRouteMatch];
479
+ type RevalidatingFetcher = [
480
+ string,
481
+ string,
482
+ AgnosticDataRouteMatch,
483
+ AgnosticDataRouteMatch[]
484
+ ];
485
+
486
+ /**
487
+ * Tuple of [href, DataRouteMatch, DataRouteMatch[]] for an active
488
+ * fetcher.load()
489
+ */
490
+ type FetchLoadMatch = [
491
+ string,
492
+ AgnosticDataRouteMatch,
493
+ AgnosticDataRouteMatch[]
494
+ ];
475
495
 
476
496
  /**
477
- * Tuple of [href, DataRouteMatch] for an active fetcher.load()
497
+ * Wrapper object to allow us to throw any response out from callLoaderOrAction
498
+ * for queryRouter while preserving whether or not it was thrown or returned
499
+ * from the loader/action
478
500
  */
479
- type FetchLoadMatch = [string, AgnosticDataRouteMatch];
501
+ interface QueryRouteResponse {
502
+ type: ResultType.data | ResultType.error;
503
+ response: Response;
504
+ }
480
505
 
481
506
  export const IDLE_NAVIGATION: NavigationStates["Idle"] = {
482
507
  state: "idle",
@@ -495,6 +520,12 @@ export const IDLE_FETCHER: FetcherStates["Idle"] = {
495
520
  formEncType: undefined,
496
521
  formData: undefined,
497
522
  };
523
+
524
+ const isBrowser =
525
+ typeof window !== "undefined" &&
526
+ typeof window.document !== "undefined" &&
527
+ typeof window.document.createElement !== "undefined";
528
+ const isServer = !isBrowser;
498
529
  //#endregion
499
530
 
500
531
  ////////////////////////////////////////////////////////////////////////////////
@@ -733,6 +764,14 @@ export function createRouter(init: RouterInit): Router {
733
764
  let { path, submission, error } = normalizeNavigateOptions(to, opts);
734
765
 
735
766
  let location = createLocation(state.location, path, opts && opts.state);
767
+
768
+ // When using navigate as a PUSH/REPLACE we aren't reading an already-encoded
769
+ // URL from window.location, so we need to encode it here so the behavior
770
+ // remains the same as POP and non-data-router usages. new URL() does all
771
+ // the same encoding we'd get from a history.pushState/window.location read
772
+ // without having to touch history
773
+ location = init.history.encodeLocation(location);
774
+
736
775
  let historyAction =
737
776
  (opts && opts.replace) === true || submission != null
738
777
  ? HistoryAction.Replace
@@ -939,7 +978,13 @@ export function createRouter(init: RouterInit): Router {
939
978
  if (!actionMatch.route.action) {
940
979
  result = getMethodNotAllowedResult(location);
941
980
  } else {
942
- result = await callLoaderOrAction("action", request, actionMatch);
981
+ result = await callLoaderOrAction(
982
+ "action",
983
+ request,
984
+ actionMatch,
985
+ matches,
986
+ router.basename
987
+ );
943
988
 
944
989
  if (request.signal.aborted) {
945
990
  return { shortCircuited: true };
@@ -1053,7 +1098,7 @@ export function createRouter(init: RouterInit): Router {
1053
1098
  // a revalidation interrupting an actionReload)
1054
1099
  if (!isUninterruptedRevalidation) {
1055
1100
  revalidatingFetchers.forEach(([key]) => {
1056
- const fetcher = state.fetchers.get(key);
1101
+ let fetcher = state.fetchers.get(key);
1057
1102
  let revalidatingFetcher: FetcherStates["Loading"] = {
1058
1103
  state: "loading",
1059
1104
  data: fetcher && fetcher.data,
@@ -1081,6 +1126,7 @@ export function createRouter(init: RouterInit): Router {
1081
1126
  let { results, loaderResults, fetcherResults } =
1082
1127
  await callLoadersAndMaybeResolveData(
1083
1128
  state.matches,
1129
+ matches,
1084
1130
  matchesToLoad,
1085
1131
  revalidatingFetchers,
1086
1132
  request
@@ -1150,7 +1196,7 @@ export function createRouter(init: RouterInit): Router {
1150
1196
  href: string,
1151
1197
  opts?: RouterFetchOptions
1152
1198
  ) {
1153
- if (typeof AbortController === "undefined") {
1199
+ if (isServer) {
1154
1200
  throw new Error(
1155
1201
  "router.fetch() was called during the server render, but it shouldn't be. " +
1156
1202
  "You are likely calling a useFetcher() method in the body of your component. " +
@@ -1170,14 +1216,14 @@ export function createRouter(init: RouterInit): Router {
1170
1216
  let match = getTargetMatch(matches, path);
1171
1217
 
1172
1218
  if (submission) {
1173
- handleFetcherAction(key, routeId, path, match, submission);
1219
+ handleFetcherAction(key, routeId, path, match, matches, submission);
1174
1220
  return;
1175
1221
  }
1176
1222
 
1177
1223
  // Store off the match so we can call it's shouldRevalidate on subsequent
1178
1224
  // revalidations
1179
- fetchLoadMatches.set(key, [path, match]);
1180
- handleFetcherLoader(key, routeId, path, match);
1225
+ fetchLoadMatches.set(key, [path, match, matches]);
1226
+ handleFetcherLoader(key, routeId, path, match, matches);
1181
1227
  }
1182
1228
 
1183
1229
  // Call the action for the matched fetcher.submit(), and then handle redirects,
@@ -1187,6 +1233,7 @@ export function createRouter(init: RouterInit): Router {
1187
1233
  routeId: string,
1188
1234
  path: string,
1189
1235
  match: AgnosticDataRouteMatch,
1236
+ requestMatches: AgnosticDataRouteMatch[],
1190
1237
  submission: Submission
1191
1238
  ) {
1192
1239
  interruptActiveLoads();
@@ -1213,7 +1260,13 @@ export function createRouter(init: RouterInit): Router {
1213
1260
  let fetchRequest = createRequest(path, abortController.signal, submission);
1214
1261
  fetchControllers.set(key, abortController);
1215
1262
 
1216
- let actionResult = await callLoaderOrAction("action", fetchRequest, match);
1263
+ let actionResult = await callLoaderOrAction(
1264
+ "action",
1265
+ fetchRequest,
1266
+ match,
1267
+ requestMatches,
1268
+ router.basename
1269
+ );
1217
1270
 
1218
1271
  if (fetchRequest.signal.aborted) {
1219
1272
  // We can delete this so long as we weren't aborted by ou our own fetcher
@@ -1315,6 +1368,7 @@ export function createRouter(init: RouterInit): Router {
1315
1368
  let { results, loaderResults, fetcherResults } =
1316
1369
  await callLoadersAndMaybeResolveData(
1317
1370
  state.matches,
1371
+ matches,
1318
1372
  matchesToLoad,
1319
1373
  revalidatingFetchers,
1320
1374
  revalidationRequest
@@ -1395,7 +1449,8 @@ export function createRouter(init: RouterInit): Router {
1395
1449
  key: string,
1396
1450
  routeId: string,
1397
1451
  path: string,
1398
- match: AgnosticDataRouteMatch
1452
+ match: AgnosticDataRouteMatch,
1453
+ matches: AgnosticDataRouteMatch[]
1399
1454
  ) {
1400
1455
  let existingFetcher = state.fetchers.get(key);
1401
1456
  // Put this fetcher into it's loading state
@@ -1417,7 +1472,9 @@ export function createRouter(init: RouterInit): Router {
1417
1472
  let result: DataResult = await callLoaderOrAction(
1418
1473
  "loader",
1419
1474
  fetchRequest,
1420
- match
1475
+ match,
1476
+ matches,
1477
+ router.basename
1421
1478
  );
1422
1479
 
1423
1480
  // Deferred isn't supported or fetcher loads, await everything and treat it
@@ -1515,6 +1572,7 @@ export function createRouter(init: RouterInit): Router {
1515
1572
 
1516
1573
  let redirectHistoryAction =
1517
1574
  replace === true ? HistoryAction.Replace : HistoryAction.Push;
1575
+
1518
1576
  await startNavigation(redirectHistoryAction, navigation.location, {
1519
1577
  overrideNavigation: navigation,
1520
1578
  });
@@ -1522,6 +1580,7 @@ export function createRouter(init: RouterInit): Router {
1522
1580
 
1523
1581
  async function callLoadersAndMaybeResolveData(
1524
1582
  currentMatches: AgnosticDataRouteMatch[],
1583
+ matches: AgnosticDataRouteMatch[],
1525
1584
  matchesToLoad: AgnosticDataRouteMatch[],
1526
1585
  fetchersToLoad: RevalidatingFetcher[],
1527
1586
  request: Request
@@ -1530,9 +1589,17 @@ export function createRouter(init: RouterInit): Router {
1530
1589
  // then slice off the results into separate arrays so we can handle them
1531
1590
  // accordingly
1532
1591
  let results = await Promise.all([
1533
- ...matchesToLoad.map((m) => callLoaderOrAction("loader", request, m)),
1534
- ...fetchersToLoad.map(([, href, match]) =>
1535
- callLoaderOrAction("loader", createRequest(href, request.signal), match)
1592
+ ...matchesToLoad.map((match) =>
1593
+ callLoaderOrAction("loader", request, match, matches, router.basename)
1594
+ ),
1595
+ ...fetchersToLoad.map(([, href, match, fetchMatches]) =>
1596
+ callLoaderOrAction(
1597
+ "loader",
1598
+ createRequest(href, request.signal),
1599
+ match,
1600
+ fetchMatches,
1601
+ router.basename
1602
+ )
1536
1603
  ),
1537
1604
  ]);
1538
1605
  let loaderResults = results.slice(0, matchesToLoad.length);
@@ -1739,7 +1806,9 @@ export function createRouter(init: RouterInit): Router {
1739
1806
  navigate,
1740
1807
  fetch,
1741
1808
  revalidate,
1742
- createHref,
1809
+ // Passthrough to history-aware createHref used by useHref so we get proper
1810
+ // hash-aware URLs in DOM paths
1811
+ createHref: (to: To) => init.history.createHref(to),
1743
1812
  getFetcher,
1744
1813
  deleteFetcher,
1745
1814
  dispose,
@@ -1755,6 +1824,9 @@ export function createRouter(init: RouterInit): Router {
1755
1824
  //#region createStaticHandler
1756
1825
  ////////////////////////////////////////////////////////////////////////////////
1757
1826
 
1827
+ const validActionMethods = new Set(["POST", "PUT", "PATCH", "DELETE"]);
1828
+ const validRequestMethods = new Set(["GET", "HEAD", ...validActionMethods]);
1829
+
1758
1830
  export function unstable_createStaticHandler(
1759
1831
  routes: AgnosticRouteObject[]
1760
1832
  ): StaticHandler {
@@ -1765,37 +1837,133 @@ export function unstable_createStaticHandler(
1765
1837
 
1766
1838
  let dataRoutes = convertRoutesToDataRoutes(routes);
1767
1839
 
1840
+ /**
1841
+ * The query() method is intended for document requests, in which we want to
1842
+ * call an optional action and potentially multiple loaders for all nested
1843
+ * routes. It returns a StaticHandlerContext object, which is very similar
1844
+ * to the router state (location, loaderData, actionData, errors, etc.) and
1845
+ * also adds SSR-specific information such as the statusCode and headers
1846
+ * from action/loaders Responses.
1847
+ *
1848
+ * It _should_ never throw and should report all errors through the
1849
+ * returned context.errors object, properly associating errors to their error
1850
+ * boundary. Additionally, it tracks _deepestRenderedBoundaryId which can be
1851
+ * used to emulate React error boundaries during SSr by performing a second
1852
+ * pass only down to the boundaryId.
1853
+ *
1854
+ * The one exception where we do not return a StaticHandlerContext is when a
1855
+ * redirect response is returned or thrown from any action/loader. We
1856
+ * propagate that out and return the raw Response so the HTTP server can
1857
+ * return it directly.
1858
+ */
1768
1859
  async function query(
1769
1860
  request: Request
1770
1861
  ): Promise<StaticHandlerContext | Response> {
1771
- let { location, result } = await queryImpl(request);
1862
+ let url = new URL(request.url);
1863
+ let location = createLocation("", createPath(url), null, "default");
1864
+ let matches = matchRoutes(dataRoutes, location);
1865
+
1866
+ if (!validRequestMethods.has(request.method)) {
1867
+ let {
1868
+ matches: methodNotAllowedMatches,
1869
+ route,
1870
+ error,
1871
+ } = getMethodNotAllowedMatches(dataRoutes);
1872
+ return {
1873
+ location,
1874
+ matches: methodNotAllowedMatches,
1875
+ loaderData: {},
1876
+ actionData: null,
1877
+ errors: {
1878
+ [route.id]: error,
1879
+ },
1880
+ statusCode: error.status,
1881
+ loaderHeaders: {},
1882
+ actionHeaders: {},
1883
+ };
1884
+ } else if (!matches) {
1885
+ let {
1886
+ matches: notFoundMatches,
1887
+ route,
1888
+ error,
1889
+ } = getNotFoundMatches(dataRoutes);
1890
+ return {
1891
+ location,
1892
+ matches: notFoundMatches,
1893
+ loaderData: {},
1894
+ actionData: null,
1895
+ errors: {
1896
+ [route.id]: error,
1897
+ },
1898
+ statusCode: error.status,
1899
+ loaderHeaders: {},
1900
+ actionHeaders: {},
1901
+ };
1902
+ }
1903
+
1904
+ let result = await queryImpl(request, location, matches);
1772
1905
  if (result instanceof Response) {
1773
1906
  return result;
1774
1907
  }
1908
+
1775
1909
  // When returning StaticHandlerContext, we patch back in the location here
1776
1910
  // since we need it for React Context. But this helps keep our submit and
1777
1911
  // loadRouteData operating on a Request instead of a Location
1778
1912
  return { location, ...result };
1779
1913
  }
1780
1914
 
1781
- async function queryRoute(request: Request, routeId: string): Promise<any> {
1782
- let { result } = await queryImpl(request, routeId);
1915
+ /**
1916
+ * The queryRoute() method is intended for targeted route requests, either
1917
+ * for fetch ?_data requests or resource route requests. In this case, we
1918
+ * are only ever calling a single action or loader, and we are returning the
1919
+ * returned value directly. In most cases, this will be a Response returned
1920
+ * from the action/loader, but it may be a primitive or other value as well -
1921
+ * and in such cases the calling context should handle that accordingly.
1922
+ *
1923
+ * We do respect the throw/return differentiation, so if an action/loader
1924
+ * throws, then this method will throw the value. This is important so we
1925
+ * can do proper boundary identification in Remix where a thrown Response
1926
+ * must go to the Catch Boundary but a returned Response is happy-path.
1927
+ *
1928
+ * One thing to note is that any Router-initiated thrown Response (such as a
1929
+ * 404 or 405) will have a custom X-Remix-Router-Error: "yes" header on it
1930
+ * in order to differentiate from responses thrown from user actions/loaders.
1931
+ */
1932
+ async function queryRoute(request: Request, routeId?: string): Promise<any> {
1933
+ let url = new URL(request.url);
1934
+ let location = createLocation("", createPath(url), null, "default");
1935
+ let matches = matchRoutes(dataRoutes, location);
1936
+
1937
+ if (!validRequestMethods.has(request.method)) {
1938
+ throw createRouterErrorResponse(null, {
1939
+ status: 405,
1940
+ statusText: "Method Not Allowed",
1941
+ });
1942
+ } else if (!matches) {
1943
+ throw createRouterErrorResponse(null, {
1944
+ status: 404,
1945
+ statusText: "Not Found",
1946
+ });
1947
+ }
1948
+
1949
+ let match = routeId
1950
+ ? matches.find((m) => m.route.id === routeId)
1951
+ : getTargetMatch(matches, location);
1952
+
1953
+ if (!match) {
1954
+ throw createRouterErrorResponse(null, {
1955
+ status: 404,
1956
+ statusText: "Not Found",
1957
+ });
1958
+ }
1959
+
1960
+ let result = await queryImpl(request, location, matches, match);
1783
1961
  if (result instanceof Response) {
1784
1962
  return result;
1785
1963
  }
1786
1964
 
1787
1965
  let error = result.errors ? Object.values(result.errors)[0] : undefined;
1788
1966
  if (error !== undefined) {
1789
- // While we always re-throw Responses returned from loaders/actions
1790
- // directly for route requests and prevent the unwrapping into an
1791
- // ErrorResponse, we still need this for error cases _prior_ the
1792
- // execution of the loader/action, such as a 404/405 error.
1793
- if (isRouteErrorResponse(error)) {
1794
- return new Response(error.data, {
1795
- status: error.status,
1796
- statusText: error.statusText,
1797
- });
1798
- }
1799
1967
  // If we got back result.errors, that means the loader/action threw
1800
1968
  // _something_ that wasn't a Response, but it's not guaranteed/required
1801
1969
  // to be an `instanceof Error` either, so we have to use throw here to
@@ -1805,66 +1973,53 @@ export function unstable_createStaticHandler(
1805
1973
 
1806
1974
  // Pick off the right state value to return
1807
1975
  let routeData = [result.actionData, result.loaderData].find((v) => v);
1808
- let value = Object.values(routeData || {})[0];
1809
-
1810
- if (isRouteErrorResponse(value)) {
1811
- return new Response(value.data, {
1812
- status: value.status,
1813
- statusText: value.statusText,
1814
- });
1815
- }
1816
-
1817
- return value;
1976
+ return Object.values(routeData || {})[0];
1818
1977
  }
1819
1978
 
1820
1979
  async function queryImpl(
1821
1980
  request: Request,
1822
- routeId?: string
1823
- ): Promise<{
1824
- location: Location;
1825
- result: Omit<StaticHandlerContext, "location"> | Response;
1826
- }> {
1827
- invariant(
1828
- request.method !== "HEAD",
1829
- "query()/queryRoute() do not support HEAD requests"
1830
- );
1981
+ location: Location,
1982
+ matches: AgnosticDataRouteMatch[],
1983
+ routeMatch?: AgnosticDataRouteMatch
1984
+ ): Promise<Omit<StaticHandlerContext, "location"> | Response> {
1831
1985
  invariant(
1832
1986
  request.signal,
1833
1987
  "query()/queryRoute() requests must contain an AbortController signal"
1834
1988
  );
1835
1989
 
1836
- let { location, matches, shortCircuitState } = matchRequest(
1837
- request,
1838
- routeId
1839
- );
1840
-
1841
1990
  try {
1842
- if (shortCircuitState) {
1843
- return { location, result: shortCircuitState };
1844
- }
1845
-
1846
- if (request.method !== "GET") {
1991
+ if (validActionMethods.has(request.method)) {
1847
1992
  let result = await submit(
1848
1993
  request,
1849
1994
  matches,
1850
- getTargetMatch(matches, location),
1851
- routeId != null
1995
+ routeMatch || getTargetMatch(matches, location),
1996
+ routeMatch != null
1852
1997
  );
1853
- return { location, result };
1998
+ return result;
1854
1999
  }
1855
2000
 
1856
- let result = await loadRouteData(request, matches, routeId != null);
1857
- return {
1858
- location,
1859
- result: {
1860
- ...result,
1861
- actionData: null,
1862
- actionHeaders: {},
1863
- },
1864
- };
2001
+ let result = await loadRouteData(request, matches, routeMatch);
2002
+ return result instanceof Response
2003
+ ? result
2004
+ : {
2005
+ ...result,
2006
+ actionData: null,
2007
+ actionHeaders: {},
2008
+ };
1865
2009
  } catch (e) {
1866
- if (e instanceof Response) {
1867
- return { location, result: e };
2010
+ // If the user threw/returned a Response in callLoaderOrAction, we throw
2011
+ // it to bail out and then return or throw here based on whether the user
2012
+ // returned or threw
2013
+ if (isQueryRouteResponse(e)) {
2014
+ if (e.type === ResultType.error && !isRedirectResponse(e.response)) {
2015
+ throw e.response;
2016
+ }
2017
+ return e.response;
2018
+ }
2019
+ // Redirects are always returned since they don't propagate to catch
2020
+ // boundaries
2021
+ if (isRedirectResponse(e)) {
2022
+ return e;
1868
2023
  }
1869
2024
  throw e;
1870
2025
  }
@@ -1878,13 +2033,20 @@ export function unstable_createStaticHandler(
1878
2033
  ): Promise<Omit<StaticHandlerContext, "location"> | Response> {
1879
2034
  let result: DataResult;
1880
2035
  if (!actionMatch.route.action) {
1881
- let href = createHref(new URL(request.url));
1882
- result = getMethodNotAllowedResult(href);
2036
+ if (isRouteRequest) {
2037
+ throw createRouterErrorResponse(null, {
2038
+ status: 405,
2039
+ statusText: "Method Not Allowed",
2040
+ });
2041
+ }
2042
+ result = getMethodNotAllowedResult(request.url);
1883
2043
  } else {
1884
2044
  result = await callLoaderOrAction(
1885
2045
  "action",
1886
2046
  request,
1887
2047
  actionMatch,
2048
+ matches,
2049
+ undefined, // Basename not currently supported in static handlers
1888
2050
  true,
1889
2051
  isRouteRequest
1890
2052
  );
@@ -1897,7 +2059,7 @@ export function unstable_createStaticHandler(
1897
2059
 
1898
2060
  if (isRedirectResult(result)) {
1899
2061
  // Uhhhh - this should never happen, we should always throw these from
1900
- // calLoaderOrAction, but the type narrowing here keeps TS happy and we
2062
+ // callLoaderOrAction, but the type narrowing here keeps TS happy and we
1901
2063
  // can get back on the "throw all redirect responses" train here should
1902
2064
  // this ever happen :/
1903
2065
  throw new Response(null, {
@@ -1913,6 +2075,8 @@ export function unstable_createStaticHandler(
1913
2075
  }
1914
2076
 
1915
2077
  if (isRouteRequest) {
2078
+ // Note: This should only be non-Response values if we get here, since
2079
+ // isRouteRequest should throw any Response received in callLoaderOrAction
1916
2080
  if (isErrorResult(result)) {
1917
2081
  let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
1918
2082
  return {
@@ -1947,7 +2111,7 @@ export function unstable_createStaticHandler(
1947
2111
  // Store off the pending error - we use it to determine which loaders
1948
2112
  // to call and will commit it when we complete the navigation
1949
2113
  let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
1950
- let context = await loadRouteData(request, matches, isRouteRequest, {
2114
+ let context = await loadRouteData(request, matches, undefined, {
1951
2115
  [boundaryMatch.route.id]: result.error,
1952
2116
  });
1953
2117
 
@@ -1964,7 +2128,7 @@ export function unstable_createStaticHandler(
1964
2128
  };
1965
2129
  }
1966
2130
 
1967
- let context = await loadRouteData(request, matches, isRouteRequest);
2131
+ let context = await loadRouteData(request, matches);
1968
2132
 
1969
2133
  return {
1970
2134
  ...context,
@@ -1982,16 +2146,20 @@ export function unstable_createStaticHandler(
1982
2146
  async function loadRouteData(
1983
2147
  request: Request,
1984
2148
  matches: AgnosticDataRouteMatch[],
1985
- isRouteRequest: boolean,
2149
+ routeMatch?: AgnosticDataRouteMatch,
1986
2150
  pendingActionError?: RouteData
1987
2151
  ): Promise<
1988
2152
  | Omit<StaticHandlerContext, "location" | "actionData" | "actionHeaders">
1989
2153
  | Response
1990
2154
  > {
1991
- let matchesToLoad = getLoaderMatchesUntilBoundary(
1992
- matches,
1993
- Object.keys(pendingActionError || {})[0]
1994
- ).filter((m) => m.route.loader);
2155
+ let isRouteRequest = routeMatch != null;
2156
+ let requestMatches = routeMatch
2157
+ ? [routeMatch]
2158
+ : getLoaderMatchesUntilBoundary(
2159
+ matches,
2160
+ Object.keys(pendingActionError || {})[0]
2161
+ );
2162
+ let matchesToLoad = requestMatches.filter((m) => m.route.loader);
1995
2163
 
1996
2164
  // Short circuit if we have no loaders to run
1997
2165
  if (matchesToLoad.length === 0) {
@@ -2005,8 +2173,16 @@ export function unstable_createStaticHandler(
2005
2173
  }
2006
2174
 
2007
2175
  let results = await Promise.all([
2008
- ...matchesToLoad.map((m) =>
2009
- callLoaderOrAction("loader", request, m, true, isRouteRequest)
2176
+ ...matchesToLoad.map((match) =>
2177
+ callLoaderOrAction(
2178
+ "loader",
2179
+ request,
2180
+ match,
2181
+ matches,
2182
+ undefined, // Basename not currently supported in static handlers
2183
+ true,
2184
+ isRouteRequest
2185
+ )
2010
2186
  ),
2011
2187
  ]);
2012
2188
 
@@ -2037,47 +2213,17 @@ export function unstable_createStaticHandler(
2037
2213
  };
2038
2214
  }
2039
2215
 
2040
- function matchRequest(
2041
- req: Request,
2042
- routeId?: string
2043
- ): {
2044
- location: Location;
2045
- matches: AgnosticDataRouteMatch[];
2046
- routeMatch?: AgnosticDataRouteMatch;
2047
- shortCircuitState?: Omit<StaticHandlerContext, "location">;
2048
- } {
2049
- let url = new URL(req.url);
2050
- let location = createLocation("", createPath(url), null, "default");
2051
- let matches = matchRoutes(dataRoutes, location);
2052
- if (matches && routeId) {
2053
- matches = matches.filter((m) => m.route.id === routeId);
2054
- }
2055
-
2056
- // Short circuit with a 404 if we match nothing
2057
- if (!matches) {
2058
- let {
2059
- matches: notFoundMatches,
2060
- route,
2061
- error,
2062
- } = getNotFoundMatches(dataRoutes);
2063
- return {
2064
- location,
2065
- matches: notFoundMatches,
2066
- shortCircuitState: {
2067
- matches: notFoundMatches,
2068
- loaderData: {},
2069
- actionData: null,
2070
- errors: {
2071
- [route.id]: error,
2072
- },
2073
- statusCode: 404,
2074
- loaderHeaders: {},
2075
- actionHeaders: {},
2076
- },
2077
- };
2078
- }
2079
-
2080
- return { location, matches };
2216
+ function createRouterErrorResponse(
2217
+ body: BodyInit | null | undefined,
2218
+ init: ResponseInit
2219
+ ) {
2220
+ return new Response(body, {
2221
+ ...init,
2222
+ headers: {
2223
+ ...init.headers,
2224
+ "X-Remix-Router-Error": "yes",
2225
+ },
2226
+ });
2081
2227
  }
2082
2228
 
2083
2229
  return {
@@ -2136,7 +2282,7 @@ function normalizeNavigateOptions(
2136
2282
  path,
2137
2283
  submission: {
2138
2284
  formMethod: opts.formMethod,
2139
- formAction: createHref(parsePath(path)),
2285
+ formAction: stripHashFromPath(path),
2140
2286
  formEncType:
2141
2287
  (opts && opts.formEncType) || "application/x-www-form-urlencoded",
2142
2288
  formData: opts.formData,
@@ -2251,10 +2397,10 @@ function getMatchesToLoad(
2251
2397
  // Pick fetcher.loads that need to be revalidated
2252
2398
  let revalidatingFetchers: RevalidatingFetcher[] = [];
2253
2399
  fetchLoadMatches &&
2254
- fetchLoadMatches.forEach(([href, match], key) => {
2400
+ fetchLoadMatches.forEach(([href, match, fetchMatches], key) => {
2255
2401
  // This fetcher was cancelled from a prior action submission - force reload
2256
2402
  if (cancelledFetcherLoads.includes(key)) {
2257
- revalidatingFetchers.push([key, href, match]);
2403
+ revalidatingFetchers.push([key, href, match, fetchMatches]);
2258
2404
  } else if (isRevalidationRequired) {
2259
2405
  let shouldRevalidate = shouldRevalidateLoader(
2260
2406
  href,
@@ -2266,7 +2412,7 @@ function getMatchesToLoad(
2266
2412
  actionResult
2267
2413
  );
2268
2414
  if (shouldRevalidate) {
2269
- revalidatingFetchers.push([key, href, match]);
2415
+ revalidatingFetchers.push([key, href, match, fetchMatches]);
2270
2416
  }
2271
2417
  }
2272
2418
  });
@@ -2360,7 +2506,9 @@ async function callLoaderOrAction(
2360
2506
  type: "loader" | "action",
2361
2507
  request: Request,
2362
2508
  match: AgnosticDataRouteMatch,
2363
- skipRedirects: boolean = false,
2509
+ matches: AgnosticDataRouteMatch[],
2510
+ basename: string | undefined,
2511
+ isStaticRequest: boolean = false,
2364
2512
  isRouteRequest: boolean = false
2365
2513
  ): Promise<DataResult> {
2366
2514
  let resultType;
@@ -2391,23 +2539,46 @@ async function callLoaderOrAction(
2391
2539
  }
2392
2540
 
2393
2541
  if (result instanceof Response) {
2394
- // Process redirects
2395
2542
  let status = result.status;
2396
- let location = result.headers.get("Location");
2397
2543
 
2398
- // For SSR single-route requests, we want to hand Responses back directly
2399
- // without unwrapping
2400
- if (isRouteRequest) {
2401
- throw result;
2402
- }
2544
+ // Process redirects
2545
+ if (status >= 300 && status <= 399) {
2546
+ let location = result.headers.get("Location");
2547
+ invariant(
2548
+ location,
2549
+ "Redirects returned/thrown from loaders/actions must have a Location header"
2550
+ );
2551
+
2552
+ // Support relative routing in redirects
2553
+ let activeMatches = matches.slice(0, matches.indexOf(match) + 1);
2554
+ let routePathnames = getPathContributingMatches(activeMatches).map(
2555
+ (match) => match.pathnameBase
2556
+ );
2557
+ let requestPath = createURL(request.url).pathname;
2558
+ let resolvedLocation = resolveTo(location, routePathnames, requestPath);
2559
+ invariant(
2560
+ createPath(resolvedLocation),
2561
+ `Unable to resolve redirect location: ${result.headers.get("Location")}`
2562
+ );
2563
+
2564
+ // Prepend the basename to the redirect location if we have one
2565
+ if (basename) {
2566
+ let path = resolvedLocation.pathname;
2567
+ resolvedLocation.pathname =
2568
+ path === "/" ? basename : joinPaths([basename, path]);
2569
+ }
2570
+
2571
+ location = createPath(resolvedLocation);
2403
2572
 
2404
- if (status >= 300 && status <= 399 && location != null) {
2405
- // Don't process redirects in the router during SSR document requests.
2573
+ // Don't process redirects in the router during static requests requests.
2406
2574
  // Instead, throw the Response and let the server handle it with an HTTP
2407
- // redirect
2408
- if (skipRedirects) {
2575
+ // redirect. We also update the Location header in place in this flow so
2576
+ // basename and relative routing is taken into account
2577
+ if (isStaticRequest) {
2578
+ result.headers.set("Location", location);
2409
2579
  throw result;
2410
2580
  }
2581
+
2411
2582
  return {
2412
2583
  type: ResultType.redirect,
2413
2584
  status,
@@ -2416,6 +2587,17 @@ async function callLoaderOrAction(
2416
2587
  };
2417
2588
  }
2418
2589
 
2590
+ // For SSR single-route requests, we want to hand Responses back directly
2591
+ // without unwrapping. We do this with the QueryRouteResponse wrapper
2592
+ // interface so we can know whether it was returned or thrown
2593
+ if (isRouteRequest) {
2594
+ // eslint-disable-next-line no-throw-literal
2595
+ throw {
2596
+ type: resultType || ResultType.data,
2597
+ response: result,
2598
+ };
2599
+ }
2600
+
2419
2601
  let data: any;
2420
2602
  let contentType = result.headers.get("Content-Type");
2421
2603
  if (contentType && contentType.startsWith("application/json")) {
@@ -2456,7 +2638,7 @@ function createRequest(
2456
2638
  signal: AbortSignal,
2457
2639
  submission?: Submission
2458
2640
  ): Request {
2459
- let url = createURL(location).toString();
2641
+ let url = createURL(stripHashFromPath(location)).toString();
2460
2642
  let init: RequestInit = { signal };
2461
2643
 
2462
2644
  if (submission) {
@@ -2669,16 +2851,18 @@ function findNearestBoundary(
2669
2851
  );
2670
2852
  }
2671
2853
 
2672
- function getNotFoundMatches(routes: AgnosticDataRouteObject[]): {
2854
+ function getShortCircuitMatches(
2855
+ routes: AgnosticDataRouteObject[],
2856
+ status: number,
2857
+ statusText: string
2858
+ ): {
2673
2859
  matches: AgnosticDataRouteMatch[];
2674
2860
  route: AgnosticDataRouteObject;
2675
2861
  error: ErrorResponse;
2676
2862
  } {
2677
2863
  // Prefer a root layout route if present, otherwise shim in a route object
2678
- let route = routes.find(
2679
- (r) => r.index || r.path === "" || r.path === "/"
2680
- ) || {
2681
- id: "__shim-404-route__",
2864
+ let route = routes.find((r) => r.index || !r.path || r.path === "/") || {
2865
+ id: `__shim-${status}-route__`,
2682
2866
  };
2683
2867
 
2684
2868
  return {
@@ -2691,12 +2875,20 @@ function getNotFoundMatches(routes: AgnosticDataRouteObject[]): {
2691
2875
  },
2692
2876
  ],
2693
2877
  route,
2694
- error: new ErrorResponse(404, "Not Found", null),
2878
+ error: new ErrorResponse(status, statusText, null),
2695
2879
  };
2696
2880
  }
2697
2881
 
2882
+ function getNotFoundMatches(routes: AgnosticDataRouteObject[]) {
2883
+ return getShortCircuitMatches(routes, 404, "Not Found");
2884
+ }
2885
+
2886
+ function getMethodNotAllowedMatches(routes: AgnosticDataRouteObject[]) {
2887
+ return getShortCircuitMatches(routes, 405, "Method Not Allowed");
2888
+ }
2889
+
2698
2890
  function getMethodNotAllowedResult(path: Location | string): ErrorResult {
2699
- let href = typeof path === "string" ? path : createHref(path);
2891
+ let href = typeof path === "string" ? path : createPath(path);
2700
2892
  console.warn(
2701
2893
  "You're trying to submit to a route that does not have an action. To " +
2702
2894
  "fix this, please add an `action` function to the route for " +
@@ -2704,11 +2896,7 @@ function getMethodNotAllowedResult(path: Location | string): ErrorResult {
2704
2896
  );
2705
2897
  return {
2706
2898
  type: ResultType.error,
2707
- error: new ErrorResponse(
2708
- 405,
2709
- "Method Not Allowed",
2710
- `No action found for [${href}]`
2711
- ),
2899
+ error: new ErrorResponse(405, "Method Not Allowed", ""),
2712
2900
  };
2713
2901
  }
2714
2902
 
@@ -2722,9 +2910,9 @@ function findRedirect(results: DataResult[]): RedirectResult | undefined {
2722
2910
  }
2723
2911
  }
2724
2912
 
2725
- // Create an href to represent a "server" URL without the hash
2726
- function createHref(location: Partial<Path> | Location | URL) {
2727
- return (location.pathname || "") + (location.search || "");
2913
+ function stripHashFromPath(path: To) {
2914
+ let parsedPath = typeof path === "string" ? parsePath(path) : path;
2915
+ return createPath({ ...parsedPath, hash: "" });
2728
2916
  }
2729
2917
 
2730
2918
  function isHashChangeOnly(a: Location, b: Location): boolean {
@@ -2745,6 +2933,24 @@ function isRedirectResult(result?: DataResult): result is RedirectResult {
2745
2933
  return (result && result.type) === ResultType.redirect;
2746
2934
  }
2747
2935
 
2936
+ function isRedirectResponse(result: any): result is Response {
2937
+ if (!(result instanceof Response)) {
2938
+ return false;
2939
+ }
2940
+
2941
+ let status = result.status;
2942
+ let location = result.headers.get("Location");
2943
+ return status >= 300 && status <= 399 && location != null;
2944
+ }
2945
+
2946
+ function isQueryRouteResponse(obj: any): obj is QueryRouteResponse {
2947
+ return (
2948
+ obj &&
2949
+ obj.response instanceof Response &&
2950
+ (obj.type === ResultType.data || ResultType.error)
2951
+ );
2952
+ }
2953
+
2748
2954
  async function resolveDeferredResults(
2749
2955
  currentMatches: AgnosticDataRouteMatch[],
2750
2956
  matchesToLoad: AgnosticDataRouteMatch[],
@@ -2836,19 +3042,14 @@ function getTargetMatch(
2836
3042
  typeof location === "string" ? parsePath(location).search : location.search;
2837
3043
  if (
2838
3044
  matches[matches.length - 1].route.index &&
2839
- !hasNakedIndexQuery(search || "")
3045
+ hasNakedIndexQuery(search || "")
2840
3046
  ) {
2841
- return matches.slice(-2)[0];
3047
+ // Return the leaf index route when index is present
3048
+ return matches[matches.length - 1];
2842
3049
  }
2843
- return matches.slice(-1)[0];
2844
- }
2845
-
2846
- function createURL(location: Location | string): URL {
2847
- let base =
2848
- typeof window !== "undefined" && typeof window.location !== "undefined"
2849
- ? window.location.origin
2850
- : "unknown://unknown";
2851
- let href = typeof location === "string" ? location : createHref(location);
2852
- return new URL(href, base);
3050
+ // Otherwise grab the deepest "path contributing" match (ignoring index and
3051
+ // pathless layout routes)
3052
+ let pathMatches = getPathContributingMatches(matches);
3053
+ return pathMatches[pathMatches.length - 1];
2853
3054
  }
2854
3055
  //#endregion