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