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

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.1
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,19 @@ 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.requestContext` is an optional server context that will be passed
3353
+ * to actions/loaders in the `context` parameter
3354
+ * - `opts.skipLoaderErrorBubbling` is an optional parameter that will prevent
3355
+ * the bubbling of errors which allows single-fetch-type implementations
3356
+ * where the client will handle the bubbling and we may need to return data
3357
+ * for the handling route
3325
3358
  */
3326
3359
  async function query(request, _temp3) {
3327
3360
  let {
3328
- requestContext
3361
+ requestContext,
3362
+ skipLoaderErrorBubbling,
3363
+ unstable_dataStrategy
3329
3364
  } = _temp3 === void 0 ? {} : _temp3;
3330
3365
  let url = new URL(request.url);
3331
3366
  let method = request.method;
@@ -3378,7 +3413,7 @@ function createStaticHandler(routes, opts) {
3378
3413
  activeDeferreds: null
3379
3414
  };
3380
3415
  }
3381
- let result = await queryImpl(request, location, matches, requestContext);
3416
+ let result = await queryImpl(request, location, matches, requestContext, unstable_dataStrategy || null, skipLoaderErrorBubbling === true, null);
3382
3417
  if (isResponse(result)) {
3383
3418
  return result;
3384
3419
  }
@@ -3411,6 +3446,12 @@ function createStaticHandler(routes, opts) {
3411
3446
  * serialize the error as they see fit while including the proper response
3412
3447
  * code. Examples here are 404 and 405 errors that occur prior to reaching
3413
3448
  * any user-defined loaders.
3449
+ *
3450
+ * - `opts.routeId` allows you to specify the specific route handler to call.
3451
+ * If not provided the handler will determine the proper route by matching
3452
+ * against `request.url`
3453
+ * - `opts.requestContext` is an optional server context that will be passed
3454
+ * to actions/loaders in the `context` parameter
3414
3455
  */
3415
3456
  async function queryRoute(request, _temp4) {
3416
3457
  let {
@@ -3444,7 +3485,7 @@ function createStaticHandler(routes, opts) {
3444
3485
  pathname: location.pathname
3445
3486
  });
3446
3487
  }
3447
- let result = await queryImpl(request, location, matches, requestContext, match);
3488
+ let result = await queryImpl(request, location, matches, requestContext, null, false, match);
3448
3489
  if (isResponse(result)) {
3449
3490
  return result;
3450
3491
  }
@@ -3471,27 +3512,27 @@ function createStaticHandler(routes, opts) {
3471
3512
  }
3472
3513
  return undefined;
3473
3514
  }
3474
- async function queryImpl(request, location, matches, requestContext, routeMatch) {
3515
+ async function queryImpl(request, location, matches, requestContext, unstable_dataStrategy, skipLoaderErrorBubbling, routeMatch) {
3475
3516
  invariant(request.signal, "query()/queryRoute() requests must contain an AbortController signal");
3476
3517
  try {
3477
3518
  if (isMutationMethod(request.method.toLowerCase())) {
3478
- let result = await submit(request, matches, routeMatch || getTargetMatch(matches, location), requestContext, routeMatch != null);
3519
+ let result = await submit(request, matches, routeMatch || getTargetMatch(matches, location), requestContext, unstable_dataStrategy, skipLoaderErrorBubbling, routeMatch != null);
3479
3520
  return result;
3480
3521
  }
3481
- let result = await loadRouteData(request, matches, requestContext, routeMatch);
3522
+ let result = await loadRouteData(request, matches, requestContext, unstable_dataStrategy, skipLoaderErrorBubbling, routeMatch);
3482
3523
  return isResponse(result) ? result : _extends({}, result, {
3483
3524
  actionData: null,
3484
3525
  actionHeaders: {}
3485
3526
  });
3486
3527
  } 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)) {
3528
+ // If the user threw/returned a Response in callLoaderOrAction for a
3529
+ // `queryRoute` call, we throw the `HandlerResult` to bail out early
3530
+ // and then return or throw the raw Response here accordingly
3531
+ if (isHandlerResult(e) && isResponse(e.result)) {
3491
3532
  if (e.type === ResultType.error) {
3492
- throw e.response;
3533
+ throw e.result;
3493
3534
  }
3494
- return e.response;
3535
+ return e.result;
3495
3536
  }
3496
3537
  // Redirects are always returned since they don't propagate to catch
3497
3538
  // boundaries
@@ -3501,7 +3542,7 @@ function createStaticHandler(routes, opts) {
3501
3542
  throw e;
3502
3543
  }
3503
3544
  }
3504
- async function submit(request, matches, actionMatch, requestContext, isRouteRequest) {
3545
+ async function submit(request, matches, actionMatch, requestContext, unstable_dataStrategy, skipLoaderErrorBubbling, isRouteRequest) {
3505
3546
  let result;
3506
3547
  if (!actionMatch.route.action && !actionMatch.route.lazy) {
3507
3548
  let error = getInternalRouterError(405, {
@@ -3517,11 +3558,8 @@ function createStaticHandler(routes, opts) {
3517
3558
  error
3518
3559
  };
3519
3560
  } else {
3520
- result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath, {
3521
- isStaticRequest: true,
3522
- isRouteRequest,
3523
- requestContext
3524
- });
3561
+ let results = await callDataStrategy("action", request, [actionMatch], matches, isRouteRequest, requestContext, unstable_dataStrategy);
3562
+ result = results[0];
3525
3563
  if (request.signal.aborted) {
3526
3564
  throwStaticHandlerAbortedError(request, isRouteRequest, future);
3527
3565
  }
@@ -3532,9 +3570,9 @@ function createStaticHandler(routes, opts) {
3532
3570
  // can get back on the "throw all redirect responses" train here should
3533
3571
  // this ever happen :/
3534
3572
  throw new Response(null, {
3535
- status: result.status,
3573
+ status: result.response.status,
3536
3574
  headers: {
3537
- Location: result.location
3575
+ Location: result.response.headers.get("Location")
3538
3576
  }
3539
3577
  });
3540
3578
  }
@@ -3571,43 +3609,42 @@ function createStaticHandler(routes, opts) {
3571
3609
  activeDeferreds: null
3572
3610
  };
3573
3611
  }
3612
+
3613
+ // Create a GET request for the loaders
3614
+ let loaderRequest = new Request(request.url, {
3615
+ headers: request.headers,
3616
+ redirect: request.redirect,
3617
+ signal: request.signal
3618
+ });
3574
3619
  if (isErrorResult(result)) {
3575
3620
  // Store off the pending error - we use it to determine which loaders
3576
3621
  // 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
- });
3622
+ let boundaryMatch = skipLoaderErrorBubbling ? actionMatch : findNearestBoundary(matches, actionMatch.route.id);
3623
+ let context = await loadRouteData(loaderRequest, matches, requestContext, unstable_dataStrategy, skipLoaderErrorBubbling, null, [boundaryMatch.route.id, result]);
3581
3624
 
3582
3625
  // action status codes take precedence over loader status codes
3583
3626
  return _extends({}, context, {
3584
- statusCode: isRouteErrorResponse(result.error) ? result.error.status : 500,
3627
+ statusCode: isRouteErrorResponse(result.error) ? result.error.status : result.statusCode != null ? result.statusCode : 500,
3585
3628
  actionData: null,
3586
3629
  actionHeaders: _extends({}, result.headers ? {
3587
3630
  [actionMatch.route.id]: result.headers
3588
3631
  } : {})
3589
3632
  });
3590
3633
  }
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
- } : {}, {
3634
+ let context = await loadRouteData(loaderRequest, matches, requestContext, unstable_dataStrategy, skipLoaderErrorBubbling, null);
3635
+ return _extends({}, context, {
3602
3636
  actionData: {
3603
3637
  [actionMatch.route.id]: result.data
3604
- },
3605
- actionHeaders: _extends({}, result.headers ? {
3638
+ }
3639
+ }, result.statusCode ? {
3640
+ statusCode: result.statusCode
3641
+ } : {}, {
3642
+ actionHeaders: result.headers ? {
3606
3643
  [actionMatch.route.id]: result.headers
3607
- } : {})
3644
+ } : {}
3608
3645
  });
3609
3646
  }
3610
- async function loadRouteData(request, matches, requestContext, routeMatch, pendingActionError) {
3647
+ async function loadRouteData(request, matches, requestContext, unstable_dataStrategy, skipLoaderErrorBubbling, routeMatch, pendingActionResult) {
3611
3648
  let isRouteRequest = routeMatch != null;
3612
3649
 
3613
3650
  // Short circuit if we have no loaders to run (queryRoute())
@@ -3618,7 +3655,7 @@ function createStaticHandler(routes, opts) {
3618
3655
  routeId: routeMatch == null ? void 0 : routeMatch.route.id
3619
3656
  });
3620
3657
  }
3621
- let requestMatches = routeMatch ? [routeMatch] : getLoaderMatchesUntilBoundary(matches, Object.keys(pendingActionError || {})[0]);
3658
+ let requestMatches = routeMatch ? [routeMatch] : pendingActionResult && isErrorResult(pendingActionResult[1]) ? getLoaderMatchesUntilBoundary(matches, pendingActionResult[0]) : matches;
3622
3659
  let matchesToLoad = requestMatches.filter(m => m.route.loader || m.route.lazy);
3623
3660
 
3624
3661
  // Short circuit if we have no loaders to run (query())
@@ -3629,24 +3666,22 @@ function createStaticHandler(routes, opts) {
3629
3666
  loaderData: matches.reduce((acc, m) => Object.assign(acc, {
3630
3667
  [m.route.id]: null
3631
3668
  }), {}),
3632
- errors: pendingActionError || null,
3669
+ errors: pendingActionResult && isErrorResult(pendingActionResult[1]) ? {
3670
+ [pendingActionResult[0]]: pendingActionResult[1].error
3671
+ } : null,
3633
3672
  statusCode: 200,
3634
3673
  loaderHeaders: {},
3635
3674
  activeDeferreds: null
3636
3675
  };
3637
3676
  }
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
- }))]);
3677
+ let results = await callDataStrategy("loader", request, matchesToLoad, matches, isRouteRequest, requestContext, unstable_dataStrategy);
3643
3678
  if (request.signal.aborted) {
3644
3679
  throwStaticHandlerAbortedError(request, isRouteRequest, future);
3645
3680
  }
3646
3681
 
3647
3682
  // Process and commit output from loaders
3648
3683
  let activeDeferreds = new Map();
3649
- let context = processRouteLoaderData(matches, matchesToLoad, results, pendingActionError, activeDeferreds);
3684
+ let context = processRouteLoaderData(matches, matchesToLoad, results, pendingActionResult, activeDeferreds, skipLoaderErrorBubbling);
3650
3685
 
3651
3686
  // Add a null for any non-loader matches for proper revalidation on the client
3652
3687
  let executedLoaders = new Set(matchesToLoad.map(match => match.route.id));
@@ -3660,6 +3695,25 @@ function createStaticHandler(routes, opts) {
3660
3695
  activeDeferreds: activeDeferreds.size > 0 ? Object.fromEntries(activeDeferreds.entries()) : null
3661
3696
  });
3662
3697
  }
3698
+
3699
+ // Utility wrapper for calling dataStrategy server-side without having to
3700
+ // pass around the manifest, mapRouteProperties, etc.
3701
+ async function callDataStrategy(type, request, matchesToLoad, matches, isRouteRequest, requestContext, unstable_dataStrategy) {
3702
+ let results = await callDataStrategyImpl(unstable_dataStrategy || defaultDataStrategy, type, request, matchesToLoad, matches, manifest, mapRouteProperties, requestContext);
3703
+ return await Promise.all(results.map((result, i) => {
3704
+ if (isRedirectHandlerResult(result)) {
3705
+ let response = result.result;
3706
+ // Throw redirects and let the server handle them with an HTTP redirect
3707
+ throw normalizeRelativeRoutingRedirectResponse(response, request, matchesToLoad[i].route.id, matches, basename, future.v7_relativeSplatPath);
3708
+ }
3709
+ if (isResponse(result.result) && isRouteRequest) {
3710
+ // For SSR single-route requests, we want to hand Responses back
3711
+ // directly without unwrapping
3712
+ throw result;
3713
+ }
3714
+ return convertHandlerResultToDataResult(result);
3715
+ }));
3716
+ }
3663
3717
  return {
3664
3718
  dataRoutes,
3665
3719
  query,
@@ -3880,14 +3934,20 @@ function getLoaderMatchesUntilBoundary(matches, boundaryId) {
3880
3934
  }
3881
3935
  return boundaryMatches;
3882
3936
  }
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;
3937
+ function getMatchesToLoad(history, state, matches, submission, location, isInitialLoad, skipActionErrorRevalidation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionResult) {
3938
+ let actionResult = pendingActionResult ? isErrorResult(pendingActionResult[1]) ? pendingActionResult[1].error : pendingActionResult[1].data : undefined;
3885
3939
  let currentUrl = history.createURL(state.location);
3886
3940
  let nextUrl = history.createURL(location);
3887
3941
 
3888
3942
  // 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);
3943
+ let boundaryId = pendingActionResult && isErrorResult(pendingActionResult[1]) ? pendingActionResult[0] : undefined;
3944
+ let boundaryMatches = boundaryId ? getLoaderMatchesUntilBoundary(matches, boundaryId) : matches;
3945
+
3946
+ // Don't revalidate loaders by default after action 4xx/5xx responses
3947
+ // when the flag is enabled. They can still opt-into revalidation via
3948
+ // `shouldRevalidate` via `actionResult`
3949
+ let actionStatus = pendingActionResult ? pendingActionResult[1].statusCode : undefined;
3950
+ let shouldSkipRevalidation = skipActionErrorRevalidation && actionStatus && actionStatus >= 400;
3891
3951
  let navigationMatches = boundaryMatches.filter((match, index) => {
3892
3952
  let {
3893
3953
  route
@@ -3900,7 +3960,7 @@ function getMatchesToLoad(history, state, matches, submission, location, isIniti
3900
3960
  return false;
3901
3961
  }
3902
3962
  if (isInitialLoad) {
3903
- if (route.loader.hydrate) {
3963
+ if (typeof route.loader !== "function" || route.loader.hydrate) {
3904
3964
  return true;
3905
3965
  }
3906
3966
  return state.loaderData[route.id] === undefined && (
@@ -3926,11 +3986,10 @@ function getMatchesToLoad(history, state, matches, submission, location, isIniti
3926
3986
  nextParams: nextRouteMatch.params
3927
3987
  }, submission, {
3928
3988
  actionResult,
3929
- defaultShouldRevalidate:
3989
+ unstable_actionStatus: actionStatus,
3990
+ defaultShouldRevalidate: shouldSkipRevalidation ? false :
3930
3991
  // 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 ||
3992
+ isRevalidationRequired || currentUrl.pathname + currentUrl.search === nextUrl.pathname + nextUrl.search ||
3934
3993
  // Search params affect all loaders
3935
3994
  currentUrl.search !== nextUrl.search || isNewRouteInstance(currentRouteMatch, nextRouteMatch)
3936
3995
  }));
@@ -3992,7 +4051,8 @@ function getMatchesToLoad(history, state, matches, submission, location, isIniti
3992
4051
  nextParams: matches[matches.length - 1].params
3993
4052
  }, submission, {
3994
4053
  actionResult,
3995
- defaultShouldRevalidate: isRevalidationRequired
4054
+ unstable_actionStatus: actionStatus,
4055
+ defaultShouldRevalidate: shouldSkipRevalidation ? false : isRevalidationRequired
3996
4056
  }));
3997
4057
  }
3998
4058
  if (shouldRevalidate) {
@@ -4094,24 +4154,92 @@ async function loadLazyRouteModule(route, mapRouteProperties, manifest) {
4094
4154
  lazy: undefined
4095
4155
  }));
4096
4156
  }
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;
4157
+
4158
+ // Default implementation of `dataStrategy` which fetches all loaders in parallel
4159
+ function defaultDataStrategy(opts) {
4160
+ return Promise.all(opts.matches.map(m => m.resolve()));
4161
+ }
4162
+ async function callDataStrategyImpl(dataStrategyImpl, type, request, matchesToLoad, matches, manifest, mapRouteProperties, requestContext) {
4163
+ let routeIdsToLoad = matchesToLoad.reduce((acc, m) => acc.add(m.route.id), new Set());
4164
+ let loadedMatches = new Set();
4165
+
4166
+ // Send all matches here to allow for a middleware-type implementation.
4167
+ // handler will be a no-op for unneeded routes and we filter those results
4168
+ // back out below.
4169
+ let results = await dataStrategyImpl({
4170
+ matches: matches.map(match => {
4171
+ let shouldLoad = routeIdsToLoad.has(match.route.id);
4172
+ // `resolve` encapsulates the route.lazy, executing the
4173
+ // loader/action, and mapping return values/thrown errors to a
4174
+ // HandlerResult. Users can pass a callback to take fine-grained control
4175
+ // over the execution of the loader/action
4176
+ let resolve = handlerOverride => {
4177
+ loadedMatches.add(match.route.id);
4178
+ return shouldLoad ? callLoaderOrAction(type, request, match, manifest, mapRouteProperties, handlerOverride, requestContext) : Promise.resolve({
4179
+ type: ResultType.data,
4180
+ result: undefined
4181
+ });
4182
+ };
4183
+ return _extends({}, match, {
4184
+ shouldLoad,
4185
+ resolve
4186
+ });
4187
+ }),
4188
+ request,
4189
+ params: matches[0].params,
4190
+ context: requestContext
4191
+ });
4192
+
4193
+ // Throw if any loadRoute implementations not called since they are what
4194
+ // ensures a route is fully loaded
4195
+ 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."));
4196
+
4197
+ // Filter out any middleware-only matches for which we didn't need to run handlers
4198
+ return results.filter((_, i) => routeIdsToLoad.has(matches[i].route.id));
4199
+ }
4200
+
4201
+ // Default logic for calling a loader/action is the user has no specified a dataStrategy
4202
+ async function callLoaderOrAction(type, request, match, manifest, mapRouteProperties, handlerOverride, staticContext) {
4102
4203
  let result;
4103
4204
  let onReject;
4104
4205
  let runHandler = handler => {
4105
4206
  // Setup a promise we can race against so that abort signals short circuit
4106
4207
  let reject;
4208
+ // This will never resolve so safe to type it as Promise<HandlerResult> to
4209
+ // satisfy the function return value
4107
4210
  let abortPromise = new Promise((_, r) => reject = r);
4108
4211
  onReject = () => reject();
4109
4212
  request.signal.addEventListener("abort", onReject);
4110
- return Promise.race([handler({
4111
- request,
4112
- params: match.params,
4113
- context: opts.requestContext
4114
- }), abortPromise]);
4213
+ let actualHandler = ctx => {
4214
+ if (typeof handler !== "function") {
4215
+ return Promise.reject(new Error("You cannot call the handler for a route which defines a boolean " + ("\"" + type + "\" [routeId: " + match.route.id + "]")));
4216
+ }
4217
+ return handler({
4218
+ request,
4219
+ params: match.params,
4220
+ context: staticContext
4221
+ }, ...(ctx !== undefined ? [ctx] : []));
4222
+ };
4223
+ let handlerPromise;
4224
+ if (handlerOverride) {
4225
+ handlerPromise = handlerOverride(ctx => actualHandler(ctx));
4226
+ } else {
4227
+ handlerPromise = (async () => {
4228
+ try {
4229
+ let val = await actualHandler();
4230
+ return {
4231
+ type: "data",
4232
+ result: val
4233
+ };
4234
+ } catch (e) {
4235
+ return {
4236
+ type: "error",
4237
+ result: e
4238
+ };
4239
+ }
4240
+ })();
4241
+ }
4242
+ return Promise.race([handlerPromise, abortPromise]);
4115
4243
  };
4116
4244
  try {
4117
4245
  let handler = match.route[type];
@@ -4119,23 +4247,23 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
4119
4247
  if (handler) {
4120
4248
  // Run statically defined handler in parallel with lazy()
4121
4249
  let handlerError;
4122
- let values = await Promise.all([
4250
+ let [value] = await Promise.all([
4123
4251
  // If the handler throws, don't let it immediately bubble out,
4124
4252
  // since we need to let the lazy() execution finish so we know if this
4125
4253
  // route has a boundary that can handle the error
4126
4254
  runHandler(handler).catch(e => {
4127
4255
  handlerError = e;
4128
4256
  }), loadLazyRouteModule(match.route, mapRouteProperties, manifest)]);
4129
- if (handlerError) {
4257
+ if (handlerError !== undefined) {
4130
4258
  throw handlerError;
4131
4259
  }
4132
- result = values[0];
4260
+ result = value;
4133
4261
  } else {
4134
4262
  // Load lazy route module, then run any returned handler
4135
4263
  await loadLazyRouteModule(match.route, mapRouteProperties, manifest);
4136
4264
  handler = match.route[type];
4137
4265
  if (handler) {
4138
- // Handler still run even if we got interrupted to maintain consistency
4266
+ // Handler still runs even if we got interrupted to maintain consistency
4139
4267
  // with un-abortable behavior of handler execution on non-lazy or
4140
4268
  // previously-lazy-loaded routes
4141
4269
  result = await runHandler(handler);
@@ -4152,7 +4280,7 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
4152
4280
  // hit the invariant below that errors on returning undefined.
4153
4281
  return {
4154
4282
  type: ResultType.data,
4155
- data: undefined
4283
+ result: undefined
4156
4284
  };
4157
4285
  }
4158
4286
  }
@@ -4165,65 +4293,29 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
4165
4293
  } else {
4166
4294
  result = await runHandler(handler);
4167
4295
  }
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`.");
4296
+ 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
4297
  } catch (e) {
4170
- resultType = ResultType.error;
4171
- result = e;
4298
+ // We should already be catching and converting normal handler executions to
4299
+ // HandlerResults and returning them, so anything that throws here is an
4300
+ // unexpected error we still need to wrap
4301
+ return {
4302
+ type: ResultType.error,
4303
+ result: e
4304
+ };
4172
4305
  } finally {
4173
4306
  if (onReject) {
4174
4307
  request.signal.removeEventListener("abort", onReject);
4175
4308
  }
4176
4309
  }
