@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.
@@ -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
  *
@@ -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,55 @@ 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
+ window.location.replace(redirect.location);
2199
+ return;
2200
+ } // There's no need to abort on redirects, since we don't detect the
2174
2201
  // redirect until the action/loaders have settled
2175
2202
 
2203
+
2176
2204
  pendingNavigationController = null;
2177
2205
  let redirectHistoryAction = replace === true ? exports.Action.Replace : exports.Action.Push;
2178
- await startNavigation(redirectHistoryAction, navigation.location, {
2179
- overrideNavigation: navigation
2180
- });
2206
+ let {
2207
+ formMethod,
2208
+ formAction,
2209
+ formEncType,
2210
+ formData
2211
+ } = state.navigation; // If this was a 307/308 submission we want to preserve the HTTP method and
2212
+ // re-submit the POST/PUT/PATCH/DELETE as a submission navigation to the
2213
+ // redirected location
2214
+
2215
+ if (redirectPreserveMethodStatusCodes.has(redirect.status) && formMethod && isSubmissionMethod(formMethod) && formEncType && formData) {
2216
+ await startNavigation(redirectHistoryAction, redirectLocation, {
2217
+ submission: {
2218
+ formMethod,
2219
+ formAction: redirect.location,
2220
+ formEncType,
2221
+ formData
2222
+ }
2223
+ });
2224
+ } else {
2225
+ // Otherwise, we kick off a new loading navigation, preserving the
2226
+ // submission info for the duration of this navigation
2227
+ await startNavigation(redirectHistoryAction, redirectLocation, {
2228
+ overrideNavigation: {
2229
+ state: "loading",
2230
+ location: redirectLocation,
2231
+ formMethod: formMethod || undefined,
2232
+ formAction: formAction || undefined,
2233
+ formEncType: formEncType || undefined,
2234
+ formData: formData || undefined
2235
+ }
2236
+ });
2237
+ }
2181
2238
  }
2182
2239
 
