@remix-run/router 1.1.0 → 1.2.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.1.0
2
+ * @remix-run/router v1.2.0
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -1417,8 +1417,10 @@
1417
1417
  // we don't get the saved positions from <ScrollRestoration /> until _after_
1418
1418
  // the initial render, we need to manually trigger a separate updateState to
1419
1419
  // send along the restoreScrollPosition
1420
+ // Set to true if we have `hydrationData` since we assume we were SSR'd and that
1421
+ // SSR did the initial scroll restoration.
1420
1422
 
1421
- let initialScrollRestored = false;
1423
+ let initialScrollRestored = init.hydrationData != null;
1422
1424
  let initialMatches = matchRoutes(dataRoutes, init.history.location, init.basename);
1423
1425
  let initialErrors = null;
1424
1426
 
@@ -1446,7 +1448,8 @@
1446
1448
  matches: initialMatches,
1447
1449
  initialized,
1448
1450
  navigation: IDLE_NAVIGATION,
1449
- restoreScrollPosition: null,
1451
+ // Don't restore on initial updateState() if we were SSR'd
1452
+ restoreScrollPosition: init.hydrationData != null ? false : null,
1450
1453
  preventScrollReset: false,
1451
1454
  revalidation: "idle",
1452
1455
  loaderData: init.hydrationData && init.hydrationData.loaderData || {},
@@ -1555,14 +1558,30 @@
1555
1558
  // location, indicating we redirected from the action (avoids false
1556
1559
  // positives for loading/submissionRedirect when actionData returned
1557
1560
  // on a prior submission)
