@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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @remix-run/router v1.0.2
2
+ * @remix-run/router v1.0.3-pre.1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -113,6 +113,10 @@ function createMemoryHistory(options) {
113
113
  return typeof to === "string" ? to : createPath(to);
114
114
  },
115
115
 
116
+ encodeLocation(location) {
117
+ return location;
118
+ },
119
+
116
120
  push(to, state) {
117
121
  action = exports.Action.Push;
118
122
  let nextLocation = createMemoryLocation(to, state);
@@ -370,6 +374,14 @@ function parsePath(path) {
370
374
 
371
375
  return parsedPath;
372
376
  }
377
+ function createURL(location) {
378
+ // window.location.origin is "null" (the literal string value) in Firefox
379
+ // under certain conditions, notably when serving from a local HTML file
380
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=878297
381
+ let base = typeof window !== "undefined" && typeof window.location !== "undefined" && window.location.origin !== "null" ? window.location.origin : "unknown://unknown";
382
+ let href = typeof location === "string" ? location : createPath(location);
383
+ return new URL(href, base);
384
+ }
373
385
 
374
386
  function getUrlBasedHistory(getLocation, createHref, validateLocation, options) {
375
387
  if (options === void 0) {
@@ -413,7 +425,7 @@ function getUrlBasedHistory(getLocation, createHref, validateLocation, options)
413
425
  if (v5Compat && listener) {
414
426
  listener({
415
427
  action,
416
- location
428
+ location: history.location
417
429
  });
418
430
  }
419
431
  }
@@ -429,7 +441,7 @@ function getUrlBasedHistory(getLocation, createHref, validateLocation, options)
429
441
  if (v5Compat && listener) {
430
442
  listener({
431
443
  action,
432
- location: location
444
+ location: history.location
433
445
  });
434
446
  }
435
447
  }
@@ -460,6 +472,16 @@ function getUrlBasedHistory(getLocation, createHref, validateLocation, options)
460
472
  return createHref(window, to);
461
473
  },
462
474
 
475
+ encodeLocation(location) {
476
+ // Encode a Location the same way window.location would
477
+ let url = createURL(createPath(location));
478
+ return _extends({}, location, {
479
+ pathname: url.pathname,
480
+ search: url.search,
481
+ hash: url.hash
482
+ });
483
+ },
484
+
463
485
  push,
464
486
  replace,
465
487
 