4310
+ return result;
4311
+ }
4312
+ async function convertHandlerResultToDataResult(handlerResult) {
4313
+ let {
4314
+ result,
4315
+ type,
4316
+ status
4317
+ } = handlerResult;
4177
4318
  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
4319
  let data;
4228
4320
  try {
4229
4321
  let contentType = result.headers.get("Content-Type");
@@ -4244,10 +4336,11 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
4244
4336
  error: e
4245
4337
  };
4246
4338
  }
4247
- if (resultType === ResultType.error) {
4339
+ if (type === ResultType.error) {
4248
4340
  return {
4249
- type: resultType,
4250
- error: new ErrorResponseImpl(status, result.statusText, data),
4341
+ type: ResultType.error,
4342
+ error: new ErrorResponseImpl(result.status, result.statusText, data),
4343
+ statusCode: result.status,
4251
4344
  headers: result.headers
4252
4345
  };
4253
4346
  }
@@ -4258,10 +4351,11 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
4258
4351
  headers: result.headers
4259
4352
  };
4260
4353
  }
4261
- if (resultType === ResultType.error) {
4354
+ if (type === ResultType.error) {
4262
4355
  return {
4263
- type: resultType,
4264
- error: result
4356
+ type: ResultType.error,
4357
+ error: result,
4358
+ statusCode: isRouteErrorResponse(result) ? result.status : status
4265
4359
  };
4266
4360
  }
4267
4361
  if (isDeferredData(result)) {
@@ -4275,10 +4369,35 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
4275
4369
  }
4276
4370
  return {
4277
4371
  type: ResultType.data,
4278
- data: result
4372
+ data: result,
4373
+ statusCode: status
4279
4374
  };
4280
4375
  }
