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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.0
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,25 @@ 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.loadRouteIds` is an optional array of routeIds to run only a subset of
2897
+ * loaders during a query() call
2898
+ * - `opts.requestContext` is an optional server context that will be passed
2899
+ * to actions/loaders in the `context` parameter
2900
+ * - `opts.skipLoaderErrorBubbling` is an optional parameter that will prevent
2901
+ * the bubbling of errors which allows single-fetch-type implementations
2902
+ * where the client will handle the bubbling and we may need to return data
2903
+ * for the handling route
2904
+ * - `opts.skipLoaders` is an optional parameter that will prevent loaders
2905
+ * from running after an action
2869
2906
  */
2870
2907
  async function query(request, _temp3) {
2871
2908
  let {
2872
- requestContext
2909
+ loadRouteIds,
2910
+ requestContext,
2911
+ skipLoaderErrorBubbling,
2912
+ skipLoaders,
2913
+ unstable_dataStrategy
2873
2914
  } = _temp3 === void 0 ? {} : _temp3;
2874
2915
  let url = new URL(request.url);
2875
2916
  let method = request.method;
@@ -2921,7 +2962,7 @@ function createStaticHandler(routes, opts) {
2921
2962
  activeDeferreds: null
2922
2963
  };
2923
2964
  }
2924
- let result = await queryImpl(request, location, matches, requestContext);
2965
+ let result = await queryImpl(request, location, matches, requestContext, unstable_dataStrategy || null, loadRouteIds || null, skipLoaderErrorBubbling === true, skipLoaders === true, null);
2925
2966
  if (isResponse(result)) {
2926
2967
  return result;
2927
2968
  }
@@ -2952,6 +2993,12 @@ function createStaticHandler(routes, opts) {
2952
2993
  * serialize the error as they see fit while including the proper response
2953
2994
  * code. Examples here are 404 and 405 errors that occur prior to reaching
2954
2995
  * any user-defined loaders.
2996
+ *
2997
+ * - `opts.routeId` allows you to specify the specific route handler to call.
2998
+ * If not provided the handler will determine the proper route by matching
2999
+ * against `request.url`
3000
+ * - `opts.requestContext` is an optional server context that will be passed
3001
+ * to actions/loaders in the `context` parameter
2955
3002
  */
2956
3003
  async function queryRoute(request, _temp4) {
2957
3004
  let {
@@ -2984,7 +3031,7 @@ function createStaticHandler(routes, opts) {
2984
3031
  pathname: location.pathname
2985
3032
  });
2986
3033
  }
2987
- let result = await queryImpl(request, location, matches, requestContext, match);
3034
+ let result = await queryImpl(request, location, matches, requestContext, null, null, false, false, match);
2988
3035
  if (isResponse(result)) {
2989
3036
  return result;
2990
3037
  }
@@ -3010,27 +3057,27 @@ function createStaticHandler(routes, opts) {
3010
3057
  }
3011
3058
  return undefined;
3012
3059
  }
3013
- async function queryImpl(request, location, matches, requestContext, routeMatch) {
3060
+ async function queryImpl(request, location, matches, requestContext, unstable_dataStrategy, loadRouteIds, skipLoaderErrorBubbling, skipLoaders, routeMatch) {
3014
3061
  invariant(request.signal, "query()/queryRoute() requests must contain an AbortController signal");
3015
3062
  try {
3016
3063
  if (isMutationMethod(request.method.toLowerCase())) {
3017
- let result = await submit(request, matches, routeMatch || getTargetMatch(matches, location), requestContext, routeMatch != null);
3064
+ let result = await submit(request, matches, routeMatch || getTargetMatch(matches, location), requestContext, unstable_dataStrategy, loadRouteIds, skipLoaderErrorBubbling, skipLoaders, routeMatch != null);
3018
3065
  return result;
3019
3066
  }
3020
- let result = await loadRouteData(request, matches, requestContext, routeMatch);
3067
+ let result = await loadRouteData(request, matches, requestContext, unstable_dataStrategy, loadRouteIds, skipLoaderErrorBubbling, routeMatch);
3021
3068
  return isResponse(result) ? result : _extends({}, result, {
3022
3069
  actionData: null,
3023
3070
  actionHeaders: {}
3024
3071
  });
3025
3072
  } 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)) {
3073
+ // If the user threw/returned a Response in callLoaderOrAction for a
3074
+ // `queryRoute` call, we throw the `HandlerResult` to bail out early
3075
+ // and then return or throw the raw Response here accordingly
3076
+ if (isHandlerResult(e) && isResponse(e.result)) {
3030
3077
  if (e.type === ResultType.error) {
3031
- throw e.response;
3078
+ throw e.result;
3032
3079
  }
3033
- return e.response;
3080
+ return e.result;
3034
3081
  }
3035
3082
  // Redirects are always returned since they don't propagate to catch
3036
3083
  // boundaries
@@ -3040,7 +3087,7 @@ function createStaticHandler(routes, opts) {
3040
3087
  throw e;
3041
3088
  }
3042
3089
  }
3043
- async function submit(request, matches, actionMatch, requestContext, isRouteRequest) {
3090
+ async function submit(request, matches, actionMatch, requestContext, unstable_dataStrategy, loadRouteIds, skipLoaderErrorBubbling, skipLoaders, isRouteRequest) {
3044
3091
  let result;
3045
3092
  if (!actionMatch.route.action && !actionMatch.route.lazy) {
3046
3093
  let error = getInternalRouterError(405, {
@@ -3056,11 +3103,8 @@ function createStaticHandler(routes, opts) {
3056
3103
  error
3057
3104
  };
3058
3105
  } else {
3059
- result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath, {
3060
- isStaticRequest: true,
3061
- isRouteRequest,
3062
- requestContext
3063
- });
3106
+ let results = await callDataStrategy("action", request, [actionMatch], matches, isRouteRequest, requestContext, unstable_dataStrategy);
3107
+ result = results[0];
3064
3108
  if (request.signal.aborted) {
3065
3109
  throwStaticHandlerAbortedError(request, isRouteRequest, future);
3066
3110
  }
@@ -3071,9 +3115,9 @@ function createStaticHandler(routes, opts) {
3071
3115
  // can get back on the "throw all redirect responses" train here should
3072
3116
  // this ever happen :/
3073
3117
  throw new Response(null, {
3074
- status: result.status,
3118
+ status: result.response.status,
3075
3119
  headers: {
3076
- Location: result.location
3120
+ Location: result.response.headers.get("Location")
3077
3121
  }
3078
3122
  });
3079
3123
  }
@@ -3110,41 +3154,73 @@ function createStaticHandler(routes, opts) {
3110
3154
  activeDeferreds: null
3111
3155
  };
3112
3156
  }
3157
+ // Create a GET request for the loaders
3158
+ let loaderRequest = new Request(request.url, {
3159
+ headers: request.headers,
3160
+ redirect: request.redirect,
3161
+ signal: request.signal
3162
+ });
3113
3163
  if (isErrorResult(result)) {
3114
3164
  // Store off the pending error - we use it to determine which loaders
3115
3165
  // 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
- });
3166
+ let boundaryMatch = skipLoaderErrorBubbling ? actionMatch : findNearestBoundary(matches, actionMatch.route.id);
3167
+ let statusCode = isRouteErrorResponse(result.error) ? result.error.status : result.statusCode != null ? result.statusCode : 500;
3168
+ let actionHeaders = _extends({}, result.headers ? {
3169
+ [actionMatch.route.id]: result.headers
3170
+ } : {});
3171
+ if (skipLoaders) {
3172
+ return {
3173
+ matches,
3174
+ loaderData: {},
3175
+ actionData: {},
3176
+ errors: {
3177
+ [boundaryMatch.route.id]: result.error
3178
+ },
3179
+ statusCode,
3180
+ loaderHeaders: {},
3181
+ actionHeaders,
3182
+ activeDeferreds: null
3183
+ };
3184
+ }
3185
+ let context = await loadRouteData(loaderRequest, matches, requestContext, unstable_dataStrategy, loadRouteIds, skipLoaderErrorBubbling, null, [boundaryMatch.route.id, result]);
3120
3186
  // action status codes take precedence over loader status codes
3121
3187
  return _extends({}, context, {
3122
- statusCode: isRouteErrorResponse(result.error) ? result.error.status : 500,
3188
+ statusCode,
3123
3189
  actionData: null,
3124
- actionHeaders: _extends({}, result.headers ? {
3125
- [actionMatch.route.id]: result.headers
3126
- } : {})
3190
+ actionHeaders
3127
3191
  });
3128
3192
  }
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
- } : {}, {
3193
+ let actionHeaders = result.headers ? {
3194
+ [actionMatch.route.id]: result.headers
3195
+ } : {};
3196
+ if (skipLoaders) {
3197
+ return {
3198
+ matches,
3199
+ loaderData: {},
3200
+ actionData: {
3201
+ [actionMatch.route.id]: result.data
3202
+ },
3203
+ errors: null,
3204
+ statusCode: result.statusCode || 200,
3205
+ loaderHeaders: {},
3206
+ actionHeaders,
3207
+ activeDeferreds: null
3208
+ };
3209
+ }
3210
+ let context = await loadRouteData(loaderRequest, matches, requestContext, unstable_dataStrategy, loadRouteIds, skipLoaderErrorBubbling, null);
3211
+ return _extends({}, context, {
3139
3212
  actionData: {
3140
3213
  [actionMatch.route.id]: result.data
3141
- },
3142
- actionHeaders: _extends({}, result.headers ? {
3214
+ }
3215
+ }, result.statusCode ? {
3216
+ statusCode: result.statusCode
3217
+ } : {}, {
3218
+ actionHeaders: result.headers ? {
3143
3219
  [actionMatch.route.id]: result.headers
3144
- } : {})
3220
+ } : {}
3145
3221
  });
3146
3222
  }
3147
- async function loadRouteData(request, matches, requestContext, routeMatch, pendingActionError) {
3223
+ async function loadRouteData(request, matches, requestContext, unstable_dataStrategy, loadRouteIds, skipLoaderErrorBubbling, routeMatch, pendingActionResult) {
3148
3224
  let isRouteRequest = routeMatch != null;
3149
3225
  // Short circuit if we have no loaders to run (queryRoute())
3150
3226
  if (isRouteRequest && !(routeMatch != null && routeMatch.route.loader) && !(routeMatch != null && routeMatch.route.lazy)) {
@@ -3154,8 +3230,11 @@ function createStaticHandler(routes, opts) {
3154
3230
  routeId: routeMatch == null ? void 0 : routeMatch.route.id
3155
3231
  });
3156
3232
  }
3157
- let requestMatches = routeMatch ? [routeMatch] : getLoaderMatchesUntilBoundary(matches, Object.keys(pendingActionError || {})[0]);
3233
+ let requestMatches = routeMatch ? [routeMatch] : pendingActionResult && isErrorResult(pendingActionResult[1]) ? getLoaderMatchesUntilBoundary(matches, pendingActionResult[0]) : matches;
3158
3234
  let matchesToLoad = requestMatches.filter(m => m.route.loader || m.route.lazy);
3235
+ if (loadRouteIds) {
3236
+ matchesToLoad = matchesToLoad.filter(m => loadRouteIds.includes(m.route.id));
3237
+ }
3159
3238
  // Short circuit if we have no loaders to run (query())
3160
3239
  if (matchesToLoad.length === 0) {
3161
3240
  return {
@@ -3164,23 +3243,21 @@ function createStaticHandler(routes, opts) {
3164
3243
  loaderData: matches.reduce((acc, m) => Object.assign(acc, {
3165
3244
  [m.route.id]: null
3166
3245
  }), {}),
3167
- errors: pendingActionError || null,
3246
+ errors: pendingActionResult && isErrorResult(pendingActionResult[1]) ? {
3247
+ [pendingActionResult[0]]: pendingActionResult[1].error
3248
+ } : null,
3168
3249
  statusCode: 200,
3169
3250
  loaderHeaders: {},
3170
3251
  activeDeferreds: null
3171
3252
  };
3172
3253
  }
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
- }))]);
3254
+ let results = await callDataStrategy("loader", request, matchesToLoad, matches, isRouteRequest, requestContext, unstable_dataStrategy);
3178
3255
  if (request.signal.aborted) {
3179
3256
  throwStaticHandlerAbortedError(request, isRouteRequest, future);
3180
3257
  }
3181
3258
  // Process and commit output from loaders
3182
3259
  let activeDeferreds = new Map();
3183
- let context = processRouteLoaderData(matches, matchesToLoad, results, pendingActionError, activeDeferreds);
3260
+ let context = processRouteLoaderData(matches, matchesToLoad, results, pendingActionResult, activeDeferreds, skipLoaderErrorBubbling);
3184
3261
  // Add a null for any non-loader matches for proper revalidation on the client
3185
3262
  let executedLoaders = new Set(matchesToLoad.map(match => match.route.id));
3186
3263
  matches.forEach(match => {
@@ -3193,6 +3270,24 @@ function createStaticHandler(routes, opts) {
3193
3270
  activeDeferreds: activeDeferreds.size > 0 ? Object.fromEntries(activeDeferreds.entries()) : null
3194
3271
  });
3195
3272
  }
3273
+ // Utility wrapper for calling dataStrategy server-side without having to
3274
+ // pass around the manifest, mapRouteProperties, etc.
3275
+ async function callDataStrategy(type, request, matchesToLoad, matches, isRouteRequest, requestContext, unstable_dataStrategy) {
3276
+ let results = await callDataStrategyImpl(unstable_dataStrategy || defaultDataStrategy, type, request, matchesToLoad, matches, manifest, mapRouteProperties, requestContext);
3277
+ return await Promise.all(results.map((result, i) => {
3278
+ if (isRedirectHandlerResult(result)) {
3279
+ let response = result.result;
3280
+ // Throw redirects and let the server handle them with an HTTP redirect
3281
+ throw normalizeRelativeRoutingRedirectResponse(response, request, matchesToLoad[i].route.id, matches, basename, future.v7_relativeSplatPath);
3282
+ }
3283
+ if (isResponse(result.result) && isRouteRequest) {
3284
+ // For SSR single-route requests, we want to hand Responses back
3285
+ // directly without unwrapping
3286
+ throw result;
3287
+ }
3288
+ return convertHandlerResultToDataResult(result);
3289
+ }));
3290
+ }
3196
3291
  return {
3197
3292
  dataRoutes,
3198
3293
  query,
@@ -3402,13 +3497,18 @@ function getLoaderMatchesUntilBoundary(matches, boundaryId) {
3402
3497
  }
3403
3498
  return boundaryMatches;
3404
3499
  }
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;
3500
+ function getMatchesToLoad(history, state, matches, submission, location, isInitialLoad, skipActionErrorRevalidation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionResult) {
3501
+ let actionResult = pendingActionResult ? isErrorResult(pendingActionResult[1]) ? pendingActionResult[1].error : pendingActionResult[1].data : undefined;
3407
3502
  let currentUrl = history.createURL(state.location);
3408
3503
  let nextUrl = history.createURL(location);
3409
3504
  // 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);
3505
+ let boundaryId = pendingActionResult && isErrorResult(pendingActionResult[1]) ? pendingActionResult[0] : undefined;
3506
+ let boundaryMatches = boundaryId ? getLoaderMatchesUntilBoundary(matches, boundaryId) : matches;
3507
+ // Don't revalidate loaders by default after action 4xx/5xx responses
3508
+ // when the flag is enabled. They can still opt-into revalidation via
3509
+ // `shouldRevalidate` via `actionResult`
3510
+ let actionStatus = pendingActionResult ? pendingActionResult[1].statusCode : undefined;
3511
+ let shouldSkipRevalidation = skipActionErrorRevalidation && actionStatus && actionStatus >= 400;
3412
3512
  let navigationMatches = boundaryMatches.filter((match, index) => {
3413
3513
  let {
3414
3514
  route
@@ -3421,7 +3521,7 @@ function getMatchesToLoad(history, state, matches, submission, location, isIniti
3421
3521
  return false;
3422
3522
  }
3423
3523
  if (isInitialLoad) {
3424
- if (route.loader.hydrate) {
3524
+ if (typeof route.loader !== "function" || route.loader.hydrate) {
3425
3525
  return true;
3426
3526
  }
3427
3527
  return state.loaderData[route.id] === undefined && (
@@ -3445,11 +3545,10 @@ function getMatchesToLoad(history, state, matches, submission, location, isIniti
3445
3545
  nextParams: nextRouteMatch.params
3446
3546
  }, submission, {
3447
3547
  actionResult,
3448
- defaultShouldRevalidate:
3548
+ unstable_actionStatus: actionStatus,
3549
+ defaultShouldRevalidate: shouldSkipRevalidation ? false :
3449
3550
  // 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 ||
3551
+ isRevalidationRequired || currentUrl.pathname + currentUrl.search === nextUrl.pathname + nextUrl.search ||
3453
3552
  // Search params affect all loaders
3454
3553
  currentUrl.search !== nextUrl.search || isNewRouteInstance(currentRouteMatch, nextRouteMatch)
3455
3554
  }));
@@ -3508,7 +3607,8 @@ function getMatchesToLoad(history, state, matches, submission, location, isIniti
3508
3607
  nextParams: matches[matches.length - 1].params
3509
3608
  }, submission, {
3510
3609
  actionResult,
3511
- defaultShouldRevalidate: isRevalidationRequired
3610
+ unstable_actionStatus: actionStatus,
3611
+ defaultShouldRevalidate: shouldSkipRevalidation ? false : isRevalidationRequired
3512
3612
  }));
3513
3613
  }
3514
3614
  if (shouldRevalidate) {
@@ -3603,24 +3703,87 @@ async function loadLazyRouteModule(route, mapRouteProperties, manifest) {
3603
3703
  lazy: undefined
3604
3704
  }));
3605
3705
  }
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;
3706
+ // Default implementation of `dataStrategy` which fetches all loaders in parallel
3707
+ function defaultDataStrategy(opts) {
3708
+ return Promise.all(opts.matches.map(m => m.resolve()));
3709
+ }
3710
+ async function callDataStrategyImpl(dataStrategyImpl, type, request, matchesToLoad, matches, manifest, mapRouteProperties, requestContext) {
3711
+ let routeIdsToLoad = matchesToLoad.reduce((acc, m) => acc.add(m.route.id), new Set());
3712
+ let loadedMatches = new Set();
3713
+ // Send all matches here to allow for a middleware-type implementation.
3714
+ // handler will be a no-op for unneeded routes and we filter those results
3715
+ // back out below.
3716
+ let results = await dataStrategyImpl({
3717
+ matches: matches.map(match => {
3718
+ let shouldLoad = routeIdsToLoad.has(match.route.id);
3719
+ // `resolve` encapsulates the route.lazy, executing the
3720
+ // loader/action, and mapping return values/thrown errors to a
3721
+ // HandlerResult. Users can pass a callback to take fine-grained control
3722
+ // over the execution of the loader/action
3723
+ let resolve = handlerOverride => {
3724
+ loadedMatches.add(match.route.id);
3725
+ return shouldLoad ? callLoaderOrAction(type, request, match, manifest, mapRouteProperties, handlerOverride, requestContext) : Promise.resolve({
3726
+ type: ResultType.data,
3727
+ result: undefined
3728
+ });
3729
+ };
3730
+ return _extends({}, match, {
3731
+ shouldLoad,
3732
+ resolve
3733
+ });
3734
+ }),
3735
+ request,
3736
+ params: matches[0].params,
3737
+ context: requestContext
3738
+ });
3739
+ // Throw if any loadRoute implementations not called since they are what
3740
+ // ensures a route is fully loaded
3741
+ 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."));
3742
+ // Filter out any middleware-only matches for which we didn't need to run handlers
3743
+ return results.filter((_, i) => routeIdsToLoad.has(matches[i].route.id));
3744
+ }
3745
+ // Default logic for calling a loader/action is the user has no specified a dataStrategy
3746
+ async function callLoaderOrAction(type, request, match, manifest, mapRouteProperties, handlerOverride, staticContext) {
3611
3747
  let result;
3612
3748
  let onReject;
3613
3749
  let runHandler = handler => {
3614
3750
  // Setup a promise we can race against so that abort signals short circuit
3615
3751
  let reject;
3752
+ // This will never resolve so safe to type it as Promise<HandlerResult> to
3753
+ // satisfy the function return value
3616
3754
  let abortPromise = new Promise((_, r) => reject = r);
3617
3755
  onReject = () => reject();
3618
3756
  request.signal.addEventListener("abort", onReject);
3619
- return Promise.race([handler({
3620
- request,
3621
- params: match.params,
3622
- context: opts.requestContext
3623
- }), abortPromise]);
3757
+ let actualHandler = ctx => {
3758
+ if (typeof handler !== "function") {
3759
+ return Promise.reject(new Error("You cannot call the handler for a route which defines a boolean " + ("\"" + type + "\" [routeId: " + match.route.id + "]")));
3760
+ }
3761
+ return handler({
3762
+ request,
3763
+ params: match.params,
3764
+ context: staticContext
3765
+ }, ...(ctx !== undefined ? [ctx] : []));
3766
+ };
3767
+ let handlerPromise;
3768
+ if (handlerOverride) {
3769
+ handlerPromise = handlerOverride(ctx => actualHandler(ctx));
3770
+ } else {
3771
+ handlerPromise = (async () => {
3772
+ try {
3773
+ let val = await actualHandler();
3774
+ return {
3775
+ type: "data",
3776
+ result: val
3777
+ };
3778
+ } catch (e) {
3779
+ return {
3780
+ type: "error",
3781
+ result: e
3782
+ };
3783
+ }
3784
+ })();
3785
+ }
3786
+ return Promise.race([handlerPromise, abortPromise]);
3624
3787
  };
3625
3788
  try {
3626
3789
  let handler = match.route[type];
@@ -3628,23 +3791,23 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
3628
3791
  if (handler) {
3629
3792
  // Run statically defined handler in parallel with lazy()
3630
3793
  let handlerError;
3631
- let values = await Promise.all([
3794
+ let [value] = await Promise.all([
3632
3795
  // If the handler throws, don't let it immediately bubble out,
3633
3796
  // since we need to let the lazy() execution finish so we know if this
3634
3797
  // route has a boundary that can handle the error
3635
3798
  runHandler(handler).catch(e => {
3636
3799
  handlerError = e;
3637
3800
  }), loadLazyRouteModule(match.route, mapRouteProperties, manifest)]);
3638
- if (handlerError) {
3801
+ if (handlerError !== undefined) {
3639
3802
  throw handlerError;
3640
3803
  }
3641
- result = values[0];
3804
+ result = value;
3642
3805
  } else {
3643
3806
  // Load lazy route module, then run any returned handler
3644
3807
  await loadLazyRouteModule(match.route, mapRouteProperties, manifest);
3645
3808
  handler = match.route[type];
3646
3809
  if (handler) {
3647
- // Handler still run even if we got interrupted to maintain consistency
3810
+ // Handler still runs even if we got interrupted to maintain consistency
3648
3811
  // with un-abortable behavior of handler execution on non-lazy or
3649
3812
  // previously-lazy-loaded routes
3650
3813
  result = await runHandler(handler);
@@ -3661,7 +3824,7 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
3661
3824
  // hit the invariant below that errors on returning undefined.
3662
3825
  return {
3663
3826
  type: ResultType.data,
3664
- data: undefined
3827
+ result: undefined
3665
3828
  };
3666
3829
  }
3667
3830
  }
@@ -3674,61 +3837,29 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
3674
3837
  } else {
3675
3838
  result = await runHandler(handler);
3676
3839
  }
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`.");
3840
+ 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
3841
  } catch (e) {
3679
- resultType = ResultType.error;
3680
- result = e;
3842
+ // We should already be catching and converting normal handler executions to
3843
+ // HandlerResults and returning them, so anything that throws here is an
3844
+ // unexpected error we still need to wrap
3845
+ return {
3846
+ type: ResultType.error,
3847
+ result: e
3848
+ };
3681
3849
  } finally {
3682
3850
  if (onReject) {
3683
3851
  request.signal.removeEventListener("abort", onReject);
3684
3852
  }
3685
3853
  }
