@remix-run/router 1.19.2 → 1.20.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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @remix-run/router v1.19.2
2
+ * @remix-run/router v1.20.0-pre.0
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -1713,8 +1713,8 @@
1713
1713
  let dataRoutes = convertRoutesToDataRoutes(init.routes, mapRouteProperties, undefined, manifest);
1714
1714
  let inFlightDataRoutes;
1715
1715
  let basename = init.basename || "/";
1716
- let dataStrategyImpl = init.unstable_dataStrategy || defaultDataStrategy;
1717
- let patchRoutesOnNavigationImpl = init.unstable_patchRoutesOnNavigation;
1716
+ let dataStrategyImpl = init.dataStrategy || defaultDataStrategy;
1717
+ let patchRoutesOnNavigationImpl = init.patchRoutesOnNavigation;
1718
1718
 
1719
1719
  // Config driven behavior flags
1720
1720
  let future = _extends({
@@ -1729,10 +1729,6 @@
1729
1729
  let unlistenHistory = null;
1730
1730
  // Externally-provided functions to call on all state changes
1731
1731
  let subscribers = new Set();
1732
- // FIFO queue of previously discovered routes to prevent re-calling on
1733
- // subsequent navigations to the same path
1734
- let discoveredRoutesMaxSize = 1000;
1735
- let discoveredRoutes = new Set();
1736
1732
  // Externally-provided object to hold scroll restoration locations during routing
1737
1733
  let savedScrollPositions = null;
1738
1734
  // Externally-provided function to get scroll restoration keys
@@ -1803,25 +1799,12 @@
1803
1799
  // were marked for explicit hydration
1804
1800
  let loaderData = init.hydrationData ? init.hydrationData.loaderData : null;
1805
1801
  let errors = init.hydrationData ? init.hydrationData.errors : null;
1806
- let isRouteInitialized = m => {
1807
- // No loader, nothing to initialize
1808
- if (!m.route.loader) {
1809
- return true;
1810
- }
1811
- // Explicitly opting-in to running on hydration
1812
- if (typeof m.route.loader === "function" && m.route.loader.hydrate === true) {
1813
- return false;
1814
- }
1815
- // Otherwise, initialized if hydrated with data or an error
1816
- return loaderData && loaderData[m.route.id] !== undefined || errors && errors[m.route.id] !== undefined;
1817
- };
1818
-
1819
1802
  // If errors exist, don't consider routes below the boundary
1820
1803
  if (errors) {
1821
1804
  let idx = initialMatches.findIndex(m => errors[m.route.id] !== undefined);
1822
- initialized = initialMatches.slice(0, idx + 1).every(isRouteInitialized);
1805
+ initialized = initialMatches.slice(0, idx + 1).every(m => !shouldLoadRouteOnHydration(m.route, loaderData, errors));
1823
1806
  } else {
1824
- initialized = initialMatches.every(isRouteInitialized);
1807
+ initialized = initialMatches.every(m => !shouldLoadRouteOnHydration(m.route, loaderData, errors));
1825
1808
  }
1826
1809
  } else {
1827
1810
  // Without partial hydration - we're initialized if we were provided any
@@ -1921,10 +1904,6 @@
1921
1904
  // we don't need to update UI state if they change
1922
1905
  let blockerFunctions = new Map();
1923
1906
 
1924
- // Map of pending patchRoutesOnNavigation() promises (keyed by path/matches) so
1925
- // that we only kick them off once for a given combo
1926
- let pendingPatchRoutes = new Map();
1927
-
1928
1907
  // Flag to ignore the next history update, so we can revert the URL change on
1929
1908
  // a POP navigation that was blocked by the user without touching router state
1930
1909
  let unblockBlockerHistoryUpdate = undefined;
@@ -2062,8 +2041,8 @@
2062
2041
  // we don't get ourselves into a loop calling the new subscriber immediately
2063
2042
  [...subscribers].forEach(subscriber => subscriber(state, {
2064
2043
  deletedFetchers: deletedFetchersKeys,
2065
- unstable_viewTransitionOpts: opts.viewTransitionOpts,
2066
- unstable_flushSync: opts.flushSync === true
2044
+ viewTransitionOpts: opts.viewTransitionOpts,
2045
+ flushSync: opts.flushSync === true
2067
2046
  }));
2068
2047
 
2069
2048
  // Remove idle fetchers from state since we only care about in-flight fetchers.
@@ -2223,7 +2202,7 @@
2223
2202
  historyAction = Action.Replace;
2224
2203
  }
2225
2204
  let preventScrollReset = opts && "preventScrollReset" in opts ? opts.preventScrollReset === true : undefined;
2226
- let flushSync = (opts && opts.unstable_flushSync) === true;
2205
+ let flushSync = (opts && opts.flushSync) === true;
2227
2206
  let blockerKey = shouldBlockNavigation({
2228
2207
  currentLocation,
2229
2208
  nextLocation,
@@ -2261,7 +2240,7 @@
2261
2240
  pendingError: error,
2262
2241
  preventScrollReset,
2263
2242
  replace: opts && opts.replace,
2264
- enableViewTransition: opts && opts.unstable_viewTransition,
2243
+ enableViewTransition: opts && opts.viewTransition,
2265
2244
  flushSync
2266
2245
  });
2267
2246
  }
@@ -2349,7 +2328,7 @@
2349
2328
  // Short circuit if it's only a hash change and not a revalidation or
2350
2329
  // mutation submission.
2351
2330
  //
2352
- // Ignore on initial page loads because since the initial load will always
2331
+ // Ignore on initial page loads because since the initial hydration will always
2353
2332
  // be "same hash". For example, on /page#hash and submit a <Form method="post">
