@remix-run/router 0.0.0-experimental-e960cf1a → 0.0.0-experimental-a0888892

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 v0.0.0-experimental-e960cf1a
2
+ * @remix-run/router v0.0.0-experimental-a0888892
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -496,6 +496,10 @@ function getUrlBasedHistory(getLocation, createHref, validateLocation, options)
496
496
  // See https://bugzilla.mozilla.org/show_bug.cgi?id=878297
497
497
  let base = window.location.origin !== "null" ? window.location.origin : window.location.href;
498
498
  let href = typeof to === "string" ? to : createPath(to);
499
+ // Treating this as a full URL will strip any trailing spaces so we need to
500
+ // pre-encode them since they might be part of a matching splat param from
501
+ // an ancestor route
502
+ href = href.replace(/ $/, "%20");
499
503
  invariant(base, "No window.location.(origin|href) available to create URL for href: " + href);
500
504
  return new URL(href, base);
501
505
  }
@@ -573,6 +577,10 @@ let ResultType = /*#__PURE__*/function (ResultType) {
573
577
  * Result from a loader or action - potentially successful or unsuccessful
574
578
  */
575
579
 
580
+ /**
581
+ * Result from a loader or action called via dataStrategy
582
+ */
583
+
576
584
  /**
577
585
  * Users can specify either lowercase or uppercase form methods on `<Form>`,
578
586
  * useSubmit(), `<fetcher.Form>`, etc.
@@ -752,14 +760,14 @@ function matchRoutes(routes, locationArg, basename) {
752
760
  rankRouteBranches(branches);
753
761
  let matches = null;
754
762
  for (let i = 0; matches == null && i < branches.length; ++i) {
755
- matches = matchRouteBranch(branches[i],
756
763
  // Incoming pathnames are generally encoded from either window.location
757
764
  // or from router.navigate, but we want to match against the unencoded
758
765
  // paths in the route definitions. Memory router locations won't be
759
766
  // encoded here but there also shouldn't be anything to decode so this
760
767
  // should be a safe operation. This avoids needing matchRoutes to be
761
768
  // history-aware.
762
- safelyDecodeURI(pathname));
769
+ let decoded = decodePath(pathname);
770
+ matches = matchRouteBranch(branches[i], decoded);
763
771
  }
764
772
  return matches;
765
773
  }
@@ -889,7 +897,7 @@ function rankRouteBranches(branches) {
889
897
  branches.sort((a, b) => a.score !== b.score ? b.score - a.score // Higher score first
890
898
  : compareIndexes(a.routesMeta.map(meta => meta.childrenIndex), b.routesMeta.map(meta => meta.childrenIndex)));
891
899
  }
892
- const paramRe = /^:\w+$/;
900
+ const paramRe = /^:[\w-]+$/;
893
901
  const dynamicSegmentValue = 3;
894
902
  const indexRouteValue = 2;
895
903
  const emptySegmentValue = 1;
@@ -979,7 +987,7 @@ function generatePath(originalPath, params) {
979
987
  // Apply the splat
980
988
  return stringify(params[star]);
981
989
  }
982
- const keyMatch = segment.match(/^:(\w+)(\??)$/);
990
+ const keyMatch = segment.match(/^:([\w-]+)(\??)$/);
983
991
  if (keyMatch) {
984
992
  const [, key, optional] = keyMatch;
985
993
  let param = params[key];
@@ -1038,7 +1046,7 @@ function matchPath(pattern, pathname) {
1038
1046
  if (isOptional && !value) {
1039
1047
  memo[paramName] = undefined;
1040
1048
  } else {
1041
- memo[paramName] = safelyDecodeURIComponent(value || "", paramName);
1049
+ memo[paramName] = (value || "").replace(/%2F/g, "/");
1042
1050
  }
1043
1051
  return memo;
1044
1052
  }, {});
@@ -1061,7 +1069,7 @@ function compilePath(path, caseSensitive, end) {
1061
1069
  let regexpSource = "^" + path.replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
1062
1070
  .replace(/^\/*/, "/") // Make sure it has a leading /
1063
1071
  .replace(/[\\.*+^${}|()[\]]/g, "\\$&") // Escape special regex chars