4281
4376
 
4377
+ // Support relative routing in internal redirects
4378
+ function normalizeRelativeRoutingRedirectResponse(response, request, routeId, matches, basename, v7_relativeSplatPath) {
4379
+ let location = response.headers.get("Location");
4380
+ invariant(location, "Redirects returned/thrown from loaders/actions must have a Location header");
4381
+ if (!ABSOLUTE_URL_REGEX.test(location)) {
4382
+ let trimmedMatches = matches.slice(0, matches.findIndex(m => m.route.id === routeId) + 1);
4383
+ location = normalizeTo(new URL(request.url), trimmedMatches, basename, true, location, v7_relativeSplatPath);
4384
+ response.headers.set("Location", location);
4385
+ }
4386
+ return response;
4387
+ }
4388
+ function normalizeRedirectLocation(location, currentUrl, basename) {
4389
+ if (ABSOLUTE_URL_REGEX.test(location)) {
4390
+ // Strip off the protocol+origin for same-origin + same-basename absolute redirects
4391
+ let normalizedLocation = location;
4392
+ let url = normalizedLocation.startsWith("//") ? new URL(currentUrl.protocol + normalizedLocation) : new URL(normalizedLocation);
4393
+ let isSameBasename = stripBasename(url.pathname, basename) != null;
4394
+ if (url.origin === currentUrl.origin && isSameBasename) {
4395
+ return url.pathname + url.search + url.hash;
4396
+ }
4397
+ }
4398
+ return location;
4399
+ }
4400
+
4282
4401
  // Utility method for creating the Request instances for loaders/actions during