1558
- let isActionReload = state.actionData != null && state.navigation.formMethod != null && state.navigation.state === "loading" && ((_state$navigation$for = state.navigation.formAction) == null ? void 0 : _state$navigation$for.split("?")[0]) === location.pathname; // Always preserve any existing loaderData from re-used routes
1559
-
1560
- let newLoaderData = newState.loaderData ? {
1561
- loaderData: mergeLoaderData(state.loaderData, newState.loaderData, newState.matches || [])
1562
- } : {};
1563
- updateState(_extends({}, isActionReload ? {} : {
1564
- actionData: null
1565
- }, newState, newLoaderData, {
1561
+ let isActionReload = state.actionData != null && state.navigation.formMethod != null && state.navigation.state === "loading" && ((_state$navigation$for = state.navigation.formAction) == null ? void 0 : _state$navigation$for.split("?")[0]) === location.pathname;
1562
+ let actionData;
1563
+
1564
+ if (newState.actionData) {
1565
+ if (Object.keys(newState.actionData).length > 0) {
1566
+ actionData = newState.actionData;
1567
+ } else {
1568
+ // Empty actionData -> clear prior actionData due to an action error
1569
+ actionData = null;
1570
+ }
1571
+ } else if (isActionReload) {
1572
+ // Keep the current data if we're wrapping up the action reload
1573
+ actionData = state.actionData;
1574
+ } else {
1575
+ // Clear actionData on any other completed navigations
1576
+ actionData = null;
1577
+ } // Always preserve any existing loaderData from re-used routes
1578
+
1579
+
1580
+ let loaderData = newState.loaderData ? mergeLoaderData(state.loaderData, newState.loaderData, newState.matches || [], newState.errors) : state.loaderData;
1581
+ updateState(_extends({}, newState, {
1582
+ // matches, errors, fetchers go through as-is
1583
+ actionData,
1584
+ loaderData,
1566
1585
  historyAction: pendingAction,
1567
1586
  location,
1568
1587
  initialized: true,
@@ -1608,7 +1627,19 @@
1608
1627
  // without having to touch history
1609
1628
 
1610
1629
  location = _extends({}, location, init.history.encodeLocation(location));
1611
- let historyAction = (opts && opts.replace) === true || submission != null && isMutationMethod(submission.formMethod) ? exports.Action.Replace : exports.Action.Push;
1630
+ let userReplace = opts && opts.replace != null ? opts.replace : undefined;
1631
+ let historyAction = exports.Action.Push;
1632
+
1633
+ if (userReplace === true) {
1634
+ historyAction = exports.Action.Replace;
1635
+ } else if (userReplace === false) ; else if (submission != null && isMutationMethod(submission.formMethod) && submission.formAction === state.location.pathname + state.location.search) {
1636
+ // By default on submissions to the current location we REPLACE so that
1637
+ // users don't have to double-click the back button to get to the prior
1638
+ // location. If the user redirects to a different location from the
1639
+ // action/loader this will be ignored and the redirect will be a PUSH
1640
+ historyAction = exports.Action.Replace;
1641
+ }
1642
+
1612
1643
  let preventScrollReset = opts && "preventScrollReset" in opts ? opts.preventScrollReset === true : undefined;
1613
1644
  return await startNavigation(historyAction, location, {
1614
1645
  submission,
@@ -1752,11 +1783,14 @@
1752
1783
 
1753
1784
 
1754
1785
  pendingNavigationController = null;
1755
- completeNavigation(location, {
1756
- matches,
1786
+ completeNavigation(location, _extends({
1787
+ matches
1788
+ }, pendingActionData ? {
1789
+ actionData: pendingActionData
1790
+ } : {}, {
1757
1791
  loaderData,
1758
1792
  errors
1759
- });
1793
+ }));
1760
1794
  } // Call the action matched by the leaf route for this navigation and handle
1761
1795
  // redirects/errors
1762
1796
 
@@ -1796,7 +1830,18 @@
1796
1830
  }
1797
1831
 
1798
1832
  if (isRedirectResult(result)) {
1799
- await startRedirectNavigation(state, result, opts && opts.replace === true);
1833
+ let replace;
1834
+
1835
+ if (opts && opts.replace != null) {
1836
+ replace = opts.replace;
1837
+ } else {
1838
+ // If the user didn't explicity indicate replace behavior, replace if
1839
+ // we redirected to the exact same location we're currently at to avoid
1840
+ // double back-buttons
1841
+ replace = result.location === state.location.pathname + state.location.search;
1842
+ }
1843
+
1844
+ await startRedirectNavigation(state, result, replace);
1800
1845
  return {
1801
1846
  shortCircuited: true
1802
1847
  };
@@ -1815,6 +1860,8 @@
1815
1860
  }
1816
1861
 
1817
1862
  return {
1863
+ // Send back an empty object we can use to clear out any prior actionData
1864
+ pendingActionData: {},
1818
1865
  pendingActionError: {
1819
1866
  [boundaryMatch.route.id]: result.error
1820
1867
  }
@@ -1858,13 +1905,14 @@
1858
1905
  cancelActiveDeferreds(routeId => !(matches && matches.some(m => m.route.id === routeId)) || matchesToLoad && matchesToLoad.some(m => m.route.id === routeId)); // Short circuit if we have no loaders to run
1859
1906
 
1860
1907
  if (matchesToLoad.length === 0 && revalidatingFetchers.length === 0) {
1861
- completeNavigation(location, {
1908
+ completeNavigation(location, _extends({
1862
1909
  matches,
1863
- loaderData: mergeLoaderData(state.loaderData, {}, matches),
1910
+ loaderData: {},
1864
1911
  // Commit pending error if we're short circuiting
1865
- errors: pendingError || null,
1866
- actionData: pendingActionData || null
1867
- });
1912
+ errors: pendingError || null
1913
+ }, pendingActionData ? {
1914
+ actionData: pendingActionData
1915
+ } : {}));
1868
1916
  return {
1869
1917
  shortCircuited: true
1870
1918
  };
@@ -1884,14 +1932,19 @@
1884
1932
  formMethod: undefined,
1885
1933
  formAction: undefined,
1886
1934
  formEncType: undefined,
1887
- formData: undefined
1935
+ formData: undefined,
1936
+ " _hasFetcherDoneAnything ": true
1888
1937
  };
1889
1938
  state.fetchers.set(key, revalidatingFetcher);
1890
1939
  });
1940
+ let actionData = pendingActionData || state.actionData;
1891
1941
  updateState(_extends({
1892
- navigation: loadingNavigation,
1893
- actionData: pendingActionData || state.actionData || null
1894
- }, revalidatingFetchers.length > 0 ? {
1942
+ navigation: loadingNavigation
1943
+ }, actionData ? Object.keys(actionData).length === 0 ? {
1944
+ actionData: null
1945
+ } : {
1946
+ actionData
1947
+ } : {}, revalidatingFetchers.length > 0 ? {
1895
1948
  fetchers: new Map(state.fetchers)
1896
1949
  } : {}));
1897
1950
  }
@@ -2015,7 +2068,8 @@
2015
2068
  let fetcher = _extends({
2016
2069
  state: "submitting"
2017
2070
  }, submission, {
2018
- data: existingFetcher && existingFetcher.data
2071
+ data: existingFetcher && existingFetcher.data,
2072
+ " _hasFetcherDoneAnything ": true
2019
2073
  });
2020
2074
 
2021
2075
  state.fetchers.set(key, fetcher);
@@ -2045,14 +2099,15 @@
2045
2099
  let loadingFetcher = _extends({
2046
2100
  state: "loading"
2047
2101
  }, submission, {
2048
- data: undefined
2102
+ data: undefined,
2103
+ " _hasFetcherDoneAnything ": true
2049
2104
  });
2050
2105
 
2051
2106
  state.fetchers.set(key, loadingFetcher);
2052
2107
  updateState({
2053
2108
  fetchers: new Map(state.fetchers)
2054
2109
  });
2055
- return startRedirectNavigation(state, actionResult);
2110
+ return startRedirectNavigation(state, actionResult, false, true);
2056
2111
  } // Process any non-redirect errors thrown
2057
2112
 
2058
2113
 
@@ -2077,7 +2132,9 @@
2077
2132
  let loadFetcher = _extends({
2078
2133
  state: "loading",
2079
2134
  data: actionResult.data
2080
- }, submission);
2135
+ }, submission, {
2136
+ " _hasFetcherDoneAnything ": true
2137
+ });
2081
2138
 
2082
2139
  state.fetchers.set(key, loadFetcher);
2083
2140
  let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(state, matches, submission, nextLocation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, {
@@ -2099,7 +2156,8 @@
2099
2156
  formMethod: undefined,
2100
2157
  formAction: undefined,
2101
2158
  formEncType: undefined,
2102
- formData: undefined
2159
+ formData: undefined,
2160
+ " _hasFetcherDoneAnything ": true
2103
2161
  };
2104
2162
  state.fetchers.set(staleKey, revalidatingFetcher);
2105
2163
  fetchControllers.set(staleKey, abortController);
@@ -2140,7 +2198,8 @@
2140
2198
  formMethod: undefined,
2141
2199
  formAction: undefined,
2142
2200
  formEncType: undefined,
2143
- formData: undefined
2201
+ formData: undefined,
2202
+ " _hasFetcherDoneAnything ": true
2144
2203
  };
2145
2204
  state.fetchers.set(key, doneFetcher);
2146
2205
  let didAbortFetchLoads = abortStaleFetchLoads(loadId); // If we are currently in a navigation loading state and this fetcher is
@@ -2162,7 +2221,7 @@
2162
2221
  // manually merge here since we aren't going through completeNavigation
2163
2222
  updateState(_extends({
2164
2223
  errors,
2165
- loaderData: mergeLoaderData(state.loaderData, loaderData, matches)
2224
+ loaderData: mergeLoaderData(state.loaderData, loaderData, matches, errors)
2166
2225
  }, didAbortFetchLoads ? {
2167
2226
  fetchers: new Map(state.fetchers)
2168
2227
  } : {}));
@@ -2181,7 +2240,8 @@
2181
2240
  formEncType: undefined,
2182
2241
  formData: undefined
2183
2242
  }, submission, {
2184
- data: existingFetcher && existingFetcher.data
2243
+ data: existingFetcher && existingFetcher.data,
2244
+ " _hasFetcherDoneAnything ": true
2185
2245
  });
