@remix-run/router 0.0.0-experimental-e960cf1a → 0.0.0-experimental-bc2c864b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @remix-run/router v0.0.0-experimental-e960cf1a
2
+ * @remix-run/router v0.0.0-experimental-bc2c864b
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -575,6 +575,10 @@
575
575
  * Result from a loader or action - potentially successful or unsuccessful
576
576
  */
577
577
 
578
+ /**
579
+ * Result from a loader or action called via dataStrategy
580
+ */
581
+
578
582
  /**
579
583
  * Users can specify either lowercase or uppercase form methods on `<Form>`,
580
584
  * useSubmit(), `<fetcher.Form>`, etc.
@@ -891,7 +895,7 @@
891
895
  branches.sort((a, b) => a.score !== b.score ? b.score - a.score // Higher score first
892
896
  : compareIndexes(a.routesMeta.map(meta => meta.childrenIndex), b.routesMeta.map(meta => meta.childrenIndex)));
893
897
  }
894
- const paramRe = /^:\w+$/;
898
+ const paramRe = /^:[\w-]+$/;
895
899
  const dynamicSegmentValue = 3;
896
900
  const indexRouteValue = 2;
897
901
  const emptySegmentValue = 1;
@@ -981,7 +985,7 @@
981
985
  // Apply the splat
982
986
  return stringify(params[star]);
983
987
  }
984
- const keyMatch = segment.match(/^:(\w+)(\??)$/);
988
+ const keyMatch = segment.match(/^:([\w-]+)(\??)$/);
985
989
  if (keyMatch) {
986
990
  const [, key, optional] = keyMatch;
987
991
  let param = params[key];
@@ -1063,7 +1067,7 @@
1063
1067
  let regexpSource = "^" + path.replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
1064
1068
  .replace(/^\/*/, "/") // Make sure it has a leading /
1065
1069
  .replace(/[\\.*+^${}|()[\]]/g, "\\$&") // Escape special regex chars
