@remix-run/router 1.0.2 → 1.0.3-pre.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
@@ -25,9 +25,12 @@ import {
25
25
  ErrorResponse,
26
26
  ResultType,
27
27
  convertRoutesToDataRoutes,
28
+ getPathContributingMatches,
28
29
  invariant,
29
30
  isRouteErrorResponse,
31
+ joinPaths,
30
32
  matchRoutes,
33
+ resolveTo,
31
34
  } from "./utils";
32
35
 
33
36
  ////////////////////////////////////////////////////////////////////////////////
@@ -469,14 +472,35 @@ interface HandleLoadersResult extends ShortCircuitable {
469
472
  }
470
473
 
471
474
  /**
472
- * Tuple of [key, href, DataRouterMatch] for a revalidating fetcher.load()
475
+ * Tuple of [key, href, DataRouteMatch, DataRouteMatch[]] for a revalidating
476
+ * fetcher.load()
473
477
  */
474
- type RevalidatingFetcher = [string, string, AgnosticDataRouteMatch];
478
+ type RevalidatingFetcher = [
479
+ string,
480
+ string,
481
+ AgnosticDataRouteMatch,
482
+ AgnosticDataRouteMatch[]
483
+ ];
484
+
485
+ /**
486
+ * Tuple of [href, DataRouteMatch, DataRouteMatch[]] for an active
487
+ * fetcher.load()
488
+ */
489
+ type FetchLoadMatch = [
490
+ string,
491
+ AgnosticDataRouteMatch,
492
+ AgnosticDataRouteMatch[]
493
+ ];
475
494
 
476
495
  /**
477
- * Tuple of [href, DataRouteMatch] for an active fetcher.load()
496
+ * Wrapper object to allow us to throw any response out from callLoaderOrAction
497
+ * for queryRouter while preserving whether or not it was thrown or returned
498
+ * from the loader/action
478
499
  */
479
- type FetchLoadMatch = [string, AgnosticDataRouteMatch];
500
+ interface QueryRouteResponse {
501
+ type: ResultType.data | ResultType.error;
502
+ response: Response;
503
+ }
480
504
 
