@remix-run/router 1.3.0-pre.0 → 1.3.0-pre.2

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/history.ts CHANGED
@@ -81,6 +81,11 @@ export interface Update {
81
81
  * The new location.
82
82
  */
83
83
  location: Location;
84
+
85
+ /**
86
+ * The delta between this location and the former location in the history stack
87
+ */
88
+ delta: number;
84
89
  }
85
90
 
86
91
  /**
@@ -181,6 +186,7 @@ export interface History {
181
186
  type HistoryState = {
182
187
  usr: any;
183
188
  key?: string;
189
+ idx: number;
184
190
  };
185
191
 
186
192
  const PopStateEventType = "popstate";
@@ -294,7 +300,7 @@ export function createMemoryHistory(
294
300
  index += 1;
295
301
  entries.splice(index, entries.length, nextLocation);
296
302
  if (v5Compat && listener) {
297
- listener({ action, location: nextLocation });
303
+ listener({ action, location: nextLocation, delta: 1 });
298
304
  }
299
305
  },
300
306
  replace(to, state) {
@@ -302,14 +308,16 @@ export function createMemoryHistory(
302
308
  let nextLocation = createMemoryLocation(to, state);
303
309
  entries[index] = nextLocation;
304
310
  if (v5Compat && listener) {
305
- listener({ action, location: nextLocation });
311
+ listener({ action, location: nextLocation, delta: 0 });
306
312
  }
307
313
  },
308
314
  go(delta) {
309
315
  action = Action.Pop;
310
- index = clampIndex(index + delta);
316
+ let nextIndex = clampIndex(index + delta);
317
+ let nextLocation = entries[nextIndex];
318
+ index = nextIndex;
311
319
  if (listener) {
312
- listener({ action, location: getCurrentLocation() });
320
+ listener({ action, location: nextLocation, delta });
313
321
  }
314
322
  },
315
323
  listen(fn: Listener) {
@@ -497,10 +505,11 @@ function createKey() {
497
505
  /**
498
506
  * For browser-based histories, we combine the state and key into an object
499
507
  */