3854
+ return result;
3855
+ }
3856
+ async function convertHandlerResultToDataResult(handlerResult) {
3857
+ let {
3858
+ result,
3859
+ type,
3860
+ status
3861
+ } = handlerResult;
3686
3862
  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
3863
  let data;
3733
3864
  try {
3734
3865
  let contentType = result.headers.get("Content-Type");
@@ -3749,10 +3880,11 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
3749
3880
  error: e
3750
3881
  };
3751
3882
  }
3752
- if (resultType === ResultType.error) {
3883
+ if (type === ResultType.error) {
3753
3884
  return {
3754
- type: resultType,
3755
- error: new ErrorResponseImpl(status, result.statusText, data),
3885
+ type: ResultType.error,
3886
+ error: new ErrorResponseImpl(result.status, result.statusText, data),
3887
+ statusCode: result.status,
3756
3888
  headers: result.headers
3757
3889
  };
3758
3890
  }
@@ -3763,10 +3895,11 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
3763
3895
  headers: result.headers
3764
3896
  };
3765
3897
  }
3766
- if (resultType === ResultType.error) {
3898
+ if (type === ResultType.error) {
3767
3899
  return {
3768
- type: resultType,
3769
- error: result
3900
+ type: ResultType.error,
3901
+ error: result,
3902
+ statusCode: isRouteErrorResponse(result) ? result.status : status
3770
3903
  };
3771
3904
  }
3772
3905
  if (isDeferredData(result)) {
@@ -3780,9 +3913,33 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
3780
3913
  }
3781
3914
  return {
3782
3915
  type: ResultType.data,
3783
- data: result
3916
+ data: result,
3917
+ statusCode: status
3784
3918
  };
3785
3919
  }
