@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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @remix-run/router v1.0.2
2
+ * @remix-run/router v1.0.3-pre.0
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -413,7 +413,7 @@ function getUrlBasedHistory(getLocation, createHref, validateLocation, options)
413
413
  if (v5Compat && listener) {
414
414
  listener({
415
415
  action,
416
- location
416
+ location: history.location
417
417
  });
418
418
  }
419
419
  }
@@ -429,7 +429,7 @@ function getUrlBasedHistory(getLocation, createHref, validateLocation, options)
429
429
  if (v5Compat && listener) {
430
430
  listener({
431
431
  action,
432
- location: location
432
+ location: history.location
433
433
  });
434
434
  }
435
435
  }
@@ -548,7 +548,10 @@ function matchRoutes(routes, locationArg, basename) {
548
548
  let matches = null;
549
549
 
550
550
  for (let i = 0; matches == null && i < branches.length; ++i) {
551
- matches = matchRouteBranch(branches[i], pathname);
551
+ matches = matchRouteBranch(branches[i], // incoming pathnames are always encoded from either window.location or
552
+ // from route.navigate, but we want to match against the unencoded paths
553
+ // in the route definitions
554
+ safelyDecodeURI(pathname));
552
555
  }
553
556
 
554
557
  return matches;
@@ -795,6 +798,15 @@ function compilePath(path, caseSensitive, end) {
795
798
  return [matcher, paramNames];
796
799
  }
797
800
 
801
+ function safelyDecodeURI(value) {
802
+ try {
803
+ return decodeURI(value);
804
+ } catch (error) {
805
+ 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 + ")."));
806
+ return value;
807
+ }
808
+ }
809
+
798
810
  function safelyDecodeURIComponent(value, paramName) {
799
811
  try {
800
812
  return decodeURIComponent(value);
@@ -898,9 +910,36 @@ function getInvalidPathError(char, field, dest, path) {
898
910
  }
899
911
  /**
900
912
  * @private
913
+ *
914
+ * When processing relative navigation we want to ignore ancestor routes that
915
+ * do not contribute to the path, such that index/pathless layout routes don't
916
+ * interfere.
917
+ *
918
+ * For example, when moving a route element into an index route and/or a
919
+ * pathless layout route, relative link behavior contained within should stay
920
+ * the same. Both of the following examples should link back to the root:
921
+ *
922
+ * <Route path="/">
923
+ * <Route path="accounts" element={<Link to=".."}>
924
+ * </Route>
925
+ *
926
+ * <Route path="/">
927
+ * <Route path="accounts">
928
+ * <Route element={<AccountsLayout />}> // <-- Does not contribute
929
+ * <Route index element={<Link to=".."} /> // <-- Does not contribute
930
+ * </Route
931
+ * </Route>
932
+ * </Route>
901
933
  */
902
934
 
903
935
 
936
+ function getPathContributingMatches(matches) {
937
+ return matches.filter((match, index) => index === 0 || match.route.path && match.route.path.length > 0);
938
+ }
939
+ /**
940
+ * @private
941
+ */
942
+
904
943
  function resolveTo(toArg, routePathnames, locationPathname, isPathRelative) {
905
944
  if (isPathRelative === void 0) {
906
945
  isPathRelative = false;
@@ -1228,7 +1267,9 @@ const IDLE_FETCHER = {
1228
1267
  formAction: undefined,
1229
1268
  formEncType: undefined,
1230
1269
  formData: undefined
1231
- }; //#endregion
1270
+ };
1271
+ const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined";
1272
+ const isServer = !isBrowser; //#endregion
1232
1273
  ////////////////////////////////////////////////////////////////////////////////
1233
1274
  //#region createRouter
1234
1275
  ////////////////////////////////////////////////////////////////////////////////
@@ -1435,7 +1476,18 @@ function createRouter(init) {
1435
1476
  submission,
1436
1477
  error
1437
1478
  } = normalizeNavigateOptions(to, opts);
1438
- let location = createLocation(state.location, path, opts && opts.state);
1479
+ let location = createLocation(state.location, path, opts && opts.state); // When using navigate as a PUSH/REPLACE we aren't reading an already-encoded
1480
+ // URL from window.location, so we need to encode it here so the behavior
1481
+ // remains the same as POP and non-data-router usages. new URL() does all
1482
+ // the same encoding we'd get from a history.pushState/window.location read
1483
+ // without having to touch history
1484
+
1485
+ let url = createURL(createPath(location));
1486
+ location = _extends({}, location, {
1487
+ pathname: url.pathname,
1488
+ search: url.search,
1489
+ hash: url.hash
1490
+ });
1439
1491
  let historyAction = (opts && opts.replace) === true || submission != null ? exports.Action.Replace : exports.Action.Push;
1440
1492
  let preventScrollReset = opts && "preventScrollReset" in opts ? opts.preventScrollReset === true : undefined;
1441
1493
  return await startNavigation(historyAction, location, {
@@ -1601,7 +1653,7 @@ function createRouter(init) {
1601
1653
  if (!actionMatch.route.action) {
1602
1654
  result = getMethodNotAllowedResult(location);
1603
1655
  } else {
1604
- result = await callLoaderOrAction("action", request, actionMatch);
1656
+ result = await callLoaderOrAction("action", request, actionMatch, matches, router.basename);
1605
1657
 
1606
1658
  if (request.signal.aborted) {
1607
1659
  return {
@@ -1696,7 +1748,7 @@ function createRouter(init) {
1696
1748
  if (!isUninterruptedRevalidation) {
1697
1749
  revalidatingFetchers.forEach(_ref2 => {
1698
1750
  let [key] = _ref2;
1699
- const fetcher = state.fetchers.get(key);
1751
+ let fetcher = state.fetchers.get(key);
1700
1752
  let revalidatingFetcher = {
1701
1753
  state: "loading",
1702
1754
  data: fetcher && fetcher.data,
@@ -1724,7 +1776,7 @@ function createRouter(init) {
1724
1776
  results,
1725
1777
  loaderResults,
1726
1778
  fetcherResults
1727
- } = await callLoadersAndMaybeResolveData(state.matches, matchesToLoad, revalidatingFetchers, request);
1779
+ } = await callLoadersAndMaybeResolveData(state.matches, matches, matchesToLoad, revalidatingFetchers, request);
1728
1780
 
1729
1781
  if (request.signal.aborted) {
1730
1782
  return {
@@ -1782,7 +1834,7 @@ function createRouter(init) {
1782
1834
 
1783
1835
 
1784
1836
  function fetch(key, routeId, href, opts) {
1785
- if (typeof AbortController === "undefined") {
1837
+ if (isServer) {
1786
1838
  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
1839
  }
1788
1840
 
@@ -1801,19 +1853,19 @@ function createRouter(init) {
1801
1853
  let match = getTargetMatch(matches, path);
1802
1854
 
1803
1855
  if (submission) {
1804
- handleFetcherAction(key, routeId, path, match, submission);
1856
+ handleFetcherAction(key, routeId, path, match, matches, submission);
1805
1857
  return;
1806
1858
  } // Store off the match so we can call it's shouldRevalidate on subsequent
1807
1859
  // revalidations
1808
1860
 
1809
1861
 
1810
- fetchLoadMatches.set(key, [path, match]);
1811
- handleFetcherLoader(key, routeId, path, match);
1862
+ fetchLoadMatches.set(key, [path, match, matches]);
1863
+ handleFetcherLoader(key, routeId, path, match, matches);
1812
1864
  } // Call the action for the matched fetcher.submit(), and then handle redirects,
1813
1865
  // errors, and revalidation
1814
1866
 
1815
1867
 
1816
- async function handleFetcherAction(key, routeId, path, match, submission) {
1868
+ async function handleFetcherAction(key, routeId, path, match, requestMatches, submission) {
1817
1869
  interruptActiveLoads();
1818
1870
  fetchLoadMatches.delete(key);
1819
1871
 
@@ -1842,7 +1894,7 @@ function createRouter(init) {
1842
1894
  let abortController = new AbortController();
1843
1895
  let fetchRequest = createRequest(path, abortController.signal, submission);
1844
1896
  fetchControllers.set(key, abortController);
1845
- let actionResult = await callLoaderOrAction("action", fetchRequest, match);
1897
+ let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, router.basename);
1846
1898
 
1847
1899
  if (fetchRequest.signal.aborted) {
1848
1900
  // We can delete this so long as we weren't aborted by ou our own fetcher
@@ -1934,7 +1986,7 @@ function createRouter(init) {
1934
1986
  results,
1935
1987
  loaderResults,
1936
1988
  fetcherResults
1937
- } = await callLoadersAndMaybeResolveData(state.matches, matchesToLoad, revalidatingFetchers, revalidationRequest);
1989
+ } = await callLoadersAndMaybeResolveData(state.matches, matches, matchesToLoad, revalidatingFetchers, revalidationRequest);
1938
1990
 
1939
1991
  if (abortController.signal.aborted) {
1940
1992
  return;
@@ -1996,7 +2048,7 @@ function createRouter(init) {
1996
2048
  } // Call the matched loader for fetcher.load(), handling redirects, errors, etc.
1997
2049
 
1998
2050
 
1999
- async function handleFetcherLoader(key, routeId, path, match) {
2051
+ async function handleFetcherLoader(key, routeId, path, match, matches) {
2000
2052
  let existingFetcher = state.fetchers.get(key); // Put this fetcher into it's loading state
2001
2053
 
2002
2054
  let loadingFetcher = {
@@ -2015,7 +2067,7 @@ function createRouter(init) {
2015
2067
  let abortController = new AbortController();
2016
2068
  let fetchRequest = createRequest(path, abortController.signal);
2017
2069
  fetchControllers.set(key, abortController);
2018
- let result = await callLoaderOrAction("loader", fetchRequest, match); // Deferred isn't supported or fetcher loads, await everything and treat it
2070
+ let result = await callLoaderOrAction("loader", fetchRequest, match, matches, router.basename); // Deferred isn't supported or fetcher loads, await everything and treat it
2019
2071
  // as a normal load. resolveDeferredData will return undefined if this
2020
2072
  // fetcher gets aborted, so we just leave result untouched and short circuit
2021
2073
  // below if that happens
@@ -2108,13 +2160,13 @@ function createRouter(init) {
2108
2160
  });
2109
2161
  }
2110
2162
 
2111
- async function callLoadersAndMaybeResolveData(currentMatches, matchesToLoad, fetchersToLoad, request) {
2163
+ async function callLoadersAndMaybeResolveData(currentMatches, matches, matchesToLoad, fetchersToLoad, request) {
2112
2164
  // Call all navigation loaders and revalidating fetcher loaders in parallel,
2113
2165
  // then slice off the results into separate arrays so we can handle them
2114
2166
  // 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);
2167
+ let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, router.basename)), ...fetchersToLoad.map(_ref8 => {
2168
+ let [, href, match, fetchMatches] = _ref8;
2169
+ return callLoaderOrAction("loader", createRequest(href, request.signal), match, fetchMatches, router.basename);
2118
2170
  })]);
2119
2171
  let loaderResults = results.slice(0, matchesToLoad.length);
2120
2172
  let fetcherResults = results.slice(matchesToLoad.length);
@@ -2306,7 +2358,9 @@ function createRouter(init) {
2306
2358
  navigate,
2307
2359
  fetch,
2308
2360
  revalidate,
2309
- createHref,
2361
+ // Passthrough to history-aware createHref used by useHref so we get proper
2362
+ // hash-aware URLs in DOM paths
2363
+ createHref: to => init.history.createHref(to),
2310
2364
  getFetcher,
2311
2365
  deleteFetcher,
2312
2366
  dispose,
@@ -2319,15 +2373,75 @@ function createRouter(init) {
2319
2373
  //#region createStaticHandler
2320
2374
  ////////////////////////////////////////////////////////////////////////////////
2321
2375
 
2376
+ const validActionMethods = new Set(["POST", "PUT", "PATCH", "DELETE"]);
2377
+ const validRequestMethods = new Set(["GET", "HEAD", ...validActionMethods]);
2322
2378
  function unstable_createStaticHandler(routes) {
2323
2379
  invariant(routes.length > 0, "You must provide a non-empty routes array to unstable_createStaticHandler");
2324
2380
  let dataRoutes = convertRoutesToDataRoutes(routes);
2381
+ /**
2382
+ * The query() method is intended for document requests, in which we want to
2383
+ * call an optional action and potentially multiple loaders for all nested
2384
+ * routes. It returns a StaticHandlerContext object, which is very similar
2385
+ * to the router state (location, loaderData, actionData, errors, etc.) and
2386
+ * also adds SSR-specific information such as the statusCode and headers
2387
+ * from action/loaders Responses.
2388
+ *
2389
+ * It _should_ never throw and should report all errors through the
2390
+ * returned context.errors object, properly associating errors to their error
2391
+ * boundary. Additionally, it tracks _deepestRenderedBoundaryId which can be
2392
+ * used to emulate React error boundaries during SSr by performing a second
2393
+ * pass only down to the boundaryId.
2394
+ *
2395
+ * The one exception where we do not return a StaticHandlerContext is when a
2396
+ * redirect response is returned or thrown from any action/loader. We
2397
+ * propagate that out and return the raw Response so the HTTP server can
2398
+ * return it directly.
2399
+ */
2325
2400
 
2326
2401
  async function query(request) {
2327
- let {
2328
- location,
2329
- result
2330
- } = await queryImpl(request);
2402
+ let url = new URL(request.url);
2403
+ let location = createLocation("", createPath(url), null, "default");
2404
+ let matches = matchRoutes(dataRoutes, location);
2405
+
2406
+ if (!validRequestMethods.has(request.method)) {
2407
+ let {
2408
+ matches: methodNotAllowedMatches,
2409
+ route,
2410
+ error
2411
+ } = getMethodNotAllowedMatches(dataRoutes);
2412
+ return {
2413
+ location,
2414
+ matches: methodNotAllowedMatches,
2415
+ loaderData: {},
2416
+ actionData: null,
2417
+ errors: {
2418
+ [route.id]: error
2419
+ },
2420
+ statusCode: error.status,
2421
+ loaderHeaders: {},
2422
+ actionHeaders: {}
2423
+ };
2424
+ } else if (!matches) {
2425
+ let {
2426
+ matches: notFoundMatches,
2427
+ route,
2428
+ error
2429
+ } = getNotFoundMatches(dataRoutes);
2430
+ return {
2431
+ location,
2432
+ matches: notFoundMatches,
2433
+ loaderData: {},
2434
+ actionData: null,
2435
+ errors: {
2436
+ [route.id]: error
2437
+ },
2438
+ statusCode: error.status,
2439
+ loaderHeaders: {},
2440
+ actionHeaders: {}
2441
+ };
2442
+ }
2443
+
2444
+ let result = await queryImpl(request, location, matches);
2331
2445
 
2332
2446
  if (result instanceof Response) {
2333
2447
  return result;
@@ -2340,11 +2454,52 @@ function unstable_createStaticHandler(routes) {
2340
2454
  location
2341
2455
  }, result);
2342
2456
  }
2457
+ /**
2458
+ * The queryRoute() method is intended for targeted route requests, either
2459
+ * for fetch ?_data requests or resource route requests. In this case, we
2460
+ * are only ever calling a single action or loader, and we are returning the
2461
+ * returned value directly. In most cases, this will be a Response returned
2462
+ * from the action/loader, but it may be a primitive or other value as well -
2463
+ * and in such cases the calling context should handle that accordingly.
2464
+ *
2465
+ * We do respect the throw/return differentiation, so if an action/loader
2466
+ * throws, then this method will throw the value. This is important so we
2467
+ * can do proper boundary identification in Remix where a thrown Response
2468
+ * must go to the Catch Boundary but a returned Response is happy-path.
2469
+ *
2470
+ * One thing to note is that any Router-initiated thrown Response (such as a
2471
+ * 404 or 405) will have a custom X-Remix-Router-Error: "yes" header on it
2472
+ * in order to differentiate from responses thrown from user actions/loaders.
2473
+ */
2474
+
2343
2475
 
2344
2476
  async function queryRoute(request, routeId) {
2345
- let {
2346
- result
2347
- } = await queryImpl(request, routeId);
2477
+ let url = new URL(request.url);
2478
+ let location = createLocation("", createPath(url), null, "default");
2479
+ let matches = matchRoutes(dataRoutes, location);
2480
+
2481
+ if (!validRequestMethods.has(request.method)) {
2482
+ throw createRouterErrorResponse(null, {
2483
+ status: 405,
2484
+ statusText: "Method Not Allowed"
2485
+ });
2486
+ } else if (!matches) {
2487
+ throw createRouterErrorResponse(null, {
2488
+ status: 404,
2489
+ statusText: "Not Found"
2490
+ });
2491
+ }
2492
+
2493
+ let match = routeId ? matches.find(m => m.route.id === routeId) : getTargetMatch(matches, location);
2494
+
2495
+ if (!match) {
2496
+ throw createRouterErrorResponse(null, {
2497
+ status: 404,
2498
+ statusText: "Not Found"
2499
+ });
2500
+ }
2501
+
2502
+ let result = await queryImpl(request, location, matches, match);
2348
2503
 
2349
2504
  if (result instanceof Response) {
2350
2505
  return result;
@@ -2353,77 +2508,48 @@ function unstable_createStaticHandler(routes) {
2353
2508
  let error = result.errors ? Object.values(result.errors)[0] : undefined;
2354
2509
 
2355
2510
  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
2511
+ // If we got back result.errors, that means the loader/action threw
2366
2512
  // _something_ that wasn't a Response, but it's not guaranteed/required
2367
2513
  // to be an `instanceof Error` either, so we have to use throw here to
2368
2514
  // preserve the "error" state outside of queryImpl.
2369
-
2370
-
2371
2515
  throw error;
2372
2516
  } // Pick off the right state value to return
2373
2517
 
2374
2518
 
2375
2519
  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;
2520
+ return Object.values(routeData || {})[0];
2386
2521
  }
2387
2522
 
2388
- async function queryImpl(request, routeId) {
2389
- invariant(request.method !== "HEAD", "query()/queryRoute() do not support HEAD requests");
2523
+ async function queryImpl(request, location, matches, routeMatch) {
2390
2524
  invariant(request.signal, "query()/queryRoute() requests must contain an AbortController signal");
2391
- let {
2392
- location,
2393
- matches,
2394
- shortCircuitState
2395
- } = matchRequest(request, routeId);
2396
2525
 
2397
2526
  try {
2398
- if (shortCircuitState) {
2399
- return {
2400
- location,
2401
- result: shortCircuitState
2402
- };
2527
+ if (validActionMethods.has(request.method)) {
2528
+ let result = await submit(request, matches, routeMatch || getTargetMatch(matches, location), routeMatch != null);
2529
+ return result;
2403
2530
  }
2404
2531
 
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
- };
2532
+ let result = await loadRouteData(request, matches, routeMatch);
2533
+ return result instanceof Response ? result : _extends({}, result, {
2534
+ actionData: null,
2535
+ actionHeaders: {}
2536
+ });
2421
2537
  } catch (e) {
2422
- if (e instanceof Response) {
2423
- return {
2424
- location,
2425
- result: e
2426
- };
2538
+ // If the user threw/returned a Response in callLoaderOrAction, we throw
2539
+ // it to bail out and then return or throw here based on whether the user
2540
+ // returned or threw
2541
+ if (isQueryRouteResponse(e)) {
2542
+ if (e.type === ResultType.error && !isRedirectResponse(e.response)) {
2543
+ throw e.response;
2544
+ }
2545
+
2546
+ return e.response;
2547
+ } // Redirects are always returned since they don't propagate to catch
2548
+ // boundaries
2549
+
2550
+
2551
+ if (isRedirectResponse(e)) {
2552
+ return e;
2427
2553
  }
2428
2554
 
2429
2555
  throw e;
@@ -2434,10 +2560,19 @@ function unstable_createStaticHandler(routes) {
2434
2560
  let result;
2435
2561
 
2436
2562
  if (!actionMatch.route.action) {
2437
- let href = createHref(new URL(request.url));
2563
+ let href = createServerHref(new URL(request.url));
2564
+
2565
+ if (isRouteRequest) {
2566
+ throw createRouterErrorResponse(null, {
2567
+ status: 405,
2568
+ statusText: "Method Not Allowed"
2569
+ });
2570
+ }
2571
+
2438
2572
  result = getMethodNotAllowedResult(href);
2439
2573
  } else {
2440
- result = await callLoaderOrAction("action", request, actionMatch, true, isRouteRequest);
2574
+ result = await callLoaderOrAction("action", request, actionMatch, matches, undefined, // Basename not currently supported in static handlers
2575
+ true, isRouteRequest);
2441
2576
 
2442
2577
  if (request.signal.aborted) {
2443
2578
  let method = isRouteRequest ? "queryRoute" : "query";
@@ -2447,7 +2582,7 @@ function unstable_createStaticHandler(routes) {
2447
2582
 
2448
2583
  if (isRedirectResult(result)) {
2449
2584
  // Uhhhh - this should never happen, we should always throw these from
2450
- // calLoaderOrAction, but the type narrowing here keeps TS happy and we
2585
+ // callLoaderOrAction, but the type narrowing here keeps TS happy and we
2451
2586
  // can get back on the "throw all redirect responses" train here should
2452
2587
  // this ever happen :/
2453
2588
  throw new Response(null, {
@@ -2463,6 +2598,8 @@ function unstable_createStaticHandler(routes) {
2463
2598
  }
2464
2599
 
2465
2600
  if (isRouteRequest) {
2601
+ // Note: This should only be non-Response values if we get here, since
2602
+ // isRouteRequest should throw any Response received in callLoaderOrAction
2466
2603
  if (isErrorResult(result)) {
2467
2604
  let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
2468
2605
  return {
@@ -2499,7 +2636,7 @@ function unstable_createStaticHandler(routes) {
2499
2636
  // Store off the pending error - we use it to determine which loaders
2500
2637
  // to call and will commit it when we complete the navigation
2501
2638
  let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
2502
- let context = await loadRouteData(request, matches, isRouteRequest, {
2639
+ let context = await loadRouteData(request, matches, undefined, {
2503
2640
  [boundaryMatch.route.id]: result.error
2504
2641
  }); // action status codes take precedence over loader status codes
2505
2642
 
@@ -2512,7 +2649,7 @@ function unstable_createStaticHandler(routes) {
2512
2649
  });
2513
2650
  }
2514
2651
 
2515
- let context = await loadRouteData(request, matches, isRouteRequest);
2652
+ let context = await loadRouteData(request, matches);
2516
2653
  return _extends({}, context, result.statusCode ? {
2517
2654
  statusCode: result.statusCode
2518
2655
  } : {}, {
@@ -2525,8 +2662,10 @@ function unstable_createStaticHandler(routes) {
2525
2662
  });
2526
2663
  }
2527
2664
 
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
2665
+ async function loadRouteData(request, matches, routeMatch, pendingActionError) {
2666
+ let isRouteRequest = routeMatch != null;
2667
+ let requestMatches = routeMatch ? [routeMatch] : getLoaderMatchesUntilBoundary(matches, Object.keys(pendingActionError || {})[0]);
2668
+ let matchesToLoad = requestMatches.filter(m => m.route.loader); // Short circuit if we have no loaders to run
2530
2669
 
2531
2670
  if (matchesToLoad.length === 0) {
2532
2671
  return {
@@ -2538,7 +2677,8 @@ function unstable_createStaticHandler(routes) {
2538
2677
  };
2539
2678
  }
2540
2679
 
2541
- let results = await Promise.all([...matchesToLoad.map(m => callLoaderOrAction("loader", request, m, true, isRouteRequest))]);
2680
+ let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, undefined, // Basename not currently supported in static handlers
2681
+ true, isRouteRequest))]);
2542
2682
 
2543
2683
  if (request.signal.aborted) {
2544
2684
  let method = isRouteRequest ? "queryRoute" : "query";
@@ -2559,43 +2699,12 @@ function unstable_createStaticHandler(routes) {
2559
2699
  });
2560
2700
  }
2561
2701
 
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
- };
2702
+ function createRouterErrorResponse(body, init) {
2703
+ return new Response(body, _extends({}, init, {
2704
+ headers: _extends({}, init.headers, {
2705
+ "X-Remix-Router-Error": "yes"
2706
+ })
2707
+ }));
2599
2708
  }
2600
2709
 
2601
2710
  return {
@@ -2644,7 +2753,7 @@ function normalizeNavigateOptions(to, opts, isFetcher) {
2644
2753
  path,
2645
2754
  submission: {
2646
2755
  formMethod: opts.formMethod,
2647
- formAction: createHref(parsePath(path)),
2756
+ formAction: createServerHref(parsePath(path)),
2648
2757
  formEncType: opts && opts.formEncType || "application/x-www-form-urlencoded",
2649
2758
  formData: opts.formData
2650
2759
  }
@@ -2727,16 +2836,16 @@ function getMatchesToLoad(state, matches, submission, location, isRevalidationRe
2727
2836
 
2728
2837
  let revalidatingFetchers = [];
2729
2838
  fetchLoadMatches && fetchLoadMatches.forEach((_ref10, key) => {
2730
- let [href, match] = _ref10;
2839
+ let [href, match, fetchMatches] = _ref10;
2731
2840
 
2732
2841
  // This fetcher was cancelled from a prior action submission - force reload
2733
2842
  if (cancelledFetcherLoads.includes(key)) {
2734
- revalidatingFetchers.push([key, href, match]);
2843
+ revalidatingFetchers.push([key, href, match, fetchMatches]);
2735
2844
  } else if (isRevalidationRequired) {
2736
2845
  let shouldRevalidate = shouldRevalidateLoader(href, match, submission, href, match, isRevalidationRequired, actionResult);
2737
2846
 
2738
2847
  if (shouldRevalidate) {
2739
- revalidatingFetchers.push([key, href, match]);
2848
+ revalidatingFetchers.push([key, href, match, fetchMatches]);
2740
2849
  }
2741
2850
  }
2742
2851
  });
@@ -2798,9 +2907,9 @@ function shouldRevalidateLoader(currentLocation, currentMatch, submission, locat
2798
2907
  return defaultShouldRevalidate;
2799
2908
  }
2800
2909
 
2801
- async function callLoaderOrAction(type, request, match, skipRedirects, isRouteRequest) {
2802
- if (skipRedirects === void 0) {
2803
- skipRedirects = false;
2910
+ async function callLoaderOrAction(type, request, match, matches, basename, isStaticRequest, isRouteRequest) {
2911
+ if (isStaticRequest === void 0) {
2912
+ isStaticRequest = false;
2804
2913
  }
2805
2914
 
2806
2915
  if (isRouteRequest === void 0) {
@@ -2832,20 +2941,30 @@ async function callLoaderOrAction(type, request, match, skipRedirects, isRouteRe
2832
2941
  }
2833
2942
 
2834
2943
  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
2944
+ let status = result.status; // Process redirects
2839
2945
 
2840
- if (isRouteRequest) {
2841
- throw result;
2842
- }
2946
+ if (status >= 300 && status <= 399) {
2947
+ let location = result.headers.get("Location");
2948
+ invariant(location, "Redirects returned/thrown from loaders/actions must have a Location header"); // Support relative routing in redirects
2843
2949
 
2844
- if (status >= 300 && status <= 399 && location != null) {
2845
- // Don't process redirects in the router during SSR document requests.
2950
+ let activeMatches = matches.slice(0, matches.indexOf(match) + 1);
2951
+ let routePathnames = getPathContributingMatches(activeMatches).map(match => match.pathnameBase);
2952
+ let requestPath = createURL(request.url).pathname;
2953
+ let resolvedLocation = resolveTo(location, routePathnames, requestPath);
2954
+ invariant(createPath(resolvedLocation), "Unable to resolve redirect location: " + result.headers.get("Location")); // Prepend the basename to the redirect location if we have one
2955
+
2956
+ if (basename) {
2957
+ let path = resolvedLocation.pathname;
2958
+ resolvedLocation.pathname = path === "/" ? basename : joinPaths([basename, path]);
2959
+ }
2960
+
2961
+ location = createPath(resolvedLocation); // Don't process redirects in the router during static requests requests.
2846
2962
  // Instead, throw the Response and let the server handle it with an HTTP
2847
- // redirect
2848
- if (skipRedirects) {
2963
+ // redirect. We also update the Location header in place in this flow so
2964
+ // basename and relative routing is taken into account
2965
+
2966
+ if (isStaticRequest) {
2967
+ result.headers.set("Location", location);
2849
2968
  throw result;
2850
2969
  }
2851
2970
 
@@ -2855,6 +2974,17 @@ async function callLoaderOrAction(type, request, match, skipRedirects, isRouteRe
2855
2974
  location,
2856
2975
  revalidate: result.headers.get("X-Remix-Revalidate") !== null
2857
2976
  };
2977
+ } // For SSR single-route requests, we want to hand Responses back directly
2978
+ // without unwrapping. We do this with the QueryRouteResponse wrapper
2979
+ // interface so we can know whether it was returned or thrown
2980
+
2981
+
2982
+ if (isRouteRequest) {
2983
+ // eslint-disable-next-line no-throw-literal
2984
+ throw {
2985
+ type: resultType || ResultType.data,
2986
+ response: result
2987
+ };
2858
2988
  }
2859
2989
 
2860
2990
  let data;
@@ -3070,10 +3200,10 @@ function findNearestBoundary(matches, routeId) {
3070
3200
  return eligibleMatches.reverse().find(m => m.route.hasErrorBoundary === true) || matches[0];
3071
3201
  }
3072
3202
 
3073
- function getNotFoundMatches(routes) {
3203
+ function getShortCircuitMatches(routes, status, statusText) {
3074
3204
  // 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__"
3205
+ let route = routes.find(r => r.index || !r.path || r.path === "/") || {
3206
+ id: "__shim-" + status + "-route__"
3077
3207
  };
3078
3208
  return {
3079
3209
  matches: [{
@@ -3083,16 +3213,24 @@ function getNotFoundMatches(routes) {
3083
3213
  route
3084
3214
  }],
3085
3215
  route,
3086
- error: new ErrorResponse(404, "Not Found", null)
3216
+ error: new ErrorResponse(status, statusText, null)
3087
3217
  };
3088
3218
  }
3089
3219
 
3220
+ function getNotFoundMatches(routes) {
3221
+ return getShortCircuitMatches(routes, 404, "Not Found");
3222
+ }
3223
+
3224
+ function getMethodNotAllowedMatches(routes) {
3225
+ return getShortCircuitMatches(routes, 405, "Method Not Allowed");
3226
+ }
3227
+
3090
3228
  function getMethodNotAllowedResult(path) {
3091
- let href = typeof path === "string" ? path : createHref(path);
3229
+ let href = typeof path === "string" ? path : createServerHref(path);
3092
3230
  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
3231
  return {
3094
3232
  type: ResultType.error,
3095
- error: new ErrorResponse(405, "Method Not Allowed", "No action found for [" + href + "]")
3233
+ error: new ErrorResponse(405, "Method Not Allowed", "")
3096
3234
  };
3097
3235
  } // Find any returned redirect errors, starting from the lowest match
3098
3236
 
@@ -3108,7 +3246,7 @@ function findRedirect(results) {
3108
3246
  } // Create an href to represent a "server" URL without the hash
3109
3247
 
3110
3248
 
3111
- function createHref(location) {
3249
+ function createServerHref(location) {
3112
3250
  return (location.pathname || "") + (location.search || "");
3113
3251
  }
3114
3252
 
@@ -3128,6 +3266,20 @@ function isRedirectResult(result) {
3128
3266
  return (result && result.type) === ResultType.redirect;
3129
3267
  }
3130
3268
 
3269
+ function isRedirectResponse(result) {
3270
+ if (!(result instanceof Response)) {
3271
+ return false;
3272
+ }
3273
+
3274
+ let status = result.status;
3275
+ let location = result.headers.get("Location");
3276
+ return status >= 300 && status <= 399 && location != null;
3277
+ }
3278
+
3279
+ function isQueryRouteResponse(obj) {
3280
+ return obj && obj.response instanceof Response && (obj.type === ResultType.data || ResultType.error);
3281
+ }
3282
+
3131
3283
  async function resolveDeferredResults(currentMatches, matchesToLoad, results, signal, isFetcher, currentLoaderData) {
3132
3284
  for (let index = 0; index < results.length; index++) {
3133
3285
  let result = results[index];
@@ -3204,16 +3356,20 @@ function createUseMatchesMatch(match, loaderData) {
3204
3356
  function getTargetMatch(matches, location) {
3205
3357
  let search = typeof location === "string" ? parsePath(location).search : location.search;
3206
3358
 
3207
- if (matches[matches.length - 1].route.index && !hasNakedIndexQuery(search || "")) {
3208
- return matches.slice(-2)[0];
3209
- }
3359
+ if (matches[matches.length - 1].route.index && hasNakedIndexQuery(search || "")) {
3360
+ // Return the leaf index route when index is present
3361
+ return matches[matches.length - 1];
3362
+ } // Otherwise grab the deepest "path contributing" match (ignoring index and
3363
+ // pathless layout routes)
3364
+
3210
3365
 
3211
- return matches.slice(-1)[0];
3366
+ let pathMatches = getPathContributingMatches(matches);
3367
+ return pathMatches[pathMatches.length - 1];
3212
3368
  }
3213
3369
 
3214
3370
  function createURL(location) {
3215
3371
  let base = typeof window !== "undefined" && typeof window.location !== "undefined" ? window.location.origin : "unknown://unknown";
3216
- let href = typeof location === "string" ? location : createHref(location);
3372
+ let href = typeof location === "string" ? location : createServerHref(location);
3217
3373
  return new URL(href, base);
3218
3374
  } //#endregion
3219
3375
 
@@ -3222,6 +3378,7 @@ exports.ErrorResponse = ErrorResponse;
3222
3378
  exports.IDLE_FETCHER = IDLE_FETCHER;
3223
3379
  exports.IDLE_NAVIGATION = IDLE_NAVIGATION;
3224
3380
  exports.UNSAFE_convertRoutesToDataRoutes = convertRoutesToDataRoutes;
3381
+ exports.UNSAFE_getPathContributingMatches = getPathContributingMatches;
3225
3382
  exports.createBrowserHistory = createBrowserHistory;
3226
3383
  exports.createHashHistory = createHashHistory;
3227
3384
  exports.createMemoryHistory = createMemoryHistory;