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