3920
+ // Support relative routing in internal redirects
3921
+ function normalizeRelativeRoutingRedirectResponse(response, request, routeId, matches, basename, v7_relativeSplatPath) {
3922
+ let location = response.headers.get("Location");
3923
+ invariant(location, "Redirects returned/thrown from loaders/actions must have a Location header");
3924
+ if (!ABSOLUTE_URL_REGEX.test(location)) {
3925
+ let trimmedMatches = matches.slice(0, matches.findIndex(m => m.route.id === routeId) + 1);
3926
+ location = normalizeTo(new URL(request.url), trimmedMatches, basename, true, location, v7_relativeSplatPath);
3927
+ response.headers.set("Location", location);
3928
+ }
3929
+ return response;
3930
+ }
3931
+ function normalizeRedirectLocation(location, currentUrl, basename) {
3932
+ if (ABSOLUTE_URL_REGEX.test(location)) {
3933
+ // Strip off the protocol+origin for same-origin + same-basename absolute redirects
3934
+ let normalizedLocation = location;
3935
+ let url = normalizedLocation.startsWith("//") ? new URL(currentUrl.protocol + normalizedLocation) : new URL(normalizedLocation);
3936
+ let isSameBasename = stripBasename(url.pathname, basename) != null;
3937
+ if (url.origin === currentUrl.origin && isSameBasename) {
3938
+ return url.pathname + url.search + url.hash;
3939
+ }
3940
+ }
3941
+ return location;
3942
+ }
3786
3943
  // Utility method for creating the Request instances for loaders/actions during