500
- function getHistoryState(location: Location): HistoryState {
508
+ function getHistoryState(location: Location, index: number): HistoryState {
501
509
  return {
502
510
  usr: location.state,
503
511
  key: location.key,
512
+ idx: index,
504
513
  };
505
514
  }
506
515
 
@@ -588,10 +597,43 @@ function getUrlBasedHistory(
588
597
  let action = Action.Pop;
589
598
  let listener: Listener | null = null;
590
599
 
600
+ let index = getIndex()!;
601
+ // Index should only be null when we initialize. If not, it's because the
602
+ // user called history.pushState or history.replaceState directly, in which
603
+ // case we should log a warning as it will result in bugs.
604
+ if (index == null) {
605
+ index = 0;
606
+ globalHistory.replaceState({ ...globalHistory.state, idx: index }, "");
607
+ }
608
+
609
+ function getIndex(): number {
610
+ let state = globalHistory.state || { idx: null };
611
+ return state.idx;
612
+ }
613
+
591
614
  function handlePop() {
592
- action = Action.Pop;
593
- if (listener) {
594
- listener({ action, location: history.location });
615
+ let nextAction = Action.Pop;
616
+ let nextIndex = getIndex();
617
+
618
+ if (nextIndex != null) {
619
+ let delta = nextIndex - index;
620
+ action = nextAction;
621
+ index = nextIndex;
622
+ if (listener) {
623
+ listener({ action, location: history.location, delta });
624
+ }
625
+ } else {
626
+ warning(
627
+ false,
628
+ // TODO: Write up a doc that explains our blocking strategy in detail
629
+ // and link to it here so people can understand better what is going on
630
+ // and how to avoid it.
631
+ `You are trying to block a POP navigation to a location that was not ` +
632
+ `created by @remix-run/router. The block will fail silently in ` +
633
+ `production, but in general you should do all navigation with the ` +
634
+ `router (instead of using window.history.pushState directly) ` +
635
+ `to avoid this situation.`
636
+ );
595
637
  }
596
638
  }
597
639
 
@@ -600,7 +642,8 @@ function getUrlBasedHistory(
600
642
  let location = createLocation(history.location, to, state);
601
643
  if (validateLocation) validateLocation(location, to);
602
644
 
603
- let historyState = getHistoryState(location);
645
+ index = getIndex() + 1;
646
+ let historyState = getHistoryState(location, index);
604
647
  let url = history.createHref(location);
605
648
 
606
649
  // try...catch because iOS limits us to 100 pushState calls :/
@@ -613,7 +656,7 @@ function getUrlBasedHistory(
613
656
  }
614
657
 
615
658
  if (v5Compat && listener) {
616
- listener({ action, location: history.location });
659
+ listener({ action, location: history.location, delta: 1 });
617
660
  }
618
661
  }
619
662
 
@@ -622,12 +665,13 @@ function getUrlBasedHistory(
622
665
  let location = createLocation(history.location, to, state);
623
666
  if (validateLocation) validateLocation(location, to);
624
667
 
625
- let historyState = getHistoryState(location);
668
+ index = getIndex();
669
+ let historyState = getHistoryState(location, index);
626
670
  let url = history.createHref(location);
627
671
  globalHistory.replaceState(historyState, "", url);
628
672
 
629
673
  if (v5Compat && listener) {
630
- listener({ action, location: history.location });
674
+ listener({ action, location: history.location, delta: 0 });
631
675
  }
632
676
  }
633
677
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remix-run/router",
3
- "version": "1.3.0-pre.0",
3
+ "version": "1.3.0-pre.2",
4
4
  "description": "Nested/Data-driven/Framework-agnostic Routing",
5
5
  "keywords": [
6
6
  "remix",
package/router.ts CHANGED
@@ -32,6 +32,7 @@ import {
32
32
  joinPaths,
33
33
  matchRoutes,
34
34
  resolveTo,
35
+ warning,
35
36
  } from "./utils";
36
37
 
37
38
  ////////////////////////////////////////////////////////////////////////////////
@@ -110,14 +111,14 @@ export interface Router {
110
111
  * Navigate forward/backward in the history stack
111
112
  * @param to Delta to move in the history stack
112
113
  */
113
- navigate(to: number): void;
114
+ navigate(to: number): Promise<void>;
114
115
 
115
116
  /**
116
117
  * Navigate to the given path
117
118
  * @param to Path to navigate to
118
119
  * @param opts Navigation options (method, submission, etc.)
119
120
  */
120
- navigate(to: To, opts?: RouterNavigateOptions): void;
121
+ navigate(to: To, opts?: RouterNavigateOptions): Promise<void>;
121
122
 
122
123
  /**
123
124
  * @internal
@@ -190,6 +191,25 @@ export interface Router {
190
191
  */
191
192
  dispose(): void;
192
193
 
194
+ /**
195
+ * @internal
196
+ * PRIVATE - DO NOT USE
197
+ *
198
+ * Get a navigation blocker
199
+ * @param key The identifier for the blocker
200
+ * @param fn The blocker function implementation
201
+ */
202
+ getBlocker(key: string, fn: BlockerFunction): Blocker;
203
+
204
+ /**
205
+ * @internal
206
+ * PRIVATE - DO NOT USE
207
+ *
208
+ * Delete a navigation blocker
209
+ * @param key The identifier for the blocker
210
+ */
211
+ deleteBlocker(key: string): void;
212
+
193
213
  /**
194
214
  * @internal
195
215
  * PRIVATE - DO NOT USE
@@ -275,6 +295,11 @@ export interface RouterState {
275
295
  * Map of current fetchers
276
296
  */
277
297
  fetchers: Map<string, Fetcher>;
298
+
299
+ /**
300
+ * Map of current blockers
301
+ */
302
+ blockers: Map<string, Blocker>;
278
303
  }
279
304
 
280
305
  /**
@@ -371,6 +396,7 @@ type LinkNavigateOptions = {
371
396
  type SubmissionNavigateOptions = {
372
397
  replace?: boolean;
373
398
  state?: any;
399
+ preventScrollReset?: boolean;
374
400
  formMethod?: FormMethod;
375
401
  formEncType?: FormEncType;
376
402
  formData: FormData;
@@ -460,6 +486,35 @@ type FetcherStates<TData = any> = {
460
486
  export type Fetcher<TData = any> =
461
487
  FetcherStates<TData>[keyof FetcherStates<TData>];
462
488
 
489
+ interface BlockerBlocked {
490
+ state: "blocked";
491
+ reset(): void;
492
+ proceed(): void;
493
+ location: Location;
494
+ }
495
+
496
+ interface BlockerUnblocked {
497
+ state: "unblocked";
498
+ reset: undefined;
499
+ proceed: undefined;
500
+ location: undefined;
501
+ }
502
+
503
+ interface BlockerProceeding {
504
+ state: "proceeding";
505
+ reset: undefined;
506
+ proceed: undefined;
507
+ location: Location;
508
+ }
509
+
510
+ export type Blocker = BlockerUnblocked | BlockerBlocked | BlockerProceeding;
511
+
512
+ export type BlockerFunction = (args: {
513
+ currentLocation: Location;
514
+ nextLocation: Location;
515
+ historyAction: HistoryAction;
516
+ }) => boolean;
517
+
463
518
  interface ShortCircuitable {
464
519
  /**
465
520
  * startNavigation does not need to complete the navigation because we
@@ -561,6 +616,13 @@ export const IDLE_FETCHER: FetcherStates["Idle"] = {
561
616
  formData: undefined,
562
617
  };
563
618
 
619
+ export const IDLE_BLOCKER: BlockerUnblocked = {
620
+ state: "unblocked",
621
+ proceed: undefined,
622
+ reset: undefined,
623
+ location: undefined,
624
+ };
625
+
564
626
  const isBrowser =
565
627
  typeof window !== "undefined" &&
566
628
  typeof window.document !== "undefined" &&
@@ -636,50 +698,76 @@ export function createRouter(init: RouterInit): Router {
636
698
  actionData: (init.hydrationData && init.hydrationData.actionData) || null,
637
699
  errors: (init.hydrationData && init.hydrationData.errors) || initialErrors,
638
700
  fetchers: new Map(),
701
+ blockers: new Map(),
639
702
  };
640
703
 
641
704
  // -- Stateful internal variables to manage navigations --
642
705
  // Current navigation in progress (to be committed in completeNavigation)
643
706
  let pendingAction: HistoryAction = HistoryAction.Pop;
707
+
644
708
  // Should the current navigation prevent the scroll reset if scroll cannot
645
709
  // be restored?
646
710
  let pendingPreventScrollReset = false;
711
+
647
712
  // AbortController for the active navigation
648
713
  let pendingNavigationController: AbortController | null;
714
+
649
715
  // We use this to avoid touching history in completeNavigation if a
650
716
  // revalidation is entirely uninterrupted
651
717
  let isUninterruptedRevalidation = false;
718
+
652
719
  // Use this internal flag to force revalidation of all loaders:
653
720
  // - submissions (completed or interrupted)
654
721
  // - useRevalidate()
655
722
  // - X-Remix-Revalidate (from redirect)
656
723
  let isRevalidationRequired = false;
724
+
657
725
  // Use this internal array to capture routes that require revalidation due
658
726
  // to a cancelled deferred on action submission
659
727
  let cancelledDeferredRoutes: string[] = [];
728
+
660
729
  // Use this internal array to capture fetcher loads that were cancelled by an
661
730
  // action navigation and require revalidation
662
731
  let cancelledFetcherLoads: string[] = [];
732
+
663
733
  // AbortControllers for any in-flight fetchers
664
734
  let fetchControllers = new Map<string, AbortController>();
735
+
665
736
  // Track loads based on the order in which they started
666
737
  let incrementingLoadId = 0;
738
+
667
739
  // Track the outstanding pending navigation data load to be compared against
668
740
  // the globally incrementing load when a fetcher load lands after a completed
669
741
  // navigation
670
742
  let pendingNavigationLoadId = -1;
743
+
671
744
  // Fetchers that triggered data reloads as a result of their actions
672
745
  let fetchReloadIds = new Map<string, number>();
746
+
673
747
  // Fetchers that triggered redirect navigations from their actions
674
748
  let fetchRedirectIds = new Set<string>();
749
+
675
750
  // Most recent href/match for fetcher.load calls for fetchers
676
751
  let fetchLoadMatches = new Map<string, FetchLoadMatch>();
752
+
677
753
  // Store DeferredData instances for active route matches. When a
678
754
  // route loader returns defer() we stick one in here. Then, when a nested
679
755
  // promise resolves we update loaderData. If a new navigation starts we
680
756
  // cancel active deferreds for eliminated routes.
681
757
  let activeDeferreds = new Map<string, DeferredData>();
682
758
 
759
+ // We ony support a single active blocker at the moment since we don't have
760
+ // any compelling use cases for multi-blocker yet
761
+ let activeBlocker: string | null = null;
762
+
763
+ // Store blocker functions in a separate Map outside of router state since
764
+ // we don't need to update UI state if they change
765
+ let blockerFunctions = new Map<string, BlockerFunction>();
766
+
767
+ // Flag to ignore the next history update, so we can revert the URL change on
768
+ // a POP navigation that was blocked by the user without touching router state
769
+ let ignoreNextHistoryUpdate = false;
770
+
683
771
  // Initialize the router, all side effects should be kicked off from here.
684
772
  // Implemented as a Fluent API for ease of:
685
773
  // let router = createRouter(init).initialize();
@@ -687,8 +775,48 @@ export function createRouter(init: RouterInit): Router {
687
775
  // If history informs us of a POP navigation, start the navigation but do not update
688
776
  // state. We'll update our own state once the navigation completes
689
777
  unlistenHistory = init.history.listen(
690
- ({ action: historyAction, location }) =>
691
- startNavigation(historyAction, location)
778
+ ({ action: historyAction, location, delta }) => {
779
+ // Ignore this event if it was just us resetting the URL from a
780
+ // blocked POP navigation
781
+ if (ignoreNextHistoryUpdate) {
782
+ ignoreNextHistoryUpdate = false;
783
+ return;
784
+ }
785
+
786
+ let blockerKey = shouldBlockNavigation({
787
+ currentLocation: state.location,
788
+ nextLocation: location,
789
+ historyAction,
790
+ });
791
+ if (blockerKey) {
792
+ // Restore the URL to match the current UI, but don't update router state
793
+ ignoreNextHistoryUpdate = true;
794
+ init.history.go(delta * -1);
795
+
796
+ // Put the blocker into a blocked state
797
+ updateBlocker(blockerKey, {
798
+ state: "blocked",
799
+ location,
800
+ proceed() {
801
+ updateBlocker(blockerKey!, {
802
+ state: "proceeding",
803
+ proceed: undefined,
804
+ reset: undefined,
805
+ location,
806
+ });
807
+ // Re-do the same POP navigation we just blocked
808
+ init.history.go(delta);
809
+ },
810
+ reset() {
811
+ deleteBlocker(blockerKey!);
812
+ updateState({ blockers: new Map(router.state.blockers) });
813
+ },
814
+ });
815
+ return;
816
+ }
817
+
818
+ return startNavigation(historyAction, location);
819
+ }
692
820
  );
693
821
 
694
822
  // Kick off initial data load if needed. Use Pop to avoid modifying history
@@ -707,6 +835,7 @@ export function createRouter(init: RouterInit): Router {
707
835
  subscribers.clear();
708
836
  pendingNavigationController && pendingNavigationController.abort();
709
837
  state.fetchers.forEach((_, key) => deleteFetcher(key));
838
+ state.blockers.forEach((_, key) => deleteBlocker(key));
710
839
  }
711
840
 
712
841
  // Subscribe to state updates for the router
@@ -771,6 +900,20 @@ export function createRouter(init: RouterInit): Router {
771
900
  )
772
901
  : state.loaderData;
773
902
 
903
+ // On a successful navigation we can assume we got through all blockers
904
+ // so we can start fresh
905
+ for (let [key] of blockerFunctions) {
906
+ deleteBlocker(key);
907
+ }
908
+
909
+ // Always respect the user flag. Otherwise don't reset on mutation
910
+ // submission navigations unless they redirect
911
+ let preventScrollReset =
912
+ pendingPreventScrollReset === true ||
913
+ (state.navigation.formMethod != null &&
914
+ isMutationMethod(state.navigation.formMethod) &&
915
+ location.state?._isRedirect !== true);
916
+
774
917
  updateState({
775
918
  ...newState, // matches, errors, fetchers go through as-is
776
919
  actionData,
@@ -780,11 +923,12 @@ export function createRouter(init: RouterInit): Router {
780
923
  initialized: true,
781
924
  navigation: IDLE_NAVIGATION,
782
925
  revalidation: "idle",
783
- // Don't restore on submission navigations
784
- restoreScrollPosition: state.navigation.formData
785
- ? false
786
- : getSavedScrollPosition(location, newState.matches || state.matches),
787
- preventScrollReset: pendingPreventScrollReset,
926
+ restoreScrollPosition: getSavedScrollPosition(
927
+ location,
928
+ newState.matches || state.matches
929
+ ),
930
+ preventScrollReset,
931
+ blockers: new Map(state.blockers),
788
932
  });
789
933
 
790
934
  if (isUninterruptedRevalidation) {
@@ -819,16 +963,17 @@ export function createRouter(init: RouterInit): Router {
819
963
 
820
964
  let { path, submission, error } = normalizeNavigateOptions(to, opts);
821
965
 
822
- let location = createLocation(state.location, path, opts && opts.state);
966
+ let currentLocation = state.location;
967
+ let nextLocation = createLocation(state.location, path, opts && opts.state);
823
968
 
824
969
  // When using navigate as a PUSH/REPLACE we aren't reading an already-encoded
825
970
  // URL from window.location, so we need to encode it here so the behavior
826
971
  // remains the same as POP and non-data-router usages. new URL() does all
827
972
  // the same encoding we'd get from a history.pushState/window.location read
828
973
  // without having to touch history
829
- location = {
830
- ...location,
831
- ...init.history.encodeLocation(location),
974
+ nextLocation = {
975
+ ...nextLocation,
976
+ ...init.history.encodeLocation(nextLocation),
832
977
  };
833
978
 
834
979
  let userReplace = opts && opts.replace != null ? opts.replace : undefined;
@@ -856,7 +1001,35 @@ export function createRouter(init: RouterInit): Router {
856
1001
  ? opts.preventScrollReset === true
857
1002
  : undefined;
858
1003
 
859
- return await startNavigation(historyAction, location, {
1004
+ let blockerKey = shouldBlockNavigation({
1005
+ currentLocation,
1006
+ nextLocation,
1007
+ historyAction,
1008
+ });
1009
+ if (blockerKey) {
1010
+ // Put the blocker into a blocked state
1011
+ updateBlocker(blockerKey, {
1012
+ state: "blocked",
1013
+ location: nextLocation,
1014
+ proceed() {
1015
+ updateBlocker(blockerKey!, {
1016
+ state: "proceeding",
1017
+ proceed: undefined,
1018
+ reset: undefined,
1019
+ location: nextLocation,
1020
+ });
1021
+ // Send the same navigation through
1022
+ navigate(to, opts);
1023
+ },
1024
+ reset() {
1025
+ deleteBlocker(blockerKey!);
1026
+ updateState({ blockers: new Map(state.blockers) });
1027
+ },
1028
+ });
1029
+ return;
1030
+ }
1031
+
1032
+ return await startNavigation(historyAction, nextLocation, {
860
1033
  submission,
861
1034
  // Send through the formData serialization error if we have one so we can
862
1035
  // render at the right error boundary after we match routes
@@ -1772,6 +1945,8 @@ export function createRouter(init: RouterInit): Router {
1772
1945
  ...submission,
1773
1946
  formAction: redirect.location,
1774
1947
  },
1948
+ // Preserve this flag across redirects
1949
+ preventScrollReset: pendingPreventScrollReset,
1775
1950
  });
1776
1951
  } else {
1777
1952
  // Otherwise, we kick off a new loading navigation, preserving the
@@ -1785,6 +1960,8 @@ export function createRouter(init: RouterInit): Router {
1785
1960
  formEncType: submission ? submission.formEncType : undefined,
1786
1961
  formData: submission ? submission.formData : undefined,
1787
1962
  },
1963
+ // Preserve this flag across redirects
1964
+ preventScrollReset: pendingPreventScrollReset,
1788
1965
  });
1789
1966
  }
1790
1967
  }
@@ -1926,6 +2103,84 @@ export function createRouter(init: RouterInit): Router {
1926
2103
  return yeetedKeys.length > 0;
1927
2104
  }
1928
2105
 
2106
+ function getBlocker(key: string, fn: BlockerFunction) {
2107
+ let blocker: Blocker = state.blockers.get(key) || IDLE_BLOCKER;
2108
+
2109
+ if (blockerFunctions.get(key) !== fn) {
2110
+ blockerFunctions.set(key, fn);
2111
+ if (activeBlocker == null) {
2112
+ // This is now the active blocker
2113
+ activeBlocker = key;
2114
+ } else if (key !== activeBlocker) {
2115
+ warning(false, "A router only supports one blocker at a time");
2116
+ }
2117
+ }
2118
+
2119
+ return blocker;
2120
+ }
2121
+
2122
+ function deleteBlocker(key: string) {
2123
+ state.blockers.delete(key);
2124
+ blockerFunctions.delete(key);
2125
+ if (activeBlocker === key) {
2126
+ activeBlocker = null;
2127
+ }
2128
+ }
2129
+
2130
+ // Utility function to update blockers, ensuring valid state transitions
2131
+ function updateBlocker(key: string, newBlocker: Blocker) {
2132
+ let blocker = state.blockers.get(key) || IDLE_BLOCKER;
2133
+
2134
+ // Poor mans state machine :)
2135
+ // https://mermaid.live/edit#pako:eNqVkc9OwzAMxl8l8nnjAYrEtDIOHEBIgwvKJTReGy3_lDpIqO27k6awMG0XcrLlnz87nwdonESogKXXBuE79rq75XZO3-yHds0RJVuv70YrPlUrCEe2HfrORS3rubqZfuhtpg5C9wk5tZ4VKcRUq88q9Z8RS0-48cE1iHJkL0ugbHuFLus9L6spZy8nX9MP2CNdomVaposqu3fGayT8T8-jJQwhepo_UtpgBQaDEUom04dZhAN1aJBDlUKJBxE1ceB2Smj0Mln-IBW5AFU2dwUiktt_2Qaq2dBfaKdEup85UV7Yd-dKjlnkabl2Pvr0DTkTreM
2136
+ invariant(
2137
+ (blocker.state === "unblocked" && newBlocker.state === "blocked") ||
2138
+ (blocker.state === "blocked" && newBlocker.state === "blocked") ||
2139
+ (blocker.state === "blocked" && newBlocker.state === "proceeding") ||
2140
+ (blocker.state === "blocked" && newBlocker.state === "unblocked") ||
2141
+ (blocker.state === "proceeding" && newBlocker.state === "unblocked"),
2142
+ `Invalid blocker state transition: ${blocker.state} -> ${newBlocker.state}`
2143
+ );
2144
+
2145
+ state.blockers.set(key, newBlocker);
2146
+ updateState({ blockers: new Map(state.blockers) });
2147
+ }
2148
+
2149
+ function shouldBlockNavigation({
2150
+ currentLocation,
2151
+ nextLocation,
2152
+ historyAction,
2153
+ }: {
2154
+ currentLocation: Location;
2155
+ nextLocation: Location;
2156
+ historyAction: HistoryAction;
2157
+ }): string | undefined {
2158
+ if (activeBlocker == null) {
2159
+ return;
2160
+ }
2161
+
2162
+ // We only allow a single blocker at the moment. This will need to be
2163
+ // updated if we enhance to support multiple blockers in the future
2164
+ let blockerFunction = blockerFunctions.get(activeBlocker);
2165
+ invariant(
2166
+ blockerFunction,
2167
+ "Could not find a function for the active blocker"
2168
+ );
2169
+ let blocker = state.blockers.get(activeBlocker);
2170
+
2171
+ if (blocker && blocker.state === "proceeding") {
2172
+ // If the blocker is currently proceeding, we don't need to re-check
2173
+ // it and can let this navigation continue
2174
+ return;
2175
+ }
2176
+
2177
+ // At this point, we know we're unblocked/blocked so we need to check the
2178
+ // user-provided blocker function
2179
+ if (blockerFunction({ currentLocation, nextLocation, historyAction })) {
2180
+ return activeBlocker;
2181
+ }
2182
+ }
2183
+
1929
2184
  function cancelActiveDeferreds(
1930
2185
  predicate?: (routeId: string) => boolean
1931
2186
  ): string[] {
@@ -2025,6 +2280,8 @@ export function createRouter(init: RouterInit): Router {
2025
2280
  getFetcher,
2026
2281
  deleteFetcher,
2027
2282
  dispose,
2283
+ getBlocker,
2284
+ deleteBlocker,
2028
2285
  _internalFetchControllers: fetchControllers,
2029
2286
  _internalActiveDeferreds: activeDeferreds,
2030
2287
  };
package/utils.ts CHANGED
@@ -900,7 +900,7 @@ export function warning(cond: any, message: string): void {
900
900
  if (typeof console !== "undefined") console.warn(message);
901
901
 
902
902
  try {
903
- // Welcome to debugging React Router!
903
+ // Welcome to debugging @remix-run/router!
904
904
  //
905
905
  // This error is thrown as a convenience so you can more easily
906
906
  // find the source for a warning that appears in the console by