@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.
package/dist/router.js CHANGED
@@ -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
  *
@@ -1320,13 +1320,15 @@ function createRouter(init) {
1320
1320
  let dataRoutes = convertRoutesToDataRoutes(init.routes, mapRouteProperties, undefined, manifest);
1321
1321
  let inFlightDataRoutes;
1322
1322
  let basename = init.basename || "/";
1323
+ let dataStrategyImpl = init.unstable_dataStrategy || defaultDataStrategy;
1323
1324
  // Config driven behavior flags
1324
1325
  let future = _extends({
1325
1326
  v7_fetcherPersist: false,
1326
1327
  v7_normalizeFormMethod: false,
1327
1328
  v7_partialHydration: false,
1328
1329
  v7_prependBasename: false,
1329
- v7_relativeSplatPath: false
1330
+ v7_relativeSplatPath: false,
1331
+ unstable_skipActionErrorRevalidation: false
1330
1332
  }, init.future);
1331
1333
  // Cleanup function for history
1332
1334
  let unlistenHistory = null;
@@ -1380,9 +1382,13 @@ function createRouter(init) {
1380
1382
  let errors = init.hydrationData ? init.hydrationData.errors : null;
1381
1383
  let isRouteInitialized = m => {
1382
1384
  // No loader, nothing to initialize
1383
- if (!m.route.loader) return true;
1385
+ if (!m.route.loader) {
1386
+ return true;
1387
+ }
1384
1388
  // Explicitly opting-in to running on hydration
1385
- if (m.route.loader.hydrate === true) return false;
1389
+ if (typeof m.route.loader === "function" && m.route.loader.hydrate === true) {
1390
+ return false;
1391
+ }
1386
1392
  // Otherwise, initialized if hydrated with data or an error
1387
1393
  return loaderData && loaderData[m.route.id] !== undefined || errors && errors[m.route.id] !== undefined;
1388
1394
  };
@@ -1874,40 +1880,37 @@ function createRouter(init) {
1874
1880
  // Create a controller/Request for this navigation
1875
1881
  pendingNavigationController = new AbortController();
1876
1882
  let request = createClientSideRequest(init.history, location, pendingNavigationController.signal, opts && opts.submission);
1877
- let pendingActionData;
1878
- let pendingError;
1883
+ let pendingActionResult;
1879
1884
  if (opts && opts.pendingError) {
1880
1885
  // If we have a pendingError, it means the user attempted a GET submission
1881
1886
  // with binary FormData so assign here and skip to handleLoaders. That
1882
1887
  // way we handle calling loaders above the boundary etc. It's not really
1883
1888
  // different from an actionError in that sense.
1884
- pendingError = {
1885
- [findNearestBoundary(matches).route.id]: opts.pendingError
1886
- };
1889
+ pendingActionResult = [findNearestBoundary(matches).route.id, {
1890
+ type: ResultType.error,
1891
+ error: opts.pendingError
1892
+ }];
1887
1893
  } else if (opts && opts.submission && isMutationMethod(opts.submission.formMethod)) {
1888
1894
  // Call action if we received an action submission
1889
- let actionOutput = await handleAction(request, location, opts.submission, matches, {
1895
+ let actionResult = await handleAction(request, location, opts.submission, matches, {
1890
1896
  replace: opts.replace,
1891
1897
  flushSync
1892
1898
  });
1893
- if (actionOutput.shortCircuited) {
1899
+ if (actionResult.shortCircuited) {
1894
1900
  return;
1895
1901
  }
1896
- pendingActionData = actionOutput.pendingActionData;
1897
- pendingError = actionOutput.pendingActionError;
1902
+ pendingActionResult = actionResult.pendingActionResult;
1898
1903
  loadingNavigation = getLoadingNavigation(location, opts.submission);
1899
1904
  flushSync = false;
1900
1905
  // Create a GET request for the loaders
1901
- request = new Request(request.url, {
1902
- signal: request.signal
1903
- });
1906
+ request = createClientSideRequest(init.history, request.url, request.signal);
1904
1907
  }
1905
1908
  // Call loaders
1906
1909
  let {
1907
1910
  shortCircuited,
1908
1911
  loaderData,
1909
1912
  errors
1910
- } = await handleLoaders(request, location, matches, loadingNavigation, opts && opts.submission, opts && opts.fetcherSubmission, opts && opts.replace, opts && opts.initialHydration === true, flushSync, pendingActionData, pendingError);
1913
+ } = await handleLoaders(request, location, matches, loadingNavigation, opts && opts.submission, opts && opts.fetcherSubmission, opts && opts.replace, opts && opts.initialHydration === true, flushSync, pendingActionResult);
1911
1914
  if (shortCircuited) {
1912
1915
  return;
1913
1916
  }
@@ -1917,9 +1920,7 @@ function createRouter(init) {
1917
1920
  pendingNavigationController = null;
1918
1921
  completeNavigation(location, _extends({
1919
1922
  matches
1920
- }, pendingActionData ? {
1921
- actionData: pendingActionData
1922
- } : {}, {
1923
+ }, getActionDataForCommit(pendingActionResult), {
1923
1924
  loaderData,
1924
1925
  errors
1925
1926
  }));
@@ -1951,7 +1952,8 @@ function createRouter(init) {
1951
1952
  })
1952
1953
  };
1953
1954
  } else {
1954
- result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
1955
+ let results = await callDataStrategy("action", request, [actionMatch], matches);
1956
+ result = results[0];
1955
1957
  if (request.signal.aborted) {
1956
1958
  return {
1957
1959
  shortCircuited: true
@@ -1966,9 +1968,10 @@ function createRouter(init) {
1966
1968
  // If the user didn't explicity indicate replace behavior, replace if
1967
1969
  // we redirected to the exact same location we're currently at to avoid
1968
1970
  // double back-buttons
1969
- replace = result.location === state.location.pathname + state.location.search;
1971
+ let location = normalizeRedirectLocation(result.response.headers.get("Location"), new URL(request.url), basename);
1972
+ replace = location === state.location.pathname + state.location.search;
1970
1973
  }
1971
- await startRedirectNavigation(state, result, {
1974
+ await startRedirectNavigation(request, result, {
1972
1975
  submission,
1973
1976
  replace
1974
1977
  });
@@ -1976,6 +1979,11 @@ function createRouter(init) {
1976
1979
  shortCircuited: true
1977
1980
  };
1978
1981
  }
1982
+ if (isDeferredResult(result)) {
1983
+ throw getInternalRouterError(400, {
1984
+ type: "defer-action"
1985
+ });
1986
+ }
1979
1987
  if (isErrorResult(result)) {
1980
1988
  // Store off the pending error - we use it to determine which loaders
1981
1989
  // to call and will commit it when we complete the navigation
@@ -1988,34 +1996,23 @@ function createRouter(init) {
1988
1996
  pendingAction = Action.Push;
1989
1997
  }
1990
1998
  return {
1991
- // Send back an empty object we can use to clear out any prior actionData
1992
- pendingActionData: {},
1993
- pendingActionError: {
1994
- [boundaryMatch.route.id]: result.error
1995
- }
1999
+ pendingActionResult: [boundaryMatch.route.id, result]
1996
2000
  };
1997
2001
  }
1998
- if (isDeferredResult(result)) {
1999
- throw getInternalRouterError(400, {
2000
- type: "defer-action"
2001
- });
2002
- }
2003
2002
  return {
2004
- pendingActionData: {
2005
- [actionMatch.route.id]: result.data
2006
- }
2003
+ pendingActionResult: [actionMatch.route.id, result]
2007
2004
  };
2008
2005
  }
2009
2006
  // Call all applicable loaders for the given matches, handling redirects,
2010
2007
  // errors, etc.
2011
- async function handleLoaders(request, location, matches, overrideNavigation, submission, fetcherSubmission, replace, initialHydration, flushSync, pendingActionData, pendingError) {
2008
+ async function handleLoaders(request, location, matches, overrideNavigation, submission, fetcherSubmission, replace, initialHydration, flushSync, pendingActionResult) {
2012
2009
  // Figure out the right navigation we want to use for data loading
2013
2010
  let loadingNavigation = overrideNavigation || getLoadingNavigation(location, submission);
2014
2011
  // If this was a redirect from an action we don't have a "submission" but
2015
2012
  // we have it on the loading navigation so use that if available
2016
2013
  let activeSubmission = submission || fetcherSubmission || getSubmissionFromNavigation(loadingNavigation);
2017
2014
  let routesToUse = inFlightDataRoutes || dataRoutes;
2018
- 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);
2015
+ 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);
2019
2016
  // Cancel pending deferreds for no-longer-matched routes or routes we're
2020
2017
  // about to reload. Note that if this is an action reload we would have
2021
2018
  // already cancelled all pending deferreds so this would be a no-op
@@ -2028,10 +2025,10 @@ function createRouter(init) {
2028
2025
  matches,
2029
2026
  loaderData: {},
2030
2027
  // Commit pending error if we're short circuiting
2031
- errors: pendingError || null
2032
- }, pendingActionData ? {
2033
- actionData: pendingActionData
2034
- } : {}, updatedFetchers ? {
2028
+ errors: pendingActionResult && isErrorResult(pendingActionResult[1]) ? {
2029
+ [pendingActionResult[0]]: pendingActionResult[1].error
2030
+ } : null
2031
+ }, getActionDataForCommit(pendingActionResult), updatedFetchers ? {
2035
2032
  fetchers: new Map(state.fetchers)
2036
2033
  } : {}), {
2037
2034
  flushSync
@@ -2052,12 +2049,24 @@ function createRouter(init) {
2052
2049
  let revalidatingFetcher = getLoadingFetcher(undefined, fetcher ? fetcher.data : undefined);
2053
2050
  state.fetchers.set(rf.key, revalidatingFetcher);
2054
2051
  });
2055
- let actionData = pendingActionData || state.actionData;
2052
+ let actionData;
2053
+ if (pendingActionResult && !isErrorResult(pendingActionResult[1])) {
2054
+ // This is cast to `any` currently because `RouteData`uses any and it
2055
+ // would be a breaking change to use any.
2056
+ // TODO: v7 - change `RouteData` to use `unknown` instead of `any`
2057
+ actionData = {
2058
+ [pendingActionResult[0]]: pendingActionResult[1].data
2059
+ };
2060
+ } else if (state.actionData) {
2061
+ if (Object.keys(state.actionData).length === 0) {
2062
+ actionData = null;
2063
+ } else {
2064
+ actionData = state.actionData;
2065
+ }
2066
+ }
2056
2067
  updateState(_extends({
2057
2068
  navigation: loadingNavigation
2058
- }, actionData ? Object.keys(actionData).length === 0 ? {
2059
- actionData: null
2060
- } : {
2069
+ }, actionData !== undefined ? {
2061
2070
  actionData
2062
2071
  } : {}, revalidatingFetchers.length > 0 ? {
2063
2072
  fetchers: new Map(state.fetchers)
@@ -2082,7 +2091,6 @@ function createRouter(init) {
2082
2091
  pendingNavigationController.signal.addEventListener("abort", abortPendingFetchRevalidations);
2083
2092
  }
2084
2093
  let {
2085
- results,
2086
2094
  loaderResults,
2087
2095
  fetcherResults
2088
2096
  } = await callLoadersAndMaybeResolveData(state.matches, matches, matchesToLoad, revalidatingFetchers, request);
@@ -2099,7 +2107,7 @@ function createRouter(init) {
2099
2107
  }
2100
2108
  revalidatingFetchers.forEach(rf => fetchControllers.delete(rf.key));
2101
2109
  // If any loaders returned a redirect Response, start a new REPLACE navigation
2102
- let redirect = findRedirect(results);
2110
+ let redirect = findRedirect([...loaderResults, ...fetcherResults]);
2103
2111
  if (redirect) {
2104
2112
  if (redirect.idx >= matchesToLoad.length) {
2105
2113
  // If this redirect came from a fetcher make sure we mark it in
@@ -2108,7 +2116,7 @@ function createRouter(init) {
2108
2116
  let fetcherKey = revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
2109
2117
  fetchRedirectIds.add(fetcherKey);
2110
2118
  }
2111
- await startRedirectNavigation(state, redirect.result, {
2119
+ await startRedirectNavigation(request, redirect.result, {
2112
2120
  replace
2113
2121
  });
2114
2122
  return {
@@ -2119,7 +2127,7 @@ function createRouter(init) {
2119
2127
  let {
2120
2128
  loaderData,
2121
2129
  errors
2122
- } = processLoaderData(state, matches, matchesToLoad, loaderResults, pendingError, revalidatingFetchers, fetcherResults, activeDeferreds);
2130
+ } = processLoaderData(state, matches, matchesToLoad, loaderResults, pendingActionResult, revalidatingFetchers, fetcherResults, activeDeferreds);
2123
2131
  // Wire up subscribers to update loaderData as promises settle
2124
2132
  activeDeferreds.forEach((deferredData, routeId) => {
2125
2133
  deferredData.subscribe(aborted => {
@@ -2222,7 +2230,8 @@ function createRouter(init) {
2222
2230
  let fetchRequest = createClientSideRequest(init.history, path, abortController.signal, submission);
2223
2231
  fetchControllers.set(key, abortController);
2224
2232
  let originatingLoadId = incrementingLoadId;
2225
- let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
2233
+ let actionResults = await callDataStrategy("action", fetchRequest, [match], requestMatches);
2234
+ let actionResult = actionResults[0];
2226
2235
  if (fetchRequest.signal.aborted) {
2227
2236
  // We can delete this so long as we weren't aborted by our own fetcher
2228
2237
  // re-submit which would have put _new_ controller is in fetchControllers
@@ -2253,7 +2262,7 @@ function createRouter(init) {
2253
2262
  } else {
2254
2263
  fetchRedirectIds.add(key);
2255
2264
  updateFetcherState(key, getLoadingFetcher(submission));
2256
- return startRedirectNavigation(state, actionResult, {
2265
+ return startRedirectNavigation(fetchRequest, actionResult, {
2257
2266
  fetcherSubmission: submission
2258
2267
  });
2259
2268
  }
@@ -2280,10 +2289,7 @@ function createRouter(init) {
2280
2289
  fetchReloadIds.set(key, loadId);
2281
2290
  let loadFetcher = getLoadingFetcher(submission, actionResult.data);
2282
2291
  state.fetchers.set(key, loadFetcher);
2283
- let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, submission, nextLocation, false, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, {
2284
- [match.route.id]: actionResult.data
2285
- }, undefined // No need to send through errors since we short circuit above
2286
- );
2292
+ 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]);
2287
2293
  // Put all revalidating fetchers into the loading state, except for the
2288
2294
  // current fetcher which we want to keep in it's current loading state which
2289
2295
  // contains it's action submission info + action data
@@ -2305,7 +2311,6 @@ function createRouter(init) {
2305
2311
  let abortPendingFetchRevalidations = () => revalidatingFetchers.forEach(rf => abortFetcher(rf.key));
2306
2312
  abortController.signal.addEventListener("abort", abortPendingFetchRevalidations);
2307
2313
  let {
2308
- results,
2309
2314
  loaderResults,
2310
2315
  fetcherResults
2311
2316
  } = await callLoadersAndMaybeResolveData(state.matches, matches, matchesToLoad, revalidatingFetchers, revalidationRequest);
@@ -2316,7 +2321,7 @@ function createRouter(init) {
2316
2321
  fetchReloadIds.delete(key);
2317
2322
  fetchControllers.delete(key);
2318
2323
  revalidatingFetchers.forEach(r => fetchControllers.delete(r.key));
2319
- let redirect = findRedirect(results);
2324
+ let redirect = findRedirect([...loaderResults, ...fetcherResults]);
2320
2325
  if (redirect) {
2321
2326
  if (redirect.idx >= matchesToLoad.length) {
2322
2327
  // If this redirect came from a fetcher make sure we mark it in
@@ -2325,7 +2330,7 @@ function createRouter(init) {
2325
2330
  let fetcherKey = revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
2326
2331
  fetchRedirectIds.add(fetcherKey);
2327
2332
  }
2328
- return startRedirectNavigation(state, redirect.result);
2333
+ return startRedirectNavigation(revalidationRequest, redirect.result);
2329
2334
  }
2330
2335
  // Process and commit output from loaders
2331
2336
  let {
@@ -2374,7 +2379,8 @@ function createRouter(init) {
2374
2379
  let fetchRequest = createClientSideRequest(init.history, path, abortController.signal);
2375
2380
  fetchControllers.set(key, abortController);
2376
2381
  let originatingLoadId = incrementingLoadId;
2377
- let result = await callLoaderOrAction("loader", fetchRequest, match, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
2382
+ let results = await callDataStrategy("loader", fetchRequest, [match], matches);
2383
+ let result = results[0];
2378
2384
  // Deferred isn't supported for fetcher loads, await everything and treat it
2379
2385
  // as a normal load. resolveDeferredData will return undefined if this
2380
2386
  // fetcher gets aborted, so we just leave result untouched and short circuit
@@ -2405,7 +2411,7 @@ function createRouter(init) {
2405
2411
  return;
2406
2412
  } else {
2407
2413
  fetchRedirectIds.add(key);
2408
- await startRedirectNavigation(state, result);
2414
+ await startRedirectNavigation(fetchRequest, result);
2409
2415
  return;
2410
2416
  }
2411
2417
  }
@@ -2437,26 +2443,28 @@ function createRouter(init) {
2437
2443
  * actually touch history until we've processed redirects, so we just use
2438
2444
  * the history action from the original navigation (PUSH or REPLACE).
2439
2445
  */
2440
- async function startRedirectNavigation(state, redirect, _temp2) {
2446
+ async function startRedirectNavigation(request, redirect, _temp2) {
2441
2447
  let {
2442
2448
  submission,
2443
2449
  fetcherSubmission,
2444
2450
  replace
2445
2451
  } = _temp2 === void 0 ? {} : _temp2;
2446
- if (redirect.revalidate) {
2452
+ if (redirect.response.headers.has("X-Remix-Revalidate")) {
2447
2453
  isRevalidationRequired = true;
2448
2454
  }
2449
- let redirectLocation = createLocation(state.location, redirect.location, {
2455
+ let location = redirect.response.headers.get("Location");
2456
+ invariant(location, "Expected a Location header on the redirect Response");
2457
+ location = normalizeRedirectLocation(location, new URL(request.url), basename);
2458
+ let redirectLocation = createLocation(state.location, location, {
2450
2459
  _isRedirect: true
2451
2460
  });
2452
- invariant(redirectLocation, "Expected a location on the redirect navigation");
2453
2461
  if (isBrowser) {
2454
2462
  let isDocumentReload = false;
2455
- if (redirect.reloadDocument) {
2463
+ if (redirect.response.headers.has("X-Remix-Reload-Document")) {
2456
2464
  // Hard reload if the response contained X-Remix-Reload-Document
2457
2465
  isDocumentReload = true;
2458
- } else if (ABSOLUTE_URL_REGEX.test(redirect.location)) {
2459
- const url = init.history.createURL(redirect.location);
2466
+ } else if (ABSOLUTE_URL_REGEX.test(location)) {
2467
+ const url = init.history.createURL(location);
2460
2468
  isDocumentReload =
2461
2469
  // Hard reload if it's an absolute URL to a new origin
2462
2470
  url.origin !== routerWindow.location.origin ||
@@ -2465,9 +2473,9 @@ function createRouter(init) {
2465
2473
  }
2466
2474
  if (isDocumentReload) {
2467
2475
  if (replace) {
2468
- routerWindow.location.replace(redirect.location);
2476
+ routerWindow.location.replace(location);
2469
2477
  } else {
2470
- routerWindow.location.assign(redirect.location);
2478
+ routerWindow.location.assign(location);
2471
2479
  }
2472
2480
  return;
2473
2481
  }
@@ -2490,10 +2498,10 @@ function createRouter(init) {
2490
2498
  // re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the
2491
2499
  // redirected location
2492
2500
  let activeSubmission = submission || fetcherSubmission;
2493
- if (redirectPreserveMethodStatusCodes.has(redirect.status) && activeSubmission && isMutationMethod(activeSubmission.formMethod)) {
2501
+ if (redirectPreserveMethodStatusCodes.has(redirect.response.status) && activeSubmission && isMutationMethod(activeSubmission.formMethod)) {
2494
2502
  await startNavigation(redirectHistoryAction, redirectLocation, {
2495
2503
  submission: _extends({}, activeSubmission, {
2496
- formAction: redirect.location
2504
+ formAction: location
2497
2505
  }),
2498
2506
  // Preserve this flag across redirects
2499
2507
  preventScrollReset: pendingPreventScrollReset
@@ -2511,28 +2519,46 @@ function createRouter(init) {
2511
2519
  });
2512
2520
  }
2513
2521
  }
2522
+ // Utility wrapper for calling dataStrategy client-side without having to
2523
+ // pass around the manifest, mapRouteProperties, etc.
2524
+ async function callDataStrategy(type, request, matchesToLoad, matches) {
2525
+ try {
2526
+ let results = await callDataStrategyImpl(dataStrategyImpl, type, request, matchesToLoad, matches, manifest, mapRouteProperties);
2527
+ return await Promise.all(results.map((result, i) => {
2528
+ if (isRedirectHandlerResult(result)) {
2529
+ let response = result.result;
2530
+ return {
2531
+ type: ResultType.redirect,
2532
+ response: normalizeRelativeRoutingRedirectResponse(response, request, matchesToLoad[i].route.id, matches, basename, future.v7_relativeSplatPath)
2533
+ };
2534
+ }
2535
+ return convertHandlerResultToDataResult(result);
2536
+ }));
2537
+ } catch (e) {
2538
+ // If the outer dataStrategy method throws, just return the error for all
2539
+ // matches - and it'll naturally bubble to the root
2540
+ return matchesToLoad.map(() => ({
2541
+ type: ResultType.error,
2542
+ error: e
2543
+ }));
2544
+ }
2545
+ }
2514
2546
  async function callLoadersAndMaybeResolveData(currentMatches, matches, matchesToLoad, fetchersToLoad, request) {
2515
- // Call all navigation loaders and revalidating fetcher loaders in parallel,
2516
- // then slice off the results into separate arrays so we can handle them
2517
- // accordingly
2518
- let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath)), ...fetchersToLoad.map(f => {
2547
+ let [loaderResults, ...fetcherResults] = await Promise.all([matchesToLoad.length ? callDataStrategy("loader", request, matchesToLoad, matches) : [], ...fetchersToLoad.map(f => {
2519
2548
  if (f.matches && f.match && f.controller) {
2520
- return callLoaderOrAction("loader", createClientSideRequest(init.history, f.path, f.controller.signal), f.match, f.matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
2549
+ let fetcherRequest = createClientSideRequest(init.history, f.path, f.controller.signal);
2550
+ return callDataStrategy("loader", fetcherRequest, [f.match], f.matches).then(r => r[0]);
2521
2551
  } else {
2522
- let error = {
2552
+ return Promise.resolve({
2523
2553
  type: ResultType.error,
2524
2554
  error: getInternalRouterError(404, {
2525
2555
  pathname: f.path
2526
2556
  })
2527
- };
2528
- return error;
2557
+ });
2529
2558
  }
2530
2559
  })]);
2531
- let loaderResults = results.slice(0, matchesToLoad.length);
2532
- let fetcherResults = results.slice(matchesToLoad.length);
2533
2560
  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)]);
2534
2561
  return {
2535
- results,
2536
2562
  loaderResults,
2537
2563
  fetcherResults
2538
2564
  };
@@ -2866,10 +2892,19 @@ function createStaticHandler(routes, opts) {
2866
2892
  * redirect response is returned or thrown from any action/loader. We
2867
2893
  * propagate that out and return the raw Response so the HTTP server can
2868
2894
  * return it directly.
2895
+ *
2896
+ * - `opts.requestContext` is an optional server context that will be passed
2897
+ * to actions/loaders in the `context` parameter
2898
+ * - `opts.skipLoaderErrorBubbling` is an optional parameter that will prevent
2899
+ * the bubbling of errors which allows single-fetch-type implementations
2900
+ * where the client will handle the bubbling and we may need to return data
2901
+ * for the handling route
2869
2902
  */
2870
2903
  async function query(request, _temp3) {
2871
2904
  let {
2872
- requestContext
2905
+ requestContext,
2906
+ skipLoaderErrorBubbling,
2907
+ unstable_dataStrategy
2873
2908
  } = _temp3 === void 0 ? {} : _temp3;
2874
2909
  let url = new URL(request.url);
2875
2910
  let method = request.method;
@@ -2921,7 +2956,7 @@ function createStaticHandler(routes, opts) {
2921
2956
  activeDeferreds: null
2922
2957
  };
2923
2958
  }
2924
- let result = await queryImpl(request, location, matches, requestContext);
2959
+ let result = await queryImpl(request, location, matches, requestContext, unstable_dataStrategy || null, skipLoaderErrorBubbling === true, null);
2925
2960
  if (isResponse(result)) {
2926
2961
  return result;
2927
2962
  }
@@ -2952,6 +2987,12 @@ function createStaticHandler(routes, opts) {
2952
2987
  * serialize the error as they see fit while including the proper response
2953
2988
  * code. Examples here are 404 and 405 errors that occur prior to reaching
2954
2989
  * any user-defined loaders.
2990
+ *
2991
+ * - `opts.routeId` allows you to specify the specific route handler to call.
2992
+ * If not provided the handler will determine the proper route by matching
2993
+ * against `request.url`
2994
+ * - `opts.requestContext` is an optional server context that will be passed
2995
+ * to actions/loaders in the `context` parameter
2955
2996
  */
2956
2997
  async function queryRoute(request, _temp4) {
2957
2998
  let {
@@ -2984,7 +3025,7 @@ function createStaticHandler(routes, opts) {
2984
3025
  pathname: location.pathname
2985
3026
  });
2986
3027
  }
2987
- let result = await queryImpl(request, location, matches, requestContext, match);
3028
+ let result = await queryImpl(request, location, matches, requestContext, null, false, match);
2988
3029
  if (isResponse(result)) {
2989
3030
  return result;
2990
3031
  }
@@ -3010,27 +3051,27 @@ function createStaticHandler(routes, opts) {
3010
3051
  }
3011
3052
  return undefined;
3012
3053
  }
3013
- async function queryImpl(request, location, matches, requestContext, routeMatch) {
3054
+ async function queryImpl(request, location, matches, requestContext, unstable_dataStrategy, skipLoaderErrorBubbling, routeMatch) {
3014
3055
  invariant(request.signal, "query()/queryRoute() requests must contain an AbortController signal");
3015
3056
  try {
3016
3057
  if (isMutationMethod(request.method.toLowerCase())) {
3017
- let result = await submit(request, matches, routeMatch || getTargetMatch(matches, location), requestContext, routeMatch != null);
3058
+ let result = await submit(request, matches, routeMatch || getTargetMatch(matches, location), requestContext, unstable_dataStrategy, skipLoaderErrorBubbling, routeMatch != null);
3018
3059
  return result;
3019
3060
  }
3020
- let result = await loadRouteData(request, matches, requestContext, routeMatch);
3061
+ let result = await loadRouteData(request, matches, requestContext, unstable_dataStrategy, skipLoaderErrorBubbling, routeMatch);
3021
3062
  return isResponse(result) ? result : _extends({}, result, {
3022
3063
  actionData: null,
3023
3064
  actionHeaders: {}
3024
3065
  });
3025
3066
  } catch (e) {
3026
- // If the user threw/returned a Response in callLoaderOrAction, we throw
3027
- // it to bail out and then return or throw here based on whether the user
3028
- // returned or threw
3029
- if (isQueryRouteResponse(e)) {
3067
+ // If the user threw/returned a Response in callLoaderOrAction for a
3068
+ // `queryRoute` call, we throw the `HandlerResult` to bail out early
3069
+ // and then return or throw the raw Response here accordingly
3070
+ if (isHandlerResult(e) && isResponse(e.result)) {
3030
3071
  if (e.type === ResultType.error) {
3031
- throw e.response;
3072
+ throw e.result;
3032
3073
  }
3033
- return e.response;
3074
+ return e.result;
3034
3075
  }
3035
3076
  // Redirects are always returned since they don't propagate to catch
3036
3077
  // boundaries
@@ -3040,7 +3081,7 @@ function createStaticHandler(routes, opts) {
3040
3081
  throw e;
3041
3082
  }
3042
3083
  }
3043
- async function submit(request, matches, actionMatch, requestContext, isRouteRequest) {
3084
+ async function submit(request, matches, actionMatch, requestContext, unstable_dataStrategy, skipLoaderErrorBubbling, isRouteRequest) {
3044
3085
  let result;
3045
3086
  if (!actionMatch.route.action && !actionMatch.route.lazy) {
3046
3087
  let error = getInternalRouterError(405, {
@@ -3056,11 +3097,8 @@ function createStaticHandler(routes, opts) {
3056
3097
  error
3057
3098
  };
3058
3099
  } else {
3059
- result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath, {
3060
- isStaticRequest: true,
3061
- isRouteRequest,
3062
- requestContext
3063
- });
3100
+ let results = await callDataStrategy("action", request, [actionMatch], matches, isRouteRequest, requestContext, unstable_dataStrategy);
3101
+ result = results[0];
3064
3102
  if (request.signal.aborted) {
3065
3103
  throwStaticHandlerAbortedError(request, isRouteRequest, future);
3066
3104
  }
@@ -3071,9 +3109,9 @@ function createStaticHandler(routes, opts) {
3071
3109
  // can get back on the "throw all redirect responses" train here should
3072
3110
  // this ever happen :/
3073
3111
  throw new Response(null, {
3074
- status: result.status,
3112
+ status: result.response.status,
3075
3113
  headers: {
3076
- Location: result.location
3114
+ Location: result.response.headers.get("Location")
3077
3115
  }
3078
3116
  });
3079
3117
  }
@@ -3110,41 +3148,40 @@ function createStaticHandler(routes, opts) {
3110
3148
  activeDeferreds: null
3111
3149
  };
3112
3150
  }
3151
+ // Create a GET request for the loaders
3152
+ let loaderRequest = new Request(request.url, {
3153
+ headers: request.headers,
3154
+ redirect: request.redirect,
3155
+ signal: request.signal
3156
+ });
3113
3157
  if (isErrorResult(result)) {
3114
3158
  // Store off the pending error - we use it to determine which loaders
3115
3159
  // to call and will commit it when we complete the navigation
3116
- let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
3117
- let context = await loadRouteData(request, matches, requestContext, undefined, {
3118
- [boundaryMatch.route.id]: result.error
3119
- });
3160
+ let boundaryMatch = skipLoaderErrorBubbling ? actionMatch : findNearestBoundary(matches, actionMatch.route.id);
3161
+ let context = await loadRouteData(loaderRequest, matches, requestContext, unstable_dataStrategy, skipLoaderErrorBubbling, null, [boundaryMatch.route.id, result]);
3120
3162
  // action status codes take precedence over loader status codes
3121
3163
  return _extends({}, context, {
3122
- statusCode: isRouteErrorResponse(result.error) ? result.error.status : 500,
3164
+ statusCode: isRouteErrorResponse(result.error) ? result.error.status : result.statusCode != null ? result.statusCode : 500,
3123
3165
  actionData: null,
3124
3166
  actionHeaders: _extends({}, result.headers ? {
3125
3167
  [actionMatch.route.id]: result.headers
3126
3168
  } : {})
3127
3169
  });
3128
3170
  }
3129
- // Create a GET request for the loaders
3130
- let loaderRequest = new Request(request.url, {
3131
- headers: request.headers,
3132
- redirect: request.redirect,
3133
- signal: request.signal
3134
- });
3135
- let context = await loadRouteData(loaderRequest, matches, requestContext);
3136
- return _extends({}, context, result.statusCode ? {
3137
- statusCode: result.statusCode
3138
- } : {}, {
3171
+ let context = await loadRouteData(loaderRequest, matches, requestContext, unstable_dataStrategy, skipLoaderErrorBubbling, null);
3172
+ return _extends({}, context, {
3139
3173
  actionData: {
3140
3174
  [actionMatch.route.id]: result.data
3141
- },
3142
- actionHeaders: _extends({}, result.headers ? {
3175
+ }
3176
+ }, result.statusCode ? {
3177
+ statusCode: result.statusCode
3178
+ } : {}, {
3179
+ actionHeaders: result.headers ? {
3143
3180
  [actionMatch.route.id]: result.headers
3144
- } : {})
3181
+ } : {}
3145
3182
  });
3146
3183
  }
3147
- async function loadRouteData(request, matches, requestContext, routeMatch, pendingActionError) {
3184
+ async function loadRouteData(request, matches, requestContext, unstable_dataStrategy, skipLoaderErrorBubbling, routeMatch, pendingActionResult) {
3148
3185
  let isRouteRequest = routeMatch != null;
3149
3186
  // Short circuit if we have no loaders to run (queryRoute())
3150
3187
  if (isRouteRequest && !(routeMatch != null && routeMatch.route.loader) && !(routeMatch != null && routeMatch.route.lazy)) {
@@ -3154,7 +3191,7 @@ function createStaticHandler(routes, opts) {
3154
3191
  routeId: routeMatch == null ? void 0 : routeMatch.route.id
3155
3192
  });
3156
3193
  }
3157
- let requestMatches = routeMatch ? [routeMatch] : getLoaderMatchesUntilBoundary(matches, Object.keys(pendingActionError || {})[0]);
3194
+ let requestMatches = routeMatch ? [routeMatch] : pendingActionResult && isErrorResult(pendingActionResult[1]) ? getLoaderMatchesUntilBoundary(matches, pendingActionResult[0]) : matches;
3158
3195
  let matchesToLoad = requestMatches.filter(m => m.route.loader || m.route.lazy);
3159
3196
  // Short circuit if we have no loaders to run (query())
3160
3197
  if (matchesToLoad.length === 0) {
@@ -3164,23 +3201,21 @@ function createStaticHandler(routes, opts) {
3164
3201
  loaderData: matches.reduce((acc, m) => Object.assign(acc, {
3165
3202
  [m.route.id]: null
3166
3203
  }), {}),
3167
- errors: pendingActionError || null,
3204
+ errors: pendingActionResult && isErrorResult(pendingActionResult[1]) ? {
3205
+ [pendingActionResult[0]]: pendingActionResult[1].error
3206
+ } : null,
3168
3207
  statusCode: 200,
3169
3208
  loaderHeaders: {},
3170
3209
  activeDeferreds: null
3171
3210
  };
3172
3211
  }
3173
- let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath, {
3174
- isStaticRequest: true,
3175
- isRouteRequest,
3176
- requestContext
3177
- }))]);
3212
+ let results = await callDataStrategy("loader", request, matchesToLoad, matches, isRouteRequest, requestContext, unstable_dataStrategy);
3178
3213
  if (request.signal.aborted) {
3179
3214
  throwStaticHandlerAbortedError(request, isRouteRequest, future);
3180
3215
  }
3181
3216
  // Process and commit output from loaders
3182
3217
  let activeDeferreds = new Map();
3183
- let context = processRouteLoaderData(matches, matchesToLoad, results, pendingActionError, activeDeferreds);
3218
+ let context = processRouteLoaderData(matches, matchesToLoad, results, pendingActionResult, activeDeferreds, skipLoaderErrorBubbling);
3184
3219
  // Add a null for any non-loader matches for proper revalidation on the client
3185
3220
  let executedLoaders = new Set(matchesToLoad.map(match => match.route.id));
3186
3221
  matches.forEach(match => {
@@ -3193,6 +3228,24 @@ function createStaticHandler(routes, opts) {
3193
3228
  activeDeferreds: activeDeferreds.size > 0 ? Object.fromEntries(activeDeferreds.entries()) : null
3194
3229
  });
3195
3230
  }
3231
+ // Utility wrapper for calling dataStrategy server-side without having to
3232
+ // pass around the manifest, mapRouteProperties, etc.
3233
+ async function callDataStrategy(type, request, matchesToLoad, matches, isRouteRequest, requestContext, unstable_dataStrategy) {
3234
+ let results = await callDataStrategyImpl(unstable_dataStrategy || defaultDataStrategy, type, request, matchesToLoad, matches, manifest, mapRouteProperties, requestContext);
3235
+ return await Promise.all(results.map((result, i) => {
3236
+ if (isRedirectHandlerResult(result)) {
3237
+ let response = result.result;
3238
+ // Throw redirects and let the server handle them with an HTTP redirect
3239
+ throw normalizeRelativeRoutingRedirectResponse(response, request, matchesToLoad[i].route.id, matches, basename, future.v7_relativeSplatPath);
3240
+ }
3241
+ if (isResponse(result.result) && isRouteRequest) {
3242
+ // For SSR single-route requests, we want to hand Responses back
3243
+ // directly without unwrapping
3244
+ throw result;
3245
+ }
3246
+ return convertHandlerResultToDataResult(result);
3247
+ }));
3248
+ }
3196
3249
  return {
3197
3250
  dataRoutes,
3198
3251
  query,
@@ -3402,13 +3455,18 @@ function getLoaderMatchesUntilBoundary(matches, boundaryId) {
3402
3455
  }
3403
3456
  return boundaryMatches;
3404
3457
  }
3405
- function getMatchesToLoad(history, state, matches, submission, location, isInitialLoad, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionData, pendingError) {
3406
- let actionResult = pendingError ? Object.values(pendingError)[0] : pendingActionData ? Object.values(pendingActionData)[0] : undefined;
3458
+ function getMatchesToLoad(history, state, matches, submission, location, isInitialLoad, skipActionErrorRevalidation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionResult) {
3459
+ let actionResult = pendingActionResult ? isErrorResult(pendingActionResult[1]) ? pendingActionResult[1].error : pendingActionResult[1].data : undefined;
3407
3460
  let currentUrl = history.createURL(state.location);
3408
3461
  let nextUrl = history.createURL(location);
3409
3462
  // Pick navigation matches that are net-new or qualify for revalidation
3410
- let boundaryId = pendingError ? Object.keys(pendingError)[0] : undefined;
3411
- let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId);
3463
+ let boundaryId = pendingActionResult && isErrorResult(pendingActionResult[1]) ? pendingActionResult[0] : undefined;
3464
+ let boundaryMatches = boundaryId ? getLoaderMatchesUntilBoundary(matches, boundaryId) : matches;
3465
+ // Don't revalidate loaders by default after action 4xx/5xx responses
3466
+ // when the flag is enabled. They can still opt-into revalidation via
3467
+ // `shouldRevalidate` via `actionResult`
3468
+ let actionStatus = pendingActionResult ? pendingActionResult[1].statusCode : undefined;
3469
+ let shouldSkipRevalidation = skipActionErrorRevalidation && actionStatus && actionStatus >= 400;
3412
3470
  let navigationMatches = boundaryMatches.filter((match, index) => {
3413
3471
  let {
3414
3472
  route
@@ -3421,7 +3479,7 @@ function getMatchesToLoad(history, state, matches, submission, location, isIniti
3421
3479
  return false;
3422
3480
  }
3423
3481
  if (isInitialLoad) {
3424
- if (route.loader.hydrate) {
3482
+ if (typeof route.loader !== "function" || route.loader.hydrate) {
3425
3483
  return true;
3426
3484
  }
3427
3485
  return state.loaderData[route.id] === undefined && (
@@ -3445,11 +3503,10 @@ function getMatchesToLoad(history, state, matches, submission, location, isIniti
3445
3503
  nextParams: nextRouteMatch.params
3446
3504
  }, submission, {
3447
3505
  actionResult,
3448
- defaultShouldRevalidate:
3506
+ unstable_actionStatus: actionStatus,
3507
+ defaultShouldRevalidate: shouldSkipRevalidation ? false :
3449
3508
  // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate
3450
- isRevalidationRequired ||
3451
- // Clicked the same link, resubmitted a GET form
3452
- currentUrl.pathname + currentUrl.search === nextUrl.pathname + nextUrl.search ||
3509
+ isRevalidationRequired || currentUrl.pathname + currentUrl.search === nextUrl.pathname + nextUrl.search ||
3453
3510
  // Search params affect all loaders
3454
3511
  currentUrl.search !== nextUrl.search || isNewRouteInstance(currentRouteMatch, nextRouteMatch)
3455
3512
  }));
@@ -3508,7 +3565,8 @@ function getMatchesToLoad(history, state, matches, submission, location, isIniti
3508
3565
  nextParams: matches[matches.length - 1].params
3509
3566
  }, submission, {
3510
3567
  actionResult,
3511
- defaultShouldRevalidate: isRevalidationRequired
3568
+ unstable_actionStatus: actionStatus,
3569
+ defaultShouldRevalidate: shouldSkipRevalidation ? false : isRevalidationRequired
3512
3570
  }));
3513
3571
  }
3514
3572
  if (shouldRevalidate) {
@@ -3603,24 +3661,87 @@ async function loadLazyRouteModule(route, mapRouteProperties, manifest) {
3603
3661
  lazy: undefined
3604
3662
  }));
3605
3663
  }
3606
- async function callLoaderOrAction(type, request, match, matches, manifest, mapRouteProperties, basename, v7_relativeSplatPath, opts) {
3607
- if (opts === void 0) {
3608
- opts = {};
3609
- }
3610
- let resultType;
3664
+ // Default implementation of `dataStrategy` which fetches all loaders in parallel
3665
+ function defaultDataStrategy(opts) {
3666
+ return Promise.all(opts.matches.map(m => m.resolve()));
3667
+ }
3668
+ async function callDataStrategyImpl(dataStrategyImpl, type, request, matchesToLoad, matches, manifest, mapRouteProperties, requestContext) {
3669
+ let routeIdsToLoad = matchesToLoad.reduce((acc, m) => acc.add(m.route.id), new Set());
3670
+ let loadedMatches = new Set();
3671
+ // Send all matches here to allow for a middleware-type implementation.
3672
+ // handler will be a no-op for unneeded routes and we filter those results
3673
+ // back out below.
3674
+ let results = await dataStrategyImpl({
3675
+ matches: matches.map(match => {
3676
+ let shouldLoad = routeIdsToLoad.has(match.route.id);
3677
+ // `resolve` encapsulates the route.lazy, executing the
3678
+ // loader/action, and mapping return values/thrown errors to a
3679
+ // HandlerResult. Users can pass a callback to take fine-grained control
3680
+ // over the execution of the loader/action
3681
+ let resolve = handlerOverride => {
3682
+ loadedMatches.add(match.route.id);
3683
+ return shouldLoad ? callLoaderOrAction(type, request, match, manifest, mapRouteProperties, handlerOverride, requestContext) : Promise.resolve({
3684
+ type: ResultType.data,
3685
+ result: undefined
3686
+ });
3687
+ };
3688
+ return _extends({}, match, {
3689
+ shouldLoad,
3690
+ resolve
3691
+ });
3692
+ }),
3693
+ request,
3694
+ params: matches[0].params,
3695
+ context: requestContext
3696
+ });
3697
+ // Throw if any loadRoute implementations not called since they are what
3698
+ // ensures a route is fully loaded
3699
+ 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."));
3700
+ // Filter out any middleware-only matches for which we didn't need to run handlers
3701
+ return results.filter((_, i) => routeIdsToLoad.has(matches[i].route.id));
3702
+ }
3703
+ // Default logic for calling a loader/action is the user has no specified a dataStrategy
3704
+ async function callLoaderOrAction(type, request, match, manifest, mapRouteProperties, handlerOverride, staticContext) {
3611
3705
  let result;
3612
3706
  let onReject;
3613
3707
  let runHandler = handler => {
3614
3708
  // Setup a promise we can race against so that abort signals short circuit
3615
3709
  let reject;
3710
+ // This will never resolve so safe to type it as Promise<HandlerResult> to
3711
+ // satisfy the function return value
3616
3712
  let abortPromise = new Promise((_, r) => reject = r);
3617
3713
  onReject = () => reject();
3618
3714
  request.signal.addEventListener("abort", onReject);
3619
- return Promise.race([handler({
3620
- request,
3621
- params: match.params,
3622
- context: opts.requestContext
3623
- }), abortPromise]);
3715
+ let actualHandler = ctx => {
3716
+ if (typeof handler !== "function") {
3717
+ return Promise.reject(new Error("You cannot call the handler for a route which defines a boolean " + ("\"" + type + "\" [routeId: " + match.route.id + "]")));
3718
+ }
3719
+ return handler({
3720
+ request,
3721
+ params: match.params,
3722
+ context: staticContext
3723
+ }, ...(ctx !== undefined ? [ctx] : []));
3724
+ };
3725
+ let handlerPromise;
3726
+ if (handlerOverride) {
3727
+ handlerPromise = handlerOverride(ctx => actualHandler(ctx));
3728
+ } else {
3729
+ handlerPromise = (async () => {
3730
+ try {
3731
+ let val = await actualHandler();
3732
+ return {
3733
+ type: "data",
3734
+ result: val
3735
+ };
3736
+ } catch (e) {
3737
+ return {
3738
+ type: "error",
3739
+ result: e
3740
+ };
3741
+ }
3742
+ })();
3743
+ }
3744
+ return Promise.race([handlerPromise, abortPromise]);
3624
3745
  };
3625
3746
  try {
3626
3747
  let handler = match.route[type];
@@ -3628,23 +3749,23 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
3628
3749
  if (handler) {
3629
3750
  // Run statically defined handler in parallel with lazy()
3630
3751
  let handlerError;
3631
- let values = await Promise.all([
3752
+ let [value] = await Promise.all([
3632
3753
  // If the handler throws, don't let it immediately bubble out,
3633
3754
  // since we need to let the lazy() execution finish so we know if this
3634
3755
  // route has a boundary that can handle the error
3635
3756
  runHandler(handler).catch(e => {
3636
3757
  handlerError = e;
3637
3758
  }), loadLazyRouteModule(match.route, mapRouteProperties, manifest)]);
3638
- if (handlerError) {
3759
+ if (handlerError !== undefined) {
3639
3760
  throw handlerError;
3640
3761
  }
3641
- result = values[0];
3762
+ result = value;
3642
3763
  } else {
3643
3764
  // Load lazy route module, then run any returned handler
3644
3765
  await loadLazyRouteModule(match.route, mapRouteProperties, manifest);
3645
3766
  handler = match.route[type];
3646
3767
  if (handler) {
3647
- // Handler still run even if we got interrupted to maintain consistency
3768
+ // Handler still runs even if we got interrupted to maintain consistency
3648
3769
  // with un-abortable behavior of handler execution on non-lazy or
3649
3770
  // previously-lazy-loaded routes
3650
3771
  result = await runHandler(handler);
@@ -3661,7 +3782,7 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
3661
3782
  // hit the invariant below that errors on returning undefined.
3662
3783
  return {
3663
3784
  type: ResultType.data,
3664
- data: undefined
3785
+ result: undefined
3665
3786
  };
3666
3787
  }
3667
3788
  }
@@ -3674,61 +3795,29 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
3674
3795
  } else {
3675
3796
  result = await runHandler(handler);
3676
3797
  }
3677
- 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`.");
3798
+ 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`.");
3678
3799
  } catch (e) {
3679
- resultType = ResultType.error;
3680
- result = e;
3800
+ // We should already be catching and converting normal handler executions to
3801
+ // HandlerResults and returning them, so anything that throws here is an
3802
+ // unexpected error we still need to wrap
3803
+ return {
3804
+ type: ResultType.error,
3805
+ result: e
3806
+ };
3681
3807
  } finally {
3682
3808
  if (onReject) {
3683
3809
  request.signal.removeEventListener("abort", onReject);
3684
3810
  }
3685
3811
  }
3812
+ return result;
3813
+ }
3814
+ async function convertHandlerResultToDataResult(handlerResult) {
3815
+ let {
3816
+ result,
3817
+ type,
3818
+ status
3819
+ } = handlerResult;
3686
3820
  if (isResponse(result)) {
3687
- let status = result.status;
3688
- // Process redirects
3689
- if (redirectStatusCodes.has(status)) {
3690
- let location = result.headers.get("Location");
3691
- invariant(location, "Redirects returned/thrown from loaders/actions must have a Location header");
3692
- // Support relative routing in internal redirects
3693
- if (!ABSOLUTE_URL_REGEX.test(location)) {
3694
- location = normalizeTo(new URL(request.url), matches.slice(0, matches.indexOf(match) + 1), basename, true, location, v7_relativeSplatPath);
3695
- } else if (!opts.isStaticRequest) {
3696
- // Strip off the protocol+origin for same-origin + same-basename absolute
3697
- // redirects. If this is a static request, we can let it go back to the
3698
- // browser as-is
3699
- let currentUrl = new URL(request.url);
3700
- let url = location.startsWith("//") ? new URL(currentUrl.protocol + location) : new URL(location);
3701
- let isSameBasename = stripBasename(url.pathname, basename) != null;
3702
- if (url.origin === currentUrl.origin && isSameBasename) {
3703
- location = url.pathname + url.search + url.hash;
3704
- }
3705
- }
3706
- // Don't process redirects in the router during static requests requests.
3707
- // Instead, throw the Response and let the server handle it with an HTTP
3708
- // redirect. We also update the Location header in place in this flow so
3709
- // basename and relative routing is taken into account
3710
- if (opts.isStaticRequest) {
3711
- result.headers.set("Location", location);
3712
- throw result;
3713
- }
3714
- return {
3715
- type: ResultType.redirect,
3716
- status,
3717
- location,
3718
- revalidate: result.headers.get("X-Remix-Revalidate") !== null,
3719
- reloadDocument: result.headers.get("X-Remix-Reload-Document") !== null
3720
- };
3721
- }
3722
- // For SSR single-route requests, we want to hand Responses back directly
3723
- // without unwrapping. We do this with the QueryRouteResponse wrapper
3724
- // interface so we can know whether it was returned or thrown
3725
- if (opts.isRouteRequest) {
3726
- let queryRouteResponse = {
3727
- type: resultType === ResultType.error ? ResultType.error : ResultType.data,
3728
- response: result
3729
- };
3730
- throw queryRouteResponse;
3731
- }
3732
3821
  let data;
3733
3822
  try {
3734
3823
  let contentType = result.headers.get("Content-Type");
@@ -3749,10 +3838,11 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
3749
3838
  error: e
3750
3839
  };
3751
3840
  }
3752
- if (resultType === ResultType.error) {
3841
+ if (type === ResultType.error) {
3753
3842
  return {
3754
- type: resultType,
3755
- error: new ErrorResponseImpl(status, result.statusText, data),
3843
+ type: ResultType.error,
3844
+ error: new ErrorResponseImpl(result.status, result.statusText, data),
3845
+ statusCode: result.status,
3756
3846
  headers: result.headers
3757
3847
  };
3758
3848
  }
@@ -3763,10 +3853,11 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
3763
3853
  headers: result.headers
3764
3854
  };
3765
3855
  }
3766
- if (resultType === ResultType.error) {
3856
+ if (type === ResultType.error) {
3767
3857
  return {
3768
- type: resultType,
3769
- error: result
3858
+ type: ResultType.error,
3859
+ error: result,
3860
+ statusCode: isRouteErrorResponse(result) ? result.status : status
3770
3861
  };
3771
3862
  }
3772
3863
  if (isDeferredData(result)) {
@@ -3780,9 +3871,33 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
3780
3871
  }
3781
3872
  return {
3782
3873
  type: ResultType.data,
3783
- data: result
3874
+ data: result,
3875
+ statusCode: status
3784
3876
  };
3785
3877
  }
3878
+ // Support relative routing in internal redirects
3879
+ function normalizeRelativeRoutingRedirectResponse(response, request, routeId, matches, basename, v7_relativeSplatPath) {
3880
+ let location = response.headers.get("Location");
3881
+ invariant(location, "Redirects returned/thrown from loaders/actions must have a Location header");
3882
+ if (!ABSOLUTE_URL_REGEX.test(location)) {
3883
+ let trimmedMatches = matches.slice(0, matches.findIndex(m => m.route.id === routeId) + 1);
3884
+ location = normalizeTo(new URL(request.url), trimmedMatches, basename, true, location, v7_relativeSplatPath);
3885
+ response.headers.set("Location", location);
3886
+ }
3887
+ return response;
3888
+ }
3889
+ function normalizeRedirectLocation(location, currentUrl, basename) {
3890
+ if (ABSOLUTE_URL_REGEX.test(location)) {
3891
+ // Strip off the protocol+origin for same-origin + same-basename absolute redirects
3892
+ let normalizedLocation = location;
3893
+ let url = normalizedLocation.startsWith("//") ? new URL(currentUrl.protocol + normalizedLocation) : new URL(normalizedLocation);
3894
+ let isSameBasename = stripBasename(url.pathname, basename) != null;
3895
+ if (url.origin === currentUrl.origin && isSameBasename) {
3896
+ return url.pathname + url.search + url.hash;
3897
+ }
3898
+ }
3899
+ return location;
3900
+ }
3786
3901
  // Utility method for creating the Request instances for loaders/actions during
3787
3902
  // client-side navigations and fetches. During SSR we will always have a
3788
3903
  // Request instance from the static handler (query/queryRoute)
@@ -3833,33 +3948,38 @@ function convertSearchParamsToFormData(searchParams) {
3833
3948
  }
3834
3949
  return formData;
3835
3950
  }
3836
- function processRouteLoaderData(matches, matchesToLoad, results, pendingError, activeDeferreds) {
3951
+ function processRouteLoaderData(matches, matchesToLoad, results, pendingActionResult, activeDeferreds, skipLoaderErrorBubbling) {
3837
3952
  // Fill in loaderData/errors from our loaders
3838
3953
  let loaderData = {};
3839
3954
  let errors = null;
3840
3955
  let statusCode;
3841
3956
  let foundError = false;
3842
3957
  let loaderHeaders = {};
3958
+ let pendingError = pendingActionResult && isErrorResult(pendingActionResult[1]) ? pendingActionResult[1].error : undefined;
3843
3959
  // Process loader results into state.loaderData/state.errors
3844
3960
  results.forEach((result, index) => {
3845
3961
  let id = matchesToLoad[index].route.id;
3846
3962
  invariant(!isRedirectResult(result), "Cannot handle redirect results in processLoaderData");
3847
3963
  if (isErrorResult(result)) {
3848
- // Look upwards from the matched route for the closest ancestor
3849
- // error boundary, defaulting to the root match
3850
- let boundaryMatch = findNearestBoundary(matches, id);
3851
3964
  let error = result.error;
3852
3965
  // If we have a pending action error, we report it at the highest-route
3853
3966
  // that throws a loader error, and then clear it out to indicate that
3854
3967
  // it was consumed
3855
- if (pendingError) {
3856
- error = Object.values(pendingError)[0];
3968
+ if (pendingError !== undefined) {
3969
+ error = pendingError;
3857
3970
  pendingError = undefined;
3858
3971
  }
3859
3972
  errors = errors || {};
3860
- // Prefer higher error values if lower errors bubble to the same boundary
3861
- if (errors[boundaryMatch.route.id] == null) {
3862
- errors[boundaryMatch.route.id] = error;
3973
+ if (skipLoaderErrorBubbling) {
3974
+ errors[id] = error;
3975
+ } else {
3976
+ // Look upwards from the matched route for the closest ancestor error
3977
+ // boundary, defaulting to the root match. Prefer higher error values
3978
+ // if lower errors bubble to the same boundary
3979
+ let boundaryMatch = findNearestBoundary(matches, id);
3980
+ if (errors[boundaryMatch.route.id] == null) {
3981
+ errors[boundaryMatch.route.id] = error;
3982
+ }
3863
3983
  }
3864
3984
  // Clear our any prior loaderData for the throwing route
3865
3985
  loaderData[id] = undefined;
@@ -3876,25 +3996,35 @@ function processRouteLoaderData(matches, matchesToLoad, results, pendingError, a
3876
3996
  if (isDeferredResult(result)) {
3877
3997
  activeDeferreds.set(id, result.deferredData);
3878
3998
  loaderData[id] = result.deferredData.data;
3999
+ // Error status codes always override success status codes, but if all
4000
+ // loaders are successful we take the deepest status code.
4001
+ if (result.statusCode != null && result.statusCode !== 200 && !foundError) {
4002
+ statusCode = result.statusCode;
4003
+ }
4004
+ if (result.headers) {
4005
+ loaderHeaders[id] = result.headers;
4006
+ }
3879
4007
  } else {
3880
4008
  loaderData[id] = result.data;
3881
- }
3882
- // Error status codes always override success status codes, but if all
3883
- // loaders are successful we take the deepest status code.
3884
- if (result.statusCode != null && result.statusCode !== 200 && !foundError) {
3885
- statusCode = result.statusCode;
3886
- }
3887
- if (result.headers) {
3888
- loaderHeaders[id] = result.headers;
4009
+ // Error status codes always override success status codes, but if all
4010
+ // loaders are successful we take the deepest status code.
4011
+ if (result.statusCode && result.statusCode !== 200 && !foundError) {
4012
+ statusCode = result.statusCode;
4013
+ }
4014
+ if (result.headers) {
4015
+ loaderHeaders[id] = result.headers;
4016
+ }
3889
4017
  }
3890
4018
  }
3891
4019
  });
3892
4020
  // If we didn't consume the pending action error (i.e., all loaders
3893
4021
  // resolved), then consume it here. Also clear out any loaderData for the
3894
4022
  // throwing route
3895
- if (pendingError) {
3896
- errors = pendingError;
3897
- loaderData[Object.keys(pendingError)[0]] = undefined;
4023
+ if (pendingError !== undefined && pendingActionResult) {
4024
+ errors = {
4025
+ [pendingActionResult[0]]: pendingError
4026
+ };
4027
+ loaderData[pendingActionResult[0]] = undefined;
3898
4028
  }
3899
4029
  return {
3900
4030
  loaderData,
@@ -3903,11 +4033,12 @@ function processRouteLoaderData(matches, matchesToLoad, results, pendingError, a
3903
4033
  loaderHeaders
3904
4034
  };
3905
4035
  }
3906
- function processLoaderData(state, matches, matchesToLoad, results, pendingError, revalidatingFetchers, fetcherResults, activeDeferreds) {
4036
+ function processLoaderData(state, matches, matchesToLoad, results, pendingActionResult, revalidatingFetchers, fetcherResults, activeDeferreds) {
3907
4037
  let {
3908
4038
  loaderData,
3909
4039
  errors
3910
- } = processRouteLoaderData(matches, matchesToLoad, results, pendingError, activeDeferreds);
4040
+ } = processRouteLoaderData(matches, matchesToLoad, results, pendingActionResult, activeDeferreds, false // This method is only called client side so we always want to bubble
4041
+ );
3911
4042
  // Process results from our revalidating fetchers
3912
4043
  for (let index = 0; index < revalidatingFetchers.length; index++) {
3913
4044
  let {
@@ -3967,6 +4098,19 @@ function mergeLoaderData(loaderData, newLoaderData, matches, errors) {
3967
4098
  }
3968
4099
  return mergedLoaderData;
3969
4100
  }
4101
+ function getActionDataForCommit(pendingActionResult) {
4102
+ if (!pendingActionResult) {
4103
+ return {};
4104
+ }
4105
+ return isErrorResult(pendingActionResult[1]) ? {
4106
+ // Clear out prior actionData on errors
4107
+ actionData: {}
4108
+ } : {
4109
+ actionData: {
4110
+ [pendingActionResult[0]]: pendingActionResult[1].data
4111
+ }
4112
+ };
4113
+ }
3970
4114
  // Find the nearest error boundary, looking upwards from the leaf route (or the
3971
4115
  // route specified by routeId) for the closest ancestor error boundary,
3972
4116
  // defaulting to the root match
@@ -4059,6 +4203,12 @@ function isHashChangeOnly(a, b) {
4059
4203
  // /page#hash -> /page
4060
4204
  return false;
4061
4205
  }
4206
+ function isHandlerResult(result) {
4207
+ return result != null && typeof result === "object" && "type" in result && "result" in result && (result.type === ResultType.data || result.type === ResultType.error);
4208
+ }
4209
+ function isRedirectHandlerResult(result) {
4210
+ return isResponse(result.result) && redirectStatusCodes.has(result.result.status);
4211
+ }
4062
4212
  function isDeferredResult(result) {
4063
4213
  return result.type === ResultType.deferred;
4064
4214
  }
@@ -4083,9 +4233,6 @@ function isRedirectResponse(result) {
4083
4233
  let location = result.headers.get("Location");
4084
4234
  return status >= 300 && status <= 399 && location != null;
4085
4235
  }
4086
- function isQueryRouteResponse(obj) {
4087
- return obj && isResponse(obj.response) && (obj.type === ResultType.data || obj.type === ResultType.error);
4088
- }
4089
4236
  function isValidMethod(method) {
4090
4237
  return validRequestMethods.has(method.toLowerCase());
4091
4238
  }