2354
2333
  // which will default to a navigation to /page
2355
2334
  if (state.initialized && !isRevalidationRequired && isHashChangeOnly(state.location, location) && !(opts && opts.submission && isMutationMethod(opts.submission.formMethod))) {
@@ -2668,9 +2647,7 @@
2668
2647
  });
2669
2648
  }
2670
2649
  revalidatingFetchers.forEach(rf => {
2671
- if (fetchControllers.has(rf.key)) {
2672
- abortFetcher(rf.key);
2673
- }
2650
+ abortFetcher(rf.key);
2674
2651
  if (rf.controller) {
2675
2652
  // Fetchers use an independent AbortController so that aborting a fetcher
2676
2653
  // (via deleteFetcher) does not abort the triggering navigation that
@@ -2730,7 +2707,7 @@
2730
2707
  let {
2731
2708
  loaderData,
2732
2709
  errors
2733
- } = processLoaderData(state, matches, matchesToLoad, loaderResults, pendingActionResult, revalidatingFetchers, fetcherResults, activeDeferreds);
2710
+ } = processLoaderData(state, matches, loaderResults, pendingActionResult, revalidatingFetchers, fetcherResults, activeDeferreds);
2734
2711
 
2735
2712
  // Wire up subscribers to update loaderData as promises settle
2736
2713
  activeDeferreds.forEach((deferredData, routeId) => {
@@ -2744,17 +2721,9 @@
2744
2721
  });
2745
2722
  });
2746
2723
 
2747
- // During partial hydration, preserve SSR errors for routes that don't re-run
2724
+ // Preserve SSR errors during partial hydration
2748
2725
  if (future.v7_partialHydration && initialHydration && state.errors) {
2749
- Object.entries(state.errors).filter(_ref2 => {
2750
- let [id] = _ref2;
2751
- return !matchesToLoad.some(m => m.route.id === id);
2752
- }).forEach(_ref3 => {
2753
- let [routeId, error] = _ref3;
2754
- errors = Object.assign(errors || {}, {
2755
- [routeId]: error
2756
- });
2757
- });
2726
+ errors = _extends({}, state.errors, errors);
2758
2727
  }
2759
2728
  let updatedFetchers = markFetchRedirectsDone();
2760
2729
  let didAbortFetchLoads = abortStaleFetchLoads(pendingNavigationLoadId);
@@ -2797,8 +2766,8 @@
2797
2766
  if (isServer) {
2798
2767
  throw new Error("router.fetch() was called during the server render, but it shouldn't be. " + "You are likely calling a useFetcher() method in the body of your component. " + "Try moving it to a useEffect or a callback.");
2799
2768
  }
2800
- if (fetchControllers.has(key)) abortFetcher(key);
2801
- let flushSync = (opts && opts.unstable_flushSync) === true;
2769
+ abortFetcher(key);
2770
+ let flushSync = (opts && opts.flushSync) === true;
2802
2771
  let routesToUse = inFlightDataRoutes || dataRoutes;
2803
2772
  let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, href, future.v7_relativeSplatPath, routeId, opts == null ? void 0 : opts.relative);
2804
2773
  let matches = matchRoutes(routesToUse, normalizedPath, basename);
@@ -2826,9 +2795,9 @@
2826
2795
  return;
2827
2796
  }
2828
2797
  let match = getTargetMatch(matches, path);
2829
- pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
2798
+ let preventScrollReset = (opts && opts.preventScrollReset) === true;
2830
2799
  if (submission && isMutationMethod(submission.formMethod)) {
2831
- handleFetcherAction(key, routeId, path, match, matches, fogOfWar.active, flushSync, submission);
2800
+ handleFetcherAction(key, routeId, path, match, matches, fogOfWar.active, flushSync, preventScrollReset, submission);
2832
2801
  return;
2833
2802
  }
2834
2803
 
@@ -2838,12 +2807,12 @@
2838
2807
  routeId,
2839
2808
  path
2840
2809
  });
2841
- handleFetcherLoader(key, routeId, path, match, matches, fogOfWar.active, flushSync, submission);
2810
+ handleFetcherLoader(key, routeId, path, match, matches, fogOfWar.active, flushSync, preventScrollReset, submission);
2842
2811
  }
2843
2812
 
2844
2813
  // Call the action for the matched fetcher.submit(), and then handle redirects,
2845
2814
  // errors, and revalidation
