@remix-run/router 1.0.3 → 1.0.4-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.3
2
+ * @remix-run/router v1.0.4-pre.1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -113,8 +113,13 @@ function createMemoryHistory(options) {
113
113
  return typeof to === "string" ? to : createPath(to);
114
114
  },
115
115
 
116
- encodeLocation(location) {
117
- return location;
116
+ encodeLocation(to) {
117
+ let path = typeof to === "string" ? parsePath(to) : to;
118
+ return {
119
+ pathname: path.pathname || "",
120
+ search: path.search || "",
121
+ hash: path.hash || ""
122
+ };
118
123
  },
119
124
 
120
125
  push(to, state) {
@@ -472,14 +477,14 @@ function getUrlBasedHistory(getLocation, createHref, validateLocation, options)
472
477
  return createHref(window, to);
473
478
  },
474
479
 
475
- encodeLocation(location) {
480
+ encodeLocation(to) {
476
481
  // Encode a Location the same way window.location would
477
- let url = createURL(createPath(location));
478
- return _extends({}, location, {
482
+ let url = createURL(typeof to === "string" ? to : createPath(to));
483
+ return {
479
484
  pathname: url.pathname,
480
485
  search: url.search,
481
486
  hash: url.hash
482
- });
487
+ };
483
488
  },
484
489
 
485
490
  push,
@@ -1254,10 +1259,21 @@ const redirect = function redirect(url, init) {
1254
1259
  */
1255
1260
 
1256
1261
  class ErrorResponse {
1257
- constructor(status, statusText, data) {
1262
+ constructor(status, statusText, data, internal) {
1263
+ if (internal === void 0) {
1264
+ internal = false;
1265
+ }
1266
+
1258
1267
  this.status = status;
1259
1268
  this.statusText = statusText || "";
1260
- this.data = data;
1269
+ this.internal = internal;
1270
+
1271
+ if (data instanceof Error) {
1272
+ this.data = data.toString();
1273
+ this.error = data;
1274
+ } else {
1275
+ this.data = data;
1276
+ }
1261
1277
  }
1262
1278
 
1263
1279
  }
@@ -1277,6 +1293,12 @@ function isRouteErrorResponse(e) {
1277
1293
  * A Router instance manages all navigation and data loading/mutations
1278
1294
  */
1279
1295
 
1296
+ const validActionMethodsArr = ["post", "put", "patch", "delete"];
1297
+ const validActionMethods = new Set(validActionMethodsArr);
1298
+ const validRequestMethodsArr = ["get", ...validActionMethodsArr];
1299
+ const validRequestMethods = new Set(validRequestMethodsArr);
1300
+ const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
1301
+ const redirectPreserveMethodStatusCodes = new Set([307, 308]);
1280
1302
  const IDLE_NAVIGATION = {
1281
1303
  state: "idle",
1282
1304
  location: undefined,
@@ -1327,11 +1349,13 @@ function createRouter(init) {
1327
1349
  if (initialMatches == null) {
1328
1350
  // If we do not match a user-provided-route, fall back to the root
1329
1351
  // to allow the error boundary to take over
1352
+ let error = getInternalRouterError(404, {
1353
+ pathname: init.history.location.pathname
1354
+ });
1330
1355
  let {
1331
1356
  matches,
1332
- route,
1333
- error
1334
- } = getNotFoundMatches(dataRoutes);
1357
+ route
1358
+ } = getShortCircuitMatches(dataRoutes);
1335
1359
  initialMatches = matches;
1336
1360
  initialErrors = {
1337
1361
  [route.id]: error
@@ -1507,7 +1531,7 @@ function createRouter(init) {
1507
1531
  // the same encoding we'd get from a history.pushState/window.location read
1508
1532
  // without having to touch history
1509
1533
 
1510
- location = init.history.encodeLocation(location);
1534
+ location = _extends({}, location, init.history.encodeLocation(location));
1511
1535
  let historyAction = (opts && opts.replace) === true || submission != null ? exports.Action.Replace : exports.Action.Push;
1512
1536
  let preventScrollReset = opts && "preventScrollReset" in opts ? opts.preventScrollReset === true : undefined;
1513
1537
  return await startNavigation(historyAction, location, {
@@ -1571,11 +1595,13 @@ function createRouter(init) {
1571
1595
  let matches = matchRoutes(dataRoutes, location, init.basename); // Short circuit with a 404 on the root error boundary if we match nothing
1572
1596
 
1573
1597
  if (!matches) {
1598
+ let error = getInternalRouterError(404, {
1599
+ pathname: location.pathname
1600
+ });
1574
1601
  let {
1575
1602
  matches: notFoundMatches,
1576
- route,
1577
- error
1578
- } = getNotFoundMatches(dataRoutes); // Cancel all pending deferred on 404s since we don't keep any routes
1603
+ route
1604
+ } = getShortCircuitMatches(dataRoutes); // Cancel all pending deferred on 404s since we don't keep any routes
1579
1605
 
1580
1606
  cancelActiveDeferreds();
1581
1607
  completeNavigation(location, {
@@ -1671,7 +1697,14 @@ function createRouter(init) {
1671
1697
  let actionMatch = getTargetMatch(matches, location);
1672
1698
 
1673
1699
  if (!actionMatch.route.action) {
1674
- result = getMethodNotAllowedResult(location);
1700
+ result = {
1701
+ type: ResultType.error,
1702
+ error: getInternalRouterError(405, {
1703
+ method: request.method,
1704
+ pathname: location.pathname,
1705
+ routeId: actionMatch.route.id
1706
+ })
1707
+ };
1675
1708
  } else {
1676
1709
  result = await callLoaderOrAction("action", request, actionMatch, matches, router.basename);
1677
1710
 
@@ -1683,12 +1716,7 @@ function createRouter(init) {
1683
1716
  }
1684
1717
 
1685
1718
  if (isRedirectResult(result)) {
1686
- let redirectNavigation = _extends({
1687
- state: "loading",
1688
- location: createLocation(state.location, result.location)
1689
- }, submission);
1690
-
1691
- await startRedirectNavigation(result, redirectNavigation, opts && opts.replace);
1719
+ await startRedirectNavigation(state, result, opts && opts.replace === true);
1692
1720
  return {
1693
1721
  shortCircuited: true
1694
1722
  };
@@ -1815,8 +1843,7 @@ function createRouter(init) {
1815
1843
  let redirect = findRedirect(results);
1816
1844
 
1817
1845
  if (redirect) {
1818
- let redirectNavigation = getLoaderRedirect(state, redirect);
1819
- await startRedirectNavigation(redirect, redirectNavigation, replace);
1846
+ await startRedirectNavigation(state, redirect, replace);
1820
1847
  return {
1821
1848
  shortCircuited: true
1822
1849
  };
@@ -1862,7 +1889,9 @@ function createRouter(init) {
1862
1889
  let matches = matchRoutes(dataRoutes, href, init.basename);
1863
1890
 
1864
1891
  if (!matches) {
1865
- setFetcherError(key, routeId, new ErrorResponse(404, "Not Found", null));
1892
+ setFetcherError(key, routeId, getInternalRouterError(404, {
1893
+ pathname: href
1894
+ }));
1866
1895
  return;
1867
1896
  }
1868
1897
 
@@ -1890,9 +1919,11 @@ function createRouter(init) {
1890
1919
  fetchLoadMatches.delete(key);
1891
1920
 
1892
1921
  if (!match.route.action) {
1893
- let {
1894
- error
1895
- } = getMethodNotAllowedResult(path);
1922
+ let error = getInternalRouterError(405, {
1923
+ method: submission.formMethod,
1924
+ pathname: path,
1925
+ routeId: routeId
1926
+ });
1896
1927
  setFetcherError(key, routeId, error);
1897
1928
  return;
1898
1929
  } // Put this fetcher into it's submitting state
@@ -1940,14 +1971,7 @@ function createRouter(init) {
1940
1971
  updateState({
1941
1972
  fetchers: new Map(state.fetchers)
1942
1973
  });
1943
-
1944
- let redirectNavigation = _extends({
1945
- state: "loading",
1946
- location: createLocation(state.location, actionResult.location)
1947
- }, submission);
1948
-
1949
- await startRedirectNavigation(actionResult, redirectNavigation);
1950
- return;
1974
+ return startRedirectNavigation(state, actionResult);
1951
1975
  } // Process any non-redirect errors thrown
1952
1976
 
1953
1977
 
@@ -2021,9 +2045,7 @@ function createRouter(init) {
2021
2045
  let redirect = findRedirect(results);
2022
2046
 
2023
2047
  if (redirect) {
2024
- let redirectNavigation = getLoaderRedirect(state, redirect);
2025
- await startRedirectNavigation(redirect, redirectNavigation);
2026
- return;
2048
+ return startRedirectNavigation(state, redirect);
2027
2049
  } // Process and commit output from loaders
2028
2050
 
2029
2051
 
@@ -2108,8 +2130,7 @@ function createRouter(init) {
2108
2130
 
2109
2131
 
2110
2132
  if (isRedirectResult(result)) {
2111
- let redirectNavigation = getLoaderRedirect(state, result);
2112
- await startRedirectNavigation(result, redirectNavigation);
2133
+ await startRedirectNavigation(state, result);
2113
2134
  return;
2114
2135
  } // Process any non-redirect errors thrown
2115
2136
 
@@ -2165,19 +2186,60 @@ function createRouter(init) {
2165
2186
  */
2166
2187
 
2167
2188
 
2168
- async function startRedirectNavigation(redirect, navigation, replace) {
2189
+ async function startRedirectNavigation(state, redirect, replace) {
2169
2190
  if (redirect.revalidate) {
2170
2191
  isRevalidationRequired = true;
2171
2192
  }
2172
2193
 
2173
- invariant(navigation.location, "Expected a location on the redirect navigation"); // There's no need to abort on redirects, since we don't detect the
2194
+ let redirectLocation = createLocation(state.location, redirect.location);
2195
+ invariant(redirectLocation, "Expected a location on the redirect navigation");
2196
+
2197
+ if (redirect.external && typeof window !== "undefined" && typeof window.location !== "undefined") {
2198
+ if (replace) {
2199
+ window.location.replace(redirect.location);
2200
+ } else {
2201
+ window.location.assign(redirect.location);
2202
+ }
2203
+
2204
+ return;
2205
+ } // There's no need to abort on redirects, since we don't detect the
2174
2206
  // redirect until the action/loaders have settled
2175
2207
 
2208
+
2176
2209
  pendingNavigationController = null;
2177
2210
  let redirectHistoryAction = replace === true ? exports.Action.Replace : exports.Action.Push;
2178
- await startNavigation(redirectHistoryAction, navigation.location, {
2179
- overrideNavigation: navigation
2180
- });
2211
+ let {
2212
+ formMethod,
2213
+ formAction,
2214
+ formEncType,
2215
+ formData
2216
+ } = state.navigation; // If this was a 307/308 submission we want to preserve the HTTP method and
2217
+ // re-submit the POST/PUT/PATCH/DELETE as a submission navigation to the
2218
+ // redirected location
2219
+
2220
+ if (redirectPreserveMethodStatusCodes.has(redirect.status) && formMethod && isSubmissionMethod(formMethod) && formEncType && formData) {
2221
+ await startNavigation(redirectHistoryAction, redirectLocation, {
2222
+ submission: {
2223
+ formMethod,
2224
+ formAction: redirect.location,
2225
+ formEncType,
2226
+ formData
2227
+ }
2228
+ });
2229
+ } else {
2230
+ // Otherwise, we kick off a new loading navigation, preserving the
2231
+ // submission info for the duration of this navigation
2232
+ await startNavigation(redirectHistoryAction, redirectLocation, {
2233
+ overrideNavigation: {
2234
+ state: "loading",
2235
+ location: redirectLocation,
2236
+ formMethod: formMethod || undefined,
2237
+ formAction: formAction || undefined,
2238
+ formEncType: formEncType || undefined,
2239
+ formData: formData || undefined
2240
+ }
2241
+ });
2242
+ }
2181
2243
  }
2182
2244
 
2183
2245
  async function callLoadersAndMaybeResolveData(currentMatches, matches, matchesToLoad, fetchersToLoad, request) {
@@ -2381,6 +2443,7 @@ function createRouter(init) {
2381
2443
  // Passthrough to history-aware createHref used by useHref so we get proper
2382
2444
  // hash-aware URLs in DOM paths
2383
2445
  createHref: to => init.history.createHref(to),
2446
+ encodeLocation: to => init.history.encodeLocation(to),
2384
2447
  getFetcher,
2385
2448
  deleteFetcher,
2386
2449
  dispose,
@@ -2393,11 +2456,10 @@ function createRouter(init) {
2393
2456
  //#region createStaticHandler
2394
2457
  ////////////////////////////////////////////////////////////////////////////////
2395
2458
 
2396
- const validActionMethods = new Set(["POST", "PUT", "PATCH", "DELETE"]);
2397
- const validRequestMethods = new Set(["GET", "HEAD", ...validActionMethods]);
2398
- function unstable_createStaticHandler(routes) {
2459
+ function unstable_createStaticHandler(routes, opts) {
2399
2460
  invariant(routes.length > 0, "You must provide a non-empty routes array to unstable_createStaticHandler");
2400
2461
  let dataRoutes = convertRoutesToDataRoutes(routes);
2462
+ let basename = (opts ? opts.basename : null) || "/";
2401
2463
  /**
2402
2464
  * The query() method is intended for document requests, in which we want to
2403
2465
  * call an optional action and potentially multiple loaders for all nested
@@ -2420,16 +2482,20 @@ function unstable_createStaticHandler(routes) {
2420
2482
 
2421
2483
  async function query(request) {
2422
2484
  let url = new URL(request.url);
2485
+ let method = request.method.toLowerCase();
2423
2486
  let location = createLocation("", createPath(url), null, "default");
2424
- let matches = matchRoutes(dataRoutes, location);
2487
+ let matches = matchRoutes(dataRoutes, location, basename); // SSR supports HEAD requests while SPA doesn't
2425
2488
 
2426
- if (!validRequestMethods.has(request.method)) {
2489
+ if (!isValidMethod(method) && method !== "head") {
2490
+ let error = getInternalRouterError(405, {
2491
+ method
2492
+ });
2427
2493
  let {
2428
2494
  matches: methodNotAllowedMatches,
2429
- route,
2430
- error
2431
- } = getMethodNotAllowedMatches(dataRoutes);
2495
+ route
2496
+ } = getShortCircuitMatches(dataRoutes);
2432
2497
  return {
2498
+ basename,
2433
2499
  location,
2434
2500
  matches: methodNotAllowedMatches,
2435
2501
  loaderData: {},
@@ -2442,12 +2508,15 @@ function unstable_createStaticHandler(routes) {
2442
2508
  actionHeaders: {}
2443
2509
  };
2444
2510
  } else if (!matches) {
2511
+ let error = getInternalRouterError(404, {
2512
+ pathname: location.pathname
2513
+ });
2445
2514
  let {
2446
2515
  matches: notFoundMatches,
2447
- route,
2448
- error
2449
- } = getNotFoundMatches(dataRoutes);
2516
+ route
2517
+ } = getShortCircuitMatches(dataRoutes);
2450
2518
  return {
2519
+ basename,
2451
2520
  location,
2452
2521
  matches: notFoundMatches,
2453
2522
  loaderData: {},
@@ -2471,7 +2540,8 @@ function unstable_createStaticHandler(routes) {
2471
2540
 
2472
2541
 
2473
2542
  return _extends({
2474
- location
2543
+ location,
2544
+ basename
2475
2545
  }, result);
2476
2546
  }
2477
2547
  /**
@@ -2487,35 +2557,42 @@ function unstable_createStaticHandler(routes) {
2487
2557
  * can do proper boundary identification in Remix where a thrown Response
2488
2558
  * must go to the Catch Boundary but a returned Response is happy-path.
2489
2559
  *
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.
2560
+ * One thing to note is that any Router-initiated Errors that make sense
2561
+ * to associate with a status code will be thrown as an ErrorResponse
2562
+ * instance which include the raw Error, such that the calling context can
2563
+ * serialize the error as they see fit while including the proper response
2564
+ * code. Examples here are 404 and 405 errors that occur prior to reaching
2565
+ * any user-defined loaders.
2493
2566
  */
2494
2567
 
2495
2568
 
2496
2569
  async function queryRoute(request, routeId) {
2497
2570
  let url = new URL(request.url);
2571
+ let method = request.method.toLowerCase();
2498
2572
  let location = createLocation("", createPath(url), null, "default");
2499
- let matches = matchRoutes(dataRoutes, location);
2573
+ let matches = matchRoutes(dataRoutes, location, basename); // SSR supports HEAD requests while SPA doesn't
2500
2574
 
2501
- if (!validRequestMethods.has(request.method)) {
2502
- throw createRouterErrorResponse(null, {
2503
- status: 405,
2504
- statusText: "Method Not Allowed"
2575
+ if (!isValidMethod(method) && method !== "head") {
2576
+ throw getInternalRouterError(405, {
2577
+ method
2505
2578
  });
2506
2579
  } else if (!matches) {
2507
- throw createRouterErrorResponse(null, {
2508
- status: 404,
2509
- statusText: "Not Found"
2580
+ throw getInternalRouterError(404, {
2581
+ pathname: location.pathname
2510
2582
  });
2511
2583
  }
2512
2584
 
2513
2585
  let match = routeId ? matches.find(m => m.route.id === routeId) : getTargetMatch(matches, location);
2514
2586
 
2515
- if (!match) {
2516
- throw createRouterErrorResponse(null, {
2517
- status: 404,
2518
- statusText: "Not Found"
2587
+ if (routeId && !match) {
2588
+ throw getInternalRouterError(403, {
2589
+ pathname: location.pathname,
2590
+ routeId
2591
+ });
2592
+ } else if (!match) {
2593
+ // This should never hit I don't think?
2594
+ throw getInternalRouterError(404, {
2595
+ pathname: location.pathname
2519
2596
  });
2520
2597
  }
2521
2598
 
@@ -2544,7 +2621,7 @@ function unstable_createStaticHandler(routes) {
2544
2621
  invariant(request.signal, "query()/queryRoute() requests must contain an AbortController signal");
2545
2622
 
2546
2623
  try {
2547
- if (validActionMethods.has(request.method)) {
2624
+ if (isSubmissionMethod(request.method.toLowerCase())) {
2548
2625
  let result = await submit(request, matches, routeMatch || getTargetMatch(matches, location), routeMatch != null);
2549
2626
  return result;
2550
2627
  }
@@ -2580,17 +2657,22 @@ function unstable_createStaticHandler(routes) {
2580
2657
  let result;
2581
2658
 
2582
2659
  if (!actionMatch.route.action) {
2660
+ let error = getInternalRouterError(405, {
2661
+ method: request.method,
2662
+ pathname: createURL(request.url).pathname,
2663
+ routeId: actionMatch.route.id
2664
+ });
2665
+
2583
2666
  if (isRouteRequest) {
2584
- throw createRouterErrorResponse(null, {
2585
- status: 405,
2586
- statusText: "Method Not Allowed"
2587
- });
2667
+ throw error;
2588
2668
  }
2589
2669
 
2590
- result = getMethodNotAllowedResult(request.url);
2670
+ result = {
2671
+ type: ResultType.error,
2672
+ error
2673
+ };
2591
2674
  } else {
2592
- result = await callLoaderOrAction("action", request, actionMatch, matches, undefined, // Basename not currently supported in static handlers
2593
- true, isRouteRequest);
2675
+ result = await callLoaderOrAction("action", request, actionMatch, matches, basename, true, isRouteRequest);
2594
2676
 
2595
2677
  if (request.signal.aborted) {
2596
2678
  let method = isRouteRequest ? "queryRoute" : "query";
@@ -2619,20 +2701,7 @@ function unstable_createStaticHandler(routes) {
2619
2701
  // Note: This should only be non-Response values if we get here, since
2620
2702
  // isRouteRequest should throw any Response received in callLoaderOrAction
2621
2703
  if (isErrorResult(result)) {
2622
- let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
2623
- return {
2624
- matches: [actionMatch],
2625
- loaderData: {},
2626
- actionData: null,
2627
- errors: {
2628
- [boundaryMatch.route.id]: result.error
2629
- },
2630
- // Note: statusCode + headers are unused here since queryRoute will
2631
- // return the raw Response or value
2632
- statusCode: 500,
2633
- loaderHeaders: {},
2634
- actionHeaders: {}
2635
- };
2704
+ throw result.error;
2636
2705
  }
2637
2706
 
2638
2707
  return {
@@ -2681,9 +2750,18 @@ function unstable_createStaticHandler(routes) {
2681
2750
  }
2682
2751
 
2683
2752
  async function loadRouteData(request, matches, routeMatch, pendingActionError) {
2684
- let isRouteRequest = routeMatch != null;
2753
+ let isRouteRequest = routeMatch != null; // Short circuit if we have no loaders to run (queryRoute())
2754
+
2755
+ if (isRouteRequest && !(routeMatch != null && routeMatch.route.loader)) {
2756
+ throw getInternalRouterError(400, {
2757
+ method: request.method,
2758
+ pathname: createURL(request.url).pathname,
2759
+ routeId: routeMatch == null ? void 0 : routeMatch.route.id
2760
+ });
2761
+ }
2762
+
2685
2763
  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
2764
+ let matchesToLoad = requestMatches.filter(m => m.route.loader); // Short circuit if we have no loaders to run (query())
2687
2765
 
2688
2766
  if (matchesToLoad.length === 0) {
2689
2767
  return {
@@ -2695,8 +2773,7 @@ function unstable_createStaticHandler(routes) {
2695
2773
  };
2696
2774
  }
2697
2775
 
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))]);
2776
+ let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, basename, true, isRouteRequest))]);
2700
2777
 
2701
2778
  if (request.signal.aborted) {
2702
2779
  let method = isRouteRequest ? "queryRoute" : "query";
@@ -2717,14 +2794,6 @@ function unstable_createStaticHandler(routes) {
2717
2794
  });
2718
2795
  }
2719
2796
 
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
- }));
2726
- }
2727
-
2728
2797
  return {
2729
2798
  dataRoutes,
2730
2799
  query,
@@ -2749,9 +2818,14 @@ function getStaticContextFromError(routes, context, error) {
2749
2818
  });
2750
2819
 
2751
2820
  return newContext;
2821
+ }
2822
+
2823
+ function isSubmissionNavigation(opts) {
2824
+ return opts != null && "formData" in opts;
2752
2825
  } // Normalize navigation options by converting formMethod=GET formData objects to
2753
2826
  // URLSearchParams so they behave identically to links with query params
2754
2827
 
2828
+
2755
2829
  function normalizeNavigateOptions(to, opts, isFetcher) {
2756
2830
  if (isFetcher === void 0) {
2757
2831
  isFetcher = false;
@@ -2759,14 +2833,23 @@ function normalizeNavigateOptions(to, opts, isFetcher) {
2759
2833
 
2760
2834
  let path = typeof to === "string" ? to : createPath(to); // Return location verbatim on non-submission navigations
2761
2835
 
2762
- if (!opts || !("formMethod" in opts) && !("formData" in opts)) {
2836
+ if (!opts || !isSubmissionNavigation(opts)) {
2763
2837
  return {
2764
2838
  path
2765
2839
  };
2840
+ }
2841
+
2842
+ if (opts.formMethod && !isValidMethod(opts.formMethod)) {
2843
+ return {
2844
+ path,
2845
+ error: getInternalRouterError(405, {
2846
+ method: opts.formMethod
2847
+ })
2848
+ };
2766
2849
  } // Create a Submission on non-GET navigations
2767
2850
 
2768
2851
 
2769
- if (opts.formMethod != null && opts.formMethod !== "get") {
2852
+ if (opts.formMethod && isSubmissionMethod(opts.formMethod)) {
2770
2853
  return {
2771
2854
  path,
2772
2855
  submission: {
@@ -2776,13 +2859,6 @@ function normalizeNavigateOptions(to, opts, isFetcher) {
2776
2859
  formData: opts.formData
2777
2860
  }
2778
2861
  };
2779
- } // No formData to flatten for GET submission
2780
-
2781
-
2782
- if (!opts.formData) {
2783
- return {
2784
- path
2785
- };
2786
2862
  } // Flatten submission onto URLSearchParams for GET submissions
2787
2863
 
2788
2864
 
@@ -2801,31 +2877,13 @@ function normalizeNavigateOptions(to, opts, isFetcher) {
2801
2877
  } catch (e) {
2802
2878
  return {
2803
2879
  path,
2804
- error: new ErrorResponse(400, "Bad Request", "Cannot submit binary form data using GET")
2880
+ error: getInternalRouterError(400)
2805
2881
  };
2806
2882
  }
2807
2883
 
2808
2884
  return {
2809
2885
  path: createPath(parsedPath)
2810
2886
  };
2811
- }
2812
-
2813
- function getLoaderRedirect(state, redirect) {
2814
- let {
2815
- formMethod,
2816
- formAction,
2817
- formEncType,
2818
- formData
2819
- } = state.navigation;
2820
- let navigation = {
2821
- state: "loading",
2822
- location: createLocation(state.location, redirect.location),
2823
- formMethod: formMethod || undefined,
2824
- formAction: formAction || undefined,
2825
- formEncType: formEncType || undefined,
2826
- formData: formData || undefined
2827
- };
2828
- return navigation;
2829
2887
  } // Filter out all routes below any caught error as they aren't going to
2830
2888
  // render so we don't need to load them
2831
2889
 
@@ -2926,6 +2984,10 @@ function shouldRevalidateLoader(currentLocation, currentMatch, submission, locat
2926
2984
  }
2927
2985
 
2928
2986
  async function callLoaderOrAction(type, request, match, matches, basename, isStaticRequest, isRouteRequest) {
2987
+ if (basename === void 0) {
2988
+ basename = "/";
2989
+ }
2990
+
2929
2991
  if (isStaticRequest === void 0) {
2930
2992
  isStaticRequest = false;
2931
2993
  }
@@ -2951,6 +3013,7 @@ async function callLoaderOrAction(type, request, match, matches, basename, isSta
2951
3013
  request,
2952
3014
  params: match.params
2953
3015
  }), abortPromise]);
3016
+ invariant(result !== undefined, "You defined " + (type === "action" ? "an action" : "a loader") + " for route " + ("\"" + match.route.id + "\" but didn't return anything from your `" + type + "` ") + "function. Please return a value or `null`.");
2954
3017
  } catch (e) {
2955
3018
  resultType = ResultType.error;
2956
3019
  result = e;
@@ -2961,26 +3024,31 @@ async function callLoaderOrAction(type, request, match, matches, basename, isSta
2961
3024
  if (result instanceof Response) {
2962
3025
  let status = result.status; // Process redirects
2963
3026
 
2964
- if (status >= 300 && status <= 399) {
3027
+ if (redirectStatusCodes.has(status)) {
2965
3028
  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
3029
+ invariant(location, "Redirects returned/thrown from loaders/actions must have a Location header"); // Check if this an external redirect that goes to a new origin
2967
3030
 
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
3031
+ let external = createURL(location).origin !== createURL("/").origin; // Support relative routing in internal redirects
2973
3032
 
2974
- if (basename) {
2975
- let path = resolvedLocation.pathname;
2976
- resolvedLocation.pathname = path === "/" ? basename : joinPaths([basename, path]);
2977
- }
3033
+ if (!external) {
3034
+ let activeMatches = matches.slice(0, matches.indexOf(match) + 1);
3035
+ let routePathnames = getPathContributingMatches(activeMatches).map(match => match.pathnameBase);
3036
+ let requestPath = createURL(request.url).pathname;
3037
+ let resolvedLocation = resolveTo(location, routePathnames, requestPath);
3038
+ invariant(createPath(resolvedLocation), "Unable to resolve redirect location: " + location); // Prepend the basename to the redirect location if we have one
3039
+
3040
+ if (basename) {
3041
+ let path = resolvedLocation.pathname;
3042
+ resolvedLocation.pathname = path === "/" ? basename : joinPaths([basename, path]);
3043
+ }
2978
3044
 
2979
- location = createPath(resolvedLocation); // Don't process redirects in the router during static requests requests.
3045
+ location = createPath(resolvedLocation);
3046
+ } // Don't process redirects in the router during static requests requests.
2980
3047
  // Instead, throw the Response and let the server handle it with an HTTP
2981
3048
  // redirect. We also update the Location header in place in this flow so
2982
3049
  // basename and relative routing is taken into account
2983
3050
 
3051
+
2984
3052
  if (isStaticRequest) {
2985
3053
  result.headers.set("Location", location);
2986
3054
  throw result;
@@ -2990,7 +3058,8 @@ async function callLoaderOrAction(type, request, match, matches, basename, isSta
2990
3058
  type: ResultType.redirect,
2991
3059
  status,
2992
3060
  location,
2993
- revalidate: result.headers.get("X-Remix-Revalidate") !== null
3061
+ revalidate: result.headers.get("X-Remix-Revalidate") !== null,
3062
+ external
2994
3063
  };
2995
3064
  } // For SSR single-route requests, we want to hand Responses back directly
2996
3065
  // without unwrapping. We do this with the QueryRouteResponse wrapper
@@ -3218,10 +3287,10 @@ function findNearestBoundary(matches, routeId) {
3218
3287
  return eligibleMatches.reverse().find(m => m.route.hasErrorBoundary === true) || matches[0];
3219
3288
  }
3220
3289
 
3221
- function getShortCircuitMatches(routes, status, statusText) {
3290
+ function getShortCircuitMatches(routes) {
3222
3291
  // Prefer a root layout route if present, otherwise shim in a route object
3223
3292
  let route = routes.find(r => r.index || !r.path || r.path === "/") || {
3224
- id: "__shim-" + status + "-route__"
3293
+ id: "__shim-error-route__"
3225
3294
  };
3226
3295
  return {
3227
3296
  matches: [{
@@ -3230,26 +3299,45 @@ function getShortCircuitMatches(routes, status, statusText) {
3230
3299
  pathnameBase: "",
3231
3300
  route
3232
3301
  }],
3233
- route,
3234
- error: new ErrorResponse(status, statusText, null)
3302
+ route
3235
3303
  };
3236
3304
  }
3237
3305
 
3238
- function getNotFoundMatches(routes) {
3239
- return getShortCircuitMatches(routes, 404, "Not Found");
3240
- }
3306
+ function getInternalRouterError(status, _temp) {
3307
+ let {
3308
+ pathname,
3309
+ routeId,
3310
+ method,
3311
+ message
3312
+ } = _temp === void 0 ? {} : _temp;
3313
+ let statusText = "Unknown Server Error";
3314
+ let errorMessage = "Unknown @remix-run/router error";
3315
+
3316
+ if (status === 400) {
3317
+ statusText = "Bad Request";
3318
+
3319
+ if (method && pathname && routeId) {
3320
+ errorMessage = "You made a " + method + " request to \"" + pathname + "\" but " + ("did not provide a `loader` for route \"" + routeId + "\", ") + "so there is no way to handle the request.";
3321
+ } else {
3322
+ errorMessage = "Cannot submit binary form data using GET";
3323
+ }
3324
+ } else if (status === 403) {
3325
+ statusText = "Forbidden";
3326
+ errorMessage = "Route \"" + routeId + "\" does not match URL \"" + pathname + "\"";
3327
+ } else if (status === 404) {
3328
+ statusText = "Not Found";
3329
+ errorMessage = "No route matches URL \"" + pathname + "\"";
3330
+ } else if (status === 405) {
3331
+ statusText = "Method Not Allowed";
3241
3332
 
3242
- function getMethodNotAllowedMatches(routes) {
3243
- return getShortCircuitMatches(routes, 405, "Method Not Allowed");
3244
- }
3333
+ if (method && pathname && routeId) {
3334
+ errorMessage = "You made a " + method.toUpperCase() + " request to \"" + pathname + "\" but " + ("did not provide an `action` for route \"" + routeId + "\", ") + "so there is no way to handle the request.";
3335
+ } else if (method) {
3336
+ errorMessage = "Invalid request method \"" + method.toUpperCase() + "\"";
3337
+ }
3338
+ }
3245
3339
 
3246
- function getMethodNotAllowedResult(path) {
3247
- let href = typeof path === "string" ? path : createPath(path);
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 + "]"));
3249
- return {
3250
- type: ResultType.error,
3251
- error: new ErrorResponse(405, "Method Not Allowed", "")
3252
- };
3340
+ return new ErrorResponse(status || 500, statusText, new Error(errorMessage), true);
3253
3341
  } // Find any returned redirect errors, starting from the lowest match
3254
3342
 
3255
3343
 
@@ -3300,6 +3388,14 @@ function isQueryRouteResponse(obj) {
3300
3388
  return obj && obj.response instanceof Response && (obj.type === ResultType.data || ResultType.error);
3301
3389
  }
3302
3390
 
3391
+ function isValidMethod(method) {
3392
+ return validRequestMethods.has(method);
3393
+ }
3394
+
3395
+ function isSubmissionMethod(method) {
3396
+ return validActionMethods.has(method);
3397
+ }
3398
+
3303
3399
  async function resolveDeferredResults(currentMatches, matchesToLoad, results, signal, isFetcher, currentLoaderData) {
3304
3400
  for (let index = 0; index < results.length; index++) {
3305
3401
  let result = results[index];