@remix-run/router 1.15.3 → 1.16.0-pre.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @remix-run/router v1.15.3
2
+ * @remix-run/router v1.16.0-pre.0
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -577,6 +577,10 @@ let ResultType = /*#__PURE__*/function (ResultType) {
577
577
  * Result from a loader or action - potentially successful or unsuccessful
578
578
  */
579
579
 
580
+ /**
581
+ * Result from a loader or action called via dataStrategy
582
+ */
583
+
580
584
  /**
581
585
  * Users can specify either lowercase or uppercase form methods on `<Form>`,
582
586
  * useSubmit(), `<fetcher.Form>`, etc.
@@ -1593,11 +1597,6 @@ function isRouteErrorResponse(error) {
1593
1597
  /**
1594
1598
  * Identified fetcher.load() calls that need to be revalidated
1595
1599
  */
1596
- /**
1597
- * Wrapper object to allow us to throw any response out from callLoaderOrAction
1598
- * for queryRouter while preserving whether or not it was thrown or returned
1599
- * from the loader/action
1600
- */
1601
1600
  const validMutationMethodsArr = ["post", "put", "patch", "delete"];
1602
1601
  const validMutationMethods = new Set(validMutationMethodsArr);
1603
1602
  const validRequestMethodsArr = ["get", ...validMutationMethodsArr];
@@ -1669,13 +1668,15 @@ function createRouter(init) {
1669
1668
  let dataRoutes = convertRoutesToDataRoutes(init.routes, mapRouteProperties, undefined, manifest);
1670
1669
  let inFlightDataRoutes;
1671
1670
  let basename = init.basename || "/";
1671
+ let dataStrategyImpl = init.unstable_dataStrategy || defaultDataStrategy;
1672
1672
  // Config driven behavior flags
1673
1673
  let future = _extends({
1674
1674
  v7_fetcherPersist: false,
1675
1675
  v7_normalizeFormMethod: false,
1676
1676
  v7_partialHydration: false,
1677
1677
  v7_prependBasename: false,
1678
- v7_relativeSplatPath: false
1678
+ v7_relativeSplatPath: false,
1679
+ unstable_skipActionErrorRevalidation: false
1679
1680
  }, init.future);
1680
1681
  // Cleanup function for history
1681
1682
  let unlistenHistory = null;
@@ -1729,9 +1730,13 @@ function createRouter(init) {
1729
1730
  let errors = init.hydrationData ? init.hydrationData.errors : null;
1730
1731
  let isRouteInitialized = m => {
1731
1732
  // No loader, nothing to initialize
1732
- if (!m.route.loader) return true;
1733
+ if (!m.route.loader) {
1734
+ return true;
1735
+ }
1733
1736
  // Explicitly opting-in to running on hydration
1734
- if (m.route.loader.hydrate === true) return false;
1737
+ if (typeof m.route.loader === "function" && m.route.loader.hydrate === true) {
1738
+ return false;
1739
+ }
1735
1740
  // Otherwise, initialized if hydrated with data or an error
1736
1741
  return loaderData && loaderData[m.route.id] !== undefined || errors && errors[m.route.id] !== undefined;
1737
1742
  };
@@ -2272,34 +2277,31 @@ function createRouter(init) {
2272
2277
  // Create a controller/Request for this navigation
2273
2278
  pendingNavigationController = new AbortController();
2274
2279
  let request = createClientSideRequest(init.history, location, pendingNavigationController.signal, opts && opts.submission);
2275
- let pendingActionData;
2276
- let pendingError;
2280
+ let pendingActionResult;
2277
2281
  if (opts && opts.pendingError) {
2278
2282
  // If we have a pendingError, it means the user attempted a GET submission
2279
2283
  // with binary FormData so assign here and skip to handleLoaders. That
2280
2284
  // way we handle calling loaders above the boundary etc. It's not really
2281
2285
  // different from an actionError in that sense.
2282
- pendingError = {
2283
- [findNearestBoundary(matches).route.id]: opts.pendingError
2284
- };
2286
+ pendingActionResult = [findNearestBoundary(matches).route.id, {
2287
+ type: ResultType.error,
2288
+ error: opts.pendingError
2289
+ }];
2285
2290
  } else if (opts && opts.submission && isMutationMethod(opts.submission.formMethod)) {
2286
2291
  // Call action if we received an action submission
2287
- let actionOutput = await handleAction(request, location, opts.submission, matches, {
2292
+ let actionResult = await handleAction(request, location, opts.submission, matches, {
2288
2293
  replace: opts.replace,
2289
2294
  flushSync
2290
2295
  });
2291
- if (actionOutput.shortCircuited) {
2296
+ if (actionResult.shortCircuited) {
2292
2297
  return;
2293
2298
  }
2294
- pendingActionData = actionOutput.pendingActionData;
2295
- pendingError = actionOutput.pendingActionError;
2299
+ pendingActionResult = actionResult.pendingActionResult;
2296
2300
  loadingNavigation = getLoadingNavigation(location, opts.submission);
2297
2301
  flushSync = false;
2298
2302
 
2299
2303
  // Create a GET request for the loaders
2300
- request = new Request(request.url, {
2301
- signal: request.signal
2302
- });
2304
+ request = createClientSideRequest(init.history, request.url, request.signal);
2303
2305
  }
2304
2306
 
2305
2307
  // Call loaders
@@ -2307,7 +2309,7 @@ function createRouter(init) {
2307
2309
  shortCircuited,
2308
2310
  loaderData,
2309
2311
  errors
2310
- } = await handleLoaders(request, location, matches, loadingNavigation, opts && opts.submission, opts && opts.fetcherSubmission, opts && opts.replace, opts && opts.initialHydration === true, flushSync, pendingActionData, pendingError);
2312
+ } = await handleLoaders(request, location, matches, loadingNavigation, opts && opts.submission, opts && opts.fetcherSubmission, opts && opts.replace, opts && opts.initialHydration === true, flushSync, pendingActionResult);
2311
2313
  if (shortCircuited) {
2312
2314
  return;
2313
2315
  }
@@ -2318,9 +2320,7 @@ function createRouter(init) {
2318
2320
  pendingNavigationController = null;
2319
2321
  completeNavigation(location, _extends({
2320
2322
  matches
2321
- }, pendingActionData ? {
2322
- actionData: pendingActionData
2323
- } : {}, {
2323
+ }, getActionDataForCommit(pendingActionResult), {
2324
2324
  loaderData,
2325
2325
  errors
2326
2326
  }));
@@ -2355,7 +2355,8 @@ function createRouter(init) {
2355
2355
  })
2356
2356
  };
2357
2357
  } else {
2358
- result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
2358
+ let results = await callDataStrategy("action", request, [actionMatch], matches);
2359
+ result = results[0];
2359
2360
  if (request.signal.aborted) {
2360
2361
  return {
2361
2362
  shortCircuited: true
@@ -2370,9 +2371,10 @@ function createRouter(init) {
2370
2371
  // If the user didn't explicity indicate replace behavior, replace if
2371
2372
  // we redirected to the exact same location we're currently at to avoid
2372
2373
  // double back-buttons
2373
- replace = result.location === state.location.pathname + state.location.search;
2374
+ let location = normalizeRedirectLocation(result.response.headers.get("Location"), new URL(request.url), basename);
2375
+ replace = location === state.location.pathname + state.location.search;
2374
2376
  }
2375
- await startRedirectNavigation(state, result, {
2377
+ await startRedirectNavigation(request, result, {
2376
2378
  submission,
2377
2379
  replace
2378
2380
  });
@@ -2380,6 +2382,11 @@ function createRouter(init) {
2380
2382
  shortCircuited: true
2381
2383
  };
2382
2384
  }
2385
+ if (isDeferredResult(result)) {
2386
+ throw getInternalRouterError(400, {
2387
+ type: "defer-action"
2388
+ });
2389
+ }
2383
2390
  if (isErrorResult(result)) {
2384
2391
  // Store off the pending error - we use it to determine which loaders
2385
2392
  // to call and will commit it when we complete the navigation
@@ -2393,28 +2400,17 @@ function createRouter(init) {
2393
2400
  pendingAction = Action.Push;
2394
2401
  }
2395
2402
  return {
2396
- // Send back an empty object we can use to clear out any prior actionData
2397
- pendingActionData: {},
2398
- pendingActionError: {
2399
- [boundaryMatch.route.id]: result.error
2400
- }
2403
+ pendingActionResult: [boundaryMatch.route.id, result]
2401
2404
  };
2402
2405
  }
2403
- if (isDeferredResult(result)) {
2404
- throw getInternalRouterError(400, {
2405
- type: "defer-action"
2406
- });
2407
- }
2408
2406
  return {
2409
- pendingActionData: {
2410
- [actionMatch.route.id]: result.data
2411
- }
2407
+ pendingActionResult: [actionMatch.route.id, result]
2412
2408
  };
2413
2409
  }
2414
2410
 
2415
2411
  // Call all applicable loaders for the given matches, handling redirects,
2416
2412
  // errors, etc.
2417
- async function handleLoaders(request, location, matches, overrideNavigation, submission, fetcherSubmission, replace, initialHydration, flushSync, pendingActionData, pendingError) {
2413
+ async function handleLoaders(request, location, matches, overrideNavigation, submission, fetcherSubmission, replace, initialHydration, flushSync, pendingActionResult) {
2418
2414
  // Figure out the right navigation we want to use for data loading
2419
2415
  let loadingNavigation = overrideNavigation || getLoadingNavigation(location, submission);
2420
2416
 
@@ -2422,7 +2418,7 @@ function createRouter(init) {
2422
2418
  // we have it on the loading navigation so use that if available
2423
2419
  let activeSubmission = submission || fetcherSubmission || getSubmissionFromNavigation(loadingNavigation);
2424
2420
  let routesToUse = inFlightDataRoutes || dataRoutes;
2425
- let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, activeSubmission, location, future.v7_partialHydration && initialHydration === true, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionData, pendingError);
2421
+ let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, activeSubmission, location, future.v7_partialHydration && initialHydration === true, future.unstable_skipActionErrorRevalidation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionResult);
2426
2422
 
2427
2423
  // Cancel pending deferreds for no-longer-matched routes or routes we're
2428
2424
  // about to reload. Note that if this is an action reload we would have
@@ -2437,10 +2433,10 @@ function createRouter(init) {
2437
2433
  matches,
2438
2434
  loaderData: {},
2439
2435
  // Commit pending error if we're short circuiting
2440
- errors: pendingError || null
2441
- }, pendingActionData ? {
2442
- actionData: pendingActionData
2443
- } : {}, updatedFetchers ? {
2436
+ errors: pendingActionResult && isErrorResult(pendingActionResult[1]) ? {
2437
+ [pendingActionResult[0]]: pendingActionResult[1].error
2438
+ } : null
2439
+ }, getActionDataForCommit(pendingActionResult), updatedFetchers ? {
2444
2440
  fetchers: new Map(state.fetchers)
2445
2441
  } : {}), {
2446
2442
  flushSync
@@ -2462,12 +2458,24 @@ function createRouter(init) {
2462
2458
  let revalidatingFetcher = getLoadingFetcher(undefined, fetcher ? fetcher.data : undefined);
2463
2459
  state.fetchers.set(rf.key, revalidatingFetcher);
2464
2460
  });
2465
- let actionData = pendingActionData || state.actionData;
2461
+ let actionData;
2462
+ if (pendingActionResult && !isErrorResult(pendingActionResult[1])) {
2463
+ // This is cast to `any` currently because `RouteData`uses any and it
2464
+ // would be a breaking change to use any.
2465
+ // TODO: v7 - change `RouteData` to use `unknown` instead of `any`
2466
+ actionData = {
2467
+ [pendingActionResult[0]]: pendingActionResult[1].data
2468
+ };
2469
+ } else if (state.actionData) {
2470
+ if (Object.keys(state.actionData).length === 0) {
2471
+ actionData = null;
2472
+ } else {
2473
+ actionData = state.actionData;
2474
+ }
2475
+ }
2466
2476
  updateState(_extends({
2467
2477
  navigation: loadingNavigation
2468
- }, actionData ? Object.keys(actionData).length === 0 ? {
2469
- actionData: null
2470
- } : {
2478
+ }, actionData !== undefined ? {
2471
2479
  actionData
2472
2480
  } : {}, revalidatingFetchers.length > 0 ? {
2473
2481
  fetchers: new Map(state.fetchers)
@@ -2493,7 +2501,6 @@ function createRouter(init) {
2493
2501
  pendingNavigationController.signal.addEventListener("abort", abortPendingFetchRevalidations);
2494
2502
  }
2495
2503
  let {
2496
- results,
2497
2504
  loaderResults,
2498
2505
  fetcherResults
2499
2506
  } = await callLoadersAndMaybeResolveData(state.matches, matches, matchesToLoad, revalidatingFetchers, request);
@@ -2512,7 +2519,7 @@ function createRouter(init) {
2512
2519
  revalidatingFetchers.forEach(rf => fetchControllers.delete(rf.key));
2513
2520
 
2514
2521
  // If any loaders returned a redirect Response, start a new REPLACE navigation
2515
- let redirect = findRedirect(results);
2522
+ let redirect = findRedirect([...loaderResults, ...fetcherResults]);
2516
2523
  if (redirect) {
2517
2524
  if (redirect.idx >= matchesToLoad.length) {
2518
2525
  // If this redirect came from a fetcher make sure we mark it in
@@ -2521,7 +2528,7 @@ function createRouter(init) {
2521
2528
  let fetcherKey = revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
2522
2529
  fetchRedirectIds.add(fetcherKey);
2523
2530
  }
2524
- await startRedirectNavigation(state, redirect.result, {
2531
+ await startRedirectNavigation(request, redirect.result, {
2525
2532
  replace
2526
2533
  });
2527
2534
  return {
@@ -2533,7 +2540,7 @@ function createRouter(init) {
2533
2540
  let {
2534
2541
  loaderData,
2535
2542
  errors
2536
- } = processLoaderData(state, matches, matchesToLoad, loaderResults, pendingError, revalidatingFetchers, fetcherResults, activeDeferreds);
2543
+ } = processLoaderData(state, matches, matchesToLoad, loaderResults, pendingActionResult, revalidatingFetchers, fetcherResults, activeDeferreds);
2537
2544
 
2538
2545
  // Wire up subscribers to update loaderData as promises settle
2539
2546
  activeDeferreds.forEach((deferredData, routeId) => {
@@ -2643,7 +2650,8 @@ function createRouter(init) {
2643
2650
  let fetchRequest = createClientSideRequest(init.history, path, abortController.signal, submission);
2644
2651
  fetchControllers.set(key, abortController);
2645
2652
  let originatingLoadId = incrementingLoadId;
2646
- let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
2653
+ let actionResults = await callDataStrategy("action", fetchRequest, [match], requestMatches);
2654
+ let actionResult = actionResults[0];
2647
2655
  if (fetchRequest.signal.aborted) {
2648
2656
  // We can delete this so long as we weren't aborted by our own fetcher
2649
2657
  // re-submit which would have put _new_ controller is in fetchControllers
@@ -2675,7 +2683,7 @@ function createRouter(init) {
2675
2683
  } else {
2676
2684
  fetchRedirectIds.add(key);
2677
2685
  updateFetcherState(key, getLoadingFetcher(submission));
2678
- return startRedirectNavigation(state, actionResult, {
2686
+ return startRedirectNavigation(fetchRequest, actionResult, {
2679
2687
  fetcherSubmission: submission
2680
2688
  });
2681
2689
  }
@@ -2704,10 +2712,7 @@ function createRouter(init) {
2704
2712
  fetchReloadIds.set(key, loadId);
2705
2713
  let loadFetcher = getLoadingFetcher(submission, actionResult.data);
2706
2714
  state.fetchers.set(key, loadFetcher);
2707
- let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, submission, nextLocation, false, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, {
2708
- [match.route.id]: actionResult.data
2709
- }, undefined // No need to send through errors since we short circuit above
2710
- );
2715
+ let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, submission, nextLocation, false, future.unstable_skipActionErrorRevalidation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, [match.route.id, actionResult]);
2711
2716
 
2712
2717
  // Put all revalidating fetchers into the loading state, except for the
2713
2718
  // current fetcher which we want to keep in it's current loading state which
@@ -2730,7 +2735,6 @@ function createRouter(init) {
2730
2735
  let abortPendingFetchRevalidations = () => revalidatingFetchers.forEach(rf => abortFetcher(rf.key));
2731
2736
  abortController.signal.addEventListener("abort", abortPendingFetchRevalidations);
2732
2737
  let {
2733
- results,
2734
2738
  loaderResults,
2735
2739
  fetcherResults
2736
2740
  } = await callLoadersAndMaybeResolveData(state.matches, matches, matchesToLoad, revalidatingFetchers, revalidationRequest);
@@ -2741,7 +2745,7 @@ function createRouter(init) {
2741
2745
  fetchReloadIds.delete(key);
2742
2746
  fetchControllers.delete(key);
2743
2747
  revalidatingFetchers.forEach(r => fetchControllers.delete(r.key));
2744
- let redirect = findRedirect(results);
2748
+ let redirect = findRedirect([...loaderResults, ...fetcherResults]);
2745
2749
  if (redirect) {
2746
2750
  if (redirect.idx >= matchesToLoad.length) {
2747
2751
  // If this redirect came from a fetcher make sure we mark it in
@@ -2750,7 +2754,7 @@ function createRouter(init) {
2750
2754
  let fetcherKey = revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
2751
2755
  fetchRedirectIds.add(fetcherKey);
2752
2756
  }
2753
- return startRedirectNavigation(state, redirect.result);
2757
+ return startRedirectNavigation(revalidationRequest, redirect.result);
2754
2758
  }
2755
2759
 
2756
2760
  // Process and commit output from loaders
@@ -2804,7 +2808,8 @@ function createRouter(init) {
2804
2808
  let fetchRequest = createClientSideRequest(init.history, path, abortController.signal);
2805
2809
  fetchControllers.set(key, abortController);
2806
2810
  let originatingLoadId = incrementingLoadId;
2807
- let result = await callLoaderOrAction("loader", fetchRequest, match, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
2811
+ let results = await callDataStrategy("loader", fetchRequest, [match], matches);
2812
+ let result = results[0];
2808
2813
 
2809
2814
  // Deferred isn't supported for fetcher loads, await everything and treat it
2810
2815
  // as a normal load. resolveDeferredData will return undefined if this
@@ -2839,7 +2844,7 @@ function createRouter(init) {
2839
2844
  return;
2840
2845
  } else {
2841
2846
  fetchRedirectIds.add(key);
2842
- await startRedirectNavigation(state, result);
2847
+ await startRedirectNavigation(fetchRequest, result);
2843
2848
  return;
2844
2849
  }
2845
2850
  }
@@ -2874,26 +2879,28 @@ function createRouter(init) {
2874
2879
  * actually touch history until we've processed redirects, so we just use
2875
2880
  * the history action from the original navigation (PUSH or REPLACE).
2876
2881
  */
2877
- async function startRedirectNavigation(state, redirect, _temp2) {
2882
+ async function startRedirectNavigation(request, redirect, _temp2) {
2878
2883
  let {
2879
2884
  submission,
2880
2885
  fetcherSubmission,
2881
2886
  replace
2882
2887
  } = _temp2 === void 0 ? {} : _temp2;
2883
- if (redirect.revalidate) {
2888
+ if (redirect.response.headers.has("X-Remix-Revalidate")) {
2884
2889
  isRevalidationRequired = true;
2885
2890
  }
2886
- let redirectLocation = createLocation(state.location, redirect.location, {
2891
+ let location = redirect.response.headers.get("Location");
2892
+ invariant(location, "Expected a Location header on the redirect Response");
2893
+ location = normalizeRedirectLocation(location, new URL(request.url), basename);
2894
+ let redirectLocation = createLocation(state.location, location, {
2887
2895
  _isRedirect: true
2888
2896
  });
2889
- invariant(redirectLocation, "Expected a location on the redirect navigation");
2890
2897
  if (isBrowser) {
2891
2898
  let isDocumentReload = false;
2892
- if (redirect.reloadDocument) {
2899
+ if (redirect.response.headers.has("X-Remix-Reload-Document")) {
2893
2900
  // Hard reload if the response contained X-Remix-Reload-Document
2894
2901
  isDocumentReload = true;
2895
- } else if (ABSOLUTE_URL_REGEX.test(redirect.location)) {
2896
- const url = init.history.createURL(redirect.location);
2902
+ } else if (ABSOLUTE_URL_REGEX.test(location)) {
2903
+ const url = init.history.createURL(location);
2897
2904
  isDocumentReload =
2898
2905
  // Hard reload if it's an absolute URL to a new origin
2899
2906
  url.origin !== routerWindow.location.origin ||
@@ -2902,9 +2909,9 @@ function createRouter(init) {
2902
2909
  }
2903
2910
  if (isDocumentReload) {
2904
2911
  if (replace) {
2905
- routerWindow.location.replace(redirect.location);
2912
+ routerWindow.location.replace(location);
2906
2913
  } else {
2907
- routerWindow.location.assign(redirect.location);
2914
+ routerWindow.location.assign(location);
2908
2915
  }
2909
2916
  return;
2910
2917
  }
@@ -2930,10 +2937,10 @@ function createRouter(init) {
2930
2937
  // re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the
2931
2938
  // redirected location
2932
2939
  let activeSubmission = submission || fetcherSubmission;
2933
- if (redirectPreserveMethodStatusCodes.has(redirect.status) && activeSubmission && isMutationMethod(activeSubmission.formMethod)) {
2940
+ if (redirectPreserveMethodStatusCodes.has(redirect.response.status) && activeSubmission && isMutationMethod(activeSubmission.formMethod)) {
2934
2941
  await startNavigation(redirectHistoryAction, redirectLocation, {
2935
2942
  submission: _extends({}, activeSubmission, {
2936
- formAction: redirect.location
2943
+ formAction: location
2937
2944
  }),
2938
2945
  // Preserve this flag across redirects
2939
2946
  preventScrollReset: pendingPreventScrollReset
@@ -2951,28 +2958,47 @@ function createRouter(init) {
2951
2958
  });
2952
2959
  }
2953
2960
  }
2961
+
2962
+ // Utility wrapper for calling dataStrategy client-side without having to
2963
+ // pass around the manifest, mapRouteProperties, etc.
2964
+ async function callDataStrategy(type, request, matchesToLoad, matches) {
2965
+ try {
2966
+ let results = await callDataStrategyImpl(dataStrategyImpl, type, request, matchesToLoad, matches, manifest, mapRouteProperties);
2967
+ return await Promise.all(results.map((result, i) => {
2968
+ if (isRedirectHandlerResult(result)) {
2969
+ let response = result.result;
2970
+ return {
2971
+ type: ResultType.redirect,
2972
+ response: normalizeRelativeRoutingRedirectResponse(response, request, matchesToLoad[i].route.id, matches, basename, future.v7_relativeSplatPath)
2973
+ };
2974
+ }
2975
+ return convertHandlerResultToDataResult(result);
2976
+ }));
2977
+ } catch (e) {
2978
+ // If the outer dataStrategy method throws, just return the error for all
2979
+ // matches - and it'll naturally bubble to the root
2980
+ return matchesToLoad.map(() => ({
2981
+ type: ResultType.error,
2982
+ error: e
2983
+ }));
2984
+ }
2985
+ }
2954
2986
  async function callLoadersAndMaybeResolveData(currentMatches, matches, matchesToLoad, fetchersToLoad, request) {
2955
- // Call all navigation loaders and revalidating fetcher loaders in parallel,
2956
- // then slice off the results into separate arrays so we can handle them
2957
- // accordingly
2958
- let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath)), ...fetchersToLoad.map(f => {
2987
+ let [loaderResults, ...fetcherResults] = await Promise.all([matchesToLoad.length ? callDataStrategy("loader", request, matchesToLoad, matches) : [], ...fetchersToLoad.map(f => {
2959
2988
  if (f.matches && f.match && f.controller) {
2960
- return callLoaderOrAction("loader", createClientSideRequest(init.history, f.path, f.controller.signal), f.match, f.matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
2989
+ let fetcherRequest = createClientSideRequest(init.history, f.path, f.controller.signal);
2990
+ return callDataStrategy("loader", fetcherRequest, [f.match], f.matches).then(r => r[0]);
2961
2991
  } else {
2962
- let error = {
2992
+ return Promise.resolve({
2963
2993
  type: ResultType.error,
2964
2994
  error: getInternalRouterError(404, {
2965
2995
  pathname: f.path
2966
2996
  })
2967
- };
2968
- return error;
2997
+ });
2969
2998
  }
2970
2999
  })]);
2971
- let loaderResults = results.slice(0, matchesToLoad.length);
2972
- let fetcherResults = results.slice(matchesToLoad.length);
2973
3000
  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)]);
2974
3001
  return {
2975
- results,
2976
3002
  loaderResults,
2977
3003
  fetcherResults
2978
3004
  };
@@ -3322,10 +3348,25 @@ function createStaticHandler(routes, opts) {
3322
3348
  * redirect response is returned or thrown from any action/loader. We
3323
3349
  * propagate that out and return the raw Response so the HTTP server can
3324
3350
  * return it directly.
3351
+ *
3352
+ * - `opts.loadRouteIds` is an optional array of routeIds to run only a subset of
3353
+ * loaders during a query() call
3354
+ * - `opts.requestContext` is an optional server context that will be passed
3355
+ * to actions/loaders in the `context` parameter
3356
+ * - `opts.skipLoaderErrorBubbling` is an optional parameter that will prevent
3357
+ * the bubbling of errors which allows single-fetch-type implementations
3358
+ * where the client will handle the bubbling and we may need to return data
3359
+ * for the handling route
3360
+ * - `opts.skipLoaders` is an optional parameter that will prevent loaders
3361
+ * from running after an action
3325
3362
  */
3326
3363
  async function query(request, _temp3) {
3327
3364
  let {
3328
- requestContext
3365
+ loadRouteIds,
3366
+ requestContext,
3367
+ skipLoaderErrorBubbling,
3368
+ skipLoaders,
3369
+ unstable_dataStrategy
3329
3370
  } = _temp3 === void 0 ? {} : _temp3;
3330
3371
  let url = new URL(request.url);
3331
3372
  let method = request.method;
@@ -3378,7 +3419,7 @@ function createStaticHandler(routes, opts) {
3378
3419
  activeDeferreds: null
3379
3420
  };
3380
3421
  }
3381
- let result = await queryImpl(request, location, matches, requestContext);
3422
+ let result = await queryImpl(request, location, matches, requestContext, unstable_dataStrategy || null, loadRouteIds || null, skipLoaderErrorBubbling === true, skipLoaders === true, null);
3382
3423
  if (isResponse(result)) {
3383
3424
  return result;
3384
3425
  }
@@ -3411,6 +3452,12 @@ function createStaticHandler(routes, opts) {
3411
3452
  * serialize the error as they see fit while including the proper response
3412
3453
  * code. Examples here are 404 and 405 errors that occur prior to reaching
3413
3454
  * any user-defined loaders.
3455
+ *
3456
+ * - `opts.routeId` allows you to specify the specific route handler to call.
3457
+ * If not provided the handler will determine the proper route by matching
3458
+ * against `request.url`
3459
+ * - `opts.requestContext` is an optional server context that will be passed
3460
+ * to actions/loaders in the `context` parameter
3414
3461
  */
3415
3462
  async function queryRoute(request, _temp4) {
3416
3463
  let {
@@ -3444,7 +3491,7 @@ function createStaticHandler(routes, opts) {
3444
3491
  pathname: location.pathname
3445
3492
  });
3446
3493
  }
3447
- let result = await queryImpl(request, location, matches, requestContext, match);
3494
+ let result = await queryImpl(request, location, matches, requestContext, null, null, false, false, match);
3448
3495
  if (isResponse(result)) {
3449
3496
  return result;
3450
3497
  }
@@ -3471,27 +3518,27 @@ function createStaticHandler(routes, opts) {
3471
3518
  }
3472
3519
  return undefined;
3473
3520
  }
3474
- async function queryImpl(request, location, matches, requestContext, routeMatch) {
3521
+ async function queryImpl(request, location, matches, requestContext, unstable_dataStrategy, loadRouteIds, skipLoaderErrorBubbling, skipLoaders, routeMatch) {
3475
3522
  invariant(request.signal, "query()/queryRoute() requests must contain an AbortController signal");
3476
3523
  try {
3477
3524
  if (isMutationMethod(request.method.toLowerCase())) {
3478
- let result = await submit(request, matches, routeMatch || getTargetMatch(matches, location), requestContext, routeMatch != null);
3525
+ let result = await submit(request, matches, routeMatch || getTargetMatch(matches, location), requestContext, unstable_dataStrategy, loadRouteIds, skipLoaderErrorBubbling, skipLoaders, routeMatch != null);
3479
3526
  return result;
3480
3527
  }
3481
- let result = await loadRouteData(request, matches, requestContext, routeMatch);
3528
+ let result = await loadRouteData(request, matches, requestContext, unstable_dataStrategy, loadRouteIds, skipLoaderErrorBubbling, routeMatch);
3482
3529
  return isResponse(result) ? result : _extends({}, result, {
3483
3530
  actionData: null,
3484
3531
  actionHeaders: {}
3485
3532
  });
3486
3533
  } catch (e) {
3487
- // If the user threw/returned a Response in callLoaderOrAction, we throw
3488
- // it to bail out and then return or throw here based on whether the user
3489
- // returned or threw
3490
- if (isQueryRouteResponse(e)) {
3534
+ // If the user threw/returned a Response in callLoaderOrAction for a
3535
+ // `queryRoute` call, we throw the `HandlerResult` to bail out early
3536
+ // and then return or throw the raw Response here accordingly
3537
+ if (isHandlerResult(e) && isResponse(e.result)) {
3491
3538
  if (e.type === ResultType.error) {
3492
- throw e.response;
3539
+ throw e.result;
3493
3540
  }
3494
- return e.response;
3541
+ return e.result;
3495
3542
  }
3496
3543
  // Redirects are always returned since they don't propagate to catch
3497
3544
  // boundaries
@@ -3501,7 +3548,7 @@ function createStaticHandler(routes, opts) {
3501
3548
  throw e;
3502
3549
  }
3503
3550
  }
3504
- async function submit(request, matches, actionMatch, requestContext, isRouteRequest) {
3551
+ async function submit(request, matches, actionMatch, requestContext, unstable_dataStrategy, loadRouteIds, skipLoaderErrorBubbling, skipLoaders, isRouteRequest) {
3505
3552
  let result;
3506
3553
  if (!actionMatch.route.action && !actionMatch.route.lazy) {
3507
3554
  let error = getInternalRouterError(405, {
@@ -3517,11 +3564,8 @@ function createStaticHandler(routes, opts) {
3517
3564
  error
3518
3565
  };
3519
3566
  } else {
3520
- result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath, {
3521
- isStaticRequest: true,
3522
- isRouteRequest,
3523
- requestContext
3524
- });
3567
+ let results = await callDataStrategy("action", request, [actionMatch], matches, isRouteRequest, requestContext, unstable_dataStrategy);
3568
+ result = results[0];
3525
3569
  if (request.signal.aborted) {
3526
3570
  throwStaticHandlerAbortedError(request, isRouteRequest, future);
3527
3571
  }
@@ -3532,9 +3576,9 @@ function createStaticHandler(routes, opts) {
3532
3576
  // can get back on the "throw all redirect responses" train here should
3533
3577
  // this ever happen :/
3534
3578
  throw new Response(null, {
3535
- status: result.status,
3579
+ status: result.response.status,
3536
3580
  headers: {
3537
- Location: result.location
3581
+ Location: result.response.headers.get("Location")
3538
3582
  }
3539
3583
  });
3540
3584
  }
@@ -3571,43 +3615,75 @@ function createStaticHandler(routes, opts) {
3571
3615
  activeDeferreds: null
3572
3616
  };
3573
3617
  }
3618
+
3619
+ // Create a GET request for the loaders
3620
+ let loaderRequest = new Request(request.url, {
3621
+ headers: request.headers,
3622
+ redirect: request.redirect,
3623
+ signal: request.signal
3624
+ });
3574
3625
  if (isErrorResult(result)) {
3575
3626
  // Store off the pending error - we use it to determine which loaders
3576
3627
  // to call and will commit it when we complete the navigation
3577
- let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
3578
- let context = await loadRouteData(request, matches, requestContext, undefined, {
3579
- [boundaryMatch.route.id]: result.error
3580
- });
3628
+ let boundaryMatch = skipLoaderErrorBubbling ? actionMatch : findNearestBoundary(matches, actionMatch.route.id);
3629
+ let statusCode = isRouteErrorResponse(result.error) ? result.error.status : result.statusCode != null ? result.statusCode : 500;
3630
+ let actionHeaders = _extends({}, result.headers ? {
3631
+ [actionMatch.route.id]: result.headers
3632
+ } : {});
3633
+ if (skipLoaders) {
3634
+ return {
3635
+ matches,
3636
+ loaderData: {},
3637
+ actionData: {},
3638
+ errors: {
3639
+ [boundaryMatch.route.id]: result.error
3640
+ },
3641
+ statusCode,
3642
+ loaderHeaders: {},
3643
+ actionHeaders,
3644
+ activeDeferreds: null
3645
+ };
3646
+ }
3647
+ let context = await loadRouteData(loaderRequest, matches, requestContext, unstable_dataStrategy, loadRouteIds, skipLoaderErrorBubbling, null, [boundaryMatch.route.id, result]);
3581
3648
 
3582
3649
  // action status codes take precedence over loader status codes
3583
3650
  return _extends({}, context, {
3584
- statusCode: isRouteErrorResponse(result.error) ? result.error.status : 500,
3651
+ statusCode,
3585
3652
  actionData: null,
3586
- actionHeaders: _extends({}, result.headers ? {
3587
- [actionMatch.route.id]: result.headers
3588
- } : {})
3653
+ actionHeaders
3589
3654
  });
3590
3655
  }
3591
-
3592
- // Create a GET request for the loaders
3593
- let loaderRequest = new Request(request.url, {
3594
- headers: request.headers,
3595
- redirect: request.redirect,
3596
- signal: request.signal
3597
- });
3598
- let context = await loadRouteData(loaderRequest, matches, requestContext);
3599
- return _extends({}, context, result.statusCode ? {
3600
- statusCode: result.statusCode
3601
- } : {}, {
3656
+ let actionHeaders = result.headers ? {
3657
+ [actionMatch.route.id]: result.headers
3658
+ } : {};
3659
+ if (skipLoaders) {
3660
+ return {
3661
+ matches,
3662
+ loaderData: {},
3663
+ actionData: {
3664
+ [actionMatch.route.id]: result.data
3665
+ },
3666
+ errors: null,
3667
+ statusCode: result.statusCode || 200,
3668
+ loaderHeaders: {},
3669
+ actionHeaders,
3670
+ activeDeferreds: null
3671
+ };
3672
+ }
3673
+ let context = await loadRouteData(loaderRequest, matches, requestContext, unstable_dataStrategy, loadRouteIds, skipLoaderErrorBubbling, null);
3674
+ return _extends({}, context, {
3602
3675
  actionData: {
3603
3676
  [actionMatch.route.id]: result.data
3604
- },
3605
- actionHeaders: _extends({}, result.headers ? {
3677
+ }
3678
+ }, result.statusCode ? {
3679
+ statusCode: result.statusCode
3680
+ } : {}, {
3681
+ actionHeaders: result.headers ? {
3606
3682
  [actionMatch.route.id]: result.headers
3607
- } : {})
3683
+ } : {}
3608
3684
  });
3609
3685
  }
3610
- async function loadRouteData(request, matches, requestContext, routeMatch, pendingActionError) {
3686
+ async function loadRouteData(request, matches, requestContext, unstable_dataStrategy, loadRouteIds, skipLoaderErrorBubbling, routeMatch, pendingActionResult) {
3611
3687
  let isRouteRequest = routeMatch != null;
3612
3688
 
3613
3689
  // Short circuit if we have no loaders to run (queryRoute())
@@ -3618,8 +3694,11 @@ function createStaticHandler(routes, opts) {
3618
3694
  routeId: routeMatch == null ? void 0 : routeMatch.route.id
3619
3695
  });
3620
3696
  }
3621
- let requestMatches = routeMatch ? [routeMatch] : getLoaderMatchesUntilBoundary(matches, Object.keys(pendingActionError || {})[0]);
3697
+ let requestMatches = routeMatch ? [routeMatch] : pendingActionResult && isErrorResult(pendingActionResult[1]) ? getLoaderMatchesUntilBoundary(matches, pendingActionResult[0]) : matches;
3622
3698
  let matchesToLoad = requestMatches.filter(m => m.route.loader || m.route.lazy);
3699
+ if (loadRouteIds) {
3700
+ matchesToLoad = matchesToLoad.filter(m => loadRouteIds.includes(m.route.id));
3701
+ }
3623
3702
 
3624
3703
  // Short circuit if we have no loaders to run (query())
3625
3704
  if (matchesToLoad.length === 0) {
@@ -3629,24 +3708,22 @@ function createStaticHandler(routes, opts) {
3629
3708
  loaderData: matches.reduce((acc, m) => Object.assign(acc, {
3630
3709
  [m.route.id]: null
3631
3710
  }), {}),
3632
- errors: pendingActionError || null,
3711
+ errors: pendingActionResult && isErrorResult(pendingActionResult[1]) ? {
3712
+ [pendingActionResult[0]]: pendingActionResult[1].error
3713
+ } : null,
3633
3714
  statusCode: 200,
3634
3715
  loaderHeaders: {},
3635
3716
  activeDeferreds: null
3636
3717
  };
3637
3718
  }
3638
- let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath, {
3639
- isStaticRequest: true,
3640
- isRouteRequest,
3641
- requestContext
3642
- }))]);
3719
+ let results = await callDataStrategy("loader", request, matchesToLoad, matches, isRouteRequest, requestContext, unstable_dataStrategy);
3643
3720
  if (request.signal.aborted) {
3644
3721
  throwStaticHandlerAbortedError(request, isRouteRequest, future);
3645
3722
  }
3646
3723
 
3647
3724
  // Process and commit output from loaders
3648
3725
  let activeDeferreds = new Map();
3649
- let context = processRouteLoaderData(matches, matchesToLoad, results, pendingActionError, activeDeferreds);
3726
+ let context = processRouteLoaderData(matches, matchesToLoad, results, pendingActionResult, activeDeferreds, skipLoaderErrorBubbling);
3650
3727
 
3651
3728
  // Add a null for any non-loader matches for proper revalidation on the client
3652
3729
  let executedLoaders = new Set(matchesToLoad.map(match => match.route.id));
@@ -3660,6 +3737,25 @@ function createStaticHandler(routes, opts) {
3660
3737
  activeDeferreds: activeDeferreds.size > 0 ? Object.fromEntries(activeDeferreds.entries()) : null
3661
3738
  });
3662
3739
  }
3740
+
3741
+ // Utility wrapper for calling dataStrategy server-side without having to
3742
+ // pass around the manifest, mapRouteProperties, etc.
3743
+ async function callDataStrategy(type, request, matchesToLoad, matches, isRouteRequest, requestContext, unstable_dataStrategy) {
3744
+ let results = await callDataStrategyImpl(unstable_dataStrategy || defaultDataStrategy, type, request, matchesToLoad, matches, manifest, mapRouteProperties, requestContext);
3745
+ return await Promise.all(results.map((result, i) => {
3746
+ if (isRedirectHandlerResult(result)) {
3747
+ let response = result.result;
3748
+ // Throw redirects and let the server handle them with an HTTP redirect
3749
+ throw normalizeRelativeRoutingRedirectResponse(response, request, matchesToLoad[i].route.id, matches, basename, future.v7_relativeSplatPath);
3750
+ }
3751
+ if (isResponse(result.result) && isRouteRequest) {
3752
+ // For SSR single-route requests, we want to hand Responses back
3753
+ // directly without unwrapping
3754
+ throw result;
3755
+ }
3756
+ return convertHandlerResultToDataResult(result);
3757
+ }));
3758
+ }
3663
3759
  return {
3664
3760
  dataRoutes,
3665
3761
  query,
@@ -3880,14 +3976,20 @@ function getLoaderMatchesUntilBoundary(matches, boundaryId) {
3880
3976
  }
3881
3977
  return boundaryMatches;
3882
3978
  }
3883
- function getMatchesToLoad(history, state, matches, submission, location, isInitialLoad, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionData, pendingError) {
3884
- let actionResult = pendingError ? Object.values(pendingError)[0] : pendingActionData ? Object.values(pendingActionData)[0] : undefined;
3979
+ function getMatchesToLoad(history, state, matches, submission, location, isInitialLoad, skipActionErrorRevalidation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionResult) {
3980
+ let actionResult = pendingActionResult ? isErrorResult(pendingActionResult[1]) ? pendingActionResult[1].error : pendingActionResult[1].data : undefined;
3885
3981
  let currentUrl = history.createURL(state.location);
3886
3982
  let nextUrl = history.createURL(location);
3887
3983
 
3888
3984
  // Pick navigation matches that are net-new or qualify for revalidation
3889
- let boundaryId = pendingError ? Object.keys(pendingError)[0] : undefined;
3890
- let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId);
3985
+ let boundaryId = pendingActionResult && isErrorResult(pendingActionResult[1]) ? pendingActionResult[0] : undefined;
3986
+ let boundaryMatches = boundaryId ? getLoaderMatchesUntilBoundary(matches, boundaryId) : matches;
3987
+
3988
+ // Don't revalidate loaders by default after action 4xx/5xx responses
3989
+ // when the flag is enabled. They can still opt-into revalidation via
3990
+ // `shouldRevalidate` via `actionResult`
3991
+ let actionStatus = pendingActionResult ? pendingActionResult[1].statusCode : undefined;
3992
+ let shouldSkipRevalidation = skipActionErrorRevalidation && actionStatus && actionStatus >= 400;
3891
3993
  let navigationMatches = boundaryMatches.filter((match, index) => {
3892
3994
  let {
3893
3995
  route
@@ -3900,7 +4002,7 @@ function getMatchesToLoad(history, state, matches, submission, location, isIniti
3900
4002
  return false;
3901
4003
  }
3902
4004
  if (isInitialLoad) {
3903
- if (route.loader.hydrate) {
4005
+ if (typeof route.loader !== "function" || route.loader.hydrate) {
3904
4006
  return true;
3905
4007
  }
3906
4008
  return state.loaderData[route.id] === undefined && (
@@ -3926,11 +4028,10 @@ function getMatchesToLoad(history, state, matches, submission, location, isIniti
3926
4028
  nextParams: nextRouteMatch.params
3927
4029
  }, submission, {
3928
4030
  actionResult,
3929
- defaultShouldRevalidate:
4031
+ unstable_actionStatus: actionStatus,
4032
+ defaultShouldRevalidate: shouldSkipRevalidation ? false :
3930
4033
  // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate
3931
- isRevalidationRequired ||
3932
- // Clicked the same link, resubmitted a GET form
3933
- currentUrl.pathname + currentUrl.search === nextUrl.pathname + nextUrl.search ||
4034
+ isRevalidationRequired || currentUrl.pathname + currentUrl.search === nextUrl.pathname + nextUrl.search ||
3934
4035
  // Search params affect all loaders
3935
4036
  currentUrl.search !== nextUrl.search || isNewRouteInstance(currentRouteMatch, nextRouteMatch)
3936
4037
  }));
@@ -3992,7 +4093,8 @@ function getMatchesToLoad(history, state, matches, submission, location, isIniti
3992
4093
  nextParams: matches[matches.length - 1].params
3993
4094
  }, submission, {
3994
4095
  actionResult,
3995
- defaultShouldRevalidate: isRevalidationRequired
4096
+ unstable_actionStatus: actionStatus,
4097
+ defaultShouldRevalidate: shouldSkipRevalidation ? false : isRevalidationRequired
3996
4098
  }));
3997
4099
  }
3998
4100
  if (shouldRevalidate) {
@@ -4094,24 +4196,92 @@ async function loadLazyRouteModule(route, mapRouteProperties, manifest) {
4094
4196
  lazy: undefined
4095
4197
  }));
4096
4198
  }
4097
- async function callLoaderOrAction(type, request, match, matches, manifest, mapRouteProperties, basename, v7_relativeSplatPath, opts) {
4098
- if (opts === void 0) {
4099
- opts = {};
4100
- }
4101
- let resultType;
4199
+
4200
+ // Default implementation of `dataStrategy` which fetches all loaders in parallel
4201
+ function defaultDataStrategy(opts) {
4202
+ return Promise.all(opts.matches.map(m => m.resolve()));
4203
+ }
4204
+ async function callDataStrategyImpl(dataStrategyImpl, type, request, matchesToLoad, matches, manifest, mapRouteProperties, requestContext) {
4205
+ let routeIdsToLoad = matchesToLoad.reduce((acc, m) => acc.add(m.route.id), new Set());
4206
+ let loadedMatches = new Set();
4207
+
4208
+ // Send all matches here to allow for a middleware-type implementation.
4209
+ // handler will be a no-op for unneeded routes and we filter those results
4210
+ // back out below.
4211
+ let results = await dataStrategyImpl({
4212
+ matches: matches.map(match => {
4213
+ let shouldLoad = routeIdsToLoad.has(match.route.id);
4214
+ // `resolve` encapsulates the route.lazy, executing the
4215
+ // loader/action, and mapping return values/thrown errors to a
4216
+ // HandlerResult. Users can pass a callback to take fine-grained control
4217
+ // over the execution of the loader/action
4218
+ let resolve = handlerOverride => {
4219
+ loadedMatches.add(match.route.id);
4220
+ return shouldLoad ? callLoaderOrAction(type, request, match, manifest, mapRouteProperties, handlerOverride, requestContext) : Promise.resolve({
4221
+ type: ResultType.data,
4222
+ result: undefined
4223
+ });
4224
+ };
4225
+ return _extends({}, match, {
4226
+ shouldLoad,
4227
+ resolve
4228
+ });
4229
+ }),
4230
+ request,
4231
+ params: matches[0].params,
4232
+ context: requestContext
4233
+ });
4234
+
4235
+ // Throw if any loadRoute implementations not called since they are what
4236
+ // ensures a route is fully loaded
4237
+ matches.forEach(m => invariant(loadedMatches.has(m.route.id), "`match.resolve()` was not called for route id \"" + m.route.id + "\". " + "You must call `match.resolve()` on every match passed to " + "`dataStrategy` to ensure all routes are properly loaded."));
4238
+
4239
+ // Filter out any middleware-only matches for which we didn't need to run handlers
4240
+ return results.filter((_, i) => routeIdsToLoad.has(matches[i].route.id));
4241
+ }
4242
+
4243
+ // Default logic for calling a loader/action is the user has no specified a dataStrategy
4244
+ async function callLoaderOrAction(type, request, match, manifest, mapRouteProperties, handlerOverride, staticContext) {
4102
4245
  let result;
4103
4246
  let onReject;
4104
4247
  let runHandler = handler => {
4105
4248
  // Setup a promise we can race against so that abort signals short circuit
4106
4249
  let reject;
4250
+ // This will never resolve so safe to type it as Promise<HandlerResult> to
4251
+ // satisfy the function return value
4107
4252
  let abortPromise = new Promise((_, r) => reject = r);
4108
4253
  onReject = () => reject();
4109
4254
  request.signal.addEventListener("abort", onReject);
4110
- return Promise.race([handler({
4111
- request,
4112
- params: match.params,
4113
- context: opts.requestContext
4114
- }), abortPromise]);
4255
+ let actualHandler = ctx => {
4256
+ if (typeof handler !== "function") {
4257
+ return Promise.reject(new Error("You cannot call the handler for a route which defines a boolean " + ("\"" + type + "\" [routeId: " + match.route.id + "]")));
4258
+ }
4259
+ return handler({
4260
+ request,
4261
+ params: match.params,
4262
+ context: staticContext
4263
+ }, ...(ctx !== undefined ? [ctx] : []));
4264
+ };
4265
+ let handlerPromise;
4266
+ if (handlerOverride) {
4267
+ handlerPromise = handlerOverride(ctx => actualHandler(ctx));
4268
+ } else {
4269
+ handlerPromise = (async () => {
4270
+ try {
4271
+ let val = await actualHandler();
4272
+ return {
4273
+ type: "data",
4274
+ result: val
4275
+ };
4276
+ } catch (e) {
4277
+ return {
4278
+ type: "error",
4279
+ result: e
4280
+ };
4281
+ }
4282
+ })();
4283
+ }
4284
+ return Promise.race([handlerPromise, abortPromise]);
4115
4285
  };
4116
4286
  try {
4117
4287
  let handler = match.route[type];
@@ -4119,23 +4289,23 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
4119
4289
  if (handler) {
4120
4290
  // Run statically defined handler in parallel with lazy()
4121
4291
  let handlerError;
4122
- let values = await Promise.all([
4292
+ let [value] = await Promise.all([
4123
4293
  // If the handler throws, don't let it immediately bubble out,
4124
4294
  // since we need to let the lazy() execution finish so we know if this
4125
4295
  // route has a boundary that can handle the error
4126
4296
  runHandler(handler).catch(e => {
4127
4297
  handlerError = e;
4128
4298
  }), loadLazyRouteModule(match.route, mapRouteProperties, manifest)]);
4129
- if (handlerError) {
4299
+ if (handlerError !== undefined) {
4130
4300
  throw handlerError;
4131
4301
  }
4132
- result = values[0];
4302
+ result = value;
4133
4303
  } else {
4134
4304
  // Load lazy route module, then run any returned handler
4135
4305
  await loadLazyRouteModule(match.route, mapRouteProperties, manifest);
4136
4306
  handler = match.route[type];
4137
4307
  if (handler) {
4138
- // Handler still run even if we got interrupted to maintain consistency
4308
+ // Handler still runs even if we got interrupted to maintain consistency
4139
4309
  // with un-abortable behavior of handler execution on non-lazy or
4140
4310
  // previously-lazy-loaded routes
4141
4311
  result = await runHandler(handler);
@@ -4152,7 +4322,7 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
4152
4322
  // hit the invariant below that errors on returning undefined.
4153
4323
  return {
4154
4324
  type: ResultType.data,
4155
- data: undefined
4325
+ result: undefined
4156
4326
  };
4157
4327
  }
4158
4328
  }
@@ -4165,65 +4335,29 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
4165
4335
  } else {
4166
4336
  result = await runHandler(handler);
4167
4337
  }
4168
- 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`.");
4338
+ invariant(result.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`.");
4169
4339
  } catch (e) {
4170
- resultType = ResultType.error;
4171
- result = e;
4340
+ // We should already be catching and converting normal handler executions to
4341
+ // HandlerResults and returning them, so anything that throws here is an
4342
+ // unexpected error we still need to wrap
4343
+ return {
4344
+ type: ResultType.error,
4345
+ result: e
4346
+ };
4172
4347
  } finally {
4173
4348
  if (onReject) {
4174
4349
  request.signal.removeEventListener("abort", onReject);
4175
4350
  }
4176
4351
  }
4352
+ return result;
4353
+ }
4354
+ async function convertHandlerResultToDataResult(handlerResult) {
4355
+ let {
4356
+ result,
4357
+ type,
4358
+ status
4359
+ } = handlerResult;
4177
4360
  if (isResponse(result)) {
4178
- let status = result.status;
4179
-
4180
- // Process redirects
4181
- if (redirectStatusCodes.has(status)) {
4182
- let location = result.headers.get("Location");
4183
- invariant(location, "Redirects returned/thrown from loaders/actions must have a Location header");
4184
-
4185
- // Support relative routing in internal redirects
4186
- if (!ABSOLUTE_URL_REGEX.test(location)) {
4187
- location = normalizeTo(new URL(request.url), matches.slice(0, matches.indexOf(match) + 1), basename, true, location, v7_relativeSplatPath);
4188
- } else if (!opts.isStaticRequest) {
4189
- // Strip off the protocol+origin for same-origin + same-basename absolute
4190
- // redirects. If this is a static request, we can let it go back to the
4191
- // browser as-is
4192
- let currentUrl = new URL(request.url);
4193
- let url = location.startsWith("//") ? new URL(currentUrl.protocol + location) : new URL(location);
4194
- let isSameBasename = stripBasename(url.pathname, basename) != null;
4195
- if (url.origin === currentUrl.origin && isSameBasename) {
4196
- location = url.pathname + url.search + url.hash;
4197
- }
4198
- }
4199
-
4200
- // Don't process redirects in the router during static requests requests.
4201
- // Instead, throw the Response and let the server handle it with an HTTP
4202
- // redirect. We also update the Location header in place in this flow so
4203
- // basename and relative routing is taken into account
4204
- if (opts.isStaticRequest) {
4205
- result.headers.set("Location", location);
4206
- throw result;
4207
- }
4208
- return {
4209
- type: ResultType.redirect,
4210
- status,
4211
- location,
4212
- revalidate: result.headers.get("X-Remix-Revalidate") !== null,
4213
- reloadDocument: result.headers.get("X-Remix-Reload-Document") !== null
4214
- };
4215
- }
4216
-
4217
- // For SSR single-route requests, we want to hand Responses back directly
4218
- // without unwrapping. We do this with the QueryRouteResponse wrapper
4219
- // interface so we can know whether it was returned or thrown
4220
- if (opts.isRouteRequest) {
4221
- let queryRouteResponse = {
4222
- type: resultType === ResultType.error ? ResultType.error : ResultType.data,
4223
- response: result
4224
- };
4225
- throw queryRouteResponse;
4226
- }
4227
4361
  let data;
4228
4362
  try {
4229
4363
  let contentType = result.headers.get("Content-Type");
@@ -4244,10 +4378,11 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
4244
4378
  error: e
4245
4379
  };
4246
4380
  }
4247
- if (resultType === ResultType.error) {
4381
+ if (type === ResultType.error) {
4248
4382
  return {
4249
- type: resultType,
4250
- error: new ErrorResponseImpl(status, result.statusText, data),
4383
+ type: ResultType.error,
4384
+ error: new ErrorResponseImpl(result.status, result.statusText, data),
4385
+ statusCode: result.status,
4251
4386
  headers: result.headers
4252
4387
  };
4253
4388
  }
@@ -4258,10 +4393,11 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
4258
4393
  headers: result.headers
4259
4394
  };
4260
4395
  }
4261
- if (resultType === ResultType.error) {
4396
+ if (type === ResultType.error) {
4262
4397
  return {
4263
- type: resultType,
4264
- error: result
4398
+ type: ResultType.error,
4399
+ error: result,
4400
+ statusCode: isRouteErrorResponse(result) ? result.status : status
4265
4401
  };
4266
4402
  }
4267
4403
  if (isDeferredData(result)) {
@@ -4275,10 +4411,35 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
4275
4411
  }
4276
4412
  return {
4277
4413
  type: ResultType.data,
4278
- data: result
4414
+ data: result,
4415
+ statusCode: status
4279
4416
  };
4280
4417
  }
4281
4418
 
4419
+ // Support relative routing in internal redirects
4420
+ function normalizeRelativeRoutingRedirectResponse(response, request, routeId, matches, basename, v7_relativeSplatPath) {
4421
+ let location = response.headers.get("Location");
4422
+ invariant(location, "Redirects returned/thrown from loaders/actions must have a Location header");
4423
+ if (!ABSOLUTE_URL_REGEX.test(location)) {
4424
+ let trimmedMatches = matches.slice(0, matches.findIndex(m => m.route.id === routeId) + 1);
4425
+ location = normalizeTo(new URL(request.url), trimmedMatches, basename, true, location, v7_relativeSplatPath);
4426
+ response.headers.set("Location", location);
4427
+ }
4428
+ return response;
4429
+ }
4430
+ function normalizeRedirectLocation(location, currentUrl, basename) {
4431
+ if (ABSOLUTE_URL_REGEX.test(location)) {
4432
+ // Strip off the protocol+origin for same-origin + same-basename absolute redirects
4433
+ let normalizedLocation = location;
4434
+ let url = normalizedLocation.startsWith("//") ? new URL(currentUrl.protocol + normalizedLocation) : new URL(normalizedLocation);
4435
+ let isSameBasename = stripBasename(url.pathname, basename) != null;
4436
+ if (url.origin === currentUrl.origin && isSameBasename) {
4437
+ return url.pathname + url.search + url.hash;
4438
+ }
4439
+ }
4440
+ return location;
4441
+ }
4442
+
4282
4443
  // Utility method for creating the Request instances for loaders/actions during
4283
4444
  // client-side navigations and fetches. During SSR we will always have a
4284
4445
  // Request instance from the static handler (query/queryRoute)
@@ -4329,35 +4490,39 @@ function convertSearchParamsToFormData(searchParams) {
4329
4490
  }
4330
4491
  return formData;
4331
4492
  }
4332
- function processRouteLoaderData(matches, matchesToLoad, results, pendingError, activeDeferreds) {
4493
+ function processRouteLoaderData(matches, matchesToLoad, results, pendingActionResult, activeDeferreds, skipLoaderErrorBubbling) {
4333
4494
  // Fill in loaderData/errors from our loaders
4334
4495
  let loaderData = {};
4335
4496
  let errors = null;
4336
4497
  let statusCode;
4337
4498
  let foundError = false;
4338
4499
  let loaderHeaders = {};
4500
+ let pendingError = pendingActionResult && isErrorResult(pendingActionResult[1]) ? pendingActionResult[1].error : undefined;
4339
4501
 
4340
4502
  // Process loader results into state.loaderData/state.errors
4341
4503
  results.forEach((result, index) => {
4342
4504
  let id = matchesToLoad[index].route.id;
4343
4505
  invariant(!isRedirectResult(result), "Cannot handle redirect results in processLoaderData");
4344
4506
  if (isErrorResult(result)) {
4345
- // Look upwards from the matched route for the closest ancestor
4346
- // error boundary, defaulting to the root match
4347
- let boundaryMatch = findNearestBoundary(matches, id);
4348
4507
  let error = result.error;
4349
4508
  // If we have a pending action error, we report it at the highest-route
4350
4509
  // that throws a loader error, and then clear it out to indicate that
4351
4510
  // it was consumed
4352
- if (pendingError) {
4353
- error = Object.values(pendingError)[0];
4511
+ if (pendingError !== undefined) {
4512
+ error = pendingError;
4354
4513
  pendingError = undefined;
4355
4514
  }
4356
4515
  errors = errors || {};
4357
-
4358
- // Prefer higher error values if lower errors bubble to the same boundary
4359
- if (errors[boundaryMatch.route.id] == null) {
4360
- errors[boundaryMatch.route.id] = error;
4516
+ if (skipLoaderErrorBubbling) {
4517
+ errors[id] = error;
4518
+ } else {
4519
+ // Look upwards from the matched route for the closest ancestor error
4520
+ // boundary, defaulting to the root match. Prefer higher error values
4521
+ // if lower errors bubble to the same boundary
4522
+ let boundaryMatch = findNearestBoundary(matches, id);
4523
+ if (errors[boundaryMatch.route.id] == null) {
4524
+ errors[boundaryMatch.route.id] = error;
4525
+ }
4361
4526
  }
4362
4527
 
4363
4528
  // Clear our any prior loaderData for the throwing route
@@ -4376,17 +4541,24 @@ function processRouteLoaderData(matches, matchesToLoad, results, pendingError, a
4376
4541
  if (isDeferredResult(result)) {
4377
4542
  activeDeferreds.set(id, result.deferredData);
4378
4543
  loaderData[id] = result.deferredData.data;
4544
+ // Error status codes always override success status codes, but if all
4545
+ // loaders are successful we take the deepest status code.
4546
+ if (result.statusCode != null && result.statusCode !== 200 && !foundError) {
4547
+ statusCode = result.statusCode;
4548
+ }
4549
+ if (result.headers) {
4550
+ loaderHeaders[id] = result.headers;
4551
+ }
4379
4552
  } else {
4380
4553
  loaderData[id] = result.data;
4381
- }
4382
-
4383
- // Error status codes always override success status codes, but if all
4384
- // loaders are successful we take the deepest status code.
4385
- if (result.statusCode != null && result.statusCode !== 200 && !foundError) {
4386
- statusCode = result.statusCode;
4387
- }
4388
- if (result.headers) {
4389
- loaderHeaders[id] = result.headers;
4554
+ // Error status codes always override success status codes, but if all
4555
+ // loaders are successful we take the deepest status code.
4556
+ if (result.statusCode && result.statusCode !== 200 && !foundError) {
4557
+ statusCode = result.statusCode;
4558
+ }
4559
+ if (result.headers) {
4560
+ loaderHeaders[id] = result.headers;
4561
+ }
4390
4562
  }
4391
4563
  }
4392
4564
  });
@@ -4394,9 +4566,11 @@ function processRouteLoaderData(matches, matchesToLoad, results, pendingError, a
4394
4566
  // If we didn't consume the pending action error (i.e., all loaders
4395
4567
  // resolved), then consume it here. Also clear out any loaderData for the
4396
4568
  // throwing route
4397
- if (pendingError) {
4398
- errors = pendingError;
4399
- loaderData[Object.keys(pendingError)[0]] = undefined;
4569
+ if (pendingError !== undefined && pendingActionResult) {
4570
+ errors = {
4571
+ [pendingActionResult[0]]: pendingError
4572
+ };
4573
+ loaderData[pendingActionResult[0]] = undefined;
4400
4574
  }
4401
4575
  return {
4402
4576
  loaderData,
@@ -4405,11 +4579,12 @@ function processRouteLoaderData(matches, matchesToLoad, results, pendingError, a
4405
4579
  loaderHeaders
4406
4580
  };
4407
4581
  }
4408
- function processLoaderData(state, matches, matchesToLoad, results, pendingError, revalidatingFetchers, fetcherResults, activeDeferreds) {
4582
+ function processLoaderData(state, matches, matchesToLoad, results, pendingActionResult, revalidatingFetchers, fetcherResults, activeDeferreds) {
4409
4583
  let {
4410
4584
  loaderData,
4411
4585
  errors
4412
- } = processRouteLoaderData(matches, matchesToLoad, results, pendingError, activeDeferreds);
4586
+ } = processRouteLoaderData(matches, matchesToLoad, results, pendingActionResult, activeDeferreds, false // This method is only called client side so we always want to bubble
4587
+ );
4413
4588
 
4414
4589
  // Process results from our revalidating fetchers
4415
4590
  for (let index = 0; index < revalidatingFetchers.length; index++) {
@@ -4471,6 +4646,19 @@ function mergeLoaderData(loaderData, newLoaderData, matches, errors) {
4471
4646
  }
4472
4647
  return mergedLoaderData;
4473
4648
  }
4649
+ function getActionDataForCommit(pendingActionResult) {
4650
+ if (!pendingActionResult) {
4651
+ return {};
4652
+ }
4653
+ return isErrorResult(pendingActionResult[1]) ? {
4654
+ // Clear out prior actionData on errors
4655
+ actionData: {}
4656
+ } : {
4657
+ actionData: {
4658
+ [pendingActionResult[0]]: pendingActionResult[1].data
4659
+ }
4660
+ };
4661
+ }
4474
4662
 
4475
4663
  // Find the nearest error boundary, looking upwards from the leaf route (or the
4476
4664
  // route specified by routeId) for the closest ancestor error boundary,
@@ -4566,6 +4754,12 @@ function isHashChangeOnly(a, b) {
4566
4754
  // /page#hash -> /page
4567
4755
  return false;
4568
4756
  }
4757
+ function isHandlerResult(result) {
4758
+ return result != null && typeof result === "object" && "type" in result && "result" in result && (result.type === ResultType.data || result.type === ResultType.error);
4759
+ }
4760
+ function isRedirectHandlerResult(result) {
4761
+ return isResponse(result.result) && redirectStatusCodes.has(result.result.status);
4762
+ }
4569
4763
  function isDeferredResult(result) {
4570
4764
  return result.type === ResultType.deferred;
4571
4765
  }
@@ -4590,9 +4784,6 @@ function isRedirectResponse(result) {
4590
4784
  let location = result.headers.get("Location");
4591
4785
  return status >= 300 && status <= 399 && location != null;
4592
4786
  }
4593
- function isQueryRouteResponse(obj) {
4594
- return obj && isResponse(obj.response) && (obj.type === ResultType.data || obj.type === ResultType.error);
4595
- }
4596
4787
  function isValidMethod(method) {
4597
4788
  return validRequestMethods.has(method.toLowerCase());
4598
4789
  }