2846
- async function handleFetcherAction(key, routeId, path, match, requestMatches, isFogOfWar, flushSync, submission) {
2815
+ async function handleFetcherAction(key, routeId, path, match, requestMatches, isFogOfWar, flushSync, preventScrollReset, submission) {
2847
2816
  interruptActiveLoads();
2848
2817
  fetchLoadMatches.delete(key);
2849
2818
  function detectAndHandle405Error(m) {
@@ -2936,7 +2905,8 @@
2936
2905
  fetchRedirectIds.add(key);
2937
2906
  updateFetcherState(key, getLoadingFetcher(submission));
2938
2907
  return startRedirectNavigation(fetchRequest, actionResult, false, {
2939
- fetcherSubmission: submission
2908
+ fetcherSubmission: submission,
2909
+ preventScrollReset
2940
2910
  });
2941
2911
  }
2942
2912
  }
@@ -2974,9 +2944,7 @@
2974
2944
  let existingFetcher = state.fetchers.get(staleKey);
2975
2945
  let revalidatingFetcher = getLoadingFetcher(undefined, existingFetcher ? existingFetcher.data : undefined);
2976
2946
  state.fetchers.set(staleKey, revalidatingFetcher);
2977
- if (fetchControllers.has(staleKey)) {
2978
- abortFetcher(staleKey);
2979
- }
2947
+ abortFetcher(staleKey);
2980
2948
  if (rf.controller) {
2981
2949
  fetchControllers.set(staleKey, rf.controller);
2982
2950
  }
@@ -2999,7 +2967,9 @@
2999
2967
  revalidatingFetchers.forEach(r => fetchControllers.delete(r.key));
3000
2968
  let redirect = findRedirect(loaderResults);
3001
2969
  if (redirect) {
3002
- return startRedirectNavigation(revalidationRequest, redirect.result, false);
2970
+ return startRedirectNavigation(revalidationRequest, redirect.result, false, {
2971
+ preventScrollReset
2972
+ });
3003
2973
  }
3004
2974
  redirect = findRedirect(fetcherResults);
3005
2975
  if (redirect) {
@@ -3007,14 +2977,16 @@
3007
2977
  // fetchRedirectIds so it doesn't get revalidated on the next set of
3008
2978
  // loader executions
3009
2979
  fetchRedirectIds.add(redirect.key);
3010
- return startRedirectNavigation(revalidationRequest, redirect.result, false);
2980
+ return startRedirectNavigation(revalidationRequest, redirect.result, false, {
2981
+ preventScrollReset
2982
+ });
3011
2983
  }
3012
2984
 
3013
2985
  // Process and commit output from loaders
3014
2986
  let {
3015
2987
  loaderData,
3016
2988
  errors
3017
- } = processLoaderData(state, matches, matchesToLoad, loaderResults, undefined, revalidatingFetchers, fetcherResults, activeDeferreds);
2989
+ } = processLoaderData(state, matches, loaderResults, undefined, revalidatingFetchers, fetcherResults, activeDeferreds);
3018
2990
 
3019
2991
  // Since we let revalidations complete even if the submitting fetcher was
3020
2992
  // deleted, only put it back to idle if it hasn't been deleted
@@ -3050,7 +3022,7 @@
3050
3022
  }
3051
3023
 
3052
3024
  // Call the matched loader for fetcher.load(), handling redirects, errors, etc.
3053
- async function handleFetcherLoader(key, routeId, path, match, matches, isFogOfWar, flushSync, submission) {
3025
+ async function handleFetcherLoader(key, routeId, path, match, matches, isFogOfWar, flushSync, preventScrollReset, submission) {
3054
3026
  let existingFetcher = state.fetchers.get(key);
3055
3027
  updateFetcherState(key, getLoadingFetcher(submission, existingFetcher ? existingFetcher.data : undefined), {
3056
3028
  flushSync
@@ -3121,7 +3093,9 @@
3121
3093
  return;
3122
3094
  } else {
3123
3095
  fetchRedirectIds.add(key);
3124
- await startRedirectNavigation(fetchRequest, result, false);
3096
+ await startRedirectNavigation(fetchRequest, result, false, {
3097
+ preventScrollReset
3098
+ });
3125
3099
  return;
3126
3100
  }
3127
3101
  }
@@ -3160,6 +3134,7 @@
3160
3134
  let {
3161
3135
  submission,
3162
3136
  fetcherSubmission,
3137
+ preventScrollReset,
3163
3138
  replace
3164
3139
  } = _temp2 === void 0 ? {} : _temp2;
3165
3140
  if (redirect.response.headers.has("X-Remix-Revalidate")) {
@@ -3220,7 +3195,7 @@
3220
3195
  formAction: location
3221
3196
  }),
3222
3197
  // Preserve these flags across redirects
3223
- preventScrollReset: pendingPreventScrollReset,
3198
+ preventScrollReset: preventScrollReset || pendingPreventScrollReset,
3224
3199
  enableViewTransition: isNavigation ? pendingViewTransitionEnabled : undefined
3225
3200
  });
3226
3201
  } else {
@@ -3232,7 +3207,7 @@
3232
3207
  // Send fetcher submissions through for shouldRevalidate
3233
3208
  fetcherSubmission,
3234
3209
  // Preserve these flags across redirects
3235
- preventScrollReset: pendingPreventScrollReset,
3210
+ preventScrollReset: preventScrollReset || pendingPreventScrollReset,
3236
3211
  enableViewTransition: isNavigation ? pendingViewTransitionEnabled : undefined
3237
3212
  });
3238
3213
  }
@@ -3313,8 +3288,8 @@
3313
3288
  fetchLoadMatches.forEach((_, key) => {
3314
3289
  if (fetchControllers.has(key)) {
3315
3290
  cancelledFetcherLoads.add(key);
3316
- abortFetcher(key);
3317
3291
  }
3292
+ abortFetcher(key);
3318
3293
  });
3319
3294
  }
3320
3295
  function updateFetcherState(key, fetcher, opts) {
@@ -3387,9 +3362,10 @@
3387
3362
  }
3388
3363
  function abortFetcher(key) {
3389
3364
  let controller = fetchControllers.get(key);
3390
- invariant(controller, "Expected fetch controller: " + key);
3391
- controller.abort();
3392
- fetchControllers.delete(key);
3365
+ if (controller) {
3366
+ controller.abort();
3367
+ fetchControllers.delete(key);
3368
+ }
3393
3369
  }
3394
3370
  function markFetchersDone(keys) {
3395
3371
  for (let key of keys) {
@@ -3454,12 +3430,12 @@
3454
3430
  blockers
3455
3431
  });
3456
3432
  }
3457
- function shouldBlockNavigation(_ref4) {
3433
+ function shouldBlockNavigation(_ref2) {
3458
3434
  let {
3459
3435
  currentLocation,
3460
3436
  nextLocation,
3461
3437
  historyAction
3462
- } = _ref4;
3438
+ } = _ref2;
3463
3439
  if (blockerFunctions.size === 0) {
3464
3440
  return;
3465
3441
  }
@@ -3581,15 +3557,6 @@
3581
3557
  }