481
505
  export const IDLE_NAVIGATION: NavigationStates["Idle"] = {
482
506
  state: "idle",
@@ -495,6 +519,12 @@ export const IDLE_FETCHER: FetcherStates["Idle"] = {
495
519
  formEncType: undefined,
496
520
  formData: undefined,
497
521
  };
522
+
523
+ const isBrowser =
524
+ typeof window !== "undefined" &&
525
+ typeof window.document !== "undefined" &&
526
+ typeof window.document.createElement !== "undefined";
527
+ const isServer = !isBrowser;
498
528
  //#endregion
499
529
 
500
530
  ////////////////////////////////////////////////////////////////////////////////
@@ -733,6 +763,20 @@ export function createRouter(init: RouterInit): Router {
733
763
  let { path, submission, error } = normalizeNavigateOptions(to, opts);
734
764
 
735
765
  let location = createLocation(state.location, path, opts && opts.state);
766
+
767
+ // When using navigate as a PUSH/REPLACE we aren't reading an already-encoded
768
+ // URL from window.location, so we need to encode it here so the behavior
769
+ // remains the same as POP and non-data-router usages. new URL() does all
770
+ // the same encoding we'd get from a history.pushState/window.location read
771
+ // without having to touch history
772
+ let url = createURL(createPath(location));
773
+ location = {
774
+ ...location,
775
+ pathname: url.pathname,
776
+ search: url.search,
777
+ hash: url.hash,
778
+ };
779
+
736
780
  let historyAction =
737
781
  (opts && opts.replace) === true || submission != null
738
782
  ? HistoryAction.Replace
@@ -939,7 +983,13 @@ export function createRouter(init: RouterInit): Router {
939
983
  if (!actionMatch.route.action) {
940
984
  result = getMethodNotAllowedResult(location);
941
985
  } else {
942
- result = await callLoaderOrAction("action", request, actionMatch);
986
+ result = await callLoaderOrAction(
987
+ "action",
988
+ request,
989
+ actionMatch,
990
+ matches,
991
+ router.basename
992
+ );
943
993
 
944
994
  if (request.signal.aborted) {
945
995
  return { shortCircuited: true };
@@ -1053,7 +1103,7 @@ export function createRouter(init: RouterInit): Router {
1053
1103
  // a revalidation interrupting an actionReload)
1054
1104
  if (!isUninterruptedRevalidation) {
1055
1105
  revalidatingFetchers.forEach(([key]) => {
1056
- const fetcher = state.fetchers.get(key);
1106
+ let fetcher = state.fetchers.get(key);
1057
1107
  let revalidatingFetcher: FetcherStates["Loading"] = {
1058
1108
  state: "loading",
1059
1109
  data: fetcher && fetcher.data,
@@ -1081,6 +1131,7 @@ export function createRouter(init: RouterInit): Router {
1081
1131
  let { results, loaderResults, fetcherResults } =
1082
1132
  await callLoadersAndMaybeResolveData(
1083
1133
  state.matches,
1134
+ matches,
1084
1135
  matchesToLoad,
1085
1136
  revalidatingFetchers,
1086
1137
  request
@@ -1150,7 +1201,7 @@ export function createRouter(init: RouterInit): Router {
1150
1201
  href: string,
1151
1202
  opts?: RouterFetchOptions
1152
1203
  ) {
1153
- if (typeof AbortController === "undefined") {
1204
+ if (isServer) {
1154
1205
  throw new Error(
1155
1206
  "router.fetch() was called during the server render, but it shouldn't be. " +
1156
1207
  "You are likely calling a useFetcher() method in the body of your component. " +
@@ -1170,14 +1221,14 @@ export function createRouter(init: RouterInit): Router {
1170
1221
  let match = getTargetMatch(matches, path);
1171
1222
 
1172
1223
  if (submission) {
1173
- handleFetcherAction(key, routeId, path, match, submission);
1224
+ handleFetcherAction(key, routeId, path, match, matches, submission);
1174
1225
  return;
1175
1226
  }
1176
1227
 
1177
1228
  // Store off the match so we can call it's shouldRevalidate on subsequent
1178
1229
  // revalidations
1179
- fetchLoadMatches.set(key, [path, match]);
1180
- handleFetcherLoader(key, routeId, path, match);
1230
+ fetchLoadMatches.set(key, [path, match, matches]);
1231
+ handleFetcherLoader(key, routeId, path, match, matches);
1181
1232
  }
1182
1233
 
1183
1234
  // Call the action for the matched fetcher.submit(), and then handle redirects,
@@ -1187,6 +1238,7 @@ export function createRouter(init: RouterInit): Router {
1187
1238
  routeId: string,
1188
1239
  path: string,
1189
1240
  match: AgnosticDataRouteMatch,
1241
+ requestMatches: AgnosticDataRouteMatch[],
1190
1242
  submission: Submission
1191
1243
  ) {
1192
1244
  interruptActiveLoads();
@@ -1213,7 +1265,13 @@ export function createRouter(init: RouterInit): Router {
1213
1265
  let fetchRequest = createRequest(path, abortController.signal, submission);
1214
1266
  fetchControllers.set(key, abortController);
1215
1267
 
1216
- let actionResult = await callLoaderOrAction("action", fetchRequest, match);
1268
+ let actionResult = await callLoaderOrAction(
1269
+ "action",
1270
+ fetchRequest,
1271
+ match,
1272
+ requestMatches,
1273
+ router.basename
1274
+ );
1217
1275
 
1218
1276
  if (fetchRequest.signal.aborted) {
1219
1277
  // We can delete this so long as we weren't aborted by ou our own fetcher
@@ -1315,6 +1373,7 @@ export function createRouter(init: RouterInit): Router {
1315
1373
  let { results, loaderResults, fetcherResults } =
1316
1374
  await callLoadersAndMaybeResolveData(
1317
1375
  state.matches,
1376
+ matches,
1318
1377
  matchesToLoad,
1319
1378
  revalidatingFetchers,
1320
1379
  revalidationRequest
@@ -1395,7 +1454,8 @@ export function createRouter(init: RouterInit): Router {
1395
1454
  key: string,
1396
1455
  routeId: string,
1397
1456
  path: string,
1398
- match: AgnosticDataRouteMatch
1457
+ match: AgnosticDataRouteMatch,
1458
+ matches: AgnosticDataRouteMatch[]
1399
1459
  ) {
1400
1460
  let existingFetcher = state.fetchers.get(key);
1401
1461
  // Put this fetcher into it's loading state
@@ -1417,7 +1477,9 @@ export function createRouter(init: RouterInit): Router {
1417
1477
  let result: DataResult = await callLoaderOrAction(
1418
1478
  "loader",
1419
1479
  fetchRequest,
1420
- match
1480
+ match,
1481
+ matches,
1482
+ router.basename
1421
1483
  );
1422
1484
 
1423
1485
  // Deferred isn't supported or fetcher loads, await everything and treat it
@@ -1515,6 +1577,7 @@ export function createRouter(init: RouterInit): Router {
1515
1577
 
1516
1578
  let redirectHistoryAction =
1517
1579
  replace === true ? HistoryAction.Replace : HistoryAction.Push;
1580
+
1518
1581
  await startNavigation(redirectHistoryAction, navigation.location, {
1519
1582
  overrideNavigation: navigation,
1520
1583
  });
@@ -1522,6 +1585,7 @@ export function createRouter(init: RouterInit): Router {
1522
1585
 
1523
1586
  async function callLoadersAndMaybeResolveData(
1524
1587
  currentMatches: AgnosticDataRouteMatch[],
1588
+ matches: AgnosticDataRouteMatch[],
1525
1589
  matchesToLoad: AgnosticDataRouteMatch[],
1526
1590
  fetchersToLoad: RevalidatingFetcher[],
1527
1591
  request: Request
@@ -1530,9 +1594,17 @@ export function createRouter(init: RouterInit): Router {
1530
1594
  // then slice off the results into separate arrays so we can handle them
1531
1595
  // accordingly
1532
1596
  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)
1597
+ ...matchesToLoad.map((match) =>
1598
+ callLoaderOrAction("loader", request, match, matches, router.basename)
1599
+ ),
1600
+ ...fetchersToLoad.map(([, href, match, fetchMatches]) =>
1601
+ callLoaderOrAction(
1602
+ "loader",
1603
+ createRequest(href, request.signal),
1604
+ match,
1605
+ fetchMatches,
1606
+ router.basename
1607
+ )
1536
1608
  ),
1537
1609
  ]);
1538
1610
  let loaderResults = results.slice(0, matchesToLoad.length);
@@ -1739,7 +1811,9 @@ export function createRouter(init: RouterInit): Router {
1739
1811
  navigate,
1740
1812
  fetch,
1741
1813
  revalidate,
1742
- createHref,
1814
+ // Passthrough to history-aware createHref used by useHref so we get proper
1815
+ // hash-aware URLs in DOM paths
1816
+ createHref: (to: To) => init.history.createHref(to),
1743
1817
  getFetcher,
1744
1818
  deleteFetcher,
1745
1819
  dispose,
@@ -1755,6 +1829,9 @@ export function createRouter(init: RouterInit): Router {
1755
1829
  //#region createStaticHandler
1756
1830
  ////////////////////////////////////////////////////////////////////////////////
1757
1831
 
1832
+ const validActionMethods = new Set(["POST", "PUT", "PATCH", "DELETE"]);
1833
+ const validRequestMethods = new Set(["GET", "HEAD", ...validActionMethods]);
1834
+
1758
1835
  export function unstable_createStaticHandler(
1759
1836
  routes: AgnosticRouteObject[]
1760
1837
  ): StaticHandler {
@@ -1765,37 +1842,133 @@ export function unstable_createStaticHandler(
1765
1842
 
1766
1843
  let dataRoutes = convertRoutesToDataRoutes(routes);
1767
1844
 
1845
+ /**
1846
+ * The query() method is intended for document requests, in which we want to
1847
+ * call an optional action and potentially multiple loaders for all nested
1848
+ * routes. It returns a StaticHandlerContext object, which is very similar
1849
+ * to the router state (location, loaderData, actionData, errors, etc.) and
1850
+ * also adds SSR-specific information such as the statusCode and headers
1851
+ * from action/loaders Responses.
1852
+ *
1853
+ * It _should_ never throw and should report all errors through the
1854
+ * returned context.errors object, properly associating errors to their error
1855
+ * boundary. Additionally, it tracks _deepestRenderedBoundaryId which can be
1856
+ * used to emulate React error boundaries during SSr by performing a second
1857
+ * pass only down to the boundaryId.
1858
+ *
1859
+ * The one exception where we do not return a StaticHandlerContext is when a
1860
+ * redirect response is returned or thrown from any action/loader. We
1861
+ * propagate that out and return the raw Response so the HTTP server can
1862
+ * return it directly.
1863
+ */
1768
1864
  async function query(
1769
1865
  request: Request
1770
1866
  ): Promise<StaticHandlerContext | Response> {
1771
- let { location, result } = await queryImpl(request);
1867
+ let url = new URL(request.url);
1868
+ let location = createLocation("", createPath(url), null, "default");
1869
+ let matches = matchRoutes(dataRoutes, location);
1870
+
1871
+ if (!validRequestMethods.has(request.method)) {
1872
+ let {
1873
+ matches: methodNotAllowedMatches,
1874
+ route,
1875
+ error,
1876
+ } = getMethodNotAllowedMatches(dataRoutes);
1877
+ return {
1878
+ location,
1879
+ matches: methodNotAllowedMatches,
1880
+ loaderData: {},
1881
+ actionData: null,
1882
+ errors: {
1883
+ [route.id]: error,
1884
+ },
1885
+ statusCode: error.status,
1886
+ loaderHeaders: {},
1887
+ actionHeaders: {},
1888
+ };
1889
+ } else if (!matches) {
1890
+ let {
1891
+ matches: notFoundMatches,
1892
+ route,
1893
+ error,
1894
+ } = getNotFoundMatches(dataRoutes);
1895
+ return {
1896
+ location,
1897
+ matches: notFoundMatches,
1898
+ loaderData: {},
1899
+ actionData: null,
1900
+ errors: {
1901
+ [route.id]: error,
1902
+ },
1903
+ statusCode: error.status,
1904
+ loaderHeaders: {},
1905
+ actionHeaders: {},
1906
+ };
1907
+ }
1908
+
1909
+ let result = await queryImpl(request, location, matches);
1772
1910
  if (result instanceof Response) {
1773
1911
  return result;
1774
1912
  }
1913
+
1775
1914
  // When returning StaticHandlerContext, we patch back in the location here
1776
1915
  // since we need it for React Context. But this helps keep our submit and
1777
1916
  // loadRouteData operating on a Request instead of a Location
1778
1917
  return { location, ...result };
1779
1918
  }
1780
1919
 
1781
- async function queryRoute(request: Request, routeId: string): Promise<any> {
1782
- let { result } = await queryImpl(request, routeId);
1920
+ /**
1921
+ * The queryRoute() method is intended for targeted route requests, either
1922
+ * for fetch ?_data requests or resource route requests. In this case, we
1923
+ * are only ever calling a single action or loader, and we are returning the
1924
+ * returned value directly. In most cases, this will be a Response returned
1925
+ * from the action/loader, but it may be a primitive or other value as well -
1926
+ * and in such cases the calling context should handle that accordingly.
1927
+ *
1928
+ * We do respect the throw/return differentiation, so if an action/loader
1929
+ * throws, then this method will throw the value. This is important so we
1930
+ * can do proper boundary identification in Remix where a thrown Response
1931
+ * must go to the Catch Boundary but a returned Response is happy-path.
1932
+ *
1933
+ * One thing to note is that any Router-initiated thrown Response (such as a
1934
+ * 404 or 405) will have a custom X-Remix-Router-Error: "yes" header on it
1935
+ * in order to differentiate from responses thrown from user actions/loaders.
1936
+ */
1937
+ async function queryRoute(request: Request, routeId?: string): Promise<any> {
1938
+ let url = new URL(request.url);
1939
+ let location = createLocation("", createPath(url), null, "default");
1940
+ let matches = matchRoutes(dataRoutes, location);
1941
+
1942
+ if (!validRequestMethods.has(request.method)) {
1943
+ throw createRouterErrorResponse(null, {
1944
+ status: 405,
1945
+ statusText: "Method Not Allowed",
1946
+ });
1947
+ } else if (!matches) {
1948
+ throw createRouterErrorResponse(null, {
1949
+ status: 404,
1950
+ statusText: "Not Found",
1951
+ });
1952
+ }
1953
+
1954
+ let match = routeId
1955
+ ? matches.find((m) => m.route.id === routeId)
1956
+ : getTargetMatch(matches, location);
1957
+
1958
+ if (!match) {
1959
+ throw createRouterErrorResponse(null, {
1960
+ status: 404,
1961
+ statusText: "Not Found",
1962
+ });
1963
+ }
1964
+
1965
+ let result = await queryImpl(request, location, matches, match);
1783
1966
  if (result instanceof Response) {
1784
1967
  return result;
1785
1968
  }
1786
1969
 
1787
1970
  let error = result.errors ? Object.values(result.errors)[0] : undefined;
1788
1971
  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
1972
  // If we got back result.errors, that means the loader/action threw
1800
1973
  // _something_ that wasn't a Response, but it's not guaranteed/required
1801
1974
  // to be an `instanceof Error` either, so we have to use throw here to
@@ -1805,66 +1978,53 @@ export function unstable_createStaticHandler(
1805
1978
 
1806
1979
  // Pick off the right state value to return
1807
1980
  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;
1981
+ return Object.values(routeData || {})[0];
1818
1982
  }
1819
1983
 
1820
1984
  async function queryImpl(
1821
1985
  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
- );
1986
+ location: Location,
1987
+ matches: AgnosticDataRouteMatch[],
1988
+ routeMatch?: AgnosticDataRouteMatch
1989
+ ): Promise<Omit<StaticHandlerContext, "location"> | Response> {
1831
1990
  invariant(
1832
1991
  request.signal,
1833
1992
  "query()/queryRoute() requests must contain an AbortController signal"
1834
1993
  );
1835
1994
 
1836
- let { location, matches, shortCircuitState } = matchRequest(
1837
- request,
1838
- routeId
1839
- );
1840
-
1841
1995
  try {
1842
- if (shortCircuitState) {
1843
- return { location, result: shortCircuitState };
1844
- }
1845
-
1846
- if (request.method !== "GET") {
1996
+ if (validActionMethods.has(request.method)) {
1847
1997
  let result = await submit(
1848
1998
  request,
1849
1999
  matches,
1850
- getTargetMatch(matches, location),
1851
- routeId != null
2000
+ routeMatch || getTargetMatch(matches, location),
2001
+ routeMatch != null
1852
2002
  );
1853
- return { location, result };
2003
+ return result;
1854
2004
  }
1855
2005
 
1856
- let result = await loadRouteData(request, matches, routeId != null);
1857
- return {
1858
- location,
1859
- result: {
1860
- ...result,
1861
- actionData: null,
1862
- actionHeaders: {},
1863
- },
1864
- };
2006
+ let result = await loadRouteData(request, matches, routeMatch);
2007
+ return result instanceof Response
2008
+ ? result
2009
+ : {
2010
+ ...result,
2011
+ actionData: null,
2012
+ actionHeaders: {},
2013
+ };
1865
2014
  } catch (e) {
1866
- if (e instanceof Response) {
1867
- return { location, result: e };
2015
+ // If the user threw/returned a Response in callLoaderOrAction, we throw
2016
+ // it to bail out and then return or throw here based on whether the user
2017
+ // returned or threw
2018
+ if (isQueryRouteResponse(e)) {
2019
+ if (e.type === ResultType.error && !isRedirectResponse(e.response)) {
2020
+ throw e.response;
2021
+ }
2022
+ return e.response;
2023
+ }
2024
+ // Redirects are always returned since they don't propagate to catch
2025
+ // boundaries
2026
+ if (isRedirectResponse(e)) {
2027
+ return e;
1868
2028
  }
1869
2029
  throw e;
1870
2030
  }
@@ -1878,13 +2038,21 @@ export function unstable_createStaticHandler(
1878
2038
  ): Promise<Omit<StaticHandlerContext, "location"> | Response> {
1879
2039
  let result: DataResult;
1880
2040
  if (!actionMatch.route.action) {
1881
- let href = createHref(new URL(request.url));
2041
+ let href = createServerHref(new URL(request.url));
2042
+ if (isRouteRequest) {
2043
+ throw createRouterErrorResponse(null, {
2044
+ status: 405,
2045
+ statusText: "Method Not Allowed",
2046
+ });
2047
+ }
1882
2048
  result = getMethodNotAllowedResult(href);
1883
2049
  } else {
1884
2050
  result = await callLoaderOrAction(
1885
2051
  "action",
1886
2052
  request,
1887
2053
  actionMatch,
2054
+ matches,
2055
+ undefined, // Basename not currently supported in static handlers
1888
2056
  true,
1889
2057
  isRouteRequest
1890
2058
  );
@@ -1897,7 +2065,7 @@ export function unstable_createStaticHandler(
1897
2065
 
1898
2066
  if (isRedirectResult(result)) {
1899
2067
  // Uhhhh - this should never happen, we should always throw these from
1900
- // calLoaderOrAction, but the type narrowing here keeps TS happy and we
2068
+ // callLoaderOrAction, but the type narrowing here keeps TS happy and we
1901
2069
  // can get back on the "throw all redirect responses" train here should
1902
2070
  // this ever happen :/
1903
2071
  throw new Response(null, {
@@ -1913,6 +2081,8 @@ export function unstable_createStaticHandler(
1913
2081
  }
1914
2082
 
1915
2083
  if (isRouteRequest) {
2084
+ // Note: This should only be non-Response values if we get here, since
2085
+ // isRouteRequest should throw any Response received in callLoaderOrAction
1916
2086
  if (isErrorResult(result)) {
1917
2087
  let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
1918
2088
  return {
@@ -1947,7 +2117,7 @@ export function unstable_createStaticHandler(
1947
2117
  // Store off the pending error - we use it to determine which loaders
1948
2118
  // to call and will commit it when we complete the navigation
1949
2119
  let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
1950
- let context = await loadRouteData(request, matches, isRouteRequest, {
2120
+ let context = await loadRouteData(request, matches, undefined, {
1951
2121
  [boundaryMatch.route.id]: result.error,
1952
2122
  });
1953
2123
 
@@ -1964,7 +2134,7 @@ export function unstable_createStaticHandler(
1964
2134
  };
1965
2135
  }
1966
2136
 
1967
- let context = await loadRouteData(request, matches, isRouteRequest);
2137
+ let context = await loadRouteData(request, matches);
1968
2138
 
1969
2139
  return {
1970
2140
  ...context,
@@ -1982,16 +2152,20 @@ export function unstable_createStaticHandler(
1982
2152
  async function loadRouteData(
1983
2153
  request: Request,
1984
2154
  matches: AgnosticDataRouteMatch[],
1985
- isRouteRequest: boolean,
2155
+ routeMatch?: AgnosticDataRouteMatch,
1986
2156
  pendingActionError?: RouteData
1987
2157
  ): Promise<
1988
2158
  | Omit<StaticHandlerContext, "location" | "actionData" | "actionHeaders">
1989
2159
  | Response
1990
2160
  > {
1991
- let matchesToLoad = getLoaderMatchesUntilBoundary(
1992
- matches,
1993
- Object.keys(pendingActionError || {})[0]
1994
- ).filter((m) => m.route.loader);
2161
+ let isRouteRequest = routeMatch != null;
2162
+ let requestMatches = routeMatch
2163
+ ? [routeMatch]
2164
+ : getLoaderMatchesUntilBoundary(
2165
+ matches,
2166
+ Object.keys(pendingActionError || {})[0]
2167
+ );
2168
+ let matchesToLoad = requestMatches.filter((m) => m.route.loader);
1995
2169
 
1996
2170
  // Short circuit if we have no loaders to run
1997
2171
  if (matchesToLoad.length === 0) {
@@ -2005,8 +2179,16 @@ export function unstable_createStaticHandler(
2005
2179
  }
2006
2180
 
2007
2181
  let results = await Promise.all([
2008
- ...matchesToLoad.map((m) =>
2009
- callLoaderOrAction("loader", request, m, true, isRouteRequest)
2182
+ ...matchesToLoad.map((match) =>
2183
+ callLoaderOrAction(
2184
+ "loader",
2185
+ request,
2186
+ match,
2187
+ matches,
2188
+ undefined, // Basename not currently supported in static handlers
2189
+ true,
2190
+ isRouteRequest
2191
+ )
2010
2192
  ),
2011
2193
  ]);
2012
2194
 
@@ -2037,47 +2219,17 @@ export function unstable_createStaticHandler(
2037
2219
  };
2038
2220
  }
2039
2221
 
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 };
2222
+ function createRouterErrorResponse(
2223
+ body: BodyInit | null | undefined,
2224
+ init: ResponseInit
2225
+ ) {
2226
+ return new Response(body, {
2227
+ ...init,
2228
+ headers: {
2229
+ ...init.headers,
2230
+ "X-Remix-Router-Error": "yes",
2231
+ },
2232
+ });
2081
2233
  }
2082
2234
 
2083
2235
  return {
@@ -2136,7 +2288,7 @@ function normalizeNavigateOptions(
2136
2288
  path,
2137
2289
  submission: {
2138
2290
  formMethod: opts.formMethod,
2139
- formAction: createHref(parsePath(path)),
2291
+ formAction: createServerHref(parsePath(path)),
2140
2292
  formEncType:
2141
2293
  (opts && opts.formEncType) || "application/x-www-form-urlencoded",
2142
2294
  formData: opts.formData,
@@ -2251,10 +2403,10 @@ function getMatchesToLoad(
2251
2403
  // Pick fetcher.loads that need to be revalidated
2252
2404
  let revalidatingFetchers: RevalidatingFetcher[] = [];
2253
2405
  fetchLoadMatches &&
2254
- fetchLoadMatches.forEach(([href, match], key) => {
2406
+ fetchLoadMatches.forEach(([href, match, fetchMatches], key) => {
2255
2407
  // This fetcher was cancelled from a prior action submission - force reload
2256
2408
  if (cancelledFetcherLoads.includes(key)) {
2257
- revalidatingFetchers.push([key, href, match]);
2409
+ revalidatingFetchers.push([key, href, match, fetchMatches]);
2258
2410
  } else if (isRevalidationRequired) {
2259
2411
  let shouldRevalidate = shouldRevalidateLoader(
2260
2412
  href,
@@ -2266,7 +2418,7 @@ function getMatchesToLoad(
2266
2418
  actionResult
2267
2419
  );
2268
2420
  if (shouldRevalidate) {
2269
- revalidatingFetchers.push([key, href, match]);
2421
+ revalidatingFetchers.push([key, href, match, fetchMatches]);
2270
2422
  }
2271
2423
  }
2272
2424
  });
@@ -2360,7 +2512,9 @@ async function callLoaderOrAction(
2360
2512
  type: "loader" | "action",
2361
2513
  request: Request,
2362
2514
  match: AgnosticDataRouteMatch,
2363
- skipRedirects: boolean = false,
2515
+ matches: AgnosticDataRouteMatch[],
2516
+ basename: string | undefined,
2517
+ isStaticRequest: boolean = false,
2364
2518
  isRouteRequest: boolean = false
2365
2519
  ): Promise<DataResult> {
2366
2520
  let resultType;
@@ -2391,23 +2545,46 @@ async function callLoaderOrAction(
2391
2545
  }
2392
2546
 
2393
2547
  if (result instanceof Response) {
2394
- // Process redirects
2395
2548
  let status = result.status;
2396
- let location = result.headers.get("Location");
2397
2549
 
2398
- // For SSR single-route requests, we want to hand Responses back directly
2399
- // without unwrapping
2400
- if (isRouteRequest) {
2401
- throw result;
2402
- }
2550
+ // Process redirects
2551
+ if (status >= 300 && status <= 399) {
2552
+ let location = result.headers.get("Location");
2553
+ invariant(
2554
+ location,
2555
+ "Redirects returned/thrown from loaders/actions must have a Location header"
2556
+ );
2557
+
2558
+ // Support relative routing in redirects
2559
+ let activeMatches = matches.slice(0, matches.indexOf(match) + 1);
2560
+ let routePathnames = getPathContributingMatches(activeMatches).map(
2561
+ (match) => match.pathnameBase
2562
+ );
2563
+ let requestPath = createURL(request.url).pathname;
2564
+ let resolvedLocation = resolveTo(location, routePathnames, requestPath);
2565
+ invariant(
2566
+ createPath(resolvedLocation),
2567
+ `Unable to resolve redirect location: ${result.headers.get("Location")}`
2568
+ );
2569
+
2570
+ // Prepend the basename to the redirect location if we have one
2571
+ if (basename) {
2572
+ let path = resolvedLocation.pathname;
2573
+ resolvedLocation.pathname =
2574
+ path === "/" ? basename : joinPaths([basename, path]);
2575
+ }
2576
+
2577
+ location = createPath(resolvedLocation);
2403
2578
 
2404
- if (status >= 300 && status <= 399 && location != null) {
2405
- // Don't process redirects in the router during SSR document requests.
2579
+ // Don't process redirects in the router during static requests requests.
2406
2580
  // Instead, throw the Response and let the server handle it with an HTTP
2407
- // redirect
2408
- if (skipRedirects) {
2581
+ // redirect. We also update the Location header in place in this flow so
2582
+ // basename and relative routing is taken into account
2583
+ if (isStaticRequest) {
2584
+ result.headers.set("Location", location);
2409
2585
  throw result;
2410
2586
  }
2587
+
2411
2588
  return {
2412
2589
  type: ResultType.redirect,
2413
2590
  status,
@@ -2416,6 +2593,17 @@ async function callLoaderOrAction(
2416
2593
  };
2417
2594
  }
2418
2595
 
2596
+ // For SSR single-route requests, we want to hand Responses back directly
2597
+ // without unwrapping. We do this with the QueryRouteResponse wrapper
2598
+ // interface so we can know whether it was returned or thrown
2599
+ if (isRouteRequest) {
2600
+ // eslint-disable-next-line no-throw-literal
2601
+ throw {
2602
+ type: resultType || ResultType.data,
2603
+ response: result,
2604
+ };
2605
+ }
2606
+
2419
2607
  let data: any;
2420
2608
  let contentType = result.headers.get("Content-Type");
2421
2609
  if (contentType && contentType.startsWith("application/json")) {
@@ -2669,16 +2857,18 @@ function findNearestBoundary(
2669
2857
  );
2670
2858
  }
2671
2859
 
2672
- function getNotFoundMatches(routes: AgnosticDataRouteObject[]): {
2860
+ function getShortCircuitMatches(
2861
+ routes: AgnosticDataRouteObject[],
2862
+ status: number,
2863
+ statusText: string
2864
+ ): {
2673
2865
  matches: AgnosticDataRouteMatch[];
2674
2866
  route: AgnosticDataRouteObject;
2675
2867
  error: ErrorResponse;
2676
2868
  } {
2677
2869
  // 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__",
2870
+ let route = routes.find((r) => r.index || !r.path || r.path === "/") || {
2871
+ id: `__shim-${status}-route__`,
2682
2872
  };
2683
2873
 
2684
2874
  return {
@@ -2691,12 +2881,20 @@ function getNotFoundMatches(routes: AgnosticDataRouteObject[]): {
2691
2881
  },
2692
2882
  ],
2693
2883
  route,
2694
- error: new ErrorResponse(404, "Not Found", null),
2884
+ error: new ErrorResponse(status, statusText, null),
2695
2885
  };
2696
2886
  }
2697
2887
 
2888
+ function getNotFoundMatches(routes: AgnosticDataRouteObject[]) {
2889
+ return getShortCircuitMatches(routes, 404, "Not Found");
2890
+ }
2891
+
2892
+ function getMethodNotAllowedMatches(routes: AgnosticDataRouteObject[]) {
2893
+ return getShortCircuitMatches(routes, 405, "Method Not Allowed");
2894
+ }
2895
+
2698
2896
  function getMethodNotAllowedResult(path: Location | string): ErrorResult {
2699
- let href = typeof path === "string" ? path : createHref(path);
2897
+ let href = typeof path === "string" ? path : createServerHref(path);
2700
2898
  console.warn(
2701
2899
  "You're trying to submit to a route that does not have an action. To " +
2702
2900
  "fix this, please add an `action` function to the route for " +
@@ -2704,11 +2902,7 @@ function getMethodNotAllowedResult(path: Location | string): ErrorResult {
2704
2902
  );
2705
2903
  return {
2706
2904
  type: ResultType.error,
2707
- error: new ErrorResponse(
2708
- 405,
2709
- "Method Not Allowed",
2710
- `No action found for [${href}]`
2711
- ),
2905
+ error: new ErrorResponse(405, "Method Not Allowed", ""),
2712
2906
  };
2713
2907
  }
2714
2908
 
@@ -2723,7 +2917,7 @@ function findRedirect(results: DataResult[]): RedirectResult | undefined {
2723
2917
  }
2724
2918
 
2725
2919
  // Create an href to represent a "server" URL without the hash
2726
- function createHref(location: Partial<Path> | Location | URL) {
2920
+ function createServerHref(location: Partial<Path> | Location | URL) {
2727
2921
  return (location.pathname || "") + (location.search || "");
2728
2922
  }
2729
2923
 
@@ -2745,6 +2939,24 @@ function isRedirectResult(result?: DataResult): result is RedirectResult {
2745
2939
  return (result && result.type) === ResultType.redirect;
2746
2940
  }
2747
2941
 
2942
+ function isRedirectResponse(result: any): result is Response {
2943
+ if (!(result instanceof Response)) {
2944
+ return false;
2945
+ }
2946
+
2947
+ let status = result.status;
2948
+ let location = result.headers.get("Location");
2949
+ return status >= 300 && status <= 399 && location != null;
2950
+ }
2951
+
2952
+ function isQueryRouteResponse(obj: any): obj is QueryRouteResponse {
2953
+ return (
2954
+ obj &&
2955
+ obj.response instanceof Response &&
2956
+ (obj.type === ResultType.data || ResultType.error)
2957
+ );
2958
+ }
2959
+
2748
2960
  async function resolveDeferredResults(
2749
2961
  currentMatches: AgnosticDataRouteMatch[],
2750
2962
  matchesToLoad: AgnosticDataRouteMatch[],
@@ -2836,11 +3048,15 @@ function getTargetMatch(
2836
3048
  typeof location === "string" ? parsePath(location).search : location.search;
2837
3049
  if (
2838
3050
  matches[matches.length - 1].route.index &&
2839
- !hasNakedIndexQuery(search || "")
3051
+ hasNakedIndexQuery(search || "")
2840
3052
  ) {
2841
- return matches.slice(-2)[0];
3053
+ // Return the leaf index route when index is present
3054
+ return matches[matches.length - 1];
2842
3055
  }
2843
- return matches.slice(-1)[0];
3056
+ // Otherwise grab the deepest "path contributing" match (ignoring index and
3057
+ // pathless layout routes)
3058
+ let pathMatches = getPathContributingMatches(matches);
3059
+ return pathMatches[pathMatches.length - 1];
2844
3060
  }
2845
3061
 
2846
3062
  function createURL(location: Location | string): URL {
@@ -2848,7 +3064,8 @@ function createURL(location: Location | string): URL {
2848
3064
  typeof window !== "undefined" && typeof window.location !== "undefined"
2849
3065
  ? window.location.origin
2850
3066
  : "unknown://unknown";
2851
- let href = typeof location === "string" ? location : createHref(location);
3067
+ let href =
3068
+ typeof location === "string" ? location : createServerHref(location);
2852
3069
  return new URL(href, base);
2853
3070
  }
2854
3071
  //#endregion