4283
4402
  // client-side navigations and fetches. During SSR we will always have a
4284
4403
  // Request instance from the static handler (query/queryRoute)
@@ -4329,35 +4448,39 @@ function convertSearchParamsToFormData(searchParams) {
4329
4448
  }
4330
4449
  return formData;
4331
4450
  }
4332
- function processRouteLoaderData(matches, matchesToLoad, results, pendingError, activeDeferreds) {
4451
+ function processRouteLoaderData(matches, matchesToLoad, results, pendingActionResult, activeDeferreds, skipLoaderErrorBubbling) {
4333
4452
  // Fill in loaderData/errors from our loaders
4334
4453
  let loaderData = {};
4335
4454
  let errors = null;
4336
4455
  let statusCode;
4337
4456
  let foundError = false;
4338
4457
  let loaderHeaders = {};
4458
+ let pendingError = pendingActionResult && isErrorResult(pendingActionResult[1]) ? pendingActionResult[1].error : undefined;
4339
4459
 
4340
4460
  // Process loader results into state.loaderData/state.errors
4341
4461
  results.forEach((result, index) => {
4342
4462
  let id = matchesToLoad[index].route.id;
4343
4463
  invariant(!isRedirectResult(result), "Cannot handle redirect results in processLoaderData");
4344
4464
  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
4465
  let error = result.error;
4349
4466
  // If we have a pending action error, we report it at the highest-route
4350
4467
  // that throws a loader error, and then clear it out to indicate that
4351
4468
  // it was consumed
4352
- if (pendingError) {
4353
- error = Object.values(pendingError)[0];
4469
+ if (pendingError !== undefined) {
4470
+ error = pendingError;
4354
4471
  pendingError = undefined;
4355
4472
  }
4356
4473
  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;
4474
+ if (skipLoaderErrorBubbling) {
4475
+ errors[id] = error;
4476
+ } else {
4477
+ // Look upwards from the matched route for the closest ancestor error
4478
+ // boundary, defaulting to the root match. Prefer higher error values
4479
+ // if lower errors bubble to the same boundary
4480
+ let boundaryMatch = findNearestBoundary(matches, id);
4481
+ if (errors[boundaryMatch.route.id] == null) {
4482
+ errors[boundaryMatch.route.id] = error;
4483
+ }
4361
4484
  }
4362
4485
 
4363
4486
  // Clear our any prior loaderData for the throwing route
@@ -4376,17 +4499,24 @@ function processRouteLoaderData(matches, matchesToLoad, results, pendingError, a
4376
4499
  if (isDeferredResult(result)) {
4377
4500
  activeDeferreds.set(id, result.deferredData);
4378
4501
  loaderData[id] = result.deferredData.data;
4502
+ // Error status codes always override success status codes, but if all
4503
+ // loaders are successful we take the deepest status code.
4504
+ if (result.statusCode != null && result.statusCode !== 200 && !foundError) {
4505
+ statusCode = result.statusCode;
4506
+ }
4507
+ if (result.headers) {
4508
+ loaderHeaders[id] = result.headers;
4509
+ }
4379
4510
  } else {
4380
4511
  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;
4512
+ // Error status codes always override success status codes, but if all
4513
+ // loaders are successful we take the deepest status code.
4514
+ if (result.statusCode && result.statusCode !== 200 && !foundError) {
4515
+ statusCode = result.statusCode;
4516
+ }
4517
+ if (result.headers) {
4518
+ loaderHeaders[id] = result.headers;
4519
+ }
4390
4520
  }
4391
4521
  }
4392
4522
  });
@@ -4394,9 +4524,11 @@ function processRouteLoaderData(matches, matchesToLoad, results, pendingError, a
4394
4524
  // If we didn't consume the pending action error (i.e., all loaders
4395
4525
  // resolved), then consume it here. Also clear out any loaderData for the
4396
4526
  // throwing route
4397
- if (pendingError) {
4398
- errors = pendingError;
4399
- loaderData[Object.keys(pendingError)[0]] = undefined;
4527
+ if (pendingError !== undefined && pendingActionResult) {
4528
+ errors = {
4529
+ [pendingActionResult[0]]: pendingError
4530
+ };
4531
+ loaderData[pendingActionResult[0]] = undefined;
4400
4532
  }
4401
4533
  return {
4402
4534
  loaderData,
@@ -4405,11 +4537,12 @@ function processRouteLoaderData(matches, matchesToLoad, results, pendingError, a
4405
4537
  loaderHeaders
4406
4538
  };
4407
4539
  }
4408
- function processLoaderData(state, matches, matchesToLoad, results, pendingError, revalidatingFetchers, fetcherResults, activeDeferreds) {
4540
+ function processLoaderData(state, matches, matchesToLoad, results, pendingActionResult, revalidatingFetchers, fetcherResults, activeDeferreds) {
4409
4541
  let {
4410
4542
  loaderData,
4411
4543
  errors
4412
- } = processRouteLoaderData(matches, matchesToLoad, results, pendingError, activeDeferreds);
4544
+ } = processRouteLoaderData(matches, matchesToLoad, results, pendingActionResult, activeDeferreds, false // This method is only called client side so we always want to bubble
4545
+ );
4413
4546
 
4414
4547
  // Process results from our revalidating fetchers
4415
4548
  for (let index = 0; index < revalidatingFetchers.length; index++) {
@@ -4471,6 +4604,19 @@ function mergeLoaderData(loaderData, newLoaderData, matches, errors) {
4471
4604
  }
4472
4605
  return mergedLoaderData;
4473
4606
  }
4607
+ function getActionDataForCommit(pendingActionResult) {
4608
+ if (!pendingActionResult) {
4609
+ return {};
4610
+ }
4611
+ return isErrorResult(pendingActionResult[1]) ? {
4612
+ // Clear out prior actionData on errors
4613
+ actionData: {}
4614
+ } : {
4615
+ actionData: {
4616
+ [pendingActionResult[0]]: pendingActionResult[1].data
4617
+ }
4618
+ };
4619
+ }
4474
4620
 
4475
4621
  // Find the nearest error boundary, looking upwards from the leaf route (or the
4476
4622
  // route specified by routeId) for the closest ancestor error boundary,
@@ -4566,6 +4712,12 @@ function isHashChangeOnly(a, b) {
4566
4712
  // /page#hash -> /page
4567
4713
  return false;
4568
4714
  }
4715
+ function isHandlerResult(result) {
4716
+ return result != null && typeof result === "object" && "type" in result && "result" in result && (result.type === ResultType.data || result.type === ResultType.error);
4717
+ }
4718
+ function isRedirectHandlerResult(result) {
4719
+ return isResponse(result.result) && redirectStatusCodes.has(result.result.status);
4720
+ }
4569
4721
  function isDeferredResult(result) {
4570
4722
  return result.type === ResultType.deferred;
4571
4723
  }
@@ -4590,9 +4742,6 @@ function isRedirectResponse(result) {
4590
4742
  let location = result.headers.get("Location");
4591
4743
  return status >= 300 && status <= 399 && location != null;
4592
4744
  }
4593
- function isQueryRouteResponse(obj) {
4594
- return obj && isResponse(obj.response) && (obj.type === ResultType.data || obj.type === ResultType.error);
4595
- }
4596
4745
  function isValidMethod(method) {
4597
4746
  return validRequestMethods.has(method.toLowerCase());
4598
4747
  }