@remix-run/router 1.0.3 → 1.0.4-pre.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/router.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @remix-run/router v1.0.3
2
+ * @remix-run/router v1.0.4-pre.0
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -116,8 +116,13 @@ function createMemoryHistory(options) {
116
116
  return typeof to === "string" ? to : createPath(to);
117
117
  },
118
118
 
119
- encodeLocation(location) {
120
- return location;
119
+ encodeLocation(to) {
120
+ let path = typeof to === "string" ? parsePath(to) : to;
121
+ return {
122
+ pathname: path.pathname || "",
123
+ search: path.search || "",
124
+ hash: path.hash || ""
125
+ };
121
126
  },
122
127
 
123
128
  push(to, state) {
@@ -449,14 +454,14 @@ function getUrlBasedHistory(getLocation, createHref, validateLocation, options)
449
454
  return createHref(window, to);
450
455
  },
451
456
 
452
- encodeLocation(location) {
457
+ encodeLocation(to) {
453
458
  // Encode a Location the same way window.location would
454
- let url = createURL(createPath(location));
455
- return _extends({}, location, {
459
+ let url = createURL(typeof to === "string" ? to : createPath(to));
460
+ return {
456
461
  pathname: url.pathname,
457
462
  search: url.search,
458
463
  hash: url.hash
459
- });
464
+ };
460
465
  },
461
466
 
462
467
  push,
@@ -1217,10 +1222,21 @@ const redirect = function redirect(url, init) {
1217
1222
  */
1218
1223
 
1219
1224
  class ErrorResponse {
1220
- constructor(status, statusText, data) {
1225
+ constructor(status, statusText, data, internal) {
1226
+ if (internal === void 0) {
1227
+ internal = false;
1228
+ }
1229
+
1221
1230
  this.status = status;
1222
1231
  this.statusText = statusText || "";
1223
- this.data = data;
1232
+ this.internal = internal;
1233
+
1234
+ if (data instanceof Error) {
1235
+ this.data = data.toString();
1236
+ this.error = data;
1237
+ } else {
1238
+ this.data = data;
1239
+ }
1224
1240
  }
1225
1241
 
1226
1242
  }
@@ -1233,6 +1249,12 @@ function isRouteErrorResponse(e) {
1233
1249
  return e instanceof ErrorResponse;
1234
1250
  }
1235
1251
 
1252
+ const validActionMethodsArr = ["post", "put", "patch", "delete"];
1253
+ const validActionMethods = new Set(validActionMethodsArr);
1254
+ const validRequestMethodsArr = ["get", ...validActionMethodsArr];
1255
+ const validRequestMethods = new Set(validRequestMethodsArr);
1256
+ const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
1257
+ const redirectPreserveMethodStatusCodes = new Set([307, 308]);
1236
1258
  const IDLE_NAVIGATION = {
1237
1259
  state: "idle",
1238
1260
  location: undefined,
@@ -1283,11 +1305,13 @@ function createRouter(init) {
1283
1305
  if (initialMatches == null) {
1284
1306
  // If we do not match a user-provided-route, fall back to the root
1285
1307
  // to allow the error boundary to take over
1308
+ let error = getInternalRouterError(404, {
1309
+ pathname: init.history.location.pathname
1310
+ });
1286
1311
  let {
1287
1312
  matches,
1288
- route,
1289
- error
1290
- } = getNotFoundMatches(dataRoutes);
1313
+ route
1314
+ } = getShortCircuitMatches(dataRoutes);
1291
1315
  initialMatches = matches;
1292
1316
  initialErrors = {
1293
1317
  [route.id]: error
@@ -1463,7 +1487,7 @@ function createRouter(init) {
1463
1487
  // the same encoding we'd get from a history.pushState/window.location read
1464
1488
  // without having to touch history
1465
1489
 
1466
- location = init.history.encodeLocation(location);
1490
+ location = _extends({}, location, init.history.encodeLocation(location));
1467
1491
  let historyAction = (opts && opts.replace) === true || submission != null ? Action.Replace : Action.Push;
1468
1492
  let preventScrollReset = opts && "preventScrollReset" in opts ? opts.preventScrollReset === true : undefined;
1469
1493
  return await startNavigation(historyAction, location, {
@@ -1527,11 +1551,13 @@ function createRouter(init) {
1527
1551
  let matches = matchRoutes(dataRoutes, location, init.basename); // Short circuit with a 404 on the root error boundary if we match nothing
1528
1552
 
1529
1553
  if (!matches) {
1554
+ let error = getInternalRouterError(404, {
1555
+ pathname: location.pathname
1556
+ });
1530
1557
  let {
1531
1558
  matches: notFoundMatches,
1532
- route,
1533
- error
1534
- } = getNotFoundMatches(dataRoutes); // Cancel all pending deferred on 404s since we don't keep any routes
1559
+ route
1560
+ } = getShortCircuitMatches(dataRoutes); // Cancel all pending deferred on 404s since we don't keep any routes
1535
1561
 
1536
1562
  cancelActiveDeferreds();
1537
1563
  completeNavigation(location, {
@@ -1627,7 +1653,14 @@ function createRouter(init) {
1627
1653
  let actionMatch = getTargetMatch(matches, location);
1628
1654
 
1629
1655
  if (!actionMatch.route.action) {
1630
- result = getMethodNotAllowedResult(location);
1656
+ result = {
1657
+ type: ResultType.error,
1658
+ error: getInternalRouterError(405, {
1659
+ method: request.method,
1660
+ pathname: location.pathname,
1661
+ routeId: actionMatch.route.id
1662
+ })
1663
+ };
1631
1664
  } else {
1632
1665
  result = await callLoaderOrAction("action", request, actionMatch, matches, router.basename);
1633
1666
 
@@ -1639,12 +1672,7 @@ function createRouter(init) {
1639
1672
  }
1640
1673
 
1641
1674
  if (isRedirectResult(result)) {
1642
- let redirectNavigation = _extends({
1643
- state: "loading",
1644
- location: createLocation(state.location, result.location)
1645
- }, submission);
1646
-
1647
- await startRedirectNavigation(result, redirectNavigation, opts && opts.replace);
1675
+ await startRedirectNavigation(state, result, opts && opts.replace === true);
1648
1676
  return {
1649
1677
  shortCircuited: true
1650
1678
  };
@@ -1771,8 +1799,7 @@ function createRouter(init) {
1771
1799
  let redirect = findRedirect(results);
1772
1800
 
1773
1801
  if (redirect) {
1774
- let redirectNavigation = getLoaderRedirect(state, redirect);
1775
- await startRedirectNavigation(redirect, redirectNavigation, replace);
1802
+ await startRedirectNavigation(state, redirect, replace);
1776
1803
  return {
1777
1804
  shortCircuited: true
1778
1805
  };
@@ -1818,7 +1845,9 @@ function createRouter(init) {
1818
1845
  let matches = matchRoutes(dataRoutes, href, init.basename);
1819
1846
 
1820
1847
  if (!matches) {
1821
- setFetcherError(key, routeId, new ErrorResponse(404, "Not Found", null));
1848
+ setFetcherError(key, routeId, getInternalRouterError(404, {
1849
+ pathname: href
1850
+ }));
1822
1851
  return;
1823
1852
  }
1824
1853
 
@@ -1846,9 +1875,11 @@ function createRouter(init) {
1846
1875
  fetchLoadMatches.delete(key);
1847
1876
 
1848
1877
  if (!match.route.action) {
1849
- let {
1850
- error
1851
- } = getMethodNotAllowedResult(path);
1878
+ let error = getInternalRouterError(405, {
1879
+ method: submission.formMethod,
1880
+ pathname: path,
1881
+ routeId: routeId
1882
+ });
1852
1883
  setFetcherError(key, routeId, error);
1853
1884
  return;
1854
1885
  } // Put this fetcher into it's submitting state
@@ -1896,14 +1927,7 @@ function createRouter(init) {
1896
1927
  updateState({
1897
1928
  fetchers: new Map(state.fetchers)
1898
1929
  });
1899
-
1900
- let redirectNavigation = _extends({
1901
- state: "loading",
1902
- location: createLocation(state.location, actionResult.location)
1903
- }, submission);
1904
-
1905
- await startRedirectNavigation(actionResult, redirectNavigation);
1906
- return;
1930
+ return startRedirectNavigation(state, actionResult);
1907
1931
  } // Process any non-redirect errors thrown
1908
1932
 
1909
1933
 
@@ -1977,9 +2001,7 @@ function createRouter(init) {
1977
2001
  let redirect = findRedirect(results);
1978
2002
 
1979
2003
  if (redirect) {
1980
- let redirectNavigation = getLoaderRedirect(state, redirect);
1981
- await startRedirectNavigation(redirect, redirectNavigation);
1982
- return;
2004
+ return startRedirectNavigation(state, redirect);
1983
2005
  } // Process and commit output from loaders
1984
2006
 
1985
2007
 
@@ -2064,8 +2086,7 @@ function createRouter(init) {
2064
2086
 
2065
2087
 
2066
2088
  if (isRedirectResult(result)) {
2067
- let redirectNavigation = getLoaderRedirect(state, result);
2068
- await startRedirectNavigation(result, redirectNavigation);
2089
+ await startRedirectNavigation(state, result);
2069
2090
  return;
2070
2091
  } // Process any non-redirect errors thrown
2071
2092
 
@@ -2121,19 +2142,55 @@ function createRouter(init) {
2121
2142
  */
2122
2143
 
2123
2144
 
2124
- async function startRedirectNavigation(redirect, navigation, replace) {
2145
+ async function startRedirectNavigation(state, redirect, replace) {
2125
2146
  if (redirect.revalidate) {
2126
2147
  isRevalidationRequired = true;
2127
2148
  }
2128
2149
 
2129
- invariant(navigation.location, "Expected a location on the redirect navigation"); // There's no need to abort on redirects, since we don't detect the
2150
+ let redirectLocation = createLocation(state.location, redirect.location);
2151
+ invariant(redirectLocation, "Expected a location on the redirect navigation");
2152
+
2153
+ if (redirect.external && typeof window !== "undefined" && typeof window.location !== "undefined") {
2154
+ window.location.replace(redirect.location);
2155
+ return;
2156
+ } // There's no need to abort on redirects, since we don't detect the
2130
2157
  // redirect until the action/loaders have settled
2131
2158
 
2159
+
2132
2160
  pendingNavigationController = null;
2133
2161
  let redirectHistoryAction = replace === true ? Action.Replace : Action.Push;
2134
- await startNavigation(redirectHistoryAction, navigation.location, {
2135
- overrideNavigation: navigation
2136
- });
2162
+ let {
2163
+ formMethod,
2164
+ formAction,
2165
+ formEncType,
2166
+ formData
2167
+ } = state.navigation; // If this was a 307/308 submission we want to preserve the HTTP method and
2168
+ // re-submit the POST/PUT/PATCH/DELETE as a submission navigation to the
2169
+ // redirected location
2170
+
2171
+ if (redirectPreserveMethodStatusCodes.has(redirect.status) && formMethod && isSubmissionMethod(formMethod) && formEncType && formData) {
2172
+ await startNavigation(redirectHistoryAction, redirectLocation, {
2173
+ submission: {
2174
+ formMethod,
2175
+ formAction: redirect.location,
2176
+ formEncType,
2177
+ formData
2178
+ }
2179
+ });
2180
+ } else {
2181
+ // Otherwise, we kick off a new loading navigation, preserving the
2182
+ // submission info for the duration of this navigation
2183
+ await startNavigation(redirectHistoryAction, redirectLocation, {
2184
+ overrideNavigation: {
2185
+ state: "loading",
2186
+ location: redirectLocation,
2187
+ formMethod: formMethod || undefined,
2188
+ formAction: formAction || undefined,
2189
+ formEncType: formEncType || undefined,
2190
+ formData: formData || undefined
2191
+ }
2192
+ });
2193
+ }
2137
2194
  }
2138
2195
 
2139
2196
  async function callLoadersAndMaybeResolveData(currentMatches, matches, matchesToLoad, fetchersToLoad, request) {
@@ -2337,6 +2394,7 @@ function createRouter(init) {
2337
2394
  // Passthrough to history-aware createHref used by useHref so we get proper
2338
2395
  // hash-aware URLs in DOM paths
2339
2396
  createHref: to => init.history.createHref(to),
2397
+ encodeLocation: to => init.history.encodeLocation(to),
2340
2398
  getFetcher,
2341
2399
  deleteFetcher,
2342
2400
  dispose,
@@ -2349,11 +2407,10 @@ function createRouter(init) {
2349
2407
  //#region createStaticHandler
2350
2408
  ////////////////////////////////////////////////////////////////////////////////
2351
2409
 
2352
- const validActionMethods = new Set(["POST", "PUT", "PATCH", "DELETE"]);
2353
- const validRequestMethods = new Set(["GET", "HEAD", ...validActionMethods]);
2354
- function unstable_createStaticHandler(routes) {
2410
+ function unstable_createStaticHandler(routes, opts) {
2355
2411
  invariant(routes.length > 0, "You must provide a non-empty routes array to unstable_createStaticHandler");
2356
2412
  let dataRoutes = convertRoutesToDataRoutes(routes);
2413
+ let basename = (opts ? opts.basename : null) || "/";
2357
2414
  /**
2358
2415
  * The query() method is intended for document requests, in which we want to
2359
2416
  * call an optional action and potentially multiple loaders for all nested
@@ -2376,16 +2433,20 @@ function unstable_createStaticHandler(routes) {
2376
2433
 
2377
2434
  async function query(request) {
2378
2435
  let url = new URL(request.url);
2436
+ let method = request.method.toLowerCase();
2379
2437
  let location = createLocation("", createPath(url), null, "default");
2380
- let matches = matchRoutes(dataRoutes, location);
2438
+ let matches = matchRoutes(dataRoutes, location, basename); // SSR supports HEAD requests while SPA doesn't
2381
2439
 
2382
- if (!validRequestMethods.has(request.method)) {
2440
+ if (!isValidMethod(method) && method !== "head") {
2441
+ let error = getInternalRouterError(405, {
2442
+ method
2443
+ });
2383
2444
  let {
2384
2445
  matches: methodNotAllowedMatches,
2385
- route,
2386
- error
2387
- } = getMethodNotAllowedMatches(dataRoutes);
2446
+ route
2447
+ } = getShortCircuitMatches(dataRoutes);
2388
2448
  return {
2449
+ basename,
2389
2450
  location,
2390
2451
  matches: methodNotAllowedMatches,
2391
2452
  loaderData: {},
@@ -2398,12 +2459,15 @@ function unstable_createStaticHandler(routes) {
2398
2459
  actionHeaders: {}
2399
2460
  };
2400
2461
  } else if (!matches) {
2462
+ let error = getInternalRouterError(404, {
2463
+ pathname: location.pathname
2464
+ });
2401
2465
  let {
2402
2466
  matches: notFoundMatches,
2403
- route,
2404
- error
2405
- } = getNotFoundMatches(dataRoutes);
2467
+ route
2468
+ } = getShortCircuitMatches(dataRoutes);
2406
2469
  return {
2470
+ basename,
2407
2471
  location,
2408
2472
  matches: notFoundMatches,
2409
2473
  loaderData: {},
@@ -2427,7 +2491,8 @@ function unstable_createStaticHandler(routes) {
2427
2491
 
2428
2492
 
2429
2493
  return _extends({
2430
- location
2494
+ location,
2495
+ basename
2431
2496
  }, result);
2432
2497
  }
2433
2498
  /**
@@ -2443,35 +2508,42 @@ function unstable_createStaticHandler(routes) {
2443
2508
  * can do proper boundary identification in Remix where a thrown Response
2444
2509
  * must go to the Catch Boundary but a returned Response is happy-path.
2445
2510
  *
2446
- * One thing to note is that any Router-initiated thrown Response (such as a
2447
- * 404 or 405) will have a custom X-Remix-Router-Error: "yes" header on it
2448
- * in order to differentiate from responses thrown from user actions/loaders.
2511
+ * One thing to note is that any Router-initiated Errors that make sense
2512
+ * to associate with a status code will be thrown as an ErrorResponse
2513
+ * instance which include the raw Error, such that the calling context can
2514
+ * serialize the error as they see fit while including the proper response
2515
+ * code. Examples here are 404 and 405 errors that occur prior to reaching
2516
+ * any user-defined loaders.
2449
2517
  */
2450
2518
 
2451
2519
 
2452
2520
  async function queryRoute(request, routeId) {
2453
2521
  let url = new URL(request.url);
2522
+ let method = request.method.toLowerCase();
2454
2523
  let location = createLocation("", createPath(url), null, "default");
2455
- let matches = matchRoutes(dataRoutes, location);
2524
+ let matches = matchRoutes(dataRoutes, location, basename); // SSR supports HEAD requests while SPA doesn't
2456
2525
 
2457
- if (!validRequestMethods.has(request.method)) {
2458
- throw createRouterErrorResponse(null, {
2459
- status: 405,
2460
- statusText: "Method Not Allowed"
2526
+ if (!isValidMethod(method) && method !== "head") {
2527
+ throw getInternalRouterError(405, {
2528
+ method
2461
2529
  });
2462
2530
  } else if (!matches) {
2463
- throw createRouterErrorResponse(null, {
2464
- status: 404,
2465
- statusText: "Not Found"
2531
+ throw getInternalRouterError(404, {
2532
+ pathname: location.pathname
2466
2533
  });
2467
2534
  }
2468
2535
 
2469
2536
  let match = routeId ? matches.find(m => m.route.id === routeId) : getTargetMatch(matches, location);
2470
2537
 
2471
- if (!match) {
2472
- throw createRouterErrorResponse(null, {
2473
- status: 404,
2474
- statusText: "Not Found"
2538
+ if (routeId && !match) {
2539
+ throw getInternalRouterError(403, {
2540
+ pathname: location.pathname,
2541
+ routeId
2542
+ });
2543
+ } else if (!match) {
2544
+ // This should never hit I don't think?
2545
+ throw getInternalRouterError(404, {
2546
+ pathname: location.pathname
2475
2547
  });
2476
2548
  }
2477
2549
 
@@ -2500,7 +2572,7 @@ function unstable_createStaticHandler(routes) {
2500
2572
  invariant(request.signal, "query()/queryRoute() requests must contain an AbortController signal");
2501
2573
 
2502
2574
  try {
2503
- if (validActionMethods.has(request.method)) {
2575
+ if (isSubmissionMethod(request.method.toLowerCase())) {
2504
2576
  let result = await submit(request, matches, routeMatch || getTargetMatch(matches, location), routeMatch != null);
2505
2577
  return result;
2506
2578
  }
@@ -2536,17 +2608,22 @@ function unstable_createStaticHandler(routes) {
2536
2608
  let result;
2537
2609
 
2538
2610
  if (!actionMatch.route.action) {
2611
+ let error = getInternalRouterError(405, {
2612
+ method: request.method,
2613
+ pathname: createURL(request.url).pathname,
2614
+ routeId: actionMatch.route.id
2615
+ });
2616
+
2539
2617
  if (isRouteRequest) {
2540
- throw createRouterErrorResponse(null, {
2541
- status: 405,
2542
- statusText: "Method Not Allowed"
2543
- });
2618
+ throw error;
2544
2619
  }
2545
2620
 
2546
- result = getMethodNotAllowedResult(request.url);
2621
+ result = {
2622
+ type: ResultType.error,
2623
+ error
2624
+ };
2547
2625
  } else {
2548
- result = await callLoaderOrAction("action", request, actionMatch, matches, undefined, // Basename not currently supported in static handlers
2549
- true, isRouteRequest);
2626
+ result = await callLoaderOrAction("action", request, actionMatch, matches, basename, true, isRouteRequest);
2550
2627
 
2551
2628
  if (request.signal.aborted) {
2552
2629
  let method = isRouteRequest ? "queryRoute" : "query";
@@ -2575,20 +2652,7 @@ function unstable_createStaticHandler(routes) {
2575
2652
  // Note: This should only be non-Response values if we get here, since
2576
2653
  // isRouteRequest should throw any Response received in callLoaderOrAction
2577
2654
  if (isErrorResult(result)) {
2578
- let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
2579
- return {
2580
- matches: [actionMatch],
2581
- loaderData: {},
2582
- actionData: null,
2583
- errors: {
2584
- [boundaryMatch.route.id]: result.error
2585
- },
2586
- // Note: statusCode + headers are unused here since queryRoute will
2587
- // return the raw Response or value
2588
- statusCode: 500,
2589
- loaderHeaders: {},
2590
- actionHeaders: {}
2591
- };
2655
+ throw result.error;
2592
2656
  }
2593
2657
 
2594
2658
  return {
@@ -2637,9 +2701,18 @@ function unstable_createStaticHandler(routes) {
2637
2701
  }
2638
2702
 
2639
2703
  async function loadRouteData(request, matches, routeMatch, pendingActionError) {
2640
- let isRouteRequest = routeMatch != null;
2704
+ let isRouteRequest = routeMatch != null; // Short circuit if we have no loaders to run (queryRoute())
2705
+
2706
+ if (isRouteRequest && !(routeMatch != null && routeMatch.route.loader)) {
2707
+ throw getInternalRouterError(400, {
2708
+ method: request.method,
2709
+ pathname: createURL(request.url).pathname,
2710
+ routeId: routeMatch == null ? void 0 : routeMatch.route.id
2711
+ });
2712
+ }
2713
+
2641
2714
  let requestMatches = routeMatch ? [routeMatch] : getLoaderMatchesUntilBoundary(matches, Object.keys(pendingActionError || {})[0]);
2642
- let matchesToLoad = requestMatches.filter(m => m.route.loader); // Short circuit if we have no loaders to run
2715
+ let matchesToLoad = requestMatches.filter(m => m.route.loader); // Short circuit if we have no loaders to run (query())
2643
2716
 
2644
2717
  if (matchesToLoad.length === 0) {
2645
2718
  return {
@@ -2651,8 +2724,7 @@ function unstable_createStaticHandler(routes) {
2651
2724
  };
2652
2725
  }
2653
2726
 
2654
- let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, undefined, // Basename not currently supported in static handlers
2655
- true, isRouteRequest))]);
2727
+ let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, basename, true, isRouteRequest))]);
2656
2728
 
2657
2729
  if (request.signal.aborted) {
2658
2730
  let method = isRouteRequest ? "queryRoute" : "query";
@@ -2673,14 +2745,6 @@ function unstable_createStaticHandler(routes) {
2673
2745
  });
2674
2746
  }
2675
2747
 
2676
- function createRouterErrorResponse(body, init) {
2677
- return new Response(body, _extends({}, init, {
2678
- headers: _extends({}, init.headers, {
2679
- "X-Remix-Router-Error": "yes"
2680
- })
2681
- }));
2682
- }
2683
-
2684
2748
  return {
2685
2749
  dataRoutes,
2686
2750
  query,
@@ -2705,9 +2769,14 @@ function getStaticContextFromError(routes, context, error) {
2705
2769
  });
2706
2770
 
2707
2771
  return newContext;
2772
+ }
2773
+
2774
+ function isSubmissionNavigation(opts) {
2775
+ return opts != null && "formData" in opts;
2708
2776
  } // Normalize navigation options by converting formMethod=GET formData objects to
2709
2777
  // URLSearchParams so they behave identically to links with query params
2710
2778
 
2779
+
2711
2780
  function normalizeNavigateOptions(to, opts, isFetcher) {
2712
2781
  if (isFetcher === void 0) {
2713
2782
  isFetcher = false;
@@ -2715,14 +2784,23 @@ function normalizeNavigateOptions(to, opts, isFetcher) {
2715
2784
 
2716
2785
  let path = typeof to === "string" ? to : createPath(to); // Return location verbatim on non-submission navigations
2717
2786
 
2718
- if (!opts || !("formMethod" in opts) && !("formData" in opts)) {
2787
+ if (!opts || !isSubmissionNavigation(opts)) {
2719
2788
  return {
2720
2789
  path
2721
2790
  };
2791
+ }
2792
+
2793
+ if (opts.formMethod && !isValidMethod(opts.formMethod)) {
2794
+ return {
2795
+ path,
2796
+ error: getInternalRouterError(405, {
2797
+ method: opts.formMethod
2798
+ })
2799
+ };
2722
2800
  } // Create a Submission on non-GET navigations
2723
2801
 
2724
2802
 
2725
- if (opts.formMethod != null && opts.formMethod !== "get") {
2803
+ if (opts.formMethod && isSubmissionMethod(opts.formMethod)) {
2726
2804
  return {
2727
2805
  path,
2728
2806
  submission: {
@@ -2732,13 +2810,6 @@ function normalizeNavigateOptions(to, opts, isFetcher) {
2732
2810
  formData: opts.formData
2733
2811
  }
2734
2812
  };
2735
- } // No formData to flatten for GET submission
2736
-
2737
-
2738
- if (!opts.formData) {
2739
- return {
2740
- path
2741
- };
2742
2813
  } // Flatten submission onto URLSearchParams for GET submissions
2743
2814
 
2744
2815
 
@@ -2757,31 +2828,13 @@ function normalizeNavigateOptions(to, opts, isFetcher) {
2757
2828
  } catch (e) {
2758
2829
  return {
2759
2830
  path,
2760
- error: new ErrorResponse(400, "Bad Request", "Cannot submit binary form data using GET")
2831
+ error: getInternalRouterError(400)
2761
2832
  };
2762
2833
  }
2763
2834
 
2764
2835
  return {
2765
2836
  path: createPath(parsedPath)
2766
2837
  };
2767
- }
2768
-
2769
- function getLoaderRedirect(state, redirect) {
2770
- let {
2771
- formMethod,
2772
- formAction,
2773
- formEncType,
2774
- formData
2775
- } = state.navigation;
2776
- let navigation = {
2777
- state: "loading",
2778
- location: createLocation(state.location, redirect.location),
2779
- formMethod: formMethod || undefined,
2780
- formAction: formAction || undefined,
2781
- formEncType: formEncType || undefined,
2782
- formData: formData || undefined
2783
- };
2784
- return navigation;
2785
2838
  } // Filter out all routes below any caught error as they aren't going to
2786
2839
  // render so we don't need to load them
2787
2840
 
@@ -2882,6 +2935,10 @@ function shouldRevalidateLoader(currentLocation, currentMatch, submission, locat
2882
2935
  }
2883
2936
 
2884
2937
  async function callLoaderOrAction(type, request, match, matches, basename, isStaticRequest, isRouteRequest) {
2938
+ if (basename === void 0) {
2939
+ basename = "/";
2940
+ }
2941
+
2885
2942
  if (isStaticRequest === void 0) {
2886
2943
  isStaticRequest = false;
2887
2944
  }
@@ -2907,6 +2964,7 @@ async function callLoaderOrAction(type, request, match, matches, basename, isSta
2907
2964
  request,
2908
2965
  params: match.params
2909
2966
  }), abortPromise]);
2967
+ 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`.");
2910
2968
  } catch (e) {
2911
2969
  resultType = ResultType.error;
2912
2970
  result = e;
@@ -2917,26 +2975,31 @@ async function callLoaderOrAction(type, request, match, matches, basename, isSta
2917
2975
  if (result instanceof Response) {
2918
2976
  let status = result.status; // Process redirects
2919
2977
 
2920
- if (status >= 300 && status <= 399) {
2978
+ if (redirectStatusCodes.has(status)) {
2921
2979
  let location = result.headers.get("Location");
2922
- invariant(location, "Redirects returned/thrown from loaders/actions must have a Location header"); // Support relative routing in redirects
2980
+ 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
2923
2981
 
2924
- let activeMatches = matches.slice(0, matches.indexOf(match) + 1);
2925
- let routePathnames = getPathContributingMatches(activeMatches).map(match => match.pathnameBase);
2926
- let requestPath = createURL(request.url).pathname;
2927
- let resolvedLocation = resolveTo(location, routePathnames, requestPath);
2928
- invariant(createPath(resolvedLocation), "Unable to resolve redirect location: " + result.headers.get("Location")); // Prepend the basename to the redirect location if we have one
2982
+ let external = createURL(location).origin !== createURL("/").origin; // Support relative routing in internal redirects
2929
2983
 
2930
- if (basename) {
2931
- let path = resolvedLocation.pathname;
2932
- resolvedLocation.pathname = path === "/" ? basename : joinPaths([basename, path]);
2933
- }
2984
+ if (!external) {
2985
+ let activeMatches = matches.slice(0, matches.indexOf(match) + 1);
2986
+ let routePathnames = getPathContributingMatches(activeMatches).map(match => match.pathnameBase);
2987
+ let requestPath = createURL(request.url).pathname;
2988
+ let resolvedLocation = resolveTo(location, routePathnames, requestPath);
2989
+ invariant(createPath(resolvedLocation), "Unable to resolve redirect location: " + location); // Prepend the basename to the redirect location if we have one
2990
+
2991
+ if (basename) {
2992
+ let path = resolvedLocation.pathname;
2993
+ resolvedLocation.pathname = path === "/" ? basename : joinPaths([basename, path]);
2994
+ }
2934
2995
 
2935
- location = createPath(resolvedLocation); // Don't process redirects in the router during static requests requests.
2996
+ location = createPath(resolvedLocation);
2997
+ } // Don't process redirects in the router during static requests requests.
2936
2998
  // Instead, throw the Response and let the server handle it with an HTTP
2937
2999
  // redirect. We also update the Location header in place in this flow so
2938
3000
  // basename and relative routing is taken into account
2939
3001
 
3002
+
2940
3003
  if (isStaticRequest) {
2941
3004
  result.headers.set("Location", location);
2942
3005
  throw result;
@@ -2946,7 +3009,8 @@ async function callLoaderOrAction(type, request, match, matches, basename, isSta
2946
3009
  type: ResultType.redirect,
2947
3010
  status,
2948
3011
  location,
2949
- revalidate: result.headers.get("X-Remix-Revalidate") !== null
3012
+ revalidate: result.headers.get("X-Remix-Revalidate") !== null,
3013
+ external
2950
3014
  };
2951
3015
  } // For SSR single-route requests, we want to hand Responses back directly
2952
3016
  // without unwrapping. We do this with the QueryRouteResponse wrapper
@@ -3174,10 +3238,10 @@ function findNearestBoundary(matches, routeId) {
3174
3238
  return eligibleMatches.reverse().find(m => m.route.hasErrorBoundary === true) || matches[0];
3175
3239
  }
3176
3240
 
3177
- function getShortCircuitMatches(routes, status, statusText) {
3241
+ function getShortCircuitMatches(routes) {
3178
3242
  // Prefer a root layout route if present, otherwise shim in a route object
3179
3243
  let route = routes.find(r => r.index || !r.path || r.path === "/") || {
3180
- id: "__shim-" + status + "-route__"
3244
+ id: "__shim-error-route__"
3181
3245
  };
3182
3246
  return {
3183
3247
  matches: [{
@@ -3186,26 +3250,45 @@ function getShortCircuitMatches(routes, status, statusText) {
3186
3250
  pathnameBase: "",
3187
3251
  route
3188
3252
  }],
3189
- route,
3190
- error: new ErrorResponse(status, statusText, null)
3253
+ route
3191
3254
  };
3192
3255
  }
3193
3256
 
3194
- function getNotFoundMatches(routes) {
3195
- return getShortCircuitMatches(routes, 404, "Not Found");
3196
- }
3257
+ function getInternalRouterError(status, _temp) {
3258
+ let {
3259
+ pathname,
3260
+ routeId,
3261
+ method,
3262
+ message
3263
+ } = _temp === void 0 ? {} : _temp;
3264
+ let statusText = "Unknown Server Error";
3265
+ let errorMessage = "Unknown @remix-run/router error";
3266
+
3267
+ if (status === 400) {
3268
+ statusText = "Bad Request";
3269
+
3270
+ if (method && pathname && routeId) {
3271
+ 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.";
3272
+ } else {
3273
+ errorMessage = "Cannot submit binary form data using GET";
3274
+ }
3275
+ } else if (status === 403) {
3276
+ statusText = "Forbidden";
3277
+ errorMessage = "Route \"" + routeId + "\" does not match URL \"" + pathname + "\"";
3278
+ } else if (status === 404) {
3279
+ statusText = "Not Found";
3280
+ errorMessage = "No route matches URL \"" + pathname + "\"";
3281
+ } else if (status === 405) {
3282
+ statusText = "Method Not Allowed";
3197
3283
 
3198
- function getMethodNotAllowedMatches(routes) {
3199
- return getShortCircuitMatches(routes, 405, "Method Not Allowed");
3200
- }
3284
+ if (method && pathname && routeId) {
3285
+ 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.";
3286
+ } else if (method) {
3287
+ errorMessage = "Invalid request method \"" + method.toUpperCase() + "\"";
3288
+ }
3289
+ }
3201
3290
 
3202
- function getMethodNotAllowedResult(path) {
3203
- let href = typeof path === "string" ? path : createPath(path);
3204
- 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 + "]"));
3205
- return {
3206
- type: ResultType.error,
3207
- error: new ErrorResponse(405, "Method Not Allowed", "")
3208
- };
3291
+ return new ErrorResponse(status || 500, statusText, new Error(errorMessage), true);
3209
3292
  } // Find any returned redirect errors, starting from the lowest match
3210
3293
 
3211
3294
 
@@ -3256,6 +3339,14 @@ function isQueryRouteResponse(obj) {
3256
3339
  return obj && obj.response instanceof Response && (obj.type === ResultType.data || ResultType.error);
3257
3340
  }
3258
3341
 
3342
+ function isValidMethod(method) {
3343
+ return validRequestMethods.has(method);
3344
+ }
3345
+
3346
+ function isSubmissionMethod(method) {
3347
+ return validActionMethods.has(method);
3348
+ }
3349
+
3259
3350
  async function resolveDeferredResults(currentMatches, matchesToLoad, results, signal, isFetcher, currentLoaderData) {
3260
3351
  for (let index = 0; index < results.length; index++) {
3261
3352
  let result = results[index];