2183
2240
  async function callLoadersAndMaybeResolveData(currentMatches, matches, matchesToLoad, fetchersToLoad, request) {
@@ -2381,6 +2438,7 @@ function createRouter(init) {
2381
2438
  // Passthrough to history-aware createHref used by useHref so we get proper
2382
2439
  // hash-aware URLs in DOM paths
2383
2440
  createHref: to => init.history.createHref(to),
2441
+ encodeLocation: to => init.history.encodeLocation(to),
2384
2442
  getFetcher,
2385
2443
  deleteFetcher,
2386
2444
  dispose,
@@ -2393,11 +2451,10 @@ function createRouter(init) {
2393
2451
  //#region createStaticHandler
2394
2452
  ////////////////////////////////////////////////////////////////////////////////
2395
2453
 
2396
- const validActionMethods = new Set(["POST", "PUT", "PATCH", "DELETE"]);
2397
- const validRequestMethods = new Set(["GET", "HEAD", ...validActionMethods]);
2398
- function unstable_createStaticHandler(routes) {
2454
+ function unstable_createStaticHandler(routes, opts) {
2399
2455
  invariant(routes.length > 0, "You must provide a non-empty routes array to unstable_createStaticHandler");
2400
2456
  let dataRoutes = convertRoutesToDataRoutes(routes);
2457
+ let basename = (opts ? opts.basename : null) || "/";
2401
2458
  /**
2402
2459
  * The query() method is intended for document requests, in which we want to
2403
2460
  * call an optional action and potentially multiple loaders for all nested
@@ -2420,16 +2477,20 @@ function unstable_createStaticHandler(routes) {
2420
2477
 
2421
2478
  async function query(request) {
2422
2479
  let url = new URL(request.url);
2480
+ let method = request.method.toLowerCase();
2423
2481
  let location = createLocation("", createPath(url), null, "default");
2424
- let matches = matchRoutes(dataRoutes, location);
2482
+ let matches = matchRoutes(dataRoutes, location, basename); // SSR supports HEAD requests while SPA doesn't
2425
2483
 
2426
- if (!validRequestMethods.has(request.method)) {
2484
+ if (!isValidMethod(method) && method !== "head") {
2485
+ let error = getInternalRouterError(405, {
2486
+ method
2487
+ });
2427
2488
  let {
2428
2489
  matches: methodNotAllowedMatches,
2429
- route,
2430
- error
2431
- } = getMethodNotAllowedMatches(dataRoutes);
2490
+ route
2491
+ } = getShortCircuitMatches(dataRoutes);
2432
2492
  return {
2493
+ basename,
2433
2494
  location,
2434
2495
  matches: methodNotAllowedMatches,
2435
2496
  loaderData: {},
@@ -2442,12 +2503,15 @@ function unstable_createStaticHandler(routes) {
2442
2503
  actionHeaders: {}
2443
2504
  };
2444
2505
  } else if (!matches) {
2506
+ let error = getInternalRouterError(404, {
2507
+ pathname: location.pathname
2508
+ });
2445
2509
  let {
2446
2510
  matches: notFoundMatches,
2447
- route,
2448
- error
2449
- } = getNotFoundMatches(dataRoutes);
2511
+ route
2512
+ } = getShortCircuitMatches(dataRoutes);
2450
2513
  return {
2514
+ basename,
2451
2515
  location,
2452
2516
  matches: notFoundMatches,
2453
2517
  loaderData: {},
@@ -2471,7 +2535,8 @@ function unstable_createStaticHandler(routes) {
2471
2535
 
2472
2536
 
2473
2537
  return _extends({
2474
- location
2538
+ location,
2539
+ basename
2475
2540
  }, result);
2476
2541
  }
2477
2542
  /**
@@ -2487,35 +2552,42 @@ function unstable_createStaticHandler(routes) {
2487
2552
  * can do proper boundary identification in Remix where a thrown Response
2488
2553
  * must go to the Catch Boundary but a returned Response is happy-path.
2489
2554
  *
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.
2555
+ * One thing to note is that any Router-initiated Errors that make sense
2556
+ * to associate with a status code will be thrown as an ErrorResponse
2557
+ * instance which include the raw Error, such that the calling context can
2558
+ * serialize the error as they see fit while including the proper response
2559
+ * code. Examples here are 404 and 405 errors that occur prior to reaching
2560
+ * any user-defined loaders.
2493
2561
  */
2494
2562
 
2495
2563
 
2496
2564
  async function queryRoute(request, routeId) {
2497
2565
  let url = new URL(request.url);
2566
+ let method = request.method.toLowerCase();
2498
2567
  let location = createLocation("", createPath(url), null, "default");
2499
- let matches = matchRoutes(dataRoutes, location);
2568
+ let matches = matchRoutes(dataRoutes, location, basename); // SSR supports HEAD requests while SPA doesn't
2500
2569
 
2501
- if (!validRequestMethods.has(request.method)) {
2502
- throw createRouterErrorResponse(null, {
2503
- status: 405,
2504
- statusText: "Method Not Allowed"
2570
+ if (!isValidMethod(method) && method !== "head") {
2571
+ throw getInternalRouterError(405, {
2572
+ method
2505
2573
  });
2506
2574
  } else if (!matches) {
2507
- throw createRouterErrorResponse(null, {
2508
- status: 404,
2509
- statusText: "Not Found"
2575
+ throw getInternalRouterError(404, {
2576
+ pathname: location.pathname
2510
2577
  });
2511
2578
  }
2512
2579
 
2513
2580
  let match = routeId ? matches.find(m => m.route.id === routeId) : getTargetMatch(matches, location);
2514
2581
 
2515
- if (!match) {
2516
- throw createRouterErrorResponse(null, {
2517
- status: 404,
2518
- statusText: "Not Found"
2582
+ if (routeId && !match) {
2583
+ throw getInternalRouterError(403, {
2584
+ pathname: location.pathname,
2585
+ routeId
2586
+ });
2587
+ } else if (!match) {
2588
+ // This should never hit I don't think?
2589
+ throw getInternalRouterError(404, {
2590
+ pathname: location.pathname
2519
2591
  });
2520
2592
  }
2521
2593
 
@@ -2544,7 +2616,7 @@ function unstable_createStaticHandler(routes) {
2544
2616
  invariant(request.signal, "query()/queryRoute() requests must contain an AbortController signal");
2545
2617
 
2546
2618
  try {
2547
- if (validActionMethods.has(request.method)) {
2619
+ if (isSubmissionMethod(request.method.toLowerCase())) {
2548
2620
  let result = await submit(request, matches, routeMatch || getTargetMatch(matches, location), routeMatch != null);
2549
2621
  return result;
2550
2622
  }
@@ -2580,17 +2652,22 @@ function unstable_createStaticHandler(routes) {
2580
2652
  let result;
2581
2653
 
2582
2654
  if (!actionMatch.route.action) {
2655
+ let error = getInternalRouterError(405, {
2656
+ method: request.method,
2657
+ pathname: createURL(request.url).pathname,
2658
+ routeId: actionMatch.route.id
2659
+ });
2660
+
2583
2661
  if (isRouteRequest) {
2584
- throw createRouterErrorResponse(null, {
2585
- status: 405,
2586
- statusText: "Method Not Allowed"
2587
- });
2662
+ throw error;
2588
2663
  }
2589
2664
 
2590
- result = getMethodNotAllowedResult(request.url);
2665
+ result = {
2666
+ type: ResultType.error,
2667
+ error
2668
+ };
2591
2669
  } else {
2592
- result = await callLoaderOrAction("action", request, actionMatch, matches, undefined, // Basename not currently supported in static handlers
2593
- true, isRouteRequest);
2670
+ result = await callLoaderOrAction("action", request, actionMatch, matches, basename, true, isRouteRequest);
2594
2671
 
2595
2672
  if (request.signal.aborted) {
2596
2673
  let method = isRouteRequest ? "queryRoute" : "query";
@@ -2619,20 +2696,7 @@ function unstable_createStaticHandler(routes) {
2619
2696
  // Note: This should only be non-Response values if we get here, since
2620
2697
  // isRouteRequest should throw any Response received in callLoaderOrAction
2621
2698
  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
- };
2699
+ throw result.error;
2636
2700
  }
2637
2701
 
2638
2702
  return {
@@ -2681,9 +2745,18 @@ function unstable_createStaticHandler(routes) {
2681
2745
  }
2682
2746
 
2683
2747
  async function loadRouteData(request, matches, routeMatch, pendingActionError) {
2684
- let isRouteRequest = routeMatch != null;
2748
+ let isRouteRequest = routeMatch != null; // Short circuit if we have no loaders to run (queryRoute())
2749
+
2750
+ if (isRouteRequest && !(routeMatch != null && routeMatch.route.loader)) {
2751
+ throw getInternalRouterError(400, {
2752
+ method: request.method,
2753
+ pathname: createURL(request.url).pathname,
2754
+ routeId: routeMatch == null ? void 0 : routeMatch.route.id
2755
+ });
2756
+ }
2757
+
2685
2758
  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
2759
+ let matchesToLoad = requestMatches.filter(m => m.route.loader); // Short circuit if we have no loaders to run (query())
2687
2760
 
2688
2761
  if (matchesToLoad.length === 0) {
2689
2762
  return {
@@ -2695,8 +2768,7 @@ function unstable_createStaticHandler(routes) {
2695
2768
  };
2696
2769
  }
2697
2770
 
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))]);
2771
+ let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, basename, true, isRouteRequest))]);
2700
2772
 
2701
2773
  if (request.signal.aborted) {
2702
2774
  let method = isRouteRequest ? "queryRoute" : "query";
@@ -2717,14 +2789,6 @@ function unstable_createStaticHandler(routes) {
2717
2789
  });
2718
2790
  }
2719
2791
 
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
2792
  return {
2729
2793
  dataRoutes,
2730
2794
  query,
@@ -2749,9 +2813,14 @@ function getStaticContextFromError(routes, context, error) {
2749
2813
  });
2750
2814
 
2751
2815
  return newContext;
2816
+ }
2817
+
2818
+ function isSubmissionNavigation(opts) {
2819
+ return opts != null && "formData" in opts;
2752
2820
  } // Normalize navigation options by converting formMethod=GET formData objects to
2753
2821
  // URLSearchParams so they behave identically to links with query params
2754
2822
 
2823
+
2755
2824
  function normalizeNavigateOptions(to, opts, isFetcher) {
2756
2825
  if (isFetcher === void 0) {
2757
2826
  isFetcher = false;
@@ -2759,14 +2828,23 @@ function normalizeNavigateOptions(to, opts, isFetcher) {
2759
2828
 
2760
2829
  let path = typeof to === "string" ? to : createPath(to); // Return location verbatim on non-submission navigations
2761
2830
 
2762
- if (!opts || !("formMethod" in opts) && !("formData" in opts)) {
2831
+ if (!opts || !isSubmissionNavigation(opts)) {
2763
2832
  return {
2764
2833
  path
2765
2834
  };
2835
+ }
2836
+
2837
+ if (opts.formMethod && !isValidMethod(opts.formMethod)) {
2838
+ return {
2839
+ path,
2840
+ error: getInternalRouterError(405, {
2841
+ method: opts.formMethod
2842
+ })
2843
+ };
2766
2844
  } // Create a Submission on non-GET navigations
2767
2845
 
2768
2846
 
2769
- if (opts.formMethod != null && opts.formMethod !== "get") {
2847
+ if (opts.formMethod && isSubmissionMethod(opts.formMethod)) {
2770
2848
  return {
2771
2849
  path,
2772
2850
  submission: {
@@ -2776,13 +2854,6 @@ function normalizeNavigateOptions(to, opts, isFetcher) {
2776
2854
  formData: opts.formData
2777
2855
  }
2778
2856
  };
2779
- } // No formData to flatten for GET submission
2780
-
2781
-
2782
- if (!opts.formData) {
2783
- return {
2784
- path
2785
- };
2786
2857
  } // Flatten submission onto URLSearchParams for GET submissions
2787
2858
 
2788
2859
 
@@ -2801,31 +2872,13 @@ function normalizeNavigateOptions(to, opts, isFetcher) {
2801
2872
  } catch (e) {
2802
2873
  return {
2803
2874
  path,
2804
- error: new ErrorResponse(400, "Bad Request", "Cannot submit binary form data using GET")
2875
+ error: getInternalRouterError(400)
2805
2876
  };
2806
2877
  }
2807
2878
 
2808
2879
  return {
2809
2880
  path: createPath(parsedPath)
2810
2881
  };
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
2882
  } // Filter out all routes below any caught error as they aren't going to
2830
2883
  // render so we don't need to load them
2831
2884
 
@@ -2926,6 +2979,10 @@ function shouldRevalidateLoader(currentLocation, currentMatch, submission, locat
2926
2979
  }
2927
2980
 
2928
2981
  async function callLoaderOrAction(type, request, match, matches, basename, isStaticRequest, isRouteRequest) {
2982
+ if (basename === void 0) {
2983
+ basename = "/";
2984
+ }
2985
+
2929
2986
  if (isStaticRequest === void 0) {
2930
2987
  isStaticRequest = false;
2931
2988
  }
@@ -2951,6 +3008,7 @@ async function callLoaderOrAction(type, request, match, matches, basename, isSta
2951
3008
  request,
2952
3009
  params: match.params
2953
3010
  }), abortPromise]);
3011
+ 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
3012
  } catch (e) {
2955
3013
  resultType = ResultType.error;
2956
3014
  result = e;
@@ -2961,26 +3019,31 @@ async function callLoaderOrAction(type, request, match, matches, basename, isSta
2961
3019
  if (result instanceof Response) {
2962
3020
  let status = result.status; // Process redirects
2963
3021
 
2964
- if (status >= 300 && status <= 399) {
3022
+ if (redirectStatusCodes.has(status)) {
2965
3023
  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
3024
+ 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
3025
 
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
3026
+ let external = createURL(location).origin !== createURL("/").origin; // Support relative routing in internal redirects
2973
3027
 
2974
- if (basename) {
2975
- let path = resolvedLocation.pathname;
2976
- resolvedLocation.pathname = path === "/" ? basename : joinPaths([basename, path]);
2977
- }
3028
+ if (!external) {
3029
+ let activeMatches = matches.slice(0, matches.indexOf(match) + 1);
3030
+ let routePathnames = getPathContributingMatches(activeMatches).map(match => match.pathnameBase);
3031
+ let requestPath = createURL(request.url).pathname;
3032
+ let resolvedLocation = resolveTo(location, routePathnames, requestPath);
3033
+ invariant(createPath(resolvedLocation), "Unable to resolve redirect location: " + location); // Prepend the basename to the redirect location if we have one
3034
+
3035
+ if (basename) {
3036
+ let path = resolvedLocation.pathname;
3037
+ resolvedLocation.pathname = path === "/" ? basename : joinPaths([basename, path]);
3038
+ }
2978
3039
 
2979
- location = createPath(resolvedLocation); // Don't process redirects in the router during static requests requests.
3040
+ location = createPath(resolvedLocation);
3041
+ } // Don't process redirects in the router during static requests requests.
2980
3042
  // Instead, throw the Response and let the server handle it with an HTTP
2981
3043
  // redirect. We also update the Location header in place in this flow so
2982
3044
  // basename and relative routing is taken into account
2983
3045
 
3046
+
2984
3047
  if (isStaticRequest) {
2985
3048
  result.headers.set("Location", location);
2986
3049
  throw result;
@@ -2990,7 +3053,8 @@ async function callLoaderOrAction(type, request, match, matches, basename, isSta
2990
3053
  type: ResultType.redirect,
2991
3054
  status,
2992
3055
  location,
2993
- revalidate: result.headers.get("X-Remix-Revalidate") !== null
3056
+ revalidate: result.headers.get("X-Remix-Revalidate") !== null,
3057
+ external
2994
3058
  };
2995
3059
  } // For SSR single-route requests, we want to hand Responses back directly
2996
3060
  // without unwrapping. We do this with the QueryRouteResponse wrapper
@@ -3218,10 +3282,10 @@ function findNearestBoundary(matches, routeId) {
3218
3282
  return eligibleMatches.reverse().find(m => m.route.hasErrorBoundary === true) || matches[0];
3219
3283
  }
3220
3284
 
3221
- function getShortCircuitMatches(routes, status, statusText) {
3285
+ function getShortCircuitMatches(routes) {
3222
3286
  // Prefer a root layout route if present, otherwise shim in a route object
3223
3287
  let route = routes.find(r => r.index || !r.path || r.path === "/") || {
3224
- id: "__shim-" + status + "-route__"
3288
+ id: "__shim-error-route__"
3225
3289
  };
3226
3290
  return {
3227
3291
  matches: [{
@@ -3230,26 +3294,45 @@ function getShortCircuitMatches(routes, status, statusText) {
3230
3294
  pathnameBase: "",
3231
3295
  route
3232
3296
  }],
3233
- route,
3234
- error: new ErrorResponse(status, statusText, null)
3297
+ route
3235
3298
  };
3236
3299
  }
3237
3300
 
3238
- function getNotFoundMatches(routes) {
3239
- return getShortCircuitMatches(routes, 404, "Not Found");
3240
- }
3301
+ function getInternalRouterError(status, _temp) {
3302
+ let {
3303
+ pathname,
3304
+ routeId,
3305
+ method,
3306
+ message
3307
+ } = _temp === void 0 ? {} : _temp;
3308
+ let statusText = "Unknown Server Error";
3309
+ let errorMessage = "Unknown @remix-run/router error";
3310
+
3311
+ if (status === 400) {
3312
+ statusText = "Bad Request";
3313
+
3314
+ if (method && pathname && routeId) {
3315
+ 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.";
3316
+ } else {
3317
+ errorMessage = "Cannot submit binary form data using GET";
3318
+ }
3319
+ } else if (status === 403) {
3320
+ statusText = "Forbidden";
3321
+ errorMessage = "Route \"" + routeId + "\" does not match URL \"" + pathname + "\"";
3322
+ } else if (status === 404) {
3323
+ statusText = "Not Found";
3324
+ errorMessage = "No route matches URL \"" + pathname + "\"";
3325
+ } else if (status === 405) {
3326
+ statusText = "Method Not Allowed";
3241
3327
 
3242
- function getMethodNotAllowedMatches(routes) {
3243
- return getShortCircuitMatches(routes, 405, "Method Not Allowed");
3244
- }
3328
+ if (method && pathname && routeId) {
3329
+ 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.";
3330
+ } else if (method) {
3331
+ errorMessage = "Invalid request method \"" + method.toUpperCase() + "\"";
3332
+ }
3333
+ }
3245
3334
 
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
- };
3335
+ return new ErrorResponse(status || 500, statusText, new Error(errorMessage), true);
3253
3336
  } // Find any returned redirect errors, starting from the lowest match
3254
3337
 
3255
3338
 
@@ -3300,6 +3383,14 @@ function isQueryRouteResponse(obj) {
3300
3383
  return obj && obj.response instanceof Response && (obj.type === ResultType.data || ResultType.error);
3301
3384
  }
3302
3385
 
3386
+ function isValidMethod(method) {
3387
+ return validRequestMethods.has(method);
3388
+ }
3389
+
3390
+ function isSubmissionMethod(method) {
3391
+ return validActionMethods.has(method);
3392
+ }
3393
+
3303
3394
  async function resolveDeferredResults(currentMatches, matchesToLoad, results, signal, isFetcher, currentLoaderData) {
3304
3395
  for (let index = 0; index < results.length; index++) {
3305
3396
  let result = results[index];