@remix-run/router 1.3.0-pre.1 → 1.3.0-pre.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # `@remix-run/router`
2
2
 
3
+ ## 1.3.0-pre.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Fix 404 bug with same-origin absolute redirects ([#9913](https://github.com/remix-run/react-router/pull/9913))
8
+ - Support `OPTIONS` requests in `staticHandler.queryRoute` ([#9914](https://github.com/remix-run/react-router/pull/9914))
9
+
10
+ ## 1.3.0-pre.2
11
+
12
+ ### Minor Changes
13
+
14
+ - Added support for navigation blocking APIs ([#9709](https://github.com/remix-run/react-router/pull/9709))
15
+
3
16
  ## 1.3.0-pre.1
4
17
 
5
18
  ### Patch Changes
package/dist/history.d.ts CHANGED
@@ -68,6 +68,10 @@ export interface Update {
68
68
  * The new location.
69
69
  */
70
70
  location: Location;
71
+ /**
72
+ * The delta between this location and the former location in the history stack
73
+ */
74
+ delta: number;
71
75
  }
72
76
  /**
73
77
  * A function that receives notifications about location changes.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @remix-run/router v1.3.0-pre.1
2
+ * @remix-run/router v1.3.0-pre.3
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -137,7 +137,8 @@ function createMemoryHistory(options) {
137
137
  if (v5Compat && listener) {
138
138
  listener({
139
139
  action,
140
- location: nextLocation
140
+ location: nextLocation,
141
+ delta: 1
141
142
  });
142
143
  }
143
144
  },
@@ -150,19 +151,23 @@ function createMemoryHistory(options) {
150
151
  if (v5Compat && listener) {
151
152
  listener({
152
153
  action,
153
- location: nextLocation
154
+ location: nextLocation,
155
+ delta: 0
154
156
  });
155
157
  }
156
158
  },
157
159
 
158
160
  go(delta) {
159
161
  action = exports.Action.Pop;
160
- index = clampIndex(index + delta);
162
+ let nextIndex = clampIndex(index + delta);
163
+ let nextLocation = entries[nextIndex];
164
+ index = nextIndex;
161
165
 
162
166
  if (listener) {
163
167
  listener({
164
168
  action,
165
- location: getCurrentLocation()
169
+ location: nextLocation,
170
+ delta
166
171
  });
167
172
  }
168
173
  },
@@ -321,10 +326,11 @@ function createKey() {
321
326
  */
322
327
 
323
328
 
324
- function getHistoryState(location) {
329
+ function getHistoryState(location, index) {
325
330
  return {
326
331
  usr: location.state,
327
- key: location.key
332
+ key: location.key,
333
+ idx: index
328
334
  };
329
335
  }
330
336
  /**
@@ -408,15 +414,45 @@ function getUrlBasedHistory(getLocation, createHref, validateLocation, options)
408
414
  let globalHistory = window.history;
409
415
  let action = exports.Action.Pop;
410
416
  let listener = null;
417
+ let index = getIndex(); // Index should only be null when we initialize. If not, it's because the
418
+ // user called history.pushState or history.replaceState directly, in which
419
+ // case we should log a warning as it will result in bugs.
420
+
421
+ if (index == null) {
422
+ index = 0;
423
+ globalHistory.replaceState(_extends({}, globalHistory.state, {
424
+ idx: index
425
+ }), "");
426
+ }
427
+
428
+ function getIndex() {
429
+ let state = globalHistory.state || {
430
+ idx: null
431
+ };
432
+ return state.idx;
433
+ }
411
434
 
412
435
  function handlePop() {
413
- action = exports.Action.Pop;
436
+ let nextAction = exports.Action.Pop;
437
+ let nextIndex = getIndex();
414
438
 
415
- if (listener) {
416
- listener({
417
- action,
418
- location: history.location
419
- });
439
+ if (nextIndex != null) {
440
+ let delta = nextIndex - index;
441
+ action = nextAction;
442
+ index = nextIndex;
443
+
444
+ if (listener) {
445
+ listener({
446
+ action,
447
+ location: history.location,
448
+ delta
449
+ });
450
+ }
451
+ } else {
452
+ warning$1(false, // TODO: Write up a doc that explains our blocking strategy in detail
453
+ // and link to it here so people can understand better what is going on
454
+ // and how to avoid it.
455
+ "You are trying to block a POP navigation to a location that was not " + "created by @remix-run/router. The block will fail silently in " + "production, but in general you should do all navigation with the " + "router (instead of using window.history.pushState directly) " + "to avoid this situation.");
420
456
  }
421
457
  }
422
458
 
@@ -424,7 +460,8 @@ function getUrlBasedHistory(getLocation, createHref, validateLocation, options)
424
460
  action = exports.Action.Push;
425
461
  let location = createLocation(history.location, to, state);
426
462
  if (validateLocation) validateLocation(location, to);
427
- let historyState = getHistoryState(location);
463
+ index = getIndex() + 1;
464
+ let historyState = getHistoryState(location, index);
428
465
  let url = history.createHref(location); // try...catch because iOS limits us to 100 pushState calls :/
429
466
 
430
467
  try {
@@ -438,7 +475,8 @@ function getUrlBasedHistory(getLocation, createHref, validateLocation, options)
438
475
  if (v5Compat && listener) {
439
476
  listener({
440
477
  action,
441
- location: history.location
478
+ location: history.location,
479
+ delta: 1
442
480
  });
443
481
  }
444
482
  }
@@ -447,14 +485,16 @@ function getUrlBasedHistory(getLocation, createHref, validateLocation, options)
447
485
  action = exports.Action.Replace;
448
486
  let location = createLocation(history.location, to, state);
449
487
  if (validateLocation) validateLocation(location, to);
450
- let historyState = getHistoryState(location);
488
+ index = getIndex();
489
+ let historyState = getHistoryState(location, index);
451
490
  let url = history.createHref(location);
452
491
  globalHistory.replaceState(historyState, "", url);
453
492
 
454
493
  if (v5Compat && listener) {
455
494
  listener({
456
495
  action,
457
- location: history.location
496
+ location: history.location,
497
+ delta: 0
458
498
  });
459
499
  }
460
500
  }
@@ -990,7 +1030,7 @@ function warning(cond, message) {
990
1030
  if (typeof console !== "undefined") console.warn(message);
991
1031
 
992
1032
  try {
993
- // Welcome to debugging React Router!
1033
+ // Welcome to debugging @remix-run/router!
994
1034
  //
995
1035
  // This error is thrown as a convenience so you can more easily
996
1036
  // find the source for a warning that appears in the console by
@@ -1433,6 +1473,12 @@ const IDLE_FETCHER = {
1433
1473
  formEncType: undefined,
1434
1474
  formData: undefined
1435
1475
  };
1476
+ const IDLE_BLOCKER = {
1477
+ state: "unblocked",
1478
+ proceed: undefined,
1479
+ reset: undefined,
1480
+ location: undefined
1481
+ };
1436
1482
  const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined";
1437
1483
  const isServer = !isBrowser; //#endregion
1438
1484
  ////////////////////////////////////////////////////////////////////////////////
@@ -1497,7 +1543,8 @@ function createRouter(init) {
1497
1543
  loaderData: init.hydrationData && init.hydrationData.loaderData || {},
1498
1544
  actionData: init.hydrationData && init.hydrationData.actionData || null,
1499
1545
  errors: init.hydrationData && init.hydrationData.errors || initialErrors,
1500
- fetchers: new Map()
1546
+ fetchers: new Map(),
1547
+ blockers: new Map()
1501
1548
  }; // -- Stateful internal variables to manage navigations --
1502
1549
  // Current navigation in progress (to be committed in completeNavigation)
1503
1550
 
@@ -1539,7 +1586,16 @@ function createRouter(init) {
1539
1586
  // promise resolves we update loaderData. If a new navigation starts we
1540
1587
  // cancel active deferreds for eliminated routes.
1541
1588
 
1542
- let activeDeferreds = new Map(); // Initialize the router, all side effects should be kicked off from here.
1589
+ let activeDeferreds = new Map(); // We ony support a single active blocker at the moment since we don't have
1590
+ // any compelling use cases for multi-blocker yet
1591
+
1592
+ let activeBlocker = null; // Store blocker functions in a separate Map outside of router state since
1593
+ // we don't need to update UI state if they change
1594
+
1595
+ let blockerFunctions = new Map(); // Flag to ignore the next history update, so we can revert the URL change on
1596
+ // a POP navigation that was blocked by the user without touching router state
1597
+
1598
+ let ignoreNextHistoryUpdate = false; // Initialize the router, all side effects should be kicked off from here.
1543
1599
  // Implemented as a Fluent API for ease of:
1544
1600
  // let router = createRouter(init).initialize();
1545
1601
 
@@ -1549,8 +1605,54 @@ function createRouter(init) {
1549
1605
  unlistenHistory = init.history.listen(_ref => {
1550
1606
  let {
1551
1607
  action: historyAction,
1552
- location
1608
+ location,
1609
+ delta
1553
1610
  } = _ref;
1611
+
1612
+ // Ignore this event if it was just us resetting the URL from a
1613
+ // blocked POP navigation
1614
+ if (ignoreNextHistoryUpdate) {
1615
+ ignoreNextHistoryUpdate = false;
1616
+ return;
1617
+ }
1618
+
1619
+ let blockerKey = shouldBlockNavigation({
1620
+ currentLocation: state.location,
1621
+ nextLocation: location,
1622
+ historyAction
1623
+ });
1624
+
1625
+ if (blockerKey) {
1626
+ // Restore the URL to match the current UI, but don't update router state
1627
+ ignoreNextHistoryUpdate = true;
1628
+ init.history.go(delta * -1); // Put the blocker into a blocked state
1629
+
1630
+ updateBlocker(blockerKey, {
1631
+ state: "blocked",
1632
+ location,
1633
+
1634
+ proceed() {
1635
+ updateBlocker(blockerKey, {
1636
+ state: "proceeding",
1637
+ proceed: undefined,
1638
+ reset: undefined,
1639
+ location
1640
+ }); // Re-do the same POP navigation we just blocked
1641
+
1642
+ init.history.go(delta);
1643
+ },
1644
+
1645
+ reset() {
1646
+ deleteBlocker(blockerKey);
1647
+ updateState({
1648
+ blockers: new Map(router.state.blockers)
1649
+ });
1650
+ }
1651
+
1652
+ });
1653
+ return;
1654
+ }
1655
+
1554
1656
  return startNavigation(historyAction, location);
1555
1657
  }); // Kick off initial data load if needed. Use Pop to avoid modifying history
1556
1658
 
@@ -1570,6 +1672,7 @@ function createRouter(init) {
1570
1672
  subscribers.clear();
1571
1673
  pendingNavigationController && pendingNavigationController.abort();
1572
1674
  state.fetchers.forEach((_, key) => deleteFetcher(key));
1675
+ state.blockers.forEach((_, key) => deleteBlocker(key));
1573
1676
  } // Subscribe to state updates for the router
1574
1677
 
1575
1678
 
@@ -1616,9 +1719,15 @@ function createRouter(init) {
1616
1719
  } // Always preserve any existing loaderData from re-used routes
1617
1720
 
1618
1721
 
1619
- let loaderData = newState.loaderData ? mergeLoaderData(state.loaderData, newState.loaderData, newState.matches || [], newState.errors) : state.loaderData; // Always respect the user flag. Otherwise don't reset on mutation
1722
+ let loaderData = newState.loaderData ? mergeLoaderData(state.loaderData, newState.loaderData, newState.matches || [], newState.errors) : state.loaderData; // On a successful navigation we can assume we got through all blockers
1723
+ // so we can start fresh
1724
+
1725
+ for (let [key] of blockerFunctions) {
1726
+ deleteBlocker(key);
1727
+ } // Always respect the user flag. Otherwise don't reset on mutation
1620
1728
  // submission navigations unless they redirect
1621
1729
 
1730
+
1622
1731
  let preventScrollReset = pendingPreventScrollReset === true || state.navigation.formMethod != null && isMutationMethod(state.navigation.formMethod) && ((_location$state2 = location.state) == null ? void 0 : _location$state2._isRedirect) !== true;
1623
1732
  updateState(_extends({}, newState, {
1624
1733
  // matches, errors, fetchers go through as-is
@@ -1630,7 +1739,8 @@ function createRouter(init) {
1630
1739
  navigation: IDLE_NAVIGATION,
1631
1740
  revalidation: "idle",
1632
1741
  restoreScrollPosition: getSavedScrollPosition(location, newState.matches || state.matches),
1633
- preventScrollReset
1742
+ preventScrollReset,
1743
+ blockers: new Map(state.blockers)
1634
1744
  }));
1635
1745
 
1636
1746
  if (isUninterruptedRevalidation) ; else if (pendingAction === exports.Action.Pop) ; else if (pendingAction === exports.Action.Push) {
@@ -1661,13 +1771,14 @@ function createRouter(init) {
1661
1771
  submission,
1662
1772
  error
1663
1773
  } = normalizeNavigateOptions(to, opts);
1664
- let location = createLocation(state.location, path, opts && opts.state); // When using navigate as a PUSH/REPLACE we aren't reading an already-encoded
1774
+ let currentLocation = state.location;
1775
+ let nextLocation = createLocation(state.location, path, opts && opts.state); // When using navigate as a PUSH/REPLACE we aren't reading an already-encoded
1665
1776
  // URL from window.location, so we need to encode it here so the behavior
1666
1777
  // remains the same as POP and non-data-router usages. new URL() does all
1667
1778
  // the same encoding we'd get from a history.pushState/window.location read
1668
1779
  // without having to touch history
1669
1780
 
1670
- location = _extends({}, location, init.history.encodeLocation(location));
1781
+ nextLocation = _extends({}, nextLocation, init.history.encodeLocation(nextLocation));
1671
1782
  let userReplace = opts && opts.replace != null ? opts.replace : undefined;
1672
1783
  let historyAction = exports.Action.Push;
1673
1784
 
@@ -1682,7 +1793,41 @@ function createRouter(init) {
1682
1793
  }
1683
1794
 
1684
1795
  let preventScrollReset = opts && "preventScrollReset" in opts ? opts.preventScrollReset === true : undefined;
1685
- return await startNavigation(historyAction, location, {
1796
+ let blockerKey = shouldBlockNavigation({
1797
+ currentLocation,
1798
+ nextLocation,
1799
+ historyAction
1800
+ });
1801
+
1802
+ if (blockerKey) {
1803
+ // Put the blocker into a blocked state
1804
+ updateBlocker(blockerKey, {
1805
+ state: "blocked",
1806
+ location: nextLocation,
1807
+
1808
+ proceed() {
1809
+ updateBlocker(blockerKey, {
1810
+ state: "proceeding",
1811
+ proceed: undefined,
1812
+ reset: undefined,
1813
+ location: nextLocation
1814
+ }); // Send the same navigation through
1815
+
1816
+ navigate(to, opts);
1817
+ },
1818
+
1819
+ reset() {
1820
+ deleteBlocker(blockerKey);
1821
+ updateState({
1822
+ blockers: new Map(state.blockers)
1823
+ });
1824
+ }
1825
+
1826
+ });
1827
+ return;
1828
+ }
1829
+
1830
+ return await startNavigation(historyAction, nextLocation, {
1686
1831
  submission,
1687
1832
  // Send through the formData serialization error if we have one so we can
1688
1833
  // render at the right error boundary after we match routes
@@ -2411,7 +2556,7 @@ function createRouter(init) {
2411
2556
  } : {}));
2412
2557
  invariant(redirectLocation, "Expected a location on the redirect navigation"); // Check if this an external redirect that goes to a new origin
2413
2558
 
2414
- if (typeof ((_window = window) == null ? void 0 : _window.location) !== "undefined") {
2559
+ if (isBrowser && typeof ((_window = window) == null ? void 0 : _window.location) !== "undefined") {
2415
2560
  let newOrigin = init.history.createURL(redirect.location).origin;
2416
2561
 
2417
2562
  if (window.location.origin !== newOrigin) {
@@ -2590,6 +2735,78 @@ function createRouter(init) {
2590
2735
  return yeetedKeys.length > 0;
2591
2736
  }
2592
2737
 
2738
+ function getBlocker(key, fn) {
2739
+ let blocker = state.blockers.get(key) || IDLE_BLOCKER;
2740
+
2741
+ if (blockerFunctions.get(key) !== fn) {
2742
+ blockerFunctions.set(key, fn);
2743
+
2744
+ if (activeBlocker == null) {
2745
+ // This is now the active blocker
2746
+ activeBlocker = key;
2747
+ } else if (key !== activeBlocker) {
2748
+ warning(false, "A router only supports one blocker at a time");
2749
+ }
2750
+ }
2751
+
2752
+ return blocker;
2753
+ }
2754
+
2755
+ function deleteBlocker(key) {
2756
+ state.blockers.delete(key);
2757
+ blockerFunctions.delete(key);
2758
+
2759
+ if (activeBlocker === key) {
2760
+ activeBlocker = null;
2761
+ }
2762
+ } // Utility function to update blockers, ensuring valid state transitions
2763
+
2764
+
2765
+ function updateBlocker(key, newBlocker) {
2766
+ let blocker = state.blockers.get(key) || IDLE_BLOCKER; // Poor mans state machine :)
2767
+ // https://mermaid.live/edit#pako:eNqVkc9OwzAMxl8l8nnjAYrEtDIOHEBIgwvKJTReGy3_lDpIqO27k6awMG0XcrLlnz87nwdonESogKXXBuE79rq75XZO3-yHds0RJVuv70YrPlUrCEe2HfrORS3rubqZfuhtpg5C9wk5tZ4VKcRUq88q9Z8RS0-48cE1iHJkL0ugbHuFLus9L6spZy8nX9MP2CNdomVaposqu3fGayT8T8-jJQwhepo_UtpgBQaDEUom04dZhAN1aJBDlUKJBxE1ceB2Smj0Mln-IBW5AFU2dwUiktt_2Qaq2dBfaKdEup85UV7Yd-dKjlnkabl2Pvr0DTkTreM
2768
+
2769
+ invariant(blocker.state === "unblocked" && newBlocker.state === "blocked" || blocker.state === "blocked" && newBlocker.state === "blocked" || blocker.state === "blocked" && newBlocker.state === "proceeding" || blocker.state === "blocked" && newBlocker.state === "unblocked" || blocker.state === "proceeding" && newBlocker.state === "unblocked", "Invalid blocker state transition: " + blocker.state + " -> " + newBlocker.state);
2770
+ state.blockers.set(key, newBlocker);
2771
+ updateState({
2772
+ blockers: new Map(state.blockers)
2773
+ });
2774
+ }
2775
+
2776
+ function shouldBlockNavigation(_ref10) {
2777
+ let {
2778
+ currentLocation,
2779
+ nextLocation,
2780
+ historyAction
2781
+ } = _ref10;
2782
+
2783
+ if (activeBlocker == null) {
2784
+ return;
2785
+ } // We only allow a single blocker at the moment. This will need to be
2786
+ // updated if we enhance to support multiple blockers in the future
2787
+
2788
+
2789
+ let blockerFunction = blockerFunctions.get(activeBlocker);
2790
+ invariant(blockerFunction, "Could not find a function for the active blocker");
2791
+ let blocker = state.blockers.get(activeBlocker);
2792
+
2793
+ if (blocker && blocker.state === "proceeding") {
2794
+ // If the blocker is currently proceeding, we don't need to re-check
2795
+ // it and can let this navigation continue
2796
+ return;
2797
+ } // At this point, we know we're unblocked/blocked so we need to check the
2798
+ // user-provided blocker function
2799
+
2800
+
2801
+ if (blockerFunction({
2802
+ currentLocation,
2803
+ nextLocation,
2804
+ historyAction
2805
+ })) {
2806
+ return activeBlocker;
2807
+ }
2808
+ }
2809
+
2593
2810
  function cancelActiveDeferreds(predicate) {
2594
2811
  let cancelledRouteIds = [];
2595
2812
  activeDeferreds.forEach((dfd, routeId) => {
@@ -2682,6 +2899,8 @@ function createRouter(init) {
2682
2899
  getFetcher,
2683
2900
  deleteFetcher,
2684
2901
  dispose,
2902
+ getBlocker,
2903
+ deleteBlocker,
2685
2904
  _internalFetchControllers: fetchControllers,
2686
2905
  _internalActiveDeferreds: activeDeferreds
2687
2906
  };
@@ -2817,7 +3036,7 @@ function createStaticHandler(routes, opts) {
2817
3036
  let location = createLocation("", createPath(url), null, "default");
2818
3037
  let matches = matchRoutes(dataRoutes, location, basename); // SSR supports HEAD requests while SPA doesn't
2819
3038
 
2820
- if (!isValidMethod(method) && method !== "head") {
3039
+ if (!isValidMethod(method) && method !== "head" && method !== "options") {
2821
3040
  throw getInternalRouterError(405, {
2822
3041
  method
2823
3042
  });
@@ -3202,8 +3421,8 @@ function getMatchesToLoad(history, state, matches, submission, location, isReval
3202
3421
  cancelledDeferredRoutes.some(id => id === match.route.id) || shouldRevalidateLoader(history, state.location, state.matches[index], submission, location, match, isRevalidationRequired, actionResult))); // Pick fetcher.loads that need to be revalidated
3203
3422
 
3204
3423
  let revalidatingFetchers = [];
3205
- fetchLoadMatches && fetchLoadMatches.forEach((_ref10, key) => {
3206
- let [href, match, fetchMatches] = _ref10;
3424
+ fetchLoadMatches && fetchLoadMatches.forEach((_ref11, key) => {
3425
+ let [href, match, fetchMatches] = _ref11;
3207
3426
 
3208
3427
  // This fetcher was cancelled from a prior action submission - force reload
3209
3428
  if (cancelledFetcherLoads.includes(key)) {
@@ -3333,6 +3552,16 @@ async function callLoaderOrAction(type, request, match, matches, basename, isSta
3333
3552
  }
3334
3553
 
3335
3554
  location = createPath(resolvedLocation);
3555
+ } else if (!isStaticRequest) {
3556
+ // Strip off the protocol+origin for same-origin absolute redirects.
3557
+ // If this is a static reques, we can let it go back to the browser
3558
+ // as-is
3559
+ let currentUrl = new URL(request.url);
3560
+ let url = location.startsWith("//") ? new URL(currentUrl.protocol + location) : new URL(location);
3561
+
3562
+ if (url.origin === currentUrl.origin) {
3563
+ location = url.pathname + url.search + url.hash;
3564
+ }
3336
3565
  } // Don't process redirects in the router during static requests requests.
3337
3566
  // Instead, throw the Response and let the server handle it with an HTTP
3338
3567
  // redirect. We also update the Location header in place in this flow so
@@ -3805,6 +4034,7 @@ function getTargetMatch(matches, location) {
3805
4034
 
3806
4035
  exports.AbortedDeferredError = AbortedDeferredError;
3807
4036
  exports.ErrorResponse = ErrorResponse;
4037
+ exports.IDLE_BLOCKER = IDLE_BLOCKER;
3808
4038
  exports.IDLE_FETCHER = IDLE_FETCHER;
3809
4039
  exports.IDLE_NAVIGATION = IDLE_NAVIGATION;
3810
4040
  exports.UNSAFE_DEFERRED_SYMBOL = UNSAFE_DEFERRED_SYMBOL;