@@ -548,7 +570,13 @@ function matchRoutes(routes, locationArg, basename) {
548
570
  let matches = null;
549
571
 
550
572
  for (let i = 0; matches == null && i < branches.length; ++i) {
551
- matches = matchRouteBranch(branches[i], pathname);
573
+ matches = matchRouteBranch(branches[i], // Incoming pathnames are generally encoded from either window.location
574
+ // or from router.navigate, but we want to match against the unencoded
575
+ // paths in the route definitions. Memory router locations won't be
576
+ // encoded here but there also shouldn't be anything to decode so this
577
+ // should be a safe operation. This avoids needing matchRoutes to be
578
+ // history-aware.
579
+ safelyDecodeURI(pathname));
552
580
  }
553
581
 
554
582
  return matches;
@@ -795,6 +823,15 @@ function compilePath(path, caseSensitive, end) {
795
823
  return [matcher, paramNames];
796
824
  }
797
825
 
826
+ function safelyDecodeURI(value) {
827
+ try {
828
+ return decodeURI(value);
829
+ } catch (error) {
830
+ warning(false, "The URL path \"" + value + "\" could not be decoded because it is is a " + "malformed URL segment. This is probably due to a bad percent " + ("encoding (" + error + ")."));
831
+ return value;
832
+ }
833
+ }
834
+
798
835
  function safelyDecodeURIComponent(value, paramName) {
799
836
  try {
800
837
  return decodeURIComponent(value);
@@ -898,9 +935,36 @@ function getInvalidPathError(char, field, dest, path) {
898
935
  }
899
936
  /**
900
937
  * @private
938
+ *
939
+ * When processing relative navigation we want to ignore ancestor routes that
940
+ * do not contribute to the path, such that index/pathless layout routes don't
941
+ * interfere.
942
+ *
943
+ * For example, when moving a route element into an index route and/or a
944
+ * pathless layout route, relative link behavior contained within should stay
945
+ * the same. Both of the following examples should link back to the root:
946
+ *
947
+ * <Route path="/">
948
+ * <Route path="accounts" element={<Link to=".."}>
949
+ * </Route>
950
+ *
951
+ * <Route path="/">
952
+ * <Route path="accounts">
953
+ * <Route element={<AccountsLayout />}> // <-- Does not contribute
954
+ * <Route index element={<Link to=".."} /> // <-- Does not contribute
955
+ * </Route
956
+ * </Route>
957
+ * </Route>
901
958
  */
902
959
 
903
960
 
961
+ function getPathContributingMatches(matches) {
962
+ return matches.filter((match, index) => index === 0 || match.route.path && match.route.path.length > 0);
963
+ }
964
+ /**
965
+ * @private
966
+ */
967
+
904
968
  function resolveTo(toArg, routePathnames, locationPathname, isPathRelative) {
905
969
  if (isPathRelative === void 0) {
906
970
  isPathRelative = false;
@@ -1228,7 +1292,9 @@ const IDLE_FETCHER = {
1228
1292
  formAction: undefined,
1229
1293
  formEncType: undefined,
1230
1294
  formData: undefined
1231
- }; //#endregion
1295
+ };
1296
+ const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined";
1297
+ const isServer = !isBrowser; //#endregion
1232
1298
  ////////////////////////////////////////////////////////////////////////////////
1233
1299
  //#region createRouter
1234
1300
  ////////////////////////////////////////////////////////////////////////////////
@@ -1435,7 +1501,13 @@ function createRouter(init) {
1435
1501
  submission,
1436
1502
  error
1437
1503
  } = normalizeNavigateOptions(to, opts);
1438
- let location = createLocation(state.location, path, opts && opts.state);
1504
+ let location = createLocation(state.location, path, opts && opts.state); // When using navigate as a PUSH/REPLACE we aren't reading an already-encoded
1505
+ // URL from window.location, so we need to encode it here so the behavior
1506
+ // remains the same as POP and non-data-router usages. new URL() does all
1507
+ // the same encoding we'd get from a history.pushState/window.location read
1508
+ // without having to touch history
1509
+
1510
+ location = init.history.encodeLocation(location);
1439
1511
  let historyAction = (opts && opts.replace) === true || submission != null ? exports.Action.Replace : exports.Action.Push;
1440
1512
  let preventScrollReset = opts && "preventScrollReset" in opts ? opts.preventScrollReset === true : undefined;
1441
1513
  return await startNavigation(historyAction, location, {
@@ -1601,7 +1673,7 @@ function createRouter(init) {
1601
1673
  if (!actionMatch.route.action) {
1602
1674
  result = getMethodNotAllowedResult(location);
1603
1675
  } else {
1604
- result = await callLoaderOrAction("action", request, actionMatch);
1676
+ result = await callLoaderOrAction("action", request, actionMatch, matches, router.basename);
1605
1677
 
1606
1678
  if (request.signal.aborted) {
1607
1679
  return {
@@ -1696,7 +1768,7 @@ function createRouter(init) {
1696
1768
  if (!isUninterruptedRevalidation) {
1697
1769
  revalidatingFetchers.forEach(_ref2 => {
1698
1770
  let [key] = _ref2;
1699
- const fetcher = state.fetchers.get(key);
1771
+ let fetcher = state.fetchers.get(key);
1700
1772
  let revalidatingFetcher = {
1701
1773
  state: "loading",
1702
1774
  data: fetcher && fetcher.data,
@@ -1724,7 +1796,7 @@ function createRouter(init) {
1724
1796
  results,
1725
1797
  loaderResults,
1726
1798
  fetcherResults
1727
- } = await callLoadersAndMaybeResolveData(state.matches, matchesToLoad, revalidatingFetchers, request);
1799
+ } = await callLoadersAndMaybeResolveData(state.matches, matches, matchesToLoad, revalidatingFetchers, request);
1728
1800
 
1729
1801
  if (request.signal.aborted) {
1730
1802
  return {
@@ -1782,7 +1854,7 @@ function createRouter(init) {
1782
1854
 
1783
1855
 
1784
1856
  function fetch(key, routeId, href, opts) {
1785
- if (typeof AbortController === "undefined") {
1857
+ if (isServer) {
1786
1858
  throw new Error("router.fetch() was called during the server render, but it shouldn't be. " + "You are likely calling a useFetcher() method in the body of your component. " + "Try moving it to a useEffect or a callback.");
1787
1859
  }
1788
1860
 
@@ -1801,19 +1873,19 @@ function createRouter(init) {
1801
1873
  let match = getTargetMatch(matches, path);
1802
1874
 
1803
1875
  if (submission) {
1804
- handleFetcherAction(key, routeId, path, match, submission);
1876
+ handleFetcherAction(key, routeId, path, match, matches, submission);
1805
1877
  return;
1806
1878
  } // Store off the match so we can call it's shouldRevalidate on subsequent
1807
1879
  // revalidations
1808
1880
 
1809
1881
 
1810
- fetchLoadMatches.set(key, [path, match]);
1811
- handleFetcherLoader(key, routeId, path, match);
1882
+ fetchLoadMatches.set(key, [path, match, matches]);
1883
+ handleFetcherLoader(key, routeId, path, match, matches);
1812
1884
  } // Call the action for the matched fetcher.submit(), and then handle redirects,
1813
1885
  // errors, and revalidation
1814
1886
 
1815
1887
 
1816
- async function handleFetcherAction(key, routeId, path, match, submission) {
1888
+ async function handleFetcherAction(key, routeId, path, match, requestMatches, submission) {
1817
1889
  interruptActiveLoads();
1818
1890
  fetchLoadMatches.delete(key);
1819
1891
 
@@ -1842,7 +1914,7 @@ function createRouter(init) {
1842
1914
  let abortController = new AbortController();
1843
1915
  let fetchRequest = createRequest(path, abortController.signal, submission);
1844
1916
  fetchControllers.set(key, abortController);
1845
- let actionResult = await callLoaderOrAction("action", fetchRequest, match);
1917
+ let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, router.basename);
1846
1918
 
1847
1919
  if (fetchRequest.signal.aborted) {
1848
1920
  // We can delete this so long as we weren't aborted by ou our own fetcher
@@ -1934,7 +2006,7 @@ function createRouter(init) {
1934
2006
  results,
1935
2007
  loaderResults,
1936
2008
  fetcherResults
1937
- } = await callLoadersAndMaybeResolveData(state.matches, matchesToLoad, revalidatingFetchers, revalidationRequest);
2009
+ } = await callLoadersAndMaybeResolveData(state.matches, matches, matchesToLoad, revalidatingFetchers, revalidationRequest);
1938
2010
 
1939
2011
  if (abortController.signal.aborted) {
1940
2012
  return;
@@ -1996,7 +2068,7 @@ function createRouter(init) {
1996
2068
  } // Call the matched loader for fetcher.load(), handling redirects, errors, etc.
1997
2069
 
1998
2070
 
1999
- async function handleFetcherLoader(key, routeId, path, match) {
2071
+ async function handleFetcherLoader(key, routeId, path, match, matches) {
2000
2072
  let existingFetcher = state.fetchers.get(key); // Put this fetcher into it's loading state
2001
2073
 
2002
2074
  let loadingFetcher = {
@@ -2015,7 +2087,7 @@ function createRouter(init) {
2015
2087
  let abortController = new AbortController();
2016
2088
  let fetchRequest = createRequest(path, abortController.signal);
2017
2089
  fetchControllers.set(key, abortController);
2018
- let result = await callLoaderOrAction("loader", fetchRequest, match); // Deferred isn't supported or fetcher loads, await everything and treat it
2090
+ let result = await callLoaderOrAction("loader", fetchRequest, match, matches, router.basename); // Deferred isn't supported or fetcher loads, await everything and treat it
2019
2091
  // as a normal load. resolveDeferredData will return undefined if this
2020
2092
  // fetcher gets aborted, so we just leave result untouched and short circuit
2021
2093
  // below if that happens
@@ -2108,13 +2180,13 @@ function createRouter(init) {
2108
2180
  });
2109
2181
  }
2110
2182
 
2111
- async function callLoadersAndMaybeResolveData(currentMatches, matchesToLoad, fetchersToLoad, request) {
2183
+ async function callLoadersAndMaybeResolveData(currentMatches, matches, matchesToLoad, fetchersToLoad, request) {
2112
2184
  // Call all navigation loaders and revalidating fetcher loaders in parallel,
2113
2185
  // then slice off the results into separate arrays so we can handle them
2114
2186
  // accordingly
2115
- let results = await Promise.all([...matchesToLoad.map(m => callLoaderOrAction("loader", request, m)), ...fetchersToLoad.map(_ref8 => {
2116
- let [, href, match] = _ref8;
2117
- return callLoaderOrAction("loader", createRequest(href, request.signal), match);
2187
+ let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, router.basename)), ...fetchersToLoad.map(_ref8 => {
2188
+ let [, href, match, fetchMatches] = _ref8;
2189
+ return callLoaderOrAction("loader", createRequest(href, request.signal), match, fetchMatches, router.basename);
2118
2190
  })]);
2119
2191
  let loaderResults = results.slice(0, matchesToLoad.length);
2120
2192
  let fetcherResults = results.slice(matchesToLoad.length);
@@ -2306,7 +2378,9 @@ function createRouter(init) {
2306
2378
  navigate,
2307
2379
  fetch,
2308
2380
  revalidate,
2309
- createHref,
2381
+ // Passthrough to history-aware createHref used by useHref so we get proper
2382
+ // hash-aware URLs in DOM paths
2383
+ createHref: to => init.history.createHref(to),
2310
2384
  getFetcher,
2311
2385
  deleteFetcher,
2312
2386
  dispose,
@@ -2319,15 +2393,75 @@ function createRouter(init) {
2319
2393
  //#region createStaticHandler
2320
2394
  ////////////////////////////////////////////////////////////////////////////////
2321
2395
 
2396
+ const validActionMethods = new Set(["POST", "PUT", "PATCH", "DELETE"]);
2397
+ const validRequestMethods = new Set(["GET", "HEAD", ...validActionMethods]);
2322
2398
  function unstable_createStaticHandler(routes) {
2323
2399
  invariant(routes.length > 0, "You must provide a non-empty routes array to unstable_createStaticHandler");
2324
2400
  let dataRoutes = convertRoutesToDataRoutes(routes);
2401
+ /**
2402
+ * The query() method is intended for document requests, in which we want to
2403
+ * call an optional action and potentially multiple loaders for all nested
2404
+ * routes. It returns a StaticHandlerContext object, which is very similar
2405
+ * to the router state (location, loaderData, actionData, errors, etc.) and
2406
+ * also adds SSR-specific information such as the statusCode and headers
2407
+ * from action/loaders Responses.
2408
+ *
2409
+ * It _should_ never throw and should report all errors through the
2410
+ * returned context.errors object, properly associating errors to their error
2411
+ * boundary. Additionally, it tracks _deepestRenderedBoundaryId which can be
2412
+ * used to emulate React error boundaries during SSr by performing a second
2413
+ * pass only down to the boundaryId.
2414
+ *
2415
+ * The one exception where we do not return a StaticHandlerContext is when a
2416
+ * redirect response is returned or thrown from any action/loader. We
2417
+ * propagate that out and return the raw Response so the HTTP server can
2418
+ * return it directly.
2419
+ */
2325
2420
 
2326
2421
  async function query(request) {
2327
- let {
2328
- location,
2329
- result
2330
- } = await queryImpl(request);
2422
+ let url = new URL(request.url);
2423
+ let location = createLocation("", createPath(url), null, "default");
2424
+ let matches = matchRoutes(dataRoutes, location);
2425
+
2426
+ if (!validRequestMethods.has(request.method)) {
2427
+ let {
2428
+ matches: methodNotAllowedMatches,
2429
+ route,
2430
+ error
2431
+ } = getMethodNotAllowedMatches(dataRoutes);
2432
+ return {
2433
+ location,
2434
+ matches: methodNotAllowedMatches,
2435
+ loaderData: {},
2436
+ actionData: null,
2437
+ errors: {
2438
+ [route.id]: error
2439
+ },
2440
+ statusCode: error.status,
2441
+ loaderHeaders: {},
2442
+ actionHeaders: {}
2443
+ };
2444
+ } else if (!matches) {
2445
+ let {
2446
+ matches: notFoundMatches,
2447
+ route,
2448
+ error
2449
+ } = getNotFoundMatches(dataRoutes);
2450
+ return {
2451
+ location,
2452
+ matches: notFoundMatches,
2453
+ loaderData: {},
2454
+ actionData: null,
2455
+ errors: {
2456
+ [route.id]: error
2457
+ },
2458
+ statusCode: error.status,
2459
+ loaderHeaders: {},
2460
+ actionHeaders: {}
2461
+ };
2462
+ }
2463
+
2464
+ let result = await queryImpl(request, location, matches);
2331
2465
 
2332
2466
  if (result instanceof Response) {
2333
2467
  return result;
@@ -2340,11 +2474,52 @@ function unstable_createStaticHandler(routes) {
2340
2474
  location
2341
2475
  }, result);
2342
2476
  }
2477
+ /**
2478
+ * The queryRoute() method is intended for targeted route requests, either
2479
+ * for fetch ?_data requests or resource route requests. In this case, we
2480
+ * are only ever calling a single action or loader, and we are returning the
2481
+ * returned value directly. In most cases, this will be a Response returned
2482
+ * from the action/loader, but it may be a primitive or other value as well -
2483
+ * and in such cases the calling context should handle that accordingly.
2484
+ *
2485
+ * We do respect the throw/return differentiation, so if an action/loader
2486
+ * throws, then this method will throw the value. This is important so we
2487
+ * can do proper boundary identification in Remix where a thrown Response
2488
+ * must go to the Catch Boundary but a returned Response is happy-path.
2489
+ *
2490
+ * One thing to note is that any Router-initiated thrown Response (such as a
2491
+ * 404 or 405) will have a custom X-Remix-Router-Error: "yes" header on it
2492
+ * in order to differentiate from responses thrown from user actions/loaders.
2493
+ */
2494
+
2343
2495
 
2344
2496
  async function queryRoute(request, routeId) {
2345
- let {
2346
- result
2347
- } = await queryImpl(request, routeId);
2497
+ let url = new URL(request.url);
2498
+ let location = createLocation("", createPath(url), null, "default");
2499
+ let matches = matchRoutes(dataRoutes, location);
2500
+
2501
+ if (!validRequestMethods.has(request.method)) {
2502
+ throw createRouterErrorResponse(null, {
2503
+ status: 405,
2504
+ statusText: "Method Not Allowed"
2505
+ });
2506
+ } else if (!matches) {
2507
+ throw createRouterErrorResponse(null, {
2508
+ status: 404,
2509
+ statusText: "Not Found"
2510
+ });
2511
+ }
2512
+
2513
+ let match = routeId ? matches.find(m => m.route.id === routeId) : getTargetMatch(matches, location);
2514
+
2515
+ if (!match) {
2516
+ throw createRouterErrorResponse(null, {
2517
+ status: 404,
2518
+ statusText: "Not Found"
2519
+ });
2520
+ }
2521
+
2522
+ let result = await queryImpl(request, location, matches, match);
2348
2523
 
2349
2524
  if (result instanceof Response) {
2350
2525
  return result;
@@ -2353,77 +2528,48 @@ function unstable_createStaticHandler(routes) {
2353
2528
  let error = result.errors ? Object.values(result.errors)[0] : undefined;
2354
2529
 
2355
2530
  if (error !== undefined) {
2356
- // While we always re-throw Responses returned from loaders/actions
2357
- // directly for route requests and prevent the unwrapping into an
2358
- // ErrorResponse, we still need this for error cases _prior_ the
2359
- // execution of the loader/action, such as a 404/405 error.
2360
- if (isRouteErrorResponse(error)) {
2361
- return new Response(error.data, {
2362
- status: error.status,
2363
- statusText: error.statusText
2364
- });
2365
- } // If we got back result.errors, that means the loader/action threw
2531
+ // If we got back result.errors, that means the loader/action threw
2366
2532
  // _something_ that wasn't a Response, but it's not guaranteed/required
2367
2533
  // to be an `instanceof Error` either, so we have to use throw here to
2368
2534
  // preserve the "error" state outside of queryImpl.
2369
-
2370
-
2371
2535
  throw error;
2372
2536
  } // Pick off the right state value to return
2373
2537
 
2374
2538
 
2375
2539
  let routeData = [result.actionData, result.loaderData].find(v => v);
2376
- let value = Object.values(routeData || {})[0];
2377
-
2378
- if (isRouteErrorResponse(value)) {
2379
- return new Response(value.data, {
2380
- status: value.status,
2381
- statusText: value.statusText
2382
- });
2383
- }
2384
-
2385
- return value;
2540
+ return Object.values(routeData || {})[0];
2386
2541
  }
2387
2542
 
2388
- async function queryImpl(request, routeId) {
2389
- invariant(request.method !== "HEAD", "query()/queryRoute() do not support HEAD requests");
2543
+ async function queryImpl(request, location, matches, routeMatch) {
2390
2544
  invariant(request.signal, "query()/queryRoute() requests must contain an AbortController signal");
2391
- let {
2392
- location,
2393
- matches,
2394
- shortCircuitState
2395
- } = matchRequest(request, routeId);
2396
2545
 
2397
2546
  try {
2398
- if (shortCircuitState) {
2399
- return {
2400
- location,
2401
- result: shortCircuitState
2402
- };
2547
+ if (validActionMethods.has(request.method)) {
2548
+ let result = await submit(request, matches, routeMatch || getTargetMatch(matches, location), routeMatch != null);
2549
+ return result;
2403
2550
  }
2404
2551
 
2405
- if (request.method !== "GET") {
2406
- let result = await submit(request, matches, getTargetMatch(matches, location), routeId != null);
2407
- return {
2408
- location,
2409
- result
2410
- };
2411
- }
2412
-
2413
- let result = await loadRouteData(request, matches, routeId != null);
2414
- return {
2415
- location,
2416
- result: _extends({}, result, {
2417
- actionData: null,
2418
- actionHeaders: {}
2419
- })
2420
- };
2552
+ let result = await loadRouteData(request, matches, routeMatch);
2553
+ return result instanceof Response ? result : _extends({}, result, {
2554
+ actionData: null,
2555
+ actionHeaders: {}
2556
+ });
2421
2557
  } catch (e) {
2422
- if (e instanceof Response) {
2423
- return {
2424
- location,
2425
- result: e
2426
- };
2558
+ // If the user threw/returned a Response in callLoaderOrAction, we throw
2559
+ // it to bail out and then return or throw here based on whether the user
2560
+ // returned or threw
2561
+ if (isQueryRouteResponse(e)) {
2562
+ if (e.type === ResultType.error && !isRedirectResponse(e.response)) {
2563
+ throw e.response;
2564
+ }
2565
+
2566
+ return e.response;
2567
+ } // Redirects are always returned since they don't propagate to catch
2568
+ // boundaries
2569
+
2570
+
2571
+ if (isRedirectResponse(e)) {
2572
+ return e;
2427
2573
  }
2428
2574
 
2429
2575
  throw e;
@@ -2434,10 +2580,17 @@ function unstable_createStaticHandler(routes) {
2434
2580
  let result;
2435
2581
 
2436
2582
  if (!actionMatch.route.action) {
2437
- let href = createHref(new URL(request.url));
2438
- result = getMethodNotAllowedResult(href);
2583
+ if (isRouteRequest) {
2584
+ throw createRouterErrorResponse(null, {
2585
+ status: 405,
2586
+ statusText: "Method Not Allowed"
2587
+ });
2588
+ }
2589
+
2590
+ result = getMethodNotAllowedResult(request.url);
2439
2591
  } else {
2440
- result = await callLoaderOrAction("action", request, actionMatch, true, isRouteRequest);
2592
+ result = await callLoaderOrAction("action", request, actionMatch, matches, undefined, // Basename not currently supported in static handlers
2593
+ true, isRouteRequest);
2441
2594
 
2442
2595
  if (request.signal.aborted) {
2443
2596
  let method = isRouteRequest ? "queryRoute" : "query";
@@ -2447,7 +2600,7 @@ function unstable_createStaticHandler(routes) {
2447
2600
 
2448
2601
  if (isRedirectResult(result)) {
2449
2602
  // Uhhhh - this should never happen, we should always throw these from
2450
- // calLoaderOrAction, but the type narrowing here keeps TS happy and we
2603
+ // callLoaderOrAction, but the type narrowing here keeps TS happy and we
2451
2604
  // can get back on the "throw all redirect responses" train here should
2452
2605
  // this ever happen :/
2453
2606
  throw new Response(null, {
@@ -2463,6 +2616,8 @@ function unstable_createStaticHandler(routes) {
2463
2616
  }
2464
2617
 
2465
2618
  if (isRouteRequest) {
2619
+ // Note: This should only be non-Response values if we get here, since
2620
+ // isRouteRequest should throw any Response received in callLoaderOrAction
2466
2621
  if (isErrorResult(result)) {
2467
2622
  let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
2468
2623
  return {
@@ -2499,7 +2654,7 @@ function unstable_createStaticHandler(routes) {
2499
2654
  // Store off the pending error - we use it to determine which loaders
2500
2655
  // to call and will commit it when we complete the navigation
2501
2656
  let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
2502
- let context = await loadRouteData(request, matches, isRouteRequest, {
2657
+ let context = await loadRouteData(request, matches, undefined, {
2503
2658
  [boundaryMatch.route.id]: result.error
2504
2659
  }); // action status codes take precedence over loader status codes
2505
2660
 
@@ -2512,7 +2667,7 @@ function unstable_createStaticHandler(routes) {
2512
2667
  });
2513
2668
  }
2514
2669
 
2515
- let context = await loadRouteData(request, matches, isRouteRequest);
2670
+ let context = await loadRouteData(request, matches);
2516
2671
  return _extends({}, context, result.statusCode ? {
2517
2672
  statusCode: result.statusCode
2518
2673
  } : {}, {
@@ -2525,8 +2680,10 @@ function unstable_createStaticHandler(routes) {
2525
2680
  });
2526
2681
  }
2527
2682
 
2528
- async function loadRouteData(request, matches, isRouteRequest, pendingActionError) {
2529
- let matchesToLoad = getLoaderMatchesUntilBoundary(matches, Object.keys(pendingActionError || {})[0]).filter(m => m.route.loader); // Short circuit if we have no loaders to run
2683
+ async function loadRouteData(request, matches, routeMatch, pendingActionError) {
2684
+ let isRouteRequest = routeMatch != null;
2685
+ let requestMatches = routeMatch ? [routeMatch] : getLoaderMatchesUntilBoundary(matches, Object.keys(pendingActionError || {})[0]);
2686
+ let matchesToLoad = requestMatches.filter(m => m.route.loader); // Short circuit if we have no loaders to run
2530
2687
 
2531
2688
  if (matchesToLoad.length === 0) {
2532
2689
  return {
@@ -2538,7 +2695,8 @@ function unstable_createStaticHandler(routes) {
2538
2695
  };
2539
2696
  }
2540
2697
 
2541
- let results = await Promise.all([...matchesToLoad.map(m => callLoaderOrAction("loader", request, m, true, isRouteRequest))]);
2698
+ let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, undefined, // Basename not currently supported in static handlers
2699
+ true, isRouteRequest))]);
2542
2700
 
2543
2701
  if (request.signal.aborted) {
2544
2702
  let method = isRouteRequest ? "queryRoute" : "query";
@@ -2559,43 +2717,12 @@ function unstable_createStaticHandler(routes) {
2559
2717
  });
2560
2718
  }
2561
2719
 
2562
- function matchRequest(req, routeId) {
2563
- let url = new URL(req.url);
2564
- let location = createLocation("", createPath(url), null, "default");
2565
- let matches = matchRoutes(dataRoutes, location);
2566
-
2567
- if (matches && routeId) {
2568
- matches = matches.filter(m => m.route.id === routeId);
2569
- } // Short circuit with a 404 if we match nothing
2570
-
2571
-
2572
- if (!matches) {
2573
- let {
2574
- matches: notFoundMatches,
2575
- route,
2576
- error
2577
- } = getNotFoundMatches(dataRoutes);
2578
- return {
2579
- location,
2580
- matches: notFoundMatches,
2581
- shortCircuitState: {
2582
- matches: notFoundMatches,
2583
- loaderData: {},
2584
- actionData: null,
2585
- errors: {
2586
- [route.id]: error
2587
- },
2588
- statusCode: 404,
2589
- loaderHeaders: {},
2590
- actionHeaders: {}
2591
- }
2592
- };
2593
- }
2594
-
2595
- return {
2596
- location,
2597
- matches
2598
- };
2720
+ function createRouterErrorResponse(body, init) {
2721
+ return new Response(body, _extends({}, init, {
2722
+ headers: _extends({}, init.headers, {
2723
+ "X-Remix-Router-Error": "yes"
2724
+ })
2725
+ }));
2599
2726
  }
2600
2727
 
2601
2728
  return {
@@ -2644,7 +2771,7 @@ function normalizeNavigateOptions(to, opts, isFetcher) {
2644
2771
  path,
2645
2772
  submission: {
2646
2773
  formMethod: opts.formMethod,
2647
- formAction: createHref(parsePath(path)),
2774
+ formAction: stripHashFromPath(path),
2648
2775
  formEncType: opts && opts.formEncType || "application/x-www-form-urlencoded",
2649
2776
  formData: opts.formData
2650
2777
  }
@@ -2727,16 +2854,16 @@ function getMatchesToLoad(state, matches, submission, location, isRevalidationRe
2727
2854
 
2728
2855
  let revalidatingFetchers = [];
2729
2856
  fetchLoadMatches && fetchLoadMatches.forEach((_ref10, key) => {
2730
- let [href, match] = _ref10;
2857
+ let [href, match, fetchMatches] = _ref10;
2731
2858
 
2732
2859
  // This fetcher was cancelled from a prior action submission - force reload
2733
2860
  if (cancelledFetcherLoads.includes(key)) {
2734
- revalidatingFetchers.push([key, href, match]);
2861
+ revalidatingFetchers.push([key, href, match, fetchMatches]);
2735
2862
  } else if (isRevalidationRequired) {
2736
2863
  let shouldRevalidate = shouldRevalidateLoader(href, match, submission, href, match, isRevalidationRequired, actionResult);
2737
2864
 
2738
2865
  if (shouldRevalidate) {
2739
- revalidatingFetchers.push([key, href, match]);
2866
+ revalidatingFetchers.push([key, href, match, fetchMatches]);
2740
2867
  }
2741
2868
  }
2742
2869
  });
@@ -2798,9 +2925,9 @@ function shouldRevalidateLoader(currentLocation, currentMatch, submission, locat
2798
2925
  return defaultShouldRevalidate;
2799
2926
  }
2800
2927
 
2801
- async function callLoaderOrAction(type, request, match, skipRedirects, isRouteRequest) {
2802
- if (skipRedirects === void 0) {
2803
- skipRedirects = false;
2928
+ async function callLoaderOrAction(type, request, match, matches, basename, isStaticRequest, isRouteRequest) {
2929
+ if (isStaticRequest === void 0) {
2930
+ isStaticRequest = false;
2804
2931
  }
2805
2932
 
2806
2933
  if (isRouteRequest === void 0) {
@@ -2832,20 +2959,30 @@ async function callLoaderOrAction(type, request, match, skipRedirects, isRouteRe
2832
2959
  }
2833
2960
 
2834
2961
  if (result instanceof Response) {
2835
- // Process redirects
2836
- let status = result.status;
2837
- let location = result.headers.get("Location"); // For SSR single-route requests, we want to hand Responses back directly
2838
- // without unwrapping
2962
+ let status = result.status; // Process redirects
2839
2963
 
2840
- if (isRouteRequest) {
2841
- throw result;
2842
- }
2964
+ if (status >= 300 && status <= 399) {
2965
+ let location = result.headers.get("Location");
2966
+ invariant(location, "Redirects returned/thrown from loaders/actions must have a Location header"); // Support relative routing in redirects
2967
+
2968
+ let activeMatches = matches.slice(0, matches.indexOf(match) + 1);
2969
+ let routePathnames = getPathContributingMatches(activeMatches).map(match => match.pathnameBase);
2970
+ let requestPath = createURL(request.url).pathname;
2971
+ let resolvedLocation = resolveTo(location, routePathnames, requestPath);
2972
+ invariant(createPath(resolvedLocation), "Unable to resolve redirect location: " + result.headers.get("Location")); // Prepend the basename to the redirect location if we have one
2843
2973
 
2844
- if (status >= 300 && status <= 399 && location != null) {
2845
- // Don't process redirects in the router during SSR document requests.
2974
+ if (basename) {
2975
+ let path = resolvedLocation.pathname;
2976
+ resolvedLocation.pathname = path === "/" ? basename : joinPaths([basename, path]);
2977
+ }
2978
+
2979
+ location = createPath(resolvedLocation); // Don't process redirects in the router during static requests requests.
2846
2980
  // Instead, throw the Response and let the server handle it with an HTTP
2847
- // redirect
2848
- if (skipRedirects) {
2981
+ // redirect. We also update the Location header in place in this flow so
2982
+ // basename and relative routing is taken into account
2983
+
2984
+ if (isStaticRequest) {
2985
+ result.headers.set("Location", location);
2849
2986
  throw result;
2850
2987
  }
2851
2988
 
@@ -2855,6 +2992,17 @@ async function callLoaderOrAction(type, request, match, skipRedirects, isRouteRe
2855
2992
  location,
2856
2993
  revalidate: result.headers.get("X-Remix-Revalidate") !== null
2857
2994
  };
2995
+ } // For SSR single-route requests, we want to hand Responses back directly
2996
+ // without unwrapping. We do this with the QueryRouteResponse wrapper
2997
+ // interface so we can know whether it was returned or thrown
2998
+
2999
+
3000
+ if (isRouteRequest) {
3001
+ // eslint-disable-next-line no-throw-literal
3002
+ throw {
3003
+ type: resultType || ResultType.data,
3004
+ response: result
3005
+ };
2858
3006
  }
2859
3007
 
2860
3008
  let data;
@@ -2903,7 +3051,7 @@ async function callLoaderOrAction(type, request, match, skipRedirects, isRouteRe
2903
3051
  }
2904
3052
 
2905
3053
  function createRequest(location, signal, submission) {
2906
- let url = createURL(location).toString();
3054
+ let url = createURL(stripHashFromPath(location)).toString();
2907
3055
  let init = {
2908
3056
  signal
2909
3057
  };
@@ -3070,10 +3218,10 @@ function findNearestBoundary(matches, routeId) {
3070
3218
  return eligibleMatches.reverse().find(m => m.route.hasErrorBoundary === true) || matches[0];
3071
3219
  }
3072
3220
 
3073
- function getNotFoundMatches(routes) {
3221
+ function getShortCircuitMatches(routes, status, statusText) {
3074
3222
  // Prefer a root layout route if present, otherwise shim in a route object
3075
- let route = routes.find(r => r.index || r.path === "" || r.path === "/") || {
3076
- id: "__shim-404-route__"
3223
+ let route = routes.find(r => r.index || !r.path || r.path === "/") || {
3224
+ id: "__shim-" + status + "-route__"
3077
3225
  };
3078
3226
  return {
3079
3227
  matches: [{
@@ -3083,16 +3231,24 @@ function getNotFoundMatches(routes) {
3083
3231
  route
3084
3232
  }],
3085
3233
  route,
3086
- error: new ErrorResponse(404, "Not Found", null)
3234
+ error: new ErrorResponse(status, statusText, null)
3087
3235
  };
3088
3236
  }
3089
3237
 
3238
+ function getNotFoundMatches(routes) {
3239
+ return getShortCircuitMatches(routes, 404, "Not Found");
3240
+ }
3241
+
3242
+ function getMethodNotAllowedMatches(routes) {
3243
+ return getShortCircuitMatches(routes, 405, "Method Not Allowed");
3244
+ }
3245
+
3090
3246
  function getMethodNotAllowedResult(path) {
3091
- let href = typeof path === "string" ? path : createHref(path);
3247
+ let href = typeof path === "string" ? path : createPath(path);
3092
3248
  console.warn("You're trying to submit to a route that does not have an action. To " + "fix this, please add an `action` function to the route for " + ("[" + href + "]"));
3093
3249
  return {
3094
3250
  type: ResultType.error,
3095
- error: new ErrorResponse(405, "Method Not Allowed", "No action found for [" + href + "]")
3251
+ error: new ErrorResponse(405, "Method Not Allowed", "")
3096
3252
  };
3097
3253
  } // Find any returned redirect errors, starting from the lowest match
3098
3254
 
@@ -3105,11 +3261,13 @@ function findRedirect(results) {
3105
3261
  return result;
3106
3262
  }
3107
3263
  }
3108
- } // Create an href to represent a "server" URL without the hash
3109
-
3264
+ }
3110
3265
 
3111
- function createHref(location) {
3112
- return (location.pathname || "") + (location.search || "");
3266
+ function stripHashFromPath(path) {
3267
+ let parsedPath = typeof path === "string" ? parsePath(path) : path;
3268
+ return createPath(_extends({}, parsedPath, {
3269
+ hash: ""
3270
+ }));
3113
3271
  }
3114
3272
 
3115
3273
  function isHashChangeOnly(a, b) {
@@ -3128,6 +3286,20 @@ function isRedirectResult(result) {
3128
3286
  return (result && result.type) === ResultType.redirect;
3129
3287
  }
3130
3288
 
3289
+ function isRedirectResponse(result) {
3290
+ if (!(result instanceof Response)) {
3291
+ return false;
3292
+ }
3293
+
3294
+ let status = result.status;
3295
+ let location = result.headers.get("Location");
3296
+ return status >= 300 && status <= 399 && location != null;
3297
+ }
3298
+
3299
+ function isQueryRouteResponse(obj) {
3300
+ return obj && obj.response instanceof Response && (obj.type === ResultType.data || ResultType.error);
3301
+ }
3302
+
3131
3303
  async function resolveDeferredResults(currentMatches, matchesToLoad, results, signal, isFetcher, currentLoaderData) {
3132
3304
  for (let index = 0; index < results.length; index++) {
3133
3305
  let result = results[index];
@@ -3204,17 +3376,15 @@ function createUseMatchesMatch(match, loaderData) {
3204
3376
  function getTargetMatch(matches, location) {
3205
3377
  let search = typeof location === "string" ? parsePath(location).search : location.search;
3206
3378
 
3207
- if (matches[matches.length - 1].route.index && !hasNakedIndexQuery(search || "")) {
3208
- return matches.slice(-2)[0];
3209
- }
3379
+ if (matches[matches.length - 1].route.index && hasNakedIndexQuery(search || "")) {
3380
+ // Return the leaf index route when index is present
3381
+ return matches[matches.length - 1];
3382
+ } // Otherwise grab the deepest "path contributing" match (ignoring index and
3383
+ // pathless layout routes)
3210
3384
 
3211
- return matches.slice(-1)[0];
3212
- }
3213
3385
 
3214
- function createURL(location) {
3215
- let base = typeof window !== "undefined" && typeof window.location !== "undefined" ? window.location.origin : "unknown://unknown";
3216
- let href = typeof location === "string" ? location : createHref(location);
3217
- return new URL(href, base);
3386
+ let pathMatches = getPathContributingMatches(matches);
3387
+ return pathMatches[pathMatches.length - 1];
3218
3388
  } //#endregion
3219
3389
 
3220
3390
  exports.AbortedDeferredError = AbortedDeferredError;
@@ -3222,6 +3392,7 @@ exports.ErrorResponse = ErrorResponse;
3222
3392
  exports.IDLE_FETCHER = IDLE_FETCHER;
3223
3393
  exports.IDLE_NAVIGATION = IDLE_NAVIGATION;
3224
3394
  exports.UNSAFE_convertRoutesToDataRoutes = convertRoutesToDataRoutes;
3395
+ exports.UNSAFE_getPathContributingMatches = getPathContributingMatches;
3225
3396
  exports.createBrowserHistory = createBrowserHistory;
3226
3397
  exports.createHashHistory = createHashHistory;
3227
3398
  exports.createMemoryHistory = createMemoryHistory;