3582
3558
  function checkFogOfWar(matches, routesToUse, pathname) {
3583
3559
  if (patchRoutesOnNavigationImpl) {
3584
- // Don't bother re-calling patchRouteOnMiss for a path we've already
3585
- // processed. the last execution would have patched the route tree
3586
- // accordingly so `matches` here are already accurate.
3587
- if (discoveredRoutes.has(pathname)) {
3588
- return {
3589
- active: false,
3590
- matches
3591
- };
3592
- }
3593
3560
  if (!matches) {
3594
3561
  let fogMatches = matchRoutesImpl(routesToUse, pathname, basename, true);
3595
3562
  return {
@@ -3615,12 +3582,26 @@
3615
3582
  };
3616
3583
  }
3617
3584
  async function discoverRoutes(matches, pathname, signal) {
3585
+ if (!patchRoutesOnNavigationImpl) {
3586
+ return {
3587
+ type: "success",
3588
+ matches
3589
+ };
3590
+ }
3618
3591
  let partialMatches = matches;
3619
3592
  while (true) {
3620
3593
  let isNonHMR = inFlightDataRoutes == null;
3621
3594
  let routesToUse = inFlightDataRoutes || dataRoutes;
3595
+ let localManifest = manifest;
3622
3596
  try {
3623
- await loadLazyRouteChildren(patchRoutesOnNavigationImpl, pathname, partialMatches, routesToUse, manifest, mapRouteProperties, pendingPatchRoutes, signal);
3597
+ await patchRoutesOnNavigationImpl({
3598
+ path: pathname,
3599
+ matches: partialMatches,
3600
+ patch: (routeId, children) => {
3601
+ if (signal.aborted) return;
3602
+ patchRoutesImpl(routeId, children, routesToUse, localManifest, mapRouteProperties);
3603
+ }
3604
+ });
3624
3605
  } catch (e) {
3625
3606
  return {
3626
3607
  type: "error",
@@ -3634,7 +3615,7 @@
3634
3615
  // trigger a re-run of memoized `router.routes` dependencies.
3635
3616
  // HMR will already update the identity and reflow when it lands
3636
3617
  // `inFlightDataRoutes` in `completeNavigation`
3637
- if (isNonHMR) {
3618
+ if (isNonHMR && !signal.aborted) {
3638
3619
  dataRoutes = [...dataRoutes];
3639
3620
  }
3640
3621
  }
@@ -3645,7 +3626,6 @@
3645
3626
  }
3646
3627
  let newMatches = matchRoutes(routesToUse, pathname, basename);
3647
3628
  if (newMatches) {
3648
- addToFifoQueue(pathname, discoveredRoutes);
3649
3629
  return {
3650
3630
  type: "success",
3651
3631
  matches: newMatches
@@ -3655,7 +3635,6 @@
3655
3635
 
3656
3636
  // Avoid loops if the second pass results in the same partial matches
3657
3637
  if (!newPartialMatches || partialMatches.length === newPartialMatches.length && partialMatches.every((m, i) => m.route.id === newPartialMatches[i].route.id)) {
3658
- addToFifoQueue(pathname, discoveredRoutes);
3659
3638
  return {
3660
3639
  type: "success",
3661
3640
  matches: null
@@ -3664,13 +3643,6 @@
3664
3643
  partialMatches = newPartialMatches;
3665
3644
  }
3666
3645
  }
3667
- function addToFifoQueue(path, queue) {
3668
- if (queue.size >= discoveredRoutesMaxSize) {
3669
- let first = queue.values().next().value;
3670
- queue.delete(first);
3671
- }
3672
- queue.add(path);
3673
- }
3674
3646
  function _internalSetRoutes(newRoutes) {
3675
3647
  manifest = {};
3676
3648
  inFlightDataRoutes = convertRoutesToDataRoutes(newRoutes, mapRouteProperties, undefined, manifest);
@@ -3795,7 +3767,7 @@
3795
3767
  let {
3796
3768
  requestContext,
3797
3769
  skipLoaderErrorBubbling,
3798
- unstable_dataStrategy
3770
+ dataStrategy
3799
3771
  } = _temp3 === void 0 ? {} : _temp3;
3800
3772
  let url = new URL(request.url);
3801
3773
  let method = request.method;
@@ -3848,7 +3820,7 @@
3848
3820
  activeDeferreds: null
3849
3821
  };
3850
3822
  }
3851
- let result = await queryImpl(request, location, matches, requestContext, unstable_dataStrategy || null, skipLoaderErrorBubbling === true, null);
3823
+ let result = await queryImpl(request, location, matches, requestContext, dataStrategy || null, skipLoaderErrorBubbling === true, null);
3852
3824
  if (isResponse(result)) {
3853
3825
  return result;
3854
3826
  }
@@ -3892,7 +3864,7 @@
3892
3864
  let {
3893
3865
  routeId,
3894
3866
  requestContext,
3895
- unstable_dataStrategy
3867
+ dataStrategy
3896
3868
  } = _temp4 === void 0 ? {} : _temp4;
3897
3869
  let url = new URL(request.url);
3898
3870
  let method = request.method;
@@ -3921,7 +3893,7 @@
3921
3893
  pathname: location.pathname
3922
3894
  });
3923
3895
  }
3924
- let result = await queryImpl(request, location, matches, requestContext, unstable_dataStrategy || null, false, match);
3896
+ let result = await queryImpl(request, location, matches, requestContext, dataStrategy || null, false, match);
3925
3897
  if (isResponse(result)) {
3926
3898
  return result;
3927
3899
  }
@@ -3948,14 +3920,14 @@
3948
3920
  }
3949
3921
  return undefined;
3950
3922
  }
3951
- async function queryImpl(request, location, matches, requestContext, unstable_dataStrategy, skipLoaderErrorBubbling, routeMatch) {
3923
+ async function queryImpl(request, location, matches, requestContext, dataStrategy, skipLoaderErrorBubbling, routeMatch) {
3952
3924
  invariant(request.signal, "query()/queryRoute() requests must contain an AbortController signal");
3953
3925
  try {
3954
3926
  if (isMutationMethod(request.method.toLowerCase())) {
3955
- let result = await submit(request, matches, routeMatch || getTargetMatch(matches, location), requestContext, unstable_dataStrategy, skipLoaderErrorBubbling, routeMatch != null);
3927
+ let result = await submit(request, matches, routeMatch || getTargetMatch(matches, location), requestContext, dataStrategy, skipLoaderErrorBubbling, routeMatch != null);
3956
3928
  return result;
3957
3929
  }
3958
- let result = await loadRouteData(request, matches, requestContext, unstable_dataStrategy, skipLoaderErrorBubbling, routeMatch);
3930
+ let result = await loadRouteData(request, matches, requestContext, dataStrategy, skipLoaderErrorBubbling, routeMatch);
3959
3931
  return isResponse(result) ? result : _extends({}, result, {
3960
3932
  actionData: null,
3961
3933
  actionHeaders: {}
@@ -3978,7 +3950,7 @@
3978
3950
  throw e;
3979
3951
  }
3980
3952
  }
3981
- async function submit(request, matches, actionMatch, requestContext, unstable_dataStrategy, skipLoaderErrorBubbling, isRouteRequest) {
3953
+ async function submit(request, matches, actionMatch, requestContext, dataStrategy, skipLoaderErrorBubbling, isRouteRequest) {
3982
3954
  let result;
3983
3955
  if (!actionMatch.route.action && !actionMatch.route.lazy) {
3984
3956
  let error = getInternalRouterError(405, {
@@ -3994,7 +3966,7 @@
3994
3966
  error
3995
3967
  };
3996
3968
  } else {
3997
- let results = await callDataStrategy("action", request, [actionMatch], matches, isRouteRequest, requestContext, unstable_dataStrategy);
3969
+ let results = await callDataStrategy("action", request, [actionMatch], matches, isRouteRequest, requestContext, dataStrategy);
3998
3970
  result = results[actionMatch.route.id];
3999
3971
  if (request.signal.aborted) {
4000
3972
  throwStaticHandlerAbortedError(request, isRouteRequest, future);
@@ -4056,7 +4028,7 @@
4056
4028
  // Store off the pending error - we use it to determine which loaders
4057
4029
  // to call and will commit it when we complete the navigation
4058
4030
  let boundaryMatch = skipLoaderErrorBubbling ? actionMatch : findNearestBoundary(matches, actionMatch.route.id);
4059
- let context = await loadRouteData(loaderRequest, matches, requestContext, unstable_dataStrategy, skipLoaderErrorBubbling, null, [boundaryMatch.route.id, result]);
4031
+ let context = await loadRouteData(loaderRequest, matches, requestContext, dataStrategy, skipLoaderErrorBubbling, null, [boundaryMatch.route.id, result]);
4060
4032
 
4061
4033
  // action status codes take precedence over loader status codes
4062
4034
  return _extends({}, context, {
@@ -4067,7 +4039,7 @@
4067
4039
  } : {})
4068
4040
  });
4069
4041
  }
4070
- let context = await loadRouteData(loaderRequest, matches, requestContext, unstable_dataStrategy, skipLoaderErrorBubbling, null);
4042
+ let context = await loadRouteData(loaderRequest, matches, requestContext, dataStrategy, skipLoaderErrorBubbling, null);
4071
4043
  return _extends({}, context, {
4072
4044
  actionData: {
4073
4045
  [actionMatch.route.id]: result.data
@@ -4080,7 +4052,7 @@
4080
4052
  } : {}
4081
4053
  });
4082
4054
  }
4083
- async function loadRouteData(request, matches, requestContext, unstable_dataStrategy, skipLoaderErrorBubbling, routeMatch, pendingActionResult) {
4055
+ async function loadRouteData(request, matches, requestContext, dataStrategy, skipLoaderErrorBubbling, routeMatch, pendingActionResult) {
4084
4056
  let isRouteRequest = routeMatch != null;
4085
4057
 
4086
4058
  // Short circuit if we have no loaders to run (queryRoute())
@@ -4110,7 +4082,7 @@
4110
4082
  activeDeferreds: null
4111
4083
  };
4112
4084
  }
4113
- let results = await callDataStrategy("loader", request, matchesToLoad, matches, isRouteRequest, requestContext, unstable_dataStrategy);
4085
+ let results = await callDataStrategy("loader", request, matchesToLoad, matches, isRouteRequest, requestContext, dataStrategy);
4114
4086
  if (request.signal.aborted) {
4115
4087
  throwStaticHandlerAbortedError(request, isRouteRequest, future);
4116
4088
  }
@@ -4134,8 +4106,8 @@
4134
4106
 
4135
4107
  // Utility wrapper for calling dataStrategy server-side without having to
4136
4108
  // pass around the manifest, mapRouteProperties, etc.
4137
- async function callDataStrategy(type, request, matchesToLoad, matches, isRouteRequest, requestContext, unstable_dataStrategy) {
4138
- let results = await callDataStrategyImpl(unstable_dataStrategy || defaultDataStrategy, type, null, request, matchesToLoad, matches, null, manifest, mapRouteProperties, requestContext);
4109
+ async function callDataStrategy(type, request, matchesToLoad, matches, isRouteRequest, requestContext, dataStrategy) {
4110
+ let results = await callDataStrategyImpl(dataStrategy || defaultDataStrategy, type, null, request, matchesToLoad, matches, null, manifest, mapRouteProperties, requestContext);
4139
4111
  let dataResults = {};
4140
4112
  await Promise.all(matches.map(async match => {
4141
4113
  if (!(match.route.id in results)) {
@@ -4222,9 +4194,21 @@
4222
4194
  path.hash = location.hash;
4223
4195
  }
4224
4196
 
4225
- // Add an ?index param for matched index routes if we don't already have one
4226
- if ((to == null || to === "" || to === ".") && activeRouteMatch && activeRouteMatch.route.index && !hasNakedIndexQuery(path.search)) {
4227
- path.search = path.search ? path.search.replace(/^\?/, "?index&") : "?index";
4197
+ // Account for `?index` params when routing to the current location
4198
+ if ((to == null || to === "" || to === ".") && activeRouteMatch) {
4199
+ let nakedIndex = hasNakedIndexQuery(path.search);
4200
+ if (activeRouteMatch.route.index && !nakedIndex) {
4201
+ // Add one when we're targeting an index route
4202
+ path.search = path.search ? path.search.replace(/^\?/, "?index&") : "?index";
4203
+ } else if (!activeRouteMatch.route.index && nakedIndex) {
4204
+ // Remove existing ones when we're not
4205
+ let params = new URLSearchParams(path.search);
4206
+ let indexValues = params.getAll("index");
4207
+ params.delete("index");
4208
+ indexValues.filter(v => v).forEach(v => params.append("index", v));
4209
+ let qs = params.toString();
4210
+ path.search = qs ? "?" + qs : "";
4211
+ }
4228
4212
  }
4229
4213
 
4230
4214
  // If we're operating within a basename, prepend it to the pathname. If
@@ -4273,8 +4257,8 @@
4273
4257
  }
4274
4258
  let text = typeof opts.body === "string" ? opts.body : opts.body instanceof FormData || opts.body instanceof URLSearchParams ?
4275
4259
  // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#plain-text-form-data
4276
- Array.from(opts.body.entries()).reduce((acc, _ref5) => {
4277
- let [name, value] = _ref5;
4260
+ Array.from(opts.body.entries()).reduce((acc, _ref3) => {
4261
+ let [name, value] = _ref3;
4278
4262
  return "" + acc + name + "=" + value + "\n";
4279
4263
  }, "") : String(opts.body);
4280
4264
  return {
@@ -4364,26 +4348,37 @@
4364
4348
  };
4365
4349
  }
4366
4350
 
4367
- // Filter out all routes below any caught error as they aren't going to
4351
+ // Filter out all routes at/below any caught error as they aren't going to
4368
4352
  // render so we don't need to load them
4369
- function getLoaderMatchesUntilBoundary(matches, boundaryId) {
4370
- let boundaryMatches = matches;
4371
- if (boundaryId) {
4372
- let index = matches.findIndex(m => m.route.id === boundaryId);
4373
- if (index >= 0) {
4374
- boundaryMatches = matches.slice(0, index);
4375
- }
4353
+ function getLoaderMatchesUntilBoundary(matches, boundaryId, includeBoundary) {
4354
+ if (includeBoundary === void 0) {
4355
+ includeBoundary = false;
4356
+ }
4357
+ let index = matches.findIndex(m => m.route.id === boundaryId);
4358
+ if (index >= 0) {
4359
+ return matches.slice(0, includeBoundary ? index + 1 : index);
4376
4360
  }
4377
- return boundaryMatches;
4361
+ return matches;
4378
4362
  }
4379
- function getMatchesToLoad(history, state, matches, submission, location, isInitialLoad, skipActionErrorRevalidation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionResult) {
4363
+ function getMatchesToLoad(history, state, matches, submission, location, initialHydration, skipActionErrorRevalidation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionResult) {
4380
4364
  let actionResult = pendingActionResult ? isErrorResult(pendingActionResult[1]) ? pendingActionResult[1].error : pendingActionResult[1].data : undefined;
4381
4365
  let currentUrl = history.createURL(state.location);
4382
4366
  let nextUrl = history.createURL(location);
4383
4367
 
4384
4368
  // Pick navigation matches that are net-new or qualify for revalidation
4385
- let boundaryId = pendingActionResult && isErrorResult(pendingActionResult[1]) ? pendingActionResult[0] : undefined;
4386
- let boundaryMatches = boundaryId ? getLoaderMatchesUntilBoundary(matches, boundaryId) : matches;
4369
+ let boundaryMatches = matches;
4370
+ if (initialHydration && state.errors) {
4371
+ // On initial hydration, only consider matches up to _and including_ the boundary.
4372
+ // This is inclusive to handle cases where a server loader ran successfully,
4373
+ // a child server loader bubbled up to this route, but this route has
4374
+ // `clientLoader.hydrate` so we want to still run the `clientLoader` so that
4375
+ // we have a complete version of `loaderData`
4376
+ boundaryMatches = getLoaderMatchesUntilBoundary(matches, Object.keys(state.errors)[0], true);
4377
+ } else if (pendingActionResult && isErrorResult(pendingActionResult[1])) {
4378
+ // If an action threw an error, we call loaders up to, but not including the
4379
+ // boundary
4380
+ boundaryMatches = getLoaderMatchesUntilBoundary(matches, pendingActionResult[0]);
4381
+ }
4387
4382
 
4388
4383
  // Don't revalidate loaders by default after action 4xx/5xx responses
4389
4384
  // when the flag is enabled. They can still opt-into revalidation via
@@ -4401,13 +4396,8 @@
4401
4396
  if (route.loader == null) {
4402
4397
  return false;
4403
4398
  }
4404
- if (isInitialLoad) {
4405
- if (typeof route.loader !== "function" || route.loader.hydrate) {
4406
- return true;
4407
- }
4408
- return state.loaderData[route.id] === undefined && (
4409
- // Don't re-run if the loader ran and threw an error
4410
- !state.errors || state.errors[route.id] === undefined);
4399
+ if (initialHydration) {
4400
+ return shouldLoadRouteOnHydration(route, state.loaderData, state.errors);
4411
4401
  }
4412
4402
 
4413
4403
  // Always call the loader on new route instances and pending defer cancellations
@@ -4441,11 +4431,11 @@
4441
4431
  let revalidatingFetchers = [];
4442
4432
  fetchLoadMatches.forEach((f, key) => {
4443
4433
  // Don't revalidate:
4444
- // - on initial load (shouldn't be any fetchers then anyway)
4434
+ // - on initial hydration (shouldn't be any fetchers then anyway)
4445
4435
  // - if fetcher won't be present in the subsequent render
4446
4436
  // - no longer matches the URL (v7_fetcherPersist=false)
4447
4437
  // - was unmounted but persisted due to v7_fetcherPersist=true
4448
- if (isInitialLoad || !matches.some(m => m.route.id === f.routeId) || deletedFetchers.has(key)) {
4438
+ if (initialHydration || !matches.some(m => m.route.id === f.routeId) || deletedFetchers.has(key)) {
4449
4439
  return;
4450
4440
  }
4451
4441
  let fetcherMatches = matchRoutes(routesToUse, f.path, basename);
@@ -4511,6 +4501,32 @@
4511
4501
  });
4512
4502
  return [navigationMatches, revalidatingFetchers];
4513
4503
  }
4504
+ function shouldLoadRouteOnHydration(route, loaderData, errors) {
4505
+ // We dunno if we have a loader - gotta find out!
4506
+ if (route.lazy) {
4507
+ return true;
4508
+ }
4509
+
4510
+ // No loader, nothing to initialize
4511
+ if (!route.loader) {
4512
+ return false;
4513
+ }
4514
+ let hasData = loaderData != null && loaderData[route.id] !== undefined;
4515
+ let hasError = errors != null && errors[route.id] !== undefined;
4516
+
4517
+ // Don't run if we error'd during SSR
4518
+ if (!hasData && hasError) {
4519
+ return false;
4520
+ }
4521
+
4522
+ // Explicitly opting-in to running on hydration
4523
+ if (typeof route.loader === "function" && route.loader.hydrate === true) {
4524
+ return true;
4525
+ }
4526
+
4527
+ // Otherwise, run if we're not yet initialized with anything
4528
+ return !hasData && !hasError;
4529
+ }
4514
4530
  function isNewLoader(currentLoaderData, currentMatch, match) {
4515
4531
  let isNew =
4516
4532
  // [a] -> [a, b]
@@ -4544,49 +4560,50 @@
4544
4560
  }
4545
4561
  return arg.defaultShouldRevalidate;
4546
4562
  }
4547
-
4548
- /**
4549
- * Idempotent utility to execute patchRoutesOnNavigation() to lazily load route
4550
- * definitions and update the routes/routeManifest
4551
- */
4552
- async function loadLazyRouteChildren(patchRoutesOnNavigationImpl, path, matches, routes, manifest, mapRouteProperties, pendingRouteChildren, signal) {
4553
- let key = [path, ...matches.map(m => m.route.id)].join("-");
4554
- try {
4555
- let pending = pendingRouteChildren.get(key);
4556
- if (!pending) {
4557
- pending = patchRoutesOnNavigationImpl({
4558
- path,
4559
- matches,
4560
- patch: (routeId, children) => {
4561
- if (!signal.aborted) {
4562
- patchRoutesImpl(routeId, children, routes, manifest, mapRouteProperties);
4563
- }
4564
- }
4565
- });
4566
- pendingRouteChildren.set(key, pending);
4567
- }
4568
- if (pending && isPromise(pending)) {
4569
- await pending;
4570
- }
4571
- } finally {
4572
- pendingRouteChildren.delete(key);
4573
- }
4574
- }
4575
4563
  function patchRoutesImpl(routeId, children, routesToUse, manifest, mapRouteProperties) {
4564
+ var _childrenToPatch;
4565
+ let childrenToPatch;
4576
4566
  if (routeId) {
4577
- var _route$children;
4578
4567
  let route = manifest[routeId];
4579
4568
  invariant(route, "No route found to patch children into: routeId = " + routeId);
4580
- let dataChildren = convertRoutesToDataRoutes(children, mapRouteProperties, [routeId, "patch", String(((_route$children = route.children) == null ? void 0 : _route$children.length) || "0")], manifest);
4581
- if (route.children) {
4582
- route.children.push(...dataChildren);
4583
- } else {
4584
- route.children = dataChildren;
4569
+ if (!route.children) {
4570
+ route.children = [];
4585
4571
  }
4572
+ childrenToPatch = route.children;
4586
4573
  } else {
4587
- let dataChildren = convertRoutesToDataRoutes(children, mapRouteProperties, ["patch", String(routesToUse.length || "0")], manifest);
4588
- routesToUse.push(...dataChildren);
4574
+ childrenToPatch = routesToUse;
4575
+ }
4576
+
4577
+ // Don't patch in routes we already know about so that `patch` is idempotent
4578
+ // to simplify user-land code. This is useful because we re-call the
4579
+ // `patchRoutesOnNavigation` function for matched routes with params.
4580
+ let uniqueChildren = children.filter(newRoute => !childrenToPatch.some(existingRoute => isSameRoute(newRoute, existingRoute)));
4581
+ let newRoutes = convertRoutesToDataRoutes(uniqueChildren, mapRouteProperties, [routeId || "_", "patch", String(((_childrenToPatch = childrenToPatch) == null ? void 0 : _childrenToPatch.length) || "0")], manifest);
4582
+ childrenToPatch.push(...newRoutes);
4583
+ }
4584
+ function isSameRoute(newRoute, existingRoute) {
4585
+ // Most optimal check is by id
4586
+ if ("id" in newRoute && "id" in existingRoute && newRoute.id === existingRoute.id) {
4587
+ return true;
4588
+ }
4589
+
4590
+ // Second is by pathing differences
4591
+ if (!(newRoute.index === existingRoute.index && newRoute.path === existingRoute.path && newRoute.caseSensitive === existingRoute.caseSensitive)) {
4592
+ return false;
4593
+ }
4594
+
4595
+ // Pathless layout routes are trickier since we need to check children.
4596
+ // If they have no children then they're the same as far as we can tell
4597
+ if ((!newRoute.children || newRoute.children.length === 0) && (!existingRoute.children || existingRoute.children.length === 0)) {
4598
+ return true;
4589
4599
  }
4600
+
4601
+ // Otherwise, we look to see if every child in the new route is already
4602
+ // represented in the existing route's children
4603
+ return newRoute.children.every((aChild, i) => {
4604
+ var _existingRoute$childr;
4605
+ return (_existingRoute$childr = existingRoute.children) == null ? void 0 : _existingRoute$childr.some(bChild => isSameRoute(aChild, bChild));
4606
+ });
4590
4607
  }
4591
4608
 
4592
4609
  /**
@@ -4643,10 +4660,10 @@
4643
4660
  }
4644
4661
 
4645
4662
  // Default implementation of `dataStrategy` which fetches all loaders in parallel
4646
- async function defaultDataStrategy(_ref6) {
4663
+ async function defaultDataStrategy(_ref4) {
4647
4664
  let {
4648
4665
  matches
4649
- } = _ref6;
4666
+ } = _ref4;
4650
4667
  let matchesToLoad = matches.filter(m => m.shouldLoad);
4651
4668
  let results = await Promise.all(matchesToLoad.map(m => m.resolve()));
4652
4669
  return results.reduce((acc, result, i) => Object.assign(acc, {
@@ -4860,7 +4877,7 @@
4860
4877
  };
4861
4878
  }
4862
4879
 
4863
- // Convert thrown unstable_data() to ErrorResponse instances
4880
+ // Convert thrown data() to ErrorResponse instances
4864
4881
  result = new ErrorResponseImpl(((_result$init2 = result.init) == null ? void 0 : _result$init2.status) || 500, undefined, result.data);
4865
4882
  }
4866
4883
  return {
@@ -5060,7 +5077,7 @@
5060
5077
  loaderHeaders
5061
5078
  };
5062
5079
  }
5063
- function processLoaderData(state, matches, matchesToLoad, results, pendingActionResult, revalidatingFetchers, fetcherResults, activeDeferreds) {
5080
+ function processLoaderData(state, matches, results, pendingActionResult, revalidatingFetchers, fetcherResults, activeDeferreds) {
5064
5081
  let {
5065
5082
  loaderData,
5066
5083
  errors
@@ -5176,7 +5193,7 @@
5176
5193
  if (status === 400) {
5177
5194
  statusText = "Bad Request";
5178
5195
  if (type === "route-discovery") {
5179
- errorMessage = "Unable to match URL \"" + pathname + "\" - the `unstable_patchRoutesOnNavigation()` " + ("function threw the following error:\n" + message);
5196
+ errorMessage = "Unable to match URL \"" + pathname + "\" - the `patchRoutesOnNavigation()` " + ("function threw the following error:\n" + message);
5180
5197
  } else if (method && pathname && routeId) {
5181
5198
  errorMessage = "You made a " + method + " request to \"" + pathname + "\" but " + ("did not provide a `loader` for route \"" + routeId + "\", ") + "so there is no way to handle the request.";
5182
5199
  } else if (type === "defer-action") {
@@ -5239,9 +5256,6 @@
5239
5256
  // /page#hash -> /page
5240
5257
  return false;
5241
5258
  }
5242
- function isPromise(val) {
5243
- return typeof val === "object" && val != null && "then" in val;
5244
- }
5245
5259
  function isDataStrategyResult(result) {
5246
5260
  return result != null && typeof result === "object" && "type" in result && "result" in result && (result.type === ResultType.data || result.type === ResultType.error);
5247
5261
  }
@@ -5559,6 +5573,7 @@
5559
5573
  exports.createPath = createPath;
5560
5574
  exports.createRouter = createRouter;
5561
5575
  exports.createStaticHandler = createStaticHandler;
5576
+ exports.data = data;
5562
5577
  exports.defer = defer;
5563
5578
  exports.generatePath = generatePath;
5564
5579
  exports.getStaticContextFromError = getStaticContextFromError;
@@ -5578,7 +5593,6 @@
5578
5593
  exports.resolvePath = resolvePath;
5579
5594
  exports.resolveTo = resolveTo;
5580
5595
  exports.stripBasename = stripBasename;
5581
- exports.unstable_data = data;
5582
5596
 
5583
5597
  Object.defineProperty(exports, '__esModule', { value: true });
5584
5598