1064
- .replace(/\/:(\w+)(\?)?/g, (_, paramName, isOptional) => {
1072
+ .replace(/\/:([\w-]+)(\?)?/g, (_, paramName, isOptional) => {
1065
1073
  params.push({
1066
1074
  paramName,
1067
1075
  isOptional: isOptional != null
@@ -1090,22 +1098,14 @@ function compilePath(path, caseSensitive, end) {
1090
1098
  let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");
1091
1099
  return [matcher, params];
1092
1100
  }
1093
- function safelyDecodeURI(value) {
1101
+ function decodePath(value) {
1094
1102
  try {
1095
- return decodeURI(value);
1103
+ return value.split("/").map(v => decodeURIComponent(v).replace(/\//g, "%2F")).join("/");
1096
1104
  } catch (error) {
1097
1105
  warning(false, "The URL path \"" + value + "\" could not be decoded because it is is a " + "malformed URL segment. This is probably due to a bad percent " + ("encoding (" + error + ")."));
1098
1106
  return value;
1099
1107
  }
1100
1108
  }
1101
- function safelyDecodeURIComponent(value, paramName) {
1102
- try {
1103
- return decodeURIComponent(value);
1104
- } catch (error) {
1105
- warning(false, "The value for the URL param \"" + paramName + "\" will not be decoded because" + (" the string \"" + value + "\" is a malformed URL segment. This is probably") + (" due to a bad percent encoding (" + error + ")."));
1106
- return value;
1107
- }
1108
- }
1109
1109
 
1110
1110
  /**
1111
1111
  * @private
@@ -1597,11 +1597,6 @@ function isRouteErrorResponse(error) {
1597
1597
  /**
1598
1598
  * Identified fetcher.load() calls that need to be revalidated
1599
1599
  */
1600
- /**
1601
- * Wrapper object to allow us to throw any response out from callLoaderOrAction
1602
- * for queryRouter while preserving whether or not it was thrown or returned
1603
- * from the loader/action
1604
- */
1605
1600
  const validMutationMethodsArr = ["post", "put", "patch", "delete"];
1606
1601
  const validMutationMethods = new Set(validMutationMethodsArr);
1607
1602
  const validRequestMethodsArr = ["get", ...validMutationMethodsArr];
@@ -1654,8 +1649,6 @@ function createRouter(init) {
1654
1649
  const isBrowser = typeof routerWindow !== "undefined" && typeof routerWindow.document !== "undefined" && typeof routerWindow.document.createElement !== "undefined";
1655
1650
  const isServer = !isBrowser;
1656
1651
  invariant(init.routes.length > 0, "You must provide a non-empty routes array to createRouter");
1657
- const dataStrategy = init.unstable_dataStrategy || defaultDataStrategy;
1658
- const callLoaderOrAction = createCallLoaderOrAction(dataStrategy);
1659
1652
  let mapRouteProperties;
1660
1653
  if (init.mapRouteProperties) {
1661
1654
  mapRouteProperties = init.mapRouteProperties;
@@ -1675,6 +1668,7 @@ function createRouter(init) {
1675
1668
  let dataRoutes = convertRoutesToDataRoutes(init.routes, mapRouteProperties, undefined, manifest);
1676
1669
  let inFlightDataRoutes;
1677
1670
  let basename = init.basename || "/";
1671
+ let dataStrategyImpl = init.unstable_dataStrategy || defaultDataStrategy;
1678
1672
  // Config driven behavior flags
1679
1673
  let future = _extends({
1680
1674
  v7_fetcherPersist: false,
@@ -1717,18 +1711,28 @@ function createRouter(init) {
1717
1711
  [route.id]: error
1718
1712
  };
1719
1713
  }
1720
-
1721
- // "Initialized" here really means "Can `RouterProvider` render my route tree?"
1722
- // Prior to `route.HydrateFallback`, we only had a root `fallbackElement` so we used
1723
- // `state.initialized` to render that instead of `<DataRoutes>`. Now that we
1724
- // support route level fallbacks we can always render and we'll just render
1725
- // as deep as we have data for and detect the nearest ancestor HydrateFallback
1726
- let initialized = future.v7_partialHydration ||
1727
- // All initialMatches need to be loaded before we're ready. If we have lazy
1728
- // functions around still then we'll need to run them in initialize()
1729
- !initialMatches.some(m => m.route.lazy) && (
1730
- // And we have to either have no loaders or have been provided hydrationData
1731
- !initialMatches.some(m => m.route.loader) || init.hydrationData != null);
1714
+ let initialized;
1715
+ let hasLazyRoutes = initialMatches.some(m => m.route.lazy);
1716
+ let hasLoaders = initialMatches.some(m => m.route.loader);
1717
+ if (hasLazyRoutes) {
1718
+ // All initialMatches need to be loaded before we're ready. If we have lazy
1719
+ // functions around still then we'll need to run them in initialize()
1720
+ initialized = false;
1721
+ } else if (!hasLoaders) {
1722
+ // If we've got no loaders to run, then we're good to go
1723
+ initialized = true;
1724
+ } else if (future.v7_partialHydration) {
1725
+ // If partial hydration is enabled, we're initialized so long as we were
1726
+ // provided with hydrationData for every route with a loader, and no loaders
1727
+ // were marked for explicit hydration
1728
+ let loaderData = init.hydrationData ? init.hydrationData.loaderData : null;
1729
+ let errors = init.hydrationData ? init.hydrationData.errors : null;
1730
+ initialized = initialMatches.every(m => m.route.loader && (typeof m.route.loader !== "function" || m.route.loader.hydrate !== true) && (loaderData && loaderData[m.route.id] !== undefined || errors && errors[m.route.id] !== undefined));
1731
+ } else {
1732
+ // Without partial hydration - we're initialized if we were provided any
1733
+ // hydrationData - which is expected to be complete
1734
+ initialized = init.hydrationData != null;
1735
+ }
1732
1736
  let router;
1733
1737
  let state = {
1734
1738
  historyAction: init.history.action,
@@ -1895,7 +1899,7 @@ function createRouter(init) {
1895
1899
  // in the normal navigation flow. For SSR it's expected that lazy modules are
1896
1900
  // resolved prior to router creation since we can't go into a fallbackElement
1897
1901
  // UI for SSR'd apps
1898
- if (!state.initialized || future.v7_partialHydration && state.matches.some(m => isUnhydratedRoute(state, m.route))) {
1902
+ if (!state.initialized) {
1899
1903
  startNavigation(Action.Pop, state.location, {
1900
1904
  initialHydration: true
1901
1905
  });
@@ -2336,7 +2340,8 @@ function createRouter(init) {
2336
2340
  })
2337
2341
  };
2338
2342
  } else {
2339
- result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
2343
+ let results = await callDataStrategy("action", request, [actionMatch], matches);
2344
+ result = results[0];
2340
2345
  if (request.signal.aborted) {
2341
2346
  return {
2342
2347
  shortCircuited: true
@@ -2351,9 +2356,9 @@ function createRouter(init) {
2351
2356
  // If the user didn't explicity indicate replace behavior, replace if
2352
2357
  // we redirected to the exact same location we're currently at to avoid
2353
2358
  // double back-buttons
2354
- replace = result.location === state.location.pathname + state.location.search;
2359
+ replace = result.response.headers.get("Location") === state.location.pathname + state.location.search;
2355
2360
  }
2356
- await startRedirectNavigation(state, result, {
2361
+ await startRedirectNavigation(request, result, {
2357
2362
  submission,
2358
2363
  replace
2359
2364
  });
@@ -2476,7 +2481,7 @@ function createRouter(init) {
2476
2481
  let {
2477
2482
  loaderResults,
2478
2483
  fetcherResults
2479
- } = await loadDataAndMaybeResolveDeferred(state.matches, matches, matchesToLoad, revalidatingFetchers, request);
2484
+ } = await callLoadersAndMaybeResolveData(state.matches, matches, matchesToLoad, revalidatingFetchers, request);
2480
2485
  if (request.signal.aborted) {
2481
2486
  return {
2482
2487
  shortCircuited: true
@@ -2501,7 +2506,7 @@ function createRouter(init) {
2501
2506
  let fetcherKey = revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
2502
2507
  fetchRedirectIds.add(fetcherKey);
2503
2508
  }
2504
- await startRedirectNavigation(state, redirect.result, {
2509
+ await startRedirectNavigation(request, redirect.result, {
2505
2510
  replace
2506
2511
  });
2507
2512
  return {
@@ -2610,7 +2615,8 @@ function createRouter(init) {
2610
2615
  let fetchRequest = createClientSideRequest(init.history, path, abortController.signal, submission);
2611
2616
  fetchControllers.set(key, abortController);
2612
2617
  let originatingLoadId = incrementingLoadId;
2613
- let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
2618
+ let actionResults = await callDataStrategy("action", fetchRequest, [match], requestMatches);
2619
+ let actionResult = actionResults[0];
2614
2620
  if (fetchRequest.signal.aborted) {
2615
2621
  // We can delete this so long as we weren't aborted by our own fetcher
2616
2622
  // re-submit which would have put _new_ controller is in fetchControllers
@@ -2642,7 +2648,7 @@ function createRouter(init) {
2642
2648
  } else {
2643
2649
  fetchRedirectIds.add(key);
2644
2650
  updateFetcherState(key, getLoadingFetcher(submission));
2645
- return startRedirectNavigation(state, actionResult, {
2651
+ return startRedirectNavigation(fetchRequest, actionResult, {
2646
2652
  fetcherSubmission: submission
2647
2653
  });
2648
2654
  }
@@ -2699,7 +2705,7 @@ function createRouter(init) {
2699
2705
  let {
2700
2706
  loaderResults,
2701
2707
  fetcherResults
2702
- } = await loadDataAndMaybeResolveDeferred(state.matches, matches, matchesToLoad, revalidatingFetchers, revalidationRequest);
2708
+ } = await callLoadersAndMaybeResolveData(state.matches, matches, matchesToLoad, revalidatingFetchers, revalidationRequest);
2703
2709
  if (abortController.signal.aborted) {
2704
2710
  return;
2705
2711
  }
@@ -2716,7 +2722,7 @@ function createRouter(init) {
2716
2722
  let fetcherKey = revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
2717
2723
  fetchRedirectIds.add(fetcherKey);
2718
2724
  }
2719
- return startRedirectNavigation(state, redirect.result);
2725
+ return startRedirectNavigation(revalidationRequest, redirect.result);
2720
2726
  }
2721
2727
 
2722
2728
  // Process and commit output from loaders
@@ -2770,7 +2776,8 @@ function createRouter(init) {
2770
2776
  let fetchRequest = createClientSideRequest(init.history, path, abortController.signal);
2771
2777
  fetchControllers.set(key, abortController);
2772
2778
  let originatingLoadId = incrementingLoadId;
2773
- let result = await callLoaderOrAction("loader", fetchRequest, match, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
2779
+ let results = await callDataStrategy("loader", fetchRequest, [match], matches);
2780
+ let result = results[0];
2774
2781
 
2775
2782
  // Deferred isn't supported for fetcher loads, await everything and treat it
2776
2783
  // as a normal load. resolveDeferredData will return undefined if this
@@ -2805,7 +2812,7 @@ function createRouter(init) {
2805
2812
  return;
2806
2813
  } else {
2807
2814
  fetchRedirectIds.add(key);
2808
- await startRedirectNavigation(state, result);
2815
+ await startRedirectNavigation(fetchRequest, result);
2809
2816
  return;
2810
2817
  }
2811
2818
  }
@@ -2840,26 +2847,40 @@ function createRouter(init) {
2840
2847
  * actually touch history until we've processed redirects, so we just use
2841
2848
  * the history action from the original navigation (PUSH or REPLACE).
2842
2849
  */
2843
- async function startRedirectNavigation(state, redirect, _temp2) {
2850
+ async function startRedirectNavigation(request, redirect, _temp2) {
2844
2851
  let {
2845
2852
  submission,
2846
2853
  fetcherSubmission,
2847
2854
  replace
2848
2855
  } = _temp2 === void 0 ? {} : _temp2;
2849
- if (redirect.revalidate) {
2856
+ if (redirect.response.headers.has("X-Remix-Revalidate")) {
2850
2857
  isRevalidationRequired = true;
2851
2858
  }
2852
- let redirectLocation = createLocation(state.location, redirect.location, {
2859
+ let location = redirect.response.headers.get("Location");
2860
+ invariant(location, "Expected a Location header on the redirect Response");
2861
+ let redirectLocation = createLocation(state.location, location, {
2853
2862
  _isRedirect: true
2854
2863
  });
2855
- invariant(redirectLocation, "Expected a location on the redirect navigation");
2864
+ if (ABSOLUTE_URL_REGEX.test(location)) {
2865
+ // Strip off the protocol+origin for same-origin + same-basename absolute redirects
2866
+ let normalizedLocation = location;
2867
+ let currentUrl = new URL(request.url);
2868
+ let url = normalizedLocation.startsWith("//") ? new URL(currentUrl.protocol + normalizedLocation) : new URL(normalizedLocation);
2869
+ let isSameBasename = stripBasename(url.pathname, basename) != null;
2870
+ if (url.origin === currentUrl.origin && isSameBasename) {
2871
+ normalizedLocation = url.pathname + url.search + url.hash;
2872
+ redirectLocation = createLocation(state.location, normalizedLocation, {
2873
+ _isRedirect: true
2874
+ });
2875
+ }
2876
+ }
2856
2877
  if (isBrowser) {
2857
2878
  let isDocumentReload = false;
2858
- if (redirect.reloadDocument) {
2879
+ if (redirect.response.headers.has("X-Remix-Reload-Document")) {
2859
2880
  // Hard reload if the response contained X-Remix-Reload-Document
2860
2881
  isDocumentReload = true;
2861
- } else if (ABSOLUTE_URL_REGEX.test(redirect.location)) {
2862
- const url = init.history.createURL(redirect.location);
2882
+ } else if (ABSOLUTE_URL_REGEX.test(location)) {
2883
+ const url = init.history.createURL(location);
2863
2884
  isDocumentReload =
2864
2885
  // Hard reload if it's an absolute URL to a new origin
2865
2886
  url.origin !== routerWindow.location.origin ||
@@ -2868,9 +2889,9 @@ function createRouter(init) {
2868
2889
  }
2869
2890
  if (isDocumentReload) {
2870
2891
  if (replace) {
2871
- routerWindow.location.replace(redirect.location);
2892
+ routerWindow.location.replace(location);
2872
2893
  } else {
2873
- routerWindow.location.assign(redirect.location);
2894
+ routerWindow.location.assign(location);
2874
2895
  }
2875
2896
  return;
2876
2897
  }
@@ -2896,10 +2917,10 @@ function createRouter(init) {
2896
2917
  // re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the
2897
2918
  // redirected location
2898
2919
  let activeSubmission = submission || fetcherSubmission;
2899
- if (redirectPreserveMethodStatusCodes.has(redirect.status) && activeSubmission && isMutationMethod(activeSubmission.formMethod)) {
2920
+ if (redirectPreserveMethodStatusCodes.has(redirect.response.status) && activeSubmission && isMutationMethod(activeSubmission.formMethod)) {
2900
2921
  await startNavigation(redirectHistoryAction, redirectLocation, {
2901
2922
  submission: _extends({}, activeSubmission, {
2902
- formAction: redirect.location
2923
+ formAction: location
2903
2924
  }),
2904
2925
  // Preserve this flag across redirects
2905
2926
  preventScrollReset: pendingPreventScrollReset
@@ -2917,16 +2938,36 @@ function createRouter(init) {
2917
2938
  });
2918
2939
  }
2919
2940
  }
2920
- async function loadDataAndMaybeResolveDeferred(currentMatches, matches, matchesToLoad, fetchersToLoad, request) {
2921
- let [loaderResults, ...fetcherResults] = await Promise.all([matchesToLoad.length ? dataStrategy({
2922
- matches: matchesToLoad.map(m => finesseToAgnosticDataStrategyMatch(m, mapRouteProperties, manifest)),
2923
- request,
2924
- type: "loader",
2925
- defaultStrategy(match) {
2926
- return callLoaderOrActionImplementation("loader", request, match, matches, basename, future.v7_relativeSplatPath);
2927
- }
2928
- }) : [], ...fetchersToLoad.map(f => {
2929
- if (!f.matches || !f.match || !f.controller) {
2941
+
2942
+ // Utility wrapper for calling dataStrategy client-side without having to
2943
+ // pass around the manifest, mapRouteProperties, etc.
2944
+ async function callDataStrategy(type, request, matchesToLoad, matches) {
2945
+ try {
2946
+ let results = await callDataStrategyImpl(dataStrategyImpl, type, request, matchesToLoad, matches, manifest, mapRouteProperties);
2947
+ return await Promise.all(results.map((result, i) => {
2948
+ if (isRedirectHandlerResult(result)) {
2949
+ return {
2950
+ type: ResultType.redirect,
2951
+ response: normalizeRelativeRoutingRedirectResponse(result.result, request, matchesToLoad[i].route.id, matches, basename, future.v7_relativeSplatPath)
2952
+ };
2953
+ }
2954
+ return convertHandlerResultToDataResult(result);
2955
+ }));
2956
+ } catch (e) {
2957
+ // If the outer dataStrategy method throws, just return the error for all
2958
+ // matches - and it'll naturally bubble to the root
2959
+ return matchesToLoad.map(() => ({
2960
+ type: ResultType.error,
2961
+ error: e
2962
+ }));
2963
+ }
2964
+ }
2965
+ async function callLoadersAndMaybeResolveData(currentMatches, matches, matchesToLoad, fetchersToLoad, request) {
2966
+ let [loaderResults, ...fetcherResults] = await Promise.all([matchesToLoad.length ? callDataStrategy("loader", request, matchesToLoad, matches) : [], ...fetchersToLoad.map(f => {
2967
+ if (f.matches && f.match && f.controller) {
2968
+ let fetcherRequest = createClientSideRequest(init.history, f.path, f.controller.signal);
2969
+ return callDataStrategy("loader", fetcherRequest, [f.match], f.matches).then(r => r[0]);
2970
+ } else {
2930
2971
  return Promise.resolve({
2931
2972
  type: ResultType.error,
2932
2973
  error: getInternalRouterError(404, {
@@ -2934,16 +2975,6 @@ function createRouter(init) {
2934
2975
  })
2935
2976
  });
2936
2977
  }
2937
- return dataStrategy({
2938
- matches: [finesseToAgnosticDataStrategyMatch(f.match, mapRouteProperties, manifest)],
2939
- request,
2940
- type: "loader",
2941
- defaultStrategy(match) {
2942
- invariant(f.controller, "Expected controller for fetcher in defaultStrategy");
2943
- invariant(f.matches, "Expected matches for fetcher in defaultStrategy");
2944
- return callLoaderOrActionImplementation("loader", createClientSideRequest(init.history, f.path, f.controller.signal), match, f.matches, basename, future.v7_relativeSplatPath);
2945
- }
2946
- }).then(r => r[0]);
2947
2978
  })]);
2948
2979
  await Promise.all([resolveDeferredResults(currentMatches, matchesToLoad, loaderResults, loaderResults.map(() => request.signal), false, state.loaderData), resolveDeferredResults(currentMatches, fetchersToLoad.map(f => f.match), fetcherResults, fetchersToLoad.map(f => f.controller ? f.controller.signal : null), true)]);
2949
2980
  return {
@@ -3257,10 +3288,9 @@ const UNSAFE_DEFERRED_SYMBOL = Symbol("deferred");
3257
3288
 
3258
3289
  function createStaticHandler(routes, opts) {
3259
3290
  invariant(routes.length > 0, "You must provide a non-empty routes array to createStaticHandler");
3260
- const dataStrategy = (opts == null ? void 0 : opts.dataStrategy) || defaultDataStrategy;
3261
- const callLoaderOrAction = createCallLoaderOrAction(dataStrategy);
3262
3291
  let manifest = {};
3263
3292
  let basename = (opts ? opts.basename : null) || "/";
3293
+ let dataStrategyImpl = (opts == null ? void 0 : opts.unstable_dataStrategy) || defaultDataStrategy;
3264
3294
  let mapRouteProperties;
3265
3295
  if (opts != null && opts.mapRouteProperties) {
3266
3296
  mapRouteProperties = opts.mapRouteProperties;
@@ -3275,7 +3305,8 @@ function createStaticHandler(routes, opts) {
3275
3305
  }
3276
3306
  // Config driven behavior flags
3277
3307
  let future = _extends({
3278
- v7_relativeSplatPath: false
3308
+ v7_relativeSplatPath: false,
3309
+ v7_throwAbortReason: false
3279
3310
  }, opts ? opts.future : null);
3280
3311
  let dataRoutes = convertRoutesToDataRoutes(routes, mapRouteProperties, undefined, manifest);
3281
3312
 
@@ -3459,14 +3490,14 @@ function createStaticHandler(routes, opts) {
3459
3490
  actionHeaders: {}
3460
3491
  });
3461
3492
  } catch (e) {
3462
- // If the user threw/returned a Response in callLoaderOrAction, we throw
3463
- // it to bail out and then return or throw here based on whether the user
3464
- // returned or threw
3465
- if (isQueryRouteResponse(e)) {
3493
+ // If the user threw/returned a Response in callLoaderOrAction for a
3494
+ // `queryRoute` call, we throw the `HandlerResult` to bail out early
3495
+ // and then return or throw the raw Response here accordingly
3496
+ if (isHandlerResult(e) && isResponse(e.result)) {
3466
3497
  if (e.type === ResultType.error) {
3467
- throw e.response;
3498
+ throw e.result;
3468
3499
  }
3469
- return e.response;
3500
+ return e.result;
3470
3501
  }
3471
3502
  // Redirects are always returned since they don't propagate to catch
3472
3503
  // boundaries
@@ -3492,14 +3523,10 @@ function createStaticHandler(routes, opts) {
3492
3523
  error
3493
3524
  };
3494
3525
  } else {
3495
- result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath, {
3496
- isStaticRequest: true,
3497
- isRouteRequest,
3498
- requestContext
3499
- });
3526
+ let results = await callDataStrategy("action", request, [actionMatch], matches, isRouteRequest, requestContext);
3527
+ result = results[0];
3500
3528
  if (request.signal.aborted) {
3501
- let method = isRouteRequest ? "queryRoute" : "query";
3502
- throw new Error(method + "() call aborted: " + request.method + " " + request.url);
3529
+ throwStaticHandlerAbortedError(request, isRouteRequest, future);
3503
3530
  }
3504
3531
  }
3505
3532
  if (isRedirectResult(result)) {
@@ -3508,9 +3535,9 @@ function createStaticHandler(routes, opts) {
3508
3535
  // can get back on the "throw all redirect responses" train here should
3509
3536
  // this ever happen :/
3510
3537
  throw new Response(null, {
3511
- status: result.status,
3538
+ status: result.response.status,
3512
3539
  headers: {
3513
- Location: result.location
3540
+ Location: result.response.headers.get("Location")
3514
3541
  }
3515
3542
  });
3516
3543
  }
@@ -3559,8 +3586,8 @@ function createStaticHandler(routes, opts) {
3559
3586
  return _extends({}, context, {
3560
3587
  statusCode: isRouteErrorResponse(result.error) ? result.error.status : 500,
3561
3588
  actionData: null,
3562
- actionHeaders: _extends({}, result.headers ? {
3563
- [actionMatch.route.id]: result.headers
3589
+ actionHeaders: _extends({}, result.response ? {
3590
+ [actionMatch.route.id]: result.response.headers
3564
3591
  } : {})
3565
3592
  });
3566
3593
  }
@@ -3572,15 +3599,17 @@ function createStaticHandler(routes, opts) {
3572
3599
  signal: request.signal
3573
3600
  });
3574
3601
  let context = await loadRouteData(loaderRequest, matches, requestContext);
3575
- return _extends({}, context, result.statusCode ? {
3576
- statusCode: result.statusCode
3577
- } : {}, {
3602
+ return _extends({}, context, {
3578
3603
  actionData: {
3579
3604
  [actionMatch.route.id]: result.data
3580
- },
3581
- actionHeaders: _extends({}, result.headers ? {
3582
- [actionMatch.route.id]: result.headers
3583
- } : {})
3605
+ }
3606
+ }, result.response ? {
3607
+ statusCode: result.response.status,
3608
+ actionHeaders: {
3609
+ [actionMatch.route.id]: result.response.headers
3610
+ }
3611
+ } : {
3612
+ actionHeaders: {}
3584
3613
  });
3585
3614
  }
3586
3615
  async function loadRouteData(request, matches, requestContext, routeMatch, pendingActionError) {
@@ -3611,21 +3640,9 @@ function createStaticHandler(routes, opts) {
3611
3640
  activeDeferreds: null
3612
3641
  };
3613
3642
  }
3614
- let results = await dataStrategy({
3615
- matches: matchesToLoad.map(m => finesseToAgnosticDataStrategyMatch(m, mapRouteProperties, manifest)),
3616
- request,
3617
- type: "loader",
3618
- defaultStrategy(match) {
3619
- return callLoaderOrActionImplementation("loader", request, match, matches, basename, future.v7_relativeSplatPath, {
3620
- isStaticRequest: true,
3621
- isRouteRequest,
3622
- requestContext
3623
- });
3624
- }
3625
- });
3643
+ let results = await callDataStrategy("loader", request, matchesToLoad, matches, isRouteRequest, requestContext);
3626
3644
  if (request.signal.aborted) {
3627
- let method = isRouteRequest ? "queryRoute" : "query";
3628
- throw new Error(method + "() call aborted: " + request.method + " " + request.url);
3645
+ throwStaticHandlerAbortedError(request, isRouteRequest, future);
3629
3646
  }
3630
3647
 
3631
3648
  // Process and commit output from loaders
@@ -3644,6 +3661,25 @@ function createStaticHandler(routes, opts) {
3644
3661
  activeDeferreds: activeDeferreds.size > 0 ? Object.fromEntries(activeDeferreds.entries()) : null
3645
3662
  });
3646
3663
  }
3664
+
3665
+ // Utility wrapper for calling dataStrategy server-side without having to
3666
+ // pass around the manifest, mapRouteProperties, etc.
3667
+ async function callDataStrategy(type, request, matchesToLoad, matches, isRouteRequest, requestContext) {
3668
+ let results = await callDataStrategyImpl(dataStrategyImpl, type, request, matchesToLoad, matches, manifest, mapRouteProperties, requestContext);
3669
+ return await Promise.all(results.map((result, i) => {
3670
+ if (isRedirectHandlerResult(result)) {
3671
+ // Throw redirects and let the server handle them with an HTTP redirect
3672
+ throw normalizeRelativeRoutingRedirectResponse(result.result, request, matchesToLoad[i].route.id, matches, basename, future.v7_relativeSplatPath);
3673
+ }
3674
+ if (isResponse(result.result) && isRouteRequest) {
3675
+ // For SSR single-route requests, we want to hand Responses back directly
3676
+ // without unwrapping. We do this with the QueryRouteResponse wrapper
3677
+ // interface so we can know whether it was returned or thrown
3678
+ throw result;
3679
+ }
3680
+ return convertHandlerResultToDataResult(result);
3681
+ }));
3682
+ }
3647
3683
  return {
3648
3684
  dataRoutes,
3649
3685
  query,
@@ -3657,27 +3693,26 @@ function createStaticHandler(routes, opts) {
3657
3693
  //#region Helpers
3658
3694
  ////////////////////////////////////////////////////////////////////////////////
3659
3695
 
3660
- function defaultDataStrategy(_ref3) {
3661
- let {
3662
- defaultStrategy,
3663
- matches
3664
- } = _ref3;
3665
- return Promise.all(matches.map(match => defaultStrategy(match)));
3666
- }
3667
-
3668
3696
  /**
3669
3697
  * Given an existing StaticHandlerContext and an error thrown at render time,
3670
3698
  * provide an updated StaticHandlerContext suitable for a second SSR render
3671
3699
  */
3672
3700
  function getStaticContextFromError(routes, context, error) {
3673
3701
  let newContext = _extends({}, context, {
3674
- statusCode: 500,
3702
+ statusCode: isRouteErrorResponse(error) ? error.status : 500,
3675
3703
  errors: {
3676
3704
  [context._deepestRenderedBoundaryId || routes[0].id]: error
3677
3705
  }
3678
3706
  });
3679
3707
  return newContext;
3680
3708
  }
3709
+ function throwStaticHandlerAbortedError(request, isRouteRequest, future) {
3710
+ if (future.v7_throwAbortReason && request.signal.reason !== undefined) {
3711
+ throw request.signal.reason;
3712
+ }
3713
+ let method = isRouteRequest ? "queryRoute" : "query";
3714
+ throw new Error(method + "() call aborted: " + request.method + " " + request.url);
3715
+ }
3681
3716
  function isSubmissionNavigation(opts) {
3682
3717
  return opts != null && ("formData" in opts && opts.formData != null || "body" in opts && opts.body !== undefined);
3683
3718
  }
@@ -3762,8 +3797,8 @@ function normalizeNavigateOptions(normalizeFormMethod, isFetcher, path, opts) {
3762
3797
  }
3763
3798
  let text = typeof opts.body === "string" ? opts.body : opts.body instanceof FormData || opts.body instanceof URLSearchParams ?
3764
3799
  // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#plain-text-form-data
3765
- Array.from(opts.body.entries()).reduce((acc, _ref4) => {
3766
- let [name, value] = _ref4;
3800
+ Array.from(opts.body.entries()).reduce((acc, _ref3) => {
3801
+ let [name, value] = _ref3;
3767
3802
  return "" + acc + name + "=" + value + "\n";
3768
3803
  }, "") : String(opts.body);
3769
3804
  return {
@@ -3874,18 +3909,24 @@ function getMatchesToLoad(history, state, matches, submission, location, isIniti
3874
3909
  let boundaryId = pendingError ? Object.keys(pendingError)[0] : undefined;
3875
3910
  let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId);
3876
3911
  let navigationMatches = boundaryMatches.filter((match, index) => {
3877
- if (isInitialLoad) {
3878
- // On initial hydration we don't do any shouldRevalidate stuff - we just
3879
- // call the unhydrated loaders
3880
- return isUnhydratedRoute(state, match.route);
3881
- }
3882
- if (match.route.lazy) {
3912
+ let {
3913
+ route
3914
+ } = match;
3915
+ if (route.lazy) {
3883
3916
  // We haven't loaded this route yet so we don't know if it's got a loader!
3884
3917
  return true;
3885
3918
  }
3886
- if (match.route.loader == null) {
3919
+ if (route.loader == null) {
3887
3920
  return false;
3888
3921
  }
3922
+ if (isInitialLoad) {
3923
+ if (typeof route.loader !== "function" || route.loader.hydrate) {
3924
+ return true;
3925
+ }
3926
+ return state.loaderData[route.id] === undefined && (
3927
+ // Don't re-run if the loader ran and threw an error
3928
+ !state.errors || state.errors[route.id] === undefined);
3929
+ }
3889
3930
 
3890
3931
  // Always call the loader on new route instances and pending defer cancellations
3891
3932
  if (isNewLoader(state.loaderData, state.matches[index], match) || cancelledDeferredRoutes.some(id => id === match.route.id)) {
@@ -3987,20 +4028,6 @@ function getMatchesToLoad(history, state, matches, submission, location, isIniti
3987
4028
  });
3988
4029
  return [navigationMatches, revalidatingFetchers];
3989
4030
  }
3990
-
3991
- // Is this route unhydrated (when v7_partialHydration=true) such that we need
3992
- // to call it's loader on the initial router creation
3993
- function isUnhydratedRoute(state, route) {
3994
- if (!route.loader) {
3995
- return false;
3996
- }
3997
- if (route.loader.hydrate) {
3998
- return true;
3999
- }
4000
- return state.loaderData[route.id] === undefined && (!state.errors ||
4001
- // Loader ran but errored - don't re-run
4002
- state.errors[route.id] === undefined);
4003
- }
4004
4031
  function isNewLoader(currentLoaderData, currentMatch, match) {
4005
4032
  let isNew =
4006
4033
  // [a] -> [a, b]
@@ -4088,44 +4115,57 @@ async function loadLazyRouteModule(route, mapRouteProperties, manifest) {
4088
4115
  }));
4089
4116
  return routeToUpdate;
4090
4117
  }
4091
- function createCallLoaderOrAction(dataStrategy) {
4092
- return async function (type, request, match, matches, manifest, mapRouteProperties, basename, v7_relativeSplatPath, opts) {
4093
- if (opts === void 0) {
4094
- opts = {};
4095
- }
4096
- let [result] = await dataStrategy({
4097
- matches: [finesseToAgnosticDataStrategyMatch(match, mapRouteProperties, manifest)],
4098
- request,
4099
- type,
4100
- defaultStrategy(match) {
4101
- return callLoaderOrActionImplementation(type, request, match, matches, basename, v7_relativeSplatPath, opts);
4102
- }
4103
- });
4104
- return result;
4105
- };
4118
+
4119
+ // Default implementation of `dataStrategy` which fetches all loaders in parallel
4120
+ function defaultDataStrategy(opts) {
4121
+ return Promise.all(opts.matches.map(m => m.bikeshed_loadRoute()));
4106
4122
  }
4107
- function finesseToAgnosticDataStrategyMatch(match, mapRouteProperties, manifest) {
4108
- let loadRoutePromise;
4109
- if (match.route.lazy) {
4110
- try {
4111
- loadRoutePromise = loadLazyRouteModule(match.route, mapRouteProperties, manifest);
4112
- } catch (error) {
4113
- loadRoutePromise = Promise.reject(error);
4114
- }
4115
- }
4116
- if (!loadRoutePromise) {
4117
- loadRoutePromise = Promise.resolve(match.route);
4118
- }
4119
- loadRoutePromise.catch(() => {});
4120
- return _extends({}, match, {
4121
- route: Object.assign(loadRoutePromise, match.route)
4123
+ async function callDataStrategyImpl(dataStrategyImpl, type, request, matchesToLoad, matches, manifest, mapRouteProperties, requestContext) {
4124
+ let routeIdsToLoad = matchesToLoad.reduce((acc, m) => acc.add(m.route.id), new Set());
4125
+ let loadedMatches = new Set();
4126
+
4127
+ // Send all matches here to allow for a middleware-type implementation.
4128
+ // handler will be a no-op for unneeded routes and we filter those results
4129
+ // back out below.
4130
+ let results = await dataStrategyImpl({
4131
+ matches: matches.map(match => {
4132
+ // `bikeshed_loadRoute` encapsulates the route.lazy, executing the
4133
+ // loader/action, and mapping return values/thrown errors to a
4134
+ // HandlerResult. Users can pass a callback to take fine-grained control
4135
+ // over the execution of the loader/action
4136
+ let bikeshed_loadRoute = handlerOverride => {
4137
+ loadedMatches.add(match.route.id);
4138
+ return routeIdsToLoad.has(match.route.id) ? callLoaderOrAction(type, request, match, manifest, mapRouteProperties, handlerOverride, requestContext) :
4139
+ // TODO: What's the best thing to do here - return an empty "success" result?
4140
+ // Or return a success result with the current route loader/action data?
4141
+ // We strip these results out if the route didn't need to be revalidated in
4142
+ // `callDataStrategy` so it doesn't matter for us. It's more of a question
4143
+ // of whether exposing the current data to the user is useful?
4144
+ Promise.resolve({
4145
+ type: ResultType.data,
4146
+ result: undefined
4147
+ });
4148
+ };
4149
+ return _extends({}, match, {
4150
+ bikeshed_loadRoute
4151
+ });
4152
+ }),
4153
+ request,
4154
+ params: matches[0].params
4122
4155
  });
4156
+
4157
+ // Throw if any loadRoute implementations not called since they are what
4158
+ // ensures a route is fully loaded
4159
+ // Throw if any loadRoute implementations not called since they are what
4160
+ // ensure a route is fully loaded
4161
+ matches.forEach(m => invariant(loadedMatches.has(m.route.id), "`match.bikeshed_loadRoute()` was not called for route id \"" + m.route.id + "\". " + "You must call `match.bikeshed_loadRoute()` on every match passed to " + "`dataStrategy` to ensure all routes are properly loaded."));
4162
+
4163
+ // Filter out any middleware-only matches for which we didn't need to run handlers
4164
+ return results.filter((_, i) => routeIdsToLoad.has(matches[i].route.id));
4123
4165
  }
4124
- async function callLoaderOrActionImplementation(type, request, match, matches, basename, v7_relativeSplatPath, opts) {
4125
- if (opts === void 0) {
4126
- opts = {};
4127
- }
4128
- let resultType;
4166
+
4167
+ // Default logic for calling a loader/action is the user has no specified a dataStrategy
4168
+ async function callLoaderOrAction(type, request, match, manifest, mapRouteProperties, handlerOverride, staticContext) {
4129
4169
  let result;
4130
4170
  let onReject;
4131
4171
  let runHandler = handler => {
@@ -4134,11 +4174,18 @@ async function callLoaderOrActionImplementation(type, request, match, matches, b
4134
4174
  let abortPromise = new Promise((_, r) => reject = r);
4135
4175
  onReject = () => reject();
4136
4176
  request.signal.addEventListener("abort", onReject);
4137
- return Promise.race([handler({
4138
- request,
4139
- params: match.params,
4140
- context: opts.requestContext
4141
- }), abortPromise]);
4177
+ let runHandlerForReal = ctx => {
4178
+ if (typeof handler !== "function") {
4179
+ return Promise.reject(new Error("You cannot call the handler for a route which defines a boolean " + ("\"" + type + "\" [routeId: " + match.route.id + "]")));
4180
+ }
4181
+ return handler({
4182
+ request,
4183
+ params: match.params,
4184
+ context: staticContext
4185
+ }, ...(ctx !== undefined ? [ctx] : []));
4186
+ };
4187
+ let handlerPromise = handlerOverride ? handlerOverride(async ctx => runHandlerForReal(ctx)) : runHandlerForReal();
4188
+ return Promise.race([handlerPromise, abortPromise]);
4142
4189
  };
4143
4190
  try {
4144
4191
  let handler = match.route[type];
@@ -4152,17 +4199,17 @@ async function callLoaderOrActionImplementation(type, request, match, matches, b
4152
4199
  // route has a boundary that can handle the error
4153
4200
  runHandler(handler).catch(e => {
4154
4201
  handlerError = e;
4155
- }), match.route]);
4202
+ }), loadLazyRouteModule(match.route, mapRouteProperties, manifest)]);
4156
4203
  if (handlerError) {
4157
4204
  throw handlerError;
4158
4205
  }
4159
4206
  result = values[0];
4160
4207
  } else {
4161
4208
  // Load lazy route module, then run any returned handler
4162
- let route = await match.route;
4163
- handler = route[type];
4209
+ await loadLazyRouteModule(match.route, mapRouteProperties, manifest);
4210
+ handler = match.route[type];
4164
4211
  if (handler) {
4165
- // Handler still run even if we got interrupted to maintain consistency
4212
+ // Handler still runs even if we got interrupted to maintain consistency
4166
4213
  // with un-abortable behavior of handler execution on non-lazy or
4167
4214
  // previously-lazy-loaded routes
4168
4215
  result = await runHandler(handler);
@@ -4179,7 +4226,7 @@ async function callLoaderOrActionImplementation(type, request, match, matches, b
4179
4226
  // hit the invariant below that errors on returning undefined.
4180
4227
  return {
4181
4228
  type: ResultType.data,
4182
- data: undefined
4229
+ result: undefined
4183
4230
  };
4184
4231
  }
4185
4232
  }
@@ -4194,70 +4241,37 @@ async function callLoaderOrActionImplementation(type, request, match, matches, b
4194
4241
  }
4195
4242
  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`.");
4196
4243
  } catch (e) {
4197
- resultType = ResultType.error;
4198
- result = e;
4244
+ return {
4245
+ type: ResultType.error,
4246
+ result: e
4247
+ };
4199
4248
  } finally {
4200
4249
  if (onReject) {
4201
4250
  request.signal.removeEventListener("abort", onReject);
4202
4251
  }
4203
4252
  }
4253
+ return {
4254
+ type: ResultType.data,
4255
+ result
4256
+ };
4257
+ }
4258
+ async function convertHandlerResultToDataResult(handlerResult) {
4259
+ let {
4260
+ result,
4261
+ type
4262
+ } = handlerResult;
4204
4263
  if (isResponse(result)) {
4205
- let status = result.status;
4206
-
4207
- // Process redirects
4208
- if (redirectStatusCodes.has(status)) {
4209
- let location = result.headers.get("Location");
4210
- invariant(location, "Redirects returned/thrown from loaders/actions must have a Location header");
4211
-
4212
- // Support relative routing in internal redirects
4213
- if (!ABSOLUTE_URL_REGEX.test(location)) {
4214
- location = normalizeTo(new URL(request.url), matches.slice(0, matches.findIndex(m => m.route.id === match.route.id) + 1), basename, true, location, v7_relativeSplatPath);
4215
- } else if (!opts.isStaticRequest) {
4216
- // Strip off the protocol+origin for same-origin + same-basename absolute
4217
- // redirects. If this is a static request, we can let it go back to the
4218
- // browser as-is
4219
- let currentUrl = new URL(request.url);
4220
- let url = location.startsWith("//") ? new URL(currentUrl.protocol + location) : new URL(location);
4221
- let isSameBasename = stripBasename(url.pathname, basename) != null;
4222
- if (url.origin === currentUrl.origin && isSameBasename) {
4223
- location = url.pathname + url.search + url.hash;
4224
- }
4225
- }
4226
-
4227
- // Don't process redirects in the router during static requests requests.
4228
- // Instead, throw the Response and let the server handle it with an HTTP
4229
- // redirect. We also update the Location header in place in this flow so
4230
- // basename and relative routing is taken into account
4231
- if (opts.isStaticRequest) {
4232
- result.headers.set("Location", location);
4233
- throw result;
4234
- }
4235
- return {
4236
- type: ResultType.redirect,
4237
- status,
4238
- location,
4239
- revalidate: result.headers.get("X-Remix-Revalidate") !== null,
4240
- reloadDocument: result.headers.get("X-Remix-Reload-Document") !== null
4241
- };
4242
- }
4243
-
4244
- // For SSR single-route requests, we want to hand Responses back directly
4245
- // without unwrapping. We do this with the QueryRouteResponse wrapper
4246
- // interface so we can know whether it was returned or thrown
4247
- if (opts.isRouteRequest) {
4248
- let queryRouteResponse = {
4249
- type: resultType === ResultType.error ? ResultType.error : ResultType.data,
4250
- response: result
4251
- };
4252
- throw queryRouteResponse;
4253
- }
4254
4264
  let data;
4255
4265
  try {
4256
4266
  let contentType = result.headers.get("Content-Type");
4257
4267
  // Check between word boundaries instead of startsWith() due to the last
4258
4268
  // paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type
4259
4269
  if (contentType && /\bapplication\/json\b/.test(contentType)) {
4260
- data = await result.json();
4270
+ if (result.body == null) {
4271
+ data = null;
4272
+ } else {
4273
+ data = await result.json();
4274
+ }
4261
4275
  } else {
4262
4276
  data = await result.text();
4263
4277
  }
@@ -4267,23 +4281,22 @@ async function callLoaderOrActionImplementation(type, request, match, matches, b
4267
4281
  error: e
4268
4282
  };
4269
4283
  }
4270
- if (resultType === ResultType.error) {
4284
+ if (type === ResultType.error) {
4271
4285
  return {
4272
- type: resultType,
4273
- error: new ErrorResponseImpl(status, result.statusText, data),
4274
- headers: result.headers
4286
+ type,
4287
+ error: new ErrorResponseImpl(result.status, result.statusText, data),
4288
+ response: result
4275
4289
  };
4276
4290
  }
4277
4291
  return {
4278
4292
  type: ResultType.data,
4279
4293
  data,
4280
- statusCode: result.status,
4281
- headers: result.headers
4294
+ response: result
4282
4295
  };
4283
4296
  }
4284
- if (resultType === ResultType.error) {
4297
+ if (type === ResultType.error) {
4285
4298
  return {
4286
- type: resultType,
4299
+ type,
4287
4300
  error: result
4288
4301
  };
4289
4302
  }
@@ -4302,6 +4315,18 @@ async function callLoaderOrActionImplementation(type, request, match, matches, b
4302
4315
  };
4303
4316
  }
4304
4317
 
4318
+ // Support relative routing in internal redirects
4319
+ function normalizeRelativeRoutingRedirectResponse(response, request, routeId, matches, basename, v7_relativeSplatPath) {
4320
+ let location = response.headers.get("Location");
4321
+ invariant(location, "Redirects returned/thrown from loaders/actions must have a Location header");
4322
+ if (!ABSOLUTE_URL_REGEX.test(location)) {
4323
+ let trimmedMatches = matches.slice(0, matches.findIndex(m => m.route.id === routeId) + 1);
4324
+ location = normalizeTo(new URL(request.url), trimmedMatches, basename, true, location, v7_relativeSplatPath);
4325
+ response.headers.set("Location", location);
4326
+ }
4327
+ return response;
4328
+ }
4329
+
4305
4330
  // Utility method for creating the Request instances for loaders/actions during
4306
4331
  // client-side navigations and fetches. During SSR we will always have a
4307
4332
  // Request instance from the static handler (query/queryRoute)
@@ -4392,24 +4417,31 @@ function processRouteLoaderData(matches, matchesToLoad, results, pendingError, a
4392
4417
  foundError = true;
4393
4418
  statusCode = isRouteErrorResponse(result.error) ? result.error.status : 500;
4394
4419
  }
4395
- if (result.headers) {
4396
- loaderHeaders[id] = result.headers;
4420
+ if (result.response) {
4421
+ loaderHeaders[id] = result.response.headers;
4397
4422
  }
4398
4423
  } else {
4399
4424
  if (isDeferredResult(result)) {
4400
4425
  activeDeferreds.set(id, result.deferredData);
4401
4426
  loaderData[id] = result.deferredData.data;
4427
+ // Error status codes always override success status codes, but if all
4428
+ // loaders are successful we take the deepest status code.
4429
+ if (result.statusCode != null && result.statusCode !== 200 && !foundError) {
4430
+ statusCode = result.statusCode;
4431
+ }
4432
+ if (result.headers) {
4433
+ loaderHeaders[id] = result.headers;
4434
+ }
4402
4435
  } else {
4403
4436
  loaderData[id] = result.data;
4404
- }
4405
-
4406
- // Error status codes always override success status codes, but if all
4407
- // loaders are successful we take the deepest status code.
4408
- if (result.statusCode != null && result.statusCode !== 200 && !foundError) {
4409
- statusCode = result.statusCode;
4410
- }
4411
- if (result.headers) {
4412
- loaderHeaders[id] = result.headers;
4437
+ // Error status codes always override success status codes, but if all
4438
+ // loaders are successful we take the deepest status code.
4439
+ if (result.response) {
4440
+ if (result.response.status !== 200 && !foundError) {
4441
+ statusCode = result.response.status;
4442
+ }
4443
+ loaderHeaders[id] = result.response.headers;
4444
+ }
4413
4445
  }
4414
4446
  }
4415
4447
  });
@@ -4589,6 +4621,12 @@ function isHashChangeOnly(a, b) {
4589
4621
  // /page#hash -> /page
4590
4622
  return false;
4591
4623
  }
4624
+ function isHandlerResult(result) {
4625
+ return result != null && typeof result === "object" && "type" in result && "result" in result && (result.type === ResultType.data || result.type === ResultType.error);
4626
+ }
4627
+ function isRedirectHandlerResult(result) {
4628
+ return isResponse(result.result) && redirectStatusCodes.has(result.result.status);
4629
+ }
4592
4630
  function isDeferredResult(result) {
4593
4631
  return result.type === ResultType.deferred;
4594
4632
  }
@@ -4613,9 +4651,6 @@ function isRedirectResponse(result) {
4613
4651
  let location = result.headers.get("Location");
4614
4652
  return status >= 300 && status <= 399 && location != null;
4615
4653
  }
4616
- function isQueryRouteResponse(obj) {
4617
- return obj && isResponse(obj.response) && (obj.type === ResultType.data || obj.type === ResultType.error);
4618
- }
4619
4654
  function isValidMethod(method) {
4620
4655
  return validRequestMethods.has(method.toLowerCase());
4621
4656
  }
@@ -4859,7 +4894,6 @@ exports.Action = Action;
4859
4894
  exports.IDLE_BLOCKER = IDLE_BLOCKER;
4860
4895
  exports.IDLE_FETCHER = IDLE_FETCHER;
4861
4896
  exports.IDLE_NAVIGATION = IDLE_NAVIGATION;
4862
- exports.ResultType = ResultType;
4863
4897
  exports.UNSAFE_DEFERRED_SYMBOL = UNSAFE_DEFERRED_SYMBOL;
4864
4898
  exports.UNSAFE_DeferredData = DeferredData;
4865
4899
  exports.UNSAFE_ErrorResponseImpl = ErrorResponseImpl;