2186
2246
 
2187
2247
  state.fetchers.set(key, loadingFetcher);
@@ -2241,7 +2301,8 @@
2241
2301
  formMethod: undefined,
2242
2302
  formAction: undefined,
2243
2303
  formEncType: undefined,
2244
- formData: undefined
2304
+ formData: undefined,
2305
+ " _hasFetcherDoneAnything ": true
2245
2306
  };
2246
2307
  state.fetchers.set(key, doneFetcher);
2247
2308
  updateState({
@@ -2269,14 +2330,19 @@
2269
2330
  */
2270
2331
 
2271
2332
 
2272
- async function startRedirectNavigation(state, redirect, replace) {
2333
+ async function startRedirectNavigation(state, redirect, replace, isFetchActionRedirect) {
2273
2334
  var _window;
2274
2335
 
2275
2336
  if (redirect.revalidate) {
2276
2337
  isRevalidationRequired = true;
2277
2338
  }
2278
2339
 
2279
- let redirectLocation = createLocation(state.location, redirect.location);
2340
+ let redirectLocation = createLocation(state.location, redirect.location, // TODO: This can be removed once we get rid of useTransition in Remix v2
2341
+ _extends({
2342
+ _isRedirect: true
2343
+ }, isFetchActionRedirect ? {
2344
+ _isFetchActionRedirect: true
2345
+ } : {}));
2280
2346
  invariant(redirectLocation, "Expected a location on the redirect navigation"); // Check if this an external redirect that goes to a new origin
2281
2347
 
2282
2348
  if (typeof ((_window = window) == null ? void 0 : _window.location) !== "undefined") {
@@ -2402,7 +2468,8 @@
2402
2468
  formMethod: undefined,
2403
2469
  formAction: undefined,
2404
2470
  formEncType: undefined,
2405
- formData: undefined
2471
+ formData: undefined,
2472
+ " _hasFetcherDoneAnything ": true
2406
2473
  };
2407
2474
  state.fetchers.set(key, doneFetcher);
2408
2475
  }
@@ -2545,8 +2612,8 @@
2545
2612
  //#region createStaticHandler
2546
2613
  ////////////////////////////////////////////////////////////////////////////////
2547
2614
 
2548
- function unstable_createStaticHandler(routes, opts) {
2549
- invariant(routes.length > 0, "You must provide a non-empty routes array to unstable_createStaticHandler");
2615
+ function createStaticHandler(routes, opts) {
2616
+ invariant(routes.length > 0, "You must provide a non-empty routes array to createStaticHandler");
2550
2617
  let dataRoutes = convertRoutesToDataRoutes(routes);
2551
2618
  let basename = (opts ? opts.basename : null) || "/";
2552
2619
  /**
@@ -2868,7 +2935,10 @@
2868
2935
  if (matchesToLoad.length === 0) {
2869
2936
  return {
2870
2937
  matches,
2871
- loaderData: {},
2938
+ // Add a null for all matched routes for proper revalidation on the client
2939
+ loaderData: matches.reduce((acc, m) => Object.assign(acc, {
2940
+ [m.route.id]: null
2941
+ }), {}),
2872
2942
  errors: pendingActionError || null,
2873
2943
  statusCode: 200,
2874
2944
  loaderHeaders: {}
@@ -2880,17 +2950,25 @@
2880
2950
  if (request.signal.aborted) {
2881
2951
  let method = isRouteRequest ? "queryRoute" : "query";
2882
2952
  throw new Error(method + "() call aborted");
2883
- } // Can't do anything with these without the Remix side of things, so just
2884
- // cancel them for now
2953
+ }
2885
2954
 
2955
+ let executedLoaders = new Set();
2956
+ results.forEach((result, i) => {
2957
+ executedLoaders.add(matchesToLoad[i].route.id); // Can't do anything with these without the Remix side of things, so just
2958
+ // cancel them for now
2886
2959
 
2887
- results.forEach(result => {
2888
2960
  if (isDeferredResult(result)) {
2889
2961
  result.deferredData.cancel();
2890
2962
  }
2891
2963
  }); // Process and commit output from loaders
2892
2964
 
2893
- let context = processRouteLoaderData(matches, matchesToLoad, results, pendingActionError);
2965
+ let context = processRouteLoaderData(matches, matchesToLoad, results, pendingActionError); // Add a null for any non-loader matches for proper revalidation on the client
2966
+
2967
+ matches.forEach(match => {
2968
+ if (!executedLoaders.has(match.route.id)) {
2969
+ context.loaderData[match.route.id] = null;
2970
+ }
2971
+ });
2894
2972
  return _extends({}, context, {
2895
2973
  matches
2896
2974
  });
@@ -3012,7 +3090,7 @@
3012
3090
  }
3013
3091
 
3014
3092
  function getMatchesToLoad(state, matches, submission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, pendingActionData, pendingError, fetchLoadMatches) {
3015
- let actionResult = pendingError ? Object.values(pendingError)[0] : pendingActionData ? Object.values(pendingActionData)[0] : null; // Pick navigation matches that are net-new or qualify for revalidation
3093
+ let actionResult = pendingError ? Object.values(pendingError)[0] : pendingActionData ? Object.values(pendingActionData)[0] : undefined; // Pick navigation matches that are net-new or qualify for revalidation
3016
3094
 
3017
3095
  let boundaryId = pendingError ? Object.keys(pendingError)[0] : undefined;
3018
3096
  let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId);
@@ -3182,9 +3260,10 @@
3182
3260
  }
3183
3261
 
3184
3262
  let data;
3185
- let contentType = result.headers.get("Content-Type");
3263
+ let contentType = result.headers.get("Content-Type"); // Check between word boundaries instead of startsWith() due to the last
3264
+ // paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type
3186
3265
 
3187
- if (contentType && contentType.startsWith("application/json")) {
3266
+ if (contentType && /\bapplication\/json\b/.test(contentType)) {
3188
3267
  data = await result.json();
3189
3268
  } else {
3190
3269
  data = await result.text();
@@ -3289,9 +3368,11 @@
3289
3368
 
3290
3369
  if (errors[boundaryMatch.route.id] == null) {
3291
3370
  errors[boundaryMatch.route.id] = error;
3292
- } // Once we find our first (highest) error, we set the status code and
3293
- // prevent deeper status codes from overriding
3371
+ } // Clear our any prior loaderData for the throwing route
3372
+
3294
3373
 
3374
+ loaderData[id] = undefined; // Once we find our first (highest) error, we set the status code and
3375
+ // prevent deeper status codes from overriding
3295
3376
 
3296
3377
  if (!foundError) {
3297
3378
  foundError = true;
@@ -3317,10 +3398,12 @@
3317
3398
  }
3318
3399
  }
3319
3400
  }); // If we didn't consume the pending action error (i.e., all loaders
3320
- // resolved), then consume it here
3401
+ // resolved), then consume it here. Also clear out any loaderData for the
3402
+ // throwing route
3321
3403
 
3322
3404
  if (pendingError) {
3323
3405
  errors = pendingError;
3406
+ loaderData[Object.keys(pendingError)[0]] = undefined;
3324
3407
  }
3325
3408
 
3326
3409
  return {
@@ -3367,7 +3450,8 @@
3367
3450
  formMethod: undefined,
3368
3451
  formAction: undefined,
3369
3452
  formEncType: undefined,
3370
- formData: undefined
3453
+ formData: undefined,
3454
+ " _hasFetcherDoneAnything ": true
3371
3455
  };
3372
3456
  state.fetchers.set(key, doneFetcher);
3373
3457
  }
@@ -3379,16 +3463,26 @@
3379
3463
  };
3380
3464
  }
3381
3465
 
3382
- function mergeLoaderData(loaderData, newLoaderData, matches) {
3466
+ function mergeLoaderData(loaderData, newLoaderData, matches, errors) {
3383
3467
  let mergedLoaderData = _extends({}, newLoaderData);
3384
3468
 
3385
- matches.forEach(match => {
3469
+ for (let match of matches) {
3386
3470
  let id = match.route.id;
3387
3471
 
3388
- if (newLoaderData[id] === undefined && loaderData[id] !== undefined) {
3472
+ if (newLoaderData.hasOwnProperty(id)) {
3473
+ if (newLoaderData[id] !== undefined) {
3474
+ mergedLoaderData[id] = newLoaderData[id];
3475
+ }
3476
+ } else if (loaderData[id] !== undefined) {
3389
3477
  mergedLoaderData[id] = loaderData[id];
3390
3478
  }
3391
- });
3479
+
3480
+ if (errors && errors.hasOwnProperty(id)) {
3481
+ // Don't keep any loader data below the boundary
3482
+ break;
3483
+ }
3484
+ }
3485
+
3392
3486
  return mergedLoaderData;
3393
3487
  } // Find the nearest error boundary, looking upwards from the leaf route (or the
3394
3488
  // route specified by routeId) for the closest ancestor error boundary,
@@ -3610,6 +3704,7 @@
3610
3704
  exports.createMemoryHistory = createMemoryHistory;
3611
3705
  exports.createPath = createPath;
3612
3706
  exports.createRouter = createRouter;
3707
+ exports.createStaticHandler = createStaticHandler;
3613
3708
  exports.defer = defer;
3614
3709
  exports.generatePath = generatePath;
3615
3710
  exports.getStaticContextFromError = getStaticContextFromError;
@@ -3626,7 +3721,6 @@
3626
3721
  exports.resolvePath = resolvePath;
3627
3722
  exports.resolveTo = resolveTo;
3628
3723
  exports.stripBasename = stripBasename;
3629
- exports.unstable_createStaticHandler = unstable_createStaticHandler;
3630
3724
  exports.warning = warning;
3631
3725
 
3632
3726
  Object.defineProperty(exports, '__esModule', { value: true });