3787
3944
  // client-side navigations and fetches. During SSR we will always have a
3788
3945
  // Request instance from the static handler (query/queryRoute)
@@ -3833,33 +3990,38 @@ function convertSearchParamsToFormData(searchParams) {
3833
3990
  }
3834
3991
  return formData;
3835
3992
  }
3836
- function processRouteLoaderData(matches, matchesToLoad, results, pendingError, activeDeferreds) {
3993
+ function processRouteLoaderData(matches, matchesToLoad, results, pendingActionResult, activeDeferreds, skipLoaderErrorBubbling) {
3837
3994
  // Fill in loaderData/errors from our loaders
3838
3995
  let loaderData = {};
3839
3996
  let errors = null;
3840
3997
  let statusCode;
3841
3998
  let foundError = false;
3842
3999
  let loaderHeaders = {};
4000
+ let pendingError = pendingActionResult && isErrorResult(pendingActionResult[1]) ? pendingActionResult[1].error : undefined;
3843
4001
  // Process loader results into state.loaderData/state.errors
3844
4002
  results.forEach((result, index) => {
3845
4003
  let id = matchesToLoad[index].route.id;
3846
4004
  invariant(!isRedirectResult(result), "Cannot handle redirect results in processLoaderData");
3847
4005
  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
4006
  let error = result.error;
3852
4007
  // If we have a pending action error, we report it at the highest-route
3853
4008
  // that throws a loader error, and then clear it out to indicate that
3854
4009
  // it was consumed
3855
- if (pendingError) {
3856
- error = Object.values(pendingError)[0];
4010
+ if (pendingError !== undefined) {
4011
+ error = pendingError;
3857
4012
  pendingError = undefined;
3858
4013
  }
3859
4014
  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;
4015
+ if (skipLoaderErrorBubbling) {
4016
+ errors[id] = error;
4017
+ } else {
4018
+ // Look upwards from the matched route for the closest ancestor error
4019
+ // boundary, defaulting to the root match. Prefer higher error values
4020
+ // if lower errors bubble to the same boundary
4021
+ let boundaryMatch = findNearestBoundary(matches, id);
4022
+ if (errors[boundaryMatch.route.id] == null) {
4023
+ errors[boundaryMatch.route.id] = error;
4024
+ }
3863
4025
  }
3864
4026
  // Clear our any prior loaderData for the throwing route
3865
4027
  loaderData[id] = undefined;
@@ -3876,25 +4038,35 @@ function processRouteLoaderData(matches, matchesToLoad, results, pendingError, a
3876
4038
  if (isDeferredResult(result)) {
3877
4039
  activeDeferreds.set(id, result.deferredData);
3878
4040
  loaderData[id] = result.deferredData.data;
4041
+ // Error status codes always override success status codes, but if all
4042
+ // loaders are successful we take the deepest status code.
4043
+ if (result.statusCode != null && result.statusCode !== 200 && !foundError) {
4044
+ statusCode = result.statusCode;
4045
+ }
4046
+ if (result.headers) {
4047
+ loaderHeaders[id] = result.headers;
4048
+ }
3879
4049
  } else {
3880
4050
  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;
4051
+ // Error status codes always override success status codes, but if all
4052
+ // loaders are successful we take the deepest status code.
4053
+ if (result.statusCode && result.statusCode !== 200 && !foundError) {
4054
+ statusCode = result.statusCode;
4055
+ }
4056
+ if (result.headers) {
4057
+ loaderHeaders[id] = result.headers;
4058
+ }
3889
4059
  }
3890
4060
  }
3891
4061
  });
3892
4062
  // If we didn't consume the pending action error (i.e., all loaders
3893
4063
  // resolved), then consume it here. Also clear out any loaderData for the
3894
4064
  // throwing route
3895
- if (pendingError) {
3896
- errors = pendingError;
3897
- loaderData[Object.keys(pendingError)[0]] = undefined;
4065
+ if (pendingError !== undefined && pendingActionResult) {
4066
+ errors = {
4067
+ [pendingActionResult[0]]: pendingError
4068
+ };
4069
+ loaderData[pendingActionResult[0]] = undefined;
3898
4070
  }
3899
4071
  return {
3900
4072
  loaderData,
@@ -3903,11 +4075,12 @@ function processRouteLoaderData(matches, matchesToLoad, results, pendingError, a
3903
4075
  loaderHeaders
3904
4076
  };
3905
4077
  }
3906
- function processLoaderData(state, matches, matchesToLoad, results, pendingError, revalidatingFetchers, fetcherResults, activeDeferreds) {
4078
+ function processLoaderData(state, matches, matchesToLoad, results, pendingActionResult, revalidatingFetchers, fetcherResults, activeDeferreds) {
3907
4079
  let {
3908
4080
  loaderData,
3909
4081
  errors
3910
- } = processRouteLoaderData(matches, matchesToLoad, results, pendingError, activeDeferreds);
4082
+ } = processRouteLoaderData(matches, matchesToLoad, results, pendingActionResult, activeDeferreds, false // This method is only called client side so we always want to bubble
4083
+ );
3911
4084
  // Process results from our revalidating fetchers
3912
4085
  for (let index = 0; index < revalidatingFetchers.length; index++) {
3913
4086
  let {
@@ -3967,6 +4140,19 @@ function mergeLoaderData(loaderData, newLoaderData, matches, errors) {
3967
4140
  }
3968
4141
  return mergedLoaderData;
3969
4142
  }
4143
+ function getActionDataForCommit(pendingActionResult) {
4144
+ if (!pendingActionResult) {
4145
+ return {};
4146
+ }
4147
+ return isErrorResult(pendingActionResult[1]) ? {
4148
+ // Clear out prior actionData on errors
4149
+ actionData: {}
4150
+ } : {
4151
+ actionData: {
4152
+ [pendingActionResult[0]]: pendingActionResult[1].data
4153
+ }
4154
+ };
4155
+ }
3970
4156
  // Find the nearest error boundary, looking upwards from the leaf route (or the
3971
4157
  // route specified by routeId) for the closest ancestor error boundary,
3972
4158
  // defaulting to the root match
@@ -4059,6 +4245,12 @@ function isHashChangeOnly(a, b) {
4059
4245
  // /page#hash -> /page
4060
4246
  return false;
4061
4247
  }
4248
+ function isHandlerResult(result) {
4249
+ return result != null && typeof result === "object" && "type" in result && "result" in result && (result.type === ResultType.data || result.type === ResultType.error);
4250
+ }
4251
+ function isRedirectHandlerResult(result) {
4252
+ return isResponse(result.result) && redirectStatusCodes.has(result.result.status);
4253
+ }
4062
4254
  function isDeferredResult(result) {
4063
4255
  return result.type === ResultType.deferred;
4064
4256
  }
@@ -4083,9 +4275,6 @@ function isRedirectResponse(result) {
4083
4275
  let location = result.headers.get("Location");
4084
4276
  return status >= 300 && status <= 399 && location != null;
4085
4277
  }
4086
- function isQueryRouteResponse(obj) {
4087
- return obj && isResponse(obj.response) && (obj.type === ResultType.data || obj.type === ResultType.error);
4088
- }
4089
4278
  function isValidMethod(method) {
4090
4279
  return validRequestMethods.has(method.toLowerCase());
4091
4280
  }