1066
- .replace(/\/:(\w+)(\?)?/g, (_, paramName, isOptional) => {
1070
+ .replace(/\/:([\w-]+)(\?)?/g, (_, paramName, isOptional) => {
1067
1071
  params.push({
1068
1072
  paramName,
1069
1073
  isOptional: isOptional != null
@@ -1599,11 +1603,6 @@
1599
1603
  /**
1600
1604
  * Identified fetcher.load() calls that need to be revalidated
1601
1605
  */
1602
- /**
1603
- * Wrapper object to allow us to throw any response out from callLoaderOrAction
1604
- * for queryRouter while preserving whether or not it was thrown or returned
1605
- * from the loader/action
1606
- */
1607
1606
  const validMutationMethodsArr = ["post", "put", "patch", "delete"];
1608
1607
  const validMutationMethods = new Set(validMutationMethodsArr);
1609
1608
  const validRequestMethodsArr = ["get", ...validMutationMethodsArr];
@@ -1656,8 +1655,6 @@
1656
1655
  const isBrowser = typeof routerWindow !== "undefined" && typeof routerWindow.document !== "undefined" && typeof routerWindow.document.createElement !== "undefined";
1657
1656
  const isServer = !isBrowser;
1658
1657
  invariant(init.routes.length > 0, "You must provide a non-empty routes array to createRouter");
1659
- const dataStrategy = init.unstable_dataStrategy || defaultDataStrategy;
1660
- const callLoaderOrAction = createCallLoaderOrAction(dataStrategy);
1661
1658
  let mapRouteProperties;
1662
1659
  if (init.mapRouteProperties) {
1663
1660
  mapRouteProperties = init.mapRouteProperties;
@@ -1677,6 +1674,7 @@
1677
1674
  let dataRoutes = convertRoutesToDataRoutes(init.routes, mapRouteProperties, undefined, manifest);
1678
1675
  let inFlightDataRoutes;
1679
1676
  let basename = init.basename || "/";
1677
+ let dataStrategyImpl = init.unstable_dataStrategy || defaultDataStrategy;
1680
1678
  // Config driven behavior flags
1681
1679
  let future = _extends({
1682
1680
  v7_fetcherPersist: false,
@@ -1719,18 +1717,28 @@
1719
1717
  [route.id]: error
1720
1718
  };
1721
1719
  }
1722
-
1723
- // "Initialized" here really means "Can `RouterProvider` render my route tree?"
1724
- // Prior to `route.HydrateFallback`, we only had a root `fallbackElement` so we used
1725
- // `state.initialized` to render that instead of `<DataRoutes>`. Now that we
1726
- // support route level fallbacks we can always render and we'll just render
1727
- // as deep as we have data for and detect the nearest ancestor HydrateFallback
1728
- let initialized = future.v7_partialHydration ||
1729
- // All initialMatches need to be loaded before we're ready. If we have lazy
1730
- // functions around still then we'll need to run them in initialize()
1731
- !initialMatches.some(m => m.route.lazy) && (
1732
- // And we have to either have no loaders or have been provided hydrationData
1733
- !initialMatches.some(m => m.route.loader) || init.hydrationData != null);
1720
+ let initialized;
1721
+ let hasLazyRoutes = initialMatches.some(m => m.route.lazy);
1722
+ let hasLoaders = initialMatches.some(m => m.route.loader);
1723
+ if (hasLazyRoutes) {
1724
+ // All initialMatches need to be loaded before we're ready. If we have lazy
1725
+ // functions around still then we'll need to run them in initialize()
1726
+ initialized = false;
1727
+ } else if (!hasLoaders) {
1728
+ // If we've got no loaders to run, then we're good to go
1729
+ initialized = true;
1730
+ } else if (future.v7_partialHydration) {
1731
+ // If partial hydration is enabled, we're initialized so long as we were
1732
+ // provided with hydrationData for every route with a loader, and no loaders
1733
+ // were marked for explicit hydration
1734
+ let loaderData = init.hydrationData ? init.hydrationData.loaderData : null;
1735
+ let errors = init.hydrationData ? init.hydrationData.errors : null;
1736
+ initialized = initialMatches.every(m => m.route.loader && m.route.loader.hydrate !== true && (loaderData && loaderData[m.route.id] !== undefined || errors && errors[m.route.id] !== undefined));
1737
+ } else {
1738
+ // Without partial hydration - we're initialized if we were provided any
1739
+ // hydrationData - which is expected to be complete
1740
+ initialized = init.hydrationData != null;
1741
+ }
1734
1742
  let router;
1735
1743
  let state = {
1736
1744
  historyAction: init.history.action,
@@ -1897,7 +1905,7 @@
1897
1905
  // in the normal navigation flow. For SSR it's expected that lazy modules are
1898
1906
  // resolved prior to router creation since we can't go into a fallbackElement
1899
1907
  // UI for SSR'd apps
1900
- if (!state.initialized || future.v7_partialHydration && state.matches.some(m => isUnhydratedRoute(state, m.route))) {
1908
+ if (!state.initialized) {
1901
1909
  startNavigation(Action.Pop, state.location, {
1902
1910
  initialHydration: true
1903
1911
  });
@@ -2338,7 +2346,8 @@
2338
2346
  })
2339
2347
  };
2340
2348
  } else {
2341
- result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
2349
+ let results = await callDataStrategy("action", request, [actionMatch], matches);
2350
+ result = results[0];
2342
2351
  if (request.signal.aborted) {
2343
2352
  return {
2344
2353
  shortCircuited: true
@@ -2353,9 +2362,9 @@
2353
2362
  // If the user didn't explicity indicate replace behavior, replace if
2354
2363
  // we redirected to the exact same location we're currently at to avoid
2355
2364
  // double back-buttons
2356
- replace = result.location === state.location.pathname + state.location.search;
2365
+ replace = result.response.headers.get("Location") === state.location.pathname + state.location.search;
2357
2366
  }
2358
- await startRedirectNavigation(state, result, {
2367
+ await startRedirectNavigation(request, result, {
2359
2368
  submission,
2360
2369
  replace
2361
2370
  });
@@ -2478,7 +2487,7 @@
2478
2487
  let {
2479
2488
  loaderResults,
2480
2489
  fetcherResults
2481
- } = await loadDataAndMaybeResolveDeferred(state.matches, matches, matchesToLoad, revalidatingFetchers, request);
2490
+ } = await callLoadersAndMaybeResolveData(state.matches, matches, matchesToLoad, revalidatingFetchers, request);
2482
2491
  if (request.signal.aborted) {
2483
2492
  return {
2484
2493
  shortCircuited: true
@@ -2503,7 +2512,7 @@
2503
2512
  let fetcherKey = revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
2504
2513
  fetchRedirectIds.add(fetcherKey);
2505
2514
  }
2506
- await startRedirectNavigation(state, redirect.result, {
2515
+ await startRedirectNavigation(request, redirect.result, {
2507
2516
  replace
2508
2517
  });
2509
2518
  return {
@@ -2612,7 +2621,8 @@
2612
2621
  let fetchRequest = createClientSideRequest(init.history, path, abortController.signal, submission);
2613
2622
  fetchControllers.set(key, abortController);
2614
2623
  let originatingLoadId = incrementingLoadId;
2615
- let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
2624
+ let actionResults = await callDataStrategy("action", fetchRequest, [match], requestMatches);
2625
+ let actionResult = actionResults[0];
2616
2626
  if (fetchRequest.signal.aborted) {
2617
2627
  // We can delete this so long as we weren't aborted by our own fetcher
2618
2628
  // re-submit which would have put _new_ controller is in fetchControllers
@@ -2644,7 +2654,7 @@
2644
2654
  } else {
2645
2655
  fetchRedirectIds.add(key);
2646
2656
  updateFetcherState(key, getLoadingFetcher(submission));
2647
- return startRedirectNavigation(state, actionResult, {
2657
+ return startRedirectNavigation(fetchRequest, actionResult, {
2648
2658
  fetcherSubmission: submission
2649
2659
  });
2650
2660
  }
@@ -2701,7 +2711,7 @@
2701
2711
  let {
2702
2712
  loaderResults,
2703
2713
  fetcherResults
2704
- } = await loadDataAndMaybeResolveDeferred(state.matches, matches, matchesToLoad, revalidatingFetchers, revalidationRequest);
2714
+ } = await callLoadersAndMaybeResolveData(state.matches, matches, matchesToLoad, revalidatingFetchers, revalidationRequest);
2705
2715
  if (abortController.signal.aborted) {
2706
2716
  return;
2707
2717
  }
@@ -2718,7 +2728,7 @@
2718
2728
  let fetcherKey = revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
2719
2729
  fetchRedirectIds.add(fetcherKey);
2720
2730
  }
2721
- return startRedirectNavigation(state, redirect.result);
2731
+ return startRedirectNavigation(revalidationRequest, redirect.result);
2722
2732
  }
2723
2733
 
2724
2734
  // Process and commit output from loaders
@@ -2772,7 +2782,8 @@
2772
2782
  let fetchRequest = createClientSideRequest(init.history, path, abortController.signal);
2773
2783
  fetchControllers.set(key, abortController);
2774
2784
  let originatingLoadId = incrementingLoadId;
2775
- let result = await callLoaderOrAction("loader", fetchRequest, match, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
2785
+ let results = await callDataStrategy("loader", fetchRequest, [match], matches);
2786
+ let result = results[0];
2776
2787
 
2777
2788
  // Deferred isn't supported for fetcher loads, await everything and treat it
2778
2789
  // as a normal load. resolveDeferredData will return undefined if this
@@ -2807,7 +2818,7 @@
2807
2818
  return;
2808
2819
  } else {
2809
2820
  fetchRedirectIds.add(key);
2810
- await startRedirectNavigation(state, result);
2821
+ await startRedirectNavigation(fetchRequest, result);
2811
2822
  return;
2812
2823
  }
2813
2824
  }
@@ -2842,26 +2853,40 @@
2842
2853
  * actually touch history until we've processed redirects, so we just use
2843
2854
  * the history action from the original navigation (PUSH or REPLACE).
2844
2855
  */
2845
- async function startRedirectNavigation(state, redirect, _temp2) {
2856
+ async function startRedirectNavigation(request, redirect, _temp2) {
2846
2857
  let {
2847
2858
  submission,
2848
2859
  fetcherSubmission,
2849
2860
  replace
2850
2861
  } = _temp2 === void 0 ? {} : _temp2;
2851
- if (redirect.revalidate) {
2862
+ if (redirect.response.headers.has("X-Remix-Revalidate")) {
2852
2863
  isRevalidationRequired = true;
2853
2864
  }
2854
- let redirectLocation = createLocation(state.location, redirect.location, {
2865
+ let location = redirect.response.headers.get("Location");
2866
+ invariant(location, "Expected a Location header on the redirect Response");
2867
+ let redirectLocation = createLocation(state.location, location, {
2855
2868
  _isRedirect: true
2856
2869
  });
2857
- invariant(redirectLocation, "Expected a location on the redirect navigation");
2870
+ if (ABSOLUTE_URL_REGEX.test(location)) {
2871
+ // Strip off the protocol+origin for same-origin + same-basename absolute redirects
2872
+ let normalizedLocation = location;
2873
+ let currentUrl = new URL(request.url);
2874
+ let url = normalizedLocation.startsWith("//") ? new URL(currentUrl.protocol + normalizedLocation) : new URL(normalizedLocation);
2875
+ let isSameBasename = stripBasename(url.pathname, basename) != null;
2876
+ if (url.origin === currentUrl.origin && isSameBasename) {
2877
+ normalizedLocation = url.pathname + url.search + url.hash;
2878
+ redirectLocation = createLocation(state.location, normalizedLocation, {
2879
+ _isRedirect: true
2880
+ });
2881
+ }
2882
+ }
2858
2883
  if (isBrowser) {
2859
2884
  let isDocumentReload = false;
2860
- if (redirect.reloadDocument) {
2885
+ if (redirect.response.headers.has("X-Remix-Reload-Document")) {
2861
2886
  // Hard reload if the response contained X-Remix-Reload-Document
2862
2887
  isDocumentReload = true;
2863
- } else if (ABSOLUTE_URL_REGEX.test(redirect.location)) {
2864
- const url = init.history.createURL(redirect.location);
2888
+ } else if (ABSOLUTE_URL_REGEX.test(location)) {
2889
+ const url = init.history.createURL(location);
2865
2890
  isDocumentReload =
2866
2891
  // Hard reload if it's an absolute URL to a new origin
2867
2892
  url.origin !== routerWindow.location.origin ||
@@ -2870,9 +2895,9 @@
2870
2895
  }
2871
2896
  if (isDocumentReload) {
2872
2897
  if (replace) {
2873
- routerWindow.location.replace(redirect.location);
2898
+ routerWindow.location.replace(location);
2874
2899
  } else {
2875
- routerWindow.location.assign(redirect.location);
2900
+ routerWindow.location.assign(location);
2876
2901
  }
2877
2902
  return;
2878
2903
  }
@@ -2898,10 +2923,10 @@
2898
2923
  // re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the
2899
2924
  // redirected location
2900
2925
  let activeSubmission = submission || fetcherSubmission;
2901
- if (redirectPreserveMethodStatusCodes.has(redirect.status) && activeSubmission && isMutationMethod(activeSubmission.formMethod)) {
2926
+ if (redirectPreserveMethodStatusCodes.has(redirect.response.status) && activeSubmission && isMutationMethod(activeSubmission.formMethod)) {
2902
2927
  await startNavigation(redirectHistoryAction, redirectLocation, {
2903
2928
  submission: _extends({}, activeSubmission, {
2904
- formAction: redirect.location
2929
+ formAction: location
2905
2930
  }),
2906
2931
  // Preserve this flag across redirects
2907
2932
  preventScrollReset: pendingPreventScrollReset
@@ -2919,16 +2944,36 @@
2919
2944
  });
2920
2945
  }
2921
2946
  }
2922
- async function loadDataAndMaybeResolveDeferred(currentMatches, matches, matchesToLoad, fetchersToLoad, request) {
2923
- let [loaderResults, ...fetcherResults] = await Promise.all([matchesToLoad.length ? dataStrategy({
2924
- matches: matchesToLoad.map(m => finesseToAgnosticDataStrategyMatch(m, mapRouteProperties, manifest)),
2925
- request,
2926
- type: "loader",
2927
- defaultStrategy(match) {
2928
- return callLoaderOrActionImplementation("loader", request, match, matches, basename, future.v7_relativeSplatPath);
2929
- }
2930
- }) : [], ...fetchersToLoad.map(f => {
2931
- if (!f.matches || !f.match || !f.controller) {
2947
+
2948
+ // Utility wrapper for calling dataStrategy client-side without having to
2949
+ // pass around the manifest, mapRouteProperties, etc.
2950
+ async function callDataStrategy(type, request, matchesToLoad, matches) {
2951
+ try {
2952
+ let results = await callDataStrategyImpl(dataStrategyImpl, type, request, matchesToLoad, matches, manifest, mapRouteProperties);
2953
+ return await Promise.all(results.map((result, i) => {
2954
+ if (isRedirectHandlerResult(result)) {
2955
+ return {
2956
+ type: ResultType.redirect,
2957
+ response: normalizeRelativeRoutingRedirectResponse(result.result, request, matchesToLoad[i].route.id, matches, basename, future.v7_relativeSplatPath)
2958
+ };
2959
+ }
2960
+ return convertHandlerResultToDataResult(result);
2961
+ }));
2962
+ } catch (e) {
2963
+ // If the outer dataStrategy method throws, just return the error for all
2964
+ // matches - and it'll naturally bubble to the root
2965
+ return matchesToLoad.map(() => ({
2966
+ type: ResultType.error,
2967
+ error: e
2968
+ }));
2969
+ }
2970
+ }
2971
+ async function callLoadersAndMaybeResolveData(currentMatches, matches, matchesToLoad, fetchersToLoad, request) {
2972
+ let [loaderResults, ...fetcherResults] = await Promise.all([matchesToLoad.length ? callDataStrategy("loader", request, matchesToLoad, matches) : [], ...fetchersToLoad.map(f => {
2973
+ if (f.matches && f.match && f.controller) {
2974
+ let fetcherRequest = createClientSideRequest(init.history, f.path, f.controller.signal);
2975
+ return callDataStrategy("loader", fetcherRequest, [f.match], f.matches).then(r => r[0]);
2976
+ } else {
2932
2977
  return Promise.resolve({
2933
2978
  type: ResultType.error,
2934
2979
  error: getInternalRouterError(404, {
@@ -2936,16 +2981,6 @@
2936
2981
  })
2937
2982
  });
2938
2983
  }
2939
- return dataStrategy({
2940
- matches: [finesseToAgnosticDataStrategyMatch(f.match, mapRouteProperties, manifest)],
2941
- request,
2942
- type: "loader",
2943
- defaultStrategy(match) {
2944
- invariant(f.controller, "Expected controller for fetcher in defaultStrategy");
2945
- invariant(f.matches, "Expected matches for fetcher in defaultStrategy");
2946
- return callLoaderOrActionImplementation("loader", createClientSideRequest(init.history, f.path, f.controller.signal), match, f.matches, basename, future.v7_relativeSplatPath);
2947
- }
2948
- }).then(r => r[0]);
2949
2984
  })]);
2950
2985
  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)]);
2951
2986
  return {
@@ -3259,10 +3294,9 @@
3259
3294
 
3260
3295
  function createStaticHandler(routes, opts) {
3261
3296
  invariant(routes.length > 0, "You must provide a non-empty routes array to createStaticHandler");
3262
- const dataStrategy = (opts == null ? void 0 : opts.dataStrategy) || defaultDataStrategy;
3263
- const callLoaderOrAction = createCallLoaderOrAction(dataStrategy);
3264
3297
  let manifest = {};
3265
3298
  let basename = (opts ? opts.basename : null) || "/";
3299
+ let dataStrategyImpl = (opts == null ? void 0 : opts.unstable_dataStrategy) || defaultDataStrategy;
3266
3300
  let mapRouteProperties;
3267
3301
  if (opts != null && opts.mapRouteProperties) {
3268
3302
  mapRouteProperties = opts.mapRouteProperties;
@@ -3277,7 +3311,8 @@
3277
3311
  }
3278
3312
  // Config driven behavior flags
3279
3313
  let future = _extends({
3280
- v7_relativeSplatPath: false
3314
+ v7_relativeSplatPath: false,
3315
+ v7_throwAbortReason: false
3281
3316
  }, opts ? opts.future : null);
3282
3317
  let dataRoutes = convertRoutesToDataRoutes(routes, mapRouteProperties, undefined, manifest);
3283
3318
 
@@ -3461,14 +3496,14 @@
3461
3496
  actionHeaders: {}
3462
3497
  });
3463
3498
  } catch (e) {
3464
- // If the user threw/returned a Response in callLoaderOrAction, we throw
3465
- // it to bail out and then return or throw here based on whether the user
3466
- // returned or threw
3467
- if (isQueryRouteResponse(e)) {
3499
+ // If the user threw/returned a Response in callLoaderOrAction for a
3500
+ // `queryRoute` call, we throw the `HandlerResult` to bail out early
3501
+ // and then return or throw the raw Response here accordingly
3502
+ if (isHandlerResult(e) && isResponse(e.result)) {
3468
3503
  if (e.type === ResultType.error) {
3469
- throw e.response;
3504
+ throw e.result;
3470
3505
  }
3471
- return e.response;
3506
+ return e.result;
3472
3507
  }
3473
3508
  // Redirects are always returned since they don't propagate to catch
3474
3509
  // boundaries
@@ -3494,14 +3529,10 @@
3494
3529
  error
3495
3530
  };
3496
3531
  } else {
3497
- result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath, {
3498
- isStaticRequest: true,
3499
- isRouteRequest,
3500
- requestContext
3501
- });
3532
+ let results = await callDataStrategy("action", request, [actionMatch], matches, isRouteRequest, requestContext);
3533
+ result = results[0];
3502
3534
  if (request.signal.aborted) {
3503
- let method = isRouteRequest ? "queryRoute" : "query";
3504
- throw new Error(method + "() call aborted: " + request.method + " " + request.url);
3535
+ throwStaticHandlerAbortedError(request, isRouteRequest, future);
3505
3536
  }
3506
3537
  }
3507
3538
  if (isRedirectResult(result)) {
@@ -3510,9 +3541,9 @@
3510
3541
  // can get back on the "throw all redirect responses" train here should
3511
3542
  // this ever happen :/
3512
3543
  throw new Response(null, {
3513
- status: result.status,
3544
+ status: result.response.status,
3514
3545
  headers: {
3515
- Location: result.location
3546
+ Location: result.response.headers.get("Location")
3516
3547
  }
3517
3548
  });
3518
3549
  }
@@ -3561,8 +3592,8 @@
3561
3592
  return _extends({}, context, {
3562
3593
  statusCode: isRouteErrorResponse(result.error) ? result.error.status : 500,
3563
3594
  actionData: null,
3564
- actionHeaders: _extends({}, result.headers ? {
3565
- [actionMatch.route.id]: result.headers
3595
+ actionHeaders: _extends({}, result.response ? {
3596
+ [actionMatch.route.id]: result.response.headers
3566
3597
  } : {})
3567
3598
  });
3568
3599
  }
@@ -3574,15 +3605,17 @@
3574
3605
  signal: request.signal
3575
3606
  });
3576
3607
  let context = await loadRouteData(loaderRequest, matches, requestContext);
3577
- return _extends({}, context, result.statusCode ? {
3578
- statusCode: result.statusCode
3579
- } : {}, {
3608
+ return _extends({}, context, {
3580
3609
  actionData: {
3581
3610
  [actionMatch.route.id]: result.data
3582
- },
3583
- actionHeaders: _extends({}, result.headers ? {
3584
- [actionMatch.route.id]: result.headers
3585
- } : {})
3611
+ }
3612
+ }, result.response ? {
3613
+ statusCode: result.response.status,
3614
+ actionHeaders: {
3615
+ [actionMatch.route.id]: result.response.headers
3616
+ }
3617
+ } : {
3618
+ actionHeaders: {}
3586
3619
  });
3587
3620
  }
3588
3621
  async function loadRouteData(request, matches, requestContext, routeMatch, pendingActionError) {
@@ -3613,21 +3646,9 @@
3613
3646
  activeDeferreds: null
3614
3647
  };
3615
3648
  }
3616
- let results = await dataStrategy({
3617
- matches: matchesToLoad.map(m => finesseToAgnosticDataStrategyMatch(m, mapRouteProperties, manifest)),
3618
- request,
3619
- type: "loader",
3620
- defaultStrategy(match) {
3621
- return callLoaderOrActionImplementation("loader", request, match, matches, basename, future.v7_relativeSplatPath, {
3622
- isStaticRequest: true,
3623
- isRouteRequest,
3624
- requestContext
3625
- });
3626
- }
3627
- });
3649
+ let results = await callDataStrategy("loader", request, matchesToLoad, matches, isRouteRequest, requestContext);
3628
3650
  if (request.signal.aborted) {
3629
- let method = isRouteRequest ? "queryRoute" : "query";
3630
- throw new Error(method + "() call aborted: " + request.method + " " + request.url);
3651
+ throwStaticHandlerAbortedError(request, isRouteRequest, future);
3631
3652
  }
3632
3653
 
3633
3654
  // Process and commit output from loaders
@@ -3646,6 +3667,25 @@
3646
3667
  activeDeferreds: activeDeferreds.size > 0 ? Object.fromEntries(activeDeferreds.entries()) : null
3647
3668
  });
3648
3669
  }
3670
+
3671
+ // Utility wrapper for calling dataStrategy server-side without having to
3672
+ // pass around the manifest, mapRouteProperties, etc.
3673
+ async function callDataStrategy(type, request, matchesToLoad, matches, isRouteRequest, requestContext) {
3674
+ let results = await callDataStrategyImpl(dataStrategyImpl, type, request, matchesToLoad, matches, manifest, mapRouteProperties, requestContext);
3675
+ return await Promise.all(results.map((result, i) => {
3676
+ if (isRedirectHandlerResult(result)) {
3677
+ // Throw redirects and let the server handle them with an HTTP redirect
3678
+ throw normalizeRelativeRoutingRedirectResponse(result.result, request, matchesToLoad[i].route.id, matches, basename, future.v7_relativeSplatPath);
3679
+ }
3680
+ if (isResponse(result.result) && isRouteRequest) {
3681
+ // For SSR single-route requests, we want to hand Responses back directly
3682
+ // without unwrapping. We do this with the QueryRouteResponse wrapper
3683
+ // interface so we can know whether it was returned or thrown
3684
+ throw result;
3685
+ }
3686
+ return convertHandlerResultToDataResult(result);
3687
+ }));
3688
+ }
3649
3689
  return {
3650
3690
  dataRoutes,
3651
3691
  query,
@@ -3659,27 +3699,26 @@
3659
3699
  //#region Helpers
3660
3700
  ////////////////////////////////////////////////////////////////////////////////
3661
3701
 
3662
- function defaultDataStrategy(_ref3) {
3663
- let {
3664
- defaultStrategy,
3665
- matches
3666
- } = _ref3;
3667
- return Promise.all(matches.map(match => defaultStrategy(match)));
3668
- }
3669
-
3670
3702
  /**
3671
3703
  * Given an existing StaticHandlerContext and an error thrown at render time,
3672
3704
  * provide an updated StaticHandlerContext suitable for a second SSR render
3673
3705
  */
3674
3706
  function getStaticContextFromError(routes, context, error) {
3675
3707
  let newContext = _extends({}, context, {
3676
- statusCode: 500,
3708
+ statusCode: isRouteErrorResponse(error) ? error.status : 500,
3677
3709
  errors: {
3678
3710
  [context._deepestRenderedBoundaryId || routes[0].id]: error
3679
3711
  }
3680
3712
  });
3681
3713
  return newContext;
3682
3714
  }
3715
+ function throwStaticHandlerAbortedError(request, isRouteRequest, future) {
3716
+ if (future.v7_throwAbortReason && request.signal.reason !== undefined) {
3717
+ throw request.signal.reason;
3718
+ }
3719
+ let method = isRouteRequest ? "queryRoute" : "query";
3720
+ throw new Error(method + "() call aborted: " + request.method + " " + request.url);
3721
+ }
3683
3722
  function isSubmissionNavigation(opts) {
3684
3723
  return opts != null && ("formData" in opts && opts.formData != null || "body" in opts && opts.body !== undefined);
3685
3724
  }
@@ -3764,8 +3803,8 @@
3764
3803
  }
3765
3804
  let text = typeof opts.body === "string" ? opts.body : opts.body instanceof FormData || opts.body instanceof URLSearchParams ?
3766
3805
  // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#plain-text-form-data
3767
- Array.from(opts.body.entries()).reduce((acc, _ref4) => {
3768
- let [name, value] = _ref4;
3806
+ Array.from(opts.body.entries()).reduce((acc, _ref3) => {
3807
+ let [name, value] = _ref3;
3769
3808
  return "" + acc + name + "=" + value + "\n";
3770
3809
  }, "") : String(opts.body);
3771
3810
  return {
@@ -3876,18 +3915,24 @@
3876
3915
  let boundaryId = pendingError ? Object.keys(pendingError)[0] : undefined;
3877
3916
  let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId);
3878
3917
  let navigationMatches = boundaryMatches.filter((match, index) => {
3879
- if (isInitialLoad) {
3880
- // On initial hydration we don't do any shouldRevalidate stuff - we just
3881
- // call the unhydrated loaders
3882
- return isUnhydratedRoute(state, match.route);
3883
- }
3884
- if (match.route.lazy) {
3918
+ let {
3919
+ route
3920
+ } = match;
3921
+ if (route.lazy) {
3885
3922
  // We haven't loaded this route yet so we don't know if it's got a loader!
3886
3923
  return true;
3887
3924
  }
3888
- if (match.route.loader == null) {
3925
+ if (route.loader == null) {
3889
3926
  return false;
3890
3927
  }
3928
+ if (isInitialLoad) {
3929
+ if (route.loader.hydrate) {
3930
+ return true;
3931
+ }
3932
+ return state.loaderData[route.id] === undefined && (
3933
+ // Don't re-run if the loader ran and threw an error
3934
+ !state.errors || state.errors[route.id] === undefined);
3935
+ }
3891
3936
 
3892
3937
  // Always call the loader on new route instances and pending defer cancellations
3893
3938
  if (isNewLoader(state.loaderData, state.matches[index], match) || cancelledDeferredRoutes.some(id => id === match.route.id)) {
@@ -3989,20 +4034,6 @@
3989
4034
  });
3990
4035
  return [navigationMatches, revalidatingFetchers];
3991
4036
  }
3992
-
3993
- // Is this route unhydrated (when v7_partialHydration=true) such that we need
3994
- // to call it's loader on the initial router creation
3995
- function isUnhydratedRoute(state, route) {
3996
- if (!route.loader) {
3997
- return false;
3998
- }
3999
- if (route.loader.hydrate) {
4000
- return true;
4001
- }
4002
- return state.loaderData[route.id] === undefined && (!state.errors ||
4003
- // Loader ran but errored - don't re-run
4004
- state.errors[route.id] === undefined);
4005
- }
4006
4037
  function isNewLoader(currentLoaderData, currentMatch, match) {
4007
4038
  let isNew =
4008
4039
  // [a] -> [a, b]
@@ -4090,44 +4121,57 @@
4090
4121
  }));
4091
4122
  return routeToUpdate;
4092
4123
  }
4093
- function createCallLoaderOrAction(dataStrategy) {
4094
- return async function (type, request, match, matches, manifest, mapRouteProperties, basename, v7_relativeSplatPath, opts) {
4095
- if (opts === void 0) {
4096
- opts = {};
4097
- }
4098
- let [result] = await dataStrategy({
4099
- matches: [finesseToAgnosticDataStrategyMatch(match, mapRouteProperties, manifest)],
4100
- request,
4101
- type,
4102
- defaultStrategy(match) {
4103
- return callLoaderOrActionImplementation(type, request, match, matches, basename, v7_relativeSplatPath, opts);
4104
- }
4105
- });
4106
- return result;
4107
- };
4124
+
4125
+ // Default implementation of `dataStrategy` which fetches all loaders in parallel
4126
+ function defaultDataStrategy(opts) {
4127
+ return Promise.all(opts.matches.map(m => m.bikeshed_loadRoute()));
4108
4128
  }
4109
- function finesseToAgnosticDataStrategyMatch(match, mapRouteProperties, manifest) {
4110
- let loadRoutePromise;
4111
- if (match.route.lazy) {
4112
- try {
4113
- loadRoutePromise = loadLazyRouteModule(match.route, mapRouteProperties, manifest);
4114
- } catch (error) {
4115
- loadRoutePromise = Promise.reject(error);
4116
- }
4117
- }
4118
- if (!loadRoutePromise) {
4119
- loadRoutePromise = Promise.resolve(match.route);
4120
- }
4121
- loadRoutePromise.catch(() => {});
4122
- return _extends({}, match, {
4123
- route: Object.assign(loadRoutePromise, match.route)
4129
+ async function callDataStrategyImpl(dataStrategyImpl, type, request, matchesToLoad, matches, manifest, mapRouteProperties, requestContext) {
4130
+ let routeIdsToLoad = matchesToLoad.reduce((acc, m) => acc.add(m.route.id), new Set());
4131
+ let loadedMatches = new Set();
4132
+
4133
+ // Send all matches here to allow for a middleware-type implementation.
4134
+ // handler will be a no-op for unneeded routes and we filter those results
4135
+ // back out below.
4136
+ let results = await dataStrategyImpl({
4137
+ matches: matches.map(match => {
4138
+ // `bikeshed_loadRoute` encapsulates the route.lazy, executing the
4139
+ // loader/action, and mapping return values/thrown errors to a
4140
+ // HandlerResult. Users can pass a callback to take fine-grained control
4141
+ // over the execution of the loader/action
4142
+ let bikeshed_loadRoute = bikeshed_handlerOverride => {
4143
+ loadedMatches.add(match.route.id);
4144
+ return routeIdsToLoad.has(match.route.id) ? callLoaderOrAction(type, request, match, manifest, mapRouteProperties, bikeshed_handlerOverride, requestContext) :
4145
+ // TODO: What's the best thing to do here - return an empty "success" result?
4146
+ // Or return a success result with the current route loader/action data?
4147
+ // We strip these results out if the route didn't need to be revalidated in
4148
+ // `callDataStrategy` so it doesn't matter for us. It's more of a question
4149
+ // of whether exposing the current data to the user is useful?
4150
+ Promise.resolve({
4151
+ type: ResultType.data,
4152
+ result: undefined
4153
+ });
4154
+ };
4155
+ return _extends({}, match, {
4156
+ bikeshed_loadRoute
4157
+ });
4158
+ }),
4159
+ request,
4160
+ params: matches[0].params
4124
4161
  });
4162
+
4163
+ // Throw if any loadRoute implementations not called since they are what
4164
+ // ensures a route is fully loaded
4165
+ // Throw if any loadRoute implementations not called since they are what
4166
+ // ensure a route is fully loaded
4167
+ matches.forEach(m => invariant(loadedMatches.has(m.route.id), "`match.bikeshed_loadRoute()` was not called for route id \"" + m.route.id + "\". " + "You must call `match.bikeshed_loadRoute()` on every match passed to " + "`dataStrategy` to ensure all routes are properly loaded."));
4168
+
4169
+ // Filter out any middleware-only matches for which we didn't need to run handlers
4170
+ return results.filter((_, i) => routeIdsToLoad.has(matches[i].route.id));
4125
4171
  }
4126
- async function callLoaderOrActionImplementation(type, request, match, matches, basename, v7_relativeSplatPath, opts) {
4127
- if (opts === void 0) {
4128
- opts = {};
4129
- }
4130
- let resultType;
4172
+
4173
+ // Default logic for calling a loader/action is the user has no specified a dataStrategy
4174
+ async function callLoaderOrAction(type, request, match, manifest, mapRouteProperties, bikeshed_handlerOverride, staticContext) {
4131
4175
  let result;
4132
4176
  let onReject;
4133
4177
  let runHandler = handler => {
@@ -4136,11 +4180,13 @@
4136
4180
  let abortPromise = new Promise((_, r) => reject = r);
4137
4181
  onReject = () => reject();
4138
4182
  request.signal.addEventListener("abort", onReject);
4139
- return Promise.race([handler({
4183
+ let handlerArg = {
4140
4184
  request,
4141
4185
  params: match.params,
4142
- context: opts.requestContext
4143
- }), abortPromise]);
4186
+ context: staticContext
4187
+ };
4188
+ let handlerPromise = bikeshed_handlerOverride ? bikeshed_handlerOverride(async ctx => ctx !== undefined ? handler(handlerArg, ctx) : handler(handlerArg)) : handler(handlerArg);
4189
+ return Promise.race([handlerPromise, abortPromise]);
4144
4190
  };
4145
4191
  try {
4146
4192
  let handler = match.route[type];
@@ -4154,17 +4200,17 @@
4154
4200
  // route has a boundary that can handle the error
4155
4201
  runHandler(handler).catch(e => {
4156
4202
  handlerError = e;
4157
- }), match.route]);
4203
+ }), loadLazyRouteModule(match.route, mapRouteProperties, manifest)]);
4158
4204
  if (handlerError) {
4159
4205
  throw handlerError;
4160
4206
  }
4161
4207
  result = values[0];
4162
4208
  } else {
4163
4209
  // Load lazy route module, then run any returned handler
4164
- let route = await match.route;
4165
- handler = route[type];
4210
+ await loadLazyRouteModule(match.route, mapRouteProperties, manifest);
4211
+ handler = match.route[type];
4166
4212
  if (handler) {
4167
- // Handler still run even if we got interrupted to maintain consistency
4213
+ // Handler still runs even if we got interrupted to maintain consistency
4168
4214
  // with un-abortable behavior of handler execution on non-lazy or
4169
4215
  // previously-lazy-loaded routes
4170
4216
  result = await runHandler(handler);
@@ -4181,7 +4227,7 @@
4181
4227
  // hit the invariant below that errors on returning undefined.
4182
4228
  return {
4183
4229
  type: ResultType.data,
4184
- data: undefined
4230
+ result: undefined
4185
4231
  };
4186
4232
  }
4187
4233
  }
@@ -4196,70 +4242,37 @@
4196
4242
  }
4197
4243
  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`.");
4198
4244
  } catch (e) {
4199
- resultType = ResultType.error;
4200
- result = e;
4245
+ return {
4246
+ type: ResultType.error,
4247
+ result: e
4248
+ };
4201
4249
  } finally {
4202
4250
  if (onReject) {
4203
4251
  request.signal.removeEventListener("abort", onReject);
4204
4252
  }
4205
4253
  }
4254
+ return {
4255
+ type: ResultType.data,
4256
+ result
4257
+ };
4258
+ }
4259
+ async function convertHandlerResultToDataResult(handlerResult) {
4260
+ let {
4261
+ result,
4262
+ type
4263
+ } = handlerResult;
4206
4264
  if (isResponse(result)) {
4207
- let status = result.status;
4208
-
4209
- // Process redirects
4210
- if (redirectStatusCodes.has(status)) {
4211
- let location = result.headers.get("Location");
4212
- invariant(location, "Redirects returned/thrown from loaders/actions must have a Location header");
4213
-
4214
- // Support relative routing in internal redirects
4215
- if (!ABSOLUTE_URL_REGEX.test(location)) {
4216
- location = normalizeTo(new URL(request.url), matches.slice(0, matches.findIndex(m => m.route.id === match.route.id) + 1), basename, true, location, v7_relativeSplatPath);
4217
- } else if (!opts.isStaticRequest) {
4218
- // Strip off the protocol+origin for same-origin + same-basename absolute
4219
- // redirects. If this is a static request, we can let it go back to the
4220
- // browser as-is
4221
- let currentUrl = new URL(request.url);
4222
- let url = location.startsWith("//") ? new URL(currentUrl.protocol + location) : new URL(location);
4223
- let isSameBasename = stripBasename(url.pathname, basename) != null;
4224
- if (url.origin === currentUrl.origin && isSameBasename) {
4225
- location = url.pathname + url.search + url.hash;
4226
- }
4227
- }
4228
-
4229
- // Don't process redirects in the router during static requests requests.
4230
- // Instead, throw the Response and let the server handle it with an HTTP
4231
- // redirect. We also update the Location header in place in this flow so
4232
- // basename and relative routing is taken into account
4233
- if (opts.isStaticRequest) {
4234
- result.headers.set("Location", location);
4235
- throw result;
4236
- }
4237
- return {
4238
- type: ResultType.redirect,
4239
- status,
4240
- location,
4241
- revalidate: result.headers.get("X-Remix-Revalidate") !== null,
4242
- reloadDocument: result.headers.get("X-Remix-Reload-Document") !== null
4243
- };
4244
- }
4245
-
4246
- // For SSR single-route requests, we want to hand Responses back directly
4247
- // without unwrapping. We do this with the QueryRouteResponse wrapper
4248
- // interface so we can know whether it was returned or thrown
4249
- if (opts.isRouteRequest) {
4250
- let queryRouteResponse = {
4251
- type: resultType === ResultType.error ? ResultType.error : ResultType.data,
4252
- response: result
4253
- };
4254
- throw queryRouteResponse;
4255
- }
4256
4265
  let data;
4257
4266
  try {
4258
4267
  let contentType = result.headers.get("Content-Type");
4259
4268
  // Check between word boundaries instead of startsWith() due to the last
4260
4269
  // paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type
4261
4270
  if (contentType && /\bapplication\/json\b/.test(contentType)) {
4262
- data = await result.json();
4271
+ if (result.body == null) {
4272
+ data = null;
4273
+ } else {
4274
+ data = await result.json();
4275
+ }
4263
4276
  } else {
4264
4277
  data = await result.text();
4265
4278
  }
@@ -4269,23 +4282,22 @@
4269
4282
  error: e
4270
4283
  };
4271
4284
  }
4272
- if (resultType === ResultType.error) {
4285
+ if (type === ResultType.error) {
4273
4286
  return {
4274
- type: resultType,
4275
- error: new ErrorResponseImpl(status, result.statusText, data),
4276
- headers: result.headers
4287
+ type,
4288
+ error: new ErrorResponseImpl(result.status, result.statusText, data),
4289
+ response: result
4277
4290
  };
4278
4291
  }
4279
4292
  return {
4280
4293
  type: ResultType.data,
4281
4294
  data,
4282
- statusCode: result.status,
4283
- headers: result.headers
4295
+ response: result
4284
4296
  };
4285
4297
  }
4286
- if (resultType === ResultType.error) {
4298
+ if (type === ResultType.error) {
4287
4299
  return {
4288
- type: resultType,
4300
+ type,
4289
4301
  error: result
4290
4302
  };
4291
4303
  }
@@ -4304,6 +4316,18 @@
4304
4316
  };
4305
4317
  }
4306
4318
 
4319
+ // Support relative routing in internal redirects
4320
+ function normalizeRelativeRoutingRedirectResponse(response, request, routeId, matches, basename, v7_relativeSplatPath) {
4321
+ let location = response.headers.get("Location");
4322
+ invariant(location, "Redirects returned/thrown from loaders/actions must have a Location header");
4323
+ if (!ABSOLUTE_URL_REGEX.test(location)) {
4324
+ let trimmedMatches = matches.slice(0, matches.findIndex(m => m.route.id === routeId) + 1);
4325
+ location = normalizeTo(new URL(request.url), trimmedMatches, basename, true, location, v7_relativeSplatPath);
4326
+ response.headers.set("Location", location);
4327
+ }
4328
+ return response;
4329
+ }
4330
+
4307
4331
  // Utility method for creating the Request instances for loaders/actions during
4308
4332
  // client-side navigations and fetches. During SSR we will always have a
4309
4333
  // Request instance from the static handler (query/queryRoute)
@@ -4394,24 +4418,31 @@
4394
4418
  foundError = true;
4395
4419
  statusCode = isRouteErrorResponse(result.error) ? result.error.status : 500;
4396
4420
  }
4397
- if (result.headers) {
4398
- loaderHeaders[id] = result.headers;
4421
+ if (result.response) {
4422
+ loaderHeaders[id] = result.response.headers;
4399
4423
  }
4400
4424
  } else {
4401
4425
  if (isDeferredResult(result)) {
4402
4426
  activeDeferreds.set(id, result.deferredData);
4403
4427
  loaderData[id] = result.deferredData.data;
4428
+ // Error status codes always override success status codes, but if all
4429
+ // loaders are successful we take the deepest status code.
4430
+ if (result.statusCode != null && result.statusCode !== 200 && !foundError) {
4431
+ statusCode = result.statusCode;
4432
+ }
4433
+ if (result.headers) {
4434
+ loaderHeaders[id] = result.headers;
4435
+ }
4404
4436
  } else {
4405
4437
  loaderData[id] = result.data;
4406
- }
4407
-
4408
- // Error status codes always override success status codes, but if all
4409
- // loaders are successful we take the deepest status code.
4410
- if (result.statusCode != null && result.statusCode !== 200 && !foundError) {
4411
- statusCode = result.statusCode;
4412
- }
4413
- if (result.headers) {
4414
- loaderHeaders[id] = result.headers;
4438
+ // Error status codes always override success status codes, but if all
4439
+ // loaders are successful we take the deepest status code.
4440
+ if (result.response) {
4441
+ if (result.response.status !== 200 && !foundError) {
4442
+ statusCode = result.response.status;
4443
+ }
4444
+ loaderHeaders[id] = result.response.headers;
4445
+ }
4415
4446
  }
4416
4447
  }
4417
4448
  });
@@ -4591,6 +4622,12 @@
4591
4622
  // /page#hash -> /page
4592
4623
  return false;
4593
4624
  }
4625
+ function isHandlerResult(result) {
4626
+ return result != null && typeof result === "object" && "type" in result && "result" in result && (result.type === ResultType.data || result.type === ResultType.error);
4627
+ }
4628
+ function isRedirectHandlerResult(result) {
4629
+ return isResponse(result.result) && redirectStatusCodes.has(result.result.status);
4630
+ }
4594
4631
  function isDeferredResult(result) {
4595
4632
  return result.type === ResultType.deferred;
4596
4633
  }
@@ -4615,9 +4652,6 @@
4615
4652
  let location = result.headers.get("Location");
4616
4653
  return status >= 300 && status <= 399 && location != null;
4617
4654
  }
4618
- function isQueryRouteResponse(obj) {
4619
- return obj && isResponse(obj.response) && (obj.type === ResultType.data || obj.type === ResultType.error);
4620
- }
4621
4655
  function isValidMethod(method) {
4622
4656
  return validRequestMethods.has(method.toLowerCase());
4623
4657
  }
@@ -4861,7 +4895,6 @@
4861
4895
  exports.IDLE_BLOCKER = IDLE_BLOCKER;
4862
4896
  exports.IDLE_FETCHER = IDLE_FETCHER;
4863
4897
  exports.IDLE_NAVIGATION = IDLE_NAVIGATION;
4864
- exports.ResultType = ResultType;
4865
4898
  exports.UNSAFE_DEFERRED_SYMBOL = UNSAFE_DEFERRED_SYMBOL;
4866
4899
  exports.UNSAFE_DeferredData = DeferredData;
4867
4900
  exports.UNSAFE_ErrorResponseImpl = ErrorResponseImpl;