source: imaps-frontend/node_modules/@remix-run/router/router.ts

main
Last change on this file was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

  • Property mode set to 100644
File size: 176.4 KB
RevLine 
[d565449]1import type { History, Location, Path, To } from "./history";
2import {
3 Action as HistoryAction,
4 createLocation,
5 createPath,
6 invariant,
7 parsePath,
8 warning,
9} from "./history";
10import type {
11 AgnosticDataRouteMatch,
12 AgnosticDataRouteObject,
13 DataStrategyMatch,
14 AgnosticRouteObject,
15 DataResult,
16 DataStrategyFunction,
17 DataStrategyFunctionArgs,
18 DeferredData,
19 DeferredResult,
20 DetectErrorBoundaryFunction,
21 ErrorResult,
22 FormEncType,
23 FormMethod,
24 HTMLFormMethod,
25 HandlerResult,
26 ImmutableRouteKey,
27 MapRoutePropertiesFunction,
28 MutationFormMethod,
29 RedirectResult,
30 RouteData,
31 RouteManifest,
32 ShouldRevalidateFunctionArgs,
33 Submission,
34 SuccessResult,
35 UIMatch,
36 V7_FormMethod,
37 V7_MutationFormMethod,
38 AgnosticPatchRoutesOnMissFunction,
39 DataWithResponseInit,
40} from "./utils";
41import {
42 ErrorResponseImpl,
43 ResultType,
44 convertRouteMatchToUiMatch,
45 convertRoutesToDataRoutes,
46 getPathContributingMatches,
47 getResolveToMatches,
48 immutableRouteKeys,
49 isRouteErrorResponse,
50 joinPaths,
51 matchRoutes,
52 matchRoutesImpl,
53 resolveTo,
54 stripBasename,
55} from "./utils";
56
57////////////////////////////////////////////////////////////////////////////////
58//#region Types and Constants
59////////////////////////////////////////////////////////////////////////////////
60
61/**
62 * A Router instance manages all navigation and data loading/mutations
63 */
64export interface Router {
65 /**
66 * @internal
67 * PRIVATE - DO NOT USE
68 *
69 * Return the basename for the router
70 */
71 get basename(): RouterInit["basename"];
72
73 /**
74 * @internal
75 * PRIVATE - DO NOT USE
76 *
77 * Return the future config for the router
78 */
79 get future(): FutureConfig;
80
81 /**
82 * @internal
83 * PRIVATE - DO NOT USE
84 *
85 * Return the current state of the router
86 */
87 get state(): RouterState;
88
89 /**
90 * @internal
91 * PRIVATE - DO NOT USE
92 *
93 * Return the routes for this router instance
94 */
95 get routes(): AgnosticDataRouteObject[];
96
97 /**
98 * @internal
99 * PRIVATE - DO NOT USE
100 *
101 * Return the window associated with the router
102 */
103 get window(): RouterInit["window"];
104
105 /**
106 * @internal
107 * PRIVATE - DO NOT USE
108 *
109 * Initialize the router, including adding history listeners and kicking off
110 * initial data fetches. Returns a function to cleanup listeners and abort
111 * any in-progress loads
112 */
113 initialize(): Router;
114
115 /**
116 * @internal
117 * PRIVATE - DO NOT USE
118 *
119 * Subscribe to router.state updates
120 *
121 * @param fn function to call with the new state
122 */
123 subscribe(fn: RouterSubscriber): () => void;
124
125 /**
126 * @internal
127 * PRIVATE - DO NOT USE
128 *
129 * Enable scroll restoration behavior in the router
130 *
131 * @param savedScrollPositions Object that will manage positions, in case
132 * it's being restored from sessionStorage
133 * @param getScrollPosition Function to get the active Y scroll position
134 * @param getKey Function to get the key to use for restoration
135 */
136 enableScrollRestoration(
137 savedScrollPositions: Record<string, number>,
138 getScrollPosition: GetScrollPositionFunction,
139 getKey?: GetScrollRestorationKeyFunction
140 ): () => void;
141
142 /**
143 * @internal
144 * PRIVATE - DO NOT USE
145 *
146 * Navigate forward/backward in the history stack
147 * @param to Delta to move in the history stack
148 */
149 navigate(to: number): Promise<void>;
150
151 /**
152 * Navigate to the given path
153 * @param to Path to navigate to
154 * @param opts Navigation options (method, submission, etc.)
155 */
156 navigate(to: To | null, opts?: RouterNavigateOptions): Promise<void>;
157
158 /**
159 * @internal
160 * PRIVATE - DO NOT USE
161 *
162 * Trigger a fetcher load/submission
163 *
164 * @param key Fetcher key
165 * @param routeId Route that owns the fetcher
166 * @param href href to fetch
167 * @param opts Fetcher options, (method, submission, etc.)
168 */
169 fetch(
170 key: string,
171 routeId: string,
172 href: string | null,
173 opts?: RouterFetchOptions
174 ): void;
175
176 /**
177 * @internal
178 * PRIVATE - DO NOT USE
179 *
180 * Trigger a revalidation of all current route loaders and fetcher loads
181 */
182 revalidate(): void;
183
184 /**
185 * @internal
186 * PRIVATE - DO NOT USE
187 *
188 * Utility function to create an href for the given location
189 * @param location
190 */
191 createHref(location: Location | URL): string;
192
193 /**
194 * @internal
195 * PRIVATE - DO NOT USE
196 *
197 * Utility function to URL encode a destination path according to the internal
198 * history implementation
199 * @param to
200 */
201 encodeLocation(to: To): Path;
202
203 /**
204 * @internal
205 * PRIVATE - DO NOT USE
206 *
207 * Get/create a fetcher for the given key
208 * @param key
209 */
210 getFetcher<TData = any>(key: string): Fetcher<TData>;
211
212 /**
213 * @internal
214 * PRIVATE - DO NOT USE
215 *
216 * Delete the fetcher for a given key
217 * @param key
218 */
219 deleteFetcher(key: string): void;
220
221 /**
222 * @internal
223 * PRIVATE - DO NOT USE
224 *
225 * Cleanup listeners and abort any in-progress loads
226 */
227 dispose(): void;
228
229 /**
230 * @internal
231 * PRIVATE - DO NOT USE
232 *
233 * Get a navigation blocker
234 * @param key The identifier for the blocker
235 * @param fn The blocker function implementation
236 */
237 getBlocker(key: string, fn: BlockerFunction): Blocker;
238
239 /**
240 * @internal
241 * PRIVATE - DO NOT USE
242 *
243 * Delete a navigation blocker
244 * @param key The identifier for the blocker
245 */
246 deleteBlocker(key: string): void;
247
248 /**
249 * @internal
250 * PRIVATE DO NOT USE
251 *
252 * Patch additional children routes into an existing parent route
253 * @param routeId The parent route id or a callback function accepting `patch`
254 * to perform batch patching
255 * @param children The additional children routes
256 */
257 patchRoutes(routeId: string | null, children: AgnosticRouteObject[]): void;
258
259 /**
260 * @internal
261 * PRIVATE - DO NOT USE
262 *
263 * HMR needs to pass in-flight route updates to React Router
264 * TODO: Replace this with granular route update APIs (addRoute, updateRoute, deleteRoute)
265 */
266 _internalSetRoutes(routes: AgnosticRouteObject[]): void;
267
268 /**
269 * @internal
270 * PRIVATE - DO NOT USE
271 *
272 * Internal fetch AbortControllers accessed by unit tests
273 */
274 _internalFetchControllers: Map<string, AbortController>;
275
276 /**
277 * @internal
278 * PRIVATE - DO NOT USE
279 *
280 * Internal pending DeferredData instances accessed by unit tests
281 */
282 _internalActiveDeferreds: Map<string, DeferredData>;
283}
284
285/**
286 * State maintained internally by the router. During a navigation, all states
287 * reflect the the "old" location unless otherwise noted.
288 */
289export interface RouterState {
290 /**
291 * The action of the most recent navigation
292 */
293 historyAction: HistoryAction;
294
295 /**
296 * The current location reflected by the router
297 */
298 location: Location;
299
300 /**
301 * The current set of route matches
302 */
303 matches: AgnosticDataRouteMatch[];
304
305 /**
306 * Tracks whether we've completed our initial data load
307 */
308 initialized: boolean;
309
310 /**
311 * Current scroll position we should start at for a new view
312 * - number -> scroll position to restore to
313 * - false -> do not restore scroll at all (used during submissions)
314 * - null -> don't have a saved position, scroll to hash or top of page
315 */
316 restoreScrollPosition: number | false | null;
317
318 /**
319 * Indicate whether this navigation should skip resetting the scroll position
320 * if we are unable to restore the scroll position
321 */
322 preventScrollReset: boolean;
323
324 /**
325 * Tracks the state of the current navigation
326 */
327 navigation: Navigation;
328
329 /**
330 * Tracks any in-progress revalidations
331 */
332 revalidation: RevalidationState;
333
334 /**
335 * Data from the loaders for the current matches
336 */
337 loaderData: RouteData;
338
339 /**
340 * Data from the action for the current matches
341 */
342 actionData: RouteData | null;
343
344 /**
345 * Errors caught from loaders for the current matches
346 */
347 errors: RouteData | null;
348
349 /**
350 * Map of current fetchers
351 */
352 fetchers: Map<string, Fetcher>;
353
354 /**
355 * Map of current blockers
356 */
357 blockers: Map<string, Blocker>;
358}
359
360/**
361 * Data that can be passed into hydrate a Router from SSR
362 */
363export type HydrationState = Partial<
364 Pick<RouterState, "loaderData" | "actionData" | "errors">
365>;
366
367/**
368 * Future flags to toggle new feature behavior
369 */
370export interface FutureConfig {
371 v7_fetcherPersist: boolean;
372 v7_normalizeFormMethod: boolean;
373 v7_partialHydration: boolean;
374 v7_prependBasename: boolean;
375 v7_relativeSplatPath: boolean;
376 v7_skipActionErrorRevalidation: boolean;
377}
378
379/**
380 * Initialization options for createRouter
381 */
382export interface RouterInit {
383 routes: AgnosticRouteObject[];
384 history: History;
385 basename?: string;
386 /**
387 * @deprecated Use `mapRouteProperties` instead
388 */
389 detectErrorBoundary?: DetectErrorBoundaryFunction;
390 mapRouteProperties?: MapRoutePropertiesFunction;
391 future?: Partial<FutureConfig>;
392 hydrationData?: HydrationState;
393 window?: Window;
394 unstable_patchRoutesOnMiss?: AgnosticPatchRoutesOnMissFunction;
395 unstable_dataStrategy?: DataStrategyFunction;
396}
397
398/**
399 * State returned from a server-side query() call
400 */
401export interface StaticHandlerContext {
402 basename: Router["basename"];
403 location: RouterState["location"];
404 matches: RouterState["matches"];
405 loaderData: RouterState["loaderData"];
406 actionData: RouterState["actionData"];
407 errors: RouterState["errors"];
408 statusCode: number;
409 loaderHeaders: Record<string, Headers>;
410 actionHeaders: Record<string, Headers>;
411 activeDeferreds: Record<string, DeferredData> | null;
412 _deepestRenderedBoundaryId?: string | null;
413}
414
415/**
416 * A StaticHandler instance manages a singular SSR navigation/fetch event
417 */
418export interface StaticHandler {
419 dataRoutes: AgnosticDataRouteObject[];
420 query(
421 request: Request,
422 opts?: {
423 requestContext?: unknown;
424 skipLoaderErrorBubbling?: boolean;
425 unstable_dataStrategy?: DataStrategyFunction;
426 }
427 ): Promise<StaticHandlerContext | Response>;
428 queryRoute(
429 request: Request,
430 opts?: {
431 routeId?: string;
432 requestContext?: unknown;
433 unstable_dataStrategy?: DataStrategyFunction;
434 }
435 ): Promise<any>;
436}
437
438type ViewTransitionOpts = {
439 currentLocation: Location;
440 nextLocation: Location;
441};
442
443/**
444 * Subscriber function signature for changes to router state
445 */
446export interface RouterSubscriber {
447 (
448 state: RouterState,
449 opts: {
450 deletedFetchers: string[];
451 unstable_viewTransitionOpts?: ViewTransitionOpts;
452 unstable_flushSync: boolean;
453 }
454 ): void;
455}
456
457/**
458 * Function signature for determining the key to be used in scroll restoration
459 * for a given location
460 */
461export interface GetScrollRestorationKeyFunction {
462 (location: Location, matches: UIMatch[]): string | null;
463}
464
465/**
466 * Function signature for determining the current scroll position
467 */
468export interface GetScrollPositionFunction {
469 (): number;
470}
471
472export type RelativeRoutingType = "route" | "path";
473
474// Allowed for any navigation or fetch
475type BaseNavigateOrFetchOptions = {
476 preventScrollReset?: boolean;
477 relative?: RelativeRoutingType;
478 unstable_flushSync?: boolean;
479};
480
481// Only allowed for navigations
482type BaseNavigateOptions = BaseNavigateOrFetchOptions & {
483 replace?: boolean;
484 state?: any;
485 fromRouteId?: string;
486 unstable_viewTransition?: boolean;
487};
488
489// Only allowed for submission navigations
490type BaseSubmissionOptions = {
491 formMethod?: HTMLFormMethod;
492 formEncType?: FormEncType;
493} & (
494 | { formData: FormData; body?: undefined }
495 | { formData?: undefined; body: any }
496);
497
498/**
499 * Options for a navigate() call for a normal (non-submission) navigation
500 */
501type LinkNavigateOptions = BaseNavigateOptions;
502
503/**
504 * Options for a navigate() call for a submission navigation
505 */
506type SubmissionNavigateOptions = BaseNavigateOptions & BaseSubmissionOptions;
507
508/**
509 * Options to pass to navigate() for a navigation
510 */
511export type RouterNavigateOptions =
512 | LinkNavigateOptions
513 | SubmissionNavigateOptions;
514
515/**
516 * Options for a fetch() load
517 */
518type LoadFetchOptions = BaseNavigateOrFetchOptions;
519
520/**
521 * Options for a fetch() submission
522 */
523type SubmitFetchOptions = BaseNavigateOrFetchOptions & BaseSubmissionOptions;
524
525/**
526 * Options to pass to fetch()
527 */
528export type RouterFetchOptions = LoadFetchOptions | SubmitFetchOptions;
529
530/**
531 * Potential states for state.navigation
532 */
533export type NavigationStates = {
534 Idle: {
535 state: "idle";
536 location: undefined;
537 formMethod: undefined;
538 formAction: undefined;
539 formEncType: undefined;
540 formData: undefined;
541 json: undefined;
542 text: undefined;
543 };
544 Loading: {
545 state: "loading";
546 location: Location;
547 formMethod: Submission["formMethod"] | undefined;
548 formAction: Submission["formAction"] | undefined;
549 formEncType: Submission["formEncType"] | undefined;
550 formData: Submission["formData"] | undefined;
551 json: Submission["json"] | undefined;
552 text: Submission["text"] | undefined;
553 };
554 Submitting: {
555 state: "submitting";
556 location: Location;
557 formMethod: Submission["formMethod"];
558 formAction: Submission["formAction"];
559 formEncType: Submission["formEncType"];
560 formData: Submission["formData"];
561 json: Submission["json"];
562 text: Submission["text"];
563 };
564};
565
566export type Navigation = NavigationStates[keyof NavigationStates];
567
568export type RevalidationState = "idle" | "loading";
569
570/**
571 * Potential states for fetchers
572 */
573type FetcherStates<TData = any> = {
574 Idle: {
575 state: "idle";
576 formMethod: undefined;
577 formAction: undefined;
578 formEncType: undefined;
579 text: undefined;
580 formData: undefined;
581 json: undefined;
582 data: TData | undefined;
583 };
584 Loading: {
585 state: "loading";
586 formMethod: Submission["formMethod"] | undefined;
587 formAction: Submission["formAction"] | undefined;
588 formEncType: Submission["formEncType"] | undefined;
589 text: Submission["text"] | undefined;
590 formData: Submission["formData"] | undefined;
591 json: Submission["json"] | undefined;
592 data: TData | undefined;
593 };
594 Submitting: {
595 state: "submitting";
596 formMethod: Submission["formMethod"];
597 formAction: Submission["formAction"];
598 formEncType: Submission["formEncType"];
599 text: Submission["text"];
600 formData: Submission["formData"];
601 json: Submission["json"];
602 data: TData | undefined;
603 };
604};
605
606export type Fetcher<TData = any> =
607 FetcherStates<TData>[keyof FetcherStates<TData>];
608
609interface BlockerBlocked {
610 state: "blocked";
611 reset(): void;
612 proceed(): void;
613 location: Location;
614}
615
616interface BlockerUnblocked {
617 state: "unblocked";
618 reset: undefined;
619 proceed: undefined;
620 location: undefined;
621}
622
623interface BlockerProceeding {
624 state: "proceeding";
625 reset: undefined;
626 proceed: undefined;
627 location: Location;
628}
629
630export type Blocker = BlockerUnblocked | BlockerBlocked | BlockerProceeding;
631
632export type BlockerFunction = (args: {
633 currentLocation: Location;
634 nextLocation: Location;
635 historyAction: HistoryAction;
636}) => boolean;
637
638interface ShortCircuitable {
639 /**
640 * startNavigation does not need to complete the navigation because we
641 * redirected or got interrupted
642 */
643 shortCircuited?: boolean;
644}
645
646type PendingActionResult = [string, SuccessResult | ErrorResult];
647
648interface HandleActionResult extends ShortCircuitable {
649 /**
650 * Route matches which may have been updated from fog of war discovery
651 */
652 matches?: RouterState["matches"];
653 /**
654 * Tuple for the returned or thrown value from the current action. The routeId
655 * is the action route for success and the bubbled boundary route for errors.
656 */
657 pendingActionResult?: PendingActionResult;
658}
659
660interface HandleLoadersResult extends ShortCircuitable {
661 /**
662 * Route matches which may have been updated from fog of war discovery
663 */
664 matches?: RouterState["matches"];
665 /**
666 * loaderData returned from the current set of loaders
667 */
668 loaderData?: RouterState["loaderData"];
669 /**
670 * errors thrown from the current set of loaders
671 */
672 errors?: RouterState["errors"];
673}
674
675/**
676 * Cached info for active fetcher.load() instances so they can participate
677 * in revalidation
678 */
679interface FetchLoadMatch {
680 routeId: string;
681 path: string;
682}
683
684/**
685 * Identified fetcher.load() calls that need to be revalidated
686 */
687interface RevalidatingFetcher extends FetchLoadMatch {
688 key: string;
689 match: AgnosticDataRouteMatch | null;
690 matches: AgnosticDataRouteMatch[] | null;
691 controller: AbortController | null;
692}
693
694const validMutationMethodsArr: MutationFormMethod[] = [
695 "post",
696 "put",
697 "patch",
698 "delete",
699];
700const validMutationMethods = new Set<MutationFormMethod>(
701 validMutationMethodsArr
702);
703
704const validRequestMethodsArr: FormMethod[] = [
705 "get",
706 ...validMutationMethodsArr,
707];
708const validRequestMethods = new Set<FormMethod>(validRequestMethodsArr);
709
710const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
711const redirectPreserveMethodStatusCodes = new Set([307, 308]);
712
713export const IDLE_NAVIGATION: NavigationStates["Idle"] = {
714 state: "idle",
715 location: undefined,
716 formMethod: undefined,
717 formAction: undefined,
718 formEncType: undefined,
719 formData: undefined,
720 json: undefined,
721 text: undefined,
722};
723
724export const IDLE_FETCHER: FetcherStates["Idle"] = {
725 state: "idle",
726 data: undefined,
727 formMethod: undefined,
728 formAction: undefined,
729 formEncType: undefined,
730 formData: undefined,
731 json: undefined,
732 text: undefined,
733};
734
735export const IDLE_BLOCKER: BlockerUnblocked = {
736 state: "unblocked",
737 proceed: undefined,
738 reset: undefined,
739 location: undefined,
740};
741
742const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
743
744const defaultMapRouteProperties: MapRoutePropertiesFunction = (route) => ({
745 hasErrorBoundary: Boolean(route.hasErrorBoundary),
746});
747
748const TRANSITIONS_STORAGE_KEY = "remix-router-transitions";
749
750//#endregion
751
752////////////////////////////////////////////////////////////////////////////////
753//#region createRouter
754////////////////////////////////////////////////////////////////////////////////
755
756/**
757 * Create a router and listen to history POP navigations
758 */
759export function createRouter(init: RouterInit): Router {
760 const routerWindow = init.window
761 ? init.window
762 : typeof window !== "undefined"
763 ? window
764 : undefined;
765 const isBrowser =
766 typeof routerWindow !== "undefined" &&
767 typeof routerWindow.document !== "undefined" &&
768 typeof routerWindow.document.createElement !== "undefined";
769 const isServer = !isBrowser;
770
771 invariant(
772 init.routes.length > 0,
773 "You must provide a non-empty routes array to createRouter"
774 );
775
776 let mapRouteProperties: MapRoutePropertiesFunction;
777 if (init.mapRouteProperties) {
778 mapRouteProperties = init.mapRouteProperties;
779 } else if (init.detectErrorBoundary) {
780 // If they are still using the deprecated version, wrap it with the new API
781 let detectErrorBoundary = init.detectErrorBoundary;
782 mapRouteProperties = (route) => ({
783 hasErrorBoundary: detectErrorBoundary(route),
784 });
785 } else {
786 mapRouteProperties = defaultMapRouteProperties;
787 }
788
789 // Routes keyed by ID
790 let manifest: RouteManifest = {};
791 // Routes in tree format for matching
792 let dataRoutes = convertRoutesToDataRoutes(
793 init.routes,
794 mapRouteProperties,
795 undefined,
796 manifest
797 );
798 let inFlightDataRoutes: AgnosticDataRouteObject[] | undefined;
799 let basename = init.basename || "/";
800 let dataStrategyImpl = init.unstable_dataStrategy || defaultDataStrategy;
801 let patchRoutesOnMissImpl = init.unstable_patchRoutesOnMiss;
802
803 // Config driven behavior flags
804 let future: FutureConfig = {
805 v7_fetcherPersist: false,
806 v7_normalizeFormMethod: false,
807 v7_partialHydration: false,
808 v7_prependBasename: false,
809 v7_relativeSplatPath: false,
810 v7_skipActionErrorRevalidation: false,
811 ...init.future,
812 };
813 // Cleanup function for history
814 let unlistenHistory: (() => void) | null = null;
815 // Externally-provided functions to call on all state changes
816 let subscribers = new Set<RouterSubscriber>();
817 // Externally-provided object to hold scroll restoration locations during routing
818 let savedScrollPositions: Record<string, number> | null = null;
819 // Externally-provided function to get scroll restoration keys
820 let getScrollRestorationKey: GetScrollRestorationKeyFunction | null = null;
821 // Externally-provided function to get current scroll position
822 let getScrollPosition: GetScrollPositionFunction | null = null;
823 // One-time flag to control the initial hydration scroll restoration. Because
824 // we don't get the saved positions from <ScrollRestoration /> until _after_
825 // the initial render, we need to manually trigger a separate updateState to
826 // send along the restoreScrollPosition
827 // Set to true if we have `hydrationData` since we assume we were SSR'd and that
828 // SSR did the initial scroll restoration.
829 let initialScrollRestored = init.hydrationData != null;
830
831 let initialMatches = matchRoutes(dataRoutes, init.history.location, basename);
832 let initialErrors: RouteData | null = null;
833
834 if (initialMatches == null && !patchRoutesOnMissImpl) {
835 // If we do not match a user-provided-route, fall back to the root
836 // to allow the error boundary to take over
837 let error = getInternalRouterError(404, {
838 pathname: init.history.location.pathname,
839 });
840 let { matches, route } = getShortCircuitMatches(dataRoutes);
841 initialMatches = matches;
842 initialErrors = { [route.id]: error };
843 }
844
845 // In SPA apps, if the user provided a patchRoutesOnMiss implementation and
846 // our initial match is a splat route, clear them out so we run through lazy
847 // discovery on hydration in case there's a more accurate lazy route match.
848 // In SSR apps (with `hydrationData`), we expect that the server will send
849 // up the proper matched routes so we don't want to run lazy discovery on
850 // initial hydration and want to hydrate into the splat route.
851 if (initialMatches && !init.hydrationData) {
852 let fogOfWar = checkFogOfWar(
853 initialMatches,
854 dataRoutes,
855 init.history.location.pathname
856 );
857 if (fogOfWar.active) {
858 initialMatches = null;
859 }
860 }
861
862 let initialized: boolean;
863 if (!initialMatches) {
864 initialized = false;
865 initialMatches = [];
866
867 // If partial hydration and fog of war is enabled, we will be running
868 // `patchRoutesOnMiss` during hydration so include any partial matches as
869 // the initial matches so we can properly render `HydrateFallback`'s
870 if (future.v7_partialHydration) {
871 let fogOfWar = checkFogOfWar(
872 null,
873 dataRoutes,
874 init.history.location.pathname
875 );
876 if (fogOfWar.active && fogOfWar.matches) {
877 initialMatches = fogOfWar.matches;
878 }
879 }
880 } else if (initialMatches.some((m) => m.route.lazy)) {
881 // All initialMatches need to be loaded before we're ready. If we have lazy
882 // functions around still then we'll need to run them in initialize()
883 initialized = false;
884 } else if (!initialMatches.some((m) => m.route.loader)) {
885 // If we've got no loaders to run, then we're good to go
886 initialized = true;
887 } else if (future.v7_partialHydration) {
888 // If partial hydration is enabled, we're initialized so long as we were
889 // provided with hydrationData for every route with a loader, and no loaders
890 // were marked for explicit hydration
891 let loaderData = init.hydrationData ? init.hydrationData.loaderData : null;
892 let errors = init.hydrationData ? init.hydrationData.errors : null;
893 let isRouteInitialized = (m: AgnosticDataRouteMatch) => {
894 // No loader, nothing to initialize
895 if (!m.route.loader) {
896 return true;
897 }
898 // Explicitly opting-in to running on hydration
899 if (
900 typeof m.route.loader === "function" &&
901 m.route.loader.hydrate === true
902 ) {
903 return false;
904 }
905 // Otherwise, initialized if hydrated with data or an error
906 return (
907 (loaderData && loaderData[m.route.id] !== undefined) ||
908 (errors && errors[m.route.id] !== undefined)
909 );
910 };
911
912 // If errors exist, don't consider routes below the boundary
913 if (errors) {
914 let idx = initialMatches.findIndex(
915 (m) => errors![m.route.id] !== undefined
916 );
917 initialized = initialMatches.slice(0, idx + 1).every(isRouteInitialized);
918 } else {
919 initialized = initialMatches.every(isRouteInitialized);
920 }
921 } else {
922 // Without partial hydration - we're initialized if we were provided any
923 // hydrationData - which is expected to be complete
924 initialized = init.hydrationData != null;
925 }
926
927 let router: Router;
928 let state: RouterState = {
929 historyAction: init.history.action,
930 location: init.history.location,
931 matches: initialMatches,
932 initialized,
933 navigation: IDLE_NAVIGATION,
934 // Don't restore on initial updateState() if we were SSR'd
935 restoreScrollPosition: init.hydrationData != null ? false : null,
936 preventScrollReset: false,
937 revalidation: "idle",
938 loaderData: (init.hydrationData && init.hydrationData.loaderData) || {},
939 actionData: (init.hydrationData && init.hydrationData.actionData) || null,
940 errors: (init.hydrationData && init.hydrationData.errors) || initialErrors,
941 fetchers: new Map(),
942 blockers: new Map(),
943 };
944
945 // -- Stateful internal variables to manage navigations --
946 // Current navigation in progress (to be committed in completeNavigation)
947 let pendingAction: HistoryAction = HistoryAction.Pop;
948
949 // Should the current navigation prevent the scroll reset if scroll cannot
950 // be restored?
951 let pendingPreventScrollReset = false;
952
953 // AbortController for the active navigation
954 let pendingNavigationController: AbortController | null;
955
956 // Should the current navigation enable document.startViewTransition?
957 let pendingViewTransitionEnabled = false;
958
959 // Store applied view transitions so we can apply them on POP
960 let appliedViewTransitions: Map<string, Set<string>> = new Map<
961 string,
962 Set<string>
963 >();
964
965 // Cleanup function for persisting applied transitions to sessionStorage
966 let removePageHideEventListener: (() => void) | null = null;
967
968 // We use this to avoid touching history in completeNavigation if a
969 // revalidation is entirely uninterrupted
970 let isUninterruptedRevalidation = false;
971
972 // Use this internal flag to force revalidation of all loaders:
973 // - submissions (completed or interrupted)
974 // - useRevalidator()
975 // - X-Remix-Revalidate (from redirect)
976 let isRevalidationRequired = false;
977
978 // Use this internal array to capture routes that require revalidation due
979 // to a cancelled deferred on action submission
980 let cancelledDeferredRoutes: string[] = [];
981
982 // Use this internal array to capture fetcher loads that were cancelled by an
983 // action navigation and require revalidation
984 let cancelledFetcherLoads: Set<string> = new Set();
985
986 // AbortControllers for any in-flight fetchers
987 let fetchControllers = new Map<string, AbortController>();
988
989 // Track loads based on the order in which they started
990 let incrementingLoadId = 0;
991
992 // Track the outstanding pending navigation data load to be compared against
993 // the globally incrementing load when a fetcher load lands after a completed
994 // navigation
995 let pendingNavigationLoadId = -1;
996
997 // Fetchers that triggered data reloads as a result of their actions
998 let fetchReloadIds = new Map<string, number>();
999
1000 // Fetchers that triggered redirect navigations
1001 let fetchRedirectIds = new Set<string>();
1002
1003 // Most recent href/match for fetcher.load calls for fetchers
1004 let fetchLoadMatches = new Map<string, FetchLoadMatch>();
1005
1006 // Ref-count mounted fetchers so we know when it's ok to clean them up
1007 let activeFetchers = new Map<string, number>();
1008
1009 // Fetchers that have requested a delete when using v7_fetcherPersist,
1010 // they'll be officially removed after they return to idle
1011 let deletedFetchers = new Set<string>();
1012
1013 // Store DeferredData instances for active route matches. When a
1014 // route loader returns defer() we stick one in here. Then, when a nested
1015 // promise resolves we update loaderData. If a new navigation starts we
1016 // cancel active deferreds for eliminated routes.
1017 let activeDeferreds = new Map<string, DeferredData>();
1018
1019 // Store blocker functions in a separate Map outside of router state since
1020 // we don't need to update UI state if they change
1021 let blockerFunctions = new Map<string, BlockerFunction>();
1022
1023 // Map of pending patchRoutesOnMiss() promises (keyed by path/matches) so
1024 // that we only kick them off once for a given combo
1025 let pendingPatchRoutes = new Map<
1026 string,
1027 ReturnType<AgnosticPatchRoutesOnMissFunction>
1028 >();
1029
1030 // Flag to ignore the next history update, so we can revert the URL change on
1031 // a POP navigation that was blocked by the user without touching router state
1032 let ignoreNextHistoryUpdate = false;
1033
1034 // Initialize the router, all side effects should be kicked off from here.
1035 // Implemented as a Fluent API for ease of:
1036 // let router = createRouter(init).initialize();
1037 function initialize() {
1038 // If history informs us of a POP navigation, start the navigation but do not update
1039 // state. We'll update our own state once the navigation completes
1040 unlistenHistory = init.history.listen(
1041 ({ action: historyAction, location, delta }) => {
1042 // Ignore this event if it was just us resetting the URL from a
1043 // blocked POP navigation
1044 if (ignoreNextHistoryUpdate) {
1045 ignoreNextHistoryUpdate = false;
1046 return;
1047 }
1048
1049 warning(
1050 blockerFunctions.size === 0 || delta != null,
1051 "You are trying to use a blocker on a POP navigation to a location " +
1052 "that was not created by @remix-run/router. This will fail silently in " +
1053 "production. This can happen if you are navigating outside the router " +
1054 "via `window.history.pushState`/`window.location.hash` instead of using " +
1055 "router navigation APIs. This can also happen if you are using " +
1056 "createHashRouter and the user manually changes the URL."
1057 );
1058
1059 let blockerKey = shouldBlockNavigation({
1060 currentLocation: state.location,
1061 nextLocation: location,
1062 historyAction,
1063 });
1064
1065 if (blockerKey && delta != null) {
1066 // Restore the URL to match the current UI, but don't update router state
1067 ignoreNextHistoryUpdate = true;
1068 init.history.go(delta * -1);
1069
1070 // Put the blocker into a blocked state
1071 updateBlocker(blockerKey, {
1072 state: "blocked",
1073 location,
1074 proceed() {
1075 updateBlocker(blockerKey!, {
1076 state: "proceeding",
1077 proceed: undefined,
1078 reset: undefined,
1079 location,
1080 });
1081 // Re-do the same POP navigation we just blocked
1082 init.history.go(delta);
1083 },
1084 reset() {
1085 let blockers = new Map(state.blockers);
1086 blockers.set(blockerKey!, IDLE_BLOCKER);
1087 updateState({ blockers });
1088 },
1089 });
1090 return;
1091 }
1092
1093 return startNavigation(historyAction, location);
1094 }
1095 );
1096
1097 if (isBrowser) {
1098 // FIXME: This feels gross. How can we cleanup the lines between
1099 // scrollRestoration/appliedTransitions persistance?
1100 restoreAppliedTransitions(routerWindow, appliedViewTransitions);
1101 let _saveAppliedTransitions = () =>
1102 persistAppliedTransitions(routerWindow, appliedViewTransitions);
1103 routerWindow.addEventListener("pagehide", _saveAppliedTransitions);
1104 removePageHideEventListener = () =>
1105 routerWindow.removeEventListener("pagehide", _saveAppliedTransitions);
1106 }
1107
1108 // Kick off initial data load if needed. Use Pop to avoid modifying history
1109 // Note we don't do any handling of lazy here. For SPA's it'll get handled
1110 // in the normal navigation flow. For SSR it's expected that lazy modules are
1111 // resolved prior to router creation since we can't go into a fallbackElement
1112 // UI for SSR'd apps
1113 if (!state.initialized) {
1114 startNavigation(HistoryAction.Pop, state.location, {
1115 initialHydration: true,
1116 });
1117 }
1118
1119 return router;
1120 }
1121
1122 // Clean up a router and it's side effects
1123 function dispose() {
1124 if (unlistenHistory) {
1125 unlistenHistory();
1126 }
1127 if (removePageHideEventListener) {
1128 removePageHideEventListener();
1129 }
1130 subscribers.clear();
1131 pendingNavigationController && pendingNavigationController.abort();
1132 state.fetchers.forEach((_, key) => deleteFetcher(key));
1133 state.blockers.forEach((_, key) => deleteBlocker(key));
1134 }
1135
1136 // Subscribe to state updates for the router
1137 function subscribe(fn: RouterSubscriber) {
1138 subscribers.add(fn);
1139 return () => subscribers.delete(fn);
1140 }
1141
1142 // Update our state and notify the calling context of the change
1143 function updateState(
1144 newState: Partial<RouterState>,
1145 opts: {
1146 flushSync?: boolean;
1147 viewTransitionOpts?: ViewTransitionOpts;
1148 } = {}
1149 ): void {
1150 state = {
1151 ...state,
1152 ...newState,
1153 };
1154
1155 // Prep fetcher cleanup so we can tell the UI which fetcher data entries
1156 // can be removed
1157 let completedFetchers: string[] = [];
1158 let deletedFetchersKeys: string[] = [];
1159
1160 if (future.v7_fetcherPersist) {
1161 state.fetchers.forEach((fetcher, key) => {
1162 if (fetcher.state === "idle") {
1163 if (deletedFetchers.has(key)) {
1164 // Unmounted from the UI and can be totally removed
1165 deletedFetchersKeys.push(key);
1166 } else {
1167 // Returned to idle but still mounted in the UI, so semi-remains for
1168 // revalidations and such
1169 completedFetchers.push(key);
1170 }
1171 }
1172 });
1173 }
1174
1175 // Iterate over a local copy so that if flushSync is used and we end up
1176 // removing and adding a new subscriber due to the useCallback dependencies,
1177 // we don't get ourselves into a loop calling the new subscriber immediately
1178 [...subscribers].forEach((subscriber) =>
1179 subscriber(state, {
1180 deletedFetchers: deletedFetchersKeys,
1181 unstable_viewTransitionOpts: opts.viewTransitionOpts,
1182 unstable_flushSync: opts.flushSync === true,
1183 })
1184 );
1185
1186 // Remove idle fetchers from state since we only care about in-flight fetchers.
1187 if (future.v7_fetcherPersist) {
1188 completedFetchers.forEach((key) => state.fetchers.delete(key));
1189 deletedFetchersKeys.forEach((key) => deleteFetcher(key));
1190 }
1191 }
1192
1193 // Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION
1194 // and setting state.[historyAction/location/matches] to the new route.
1195 // - Location is a required param
1196 // - Navigation will always be set to IDLE_NAVIGATION
1197 // - Can pass any other state in newState
1198 function completeNavigation(
1199 location: Location,
1200 newState: Partial<Omit<RouterState, "action" | "location" | "navigation">>,
1201 { flushSync }: { flushSync?: boolean } = {}
1202 ): void {
1203 // Deduce if we're in a loading/actionReload state:
1204 // - We have committed actionData in the store
1205 // - The current navigation was a mutation submission
1206 // - We're past the submitting state and into the loading state
1207 // - The location being loaded is not the result of a redirect
1208 let isActionReload =
1209 state.actionData != null &&
1210 state.navigation.formMethod != null &&
1211 isMutationMethod(state.navigation.formMethod) &&
1212 state.navigation.state === "loading" &&
1213 location.state?._isRedirect !== true;
1214
1215 let actionData: RouteData | null;
1216 if (newState.actionData) {
1217 if (Object.keys(newState.actionData).length > 0) {
1218 actionData = newState.actionData;
1219 } else {
1220 // Empty actionData -> clear prior actionData due to an action error
1221 actionData = null;
1222 }
1223 } else if (isActionReload) {
1224 // Keep the current data if we're wrapping up the action reload
1225 actionData = state.actionData;
1226 } else {
1227 // Clear actionData on any other completed navigations
1228 actionData = null;
1229 }
1230
1231 // Always preserve any existing loaderData from re-used routes
1232 let loaderData = newState.loaderData
1233 ? mergeLoaderData(
1234 state.loaderData,
1235 newState.loaderData,
1236 newState.matches || [],
1237 newState.errors
1238 )
1239 : state.loaderData;
1240
1241 // On a successful navigation we can assume we got through all blockers
1242 // so we can start fresh
1243 let blockers = state.blockers;
1244 if (blockers.size > 0) {
1245 blockers = new Map(blockers);
1246 blockers.forEach((_, k) => blockers.set(k, IDLE_BLOCKER));
1247 }
1248
1249 // Always respect the user flag. Otherwise don't reset on mutation
1250 // submission navigations unless they redirect
1251 let preventScrollReset =
1252 pendingPreventScrollReset === true ||
1253 (state.navigation.formMethod != null &&
1254 isMutationMethod(state.navigation.formMethod) &&
1255 location.state?._isRedirect !== true);
1256
1257 // Commit any in-flight routes at the end of the HMR revalidation "navigation"
1258 if (inFlightDataRoutes) {
1259 dataRoutes = inFlightDataRoutes;
1260 inFlightDataRoutes = undefined;
1261 }
1262
1263 if (isUninterruptedRevalidation) {
1264 // If this was an uninterrupted revalidation then do not touch history
1265 } else if (pendingAction === HistoryAction.Pop) {
1266 // Do nothing for POP - URL has already been updated
1267 } else if (pendingAction === HistoryAction.Push) {
1268 init.history.push(location, location.state);
1269 } else if (pendingAction === HistoryAction.Replace) {
1270 init.history.replace(location, location.state);
1271 }
1272
1273 let viewTransitionOpts: ViewTransitionOpts | undefined;
1274
1275 // On POP, enable transitions if they were enabled on the original navigation
1276 if (pendingAction === HistoryAction.Pop) {
1277 // Forward takes precedence so they behave like the original navigation
1278 let priorPaths = appliedViewTransitions.get(state.location.pathname);
1279 if (priorPaths && priorPaths.has(location.pathname)) {
1280 viewTransitionOpts = {
1281 currentLocation: state.location,
1282 nextLocation: location,
1283 };
1284 } else if (appliedViewTransitions.has(location.pathname)) {
1285 // If we don't have a previous forward nav, assume we're popping back to
1286 // the new location and enable if that location previously enabled
1287 viewTransitionOpts = {
1288 currentLocation: location,
1289 nextLocation: state.location,
1290 };
1291 }
1292 } else if (pendingViewTransitionEnabled) {
1293 // Store the applied transition on PUSH/REPLACE
1294 let toPaths = appliedViewTransitions.get(state.location.pathname);
1295 if (toPaths) {
1296 toPaths.add(location.pathname);
1297 } else {
1298 toPaths = new Set<string>([location.pathname]);
1299 appliedViewTransitions.set(state.location.pathname, toPaths);
1300 }
1301 viewTransitionOpts = {
1302 currentLocation: state.location,
1303 nextLocation: location,
1304 };
1305 }
1306
1307 updateState(
1308 {
1309 ...newState, // matches, errors, fetchers go through as-is
1310 actionData,
1311 loaderData,
1312 historyAction: pendingAction,
1313 location,
1314 initialized: true,
1315 navigation: IDLE_NAVIGATION,
1316 revalidation: "idle",
1317 restoreScrollPosition: getSavedScrollPosition(
1318 location,
1319 newState.matches || state.matches
1320 ),
1321 preventScrollReset,
1322 blockers,
1323 },
1324 {
1325 viewTransitionOpts,
1326 flushSync: flushSync === true,
1327 }
1328 );
1329
1330 // Reset stateful navigation vars
1331 pendingAction = HistoryAction.Pop;
1332 pendingPreventScrollReset = false;
1333 pendingViewTransitionEnabled = false;
1334 isUninterruptedRevalidation = false;
1335 isRevalidationRequired = false;
1336 cancelledDeferredRoutes = [];
1337 }
1338
1339 // Trigger a navigation event, which can either be a numerical POP or a PUSH
1340 // replace with an optional submission
1341 async function navigate(
1342 to: number | To | null,
1343 opts?: RouterNavigateOptions
1344 ): Promise<void> {
1345 if (typeof to === "number") {
1346 init.history.go(to);
1347 return;
1348 }
1349
1350 let normalizedPath = normalizeTo(
1351 state.location,
1352 state.matches,
1353 basename,
1354 future.v7_prependBasename,
1355 to,
1356 future.v7_relativeSplatPath,
1357 opts?.fromRouteId,
1358 opts?.relative
1359 );
1360 let { path, submission, error } = normalizeNavigateOptions(
1361 future.v7_normalizeFormMethod,
1362 false,
1363 normalizedPath,
1364 opts
1365 );
1366
1367 let currentLocation = state.location;
1368 let nextLocation = createLocation(state.location, path, opts && opts.state);
1369
1370 // When using navigate as a PUSH/REPLACE we aren't reading an already-encoded
1371 // URL from window.location, so we need to encode it here so the behavior
1372 // remains the same as POP and non-data-router usages. new URL() does all
1373 // the same encoding we'd get from a history.pushState/window.location read
1374 // without having to touch history
1375 nextLocation = {
1376 ...nextLocation,
1377 ...init.history.encodeLocation(nextLocation),
1378 };
1379
1380 let userReplace = opts && opts.replace != null ? opts.replace : undefined;
1381
1382 let historyAction = HistoryAction.Push;
1383
1384 if (userReplace === true) {
1385 historyAction = HistoryAction.Replace;
1386 } else if (userReplace === false) {
1387 // no-op
1388 } else if (
1389 submission != null &&
1390 isMutationMethod(submission.formMethod) &&
1391 submission.formAction === state.location.pathname + state.location.search
1392 ) {
1393 // By default on submissions to the current location we REPLACE so that
1394 // users don't have to double-click the back button to get to the prior
1395 // location. If the user redirects to a different location from the
1396 // action/loader this will be ignored and the redirect will be a PUSH
1397 historyAction = HistoryAction.Replace;
1398 }
1399
1400 let preventScrollReset =
1401 opts && "preventScrollReset" in opts
1402 ? opts.preventScrollReset === true
1403 : undefined;
1404
1405 let flushSync = (opts && opts.unstable_flushSync) === true;
1406
1407 let blockerKey = shouldBlockNavigation({
1408 currentLocation,
1409 nextLocation,
1410 historyAction,
1411 });
1412
1413 if (blockerKey) {
1414 // Put the blocker into a blocked state
1415 updateBlocker(blockerKey, {
1416 state: "blocked",
1417 location: nextLocation,
1418 proceed() {
1419 updateBlocker(blockerKey!, {
1420 state: "proceeding",
1421 proceed: undefined,
1422 reset: undefined,
1423 location: nextLocation,
1424 });
1425 // Send the same navigation through
1426 navigate(to, opts);
1427 },
1428 reset() {
1429 let blockers = new Map(state.blockers);
1430 blockers.set(blockerKey!, IDLE_BLOCKER);
1431 updateState({ blockers });
1432 },
1433 });
1434 return;
1435 }
1436
1437 return await startNavigation(historyAction, nextLocation, {
1438 submission,
1439 // Send through the formData serialization error if we have one so we can
1440 // render at the right error boundary after we match routes
1441 pendingError: error,
1442 preventScrollReset,
1443 replace: opts && opts.replace,
1444 enableViewTransition: opts && opts.unstable_viewTransition,
1445 flushSync,
1446 });
1447 }
1448
1449 // Revalidate all current loaders. If a navigation is in progress or if this
1450 // is interrupted by a navigation, allow this to "succeed" by calling all
1451 // loaders during the next loader round
1452 function revalidate() {
1453 interruptActiveLoads();
1454 updateState({ revalidation: "loading" });
1455
1456 // If we're currently submitting an action, we don't need to start a new
1457 // navigation, we'll just let the follow up loader execution call all loaders
1458 if (state.navigation.state === "submitting") {
1459 return;
1460 }
1461
1462 // If we're currently in an idle state, start a new navigation for the current
1463 // action/location and mark it as uninterrupted, which will skip the history
1464 // update in completeNavigation
1465 if (state.navigation.state === "idle") {
1466 startNavigation(state.historyAction, state.location, {
1467 startUninterruptedRevalidation: true,
1468 });
1469 return;
1470 }
1471
1472 // Otherwise, if we're currently in a loading state, just start a new
1473 // navigation to the navigation.location but do not trigger an uninterrupted
1474 // revalidation so that history correctly updates once the navigation completes
1475 startNavigation(
1476 pendingAction || state.historyAction,
1477 state.navigation.location,
1478 { overrideNavigation: state.navigation }
1479 );
1480 }
1481
1482 // Start a navigation to the given action/location. Can optionally provide a
1483 // overrideNavigation which will override the normalLoad in the case of a redirect
1484 // navigation
1485 async function startNavigation(
1486 historyAction: HistoryAction,
1487 location: Location,
1488 opts?: {
1489 initialHydration?: boolean;
1490 submission?: Submission;
1491 fetcherSubmission?: Submission;
1492 overrideNavigation?: Navigation;
1493 pendingError?: ErrorResponseImpl;
1494 startUninterruptedRevalidation?: boolean;
1495 preventScrollReset?: boolean;
1496 replace?: boolean;
1497 enableViewTransition?: boolean;
1498 flushSync?: boolean;
1499 }
1500 ): Promise<void> {
1501 // Abort any in-progress navigations and start a new one. Unset any ongoing
1502 // uninterrupted revalidations unless told otherwise, since we want this
1503 // new navigation to update history normally
1504 pendingNavigationController && pendingNavigationController.abort();
1505 pendingNavigationController = null;
1506 pendingAction = historyAction;
1507 isUninterruptedRevalidation =
1508 (opts && opts.startUninterruptedRevalidation) === true;
1509
1510 // Save the current scroll position every time we start a new navigation,
1511 // and track whether we should reset scroll on completion
1512 saveScrollPosition(state.location, state.matches);
1513 pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
1514
1515 pendingViewTransitionEnabled = (opts && opts.enableViewTransition) === true;
1516
1517 let routesToUse = inFlightDataRoutes || dataRoutes;
1518 let loadingNavigation = opts && opts.overrideNavigation;
1519 let matches = matchRoutes(routesToUse, location, basename);
1520 let flushSync = (opts && opts.flushSync) === true;
1521
1522 let fogOfWar = checkFogOfWar(matches, routesToUse, location.pathname);
1523 if (fogOfWar.active && fogOfWar.matches) {
1524 matches = fogOfWar.matches;
1525 }
1526
1527 // Short circuit with a 404 on the root error boundary if we match nothing
1528 if (!matches) {
1529 let { error, notFoundMatches, route } = handleNavigational404(
1530 location.pathname
1531 );
1532 completeNavigation(
1533 location,
1534 {
1535 matches: notFoundMatches,
1536 loaderData: {},
1537 errors: {
1538 [route.id]: error,
1539 },
1540 },
1541 { flushSync }
1542 );
1543 return;
1544 }
1545
1546 // Short circuit if it's only a hash change and not a revalidation or
1547 // mutation submission.
1548 //
1549 // Ignore on initial page loads because since the initial load will always
1550 // be "same hash". For example, on /page#hash and submit a <Form method="post">
1551 // which will default to a navigation to /page
1552 if (
1553 state.initialized &&
1554 !isRevalidationRequired &&
1555 isHashChangeOnly(state.location, location) &&
1556 !(opts && opts.submission && isMutationMethod(opts.submission.formMethod))
1557 ) {
1558 completeNavigation(location, { matches }, { flushSync });
1559 return;
1560 }
1561
1562 // Create a controller/Request for this navigation
1563 pendingNavigationController = new AbortController();
1564 let request = createClientSideRequest(
1565 init.history,
1566 location,
1567 pendingNavigationController.signal,
1568 opts && opts.submission
1569 );
1570 let pendingActionResult: PendingActionResult | undefined;
1571
1572 if (opts && opts.pendingError) {
1573 // If we have a pendingError, it means the user attempted a GET submission
1574 // with binary FormData so assign here and skip to handleLoaders. That
1575 // way we handle calling loaders above the boundary etc. It's not really
1576 // different from an actionError in that sense.
1577 pendingActionResult = [
1578 findNearestBoundary(matches).route.id,
1579 { type: ResultType.error, error: opts.pendingError },
1580 ];
1581 } else if (
1582 opts &&
1583 opts.submission &&
1584 isMutationMethod(opts.submission.formMethod)
1585 ) {
1586 // Call action if we received an action submission
1587 let actionResult = await handleAction(
1588 request,
1589 location,
1590 opts.submission,
1591 matches,
1592 fogOfWar.active,
1593 { replace: opts.replace, flushSync }
1594 );
1595
1596 if (actionResult.shortCircuited) {
1597 return;
1598 }
1599
1600 // If we received a 404 from handleAction, it's because we couldn't lazily
1601 // discover the destination route so we don't want to call loaders
1602 if (actionResult.pendingActionResult) {
1603 let [routeId, result] = actionResult.pendingActionResult;
1604 if (
1605 isErrorResult(result) &&
1606 isRouteErrorResponse(result.error) &&
1607 result.error.status === 404
1608 ) {
1609 pendingNavigationController = null;
1610
1611 completeNavigation(location, {
1612 matches: actionResult.matches,
1613 loaderData: {},
1614 errors: {
1615 [routeId]: result.error,
1616 },
1617 });
1618 return;
1619 }
1620 }
1621
1622 matches = actionResult.matches || matches;
1623 pendingActionResult = actionResult.pendingActionResult;
1624 loadingNavigation = getLoadingNavigation(location, opts.submission);
1625 flushSync = false;
1626 // No need to do fog of war matching again on loader execution
1627 fogOfWar.active = false;
1628
1629 // Create a GET request for the loaders
1630 request = createClientSideRequest(
1631 init.history,
1632 request.url,
1633 request.signal
1634 );
1635 }
1636
1637 // Call loaders
1638 let {
1639 shortCircuited,
1640 matches: updatedMatches,
1641 loaderData,
1642 errors,
1643 } = await handleLoaders(
1644 request,
1645 location,
1646 matches,
1647 fogOfWar.active,
1648 loadingNavigation,
1649 opts && opts.submission,
1650 opts && opts.fetcherSubmission,
1651 opts && opts.replace,
1652 opts && opts.initialHydration === true,
1653 flushSync,
1654 pendingActionResult
1655 );
1656
1657 if (shortCircuited) {
1658 return;
1659 }
1660
1661 // Clean up now that the action/loaders have completed. Don't clean up if
1662 // we short circuited because pendingNavigationController will have already
1663 // been assigned to a new controller for the next navigation
1664 pendingNavigationController = null;
1665
1666 completeNavigation(location, {
1667 matches: updatedMatches || matches,
1668 ...getActionDataForCommit(pendingActionResult),
1669 loaderData,
1670 errors,
1671 });
1672 }
1673
1674 // Call the action matched by the leaf route for this navigation and handle
1675 // redirects/errors
1676 async function handleAction(
1677 request: Request,
1678 location: Location,
1679 submission: Submission,
1680 matches: AgnosticDataRouteMatch[],
1681 isFogOfWar: boolean,
1682 opts: { replace?: boolean; flushSync?: boolean } = {}
1683 ): Promise<HandleActionResult> {
1684 interruptActiveLoads();
1685
1686 // Put us in a submitting state
1687 let navigation = getSubmittingNavigation(location, submission);
1688 updateState({ navigation }, { flushSync: opts.flushSync === true });
1689
1690 if (isFogOfWar) {
1691 let discoverResult = await discoverRoutes(
1692 matches,
1693 location.pathname,
1694 request.signal
1695 );
1696 if (discoverResult.type === "aborted") {
1697 return { shortCircuited: true };
1698 } else if (discoverResult.type === "error") {
1699 let { boundaryId, error } = handleDiscoverRouteError(
1700 location.pathname,
1701 discoverResult
1702 );
1703 return {
1704 matches: discoverResult.partialMatches,
1705 pendingActionResult: [
1706 boundaryId,
1707 {
1708 type: ResultType.error,
1709 error,
1710 },
1711 ],
1712 };
1713 } else if (!discoverResult.matches) {
1714 let { notFoundMatches, error, route } = handleNavigational404(
1715 location.pathname
1716 );
1717 return {
1718 matches: notFoundMatches,
1719 pendingActionResult: [
1720 route.id,
1721 {
1722 type: ResultType.error,
1723 error,
1724 },
1725 ],
1726 };
1727 } else {
1728 matches = discoverResult.matches;
1729 }
1730 }
1731
1732 // Call our action and get the result
1733 let result: DataResult;
1734 let actionMatch = getTargetMatch(matches, location);
1735
1736 if (!actionMatch.route.action && !actionMatch.route.lazy) {
1737 result = {
1738 type: ResultType.error,
1739 error: getInternalRouterError(405, {
1740 method: request.method,
1741 pathname: location.pathname,
1742 routeId: actionMatch.route.id,
1743 }),
1744 };
1745 } else {
1746 let results = await callDataStrategy(
1747 "action",
1748 request,
1749 [actionMatch],
1750 matches
1751 );
1752 result = results[0];
1753
1754 if (request.signal.aborted) {
1755 return { shortCircuited: true };
1756 }
1757 }
1758
1759 if (isRedirectResult(result)) {
1760 let replace: boolean;
1761 if (opts && opts.replace != null) {
1762 replace = opts.replace;
1763 } else {
1764 // If the user didn't explicity indicate replace behavior, replace if
1765 // we redirected to the exact same location we're currently at to avoid
1766 // double back-buttons
1767 let location = normalizeRedirectLocation(
1768 result.response.headers.get("Location")!,
1769 new URL(request.url),
1770 basename
1771 );
1772 replace = location === state.location.pathname + state.location.search;
1773 }
1774 await startRedirectNavigation(request, result, {
1775 submission,
1776 replace,
1777 });
1778 return { shortCircuited: true };
1779 }
1780
1781 if (isDeferredResult(result)) {
1782 throw getInternalRouterError(400, { type: "defer-action" });
1783 }
1784
1785 if (isErrorResult(result)) {
1786 // Store off the pending error - we use it to determine which loaders
1787 // to call and will commit it when we complete the navigation
1788 let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
1789
1790 // By default, all submissions to the current location are REPLACE
1791 // navigations, but if the action threw an error that'll be rendered in
1792 // an errorElement, we fall back to PUSH so that the user can use the
1793 // back button to get back to the pre-submission form location to try
1794 // again
1795 if ((opts && opts.replace) !== true) {
1796 pendingAction = HistoryAction.Push;
1797 }
1798
1799 return {
1800 matches,
1801 pendingActionResult: [boundaryMatch.route.id, result],
1802 };
1803 }
1804
1805 return {
1806 matches,
1807 pendingActionResult: [actionMatch.route.id, result],
1808 };
1809 }
1810
1811 // Call all applicable loaders for the given matches, handling redirects,
1812 // errors, etc.
1813 async function handleLoaders(
1814 request: Request,
1815 location: Location,
1816 matches: AgnosticDataRouteMatch[],
1817 isFogOfWar: boolean,
1818 overrideNavigation?: Navigation,
1819 submission?: Submission,
1820 fetcherSubmission?: Submission,
1821 replace?: boolean,
1822 initialHydration?: boolean,
1823 flushSync?: boolean,
1824 pendingActionResult?: PendingActionResult
1825 ): Promise<HandleLoadersResult> {
1826 // Figure out the right navigation we want to use for data loading
1827 let loadingNavigation =
1828 overrideNavigation || getLoadingNavigation(location, submission);
1829
1830 // If this was a redirect from an action we don't have a "submission" but
1831 // we have it on the loading navigation so use that if available
1832 let activeSubmission =
1833 submission ||
1834 fetcherSubmission ||
1835 getSubmissionFromNavigation(loadingNavigation);
1836
1837 // If this is an uninterrupted revalidation, we remain in our current idle
1838 // state. If not, we need to switch to our loading state and load data,
1839 // preserving any new action data or existing action data (in the case of
1840 // a revalidation interrupting an actionReload)
1841 // If we have partialHydration enabled, then don't update the state for the
1842 // initial data load since it's not a "navigation"
1843 let shouldUpdateNavigationState =
1844 !isUninterruptedRevalidation &&
1845 (!future.v7_partialHydration || !initialHydration);
1846
1847 // When fog of war is enabled, we enter our `loading` state earlier so we
1848 // can discover new routes during the `loading` state. We skip this if
1849 // we've already run actions since we would have done our matching already.
1850 // If the children() function threw then, we want to proceed with the
1851 // partial matches it discovered.
1852 if (isFogOfWar) {
1853 if (shouldUpdateNavigationState) {
1854 let actionData = getUpdatedActionData(pendingActionResult);
1855 updateState(
1856 {
1857 navigation: loadingNavigation,
1858 ...(actionData !== undefined ? { actionData } : {}),
1859 },
1860 {
1861 flushSync,
1862 }
1863 );
1864 }
1865
1866 let discoverResult = await discoverRoutes(
1867 matches,
1868 location.pathname,
1869 request.signal
1870 );
1871
1872 if (discoverResult.type === "aborted") {
1873 return { shortCircuited: true };
1874 } else if (discoverResult.type === "error") {
1875 let { boundaryId, error } = handleDiscoverRouteError(
1876 location.pathname,
1877 discoverResult
1878 );
1879 return {
1880 matches: discoverResult.partialMatches,
1881 loaderData: {},
1882 errors: {
1883 [boundaryId]: error,
1884 },
1885 };
1886 } else if (!discoverResult.matches) {
1887 let { error, notFoundMatches, route } = handleNavigational404(
1888 location.pathname
1889 );
1890 return {
1891 matches: notFoundMatches,
1892 loaderData: {},
1893 errors: {
1894 [route.id]: error,
1895 },
1896 };
1897 } else {
1898 matches = discoverResult.matches;
1899 }
1900 }
1901
1902 let routesToUse = inFlightDataRoutes || dataRoutes;
1903 let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(
1904 init.history,
1905 state,
1906 matches,
1907 activeSubmission,
1908 location,
1909 future.v7_partialHydration && initialHydration === true,
1910 future.v7_skipActionErrorRevalidation,
1911 isRevalidationRequired,
1912 cancelledDeferredRoutes,
1913 cancelledFetcherLoads,
1914 deletedFetchers,
1915 fetchLoadMatches,
1916 fetchRedirectIds,
1917 routesToUse,
1918 basename,
1919 pendingActionResult
1920 );
1921
1922 // Cancel pending deferreds for no-longer-matched routes or routes we're
1923 // about to reload. Note that if this is an action reload we would have
1924 // already cancelled all pending deferreds so this would be a no-op
1925 cancelActiveDeferreds(
1926 (routeId) =>
1927 !(matches && matches.some((m) => m.route.id === routeId)) ||
1928 (matchesToLoad && matchesToLoad.some((m) => m.route.id === routeId))
1929 );
1930
1931 pendingNavigationLoadId = ++incrementingLoadId;
1932
1933 // Short circuit if we have no loaders to run
1934 if (matchesToLoad.length === 0 && revalidatingFetchers.length === 0) {
1935 let updatedFetchers = markFetchRedirectsDone();
1936 completeNavigation(
1937 location,
1938 {
1939 matches,
1940 loaderData: {},
1941 // Commit pending error if we're short circuiting
1942 errors:
1943 pendingActionResult && isErrorResult(pendingActionResult[1])
1944 ? { [pendingActionResult[0]]: pendingActionResult[1].error }
1945 : null,
1946 ...getActionDataForCommit(pendingActionResult),
1947 ...(updatedFetchers ? { fetchers: new Map(state.fetchers) } : {}),
1948 },
1949 { flushSync }
1950 );
1951 return { shortCircuited: true };
1952 }
1953
1954 if (shouldUpdateNavigationState) {
1955 let updates: Partial<RouterState> = {};
1956 if (!isFogOfWar) {
1957 // Only update navigation/actionNData if we didn't already do it above
1958 updates.navigation = loadingNavigation;
1959 let actionData = getUpdatedActionData(pendingActionResult);
1960 if (actionData !== undefined) {
1961 updates.actionData = actionData;
1962 }
1963 }
1964 if (revalidatingFetchers.length > 0) {
1965 updates.fetchers = getUpdatedRevalidatingFetchers(revalidatingFetchers);
1966 }
1967 updateState(updates, { flushSync });
1968 }
1969
1970 revalidatingFetchers.forEach((rf) => {
1971 if (fetchControllers.has(rf.key)) {
1972 abortFetcher(rf.key);
1973 }
1974 if (rf.controller) {
1975 // Fetchers use an independent AbortController so that aborting a fetcher
1976 // (via deleteFetcher) does not abort the triggering navigation that
1977 // triggered the revalidation
1978 fetchControllers.set(rf.key, rf.controller);
1979 }
1980 });
1981
1982 // Proxy navigation abort through to revalidation fetchers
1983 let abortPendingFetchRevalidations = () =>
1984 revalidatingFetchers.forEach((f) => abortFetcher(f.key));
1985 if (pendingNavigationController) {
1986 pendingNavigationController.signal.addEventListener(
1987 "abort",
1988 abortPendingFetchRevalidations
1989 );
1990 }
1991
1992 let { loaderResults, fetcherResults } =
1993 await callLoadersAndMaybeResolveData(
1994 state.matches,
1995 matches,
1996 matchesToLoad,
1997 revalidatingFetchers,
1998 request
1999 );
2000
2001 if (request.signal.aborted) {
2002 return { shortCircuited: true };
2003 }
2004
2005 // Clean up _after_ loaders have completed. Don't clean up if we short
2006 // circuited because fetchControllers would have been aborted and
2007 // reassigned to new controllers for the next navigation
2008 if (pendingNavigationController) {
2009 pendingNavigationController.signal.removeEventListener(
2010 "abort",
2011 abortPendingFetchRevalidations
2012 );
2013 }
2014 revalidatingFetchers.forEach((rf) => fetchControllers.delete(rf.key));
2015
2016 // If any loaders returned a redirect Response, start a new REPLACE navigation
2017 let redirect = findRedirect([...loaderResults, ...fetcherResults]);
2018 if (redirect) {
2019 if (redirect.idx >= matchesToLoad.length) {
2020 // If this redirect came from a fetcher make sure we mark it in
2021 // fetchRedirectIds so it doesn't get revalidated on the next set of
2022 // loader executions
2023 let fetcherKey =
2024 revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
2025 fetchRedirectIds.add(fetcherKey);
2026 }
2027 await startRedirectNavigation(request, redirect.result, {
2028 replace,
2029 });
2030 return { shortCircuited: true };
2031 }
2032
2033 // Process and commit output from loaders
2034 let { loaderData, errors } = processLoaderData(
2035 state,
2036 matches,
2037 matchesToLoad,
2038 loaderResults,
2039 pendingActionResult,
2040 revalidatingFetchers,
2041 fetcherResults,
2042 activeDeferreds
2043 );
2044
2045 // Wire up subscribers to update loaderData as promises settle
2046 activeDeferreds.forEach((deferredData, routeId) => {
2047 deferredData.subscribe((aborted) => {
2048 // Note: No need to updateState here since the TrackedPromise on
2049 // loaderData is stable across resolve/reject
2050 // Remove this instance if we were aborted or if promises have settled
2051 if (aborted || deferredData.done) {
2052 activeDeferreds.delete(routeId);
2053 }
2054 });
2055 });
2056
2057 // During partial hydration, preserve SSR errors for routes that don't re-run
2058 if (future.v7_partialHydration && initialHydration && state.errors) {
2059 Object.entries(state.errors)
2060 .filter(([id]) => !matchesToLoad.some((m) => m.route.id === id))
2061 .forEach(([routeId, error]) => {
2062 errors = Object.assign(errors || {}, { [routeId]: error });
2063 });
2064 }
2065
2066 let updatedFetchers = markFetchRedirectsDone();
2067 let didAbortFetchLoads = abortStaleFetchLoads(pendingNavigationLoadId);
2068 let shouldUpdateFetchers =
2069 updatedFetchers || didAbortFetchLoads || revalidatingFetchers.length > 0;
2070
2071 return {
2072 matches,
2073 loaderData,
2074 errors,
2075 ...(shouldUpdateFetchers ? { fetchers: new Map(state.fetchers) } : {}),
2076 };
2077 }
2078
2079 function getUpdatedActionData(
2080 pendingActionResult: PendingActionResult | undefined
2081 ): Record<string, RouteData> | null | undefined {
2082 if (pendingActionResult && !isErrorResult(pendingActionResult[1])) {
2083 // This is cast to `any` currently because `RouteData`uses any and it
2084 // would be a breaking change to use any.
2085 // TODO: v7 - change `RouteData` to use `unknown` instead of `any`
2086 return {
2087 [pendingActionResult[0]]: pendingActionResult[1].data as any,
2088 };
2089 } else if (state.actionData) {
2090 if (Object.keys(state.actionData).length === 0) {
2091 return null;
2092 } else {
2093 return state.actionData;
2094 }
2095 }
2096 }
2097
2098 function getUpdatedRevalidatingFetchers(
2099 revalidatingFetchers: RevalidatingFetcher[]
2100 ) {
2101 revalidatingFetchers.forEach((rf) => {
2102 let fetcher = state.fetchers.get(rf.key);
2103 let revalidatingFetcher = getLoadingFetcher(
2104 undefined,
2105 fetcher ? fetcher.data : undefined
2106 );
2107 state.fetchers.set(rf.key, revalidatingFetcher);
2108 });
2109 return new Map(state.fetchers);
2110 }
2111
2112 // Trigger a fetcher load/submit for the given fetcher key
2113 function fetch(
2114 key: string,
2115 routeId: string,
2116 href: string | null,
2117 opts?: RouterFetchOptions
2118 ) {
2119 if (isServer) {
2120 throw new Error(
2121 "router.fetch() was called during the server render, but it shouldn't be. " +
2122 "You are likely calling a useFetcher() method in the body of your component. " +
2123 "Try moving it to a useEffect or a callback."
2124 );
2125 }
2126
2127 if (fetchControllers.has(key)) abortFetcher(key);
2128 let flushSync = (opts && opts.unstable_flushSync) === true;
2129
2130 let routesToUse = inFlightDataRoutes || dataRoutes;
2131 let normalizedPath = normalizeTo(
2132 state.location,
2133 state.matches,
2134 basename,
2135 future.v7_prependBasename,
2136 href,
2137 future.v7_relativeSplatPath,
2138 routeId,
2139 opts?.relative
2140 );
2141 let matches = matchRoutes(routesToUse, normalizedPath, basename);
2142
2143 let fogOfWar = checkFogOfWar(matches, routesToUse, normalizedPath);
2144 if (fogOfWar.active && fogOfWar.matches) {
2145 matches = fogOfWar.matches;
2146 }
2147
2148 if (!matches) {
2149 setFetcherError(
2150 key,
2151 routeId,
2152 getInternalRouterError(404, { pathname: normalizedPath }),
2153 { flushSync }
2154 );
2155 return;
2156 }
2157
2158 let { path, submission, error } = normalizeNavigateOptions(
2159 future.v7_normalizeFormMethod,
2160 true,
2161 normalizedPath,
2162 opts
2163 );
2164
2165 if (error) {
2166 setFetcherError(key, routeId, error, { flushSync });
2167 return;
2168 }
2169
2170 let match = getTargetMatch(matches, path);
2171
2172 pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
2173
2174 if (submission && isMutationMethod(submission.formMethod)) {
2175 handleFetcherAction(
2176 key,
2177 routeId,
2178 path,
2179 match,
2180 matches,
2181 fogOfWar.active,
2182 flushSync,
2183 submission
2184 );
2185 return;
2186 }
2187
2188 // Store off the match so we can call it's shouldRevalidate on subsequent
2189 // revalidations
2190 fetchLoadMatches.set(key, { routeId, path });
2191 handleFetcherLoader(
2192 key,
2193 routeId,
2194 path,
2195 match,
2196 matches,
2197 fogOfWar.active,
2198 flushSync,
2199 submission
2200 );
2201 }
2202
2203 // Call the action for the matched fetcher.submit(), and then handle redirects,
2204 // errors, and revalidation
2205 async function handleFetcherAction(
2206 key: string,
2207 routeId: string,
2208 path: string,
2209 match: AgnosticDataRouteMatch,
2210 requestMatches: AgnosticDataRouteMatch[],
2211 isFogOfWar: boolean,
2212 flushSync: boolean,
2213 submission: Submission
2214 ) {
2215 interruptActiveLoads();
2216 fetchLoadMatches.delete(key);
2217
2218 function detectAndHandle405Error(m: AgnosticDataRouteMatch) {
2219 if (!m.route.action && !m.route.lazy) {
2220 let error = getInternalRouterError(405, {
2221 method: submission.formMethod,
2222 pathname: path,
2223 routeId: routeId,
2224 });
2225 setFetcherError(key, routeId, error, { flushSync });
2226 return true;
2227 }
2228 return false;
2229 }
2230
2231 if (!isFogOfWar && detectAndHandle405Error(match)) {
2232 return;
2233 }
2234
2235 // Put this fetcher into it's submitting state
2236 let existingFetcher = state.fetchers.get(key);
2237 updateFetcherState(key, getSubmittingFetcher(submission, existingFetcher), {
2238 flushSync,
2239 });
2240
2241 let abortController = new AbortController();
2242 let fetchRequest = createClientSideRequest(
2243 init.history,
2244 path,
2245 abortController.signal,
2246 submission
2247 );
2248
2249 if (isFogOfWar) {
2250 let discoverResult = await discoverRoutes(
2251 requestMatches,
2252 path,
2253 fetchRequest.signal
2254 );
2255
2256 if (discoverResult.type === "aborted") {
2257 return;
2258 } else if (discoverResult.type === "error") {
2259 let { error } = handleDiscoverRouteError(path, discoverResult);
2260 setFetcherError(key, routeId, error, { flushSync });
2261 return;
2262 } else if (!discoverResult.matches) {
2263 setFetcherError(
2264 key,
2265 routeId,
2266 getInternalRouterError(404, { pathname: path }),
2267 { flushSync }
2268 );
2269 return;
2270 } else {
2271 requestMatches = discoverResult.matches;
2272 match = getTargetMatch(requestMatches, path);
2273
2274 if (detectAndHandle405Error(match)) {
2275 return;
2276 }
2277 }
2278 }
2279
2280 // Call the action for the fetcher
2281 fetchControllers.set(key, abortController);
2282
2283 let originatingLoadId = incrementingLoadId;
2284 let actionResults = await callDataStrategy(
2285 "action",
2286 fetchRequest,
2287 [match],
2288 requestMatches
2289 );
2290 let actionResult = actionResults[0];
2291
2292 if (fetchRequest.signal.aborted) {
2293 // We can delete this so long as we weren't aborted by our own fetcher
2294 // re-submit which would have put _new_ controller is in fetchControllers
2295 if (fetchControllers.get(key) === abortController) {
2296 fetchControllers.delete(key);
2297 }
2298 return;
2299 }
2300
2301 // When using v7_fetcherPersist, we don't want errors bubbling up to the UI
2302 // or redirects processed for unmounted fetchers so we just revert them to
2303 // idle
2304 if (future.v7_fetcherPersist && deletedFetchers.has(key)) {
2305 if (isRedirectResult(actionResult) || isErrorResult(actionResult)) {
2306 updateFetcherState(key, getDoneFetcher(undefined));
2307 return;
2308 }
2309 // Let SuccessResult's fall through for revalidation
2310 } else {
2311 if (isRedirectResult(actionResult)) {
2312 fetchControllers.delete(key);
2313 if (pendingNavigationLoadId > originatingLoadId) {
2314 // A new navigation was kicked off after our action started, so that
2315 // should take precedence over this redirect navigation. We already
2316 // set isRevalidationRequired so all loaders for the new route should
2317 // fire unless opted out via shouldRevalidate
2318 updateFetcherState(key, getDoneFetcher(undefined));
2319 return;
2320 } else {
2321 fetchRedirectIds.add(key);
2322 updateFetcherState(key, getLoadingFetcher(submission));
2323 return startRedirectNavigation(fetchRequest, actionResult, {
2324 fetcherSubmission: submission,
2325 });
2326 }
2327 }
2328
2329 // Process any non-redirect errors thrown
2330 if (isErrorResult(actionResult)) {
2331 setFetcherError(key, routeId, actionResult.error);
2332 return;
2333 }
2334 }
2335
2336 if (isDeferredResult(actionResult)) {
2337 throw getInternalRouterError(400, { type: "defer-action" });
2338 }
2339
2340 // Start the data load for current matches, or the next location if we're
2341 // in the middle of a navigation
2342 let nextLocation = state.navigation.location || state.location;
2343 let revalidationRequest = createClientSideRequest(
2344 init.history,
2345 nextLocation,
2346 abortController.signal
2347 );
2348 let routesToUse = inFlightDataRoutes || dataRoutes;
2349 let matches =
2350 state.navigation.state !== "idle"
2351 ? matchRoutes(routesToUse, state.navigation.location, basename)
2352 : state.matches;
2353
2354 invariant(matches, "Didn't find any matches after fetcher action");
2355
2356 let loadId = ++incrementingLoadId;
2357 fetchReloadIds.set(key, loadId);
2358
2359 let loadFetcher = getLoadingFetcher(submission, actionResult.data);
2360 state.fetchers.set(key, loadFetcher);
2361
2362 let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(
2363 init.history,
2364 state,
2365 matches,
2366 submission,
2367 nextLocation,
2368 false,
2369 future.v7_skipActionErrorRevalidation,
2370 isRevalidationRequired,
2371 cancelledDeferredRoutes,
2372 cancelledFetcherLoads,
2373 deletedFetchers,
2374 fetchLoadMatches,
2375 fetchRedirectIds,
2376 routesToUse,
2377 basename,
2378 [match.route.id, actionResult]
2379 );
2380
2381 // Put all revalidating fetchers into the loading state, except for the
2382 // current fetcher which we want to keep in it's current loading state which
2383 // contains it's action submission info + action data
2384 revalidatingFetchers
2385 .filter((rf) => rf.key !== key)
2386 .forEach((rf) => {
2387 let staleKey = rf.key;
2388 let existingFetcher = state.fetchers.get(staleKey);
2389 let revalidatingFetcher = getLoadingFetcher(
2390 undefined,
2391 existingFetcher ? existingFetcher.data : undefined
2392 );
2393 state.fetchers.set(staleKey, revalidatingFetcher);
2394 if (fetchControllers.has(staleKey)) {
2395 abortFetcher(staleKey);
2396 }
2397 if (rf.controller) {
2398 fetchControllers.set(staleKey, rf.controller);
2399 }
2400 });
2401
2402 updateState({ fetchers: new Map(state.fetchers) });
2403
2404 let abortPendingFetchRevalidations = () =>
2405 revalidatingFetchers.forEach((rf) => abortFetcher(rf.key));
2406
2407 abortController.signal.addEventListener(
2408 "abort",
2409 abortPendingFetchRevalidations
2410 );
2411
2412 let { loaderResults, fetcherResults } =
2413 await callLoadersAndMaybeResolveData(
2414 state.matches,
2415 matches,
2416 matchesToLoad,
2417 revalidatingFetchers,
2418 revalidationRequest
2419 );
2420
2421 if (abortController.signal.aborted) {
2422 return;
2423 }
2424
2425 abortController.signal.removeEventListener(
2426 "abort",
2427 abortPendingFetchRevalidations
2428 );
2429
2430 fetchReloadIds.delete(key);
2431 fetchControllers.delete(key);
2432 revalidatingFetchers.forEach((r) => fetchControllers.delete(r.key));
2433
2434 let redirect = findRedirect([...loaderResults, ...fetcherResults]);
2435 if (redirect) {
2436 if (redirect.idx >= matchesToLoad.length) {
2437 // If this redirect came from a fetcher make sure we mark it in
2438 // fetchRedirectIds so it doesn't get revalidated on the next set of
2439 // loader executions
2440 let fetcherKey =
2441 revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
2442 fetchRedirectIds.add(fetcherKey);
2443 }
2444 return startRedirectNavigation(revalidationRequest, redirect.result);
2445 }
2446
2447 // Process and commit output from loaders
2448 let { loaderData, errors } = processLoaderData(
2449 state,
2450 state.matches,
2451 matchesToLoad,
2452 loaderResults,
2453 undefined,
2454 revalidatingFetchers,
2455 fetcherResults,
2456 activeDeferreds
2457 );
2458
2459 // Since we let revalidations complete even if the submitting fetcher was
2460 // deleted, only put it back to idle if it hasn't been deleted
2461 if (state.fetchers.has(key)) {
2462 let doneFetcher = getDoneFetcher(actionResult.data);
2463 state.fetchers.set(key, doneFetcher);
2464 }
2465
2466 abortStaleFetchLoads(loadId);
2467
2468 // If we are currently in a navigation loading state and this fetcher is
2469 // more recent than the navigation, we want the newer data so abort the
2470 // navigation and complete it with the fetcher data
2471 if (
2472 state.navigation.state === "loading" &&
2473 loadId > pendingNavigationLoadId
2474 ) {
2475 invariant(pendingAction, "Expected pending action");
2476 pendingNavigationController && pendingNavigationController.abort();
2477
2478 completeNavigation(state.navigation.location, {
2479 matches,
2480 loaderData,
2481 errors,
2482 fetchers: new Map(state.fetchers),
2483 });
2484 } else {
2485 // otherwise just update with the fetcher data, preserving any existing
2486 // loaderData for loaders that did not need to reload. We have to
2487 // manually merge here since we aren't going through completeNavigation
2488 updateState({
2489 errors,
2490 loaderData: mergeLoaderData(
2491 state.loaderData,
2492 loaderData,
2493 matches,
2494 errors
2495 ),
2496 fetchers: new Map(state.fetchers),
2497 });
2498 isRevalidationRequired = false;
2499 }
2500 }
2501
2502 // Call the matched loader for fetcher.load(), handling redirects, errors, etc.
2503 async function handleFetcherLoader(
2504 key: string,
2505 routeId: string,
2506 path: string,
2507 match: AgnosticDataRouteMatch,
2508 matches: AgnosticDataRouteMatch[],
2509 isFogOfWar: boolean,
2510 flushSync: boolean,
2511 submission?: Submission
2512 ) {
2513 let existingFetcher = state.fetchers.get(key);
2514 updateFetcherState(
2515 key,
2516 getLoadingFetcher(
2517 submission,
2518 existingFetcher ? existingFetcher.data : undefined
2519 ),
2520 { flushSync }
2521 );
2522
2523 let abortController = new AbortController();
2524 let fetchRequest = createClientSideRequest(
2525 init.history,
2526 path,
2527 abortController.signal
2528 );
2529
2530 if (isFogOfWar) {
2531 let discoverResult = await discoverRoutes(
2532 matches,
2533 path,
2534 fetchRequest.signal
2535 );
2536
2537 if (discoverResult.type === "aborted") {
2538 return;
2539 } else if (discoverResult.type === "error") {
2540 let { error } = handleDiscoverRouteError(path, discoverResult);
2541 setFetcherError(key, routeId, error, { flushSync });
2542 return;
2543 } else if (!discoverResult.matches) {
2544 setFetcherError(
2545 key,
2546 routeId,
2547 getInternalRouterError(404, { pathname: path }),
2548 { flushSync }
2549 );
2550 return;
2551 } else {
2552 matches = discoverResult.matches;
2553 match = getTargetMatch(matches, path);
2554 }
2555 }
2556
2557 // Call the loader for this fetcher route match
2558 fetchControllers.set(key, abortController);
2559
2560 let originatingLoadId = incrementingLoadId;
2561 let results = await callDataStrategy(
2562 "loader",
2563 fetchRequest,
2564 [match],
2565 matches
2566 );
2567 let result = results[0];
2568
2569 // Deferred isn't supported for fetcher loads, await everything and treat it
2570 // as a normal load. resolveDeferredData will return undefined if this
2571 // fetcher gets aborted, so we just leave result untouched and short circuit
2572 // below if that happens
2573 if (isDeferredResult(result)) {
2574 result =
2575 (await resolveDeferredData(result, fetchRequest.signal, true)) ||
2576 result;
2577 }
2578
2579 // We can delete this so long as we weren't aborted by our our own fetcher
2580 // re-load which would have put _new_ controller is in fetchControllers
2581 if (fetchControllers.get(key) === abortController) {
2582 fetchControllers.delete(key);
2583 }
2584
2585 if (fetchRequest.signal.aborted) {
2586 return;
2587 }
2588
2589 // We don't want errors bubbling up or redirects followed for unmounted
2590 // fetchers, so short circuit here if it was removed from the UI
2591 if (deletedFetchers.has(key)) {
2592 updateFetcherState(key, getDoneFetcher(undefined));
2593 return;
2594 }
2595
2596 // If the loader threw a redirect Response, start a new REPLACE navigation
2597 if (isRedirectResult(result)) {
2598 if (pendingNavigationLoadId > originatingLoadId) {
2599 // A new navigation was kicked off after our loader started, so that
2600 // should take precedence over this redirect navigation
2601 updateFetcherState(key, getDoneFetcher(undefined));
2602 return;
2603 } else {
2604 fetchRedirectIds.add(key);
2605 await startRedirectNavigation(fetchRequest, result);
2606 return;
2607 }
2608 }
2609
2610 // Process any non-redirect errors thrown
2611 if (isErrorResult(result)) {
2612 setFetcherError(key, routeId, result.error);
2613 return;
2614 }
2615
2616 invariant(!isDeferredResult(result), "Unhandled fetcher deferred data");
2617
2618 // Put the fetcher back into an idle state
2619 updateFetcherState(key, getDoneFetcher(result.data));
2620 }
2621
2622 /**
2623 * Utility function to handle redirects returned from an action or loader.
2624 * Normally, a redirect "replaces" the navigation that triggered it. So, for
2625 * example:
2626 *
2627 * - user is on /a
2628 * - user clicks a link to /b
2629 * - loader for /b redirects to /c
2630 *
2631 * In a non-JS app the browser would track the in-flight navigation to /b and
2632 * then replace it with /c when it encountered the redirect response. In
2633 * the end it would only ever update the URL bar with /c.
2634 *
2635 * In client-side routing using pushState/replaceState, we aim to emulate
2636 * this behavior and we also do not update history until the end of the
2637 * navigation (including processed redirects). This means that we never
2638 * actually touch history until we've processed redirects, so we just use
2639 * the history action from the original navigation (PUSH or REPLACE).
2640 */
2641 async function startRedirectNavigation(
2642 request: Request,
2643 redirect: RedirectResult,
2644 {
2645 submission,
2646 fetcherSubmission,
2647 replace,
2648 }: {
2649 submission?: Submission;
2650 fetcherSubmission?: Submission;
2651 replace?: boolean;
2652 } = {}
2653 ) {
2654 if (redirect.response.headers.has("X-Remix-Revalidate")) {
2655 isRevalidationRequired = true;
2656 }
2657
2658 let location = redirect.response.headers.get("Location");
2659 invariant(location, "Expected a Location header on the redirect Response");
2660 location = normalizeRedirectLocation(
2661 location,
2662 new URL(request.url),
2663 basename
2664 );
2665 let redirectLocation = createLocation(state.location, location, {
2666 _isRedirect: true,
2667 });
2668
2669 if (isBrowser) {
2670 let isDocumentReload = false;
2671
2672 if (redirect.response.headers.has("X-Remix-Reload-Document")) {
2673 // Hard reload if the response contained X-Remix-Reload-Document
2674 isDocumentReload = true;
2675 } else if (ABSOLUTE_URL_REGEX.test(location)) {
2676 const url = init.history.createURL(location);
2677 isDocumentReload =
2678 // Hard reload if it's an absolute URL to a new origin
2679 url.origin !== routerWindow.location.origin ||
2680 // Hard reload if it's an absolute URL that does not match our basename
2681 stripBasename(url.pathname, basename) == null;
2682 }
2683
2684 if (isDocumentReload) {
2685 if (replace) {
2686 routerWindow.location.replace(location);
2687 } else {
2688 routerWindow.location.assign(location);
2689 }
2690 return;
2691 }
2692 }
2693
2694 // There's no need to abort on redirects, since we don't detect the
2695 // redirect until the action/loaders have settled
2696 pendingNavigationController = null;
2697
2698 let redirectHistoryAction =
2699 replace === true || redirect.response.headers.has("X-Remix-Replace")
2700 ? HistoryAction.Replace
2701 : HistoryAction.Push;
2702
2703 // Use the incoming submission if provided, fallback on the active one in
2704 // state.navigation
2705 let { formMethod, formAction, formEncType } = state.navigation;
2706 if (
2707 !submission &&
2708 !fetcherSubmission &&
2709 formMethod &&
2710 formAction &&
2711 formEncType
2712 ) {
2713 submission = getSubmissionFromNavigation(state.navigation);
2714 }
2715
2716 // If this was a 307/308 submission we want to preserve the HTTP method and
2717 // re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the
2718 // redirected location
2719 let activeSubmission = submission || fetcherSubmission;
2720 if (
2721 redirectPreserveMethodStatusCodes.has(redirect.response.status) &&
2722 activeSubmission &&
2723 isMutationMethod(activeSubmission.formMethod)
2724 ) {
2725 await startNavigation(redirectHistoryAction, redirectLocation, {
2726 submission: {
2727 ...activeSubmission,
2728 formAction: location,
2729 },
2730 // Preserve this flag across redirects
2731 preventScrollReset: pendingPreventScrollReset,
2732 });
2733 } else {
2734 // If we have a navigation submission, we will preserve it through the
2735 // redirect navigation
2736 let overrideNavigation = getLoadingNavigation(
2737 redirectLocation,
2738 submission
2739 );
2740 await startNavigation(redirectHistoryAction, redirectLocation, {
2741 overrideNavigation,
2742 // Send fetcher submissions through for shouldRevalidate
2743 fetcherSubmission,
2744 // Preserve this flag across redirects
2745 preventScrollReset: pendingPreventScrollReset,
2746 });
2747 }
2748 }
2749
2750 // Utility wrapper for calling dataStrategy client-side without having to
2751 // pass around the manifest, mapRouteProperties, etc.
2752 async function callDataStrategy(
2753 type: "loader" | "action",
2754 request: Request,
2755 matchesToLoad: AgnosticDataRouteMatch[],
2756 matches: AgnosticDataRouteMatch[]
2757 ): Promise<DataResult[]> {
2758 try {
2759 let results = await callDataStrategyImpl(
2760 dataStrategyImpl,
2761 type,
2762 request,
2763 matchesToLoad,
2764 matches,
2765 manifest,
2766 mapRouteProperties
2767 );
2768
2769 return await Promise.all(
2770 results.map((result, i) => {
2771 if (isRedirectHandlerResult(result)) {
2772 let response = result.result as Response;
2773 return {
2774 type: ResultType.redirect,
2775 response: normalizeRelativeRoutingRedirectResponse(
2776 response,
2777 request,
2778 matchesToLoad[i].route.id,
2779 matches,
2780 basename,
2781 future.v7_relativeSplatPath
2782 ),
2783 };
2784 }
2785
2786 return convertHandlerResultToDataResult(result);
2787 })
2788 );
2789 } catch (e) {
2790 // If the outer dataStrategy method throws, just return the error for all
2791 // matches - and it'll naturally bubble to the root
2792 return matchesToLoad.map(() => ({
2793 type: ResultType.error,
2794 error: e,
2795 }));
2796 }
2797 }
2798
2799 async function callLoadersAndMaybeResolveData(
2800 currentMatches: AgnosticDataRouteMatch[],
2801 matches: AgnosticDataRouteMatch[],
2802 matchesToLoad: AgnosticDataRouteMatch[],
2803 fetchersToLoad: RevalidatingFetcher[],
2804 request: Request
2805 ) {
2806 let [loaderResults, ...fetcherResults] = await Promise.all([
2807 matchesToLoad.length
2808 ? callDataStrategy("loader", request, matchesToLoad, matches)
2809 : [],
2810 ...fetchersToLoad.map((f) => {
2811 if (f.matches && f.match && f.controller) {
2812 let fetcherRequest = createClientSideRequest(
2813 init.history,
2814 f.path,
2815 f.controller.signal
2816 );
2817 return callDataStrategy(
2818 "loader",
2819 fetcherRequest,
2820 [f.match],
2821 f.matches
2822 ).then((r) => r[0]);
2823 } else {
2824 return Promise.resolve<DataResult>({
2825 type: ResultType.error,
2826 error: getInternalRouterError(404, {
2827 pathname: f.path,
2828 }),
2829 });
2830 }
2831 }),
2832 ]);
2833
2834 await Promise.all([
2835 resolveDeferredResults(
2836 currentMatches,
2837 matchesToLoad,
2838 loaderResults,
2839 loaderResults.map(() => request.signal),
2840 false,
2841 state.loaderData
2842 ),
2843 resolveDeferredResults(
2844 currentMatches,
2845 fetchersToLoad.map((f) => f.match),
2846 fetcherResults,
2847 fetchersToLoad.map((f) => (f.controller ? f.controller.signal : null)),
2848 true
2849 ),
2850 ]);
2851
2852 return {
2853 loaderResults,
2854 fetcherResults,
2855 };
2856 }
2857
2858 function interruptActiveLoads() {
2859 // Every interruption triggers a revalidation
2860 isRevalidationRequired = true;
2861
2862 // Cancel pending route-level deferreds and mark cancelled routes for
2863 // revalidation
2864 cancelledDeferredRoutes.push(...cancelActiveDeferreds());
2865
2866 // Abort in-flight fetcher loads
2867 fetchLoadMatches.forEach((_, key) => {
2868 if (fetchControllers.has(key)) {
2869 cancelledFetcherLoads.add(key);
2870 abortFetcher(key);
2871 }
2872 });
2873 }
2874
2875 function updateFetcherState(
2876 key: string,
2877 fetcher: Fetcher,
2878 opts: { flushSync?: boolean } = {}
2879 ) {
2880 state.fetchers.set(key, fetcher);
2881 updateState(
2882 { fetchers: new Map(state.fetchers) },
2883 { flushSync: (opts && opts.flushSync) === true }
2884 );
2885 }
2886
2887 function setFetcherError(
2888 key: string,
2889 routeId: string,
2890 error: any,
2891 opts: { flushSync?: boolean } = {}
2892 ) {
2893 let boundaryMatch = findNearestBoundary(state.matches, routeId);
2894 deleteFetcher(key);
2895 updateState(
2896 {
2897 errors: {
2898 [boundaryMatch.route.id]: error,
2899 },
2900 fetchers: new Map(state.fetchers),
2901 },
2902 { flushSync: (opts && opts.flushSync) === true }
2903 );
2904 }
2905
2906 function getFetcher<TData = any>(key: string): Fetcher<TData> {
2907 if (future.v7_fetcherPersist) {
2908 activeFetchers.set(key, (activeFetchers.get(key) || 0) + 1);
2909 // If this fetcher was previously marked for deletion, unmark it since we
2910 // have a new instance
2911 if (deletedFetchers.has(key)) {
2912 deletedFetchers.delete(key);
2913 }
2914 }
2915 return state.fetchers.get(key) || IDLE_FETCHER;
2916 }
2917
2918 function deleteFetcher(key: string): void {
2919 let fetcher = state.fetchers.get(key);
2920 // Don't abort the controller if this is a deletion of a fetcher.submit()
2921 // in it's loading phase since - we don't want to abort the corresponding
2922 // revalidation and want them to complete and land
2923 if (
2924 fetchControllers.has(key) &&
2925 !(fetcher && fetcher.state === "loading" && fetchReloadIds.has(key))
2926 ) {
2927 abortFetcher(key);
2928 }
2929 fetchLoadMatches.delete(key);
2930 fetchReloadIds.delete(key);
2931 fetchRedirectIds.delete(key);
2932 deletedFetchers.delete(key);
2933 cancelledFetcherLoads.delete(key);
2934 state.fetchers.delete(key);
2935 }
2936
2937 function deleteFetcherAndUpdateState(key: string): void {
2938 if (future.v7_fetcherPersist) {
2939 let count = (activeFetchers.get(key) || 0) - 1;
2940 if (count <= 0) {
2941 activeFetchers.delete(key);
2942 deletedFetchers.add(key);
2943 } else {
2944 activeFetchers.set(key, count);
2945 }
2946 } else {
2947 deleteFetcher(key);
2948 }
2949 updateState({ fetchers: new Map(state.fetchers) });
2950 }
2951
2952 function abortFetcher(key: string) {
2953 let controller = fetchControllers.get(key);
2954 invariant(controller, `Expected fetch controller: ${key}`);
2955 controller.abort();
2956 fetchControllers.delete(key);
2957 }
2958
2959 function markFetchersDone(keys: string[]) {
2960 for (let key of keys) {
2961 let fetcher = getFetcher(key);
2962 let doneFetcher = getDoneFetcher(fetcher.data);
2963 state.fetchers.set(key, doneFetcher);
2964 }
2965 }
2966
2967 function markFetchRedirectsDone(): boolean {
2968 let doneKeys = [];
2969 let updatedFetchers = false;
2970 for (let key of fetchRedirectIds) {
2971 let fetcher = state.fetchers.get(key);
2972 invariant(fetcher, `Expected fetcher: ${key}`);
2973 if (fetcher.state === "loading") {
2974 fetchRedirectIds.delete(key);
2975 doneKeys.push(key);
2976 updatedFetchers = true;
2977 }
2978 }
2979 markFetchersDone(doneKeys);
2980 return updatedFetchers;
2981 }
2982
2983 function abortStaleFetchLoads(landedId: number): boolean {
2984 let yeetedKeys = [];
2985 for (let [key, id] of fetchReloadIds) {
2986 if (id < landedId) {
2987 let fetcher = state.fetchers.get(key);
2988 invariant(fetcher, `Expected fetcher: ${key}`);
2989 if (fetcher.state === "loading") {
2990 abortFetcher(key);
2991 fetchReloadIds.delete(key);
2992 yeetedKeys.push(key);
2993 }
2994 }
2995 }
2996 markFetchersDone(yeetedKeys);
2997 return yeetedKeys.length > 0;
2998 }
2999
3000 function getBlocker(key: string, fn: BlockerFunction) {
3001 let blocker: Blocker = state.blockers.get(key) || IDLE_BLOCKER;
3002
3003 if (blockerFunctions.get(key) !== fn) {
3004 blockerFunctions.set(key, fn);
3005 }
3006
3007 return blocker;
3008 }
3009
3010 function deleteBlocker(key: string) {
3011 state.blockers.delete(key);
3012 blockerFunctions.delete(key);
3013 }
3014
3015 // Utility function to update blockers, ensuring valid state transitions
3016 function updateBlocker(key: string, newBlocker: Blocker) {
3017 let blocker = state.blockers.get(key) || IDLE_BLOCKER;
3018
3019 // Poor mans state machine :)
3020 // https://mermaid.live/edit#pako:eNqVkc9OwzAMxl8l8nnjAYrEtDIOHEBIgwvKJTReGy3_lDpIqO27k6awMG0XcrLlnz87nwdonESogKXXBuE79rq75XZO3-yHds0RJVuv70YrPlUrCEe2HfrORS3rubqZfuhtpg5C9wk5tZ4VKcRUq88q9Z8RS0-48cE1iHJkL0ugbHuFLus9L6spZy8nX9MP2CNdomVaposqu3fGayT8T8-jJQwhepo_UtpgBQaDEUom04dZhAN1aJBDlUKJBxE1ceB2Smj0Mln-IBW5AFU2dwUiktt_2Qaq2dBfaKdEup85UV7Yd-dKjlnkabl2Pvr0DTkTreM
3021 invariant(
3022 (blocker.state === "unblocked" && newBlocker.state === "blocked") ||
3023 (blocker.state === "blocked" && newBlocker.state === "blocked") ||
3024 (blocker.state === "blocked" && newBlocker.state === "proceeding") ||
3025 (blocker.state === "blocked" && newBlocker.state === "unblocked") ||
3026 (blocker.state === "proceeding" && newBlocker.state === "unblocked"),
3027 `Invalid blocker state transition: ${blocker.state} -> ${newBlocker.state}`
3028 );
3029
3030 let blockers = new Map(state.blockers);
3031 blockers.set(key, newBlocker);
3032 updateState({ blockers });
3033 }
3034
3035 function shouldBlockNavigation({
3036 currentLocation,
3037 nextLocation,
3038 historyAction,
3039 }: {
3040 currentLocation: Location;
3041 nextLocation: Location;
3042 historyAction: HistoryAction;
3043 }): string | undefined {
3044 if (blockerFunctions.size === 0) {
3045 return;
3046 }
3047
3048 // We ony support a single active blocker at the moment since we don't have
3049 // any compelling use cases for multi-blocker yet
3050 if (blockerFunctions.size > 1) {
3051 warning(false, "A router only supports one blocker at a time");
3052 }
3053
3054 let entries = Array.from(blockerFunctions.entries());
3055 let [blockerKey, blockerFunction] = entries[entries.length - 1];
3056 let blocker = state.blockers.get(blockerKey);
3057
3058 if (blocker && blocker.state === "proceeding") {
3059 // If the blocker is currently proceeding, we don't need to re-check
3060 // it and can let this navigation continue
3061 return;
3062 }
3063
3064 // At this point, we know we're unblocked/blocked so we need to check the
3065 // user-provided blocker function
3066 if (blockerFunction({ currentLocation, nextLocation, historyAction })) {
3067 return blockerKey;
3068 }
3069 }
3070
3071 function handleNavigational404(pathname: string) {
3072 let error = getInternalRouterError(404, { pathname });
3073 let routesToUse = inFlightDataRoutes || dataRoutes;
3074 let { matches, route } = getShortCircuitMatches(routesToUse);
3075
3076 // Cancel all pending deferred on 404s since we don't keep any routes
3077 cancelActiveDeferreds();
3078
3079 return { notFoundMatches: matches, route, error };
3080 }
3081
3082 function handleDiscoverRouteError(
3083 pathname: string,
3084 discoverResult: DiscoverRoutesErrorResult
3085 ) {
3086 return {
3087 boundaryId: findNearestBoundary(discoverResult.partialMatches).route.id,
3088 error: getInternalRouterError(400, {
3089 type: "route-discovery",
3090 pathname,
3091 message:
3092 discoverResult.error != null && "message" in discoverResult.error
3093 ? discoverResult.error
3094 : String(discoverResult.error),
3095 }),
3096 };
3097 }
3098
3099 function cancelActiveDeferreds(
3100 predicate?: (routeId: string) => boolean
3101 ): string[] {
3102 let cancelledRouteIds: string[] = [];
3103 activeDeferreds.forEach((dfd, routeId) => {
3104 if (!predicate || predicate(routeId)) {
3105 // Cancel the deferred - but do not remove from activeDeferreds here -
3106 // we rely on the subscribers to do that so our tests can assert proper
3107 // cleanup via _internalActiveDeferreds
3108 dfd.cancel();
3109 cancelledRouteIds.push(routeId);
3110 activeDeferreds.delete(routeId);
3111 }
3112 });
3113 return cancelledRouteIds;
3114 }
3115
3116 // Opt in to capturing and reporting scroll positions during navigations,
3117 // used by the <ScrollRestoration> component
3118 function enableScrollRestoration(
3119 positions: Record<string, number>,
3120 getPosition: GetScrollPositionFunction,
3121 getKey?: GetScrollRestorationKeyFunction
3122 ) {
3123 savedScrollPositions = positions;
3124 getScrollPosition = getPosition;
3125 getScrollRestorationKey = getKey || null;
3126
3127 // Perform initial hydration scroll restoration, since we miss the boat on
3128 // the initial updateState() because we've not yet rendered <ScrollRestoration/>
3129 // and therefore have no savedScrollPositions available
3130 if (!initialScrollRestored && state.navigation === IDLE_NAVIGATION) {
3131 initialScrollRestored = true;
3132 let y = getSavedScrollPosition(state.location, state.matches);
3133 if (y != null) {
3134 updateState({ restoreScrollPosition: y });
3135 }
3136 }
3137
3138 return () => {
3139 savedScrollPositions = null;
3140 getScrollPosition = null;
3141 getScrollRestorationKey = null;
3142 };
3143 }
3144
3145 function getScrollKey(location: Location, matches: AgnosticDataRouteMatch[]) {
3146 if (getScrollRestorationKey) {
3147 let key = getScrollRestorationKey(
3148 location,
3149 matches.map((m) => convertRouteMatchToUiMatch(m, state.loaderData))
3150 );
3151 return key || location.key;
3152 }
3153 return location.key;
3154 }
3155
3156 function saveScrollPosition(
3157 location: Location,
3158 matches: AgnosticDataRouteMatch[]
3159 ): void {
3160 if (savedScrollPositions && getScrollPosition) {
3161 let key = getScrollKey(location, matches);
3162 savedScrollPositions[key] = getScrollPosition();
3163 }
3164 }
3165
3166 function getSavedScrollPosition(
3167 location: Location,
3168 matches: AgnosticDataRouteMatch[]
3169 ): number | null {
3170 if (savedScrollPositions) {
3171 let key = getScrollKey(location, matches);
3172 let y = savedScrollPositions[key];
3173 if (typeof y === "number") {
3174 return y;
3175 }
3176 }
3177 return null;
3178 }
3179
3180 function checkFogOfWar(
3181 matches: AgnosticDataRouteMatch[] | null,
3182 routesToUse: AgnosticDataRouteObject[],
3183 pathname: string
3184 ): { active: boolean; matches: AgnosticDataRouteMatch[] | null } {
3185 if (patchRoutesOnMissImpl) {
3186 if (!matches) {
3187 let fogMatches = matchRoutesImpl<AgnosticDataRouteObject>(
3188 routesToUse,
3189 pathname,
3190 basename,
3191 true
3192 );
3193
3194 return { active: true, matches: fogMatches || [] };
3195 } else {
3196 let leafRoute = matches[matches.length - 1].route;
3197 if (
3198 leafRoute.path &&
3199 (leafRoute.path === "*" || leafRoute.path.endsWith("/*"))
3200 ) {
3201 // If we matched a splat, it might only be because we haven't yet fetched
3202 // the children that would match with a higher score, so let's fetch
3203 // around and find out
3204 let partialMatches = matchRoutesImpl<AgnosticDataRouteObject>(
3205 routesToUse,
3206 pathname,
3207 basename,
3208 true
3209 );
3210 return { active: true, matches: partialMatches };
3211 }
3212 }
3213 }
3214
3215 return { active: false, matches: null };
3216 }
3217
3218 type DiscoverRoutesSuccessResult = {
3219 type: "success";
3220 matches: AgnosticDataRouteMatch[] | null;
3221 };
3222 type DiscoverRoutesErrorResult = {
3223 type: "error";
3224 error: any;
3225 partialMatches: AgnosticDataRouteMatch[];
3226 };
3227 type DiscoverRoutesAbortedResult = { type: "aborted" };
3228 type DiscoverRoutesResult =
3229 | DiscoverRoutesSuccessResult
3230 | DiscoverRoutesErrorResult
3231 | DiscoverRoutesAbortedResult;
3232
3233 async function discoverRoutes(
3234 matches: AgnosticDataRouteMatch[],
3235 pathname: string,
3236 signal: AbortSignal
3237 ): Promise<DiscoverRoutesResult> {
3238 let partialMatches: AgnosticDataRouteMatch[] | null = matches;
3239 let route =
3240 partialMatches.length > 0
3241 ? partialMatches[partialMatches.length - 1].route
3242 : null;
3243 while (true) {
3244 let isNonHMR = inFlightDataRoutes == null;
3245 let routesToUse = inFlightDataRoutes || dataRoutes;
3246 try {
3247 await loadLazyRouteChildren(
3248 patchRoutesOnMissImpl!,
3249 pathname,
3250 partialMatches,
3251 routesToUse,
3252 manifest,
3253 mapRouteProperties,
3254 pendingPatchRoutes,
3255 signal
3256 );
3257 } catch (e) {
3258 return { type: "error", error: e, partialMatches };
3259 } finally {
3260 // If we are not in the middle of an HMR revalidation and we changed the
3261 // routes, provide a new identity so when we `updateState` at the end of
3262 // this navigation/fetch `router.routes` will be a new identity and
3263 // trigger a re-run of memoized `router.routes` dependencies.
3264 // HMR will already update the identity and reflow when it lands
3265 // `inFlightDataRoutes` in `completeNavigation`
3266 if (isNonHMR) {
3267 dataRoutes = [...dataRoutes];
3268 }
3269 }
3270
3271 if (signal.aborted) {
3272 return { type: "aborted" };
3273 }
3274
3275 let newMatches = matchRoutes(routesToUse, pathname, basename);
3276 let matchedSplat = false;
3277 if (newMatches) {
3278 let leafRoute = newMatches[newMatches.length - 1].route;
3279
3280 if (leafRoute.index) {
3281 // If we found an index route, we can stop
3282 return { type: "success", matches: newMatches };
3283 }
3284
3285 if (leafRoute.path && leafRoute.path.length > 0) {
3286 if (leafRoute.path === "*") {
3287 // If we found a splat route, we can't be sure there's not a
3288 // higher-scoring route down some partial matches trail so we need
3289 // to check that out
3290 matchedSplat = true;
3291 } else {
3292 // If we found a non-splat route, we can stop
3293 return { type: "success", matches: newMatches };
3294 }
3295 }
3296 }
3297
3298 let newPartialMatches = matchRoutesImpl<AgnosticDataRouteObject>(
3299 routesToUse,
3300 pathname,
3301 basename,
3302 true
3303 );
3304
3305 // If we are no longer partially matching anything, this was either a
3306 // legit splat match above, or it's a 404. Also avoid loops if the
3307 // second pass results in the same partial matches
3308 if (
3309 !newPartialMatches ||
3310 partialMatches.map((m) => m.route.id).join("-") ===
3311 newPartialMatches.map((m) => m.route.id).join("-")
3312 ) {
3313 return { type: "success", matches: matchedSplat ? newMatches : null };
3314 }
3315
3316 partialMatches = newPartialMatches;
3317 route = partialMatches[partialMatches.length - 1].route;
3318 if (route.path === "*") {
3319 // The splat is still our most accurate partial, so run with it
3320 return { type: "success", matches: partialMatches };
3321 }
3322 }
3323 }
3324
3325 function _internalSetRoutes(newRoutes: AgnosticDataRouteObject[]) {
3326 manifest = {};
3327 inFlightDataRoutes = convertRoutesToDataRoutes(
3328 newRoutes,
3329 mapRouteProperties,
3330 undefined,
3331 manifest
3332 );
3333 }
3334
3335 function patchRoutes(
3336 routeId: string | null,
3337 children: AgnosticRouteObject[]
3338 ): void {
3339 let isNonHMR = inFlightDataRoutes == null;
3340 let routesToUse = inFlightDataRoutes || dataRoutes;
3341 patchRoutesImpl(
3342 routeId,
3343 children,
3344 routesToUse,
3345 manifest,
3346 mapRouteProperties
3347 );
3348
3349 // If we are not in the middle of an HMR revalidation and we changed the
3350 // routes, provide a new identity and trigger a reflow via `updateState`
3351 // to re-run memoized `router.routes` dependencies.
3352 // HMR will already update the identity and reflow when it lands
3353 // `inFlightDataRoutes` in `completeNavigation`
3354 if (isNonHMR) {
3355 dataRoutes = [...dataRoutes];
3356 updateState({});
3357 }
3358 }
3359
3360 router = {
3361 get basename() {
3362 return basename;
3363 },
3364 get future() {
3365 return future;
3366 },
3367 get state() {
3368 return state;
3369 },
3370 get routes() {
3371 return dataRoutes;
3372 },
3373 get window() {
3374 return routerWindow;
3375 },
3376 initialize,
3377 subscribe,
3378 enableScrollRestoration,
3379 navigate,
3380 fetch,
3381 revalidate,
3382 // Passthrough to history-aware createHref used by useHref so we get proper
3383 // hash-aware URLs in DOM paths
3384 createHref: (to: To) => init.history.createHref(to),
3385 encodeLocation: (to: To) => init.history.encodeLocation(to),
3386 getFetcher,
3387 deleteFetcher: deleteFetcherAndUpdateState,
3388 dispose,
3389 getBlocker,
3390 deleteBlocker,
3391 patchRoutes,
3392 _internalFetchControllers: fetchControllers,
3393 _internalActiveDeferreds: activeDeferreds,
3394 // TODO: Remove setRoutes, it's temporary to avoid dealing with
3395 // updating the tree while validating the update algorithm.
3396 _internalSetRoutes,
3397 };
3398
3399 return router;
3400}
3401//#endregion
3402
3403////////////////////////////////////////////////////////////////////////////////
3404//#region createStaticHandler
3405////////////////////////////////////////////////////////////////////////////////
3406
3407export const UNSAFE_DEFERRED_SYMBOL = Symbol("deferred");
3408
3409/**
3410 * Future flags to toggle new feature behavior
3411 */
3412export interface StaticHandlerFutureConfig {
3413 v7_relativeSplatPath: boolean;
3414 v7_throwAbortReason: boolean;
3415}
3416
3417export interface CreateStaticHandlerOptions {
3418 basename?: string;
3419 /**
3420 * @deprecated Use `mapRouteProperties` instead
3421 */
3422 detectErrorBoundary?: DetectErrorBoundaryFunction;
3423 mapRouteProperties?: MapRoutePropertiesFunction;
3424 future?: Partial<StaticHandlerFutureConfig>;
3425}
3426
3427export function createStaticHandler(
3428 routes: AgnosticRouteObject[],
3429 opts?: CreateStaticHandlerOptions
3430): StaticHandler {
3431 invariant(
3432 routes.length > 0,
3433 "You must provide a non-empty routes array to createStaticHandler"
3434 );
3435
3436 let manifest: RouteManifest = {};
3437 let basename = (opts ? opts.basename : null) || "/";
3438 let mapRouteProperties: MapRoutePropertiesFunction;
3439 if (opts?.mapRouteProperties) {
3440 mapRouteProperties = opts.mapRouteProperties;
3441 } else if (opts?.detectErrorBoundary) {
3442 // If they are still using the deprecated version, wrap it with the new API
3443 let detectErrorBoundary = opts.detectErrorBoundary;
3444 mapRouteProperties = (route) => ({
3445 hasErrorBoundary: detectErrorBoundary(route),
3446 });
3447 } else {
3448 mapRouteProperties = defaultMapRouteProperties;
3449 }
3450 // Config driven behavior flags
3451 let future: StaticHandlerFutureConfig = {
3452 v7_relativeSplatPath: false,
3453 v7_throwAbortReason: false,
3454 ...(opts ? opts.future : null),
3455 };
3456
3457 let dataRoutes = convertRoutesToDataRoutes(
3458 routes,
3459 mapRouteProperties,
3460 undefined,
3461 manifest
3462 );
3463
3464 /**
3465 * The query() method is intended for document requests, in which we want to
3466 * call an optional action and potentially multiple loaders for all nested
3467 * routes. It returns a StaticHandlerContext object, which is very similar
3468 * to the router state (location, loaderData, actionData, errors, etc.) and
3469 * also adds SSR-specific information such as the statusCode and headers
3470 * from action/loaders Responses.
3471 *
3472 * It _should_ never throw and should report all errors through the
3473 * returned context.errors object, properly associating errors to their error
3474 * boundary. Additionally, it tracks _deepestRenderedBoundaryId which can be
3475 * used to emulate React error boundaries during SSr by performing a second
3476 * pass only down to the boundaryId.
3477 *
3478 * The one exception where we do not return a StaticHandlerContext is when a
3479 * redirect response is returned or thrown from any action/loader. We
3480 * propagate that out and return the raw Response so the HTTP server can
3481 * return it directly.
3482 *
3483 * - `opts.requestContext` is an optional server context that will be passed
3484 * to actions/loaders in the `context` parameter
3485 * - `opts.skipLoaderErrorBubbling` is an optional parameter that will prevent
3486 * the bubbling of errors which allows single-fetch-type implementations
3487 * where the client will handle the bubbling and we may need to return data
3488 * for the handling route
3489 */
3490 async function query(
3491 request: Request,
3492 {
3493 requestContext,
3494 skipLoaderErrorBubbling,
3495 unstable_dataStrategy,
3496 }: {
3497 requestContext?: unknown;
3498 skipLoaderErrorBubbling?: boolean;
3499 unstable_dataStrategy?: DataStrategyFunction;
3500 } = {}
3501 ): Promise<StaticHandlerContext | Response> {
3502 let url = new URL(request.url);
3503 let method = request.method;
3504 let location = createLocation("", createPath(url), null, "default");
3505 let matches = matchRoutes(dataRoutes, location, basename);
3506
3507 // SSR supports HEAD requests while SPA doesn't
3508 if (!isValidMethod(method) && method !== "HEAD") {
3509 let error = getInternalRouterError(405, { method });
3510 let { matches: methodNotAllowedMatches, route } =
3511 getShortCircuitMatches(dataRoutes);
3512 return {
3513 basename,
3514 location,
3515 matches: methodNotAllowedMatches,
3516 loaderData: {},
3517 actionData: null,
3518 errors: {
3519 [route.id]: error,
3520 },
3521 statusCode: error.status,
3522 loaderHeaders: {},
3523 actionHeaders: {},
3524 activeDeferreds: null,
3525 };
3526 } else if (!matches) {
3527 let error = getInternalRouterError(404, { pathname: location.pathname });
3528 let { matches: notFoundMatches, route } =
3529 getShortCircuitMatches(dataRoutes);
3530 return {
3531 basename,
3532 location,
3533 matches: notFoundMatches,
3534 loaderData: {},
3535 actionData: null,
3536 errors: {
3537 [route.id]: error,
3538 },
3539 statusCode: error.status,
3540 loaderHeaders: {},
3541 actionHeaders: {},
3542 activeDeferreds: null,
3543 };
3544 }
3545
3546 let result = await queryImpl(
3547 request,
3548 location,
3549 matches,
3550 requestContext,
3551 unstable_dataStrategy || null,
3552 skipLoaderErrorBubbling === true,
3553 null
3554 );
3555 if (isResponse(result)) {
3556 return result;
3557 }
3558
3559 // When returning StaticHandlerContext, we patch back in the location here
3560 // since we need it for React Context. But this helps keep our submit and
3561 // loadRouteData operating on a Request instead of a Location
3562 return { location, basename, ...result };
3563 }
3564
3565 /**
3566 * The queryRoute() method is intended for targeted route requests, either
3567 * for fetch ?_data requests or resource route requests. In this case, we
3568 * are only ever calling a single action or loader, and we are returning the
3569 * returned value directly. In most cases, this will be a Response returned
3570 * from the action/loader, but it may be a primitive or other value as well -
3571 * and in such cases the calling context should handle that accordingly.
3572 *
3573 * We do respect the throw/return differentiation, so if an action/loader
3574 * throws, then this method will throw the value. This is important so we
3575 * can do proper boundary identification in Remix where a thrown Response
3576 * must go to the Catch Boundary but a returned Response is happy-path.
3577 *
3578 * One thing to note is that any Router-initiated Errors that make sense
3579 * to associate with a status code will be thrown as an ErrorResponse
3580 * instance which include the raw Error, such that the calling context can
3581 * serialize the error as they see fit while including the proper response
3582 * code. Examples here are 404 and 405 errors that occur prior to reaching
3583 * any user-defined loaders.
3584 *
3585 * - `opts.routeId` allows you to specify the specific route handler to call.
3586 * If not provided the handler will determine the proper route by matching
3587 * against `request.url`
3588 * - `opts.requestContext` is an optional server context that will be passed
3589 * to actions/loaders in the `context` parameter
3590 */
3591 async function queryRoute(
3592 request: Request,
3593 {
3594 routeId,
3595 requestContext,
3596 unstable_dataStrategy,
3597 }: {
3598 requestContext?: unknown;
3599 routeId?: string;
3600 unstable_dataStrategy?: DataStrategyFunction;
3601 } = {}
3602 ): Promise<any> {
3603 let url = new URL(request.url);
3604 let method = request.method;
3605 let location = createLocation("", createPath(url), null, "default");
3606 let matches = matchRoutes(dataRoutes, location, basename);
3607
3608 // SSR supports HEAD requests while SPA doesn't
3609 if (!isValidMethod(method) && method !== "HEAD" && method !== "OPTIONS") {
3610 throw getInternalRouterError(405, { method });
3611 } else if (!matches) {
3612 throw getInternalRouterError(404, { pathname: location.pathname });
3613 }
3614
3615 let match = routeId
3616 ? matches.find((m) => m.route.id === routeId)
3617 : getTargetMatch(matches, location);
3618
3619 if (routeId && !match) {
3620 throw getInternalRouterError(403, {
3621 pathname: location.pathname,
3622 routeId,
3623 });
3624 } else if (!match) {
3625 // This should never hit I don't think?
3626 throw getInternalRouterError(404, { pathname: location.pathname });
3627 }
3628
3629 let result = await queryImpl(
3630 request,
3631 location,
3632 matches,
3633 requestContext,
3634 unstable_dataStrategy || null,
3635 false,
3636 match
3637 );
3638
3639 if (isResponse(result)) {
3640 return result;
3641 }
3642
3643 let error = result.errors ? Object.values(result.errors)[0] : undefined;
3644 if (error !== undefined) {
3645 // If we got back result.errors, that means the loader/action threw
3646 // _something_ that wasn't a Response, but it's not guaranteed/required
3647 // to be an `instanceof Error` either, so we have to use throw here to
3648 // preserve the "error" state outside of queryImpl.
3649 throw error;
3650 }
3651
3652 // Pick off the right state value to return
3653 if (result.actionData) {
3654 return Object.values(result.actionData)[0];
3655 }
3656
3657 if (result.loaderData) {
3658 let data = Object.values(result.loaderData)[0];
3659 if (result.activeDeferreds?.[match.route.id]) {
3660 data[UNSAFE_DEFERRED_SYMBOL] = result.activeDeferreds[match.route.id];
3661 }
3662 return data;
3663 }
3664
3665 return undefined;
3666 }
3667
3668 async function queryImpl(
3669 request: Request,
3670 location: Location,
3671 matches: AgnosticDataRouteMatch[],
3672 requestContext: unknown,
3673 unstable_dataStrategy: DataStrategyFunction | null,
3674 skipLoaderErrorBubbling: boolean,
3675 routeMatch: AgnosticDataRouteMatch | null
3676 ): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
3677 invariant(
3678 request.signal,
3679 "query()/queryRoute() requests must contain an AbortController signal"
3680 );
3681
3682 try {
3683 if (isMutationMethod(request.method.toLowerCase())) {
3684 let result = await submit(
3685 request,
3686 matches,
3687 routeMatch || getTargetMatch(matches, location),
3688 requestContext,
3689 unstable_dataStrategy,
3690 skipLoaderErrorBubbling,
3691 routeMatch != null
3692 );
3693 return result;
3694 }
3695
3696 let result = await loadRouteData(
3697 request,
3698 matches,
3699 requestContext,
3700 unstable_dataStrategy,
3701 skipLoaderErrorBubbling,
3702 routeMatch
3703 );
3704 return isResponse(result)
3705 ? result
3706 : {
3707 ...result,
3708 actionData: null,
3709 actionHeaders: {},
3710 };
3711 } catch (e) {
3712 // If the user threw/returned a Response in callLoaderOrAction for a
3713 // `queryRoute` call, we throw the `HandlerResult` to bail out early
3714 // and then return or throw the raw Response here accordingly
3715 if (isHandlerResult(e) && isResponse(e.result)) {
3716 if (e.type === ResultType.error) {
3717 throw e.result;
3718 }
3719 return e.result;
3720 }
3721 // Redirects are always returned since they don't propagate to catch
3722 // boundaries
3723 if (isRedirectResponse(e)) {
3724 return e;
3725 }
3726 throw e;
3727 }
3728 }
3729
3730 async function submit(
3731 request: Request,
3732 matches: AgnosticDataRouteMatch[],
3733 actionMatch: AgnosticDataRouteMatch,
3734 requestContext: unknown,
3735 unstable_dataStrategy: DataStrategyFunction | null,
3736 skipLoaderErrorBubbling: boolean,
3737 isRouteRequest: boolean
3738 ): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
3739 let result: DataResult;
3740
3741 if (!actionMatch.route.action && !actionMatch.route.lazy) {
3742 let error = getInternalRouterError(405, {
3743 method: request.method,
3744 pathname: new URL(request.url).pathname,
3745 routeId: actionMatch.route.id,
3746 });
3747 if (isRouteRequest) {
3748 throw error;
3749 }
3750 result = {
3751 type: ResultType.error,
3752 error,
3753 };
3754 } else {
3755 let results = await callDataStrategy(
3756 "action",
3757 request,
3758 [actionMatch],
3759 matches,
3760 isRouteRequest,
3761 requestContext,
3762 unstable_dataStrategy
3763 );
3764 result = results[0];
3765
3766 if (request.signal.aborted) {
3767 throwStaticHandlerAbortedError(request, isRouteRequest, future);
3768 }
3769 }
3770
3771 if (isRedirectResult(result)) {
3772 // Uhhhh - this should never happen, we should always throw these from
3773 // callLoaderOrAction, but the type narrowing here keeps TS happy and we
3774 // can get back on the "throw all redirect responses" train here should
3775 // this ever happen :/
3776 throw new Response(null, {
3777 status: result.response.status,
3778 headers: {
3779 Location: result.response.headers.get("Location")!,
3780 },
3781 });
3782 }
3783
3784 if (isDeferredResult(result)) {
3785 let error = getInternalRouterError(400, { type: "defer-action" });
3786 if (isRouteRequest) {
3787 throw error;
3788 }
3789 result = {
3790 type: ResultType.error,
3791 error,
3792 };
3793 }
3794
3795 if (isRouteRequest) {
3796 // Note: This should only be non-Response values if we get here, since
3797 // isRouteRequest should throw any Response received in callLoaderOrAction
3798 if (isErrorResult(result)) {
3799 throw result.error;
3800 }
3801
3802 return {
3803 matches: [actionMatch],
3804 loaderData: {},
3805 actionData: { [actionMatch.route.id]: result.data },
3806 errors: null,
3807 // Note: statusCode + headers are unused here since queryRoute will
3808 // return the raw Response or value
3809 statusCode: 200,
3810 loaderHeaders: {},
3811 actionHeaders: {},
3812 activeDeferreds: null,
3813 };
3814 }
3815
3816 // Create a GET request for the loaders
3817 let loaderRequest = new Request(request.url, {
3818 headers: request.headers,
3819 redirect: request.redirect,
3820 signal: request.signal,
3821 });
3822
3823 if (isErrorResult(result)) {
3824 // Store off the pending error - we use it to determine which loaders
3825 // to call and will commit it when we complete the navigation
3826 let boundaryMatch = skipLoaderErrorBubbling
3827 ? actionMatch
3828 : findNearestBoundary(matches, actionMatch.route.id);
3829
3830 let context = await loadRouteData(
3831 loaderRequest,
3832 matches,
3833 requestContext,
3834 unstable_dataStrategy,
3835 skipLoaderErrorBubbling,
3836 null,
3837 [boundaryMatch.route.id, result]
3838 );
3839
3840 // action status codes take precedence over loader status codes
3841 return {
3842 ...context,
3843 statusCode: isRouteErrorResponse(result.error)
3844 ? result.error.status
3845 : result.statusCode != null
3846 ? result.statusCode
3847 : 500,
3848 actionData: null,
3849 actionHeaders: {
3850 ...(result.headers ? { [actionMatch.route.id]: result.headers } : {}),
3851 },
3852 };
3853 }
3854
3855 let context = await loadRouteData(
3856 loaderRequest,
3857 matches,
3858 requestContext,
3859 unstable_dataStrategy,
3860 skipLoaderErrorBubbling,
3861 null
3862 );
3863
3864 return {
3865 ...context,
3866 actionData: {
3867 [actionMatch.route.id]: result.data,
3868 },
3869 // action status codes take precedence over loader status codes
3870 ...(result.statusCode ? { statusCode: result.statusCode } : {}),
3871 actionHeaders: result.headers
3872 ? { [actionMatch.route.id]: result.headers }
3873 : {},
3874 };
3875 }
3876
3877 async function loadRouteData(
3878 request: Request,
3879 matches: AgnosticDataRouteMatch[],
3880 requestContext: unknown,
3881 unstable_dataStrategy: DataStrategyFunction | null,
3882 skipLoaderErrorBubbling: boolean,
3883 routeMatch: AgnosticDataRouteMatch | null,
3884 pendingActionResult?: PendingActionResult
3885 ): Promise<
3886 | Omit<
3887 StaticHandlerContext,
3888 "location" | "basename" | "actionData" | "actionHeaders"
3889 >
3890 | Response
3891 > {
3892 let isRouteRequest = routeMatch != null;
3893
3894 // Short circuit if we have no loaders to run (queryRoute())
3895 if (
3896 isRouteRequest &&
3897 !routeMatch?.route.loader &&
3898 !routeMatch?.route.lazy
3899 ) {
3900 throw getInternalRouterError(400, {
3901 method: request.method,
3902 pathname: new URL(request.url).pathname,
3903 routeId: routeMatch?.route.id,
3904 });
3905 }
3906
3907 let requestMatches = routeMatch
3908 ? [routeMatch]
3909 : pendingActionResult && isErrorResult(pendingActionResult[1])
3910 ? getLoaderMatchesUntilBoundary(matches, pendingActionResult[0])
3911 : matches;
3912 let matchesToLoad = requestMatches.filter(
3913 (m) => m.route.loader || m.route.lazy
3914 );
3915
3916 // Short circuit if we have no loaders to run (query())
3917 if (matchesToLoad.length === 0) {
3918 return {
3919 matches,
3920 // Add a null for all matched routes for proper revalidation on the client
3921 loaderData: matches.reduce(
3922 (acc, m) => Object.assign(acc, { [m.route.id]: null }),
3923 {}
3924 ),
3925 errors:
3926 pendingActionResult && isErrorResult(pendingActionResult[1])
3927 ? {
3928 [pendingActionResult[0]]: pendingActionResult[1].error,
3929 }
3930 : null,
3931 statusCode: 200,
3932 loaderHeaders: {},
3933 activeDeferreds: null,
3934 };
3935 }
3936
3937 let results = await callDataStrategy(
3938 "loader",
3939 request,
3940 matchesToLoad,
3941 matches,
3942 isRouteRequest,
3943 requestContext,
3944 unstable_dataStrategy
3945 );
3946
3947 if (request.signal.aborted) {
3948 throwStaticHandlerAbortedError(request, isRouteRequest, future);
3949 }
3950
3951 // Process and commit output from loaders
3952 let activeDeferreds = new Map<string, DeferredData>();
3953 let context = processRouteLoaderData(
3954 matches,
3955 matchesToLoad,
3956 results,
3957 pendingActionResult,
3958 activeDeferreds,
3959 skipLoaderErrorBubbling
3960 );
3961
3962 // Add a null for any non-loader matches for proper revalidation on the client
3963 let executedLoaders = new Set<string>(
3964 matchesToLoad.map((match) => match.route.id)
3965 );
3966 matches.forEach((match) => {
3967 if (!executedLoaders.has(match.route.id)) {
3968 context.loaderData[match.route.id] = null;
3969 }
3970 });
3971
3972 return {
3973 ...context,
3974 matches,
3975 activeDeferreds:
3976 activeDeferreds.size > 0
3977 ? Object.fromEntries(activeDeferreds.entries())
3978 : null,
3979 };
3980 }
3981
3982 // Utility wrapper for calling dataStrategy server-side without having to
3983 // pass around the manifest, mapRouteProperties, etc.
3984 async function callDataStrategy(
3985 type: "loader" | "action",
3986 request: Request,
3987 matchesToLoad: AgnosticDataRouteMatch[],
3988 matches: AgnosticDataRouteMatch[],
3989 isRouteRequest: boolean,
3990 requestContext: unknown,
3991 unstable_dataStrategy: DataStrategyFunction | null
3992 ): Promise<DataResult[]> {
3993 let results = await callDataStrategyImpl(
3994 unstable_dataStrategy || defaultDataStrategy,
3995 type,
3996 request,
3997 matchesToLoad,
3998 matches,
3999 manifest,
4000 mapRouteProperties,
4001 requestContext
4002 );
4003
4004 return await Promise.all(
4005 results.map((result, i) => {
4006 if (isRedirectHandlerResult(result)) {
4007 let response = result.result as Response;
4008 // Throw redirects and let the server handle them with an HTTP redirect
4009 throw normalizeRelativeRoutingRedirectResponse(
4010 response,
4011 request,
4012 matchesToLoad[i].route.id,
4013 matches,
4014 basename,
4015 future.v7_relativeSplatPath
4016 );
4017 }
4018 if (isResponse(result.result) && isRouteRequest) {
4019 // For SSR single-route requests, we want to hand Responses back
4020 // directly without unwrapping
4021 throw result;
4022 }
4023
4024 return convertHandlerResultToDataResult(result);
4025 })
4026 );
4027 }
4028
4029 return {
4030 dataRoutes,
4031 query,
4032 queryRoute,
4033 };
4034}
4035
4036//#endregion
4037
4038////////////////////////////////////////////////////////////////////////////////
4039//#region Helpers
4040////////////////////////////////////////////////////////////////////////////////
4041
4042/**
4043 * Given an existing StaticHandlerContext and an error thrown at render time,
4044 * provide an updated StaticHandlerContext suitable for a second SSR render
4045 */
4046export function getStaticContextFromError(
4047 routes: AgnosticDataRouteObject[],
4048 context: StaticHandlerContext,
4049 error: any
4050) {
4051 let newContext: StaticHandlerContext = {
4052 ...context,
4053 statusCode: isRouteErrorResponse(error) ? error.status : 500,
4054 errors: {
4055 [context._deepestRenderedBoundaryId || routes[0].id]: error,
4056 },
4057 };
4058 return newContext;
4059}
4060
4061function throwStaticHandlerAbortedError(
4062 request: Request,
4063 isRouteRequest: boolean,
4064 future: StaticHandlerFutureConfig
4065) {
4066 if (future.v7_throwAbortReason && request.signal.reason !== undefined) {
4067 throw request.signal.reason;
4068 }
4069
4070 let method = isRouteRequest ? "queryRoute" : "query";
4071 throw new Error(`${method}() call aborted: ${request.method} ${request.url}`);
4072}
4073
4074function isSubmissionNavigation(
4075 opts: BaseNavigateOrFetchOptions
4076): opts is SubmissionNavigateOptions {
4077 return (
4078 opts != null &&
4079 (("formData" in opts && opts.formData != null) ||
4080 ("body" in opts && opts.body !== undefined))
4081 );
4082}
4083
4084function normalizeTo(
4085 location: Path,
4086 matches: AgnosticDataRouteMatch[],
4087 basename: string,
4088 prependBasename: boolean,
4089 to: To | null,
4090 v7_relativeSplatPath: boolean,
4091 fromRouteId?: string,
4092 relative?: RelativeRoutingType
4093) {
4094 let contextualMatches: AgnosticDataRouteMatch[];
4095 let activeRouteMatch: AgnosticDataRouteMatch | undefined;
4096 if (fromRouteId) {
4097 // Grab matches up to the calling route so our route-relative logic is
4098 // relative to the correct source route
4099 contextualMatches = [];
4100 for (let match of matches) {
4101 contextualMatches.push(match);
4102 if (match.route.id === fromRouteId) {
4103 activeRouteMatch = match;
4104 break;
4105 }
4106 }
4107 } else {
4108 contextualMatches = matches;
4109 activeRouteMatch = matches[matches.length - 1];
4110 }
4111
4112 // Resolve the relative path
4113 let path = resolveTo(
4114 to ? to : ".",
4115 getResolveToMatches(contextualMatches, v7_relativeSplatPath),
4116 stripBasename(location.pathname, basename) || location.pathname,
4117 relative === "path"
4118 );
4119
4120 // When `to` is not specified we inherit search/hash from the current
4121 // location, unlike when to="." and we just inherit the path.
4122 // See https://github.com/remix-run/remix/issues/927
4123 if (to == null) {
4124 path.search = location.search;
4125 path.hash = location.hash;
4126 }
4127
4128 // Add an ?index param for matched index routes if we don't already have one
4129 if (
4130 (to == null || to === "" || to === ".") &&
4131 activeRouteMatch &&
4132 activeRouteMatch.route.index &&
4133 !hasNakedIndexQuery(path.search)
4134 ) {
4135 path.search = path.search
4136 ? path.search.replace(/^\?/, "?index&")
4137 : "?index";
4138 }
4139
4140 // If we're operating within a basename, prepend it to the pathname. If
4141 // this is a root navigation, then just use the raw basename which allows
4142 // the basename to have full control over the presence of a trailing slash
4143 // on root actions
4144 if (prependBasename && basename !== "/") {
4145 path.pathname =
4146 path.pathname === "/" ? basename : joinPaths([basename, path.pathname]);
4147 }
4148
4149 return createPath(path);
4150}
4151
4152// Normalize navigation options by converting formMethod=GET formData objects to
4153// URLSearchParams so they behave identically to links with query params
4154function normalizeNavigateOptions(
4155 normalizeFormMethod: boolean,
4156 isFetcher: boolean,
4157 path: string,
4158 opts?: BaseNavigateOrFetchOptions
4159): {
4160 path: string;
4161 submission?: Submission;
4162 error?: ErrorResponseImpl;
4163} {
4164 // Return location verbatim on non-submission navigations
4165 if (!opts || !isSubmissionNavigation(opts)) {
4166 return { path };
4167 }
4168
4169 if (opts.formMethod && !isValidMethod(opts.formMethod)) {
4170 return {
4171 path,
4172 error: getInternalRouterError(405, { method: opts.formMethod }),
4173 };
4174 }
4175
4176 let getInvalidBodyError = () => ({
4177 path,
4178 error: getInternalRouterError(400, { type: "invalid-body" }),
4179 });
4180
4181 // Create a Submission on non-GET navigations
4182 let rawFormMethod = opts.formMethod || "get";
4183 let formMethod = normalizeFormMethod
4184 ? (rawFormMethod.toUpperCase() as V7_FormMethod)
4185 : (rawFormMethod.toLowerCase() as FormMethod);
4186 let formAction = stripHashFromPath(path);
4187
4188 if (opts.body !== undefined) {
4189 if (opts.formEncType === "text/plain") {
4190 // text only support POST/PUT/PATCH/DELETE submissions
4191 if (!isMutationMethod(formMethod)) {
4192 return getInvalidBodyError();
4193 }
4194
4195 let text =
4196 typeof opts.body === "string"
4197 ? opts.body
4198 : opts.body instanceof FormData ||
4199 opts.body instanceof URLSearchParams
4200 ? // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#plain-text-form-data
4201 Array.from(opts.body.entries()).reduce(
4202 (acc, [name, value]) => `${acc}${name}=${value}\n`,
4203 ""
4204 )
4205 : String(opts.body);
4206
4207 return {
4208 path,
4209 submission: {
4210 formMethod,
4211 formAction,
4212 formEncType: opts.formEncType,
4213 formData: undefined,
4214 json: undefined,
4215 text,
4216 },
4217 };
4218 } else if (opts.formEncType === "application/json") {
4219 // json only supports POST/PUT/PATCH/DELETE submissions
4220 if (!isMutationMethod(formMethod)) {
4221 return getInvalidBodyError();
4222 }
4223
4224 try {
4225 let json =
4226 typeof opts.body === "string" ? JSON.parse(opts.body) : opts.body;
4227
4228 return {
4229 path,
4230 submission: {
4231 formMethod,
4232 formAction,
4233 formEncType: opts.formEncType,
4234 formData: undefined,
4235 json,
4236 text: undefined,
4237 },
4238 };
4239 } catch (e) {
4240 return getInvalidBodyError();
4241 }
4242 }
4243 }
4244
4245 invariant(
4246 typeof FormData === "function",
4247 "FormData is not available in this environment"
4248 );
4249
4250 let searchParams: URLSearchParams;
4251 let formData: FormData;
4252
4253 if (opts.formData) {
4254 searchParams = convertFormDataToSearchParams(opts.formData);
4255 formData = opts.formData;
4256 } else if (opts.body instanceof FormData) {
4257 searchParams = convertFormDataToSearchParams(opts.body);
4258 formData = opts.body;
4259 } else if (opts.body instanceof URLSearchParams) {
4260 searchParams = opts.body;
4261 formData = convertSearchParamsToFormData(searchParams);
4262 } else if (opts.body == null) {
4263 searchParams = new URLSearchParams();
4264 formData = new FormData();
4265 } else {
4266 try {
4267 searchParams = new URLSearchParams(opts.body);
4268 formData = convertSearchParamsToFormData(searchParams);
4269 } catch (e) {
4270 return getInvalidBodyError();
4271 }
4272 }
4273
4274 let submission: Submission = {
4275 formMethod,
4276 formAction,
4277 formEncType:
4278 (opts && opts.formEncType) || "application/x-www-form-urlencoded",
4279 formData,
4280 json: undefined,
4281 text: undefined,
4282 };
4283
4284 if (isMutationMethod(submission.formMethod)) {
4285 return { path, submission };
4286 }
4287
4288 // Flatten submission onto URLSearchParams for GET submissions
4289 let parsedPath = parsePath(path);
4290 // On GET navigation submissions we can drop the ?index param from the
4291 // resulting location since all loaders will run. But fetcher GET submissions
4292 // only run a single loader so we need to preserve any incoming ?index params
4293 if (isFetcher && parsedPath.search && hasNakedIndexQuery(parsedPath.search)) {
4294 searchParams.append("index", "");
4295 }
4296 parsedPath.search = `?${searchParams}`;
4297
4298 return { path: createPath(parsedPath), submission };
4299}
4300
4301// Filter out all routes below any caught error as they aren't going to
4302// render so we don't need to load them
4303function getLoaderMatchesUntilBoundary(
4304 matches: AgnosticDataRouteMatch[],
4305 boundaryId: string
4306) {
4307 let boundaryMatches = matches;
4308 if (boundaryId) {
4309 let index = matches.findIndex((m) => m.route.id === boundaryId);
4310 if (index >= 0) {
4311 boundaryMatches = matches.slice(0, index);
4312 }
4313 }
4314 return boundaryMatches;
4315}
4316
4317function getMatchesToLoad(
4318 history: History,
4319 state: RouterState,
4320 matches: AgnosticDataRouteMatch[],
4321 submission: Submission | undefined,
4322 location: Location,
4323 isInitialLoad: boolean,
4324 skipActionErrorRevalidation: boolean,
4325 isRevalidationRequired: boolean,
4326 cancelledDeferredRoutes: string[],
4327 cancelledFetcherLoads: Set<string>,
4328 deletedFetchers: Set<string>,
4329 fetchLoadMatches: Map<string, FetchLoadMatch>,
4330 fetchRedirectIds: Set<string>,
4331 routesToUse: AgnosticDataRouteObject[],
4332 basename: string | undefined,
4333 pendingActionResult?: PendingActionResult
4334): [AgnosticDataRouteMatch[], RevalidatingFetcher[]] {
4335 let actionResult = pendingActionResult
4336 ? isErrorResult(pendingActionResult[1])
4337 ? pendingActionResult[1].error
4338 : pendingActionResult[1].data
4339 : undefined;
4340 let currentUrl = history.createURL(state.location);
4341 let nextUrl = history.createURL(location);
4342
4343 // Pick navigation matches that are net-new or qualify for revalidation
4344 let boundaryId =
4345 pendingActionResult && isErrorResult(pendingActionResult[1])
4346 ? pendingActionResult[0]
4347 : undefined;
4348 let boundaryMatches = boundaryId
4349 ? getLoaderMatchesUntilBoundary(matches, boundaryId)
4350 : matches;
4351
4352 // Don't revalidate loaders by default after action 4xx/5xx responses
4353 // when the flag is enabled. They can still opt-into revalidation via
4354 // `shouldRevalidate` via `actionResult`
4355 let actionStatus = pendingActionResult
4356 ? pendingActionResult[1].statusCode
4357 : undefined;
4358 let shouldSkipRevalidation =
4359 skipActionErrorRevalidation && actionStatus && actionStatus >= 400;
4360
4361 let navigationMatches = boundaryMatches.filter((match, index) => {
4362 let { route } = match;
4363 if (route.lazy) {
4364 // We haven't loaded this route yet so we don't know if it's got a loader!
4365 return true;
4366 }
4367
4368 if (route.loader == null) {
4369 return false;
4370 }
4371
4372 if (isInitialLoad) {
4373 if (typeof route.loader !== "function" || route.loader.hydrate) {
4374 return true;
4375 }
4376 return (
4377 state.loaderData[route.id] === undefined &&
4378 // Don't re-run if the loader ran and threw an error
4379 (!state.errors || state.errors[route.id] === undefined)
4380 );
4381 }
4382
4383 // Always call the loader on new route instances and pending defer cancellations
4384 if (
4385 isNewLoader(state.loaderData, state.matches[index], match) ||
4386 cancelledDeferredRoutes.some((id) => id === match.route.id)
4387 ) {
4388 return true;
4389 }
4390
4391 // This is the default implementation for when we revalidate. If the route
4392 // provides it's own implementation, then we give them full control but
4393 // provide this value so they can leverage it if needed after they check
4394 // their own specific use cases
4395 let currentRouteMatch = state.matches[index];
4396 let nextRouteMatch = match;
4397
4398 return shouldRevalidateLoader(match, {
4399 currentUrl,
4400 currentParams: currentRouteMatch.params,
4401 nextUrl,
4402 nextParams: nextRouteMatch.params,
4403 ...submission,
4404 actionResult,
4405 actionStatus,
4406 defaultShouldRevalidate: shouldSkipRevalidation
4407 ? false
4408 : // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate
4409 isRevalidationRequired ||
4410 currentUrl.pathname + currentUrl.search ===
4411 nextUrl.pathname + nextUrl.search ||
4412 // Search params affect all loaders
4413 currentUrl.search !== nextUrl.search ||
4414 isNewRouteInstance(currentRouteMatch, nextRouteMatch),
4415 });
4416 });
4417
4418 // Pick fetcher.loads that need to be revalidated
4419 let revalidatingFetchers: RevalidatingFetcher[] = [];
4420 fetchLoadMatches.forEach((f, key) => {
4421 // Don't revalidate:
4422 // - on initial load (shouldn't be any fetchers then anyway)
4423 // - if fetcher won't be present in the subsequent render
4424 // - no longer matches the URL (v7_fetcherPersist=false)
4425 // - was unmounted but persisted due to v7_fetcherPersist=true
4426 if (
4427 isInitialLoad ||
4428 !matches.some((m) => m.route.id === f.routeId) ||
4429 deletedFetchers.has(key)
4430 ) {
4431 return;
4432 }
4433
4434 let fetcherMatches = matchRoutes(routesToUse, f.path, basename);
4435
4436 // If the fetcher path no longer matches, push it in with null matches so
4437 // we can trigger a 404 in callLoadersAndMaybeResolveData. Note this is
4438 // currently only a use-case for Remix HMR where the route tree can change
4439 // at runtime and remove a route previously loaded via a fetcher
4440 if (!fetcherMatches) {
4441 revalidatingFetchers.push({
4442 key,
4443 routeId: f.routeId,
4444 path: f.path,
4445 matches: null,
4446 match: null,
4447 controller: null,
4448 });
4449 return;
4450 }
4451
4452 // Revalidating fetchers are decoupled from the route matches since they
4453 // load from a static href. They revalidate based on explicit revalidation
4454 // (submission, useRevalidator, or X-Remix-Revalidate)
4455 let fetcher = state.fetchers.get(key);
4456 let fetcherMatch = getTargetMatch(fetcherMatches, f.path);
4457
4458 let shouldRevalidate = false;
4459 if (fetchRedirectIds.has(key)) {
4460 // Never trigger a revalidation of an actively redirecting fetcher
4461 shouldRevalidate = false;
4462 } else if (cancelledFetcherLoads.has(key)) {
4463 // Always mark for revalidation if the fetcher was cancelled
4464 cancelledFetcherLoads.delete(key);
4465 shouldRevalidate = true;
4466 } else if (
4467 fetcher &&
4468 fetcher.state !== "idle" &&
4469 fetcher.data === undefined
4470 ) {
4471 // If the fetcher hasn't ever completed loading yet, then this isn't a
4472 // revalidation, it would just be a brand new load if an explicit
4473 // revalidation is required
4474 shouldRevalidate = isRevalidationRequired;
4475 } else {
4476 // Otherwise fall back on any user-defined shouldRevalidate, defaulting
4477 // to explicit revalidations only
4478 shouldRevalidate = shouldRevalidateLoader(fetcherMatch, {
4479 currentUrl,
4480 currentParams: state.matches[state.matches.length - 1].params,
4481 nextUrl,
4482 nextParams: matches[matches.length - 1].params,
4483 ...submission,
4484 actionResult,
4485 actionStatus,
4486 defaultShouldRevalidate: shouldSkipRevalidation
4487 ? false
4488 : isRevalidationRequired,
4489 });
4490 }
4491
4492 if (shouldRevalidate) {
4493 revalidatingFetchers.push({
4494 key,
4495 routeId: f.routeId,
4496 path: f.path,
4497 matches: fetcherMatches,
4498 match: fetcherMatch,
4499 controller: new AbortController(),
4500 });
4501 }
4502 });
4503
4504 return [navigationMatches, revalidatingFetchers];
4505}
4506
4507function isNewLoader(
4508 currentLoaderData: RouteData,
4509 currentMatch: AgnosticDataRouteMatch,
4510 match: AgnosticDataRouteMatch
4511) {
4512 let isNew =
4513 // [a] -> [a, b]
4514 !currentMatch ||
4515 // [a, b] -> [a, c]
4516 match.route.id !== currentMatch.route.id;
4517
4518 // Handle the case that we don't have data for a re-used route, potentially
4519 // from a prior error or from a cancelled pending deferred
4520 let isMissingData = currentLoaderData[match.route.id] === undefined;
4521
4522 // Always load if this is a net-new route or we don't yet have data
4523 return isNew || isMissingData;
4524}
4525
4526function isNewRouteInstance(
4527 currentMatch: AgnosticDataRouteMatch,
4528 match: AgnosticDataRouteMatch
4529) {
4530 let currentPath = currentMatch.route.path;
4531 return (
4532 // param change for this match, /users/123 -> /users/456
4533 currentMatch.pathname !== match.pathname ||
4534 // splat param changed, which is not present in match.path
4535 // e.g. /files/images/avatar.jpg -> files/finances.xls
4536 (currentPath != null &&
4537 currentPath.endsWith("*") &&
4538 currentMatch.params["*"] !== match.params["*"])
4539 );
4540}
4541
4542function shouldRevalidateLoader(
4543 loaderMatch: AgnosticDataRouteMatch,
4544 arg: ShouldRevalidateFunctionArgs
4545) {
4546 if (loaderMatch.route.shouldRevalidate) {
4547 let routeChoice = loaderMatch.route.shouldRevalidate(arg);
4548 if (typeof routeChoice === "boolean") {
4549 return routeChoice;
4550 }
4551 }
4552
4553 return arg.defaultShouldRevalidate;
4554}
4555
4556/**
4557 * Idempotent utility to execute patchRoutesOnMiss() to lazily load route
4558 * definitions and update the routes/routeManifest
4559 */
4560async function loadLazyRouteChildren(
4561 patchRoutesOnMissImpl: AgnosticPatchRoutesOnMissFunction,
4562 path: string,
4563 matches: AgnosticDataRouteMatch[],
4564 routes: AgnosticDataRouteObject[],
4565 manifest: RouteManifest,
4566 mapRouteProperties: MapRoutePropertiesFunction,
4567 pendingRouteChildren: Map<string, ReturnType<typeof patchRoutesOnMissImpl>>,
4568 signal: AbortSignal
4569) {
4570 let key = [path, ...matches.map((m) => m.route.id)].join("-");
4571 try {
4572 let pending = pendingRouteChildren.get(key);
4573 if (!pending) {
4574 pending = patchRoutesOnMissImpl({
4575 path,
4576 matches,
4577 patch: (routeId, children) => {
4578 if (!signal.aborted) {
4579 patchRoutesImpl(
4580 routeId,
4581 children,
4582 routes,
4583 manifest,
4584 mapRouteProperties
4585 );
4586 }
4587 },
4588 });
4589 pendingRouteChildren.set(key, pending);
4590 }
4591
4592 if (pending && isPromise<AgnosticRouteObject[]>(pending)) {
4593 await pending;
4594 }
4595 } finally {
4596 pendingRouteChildren.delete(key);
4597 }
4598}
4599
4600function patchRoutesImpl(
4601 routeId: string | null,
4602 children: AgnosticRouteObject[],
4603 routesToUse: AgnosticDataRouteObject[],
4604 manifest: RouteManifest,
4605 mapRouteProperties: MapRoutePropertiesFunction
4606) {
4607 if (routeId) {
4608 let route = manifest[routeId];
4609 invariant(
4610 route,
4611 `No route found to patch children into: routeId = ${routeId}`
4612 );
4613 let dataChildren = convertRoutesToDataRoutes(
4614 children,
4615 mapRouteProperties,
4616 [routeId, "patch", String(route.children?.length || "0")],
4617 manifest
4618 );
4619 if (route.children) {
4620 route.children.push(...dataChildren);
4621 } else {
4622 route.children = dataChildren;
4623 }
4624 } else {
4625 let dataChildren = convertRoutesToDataRoutes(
4626 children,
4627 mapRouteProperties,
4628 ["patch", String(routesToUse.length || "0")],
4629 manifest
4630 );
4631 routesToUse.push(...dataChildren);
4632 }
4633}
4634
4635/**
4636 * Execute route.lazy() methods to lazily load route modules (loader, action,
4637 * shouldRevalidate) and update the routeManifest in place which shares objects
4638 * with dataRoutes so those get updated as well.
4639 */
4640async function loadLazyRouteModule(
4641 route: AgnosticDataRouteObject,
4642 mapRouteProperties: MapRoutePropertiesFunction,
4643 manifest: RouteManifest
4644) {
4645 if (!route.lazy) {
4646 return;
4647 }
4648
4649 let lazyRoute = await route.lazy();
4650
4651 // If the lazy route function was executed and removed by another parallel
4652 // call then we can return - first lazy() to finish wins because the return
4653 // value of lazy is expected to be static
4654 if (!route.lazy) {
4655 return;
4656 }
4657
4658 let routeToUpdate = manifest[route.id];
4659 invariant(routeToUpdate, "No route found in manifest");
4660
4661 // Update the route in place. This should be safe because there's no way
4662 // we could yet be sitting on this route as we can't get there without
4663 // resolving lazy() first.
4664 //
4665 // This is different than the HMR "update" use-case where we may actively be
4666 // on the route being updated. The main concern boils down to "does this
4667 // mutation affect any ongoing navigations or any current state.matches
4668 // values?". If not, it should be safe to update in place.
4669 let routeUpdates: Record<string, any> = {};
4670 for (let lazyRouteProperty in lazyRoute) {
4671 let staticRouteValue =
4672 routeToUpdate[lazyRouteProperty as keyof typeof routeToUpdate];
4673
4674 let isPropertyStaticallyDefined =
4675 staticRouteValue !== undefined &&
4676 // This property isn't static since it should always be updated based
4677 // on the route updates
4678 lazyRouteProperty !== "hasErrorBoundary";
4679
4680 warning(
4681 !isPropertyStaticallyDefined,
4682 `Route "${routeToUpdate.id}" has a static property "${lazyRouteProperty}" ` +
4683 `defined but its lazy function is also returning a value for this property. ` +
4684 `The lazy route property "${lazyRouteProperty}" will be ignored.`
4685 );
4686
4687 if (
4688 !isPropertyStaticallyDefined &&
4689 !immutableRouteKeys.has(lazyRouteProperty as ImmutableRouteKey)
4690 ) {
4691 routeUpdates[lazyRouteProperty] =
4692 lazyRoute[lazyRouteProperty as keyof typeof lazyRoute];
4693 }
4694 }
4695
4696 // Mutate the route with the provided updates. Do this first so we pass
4697 // the updated version to mapRouteProperties
4698 Object.assign(routeToUpdate, routeUpdates);
4699
4700 // Mutate the `hasErrorBoundary` property on the route based on the route
4701 // updates and remove the `lazy` function so we don't resolve the lazy
4702 // route again.
4703 Object.assign(routeToUpdate, {
4704 // To keep things framework agnostic, we use the provided
4705 // `mapRouteProperties` (or wrapped `detectErrorBoundary`) function to
4706 // set the framework-aware properties (`element`/`hasErrorBoundary`) since
4707 // the logic will differ between frameworks.
4708 ...mapRouteProperties(routeToUpdate),
4709 lazy: undefined,
4710 });
4711}
4712
4713// Default implementation of `dataStrategy` which fetches all loaders in parallel
4714function defaultDataStrategy(
4715 opts: DataStrategyFunctionArgs
4716): ReturnType<DataStrategyFunction> {
4717 return Promise.all(opts.matches.map((m) => m.resolve()));
4718}
4719
4720async function callDataStrategyImpl(
4721 dataStrategyImpl: DataStrategyFunction,
4722 type: "loader" | "action",
4723 request: Request,
4724 matchesToLoad: AgnosticDataRouteMatch[],
4725 matches: AgnosticDataRouteMatch[],
4726 manifest: RouteManifest,
4727 mapRouteProperties: MapRoutePropertiesFunction,
4728 requestContext?: unknown
4729): Promise<HandlerResult[]> {
4730 let routeIdsToLoad = matchesToLoad.reduce(
4731 (acc, m) => acc.add(m.route.id),
4732 new Set<string>()
4733 );
4734 let loadedMatches = new Set<string>();
4735
4736 // Send all matches here to allow for a middleware-type implementation.
4737 // handler will be a no-op for unneeded routes and we filter those results
4738 // back out below.
4739 let results = await dataStrategyImpl({
4740 matches: matches.map((match) => {
4741 let shouldLoad = routeIdsToLoad.has(match.route.id);
4742 // `resolve` encapsulates the route.lazy, executing the
4743 // loader/action, and mapping return values/thrown errors to a
4744 // HandlerResult. Users can pass a callback to take fine-grained control
4745 // over the execution of the loader/action
4746 let resolve: DataStrategyMatch["resolve"] = (handlerOverride) => {
4747 loadedMatches.add(match.route.id);
4748 return shouldLoad
4749 ? callLoaderOrAction(
4750 type,
4751 request,
4752 match,
4753 manifest,
4754 mapRouteProperties,
4755 handlerOverride,
4756 requestContext
4757 )
4758 : Promise.resolve({ type: ResultType.data, result: undefined });
4759 };
4760
4761 return {
4762 ...match,
4763 shouldLoad,
4764 resolve,
4765 };
4766 }),
4767 request,
4768 params: matches[0].params,
4769 context: requestContext,
4770 });
4771
4772 // Throw if any loadRoute implementations not called since they are what
4773 // ensures a route is fully loaded
4774 matches.forEach((m) =>
4775 invariant(
4776 loadedMatches.has(m.route.id),
4777 `\`match.resolve()\` was not called for route id "${m.route.id}". ` +
4778 "You must call `match.resolve()` on every match passed to " +
4779 "`dataStrategy` to ensure all routes are properly loaded."
4780 )
4781 );
4782
4783 // Filter out any middleware-only matches for which we didn't need to run handlers
4784 return results.filter((_, i) => routeIdsToLoad.has(matches[i].route.id));
4785}
4786
4787// Default logic for calling a loader/action is the user has no specified a dataStrategy
4788async function callLoaderOrAction(
4789 type: "loader" | "action",
4790 request: Request,
4791 match: AgnosticDataRouteMatch,
4792 manifest: RouteManifest,
4793 mapRouteProperties: MapRoutePropertiesFunction,
4794 handlerOverride: Parameters<DataStrategyMatch["resolve"]>[0],
4795 staticContext?: unknown
4796): Promise<HandlerResult> {
4797 let result: HandlerResult;
4798 let onReject: (() => void) | undefined;
4799
4800 let runHandler = (
4801 handler: AgnosticRouteObject["loader"] | AgnosticRouteObject["action"]
4802 ): Promise<HandlerResult> => {
4803 // Setup a promise we can race against so that abort signals short circuit
4804 let reject: () => void;
4805 // This will never resolve so safe to type it as Promise<HandlerResult> to
4806 // satisfy the function return value
4807 let abortPromise = new Promise<HandlerResult>((_, r) => (reject = r));
4808 onReject = () => reject();
4809 request.signal.addEventListener("abort", onReject);
4810
4811 let actualHandler = (ctx?: unknown) => {
4812 if (typeof handler !== "function") {
4813 return Promise.reject(
4814 new Error(
4815 `You cannot call the handler for a route which defines a boolean ` +
4816 `"${type}" [routeId: ${match.route.id}]`
4817 )
4818 );
4819 }
4820 return handler(
4821 {
4822 request,
4823 params: match.params,
4824 context: staticContext,
4825 },
4826 ...(ctx !== undefined ? [ctx] : [])
4827 );
4828 };
4829
4830 let handlerPromise: Promise<HandlerResult>;
4831 if (handlerOverride) {
4832 handlerPromise = handlerOverride((ctx: unknown) => actualHandler(ctx));
4833 } else {
4834 handlerPromise = (async () => {
4835 try {
4836 let val = await actualHandler();
4837 return { type: "data", result: val };
4838 } catch (e) {
4839 return { type: "error", result: e };
4840 }
4841 })();
4842 }
4843
4844 return Promise.race([handlerPromise, abortPromise]);
4845 };
4846
4847 try {
4848 let handler = match.route[type];
4849
4850 if (match.route.lazy) {
4851 if (handler) {
4852 // Run statically defined handler in parallel with lazy()
4853 let handlerError;
4854 let [value] = await Promise.all([
4855 // If the handler throws, don't let it immediately bubble out,
4856 // since we need to let the lazy() execution finish so we know if this
4857 // route has a boundary that can handle the error
4858 runHandler(handler).catch((e) => {
4859 handlerError = e;
4860 }),
4861 loadLazyRouteModule(match.route, mapRouteProperties, manifest),
4862 ]);
4863 if (handlerError !== undefined) {
4864 throw handlerError;
4865 }
4866 result = value!;
4867 } else {
4868 // Load lazy route module, then run any returned handler
4869 await loadLazyRouteModule(match.route, mapRouteProperties, manifest);
4870
4871 handler = match.route[type];
4872 if (handler) {
4873 // Handler still runs even if we got interrupted to maintain consistency
4874 // with un-abortable behavior of handler execution on non-lazy or
4875 // previously-lazy-loaded routes
4876 result = await runHandler(handler);
4877 } else if (type === "action") {
4878 let url = new URL(request.url);
4879 let pathname = url.pathname + url.search;
4880 throw getInternalRouterError(405, {
4881 method: request.method,
4882 pathname,
4883 routeId: match.route.id,
4884 });
4885 } else {
4886 // lazy() route has no loader to run. Short circuit here so we don't
4887 // hit the invariant below that errors on returning undefined.
4888 return { type: ResultType.data, result: undefined };
4889 }
4890 }
4891 } else if (!handler) {
4892 let url = new URL(request.url);
4893 let pathname = url.pathname + url.search;
4894 throw getInternalRouterError(404, {
4895 pathname,
4896 });
4897 } else {
4898 result = await runHandler(handler);
4899 }
4900
4901 invariant(
4902 result.result !== undefined,
4903 `You defined ${type === "action" ? "an action" : "a loader"} for route ` +
4904 `"${match.route.id}" but didn't return anything from your \`${type}\` ` +
4905 `function. Please return a value or \`null\`.`
4906 );
4907 } catch (e) {
4908 // We should already be catching and converting normal handler executions to
4909 // HandlerResults and returning them, so anything that throws here is an
4910 // unexpected error we still need to wrap
4911 return { type: ResultType.error, result: e };
4912 } finally {
4913 if (onReject) {
4914 request.signal.removeEventListener("abort", onReject);
4915 }
4916 }
4917
4918 return result;
4919}
4920
4921async function convertHandlerResultToDataResult(
4922 handlerResult: HandlerResult
4923): Promise<DataResult> {
4924 let { result, type } = handlerResult;
4925
4926 if (isResponse(result)) {
4927 let data: any;
4928
4929 try {
4930 let contentType = result.headers.get("Content-Type");
4931 // Check between word boundaries instead of startsWith() due to the last
4932 // paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type
4933 if (contentType && /\bapplication\/json\b/.test(contentType)) {
4934 if (result.body == null) {
4935 data = null;
4936 } else {
4937 data = await result.json();
4938 }
4939 } else {
4940 data = await result.text();
4941 }
4942 } catch (e) {
4943 return { type: ResultType.error, error: e };
4944 }
4945
4946 if (type === ResultType.error) {
4947 return {
4948 type: ResultType.error,
4949 error: new ErrorResponseImpl(result.status, result.statusText, data),
4950 statusCode: result.status,
4951 headers: result.headers,
4952 };
4953 }
4954
4955 return {
4956 type: ResultType.data,
4957 data,
4958 statusCode: result.status,
4959 headers: result.headers,
4960 };
4961 }
4962
4963 if (type === ResultType.error) {
4964 if (isDataWithResponseInit(result)) {
4965 if (result.data instanceof Error) {
4966 return {
4967 type: ResultType.error,
4968 error: result.data,
4969 statusCode: result.init?.status,
4970 };
4971 }
4972
4973 // Convert thrown unstable_data() to ErrorResponse instances
4974 result = new ErrorResponseImpl(
4975 result.init?.status || 500,
4976 undefined,
4977 result.data
4978 );
4979 }
4980 return {
4981 type: ResultType.error,
4982 error: result,
4983 statusCode: isRouteErrorResponse(result) ? result.status : undefined,
4984 };
4985 }
4986
4987 if (isDeferredData(result)) {
4988 return {
4989 type: ResultType.deferred,
4990 deferredData: result,
4991 statusCode: result.init?.status,
4992 headers: result.init?.headers && new Headers(result.init.headers),
4993 };
4994 }
4995
4996 if (isDataWithResponseInit(result)) {
4997 return {
4998 type: ResultType.data,
4999 data: result.data,
5000 statusCode: result.init?.status,
5001 headers: result.init?.headers
5002 ? new Headers(result.init.headers)
5003 : undefined,
5004 };
5005 }
5006
5007 return { type: ResultType.data, data: result };
5008}
5009
5010// Support relative routing in internal redirects
5011function normalizeRelativeRoutingRedirectResponse(
5012 response: Response,
5013 request: Request,
5014 routeId: string,
5015 matches: AgnosticDataRouteMatch[],
5016 basename: string,
5017 v7_relativeSplatPath: boolean
5018) {
5019 let location = response.headers.get("Location");
5020 invariant(
5021 location,
5022 "Redirects returned/thrown from loaders/actions must have a Location header"
5023 );
5024
5025 if (!ABSOLUTE_URL_REGEX.test(location)) {
5026 let trimmedMatches = matches.slice(
5027 0,
5028 matches.findIndex((m) => m.route.id === routeId) + 1
5029 );
5030 location = normalizeTo(
5031 new URL(request.url),
5032 trimmedMatches,
5033 basename,
5034 true,
5035 location,
5036 v7_relativeSplatPath
5037 );
5038 response.headers.set("Location", location);
5039 }
5040
5041 return response;
5042}
5043
5044function normalizeRedirectLocation(
5045 location: string,
5046 currentUrl: URL,
5047 basename: string
5048): string {
5049 if (ABSOLUTE_URL_REGEX.test(location)) {
5050 // Strip off the protocol+origin for same-origin + same-basename absolute redirects
5051 let normalizedLocation = location;
5052 let url = normalizedLocation.startsWith("//")
5053 ? new URL(currentUrl.protocol + normalizedLocation)
5054 : new URL(normalizedLocation);
5055 let isSameBasename = stripBasename(url.pathname, basename) != null;
5056 if (url.origin === currentUrl.origin && isSameBasename) {
5057 return url.pathname + url.search + url.hash;
5058 }
5059 }
5060 return location;
5061}
5062
5063// Utility method for creating the Request instances for loaders/actions during
5064// client-side navigations and fetches. During SSR we will always have a
5065// Request instance from the static handler (query/queryRoute)
5066function createClientSideRequest(
5067 history: History,
5068 location: string | Location,
5069 signal: AbortSignal,
5070 submission?: Submission
5071): Request {
5072 let url = history.createURL(stripHashFromPath(location)).toString();
5073 let init: RequestInit = { signal };
5074
5075 if (submission && isMutationMethod(submission.formMethod)) {
5076 let { formMethod, formEncType } = submission;
5077 // Didn't think we needed this but it turns out unlike other methods, patch
5078 // won't be properly normalized to uppercase and results in a 405 error.
5079 // See: https://fetch.spec.whatwg.org/#concept-method
5080 init.method = formMethod.toUpperCase();
5081
5082 if (formEncType === "application/json") {
5083 init.headers = new Headers({ "Content-Type": formEncType });
5084 init.body = JSON.stringify(submission.json);
5085 } else if (formEncType === "text/plain") {
5086 // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
5087 init.body = submission.text;
5088 } else if (
5089 formEncType === "application/x-www-form-urlencoded" &&
5090 submission.formData
5091 ) {
5092 // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
5093 init.body = convertFormDataToSearchParams(submission.formData);
5094 } else {
5095 // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
5096 init.body = submission.formData;
5097 }
5098 }
5099
5100 return new Request(url, init);
5101}
5102
5103function convertFormDataToSearchParams(formData: FormData): URLSearchParams {
5104 let searchParams = new URLSearchParams();
5105
5106 for (let [key, value] of formData.entries()) {
5107 // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#converting-an-entry-list-to-a-list-of-name-value-pairs
5108 searchParams.append(key, typeof value === "string" ? value : value.name);
5109 }
5110
5111 return searchParams;
5112}
5113
5114function convertSearchParamsToFormData(
5115 searchParams: URLSearchParams
5116): FormData {
5117 let formData = new FormData();
5118 for (let [key, value] of searchParams.entries()) {
5119 formData.append(key, value);
5120 }
5121 return formData;
5122}
5123
5124function processRouteLoaderData(
5125 matches: AgnosticDataRouteMatch[],
5126 matchesToLoad: AgnosticDataRouteMatch[],
5127 results: DataResult[],
5128 pendingActionResult: PendingActionResult | undefined,
5129 activeDeferreds: Map<string, DeferredData>,
5130 skipLoaderErrorBubbling: boolean
5131): {
5132 loaderData: RouterState["loaderData"];
5133 errors: RouterState["errors"] | null;
5134 statusCode: number;
5135 loaderHeaders: Record<string, Headers>;
5136} {
5137 // Fill in loaderData/errors from our loaders
5138 let loaderData: RouterState["loaderData"] = {};
5139 let errors: RouterState["errors"] | null = null;
5140 let statusCode: number | undefined;
5141 let foundError = false;
5142 let loaderHeaders: Record<string, Headers> = {};
5143 let pendingError =
5144 pendingActionResult && isErrorResult(pendingActionResult[1])
5145 ? pendingActionResult[1].error
5146 : undefined;
5147
5148 // Process loader results into state.loaderData/state.errors
5149 results.forEach((result, index) => {
5150 let id = matchesToLoad[index].route.id;
5151 invariant(
5152 !isRedirectResult(result),
5153 "Cannot handle redirect results in processLoaderData"
5154 );
5155 if (isErrorResult(result)) {
5156 let error = result.error;
5157 // If we have a pending action error, we report it at the highest-route
5158 // that throws a loader error, and then clear it out to indicate that
5159 // it was consumed
5160 if (pendingError !== undefined) {
5161 error = pendingError;
5162 pendingError = undefined;
5163 }
5164
5165 errors = errors || {};
5166
5167 if (skipLoaderErrorBubbling) {
5168 errors[id] = error;
5169 } else {
5170 // Look upwards from the matched route for the closest ancestor error
5171 // boundary, defaulting to the root match. Prefer higher error values
5172 // if lower errors bubble to the same boundary
5173 let boundaryMatch = findNearestBoundary(matches, id);
5174 if (errors[boundaryMatch.route.id] == null) {
5175 errors[boundaryMatch.route.id] = error;
5176 }
5177 }
5178
5179 // Clear our any prior loaderData for the throwing route
5180 loaderData[id] = undefined;
5181
5182 // Once we find our first (highest) error, we set the status code and
5183 // prevent deeper status codes from overriding
5184 if (!foundError) {
5185 foundError = true;
5186 statusCode = isRouteErrorResponse(result.error)
5187 ? result.error.status
5188 : 500;
5189 }
5190 if (result.headers) {
5191 loaderHeaders[id] = result.headers;
5192 }
5193 } else {
5194 if (isDeferredResult(result)) {
5195 activeDeferreds.set(id, result.deferredData);
5196 loaderData[id] = result.deferredData.data;
5197 // Error status codes always override success status codes, but if all
5198 // loaders are successful we take the deepest status code.
5199 if (
5200 result.statusCode != null &&
5201 result.statusCode !== 200 &&
5202 !foundError
5203 ) {
5204 statusCode = result.statusCode;
5205 }
5206 if (result.headers) {
5207 loaderHeaders[id] = result.headers;
5208 }
5209 } else {
5210 loaderData[id] = result.data;
5211 // Error status codes always override success status codes, but if all
5212 // loaders are successful we take the deepest status code.
5213 if (result.statusCode && result.statusCode !== 200 && !foundError) {
5214 statusCode = result.statusCode;
5215 }
5216 if (result.headers) {
5217 loaderHeaders[id] = result.headers;
5218 }
5219 }
5220 }
5221 });
5222
5223 // If we didn't consume the pending action error (i.e., all loaders
5224 // resolved), then consume it here. Also clear out any loaderData for the
5225 // throwing route
5226 if (pendingError !== undefined && pendingActionResult) {
5227 errors = { [pendingActionResult[0]]: pendingError };
5228 loaderData[pendingActionResult[0]] = undefined;
5229 }
5230
5231 return {
5232 loaderData,
5233 errors,
5234 statusCode: statusCode || 200,
5235 loaderHeaders,
5236 };
5237}
5238
5239function processLoaderData(
5240 state: RouterState,
5241 matches: AgnosticDataRouteMatch[],
5242 matchesToLoad: AgnosticDataRouteMatch[],
5243 results: DataResult[],
5244 pendingActionResult: PendingActionResult | undefined,
5245 revalidatingFetchers: RevalidatingFetcher[],
5246 fetcherResults: DataResult[],
5247 activeDeferreds: Map<string, DeferredData>
5248): {
5249 loaderData: RouterState["loaderData"];
5250 errors?: RouterState["errors"];
5251} {
5252 let { loaderData, errors } = processRouteLoaderData(
5253 matches,
5254 matchesToLoad,
5255 results,
5256 pendingActionResult,
5257 activeDeferreds,
5258 false // This method is only called client side so we always want to bubble
5259 );
5260
5261 // Process results from our revalidating fetchers
5262 for (let index = 0; index < revalidatingFetchers.length; index++) {
5263 let { key, match, controller } = revalidatingFetchers[index];
5264 invariant(
5265 fetcherResults !== undefined && fetcherResults[index] !== undefined,
5266 "Did not find corresponding fetcher result"
5267 );
5268 let result = fetcherResults[index];
5269
5270 // Process fetcher non-redirect errors
5271 if (controller && controller.signal.aborted) {
5272 // Nothing to do for aborted fetchers
5273 continue;
5274 } else if (isErrorResult(result)) {
5275 let boundaryMatch = findNearestBoundary(state.matches, match?.route.id);
5276 if (!(errors && errors[boundaryMatch.route.id])) {
5277 errors = {
5278 ...errors,
5279 [boundaryMatch.route.id]: result.error,
5280 };
5281 }
5282 state.fetchers.delete(key);
5283 } else if (isRedirectResult(result)) {
5284 // Should never get here, redirects should get processed above, but we
5285 // keep this to type narrow to a success result in the else
5286 invariant(false, "Unhandled fetcher revalidation redirect");
5287 } else if (isDeferredResult(result)) {
5288 // Should never get here, deferred data should be awaited for fetchers
5289 // in resolveDeferredResults
5290 invariant(false, "Unhandled fetcher deferred data");
5291 } else {
5292 let doneFetcher = getDoneFetcher(result.data);
5293 state.fetchers.set(key, doneFetcher);
5294 }
5295 }
5296
5297 return { loaderData, errors };
5298}
5299
5300function mergeLoaderData(
5301 loaderData: RouteData,
5302 newLoaderData: RouteData,
5303 matches: AgnosticDataRouteMatch[],
5304 errors: RouteData | null | undefined
5305): RouteData {
5306 let mergedLoaderData = { ...newLoaderData };
5307 for (let match of matches) {
5308 let id = match.route.id;
5309 if (newLoaderData.hasOwnProperty(id)) {
5310 if (newLoaderData[id] !== undefined) {
5311 mergedLoaderData[id] = newLoaderData[id];
5312 } else {
5313 // No-op - this is so we ignore existing data if we have a key in the
5314 // incoming object with an undefined value, which is how we unset a prior
5315 // loaderData if we encounter a loader error
5316 }
5317 } else if (loaderData[id] !== undefined && match.route.loader) {
5318 // Preserve existing keys not included in newLoaderData and where a loader
5319 // wasn't removed by HMR
5320 mergedLoaderData[id] = loaderData[id];
5321 }
5322
5323 if (errors && errors.hasOwnProperty(id)) {
5324 // Don't keep any loader data below the boundary
5325 break;
5326 }
5327 }
5328 return mergedLoaderData;
5329}
5330
5331function getActionDataForCommit(
5332 pendingActionResult: PendingActionResult | undefined
5333) {
5334 if (!pendingActionResult) {
5335 return {};
5336 }
5337 return isErrorResult(pendingActionResult[1])
5338 ? {
5339 // Clear out prior actionData on errors
5340 actionData: {},
5341 }
5342 : {
5343 actionData: {
5344 [pendingActionResult[0]]: pendingActionResult[1].data,
5345 },
5346 };
5347}
5348
5349// Find the nearest error boundary, looking upwards from the leaf route (or the
5350// route specified by routeId) for the closest ancestor error boundary,
5351// defaulting to the root match
5352function findNearestBoundary(
5353 matches: AgnosticDataRouteMatch[],
5354 routeId?: string
5355): AgnosticDataRouteMatch {
5356 let eligibleMatches = routeId
5357 ? matches.slice(0, matches.findIndex((m) => m.route.id === routeId) + 1)
5358 : [...matches];
5359 return (
5360 eligibleMatches.reverse().find((m) => m.route.hasErrorBoundary === true) ||
5361 matches[0]
5362 );
5363}
5364
5365function getShortCircuitMatches(routes: AgnosticDataRouteObject[]): {
5366 matches: AgnosticDataRouteMatch[];
5367 route: AgnosticDataRouteObject;
5368} {
5369 // Prefer a root layout route if present, otherwise shim in a route object
5370 let route =
5371 routes.length === 1
5372 ? routes[0]
5373 : routes.find((r) => r.index || !r.path || r.path === "/") || {
5374 id: `__shim-error-route__`,
5375 };
5376
5377 return {
5378 matches: [
5379 {
5380 params: {},
5381 pathname: "",
5382 pathnameBase: "",
5383 route,
5384 },
5385 ],
5386 route,
5387 };
5388}
5389
5390function getInternalRouterError(
5391 status: number,
5392 {
5393 pathname,
5394 routeId,
5395 method,
5396 type,
5397 message,
5398 }: {
5399 pathname?: string;
5400 routeId?: string;
5401 method?: string;
5402 type?: "defer-action" | "invalid-body" | "route-discovery";
5403 message?: string;
5404 } = {}
5405) {
5406 let statusText = "Unknown Server Error";
5407 let errorMessage = "Unknown @remix-run/router error";
5408
5409 if (status === 400) {
5410 statusText = "Bad Request";
5411 if (type === "route-discovery") {
5412 errorMessage =
5413 `Unable to match URL "${pathname}" - the \`unstable_patchRoutesOnMiss()\` ` +
5414 `function threw the following error:\n${message}`;
5415 } else if (method && pathname && routeId) {
5416 errorMessage =
5417 `You made a ${method} request to "${pathname}" but ` +
5418 `did not provide a \`loader\` for route "${routeId}", ` +
5419 `so there is no way to handle the request.`;
5420 } else if (type === "defer-action") {
5421 errorMessage = "defer() is not supported in actions";
5422 } else if (type === "invalid-body") {
5423 errorMessage = "Unable to encode submission body";
5424 }
5425 } else if (status === 403) {
5426 statusText = "Forbidden";
5427 errorMessage = `Route "${routeId}" does not match URL "${pathname}"`;
5428 } else if (status === 404) {
5429 statusText = "Not Found";
5430 errorMessage = `No route matches URL "${pathname}"`;
5431 } else if (status === 405) {
5432 statusText = "Method Not Allowed";
5433 if (method && pathname && routeId) {
5434 errorMessage =
5435 `You made a ${method.toUpperCase()} request to "${pathname}" but ` +
5436 `did not provide an \`action\` for route "${routeId}", ` +
5437 `so there is no way to handle the request.`;
5438 } else if (method) {
5439 errorMessage = `Invalid request method "${method.toUpperCase()}"`;
5440 }
5441 }
5442
5443 return new ErrorResponseImpl(
5444 status || 500,
5445 statusText,
5446 new Error(errorMessage),
5447 true
5448 );
5449}
5450
5451// Find any returned redirect errors, starting from the lowest match
5452function findRedirect(
5453 results: DataResult[]
5454): { result: RedirectResult; idx: number } | undefined {
5455 for (let i = results.length - 1; i >= 0; i--) {
5456 let result = results[i];
5457 if (isRedirectResult(result)) {
5458 return { result, idx: i };
5459 }
5460 }
5461}
5462
5463function stripHashFromPath(path: To) {
5464 let parsedPath = typeof path === "string" ? parsePath(path) : path;
5465 return createPath({ ...parsedPath, hash: "" });
5466}
5467
5468function isHashChangeOnly(a: Location, b: Location): boolean {
5469 if (a.pathname !== b.pathname || a.search !== b.search) {
5470 return false;
5471 }
5472
5473 if (a.hash === "") {
5474 // /page -> /page#hash
5475 return b.hash !== "";
5476 } else if (a.hash === b.hash) {
5477 // /page#hash -> /page#hash
5478 return true;
5479 } else if (b.hash !== "") {
5480 // /page#hash -> /page#other
5481 return true;
5482 }
5483
5484 // If the hash is removed the browser will re-perform a request to the server
5485 // /page#hash -> /page
5486 return false;
5487}
5488
5489function isPromise<T = unknown>(val: unknown): val is Promise<T> {
5490 return typeof val === "object" && val != null && "then" in val;
5491}
5492
5493function isHandlerResult(result: unknown): result is HandlerResult {
5494 return (
5495 result != null &&
5496 typeof result === "object" &&
5497 "type" in result &&
5498 "result" in result &&
5499 (result.type === ResultType.data || result.type === ResultType.error)
5500 );
5501}
5502
5503function isRedirectHandlerResult(result: HandlerResult) {
5504 return (
5505 isResponse(result.result) && redirectStatusCodes.has(result.result.status)
5506 );
5507}
5508
5509function isDeferredResult(result: DataResult): result is DeferredResult {
5510 return result.type === ResultType.deferred;
5511}
5512
5513function isErrorResult(result: DataResult): result is ErrorResult {
5514 return result.type === ResultType.error;
5515}
5516
5517function isRedirectResult(result?: DataResult): result is RedirectResult {
5518 return (result && result.type) === ResultType.redirect;
5519}
5520
5521export function isDataWithResponseInit(
5522 value: any
5523): value is DataWithResponseInit<unknown> {
5524 return (
5525 typeof value === "object" &&
5526 value != null &&
5527 "type" in value &&
5528 "data" in value &&
5529 "init" in value &&
5530 value.type === "DataWithResponseInit"
5531 );
5532}
5533
5534export function isDeferredData(value: any): value is DeferredData {
5535 let deferred: DeferredData = value;
5536 return (
5537 deferred &&
5538 typeof deferred === "object" &&
5539 typeof deferred.data === "object" &&
5540 typeof deferred.subscribe === "function" &&
5541 typeof deferred.cancel === "function" &&
5542 typeof deferred.resolveData === "function"
5543 );
5544}
5545
5546function isResponse(value: any): value is Response {
5547 return (
5548 value != null &&
5549 typeof value.status === "number" &&
5550 typeof value.statusText === "string" &&
5551 typeof value.headers === "object" &&
5552 typeof value.body !== "undefined"
5553 );
5554}
5555
5556function isRedirectResponse(result: any): result is Response {
5557 if (!isResponse(result)) {
5558 return false;
5559 }
5560
5561 let status = result.status;
5562 let location = result.headers.get("Location");
5563 return status >= 300 && status <= 399 && location != null;
5564}
5565
5566function isValidMethod(method: string): method is FormMethod | V7_FormMethod {
5567 return validRequestMethods.has(method.toLowerCase() as FormMethod);
5568}
5569
5570function isMutationMethod(
5571 method: string
5572): method is MutationFormMethod | V7_MutationFormMethod {
5573 return validMutationMethods.has(method.toLowerCase() as MutationFormMethod);
5574}
5575
5576async function resolveDeferredResults(
5577 currentMatches: AgnosticDataRouteMatch[],
5578 matchesToLoad: (AgnosticDataRouteMatch | null)[],
5579 results: DataResult[],
5580 signals: (AbortSignal | null)[],
5581 isFetcher: boolean,
5582 currentLoaderData?: RouteData
5583) {
5584 for (let index = 0; index < results.length; index++) {
5585 let result = results[index];
5586 let match = matchesToLoad[index];
5587 // If we don't have a match, then we can have a deferred result to do
5588 // anything with. This is for revalidating fetchers where the route was
5589 // removed during HMR
5590 if (!match) {
5591 continue;
5592 }
5593
5594 let currentMatch = currentMatches.find(
5595 (m) => m.route.id === match!.route.id
5596 );
5597 let isRevalidatingLoader =
5598 currentMatch != null &&
5599 !isNewRouteInstance(currentMatch, match) &&
5600 (currentLoaderData && currentLoaderData[match.route.id]) !== undefined;
5601
5602 if (isDeferredResult(result) && (isFetcher || isRevalidatingLoader)) {
5603 // Note: we do not have to touch activeDeferreds here since we race them
5604 // against the signal in resolveDeferredData and they'll get aborted
5605 // there if needed
5606 let signal = signals[index];
5607 invariant(
5608 signal,
5609 "Expected an AbortSignal for revalidating fetcher deferred result"
5610 );
5611 await resolveDeferredData(result, signal, isFetcher).then((result) => {
5612 if (result) {
5613 results[index] = result || results[index];
5614 }
5615 });
5616 }
5617 }
5618}
5619
5620async function resolveDeferredData(
5621 result: DeferredResult,
5622 signal: AbortSignal,
5623 unwrap = false
5624): Promise<SuccessResult | ErrorResult | undefined> {
5625 let aborted = await result.deferredData.resolveData(signal);
5626 if (aborted) {
5627 return;
5628 }
5629
5630 if (unwrap) {
5631 try {
5632 return {
5633 type: ResultType.data,
5634 data: result.deferredData.unwrappedData,
5635 };
5636 } catch (e) {
5637 // Handle any TrackedPromise._error values encountered while unwrapping
5638 return {
5639 type: ResultType.error,
5640 error: e,
5641 };
5642 }
5643 }
5644
5645 return {
5646 type: ResultType.data,
5647 data: result.deferredData.data,
5648 };
5649}
5650
5651function hasNakedIndexQuery(search: string): boolean {
5652 return new URLSearchParams(search).getAll("index").some((v) => v === "");
5653}
5654
5655function getTargetMatch(
5656 matches: AgnosticDataRouteMatch[],
5657 location: Location | string
5658) {
5659 let search =
5660 typeof location === "string" ? parsePath(location).search : location.search;
5661 if (
5662 matches[matches.length - 1].route.index &&
5663 hasNakedIndexQuery(search || "")
5664 ) {
5665 // Return the leaf index route when index is present
5666 return matches[matches.length - 1];
5667 }
5668 // Otherwise grab the deepest "path contributing" match (ignoring index and
5669 // pathless layout routes)
5670 let pathMatches = getPathContributingMatches(matches);
5671 return pathMatches[pathMatches.length - 1];
5672}
5673
5674function getSubmissionFromNavigation(
5675 navigation: Navigation
5676): Submission | undefined {
5677 let { formMethod, formAction, formEncType, text, formData, json } =
5678 navigation;
5679 if (!formMethod || !formAction || !formEncType) {
5680 return;
5681 }
5682
5683 if (text != null) {
5684 return {
5685 formMethod,
5686 formAction,
5687 formEncType,
5688 formData: undefined,
5689 json: undefined,
5690 text,
5691 };
5692 } else if (formData != null) {
5693 return {
5694 formMethod,
5695 formAction,
5696 formEncType,
5697 formData,
5698 json: undefined,
5699 text: undefined,
5700 };
5701 } else if (json !== undefined) {
5702 return {
5703 formMethod,
5704 formAction,
5705 formEncType,
5706 formData: undefined,
5707 json,
5708 text: undefined,
5709 };
5710 }
5711}
5712
5713function getLoadingNavigation(
5714 location: Location,
5715 submission?: Submission
5716): NavigationStates["Loading"] {
5717 if (submission) {
5718 let navigation: NavigationStates["Loading"] = {
5719 state: "loading",
5720 location,
5721 formMethod: submission.formMethod,
5722 formAction: submission.formAction,
5723 formEncType: submission.formEncType,
5724 formData: submission.formData,
5725 json: submission.json,
5726 text: submission.text,
5727 };
5728 return navigation;
5729 } else {
5730 let navigation: NavigationStates["Loading"] = {
5731 state: "loading",
5732 location,
5733 formMethod: undefined,
5734 formAction: undefined,
5735 formEncType: undefined,
5736 formData: undefined,
5737 json: undefined,
5738 text: undefined,
5739 };
5740 return navigation;
5741 }
5742}
5743
5744function getSubmittingNavigation(
5745 location: Location,
5746 submission: Submission
5747): NavigationStates["Submitting"] {
5748 let navigation: NavigationStates["Submitting"] = {
5749 state: "submitting",
5750 location,
5751 formMethod: submission.formMethod,
5752 formAction: submission.formAction,
5753 formEncType: submission.formEncType,
5754 formData: submission.formData,
5755 json: submission.json,
5756 text: submission.text,
5757 };
5758 return navigation;
5759}
5760
5761function getLoadingFetcher(
5762 submission?: Submission,
5763 data?: Fetcher["data"]
5764): FetcherStates["Loading"] {
5765 if (submission) {
5766 let fetcher: FetcherStates["Loading"] = {
5767 state: "loading",
5768 formMethod: submission.formMethod,
5769 formAction: submission.formAction,
5770 formEncType: submission.formEncType,
5771 formData: submission.formData,
5772 json: submission.json,
5773 text: submission.text,
5774 data,
5775 };
5776 return fetcher;
5777 } else {
5778 let fetcher: FetcherStates["Loading"] = {
5779 state: "loading",
5780 formMethod: undefined,
5781 formAction: undefined,
5782 formEncType: undefined,
5783 formData: undefined,
5784 json: undefined,
5785 text: undefined,
5786 data,
5787 };
5788 return fetcher;
5789 }
5790}
5791
5792function getSubmittingFetcher(
5793 submission: Submission,
5794 existingFetcher?: Fetcher
5795): FetcherStates["Submitting"] {
5796 let fetcher: FetcherStates["Submitting"] = {
5797 state: "submitting",
5798 formMethod: submission.formMethod,
5799 formAction: submission.formAction,
5800 formEncType: submission.formEncType,
5801 formData: submission.formData,
5802 json: submission.json,
5803 text: submission.text,
5804 data: existingFetcher ? existingFetcher.data : undefined,
5805 };
5806 return fetcher;
5807}
5808
5809function getDoneFetcher(data: Fetcher["data"]): FetcherStates["Idle"] {
5810 let fetcher: FetcherStates["Idle"] = {
5811 state: "idle",
5812 formMethod: undefined,
5813 formAction: undefined,
5814 formEncType: undefined,
5815 formData: undefined,
5816 json: undefined,
5817 text: undefined,
5818 data,
5819 };
5820 return fetcher;
5821}
5822
5823function restoreAppliedTransitions(
5824 _window: Window,
5825 transitions: Map<string, Set<string>>
5826) {
5827 try {
5828 let sessionPositions = _window.sessionStorage.getItem(
5829 TRANSITIONS_STORAGE_KEY
5830 );
5831 if (sessionPositions) {
5832 let json = JSON.parse(sessionPositions);
5833 for (let [k, v] of Object.entries(json || {})) {
5834 if (v && Array.isArray(v)) {
5835 transitions.set(k, new Set(v || []));
5836 }
5837 }
5838 }
5839 } catch (e) {
5840 // no-op, use default empty object
5841 }
5842}
5843
5844function persistAppliedTransitions(
5845 _window: Window,
5846 transitions: Map<string, Set<string>>
5847) {
5848 if (transitions.size > 0) {
5849 let json: Record<string, string[]> = {};
5850 for (let [k, v] of transitions) {
5851 json[k] = [...v];
5852 }
5853 try {
5854 _window.sessionStorage.setItem(
5855 TRANSITIONS_STORAGE_KEY,
5856 JSON.stringify(json)
5857 );
5858 } catch (error) {
5859 warning(
5860 false,
5861 `Failed to save applied view transitions in sessionStorage (${error}).`
5862 );
5863 }
5864 }
5865}
5866//#endregion
Note: See TracBrowser for help on using the repository browser.