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