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

main
Last change on this file since 0c6b92a was 0c6b92a, checked in by stefan toskovski <stefantoska84@…>, 5 weeks ago

Pred finalna verzija

  • Property mode set to 100644
File size: 178.5 KB
Line 
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 DataStrategyResult,
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 AgnosticPatchRoutesOnNavigationFunction,
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 dataStrategy?: DataStrategyFunction;
395 patchRoutesOnNavigation?: AgnosticPatchRoutesOnNavigationFunction;
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 dataStrategy?: DataStrategyFunction;
426 }
427 ): Promise<StaticHandlerContext | Response>;
428 queryRoute(
429 request: Request,
430 opts?: {
431 routeId?: string;
432 requestContext?: unknown;
433 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 viewTransitionOpts?: ViewTransitionOpts;
452 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 flushSync?: boolean;
479};
480
481// Only allowed for navigations
482type BaseNavigateOptions = BaseNavigateOrFetchOptions & {
483 replace?: boolean;
484 state?: any;
485 fromRouteId?: string;
486 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.dataStrategy || defaultDataStrategy;
801 let patchRoutesOnNavigationImpl = init.patchRoutesOnNavigation;
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 && !patchRoutesOnNavigationImpl) {
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 patchRoutesOnNavigation 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 // `patchRoutesOnNavigation` 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 // If errors exist, don't consider routes below the boundary
894 if (errors) {
895 let idx = initialMatches.findIndex(
896 (m) => errors![m.route.id] !== undefined
897 );
898 initialized = initialMatches
899 .slice(0, idx + 1)
900 .every((m) => !shouldLoadRouteOnHydration(m.route, loaderData, errors));
901 } else {
902 initialized = initialMatches.every(
903 (m) => !shouldLoadRouteOnHydration(m.route, loaderData, errors)
904 );
905 }
906 } else {
907 // Without partial hydration - we're initialized if we were provided any
908 // hydrationData - which is expected to be complete
909 initialized = init.hydrationData != null;
910 }
911
912 let router: Router;
913 let state: RouterState = {
914 historyAction: init.history.action,
915 location: init.history.location,
916 matches: initialMatches,
917 initialized,
918 navigation: IDLE_NAVIGATION,
919 // Don't restore on initial updateState() if we were SSR'd
920 restoreScrollPosition: init.hydrationData != null ? false : null,
921 preventScrollReset: false,
922 revalidation: "idle",
923 loaderData: (init.hydrationData && init.hydrationData.loaderData) || {},
924 actionData: (init.hydrationData && init.hydrationData.actionData) || null,
925 errors: (init.hydrationData && init.hydrationData.errors) || initialErrors,
926 fetchers: new Map(),
927 blockers: new Map(),
928 };
929
930 // -- Stateful internal variables to manage navigations --
931 // Current navigation in progress (to be committed in completeNavigation)
932 let pendingAction: HistoryAction = HistoryAction.Pop;
933
934 // Should the current navigation prevent the scroll reset if scroll cannot
935 // be restored?
936 let pendingPreventScrollReset = false;
937
938 // AbortController for the active navigation
939 let pendingNavigationController: AbortController | null;
940
941 // Should the current navigation enable document.startViewTransition?
942 let pendingViewTransitionEnabled = false;
943
944 // Store applied view transitions so we can apply them on POP
945 let appliedViewTransitions: Map<string, Set<string>> = new Map<
946 string,
947 Set<string>
948 >();
949
950 // Cleanup function for persisting applied transitions to sessionStorage
951 let removePageHideEventListener: (() => void) | null = null;
952
953 // We use this to avoid touching history in completeNavigation if a
954 // revalidation is entirely uninterrupted
955 let isUninterruptedRevalidation = false;
956
957 // Use this internal flag to force revalidation of all loaders:
958 // - submissions (completed or interrupted)
959 // - useRevalidator()
960 // - X-Remix-Revalidate (from redirect)
961 let isRevalidationRequired = false;
962
963 // Use this internal array to capture routes that require revalidation due
964 // to a cancelled deferred on action submission
965 let cancelledDeferredRoutes: string[] = [];
966
967 // Use this internal array to capture fetcher loads that were cancelled by an
968 // action navigation and require revalidation
969 let cancelledFetcherLoads: Set<string> = new Set();
970
971 // AbortControllers for any in-flight fetchers
972 let fetchControllers = new Map<string, AbortController>();
973
974 // Track loads based on the order in which they started
975 let incrementingLoadId = 0;
976
977 // Track the outstanding pending navigation data load to be compared against
978 // the globally incrementing load when a fetcher load lands after a completed
979 // navigation
980 let pendingNavigationLoadId = -1;
981
982 // Fetchers that triggered data reloads as a result of their actions
983 let fetchReloadIds = new Map<string, number>();
984
985 // Fetchers that triggered redirect navigations
986 let fetchRedirectIds = new Set<string>();
987
988 // Most recent href/match for fetcher.load calls for fetchers
989 let fetchLoadMatches = new Map<string, FetchLoadMatch>();
990
991 // Ref-count mounted fetchers so we know when it's ok to clean them up
992 let activeFetchers = new Map<string, number>();
993
994 // Fetchers that have requested a delete when using v7_fetcherPersist,
995 // they'll be officially removed after they return to idle
996 let deletedFetchers = new Set<string>();
997
998 // Store DeferredData instances for active route matches. When a
999 // route loader returns defer() we stick one in here. Then, when a nested
1000 // promise resolves we update loaderData. If a new navigation starts we
1001 // cancel active deferreds for eliminated routes.
1002 let activeDeferreds = new Map<string, DeferredData>();
1003
1004 // Store blocker functions in a separate Map outside of router state since
1005 // we don't need to update UI state if they change
1006 let blockerFunctions = new Map<string, BlockerFunction>();
1007
1008 // Map of pending patchRoutesOnNavigation() promises (keyed by path/matches) so
1009 // that we only kick them off once for a given combo
1010 let pendingPatchRoutes = new Map<
1011 string,
1012 ReturnType<AgnosticPatchRoutesOnNavigationFunction>
1013 >();
1014
1015 // Flag to ignore the next history update, so we can revert the URL change on
1016 // a POP navigation that was blocked by the user without touching router state
1017 let unblockBlockerHistoryUpdate: (() => void) | undefined = undefined;
1018
1019 // Initialize the router, all side effects should be kicked off from here.
1020 // Implemented as a Fluent API for ease of:
1021 // let router = createRouter(init).initialize();
1022 function initialize() {
1023 // If history informs us of a POP navigation, start the navigation but do not update
1024 // state. We'll update our own state once the navigation completes
1025 unlistenHistory = init.history.listen(
1026 ({ action: historyAction, location, delta }) => {
1027 // Ignore this event if it was just us resetting the URL from a
1028 // blocked POP navigation
1029 if (unblockBlockerHistoryUpdate) {
1030 unblockBlockerHistoryUpdate();
1031 unblockBlockerHistoryUpdate = undefined;
1032 return;
1033 }
1034
1035 warning(
1036 blockerFunctions.size === 0 || delta != null,
1037 "You are trying to use a blocker on a POP navigation to a location " +
1038 "that was not created by @remix-run/router. This will fail silently in " +
1039 "production. This can happen if you are navigating outside the router " +
1040 "via `window.history.pushState`/`window.location.hash` instead of using " +
1041 "router navigation APIs. This can also happen if you are using " +
1042 "createHashRouter and the user manually changes the URL."
1043 );
1044
1045 let blockerKey = shouldBlockNavigation({
1046 currentLocation: state.location,
1047 nextLocation: location,
1048 historyAction,
1049 });
1050
1051 if (blockerKey && delta != null) {
1052 // Restore the URL to match the current UI, but don't update router state
1053 let nextHistoryUpdatePromise = new Promise<void>((resolve) => {
1054 unblockBlockerHistoryUpdate = resolve;
1055 });
1056 init.history.go(delta * -1);
1057
1058 // Put the blocker into a blocked state
1059 updateBlocker(blockerKey, {
1060 state: "blocked",
1061 location,
1062 proceed() {
1063 updateBlocker(blockerKey!, {
1064 state: "proceeding",
1065 proceed: undefined,
1066 reset: undefined,
1067 location,
1068 });
1069 // Re-do the same POP navigation we just blocked, after the url
1070 // restoration is also complete. See:
1071 // https://github.com/remix-run/react-router/issues/11613
1072 nextHistoryUpdatePromise.then(() => init.history.go(delta));
1073 },
1074 reset() {
1075 let blockers = new Map(state.blockers);
1076 blockers.set(blockerKey!, IDLE_BLOCKER);
1077 updateState({ blockers });
1078 },
1079 });
1080 return;
1081 }
1082
1083 return startNavigation(historyAction, location);
1084 }
1085 );
1086
1087 if (isBrowser) {
1088 // FIXME: This feels gross. How can we cleanup the lines between
1089 // scrollRestoration/appliedTransitions persistance?
1090 restoreAppliedTransitions(routerWindow, appliedViewTransitions);
1091 let _saveAppliedTransitions = () =>
1092 persistAppliedTransitions(routerWindow, appliedViewTransitions);
1093 routerWindow.addEventListener("pagehide", _saveAppliedTransitions);
1094 removePageHideEventListener = () =>
1095 routerWindow.removeEventListener("pagehide", _saveAppliedTransitions);
1096 }
1097
1098 // Kick off initial data load if needed. Use Pop to avoid modifying history
1099 // Note we don't do any handling of lazy here. For SPA's it'll get handled
1100 // in the normal navigation flow. For SSR it's expected that lazy modules are
1101 // resolved prior to router creation since we can't go into a fallbackElement
1102 // UI for SSR'd apps
1103 if (!state.initialized) {
1104 startNavigation(HistoryAction.Pop, state.location, {
1105 initialHydration: true,
1106 });
1107 }
1108
1109 return router;
1110 }
1111
1112 // Clean up a router and it's side effects
1113 function dispose() {
1114 if (unlistenHistory) {
1115 unlistenHistory();
1116 }
1117 if (removePageHideEventListener) {
1118 removePageHideEventListener();
1119 }
1120 subscribers.clear();
1121 pendingNavigationController && pendingNavigationController.abort();
1122 state.fetchers.forEach((_, key) => deleteFetcher(key));
1123 state.blockers.forEach((_, key) => deleteBlocker(key));
1124 }
1125
1126 // Subscribe to state updates for the router
1127 function subscribe(fn: RouterSubscriber) {
1128 subscribers.add(fn);
1129 return () => subscribers.delete(fn);
1130 }
1131
1132 // Update our state and notify the calling context of the change
1133 function updateState(
1134 newState: Partial<RouterState>,
1135 opts: {
1136 flushSync?: boolean;
1137 viewTransitionOpts?: ViewTransitionOpts;
1138 } = {}
1139 ): void {
1140 state = {
1141 ...state,
1142 ...newState,
1143 };
1144
1145 // Prep fetcher cleanup so we can tell the UI which fetcher data entries
1146 // can be removed
1147 let completedFetchers: string[] = [];
1148 let deletedFetchersKeys: string[] = [];
1149
1150 if (future.v7_fetcherPersist) {
1151 state.fetchers.forEach((fetcher, key) => {
1152 if (fetcher.state === "idle") {
1153 if (deletedFetchers.has(key)) {
1154 // Unmounted from the UI and can be totally removed
1155 deletedFetchersKeys.push(key);
1156 } else {
1157 // Returned to idle but still mounted in the UI, so semi-remains for
1158 // revalidations and such
1159 completedFetchers.push(key);
1160 }
1161 }
1162 });
1163 }
1164
1165 // Iterate over a local copy so that if flushSync is used and we end up
1166 // removing and adding a new subscriber due to the useCallback dependencies,
1167 // we don't get ourselves into a loop calling the new subscriber immediately
1168 [...subscribers].forEach((subscriber) =>
1169 subscriber(state, {
1170 deletedFetchers: deletedFetchersKeys,
1171 viewTransitionOpts: opts.viewTransitionOpts,
1172 flushSync: opts.flushSync === true,
1173 })
1174 );
1175
1176 // Remove idle fetchers from state since we only care about in-flight fetchers.
1177 if (future.v7_fetcherPersist) {
1178 completedFetchers.forEach((key) => state.fetchers.delete(key));
1179 deletedFetchersKeys.forEach((key) => deleteFetcher(key));
1180 }
1181 }
1182
1183 // Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION
1184 // and setting state.[historyAction/location/matches] to the new route.
1185 // - Location is a required param
1186 // - Navigation will always be set to IDLE_NAVIGATION
1187 // - Can pass any other state in newState
1188 function completeNavigation(
1189 location: Location,
1190 newState: Partial<Omit<RouterState, "action" | "location" | "navigation">>,
1191 { flushSync }: { flushSync?: boolean } = {}
1192 ): void {
1193 // Deduce if we're in a loading/actionReload state:
1194 // - We have committed actionData in the store
1195 // - The current navigation was a mutation submission
1196 // - We're past the submitting state and into the loading state
1197 // - The location being loaded is not the result of a redirect
1198 let isActionReload =
1199 state.actionData != null &&
1200 state.navigation.formMethod != null &&
1201 isMutationMethod(state.navigation.formMethod) &&
1202 state.navigation.state === "loading" &&
1203 location.state?._isRedirect !== true;
1204
1205 let actionData: RouteData | null;
1206 if (newState.actionData) {
1207 if (Object.keys(newState.actionData).length > 0) {
1208 actionData = newState.actionData;
1209 } else {
1210 // Empty actionData -> clear prior actionData due to an action error
1211 actionData = null;
1212 }
1213 } else if (isActionReload) {
1214 // Keep the current data if we're wrapping up the action reload
1215 actionData = state.actionData;
1216 } else {
1217 // Clear actionData on any other completed navigations
1218 actionData = null;
1219 }
1220
1221 // Always preserve any existing loaderData from re-used routes
1222 let loaderData = newState.loaderData
1223 ? mergeLoaderData(
1224 state.loaderData,
1225 newState.loaderData,
1226 newState.matches || [],
1227 newState.errors
1228 )
1229 : state.loaderData;
1230
1231 // On a successful navigation we can assume we got through all blockers
1232 // so we can start fresh
1233 let blockers = state.blockers;
1234 if (blockers.size > 0) {
1235 blockers = new Map(blockers);
1236 blockers.forEach((_, k) => blockers.set(k, IDLE_BLOCKER));
1237 }
1238
1239 // Always respect the user flag. Otherwise don't reset on mutation
1240 // submission navigations unless they redirect
1241 let preventScrollReset =
1242 pendingPreventScrollReset === true ||
1243 (state.navigation.formMethod != null &&
1244 isMutationMethod(state.navigation.formMethod) &&
1245 location.state?._isRedirect !== true);
1246
1247 // Commit any in-flight routes at the end of the HMR revalidation "navigation"
1248 if (inFlightDataRoutes) {
1249 dataRoutes = inFlightDataRoutes;
1250 inFlightDataRoutes = undefined;
1251 }
1252
1253 if (isUninterruptedRevalidation) {
1254 // If this was an uninterrupted revalidation then do not touch history
1255 } else if (pendingAction === HistoryAction.Pop) {
1256 // Do nothing for POP - URL has already been updated
1257 } else if (pendingAction === HistoryAction.Push) {
1258 init.history.push(location, location.state);
1259 } else if (pendingAction === HistoryAction.Replace) {
1260 init.history.replace(location, location.state);
1261 }
1262
1263 let viewTransitionOpts: ViewTransitionOpts | undefined;
1264
1265 // On POP, enable transitions if they were enabled on the original navigation
1266 if (pendingAction === HistoryAction.Pop) {
1267 // Forward takes precedence so they behave like the original navigation
1268 let priorPaths = appliedViewTransitions.get(state.location.pathname);
1269 if (priorPaths && priorPaths.has(location.pathname)) {
1270 viewTransitionOpts = {
1271 currentLocation: state.location,
1272 nextLocation: location,
1273 };
1274 } else if (appliedViewTransitions.has(location.pathname)) {
1275 // If we don't have a previous forward nav, assume we're popping back to
1276 // the new location and enable if that location previously enabled
1277 viewTransitionOpts = {
1278 currentLocation: location,
1279 nextLocation: state.location,
1280 };
1281 }
1282 } else if (pendingViewTransitionEnabled) {
1283 // Store the applied transition on PUSH/REPLACE
1284 let toPaths = appliedViewTransitions.get(state.location.pathname);
1285 if (toPaths) {
1286 toPaths.add(location.pathname);
1287 } else {
1288 toPaths = new Set<string>([location.pathname]);
1289 appliedViewTransitions.set(state.location.pathname, toPaths);
1290 }
1291 viewTransitionOpts = {
1292 currentLocation: state.location,
1293 nextLocation: location,
1294 };
1295 }
1296
1297 updateState(
1298 {
1299 ...newState, // matches, errors, fetchers go through as-is
1300 actionData,
1301 loaderData,
1302 historyAction: pendingAction,
1303 location,
1304 initialized: true,
1305 navigation: IDLE_NAVIGATION,
1306 revalidation: "idle",
1307 restoreScrollPosition: getSavedScrollPosition(
1308 location,
1309 newState.matches || state.matches
1310 ),
1311 preventScrollReset,
1312 blockers,
1313 },
1314 {
1315 viewTransitionOpts,
1316 flushSync: flushSync === true,
1317 }
1318 );
1319
1320 // Reset stateful navigation vars
1321 pendingAction = HistoryAction.Pop;
1322 pendingPreventScrollReset = false;
1323 pendingViewTransitionEnabled = false;
1324 isUninterruptedRevalidation = false;
1325 isRevalidationRequired = false;
1326 cancelledDeferredRoutes = [];
1327 }
1328
1329 // Trigger a navigation event, which can either be a numerical POP or a PUSH
1330 // replace with an optional submission
1331 async function navigate(
1332 to: number | To | null,
1333 opts?: RouterNavigateOptions
1334 ): Promise<void> {
1335 if (typeof to === "number") {
1336 init.history.go(to);
1337 return;
1338 }
1339
1340 let normalizedPath = normalizeTo(
1341 state.location,
1342 state.matches,
1343 basename,
1344 future.v7_prependBasename,
1345 to,
1346 future.v7_relativeSplatPath,
1347 opts?.fromRouteId,
1348 opts?.relative
1349 );
1350 let { path, submission, error } = normalizeNavigateOptions(
1351 future.v7_normalizeFormMethod,
1352 false,
1353 normalizedPath,
1354 opts
1355 );
1356
1357 let currentLocation = state.location;
1358 let nextLocation = createLocation(state.location, path, opts && opts.state);
1359
1360 // When using navigate as a PUSH/REPLACE we aren't reading an already-encoded
1361 // URL from window.location, so we need to encode it here so the behavior
1362 // remains the same as POP and non-data-router usages. new URL() does all
1363 // the same encoding we'd get from a history.pushState/window.location read
1364 // without having to touch history
1365 nextLocation = {
1366 ...nextLocation,
1367 ...init.history.encodeLocation(nextLocation),
1368 };
1369
1370 let userReplace = opts && opts.replace != null ? opts.replace : undefined;
1371
1372 let historyAction = HistoryAction.Push;
1373
1374 if (userReplace === true) {
1375 historyAction = HistoryAction.Replace;
1376 } else if (userReplace === false) {
1377 // no-op
1378 } else if (
1379 submission != null &&
1380 isMutationMethod(submission.formMethod) &&
1381 submission.formAction === state.location.pathname + state.location.search
1382 ) {
1383 // By default on submissions to the current location we REPLACE so that
1384 // users don't have to double-click the back button to get to the prior
1385 // location. If the user redirects to a different location from the
1386 // action/loader this will be ignored and the redirect will be a PUSH
1387 historyAction = HistoryAction.Replace;
1388 }
1389
1390 let preventScrollReset =
1391 opts && "preventScrollReset" in opts
1392 ? opts.preventScrollReset === true
1393 : undefined;
1394
1395 let flushSync = (opts && opts.flushSync) === true;
1396
1397 let blockerKey = shouldBlockNavigation({
1398 currentLocation,
1399 nextLocation,
1400 historyAction,
1401 });
1402
1403 if (blockerKey) {
1404 // Put the blocker into a blocked state
1405 updateBlocker(blockerKey, {
1406 state: "blocked",
1407 location: nextLocation,
1408 proceed() {
1409 updateBlocker(blockerKey!, {
1410 state: "proceeding",
1411 proceed: undefined,
1412 reset: undefined,
1413 location: nextLocation,
1414 });
1415 // Send the same navigation through
1416 navigate(to, opts);
1417 },
1418 reset() {
1419 let blockers = new Map(state.blockers);
1420 blockers.set(blockerKey!, IDLE_BLOCKER);
1421 updateState({ blockers });
1422 },
1423 });
1424 return;
1425 }
1426
1427 return await startNavigation(historyAction, nextLocation, {
1428 submission,
1429 // Send through the formData serialization error if we have one so we can
1430 // render at the right error boundary after we match routes
1431 pendingError: error,
1432 preventScrollReset,
1433 replace: opts && opts.replace,
1434 enableViewTransition: opts && opts.viewTransition,
1435 flushSync,
1436 });
1437 }
1438
1439 // Revalidate all current loaders. If a navigation is in progress or if this
1440 // is interrupted by a navigation, allow this to "succeed" by calling all
1441 // loaders during the next loader round
1442 function revalidate() {
1443 interruptActiveLoads();
1444 updateState({ revalidation: "loading" });
1445
1446 // If we're currently submitting an action, we don't need to start a new
1447 // navigation, we'll just let the follow up loader execution call all loaders
1448 if (state.navigation.state === "submitting") {
1449 return;
1450 }
1451
1452 // If we're currently in an idle state, start a new navigation for the current
1453 // action/location and mark it as uninterrupted, which will skip the history
1454 // update in completeNavigation
1455 if (state.navigation.state === "idle") {
1456 startNavigation(state.historyAction, state.location, {
1457 startUninterruptedRevalidation: true,
1458 });
1459 return;
1460 }
1461
1462 // Otherwise, if we're currently in a loading state, just start a new
1463 // navigation to the navigation.location but do not trigger an uninterrupted
1464 // revalidation so that history correctly updates once the navigation completes
1465 startNavigation(
1466 pendingAction || state.historyAction,
1467 state.navigation.location,
1468 {
1469 overrideNavigation: state.navigation,
1470 // Proxy through any rending view transition
1471 enableViewTransition: pendingViewTransitionEnabled === true,
1472 }
1473 );
1474 }
1475
1476 // Start a navigation to the given action/location. Can optionally provide a
1477 // overrideNavigation which will override the normalLoad in the case of a redirect
1478 // navigation
1479 async function startNavigation(
1480 historyAction: HistoryAction,
1481 location: Location,
1482 opts?: {
1483 initialHydration?: boolean;
1484 submission?: Submission;
1485 fetcherSubmission?: Submission;
1486 overrideNavigation?: Navigation;
1487 pendingError?: ErrorResponseImpl;
1488 startUninterruptedRevalidation?: boolean;
1489 preventScrollReset?: boolean;
1490 replace?: boolean;
1491 enableViewTransition?: boolean;
1492 flushSync?: boolean;
1493 }
1494 ): Promise<void> {
1495 // Abort any in-progress navigations and start a new one. Unset any ongoing
1496 // uninterrupted revalidations unless told otherwise, since we want this
1497 // new navigation to update history normally
1498 pendingNavigationController && pendingNavigationController.abort();
1499 pendingNavigationController = null;
1500 pendingAction = historyAction;
1501 isUninterruptedRevalidation =
1502 (opts && opts.startUninterruptedRevalidation) === true;
1503
1504 // Save the current scroll position every time we start a new navigation,
1505 // and track whether we should reset scroll on completion
1506 saveScrollPosition(state.location, state.matches);
1507 pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
1508
1509 pendingViewTransitionEnabled = (opts && opts.enableViewTransition) === true;
1510
1511 let routesToUse = inFlightDataRoutes || dataRoutes;
1512 let loadingNavigation = opts && opts.overrideNavigation;
1513 let matches = matchRoutes(routesToUse, location, basename);
1514 let flushSync = (opts && opts.flushSync) === true;
1515
1516 let fogOfWar = checkFogOfWar(matches, routesToUse, location.pathname);
1517 if (fogOfWar.active && fogOfWar.matches) {
1518 matches = fogOfWar.matches;
1519 }
1520
1521 // Short circuit with a 404 on the root error boundary if we match nothing
1522 if (!matches) {
1523 let { error, notFoundMatches, route } = handleNavigational404(
1524 location.pathname
1525 );
1526 completeNavigation(
1527 location,
1528 {
1529 matches: notFoundMatches,
1530 loaderData: {},
1531 errors: {
1532 [route.id]: error,
1533 },
1534 },
1535 { flushSync }
1536 );
1537 return;
1538 }
1539
1540 // Short circuit if it's only a hash change and not a revalidation or
1541 // mutation submission.
1542 //
1543 // Ignore on initial page loads because since the initial hydration will always
1544 // be "same hash". For example, on /page#hash and submit a <Form method="post">
1545 // which will default to a navigation to /page
1546 if (
1547 state.initialized &&
1548 !isRevalidationRequired &&
1549 isHashChangeOnly(state.location, location) &&
1550 !(opts && opts.submission && isMutationMethod(opts.submission.formMethod))
1551 ) {
1552 completeNavigation(location, { matches }, { flushSync });
1553 return;
1554 }
1555
1556 // Create a controller/Request for this navigation
1557 pendingNavigationController = new AbortController();
1558 let request = createClientSideRequest(
1559 init.history,
1560 location,
1561 pendingNavigationController.signal,
1562 opts && opts.submission
1563 );
1564 let pendingActionResult: PendingActionResult | undefined;
1565
1566 if (opts && opts.pendingError) {
1567 // If we have a pendingError, it means the user attempted a GET submission
1568 // with binary FormData so assign here and skip to handleLoaders. That
1569 // way we handle calling loaders above the boundary etc. It's not really
1570 // different from an actionError in that sense.
1571 pendingActionResult = [
1572 findNearestBoundary(matches).route.id,
1573 { type: ResultType.error, error: opts.pendingError },
1574 ];
1575 } else if (
1576 opts &&
1577 opts.submission &&
1578 isMutationMethod(opts.submission.formMethod)
1579 ) {
1580 // Call action if we received an action submission
1581 let actionResult = await handleAction(
1582 request,
1583 location,
1584 opts.submission,
1585 matches,
1586 fogOfWar.active,
1587 { replace: opts.replace, flushSync }
1588 );
1589
1590 if (actionResult.shortCircuited) {
1591 return;
1592 }
1593
1594 // If we received a 404 from handleAction, it's because we couldn't lazily
1595 // discover the destination route so we don't want to call loaders
1596 if (actionResult.pendingActionResult) {
1597 let [routeId, result] = actionResult.pendingActionResult;
1598 if (
1599 isErrorResult(result) &&
1600 isRouteErrorResponse(result.error) &&
1601 result.error.status === 404
1602 ) {
1603 pendingNavigationController = null;
1604
1605 completeNavigation(location, {
1606 matches: actionResult.matches,
1607 loaderData: {},
1608 errors: {
1609 [routeId]: result.error,
1610 },
1611 });
1612 return;
1613 }
1614 }
1615
1616 matches = actionResult.matches || matches;
1617 pendingActionResult = actionResult.pendingActionResult;
1618 loadingNavigation = getLoadingNavigation(location, opts.submission);
1619 flushSync = false;
1620 // No need to do fog of war matching again on loader execution
1621 fogOfWar.active = false;
1622
1623 // Create a GET request for the loaders
1624 request = createClientSideRequest(
1625 init.history,
1626 request.url,
1627 request.signal
1628 );
1629 }
1630
1631 // Call loaders
1632 let {
1633 shortCircuited,
1634 matches: updatedMatches,
1635 loaderData,
1636 errors,
1637 } = await handleLoaders(
1638 request,
1639 location,
1640 matches,
1641 fogOfWar.active,
1642 loadingNavigation,
1643 opts && opts.submission,
1644 opts && opts.fetcherSubmission,
1645 opts && opts.replace,
1646 opts && opts.initialHydration === true,
1647 flushSync,
1648 pendingActionResult
1649 );
1650
1651 if (shortCircuited) {
1652 return;
1653 }
1654
1655 // Clean up now that the action/loaders have completed. Don't clean up if
1656 // we short circuited because pendingNavigationController will have already
1657 // been assigned to a new controller for the next navigation
1658 pendingNavigationController = null;
1659
1660 completeNavigation(location, {
1661 matches: updatedMatches || matches,
1662 ...getActionDataForCommit(pendingActionResult),
1663 loaderData,
1664 errors,
1665 });
1666 }
1667
1668 // Call the action matched by the leaf route for this navigation and handle
1669 // redirects/errors
1670 async function handleAction(
1671 request: Request,
1672 location: Location,
1673 submission: Submission,
1674 matches: AgnosticDataRouteMatch[],
1675 isFogOfWar: boolean,
1676 opts: { replace?: boolean; flushSync?: boolean } = {}
1677 ): Promise<HandleActionResult> {
1678 interruptActiveLoads();
1679
1680 // Put us in a submitting state
1681 let navigation = getSubmittingNavigation(location, submission);
1682 updateState({ navigation }, { flushSync: opts.flushSync === true });
1683
1684 if (isFogOfWar) {
1685 let discoverResult = await discoverRoutes(
1686 matches,
1687 location.pathname,
1688 request.signal
1689 );
1690 if (discoverResult.type === "aborted") {
1691 return { shortCircuited: true };
1692 } else if (discoverResult.type === "error") {
1693 let boundaryId = findNearestBoundary(discoverResult.partialMatches)
1694 .route.id;
1695 return {
1696 matches: discoverResult.partialMatches,
1697 pendingActionResult: [
1698 boundaryId,
1699 {
1700 type: ResultType.error,
1701 error: discoverResult.error,
1702 },
1703 ],
1704 };
1705 } else if (!discoverResult.matches) {
1706 let { notFoundMatches, error, route } = handleNavigational404(
1707 location.pathname
1708 );
1709 return {
1710 matches: notFoundMatches,
1711 pendingActionResult: [
1712 route.id,
1713 {
1714 type: ResultType.error,
1715 error,
1716 },
1717 ],
1718 };
1719 } else {
1720 matches = discoverResult.matches;
1721 }
1722 }
1723
1724 // Call our action and get the result
1725 let result: DataResult;
1726 let actionMatch = getTargetMatch(matches, location);
1727
1728 if (!actionMatch.route.action && !actionMatch.route.lazy) {
1729 result = {
1730 type: ResultType.error,
1731 error: getInternalRouterError(405, {
1732 method: request.method,
1733 pathname: location.pathname,
1734 routeId: actionMatch.route.id,
1735 }),
1736 };
1737 } else {
1738 let results = await callDataStrategy(
1739 "action",
1740 state,
1741 request,
1742 [actionMatch],
1743 matches,
1744 null
1745 );
1746 result = results[actionMatch.route.id];
1747
1748 if (request.signal.aborted) {
1749 return { shortCircuited: true };
1750 }
1751 }
1752
1753 if (isRedirectResult(result)) {
1754 let replace: boolean;
1755 if (opts && opts.replace != null) {
1756 replace = opts.replace;
1757 } else {
1758 // If the user didn't explicity indicate replace behavior, replace if
1759 // we redirected to the exact same location we're currently at to avoid
1760 // double back-buttons
1761 let location = normalizeRedirectLocation(
1762 result.response.headers.get("Location")!,
1763 new URL(request.url),
1764 basename
1765 );
1766 replace = location === state.location.pathname + state.location.search;
1767 }
1768 await startRedirectNavigation(request, result, true, {
1769 submission,
1770 replace,
1771 });
1772 return { shortCircuited: true };
1773 }
1774
1775 if (isDeferredResult(result)) {
1776 throw getInternalRouterError(400, { type: "defer-action" });
1777 }
1778
1779 if (isErrorResult(result)) {
1780 // Store off the pending error - we use it to determine which loaders
1781 // to call and will commit it when we complete the navigation
1782 let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
1783
1784 // By default, all submissions to the current location are REPLACE
1785 // navigations, but if the action threw an error that'll be rendered in
1786 // an errorElement, we fall back to PUSH so that the user can use the
1787 // back button to get back to the pre-submission form location to try
1788 // again
1789 if ((opts && opts.replace) !== true) {
1790 pendingAction = HistoryAction.Push;
1791 }
1792
1793 return {
1794 matches,
1795 pendingActionResult: [boundaryMatch.route.id, result],
1796 };
1797 }
1798
1799 return {
1800 matches,
1801 pendingActionResult: [actionMatch.route.id, result],
1802 };
1803 }
1804
1805 // Call all applicable loaders for the given matches, handling redirects,
1806 // errors, etc.
1807 async function handleLoaders(
1808 request: Request,
1809 location: Location,
1810 matches: AgnosticDataRouteMatch[],
1811 isFogOfWar: boolean,
1812 overrideNavigation?: Navigation,
1813 submission?: Submission,
1814 fetcherSubmission?: Submission,
1815 replace?: boolean,
1816 initialHydration?: boolean,
1817 flushSync?: boolean,
1818 pendingActionResult?: PendingActionResult
1819 ): Promise<HandleLoadersResult> {
1820 // Figure out the right navigation we want to use for data loading
1821 let loadingNavigation =
1822 overrideNavigation || getLoadingNavigation(location, submission);
1823
1824 // If this was a redirect from an action we don't have a "submission" but
1825 // we have it on the loading navigation so use that if available
1826 let activeSubmission =
1827 submission ||
1828 fetcherSubmission ||
1829 getSubmissionFromNavigation(loadingNavigation);
1830
1831 // If this is an uninterrupted revalidation, we remain in our current idle
1832 // state. If not, we need to switch to our loading state and load data,
1833 // preserving any new action data or existing action data (in the case of
1834 // a revalidation interrupting an actionReload)
1835 // If we have partialHydration enabled, then don't update the state for the
1836 // initial data load since it's not a "navigation"
1837 let shouldUpdateNavigationState =
1838 !isUninterruptedRevalidation &&
1839 (!future.v7_partialHydration || !initialHydration);
1840
1841 // When fog of war is enabled, we enter our `loading` state earlier so we
1842 // can discover new routes during the `loading` state. We skip this if
1843 // we've already run actions since we would have done our matching already.
1844 // If the children() function threw then, we want to proceed with the
1845 // partial matches it discovered.
1846 if (isFogOfWar) {
1847 if (shouldUpdateNavigationState) {
1848 let actionData = getUpdatedActionData(pendingActionResult);
1849 updateState(
1850 {
1851 navigation: loadingNavigation,
1852 ...(actionData !== undefined ? { actionData } : {}),
1853 },
1854 {
1855 flushSync,
1856 }
1857 );
1858 }
1859
1860 let discoverResult = await discoverRoutes(
1861 matches,
1862 location.pathname,
1863 request.signal
1864 );
1865
1866 if (discoverResult.type === "aborted") {
1867 return { shortCircuited: true };
1868 } else if (discoverResult.type === "error") {
1869 let boundaryId = findNearestBoundary(discoverResult.partialMatches)
1870 .route.id;
1871 return {
1872 matches: discoverResult.partialMatches,
1873 loaderData: {},
1874 errors: {
1875 [boundaryId]: discoverResult.error,
1876 },
1877 };
1878 } else if (!discoverResult.matches) {
1879 let { error, notFoundMatches, route } = handleNavigational404(
1880 location.pathname
1881 );
1882 return {
1883 matches: notFoundMatches,
1884 loaderData: {},
1885 errors: {
1886 [route.id]: error,
1887 },
1888 };
1889 } else {
1890 matches = discoverResult.matches;
1891 }
1892 }
1893
1894 let routesToUse = inFlightDataRoutes || dataRoutes;
1895 let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(
1896 init.history,
1897 state,
1898 matches,
1899 activeSubmission,
1900 location,
1901 future.v7_partialHydration && initialHydration === true,
1902 future.v7_skipActionErrorRevalidation,
1903 isRevalidationRequired,
1904 cancelledDeferredRoutes,
1905 cancelledFetcherLoads,
1906 deletedFetchers,
1907 fetchLoadMatches,
1908 fetchRedirectIds,
1909 routesToUse,
1910 basename,
1911 pendingActionResult
1912 );
1913
1914 // Cancel pending deferreds for no-longer-matched routes or routes we're
1915 // about to reload. Note that if this is an action reload we would have
1916 // already cancelled all pending deferreds so this would be a no-op
1917 cancelActiveDeferreds(
1918 (routeId) =>
1919 !(matches && matches.some((m) => m.route.id === routeId)) ||
1920 (matchesToLoad && matchesToLoad.some((m) => m.route.id === routeId))
1921 );
1922
1923 pendingNavigationLoadId = ++incrementingLoadId;
1924
1925 // Short circuit if we have no loaders to run
1926 if (matchesToLoad.length === 0 && revalidatingFetchers.length === 0) {
1927 let updatedFetchers = markFetchRedirectsDone();
1928 completeNavigation(
1929 location,
1930 {
1931 matches,
1932 loaderData: {},
1933 // Commit pending error if we're short circuiting
1934 errors:
1935 pendingActionResult && isErrorResult(pendingActionResult[1])
1936 ? { [pendingActionResult[0]]: pendingActionResult[1].error }
1937 : null,
1938 ...getActionDataForCommit(pendingActionResult),
1939 ...(updatedFetchers ? { fetchers: new Map(state.fetchers) } : {}),
1940 },
1941 { flushSync }
1942 );
1943 return { shortCircuited: true };
1944 }
1945
1946 if (shouldUpdateNavigationState) {
1947 let updates: Partial<RouterState> = {};
1948 if (!isFogOfWar) {
1949 // Only update navigation/actionNData if we didn't already do it above
1950 updates.navigation = loadingNavigation;
1951 let actionData = getUpdatedActionData(pendingActionResult);
1952 if (actionData !== undefined) {
1953 updates.actionData = actionData;
1954 }
1955 }
1956 if (revalidatingFetchers.length > 0) {
1957 updates.fetchers = getUpdatedRevalidatingFetchers(revalidatingFetchers);
1958 }
1959 updateState(updates, { flushSync });
1960 }
1961
1962 revalidatingFetchers.forEach((rf) => {
1963 abortFetcher(rf.key);
1964 if (rf.controller) {
1965 // Fetchers use an independent AbortController so that aborting a fetcher
1966 // (via deleteFetcher) does not abort the triggering navigation that
1967 // triggered the revalidation
1968 fetchControllers.set(rf.key, rf.controller);
1969 }
1970 });
1971
1972 // Proxy navigation abort through to revalidation fetchers
1973 let abortPendingFetchRevalidations = () =>
1974 revalidatingFetchers.forEach((f) => abortFetcher(f.key));
1975 if (pendingNavigationController) {
1976 pendingNavigationController.signal.addEventListener(
1977 "abort",
1978 abortPendingFetchRevalidations
1979 );
1980 }
1981
1982 let { loaderResults, fetcherResults } =
1983 await callLoadersAndMaybeResolveData(
1984 state,
1985 matches,
1986 matchesToLoad,
1987 revalidatingFetchers,
1988 request
1989 );
1990
1991 if (request.signal.aborted) {
1992 return { shortCircuited: true };
1993 }
1994
1995 // Clean up _after_ loaders have completed. Don't clean up if we short
1996 // circuited because fetchControllers would have been aborted and
1997 // reassigned to new controllers for the next navigation
1998 if (pendingNavigationController) {
1999 pendingNavigationController.signal.removeEventListener(
2000 "abort",
2001 abortPendingFetchRevalidations
2002 );
2003 }
2004
2005 revalidatingFetchers.forEach((rf) => fetchControllers.delete(rf.key));
2006
2007 // If any loaders returned a redirect Response, start a new REPLACE navigation
2008 let redirect = findRedirect(loaderResults);
2009 if (redirect) {
2010 await startRedirectNavigation(request, redirect.result, true, {
2011 replace,
2012 });
2013 return { shortCircuited: true };
2014 }
2015
2016 redirect = findRedirect(fetcherResults);
2017 if (redirect) {
2018 // If this redirect came from a fetcher make sure we mark it in
2019 // fetchRedirectIds so it doesn't get revalidated on the next set of
2020 // loader executions
2021 fetchRedirectIds.add(redirect.key);
2022 await startRedirectNavigation(request, redirect.result, true, {
2023 replace,
2024 });
2025 return { shortCircuited: true };
2026 }
2027
2028 // Process and commit output from loaders
2029 let { loaderData, errors } = processLoaderData(
2030 state,
2031 matches,
2032 loaderResults,
2033 pendingActionResult,
2034 revalidatingFetchers,
2035 fetcherResults,
2036 activeDeferreds
2037 );
2038
2039 // Wire up subscribers to update loaderData as promises settle
2040 activeDeferreds.forEach((deferredData, routeId) => {
2041 deferredData.subscribe((aborted) => {
2042 // Note: No need to updateState here since the TrackedPromise on
2043 // loaderData is stable across resolve/reject
2044 // Remove this instance if we were aborted or if promises have settled
2045 if (aborted || deferredData.done) {
2046 activeDeferreds.delete(routeId);
2047 }
2048 });
2049 });
2050
2051 // Preserve SSR errors during partial hydration
2052 if (future.v7_partialHydration && initialHydration && state.errors) {
2053 errors = { ...state.errors, ...errors };
2054 }
2055
2056 let updatedFetchers = markFetchRedirectsDone();
2057 let didAbortFetchLoads = abortStaleFetchLoads(pendingNavigationLoadId);
2058 let shouldUpdateFetchers =
2059 updatedFetchers || didAbortFetchLoads || revalidatingFetchers.length > 0;
2060
2061 return {
2062 matches,
2063 loaderData,
2064 errors,
2065 ...(shouldUpdateFetchers ? { fetchers: new Map(state.fetchers) } : {}),
2066 };
2067 }
2068
2069 function getUpdatedActionData(
2070 pendingActionResult: PendingActionResult | undefined
2071 ): Record<string, RouteData> | null | undefined {
2072 if (pendingActionResult && !isErrorResult(pendingActionResult[1])) {
2073 // This is cast to `any` currently because `RouteData`uses any and it
2074 // would be a breaking change to use any.
2075 // TODO: v7 - change `RouteData` to use `unknown` instead of `any`
2076 return {
2077 [pendingActionResult[0]]: pendingActionResult[1].data as any,
2078 };
2079 } else if (state.actionData) {
2080 if (Object.keys(state.actionData).length === 0) {
2081 return null;
2082 } else {
2083 return state.actionData;
2084 }
2085 }
2086 }
2087
2088 function getUpdatedRevalidatingFetchers(
2089 revalidatingFetchers: RevalidatingFetcher[]
2090 ) {
2091 revalidatingFetchers.forEach((rf) => {
2092 let fetcher = state.fetchers.get(rf.key);
2093 let revalidatingFetcher = getLoadingFetcher(
2094 undefined,
2095 fetcher ? fetcher.data : undefined
2096 );
2097 state.fetchers.set(rf.key, revalidatingFetcher);
2098 });
2099 return new Map(state.fetchers);
2100 }
2101
2102 // Trigger a fetcher load/submit for the given fetcher key
2103 function fetch(
2104 key: string,
2105 routeId: string,
2106 href: string | null,
2107 opts?: RouterFetchOptions
2108 ) {
2109 if (isServer) {
2110 throw new Error(
2111 "router.fetch() was called during the server render, but it shouldn't be. " +
2112 "You are likely calling a useFetcher() method in the body of your component. " +
2113 "Try moving it to a useEffect or a callback."
2114 );
2115 }
2116
2117 abortFetcher(key);
2118
2119 let flushSync = (opts && opts.flushSync) === true;
2120
2121 let routesToUse = inFlightDataRoutes || dataRoutes;
2122 let normalizedPath = normalizeTo(
2123 state.location,
2124 state.matches,
2125 basename,
2126 future.v7_prependBasename,
2127 href,
2128 future.v7_relativeSplatPath,
2129 routeId,
2130 opts?.relative
2131 );
2132 let matches = matchRoutes(routesToUse, normalizedPath, basename);
2133
2134 let fogOfWar = checkFogOfWar(matches, routesToUse, normalizedPath);
2135 if (fogOfWar.active && fogOfWar.matches) {
2136 matches = fogOfWar.matches;
2137 }
2138
2139 if (!matches) {
2140 setFetcherError(
2141 key,
2142 routeId,
2143 getInternalRouterError(404, { pathname: normalizedPath }),
2144 { flushSync }
2145 );
2146 return;
2147 }
2148
2149 let { path, submission, error } = normalizeNavigateOptions(
2150 future.v7_normalizeFormMethod,
2151 true,
2152 normalizedPath,
2153 opts
2154 );
2155
2156 if (error) {
2157 setFetcherError(key, routeId, error, { flushSync });
2158 return;
2159 }
2160
2161 let match = getTargetMatch(matches, path);
2162
2163 let preventScrollReset = (opts && opts.preventScrollReset) === true;
2164
2165 if (submission && isMutationMethod(submission.formMethod)) {
2166 handleFetcherAction(
2167 key,
2168 routeId,
2169 path,
2170 match,
2171 matches,
2172 fogOfWar.active,
2173 flushSync,
2174 preventScrollReset,
2175 submission
2176 );
2177 return;
2178 }
2179
2180 // Store off the match so we can call it's shouldRevalidate on subsequent
2181 // revalidations
2182 fetchLoadMatches.set(key, { routeId, path });
2183 handleFetcherLoader(
2184 key,
2185 routeId,
2186 path,
2187 match,
2188 matches,
2189 fogOfWar.active,
2190 flushSync,
2191 preventScrollReset,
2192 submission
2193 );
2194 }
2195
2196 // Call the action for the matched fetcher.submit(), and then handle redirects,
2197 // errors, and revalidation
2198 async function handleFetcherAction(
2199 key: string,
2200 routeId: string,
2201 path: string,
2202 match: AgnosticDataRouteMatch,
2203 requestMatches: AgnosticDataRouteMatch[],
2204 isFogOfWar: boolean,
2205 flushSync: boolean,
2206 preventScrollReset: boolean,
2207 submission: Submission
2208 ) {
2209 interruptActiveLoads();
2210 fetchLoadMatches.delete(key);
2211
2212 function detectAndHandle405Error(m: AgnosticDataRouteMatch) {
2213 if (!m.route.action && !m.route.lazy) {
2214 let error = getInternalRouterError(405, {
2215 method: submission.formMethod,
2216 pathname: path,
2217 routeId: routeId,
2218 });
2219 setFetcherError(key, routeId, error, { flushSync });
2220 return true;
2221 }
2222 return false;
2223 }
2224
2225 if (!isFogOfWar && detectAndHandle405Error(match)) {
2226 return;
2227 }
2228
2229 // Put this fetcher into it's submitting state
2230 let existingFetcher = state.fetchers.get(key);
2231 updateFetcherState(key, getSubmittingFetcher(submission, existingFetcher), {
2232 flushSync,
2233 });
2234
2235 let abortController = new AbortController();
2236 let fetchRequest = createClientSideRequest(
2237 init.history,
2238 path,
2239 abortController.signal,
2240 submission
2241 );
2242
2243 if (isFogOfWar) {
2244 let discoverResult = await discoverRoutes(
2245 requestMatches,
2246 path,
2247 fetchRequest.signal
2248 );
2249
2250 if (discoverResult.type === "aborted") {
2251 return;
2252 } else if (discoverResult.type === "error") {
2253 setFetcherError(key, routeId, discoverResult.error, { flushSync });
2254 return;
2255 } else if (!discoverResult.matches) {
2256 setFetcherError(
2257 key,
2258 routeId,
2259 getInternalRouterError(404, { pathname: path }),
2260 { flushSync }
2261 );
2262 return;
2263 } else {
2264 requestMatches = discoverResult.matches;
2265 match = getTargetMatch(requestMatches, path);
2266
2267 if (detectAndHandle405Error(match)) {
2268 return;
2269 }
2270 }
2271 }
2272
2273 // Call the action for the fetcher
2274 fetchControllers.set(key, abortController);
2275
2276 let originatingLoadId = incrementingLoadId;
2277 let actionResults = await callDataStrategy(
2278 "action",
2279 state,
2280 fetchRequest,
2281 [match],
2282 requestMatches,
2283 key
2284 );
2285 let actionResult = actionResults[match.route.id];
2286
2287 if (fetchRequest.signal.aborted) {
2288 // We can delete this so long as we weren't aborted by our own fetcher
2289 // re-submit which would have put _new_ controller is in fetchControllers
2290 if (fetchControllers.get(key) === abortController) {
2291 fetchControllers.delete(key);
2292 }
2293 return;
2294 }
2295
2296 // When using v7_fetcherPersist, we don't want errors bubbling up to the UI
2297 // or redirects processed for unmounted fetchers so we just revert them to
2298 // idle
2299 if (future.v7_fetcherPersist && deletedFetchers.has(key)) {
2300 if (isRedirectResult(actionResult) || isErrorResult(actionResult)) {
2301 updateFetcherState(key, getDoneFetcher(undefined));
2302 return;
2303 }
2304 // Let SuccessResult's fall through for revalidation
2305 } else {
2306 if (isRedirectResult(actionResult)) {
2307 fetchControllers.delete(key);
2308 if (pendingNavigationLoadId > originatingLoadId) {
2309 // A new navigation was kicked off after our action started, so that
2310 // should take precedence over this redirect navigation. We already
2311 // set isRevalidationRequired so all loaders for the new route should
2312 // fire unless opted out via shouldRevalidate
2313 updateFetcherState(key, getDoneFetcher(undefined));
2314 return;
2315 } else {
2316 fetchRedirectIds.add(key);
2317 updateFetcherState(key, getLoadingFetcher(submission));
2318 return startRedirectNavigation(fetchRequest, actionResult, false, {
2319 fetcherSubmission: submission,
2320 preventScrollReset,
2321 });
2322 }
2323 }
2324
2325 // Process any non-redirect errors thrown
2326 if (isErrorResult(actionResult)) {
2327 setFetcherError(key, routeId, actionResult.error);
2328 return;
2329 }
2330 }
2331
2332 if (isDeferredResult(actionResult)) {
2333 throw getInternalRouterError(400, { type: "defer-action" });
2334 }
2335
2336 // Start the data load for current matches, or the next location if we're
2337 // in the middle of a navigation
2338 let nextLocation = state.navigation.location || state.location;
2339 let revalidationRequest = createClientSideRequest(
2340 init.history,
2341 nextLocation,
2342 abortController.signal
2343 );
2344 let routesToUse = inFlightDataRoutes || dataRoutes;
2345 let matches =
2346 state.navigation.state !== "idle"
2347 ? matchRoutes(routesToUse, state.navigation.location, basename)
2348 : state.matches;
2349
2350 invariant(matches, "Didn't find any matches after fetcher action");
2351
2352 let loadId = ++incrementingLoadId;
2353 fetchReloadIds.set(key, loadId);
2354
2355 let loadFetcher = getLoadingFetcher(submission, actionResult.data);
2356 state.fetchers.set(key, loadFetcher);
2357
2358 let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(
2359 init.history,
2360 state,
2361 matches,
2362 submission,
2363 nextLocation,
2364 false,
2365 future.v7_skipActionErrorRevalidation,
2366 isRevalidationRequired,
2367 cancelledDeferredRoutes,
2368 cancelledFetcherLoads,
2369 deletedFetchers,
2370 fetchLoadMatches,
2371 fetchRedirectIds,
2372 routesToUse,
2373 basename,
2374 [match.route.id, actionResult]
2375 );
2376
2377 // Put all revalidating fetchers into the loading state, except for the
2378 // current fetcher which we want to keep in it's current loading state which
2379 // contains it's action submission info + action data
2380 revalidatingFetchers
2381 .filter((rf) => rf.key !== key)
2382 .forEach((rf) => {
2383 let staleKey = rf.key;
2384 let existingFetcher = state.fetchers.get(staleKey);
2385 let revalidatingFetcher = getLoadingFetcher(
2386 undefined,
2387 existingFetcher ? existingFetcher.data : undefined
2388 );
2389 state.fetchers.set(staleKey, revalidatingFetcher);
2390 abortFetcher(staleKey);
2391 if (rf.controller) {
2392 fetchControllers.set(staleKey, rf.controller);
2393 }
2394 });
2395
2396 updateState({ fetchers: new Map(state.fetchers) });
2397
2398 let abortPendingFetchRevalidations = () =>
2399 revalidatingFetchers.forEach((rf) => abortFetcher(rf.key));
2400
2401 abortController.signal.addEventListener(
2402 "abort",
2403 abortPendingFetchRevalidations
2404 );
2405
2406 let { loaderResults, fetcherResults } =
2407 await callLoadersAndMaybeResolveData(
2408 state,
2409 matches,
2410 matchesToLoad,
2411 revalidatingFetchers,
2412 revalidationRequest
2413 );
2414
2415 if (abortController.signal.aborted) {
2416 return;
2417 }
2418
2419 abortController.signal.removeEventListener(
2420 "abort",
2421 abortPendingFetchRevalidations
2422 );
2423
2424 fetchReloadIds.delete(key);
2425 fetchControllers.delete(key);
2426 revalidatingFetchers.forEach((r) => fetchControllers.delete(r.key));
2427
2428 let redirect = findRedirect(loaderResults);
2429 if (redirect) {
2430 return startRedirectNavigation(
2431 revalidationRequest,
2432 redirect.result,
2433 false,
2434 { preventScrollReset }
2435 );
2436 }
2437
2438 redirect = findRedirect(fetcherResults);
2439 if (redirect) {
2440 // If this redirect came from a fetcher make sure we mark it in
2441 // fetchRedirectIds so it doesn't get revalidated on the next set of
2442 // loader executions
2443 fetchRedirectIds.add(redirect.key);
2444 return startRedirectNavigation(
2445 revalidationRequest,
2446 redirect.result,
2447 false,
2448 { preventScrollReset }
2449 );
2450 }
2451
2452 // Process and commit output from loaders
2453 let { loaderData, errors } = processLoaderData(
2454 state,
2455 matches,
2456 loaderResults,
2457 undefined,
2458 revalidatingFetchers,
2459 fetcherResults,
2460 activeDeferreds
2461 );
2462
2463 // Since we let revalidations complete even if the submitting fetcher was
2464 // deleted, only put it back to idle if it hasn't been deleted
2465 if (state.fetchers.has(key)) {
2466 let doneFetcher = getDoneFetcher(actionResult.data);
2467 state.fetchers.set(key, doneFetcher);
2468 }
2469
2470 abortStaleFetchLoads(loadId);
2471
2472 // If we are currently in a navigation loading state and this fetcher is
2473 // more recent than the navigation, we want the newer data so abort the
2474 // navigation and complete it with the fetcher data
2475 if (
2476 state.navigation.state === "loading" &&
2477 loadId > pendingNavigationLoadId
2478 ) {
2479 invariant(pendingAction, "Expected pending action");
2480 pendingNavigationController && pendingNavigationController.abort();
2481
2482 completeNavigation(state.navigation.location, {
2483 matches,
2484 loaderData,
2485 errors,
2486 fetchers: new Map(state.fetchers),
2487 });
2488 } else {
2489 // otherwise just update with the fetcher data, preserving any existing
2490 // loaderData for loaders that did not need to reload. We have to
2491 // manually merge here since we aren't going through completeNavigation
2492 updateState({
2493 errors,
2494 loaderData: mergeLoaderData(
2495 state.loaderData,
2496 loaderData,
2497 matches,
2498 errors
2499 ),
2500 fetchers: new Map(state.fetchers),
2501 });
2502 isRevalidationRequired = false;
2503 }
2504 }
2505
2506 // Call the matched loader for fetcher.load(), handling redirects, errors, etc.
2507 async function handleFetcherLoader(
2508 key: string,
2509 routeId: string,
2510 path: string,
2511 match: AgnosticDataRouteMatch,
2512 matches: AgnosticDataRouteMatch[],
2513 isFogOfWar: boolean,
2514 flushSync: boolean,
2515 preventScrollReset: boolean,
2516 submission?: Submission
2517 ) {
2518 let existingFetcher = state.fetchers.get(key);
2519 updateFetcherState(
2520 key,
2521 getLoadingFetcher(
2522 submission,
2523 existingFetcher ? existingFetcher.data : undefined
2524 ),
2525 { flushSync }
2526 );
2527
2528 let abortController = new AbortController();
2529 let fetchRequest = createClientSideRequest(
2530 init.history,
2531 path,
2532 abortController.signal
2533 );
2534
2535 if (isFogOfWar) {
2536 let discoverResult = await discoverRoutes(
2537 matches,
2538 path,
2539 fetchRequest.signal
2540 );
2541
2542 if (discoverResult.type === "aborted") {
2543 return;
2544 } else if (discoverResult.type === "error") {
2545 setFetcherError(key, routeId, discoverResult.error, { flushSync });
2546 return;
2547 } else if (!discoverResult.matches) {
2548 setFetcherError(
2549 key,
2550 routeId,
2551 getInternalRouterError(404, { pathname: path }),
2552 { flushSync }
2553 );
2554 return;
2555 } else {
2556 matches = discoverResult.matches;
2557 match = getTargetMatch(matches, path);
2558 }
2559 }
2560
2561 // Call the loader for this fetcher route match
2562 fetchControllers.set(key, abortController);
2563
2564 let originatingLoadId = incrementingLoadId;
2565 let results = await callDataStrategy(
2566 "loader",
2567 state,
2568 fetchRequest,
2569 [match],
2570 matches,
2571 key
2572 );
2573 let result = results[match.route.id];
2574
2575 // Deferred isn't supported for fetcher loads, await everything and treat it
2576 // as a normal load. resolveDeferredData will return undefined if this
2577 // fetcher gets aborted, so we just leave result untouched and short circuit
2578 // below if that happens
2579 if (isDeferredResult(result)) {
2580 result =
2581 (await resolveDeferredData(result, fetchRequest.signal, true)) ||
2582 result;
2583 }
2584
2585 // We can delete this so long as we weren't aborted by our our own fetcher
2586 // re-load which would have put _new_ controller is in fetchControllers
2587 if (fetchControllers.get(key) === abortController) {
2588 fetchControllers.delete(key);
2589 }
2590
2591 if (fetchRequest.signal.aborted) {
2592 return;
2593 }
2594
2595 // We don't want errors bubbling up or redirects followed for unmounted
2596 // fetchers, so short circuit here if it was removed from the UI
2597 if (deletedFetchers.has(key)) {
2598 updateFetcherState(key, getDoneFetcher(undefined));
2599 return;
2600 }
2601
2602 // If the loader threw a redirect Response, start a new REPLACE navigation
2603 if (isRedirectResult(result)) {
2604 if (pendingNavigationLoadId > originatingLoadId) {
2605 // A new navigation was kicked off after our loader started, so that
2606 // should take precedence over this redirect navigation
2607 updateFetcherState(key, getDoneFetcher(undefined));
2608 return;
2609 } else {
2610 fetchRedirectIds.add(key);
2611 await startRedirectNavigation(fetchRequest, result, false, {
2612 preventScrollReset,
2613 });
2614 return;
2615 }
2616 }
2617
2618 // Process any non-redirect errors thrown
2619 if (isErrorResult(result)) {
2620 setFetcherError(key, routeId, result.error);
2621 return;
2622 }
2623
2624 invariant(!isDeferredResult(result), "Unhandled fetcher deferred data");
2625
2626 // Put the fetcher back into an idle state
2627 updateFetcherState(key, getDoneFetcher(result.data));
2628 }
2629
2630 /**
2631 * Utility function to handle redirects returned from an action or loader.
2632 * Normally, a redirect "replaces" the navigation that triggered it. So, for
2633 * example:
2634 *
2635 * - user is on /a
2636 * - user clicks a link to /b
2637 * - loader for /b redirects to /c
2638 *
2639 * In a non-JS app the browser would track the in-flight navigation to /b and
2640 * then replace it with /c when it encountered the redirect response. In
2641 * the end it would only ever update the URL bar with /c.
2642 *
2643 * In client-side routing using pushState/replaceState, we aim to emulate
2644 * this behavior and we also do not update history until the end of the
2645 * navigation (including processed redirects). This means that we never
2646 * actually touch history until we've processed redirects, so we just use
2647 * the history action from the original navigation (PUSH or REPLACE).
2648 */
2649 async function startRedirectNavigation(
2650 request: Request,
2651 redirect: RedirectResult,
2652 isNavigation: boolean,
2653 {
2654 submission,
2655 fetcherSubmission,
2656 preventScrollReset,
2657 replace,
2658 }: {
2659 submission?: Submission;
2660 fetcherSubmission?: Submission;
2661 preventScrollReset?: boolean;
2662 replace?: boolean;
2663 } = {}
2664 ) {
2665 if (redirect.response.headers.has("X-Remix-Revalidate")) {
2666 isRevalidationRequired = true;
2667 }
2668
2669 let location = redirect.response.headers.get("Location");
2670 invariant(location, "Expected a Location header on the redirect Response");
2671 location = normalizeRedirectLocation(
2672 location,
2673 new URL(request.url),
2674 basename
2675 );
2676 let redirectLocation = createLocation(state.location, location, {
2677 _isRedirect: true,
2678 });
2679
2680 if (isBrowser) {
2681 let isDocumentReload = false;
2682
2683 if (redirect.response.headers.has("X-Remix-Reload-Document")) {
2684 // Hard reload if the response contained X-Remix-Reload-Document
2685 isDocumentReload = true;
2686 } else if (ABSOLUTE_URL_REGEX.test(location)) {
2687 const url = init.history.createURL(location);
2688 isDocumentReload =
2689 // Hard reload if it's an absolute URL to a new origin
2690 url.origin !== routerWindow.location.origin ||
2691 // Hard reload if it's an absolute URL that does not match our basename
2692 stripBasename(url.pathname, basename) == null;
2693 }
2694
2695 if (isDocumentReload) {
2696 if (replace) {
2697 routerWindow.location.replace(location);
2698 } else {
2699 routerWindow.location.assign(location);
2700 }
2701 return;
2702 }
2703 }
2704
2705 // There's no need to abort on redirects, since we don't detect the
2706 // redirect until the action/loaders have settled
2707 pendingNavigationController = null;
2708
2709 let redirectHistoryAction =
2710 replace === true || redirect.response.headers.has("X-Remix-Replace")
2711 ? HistoryAction.Replace
2712 : HistoryAction.Push;
2713
2714 // Use the incoming submission if provided, fallback on the active one in
2715 // state.navigation
2716 let { formMethod, formAction, formEncType } = state.navigation;
2717 if (
2718 !submission &&
2719 !fetcherSubmission &&
2720 formMethod &&
2721 formAction &&
2722 formEncType
2723 ) {
2724 submission = getSubmissionFromNavigation(state.navigation);
2725 }
2726
2727 // If this was a 307/308 submission we want to preserve the HTTP method and
2728 // re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the
2729 // redirected location
2730 let activeSubmission = submission || fetcherSubmission;
2731 if (
2732 redirectPreserveMethodStatusCodes.has(redirect.response.status) &&
2733 activeSubmission &&
2734 isMutationMethod(activeSubmission.formMethod)
2735 ) {
2736 await startNavigation(redirectHistoryAction, redirectLocation, {
2737 submission: {
2738 ...activeSubmission,
2739 formAction: location,
2740 },
2741 // Preserve these flags across redirects
2742 preventScrollReset: preventScrollReset || pendingPreventScrollReset,
2743 enableViewTransition: isNavigation
2744 ? pendingViewTransitionEnabled
2745 : undefined,
2746 });
2747 } else {
2748 // If we have a navigation submission, we will preserve it through the
2749 // redirect navigation
2750 let overrideNavigation = getLoadingNavigation(
2751 redirectLocation,
2752 submission
2753 );
2754 await startNavigation(redirectHistoryAction, redirectLocation, {
2755 overrideNavigation,
2756 // Send fetcher submissions through for shouldRevalidate
2757 fetcherSubmission,
2758 // Preserve these flags across redirects
2759 preventScrollReset: preventScrollReset || pendingPreventScrollReset,
2760 enableViewTransition: isNavigation
2761 ? pendingViewTransitionEnabled
2762 : undefined,
2763 });
2764 }
2765 }
2766
2767 // Utility wrapper for calling dataStrategy client-side without having to
2768 // pass around the manifest, mapRouteProperties, etc.
2769 async function callDataStrategy(
2770 type: "loader" | "action",
2771 state: RouterState,
2772 request: Request,
2773 matchesToLoad: AgnosticDataRouteMatch[],
2774 matches: AgnosticDataRouteMatch[],
2775 fetcherKey: string | null
2776 ): Promise<Record<string, DataResult>> {
2777 let results: Record<string, DataStrategyResult>;
2778 let dataResults: Record<string, DataResult> = {};
2779 try {
2780 results = await callDataStrategyImpl(
2781 dataStrategyImpl,
2782 type,
2783 state,
2784 request,
2785 matchesToLoad,
2786 matches,
2787 fetcherKey,
2788 manifest,
2789 mapRouteProperties
2790 );
2791 } catch (e) {
2792 // If the outer dataStrategy method throws, just return the error for all
2793 // matches - and it'll naturally bubble to the root
2794 matchesToLoad.forEach((m) => {
2795 dataResults[m.route.id] = {
2796 type: ResultType.error,
2797 error: e,
2798 };
2799 });
2800 return dataResults;
2801 }
2802
2803 for (let [routeId, result] of Object.entries(results)) {
2804 if (isRedirectDataStrategyResultResult(result)) {
2805 let response = result.result as Response;
2806 dataResults[routeId] = {
2807 type: ResultType.redirect,
2808 response: normalizeRelativeRoutingRedirectResponse(
2809 response,
2810 request,
2811 routeId,
2812 matches,
2813 basename,
2814 future.v7_relativeSplatPath
2815 ),
2816 };
2817 } else {
2818 dataResults[routeId] = await convertDataStrategyResultToDataResult(
2819 result
2820 );
2821 }
2822 }
2823
2824 return dataResults;
2825 }
2826
2827 async function callLoadersAndMaybeResolveData(
2828 state: RouterState,
2829 matches: AgnosticDataRouteMatch[],
2830 matchesToLoad: AgnosticDataRouteMatch[],
2831 fetchersToLoad: RevalidatingFetcher[],
2832 request: Request
2833 ) {
2834 let currentMatches = state.matches;
2835
2836 // Kick off loaders and fetchers in parallel
2837 let loaderResultsPromise = callDataStrategy(
2838 "loader",
2839 state,
2840 request,
2841 matchesToLoad,
2842 matches,
2843 null
2844 );
2845
2846 let fetcherResultsPromise = Promise.all(
2847 fetchersToLoad.map(async (f) => {
2848 if (f.matches && f.match && f.controller) {
2849 let results = await callDataStrategy(
2850 "loader",
2851 state,
2852 createClientSideRequest(init.history, f.path, f.controller.signal),
2853 [f.match],
2854 f.matches,
2855 f.key
2856 );
2857 let result = results[f.match.route.id];
2858 // Fetcher results are keyed by fetcher key from here on out, not routeId
2859 return { [f.key]: result };
2860 } else {
2861 return Promise.resolve({
2862 [f.key]: {
2863 type: ResultType.error,
2864 error: getInternalRouterError(404, {
2865 pathname: f.path,
2866 }),
2867 } as ErrorResult,
2868 });
2869 }
2870 })
2871 );
2872
2873 let loaderResults = await loaderResultsPromise;
2874 let fetcherResults = (await fetcherResultsPromise).reduce(
2875 (acc, r) => Object.assign(acc, r),
2876 {}
2877 );
2878
2879 await Promise.all([
2880 resolveNavigationDeferredResults(
2881 matches,
2882 loaderResults,
2883 request.signal,
2884 currentMatches,
2885 state.loaderData
2886 ),
2887 resolveFetcherDeferredResults(matches, fetcherResults, fetchersToLoad),
2888 ]);
2889
2890 return {
2891 loaderResults,
2892 fetcherResults,
2893 };
2894 }
2895
2896 function interruptActiveLoads() {
2897 // Every interruption triggers a revalidation
2898 isRevalidationRequired = true;
2899
2900 // Cancel pending route-level deferreds and mark cancelled routes for
2901 // revalidation
2902 cancelledDeferredRoutes.push(...cancelActiveDeferreds());
2903
2904 // Abort in-flight fetcher loads
2905 fetchLoadMatches.forEach((_, key) => {
2906 if (fetchControllers.has(key)) {
2907 cancelledFetcherLoads.add(key);
2908 }
2909 abortFetcher(key);
2910 });
2911 }
2912
2913 function updateFetcherState(
2914 key: string,
2915 fetcher: Fetcher,
2916 opts: { flushSync?: boolean } = {}
2917 ) {
2918 state.fetchers.set(key, fetcher);
2919 updateState(
2920 { fetchers: new Map(state.fetchers) },
2921 { flushSync: (opts && opts.flushSync) === true }
2922 );
2923 }
2924
2925 function setFetcherError(
2926 key: string,
2927 routeId: string,
2928 error: any,
2929 opts: { flushSync?: boolean } = {}
2930 ) {
2931 let boundaryMatch = findNearestBoundary(state.matches, routeId);
2932 deleteFetcher(key);
2933 updateState(
2934 {
2935 errors: {
2936 [boundaryMatch.route.id]: error,
2937 },
2938 fetchers: new Map(state.fetchers),
2939 },
2940 { flushSync: (opts && opts.flushSync) === true }
2941 );
2942 }
2943
2944 function getFetcher<TData = any>(key: string): Fetcher<TData> {
2945 if (future.v7_fetcherPersist) {
2946 activeFetchers.set(key, (activeFetchers.get(key) || 0) + 1);
2947 // If this fetcher was previously marked for deletion, unmark it since we
2948 // have a new instance
2949 if (deletedFetchers.has(key)) {
2950 deletedFetchers.delete(key);
2951 }
2952 }
2953 return state.fetchers.get(key) || IDLE_FETCHER;
2954 }
2955
2956 function deleteFetcher(key: string): void {
2957 let fetcher = state.fetchers.get(key);
2958 // Don't abort the controller if this is a deletion of a fetcher.submit()
2959 // in it's loading phase since - we don't want to abort the corresponding
2960 // revalidation and want them to complete and land
2961 if (
2962 fetchControllers.has(key) &&
2963 !(fetcher && fetcher.state === "loading" && fetchReloadIds.has(key))
2964 ) {
2965 abortFetcher(key);
2966 }
2967 fetchLoadMatches.delete(key);
2968 fetchReloadIds.delete(key);
2969 fetchRedirectIds.delete(key);
2970 deletedFetchers.delete(key);
2971 cancelledFetcherLoads.delete(key);
2972 state.fetchers.delete(key);
2973 }
2974
2975 function deleteFetcherAndUpdateState(key: string): void {
2976 if (future.v7_fetcherPersist) {
2977 let count = (activeFetchers.get(key) || 0) - 1;
2978 if (count <= 0) {
2979 activeFetchers.delete(key);
2980 deletedFetchers.add(key);
2981 } else {
2982 activeFetchers.set(key, count);
2983 }
2984 } else {
2985 deleteFetcher(key);
2986 }
2987 updateState({ fetchers: new Map(state.fetchers) });
2988 }
2989
2990 function abortFetcher(key: string) {
2991 let controller = fetchControllers.get(key);
2992 if (controller) {
2993 controller.abort();
2994 fetchControllers.delete(key);
2995 }
2996 }
2997
2998 function markFetchersDone(keys: string[]) {
2999 for (let key of keys) {
3000 let fetcher = getFetcher(key);
3001 let doneFetcher = getDoneFetcher(fetcher.data);
3002 state.fetchers.set(key, doneFetcher);
3003 }
3004 }
3005
3006 function markFetchRedirectsDone(): boolean {
3007 let doneKeys = [];
3008 let updatedFetchers = false;
3009 for (let key of fetchRedirectIds) {
3010 let fetcher = state.fetchers.get(key);
3011 invariant(fetcher, `Expected fetcher: ${key}`);
3012 if (fetcher.state === "loading") {
3013 fetchRedirectIds.delete(key);
3014 doneKeys.push(key);
3015 updatedFetchers = true;
3016 }
3017 }
3018 markFetchersDone(doneKeys);
3019 return updatedFetchers;
3020 }
3021
3022 function abortStaleFetchLoads(landedId: number): boolean {
3023 let yeetedKeys = [];
3024 for (let [key, id] of fetchReloadIds) {
3025 if (id < landedId) {
3026 let fetcher = state.fetchers.get(key);
3027 invariant(fetcher, `Expected fetcher: ${key}`);
3028 if (fetcher.state === "loading") {
3029 abortFetcher(key);
3030 fetchReloadIds.delete(key);
3031 yeetedKeys.push(key);
3032 }
3033 }
3034 }
3035 markFetchersDone(yeetedKeys);
3036 return yeetedKeys.length > 0;
3037 }
3038
3039 function getBlocker(key: string, fn: BlockerFunction) {
3040 let blocker: Blocker = state.blockers.get(key) || IDLE_BLOCKER;
3041
3042 if (blockerFunctions.get(key) !== fn) {
3043 blockerFunctions.set(key, fn);
3044 }
3045
3046 return blocker;
3047 }
3048
3049 function deleteBlocker(key: string) {
3050 state.blockers.delete(key);
3051 blockerFunctions.delete(key);
3052 }
3053
3054 // Utility function to update blockers, ensuring valid state transitions
3055 function updateBlocker(key: string, newBlocker: Blocker) {
3056 let blocker = state.blockers.get(key) || IDLE_BLOCKER;
3057
3058 // Poor mans state machine :)
3059 // https://mermaid.live/edit#pako:eNqVkc9OwzAMxl8l8nnjAYrEtDIOHEBIgwvKJTReGy3_lDpIqO27k6awMG0XcrLlnz87nwdonESogKXXBuE79rq75XZO3-yHds0RJVuv70YrPlUrCEe2HfrORS3rubqZfuhtpg5C9wk5tZ4VKcRUq88q9Z8RS0-48cE1iHJkL0ugbHuFLus9L6spZy8nX9MP2CNdomVaposqu3fGayT8T8-jJQwhepo_UtpgBQaDEUom04dZhAN1aJBDlUKJBxE1ceB2Smj0Mln-IBW5AFU2dwUiktt_2Qaq2dBfaKdEup85UV7Yd-dKjlnkabl2Pvr0DTkTreM
3060 invariant(
3061 (blocker.state === "unblocked" && newBlocker.state === "blocked") ||
3062 (blocker.state === "blocked" && newBlocker.state === "blocked") ||
3063 (blocker.state === "blocked" && newBlocker.state === "proceeding") ||
3064 (blocker.state === "blocked" && newBlocker.state === "unblocked") ||
3065 (blocker.state === "proceeding" && newBlocker.state === "unblocked"),
3066 `Invalid blocker state transition: ${blocker.state} -> ${newBlocker.state}`
3067 );
3068
3069 let blockers = new Map(state.blockers);
3070 blockers.set(key, newBlocker);
3071 updateState({ blockers });
3072 }
3073
3074 function shouldBlockNavigation({
3075 currentLocation,
3076 nextLocation,
3077 historyAction,
3078 }: {
3079 currentLocation: Location;
3080 nextLocation: Location;
3081 historyAction: HistoryAction;
3082 }): string | undefined {
3083 if (blockerFunctions.size === 0) {
3084 return;
3085 }
3086
3087 // We ony support a single active blocker at the moment since we don't have
3088 // any compelling use cases for multi-blocker yet
3089 if (blockerFunctions.size > 1) {
3090 warning(false, "A router only supports one blocker at a time");
3091 }
3092
3093 let entries = Array.from(blockerFunctions.entries());
3094 let [blockerKey, blockerFunction] = entries[entries.length - 1];
3095 let blocker = state.blockers.get(blockerKey);
3096
3097 if (blocker && blocker.state === "proceeding") {
3098 // If the blocker is currently proceeding, we don't need to re-check
3099 // it and can let this navigation continue
3100 return;
3101 }
3102
3103 // At this point, we know we're unblocked/blocked so we need to check the
3104 // user-provided blocker function
3105 if (blockerFunction({ currentLocation, nextLocation, historyAction })) {
3106 return blockerKey;
3107 }
3108 }
3109
3110 function handleNavigational404(pathname: string) {
3111 let error = getInternalRouterError(404, { pathname });
3112 let routesToUse = inFlightDataRoutes || dataRoutes;
3113 let { matches, route } = getShortCircuitMatches(routesToUse);
3114
3115 // Cancel all pending deferred on 404s since we don't keep any routes
3116 cancelActiveDeferreds();
3117
3118 return { notFoundMatches: matches, route, error };
3119 }
3120
3121 function cancelActiveDeferreds(
3122 predicate?: (routeId: string) => boolean
3123 ): string[] {
3124 let cancelledRouteIds: string[] = [];
3125 activeDeferreds.forEach((dfd, routeId) => {
3126 if (!predicate || predicate(routeId)) {
3127 // Cancel the deferred - but do not remove from activeDeferreds here -
3128 // we rely on the subscribers to do that so our tests can assert proper
3129 // cleanup via _internalActiveDeferreds
3130 dfd.cancel();
3131 cancelledRouteIds.push(routeId);
3132 activeDeferreds.delete(routeId);
3133 }
3134 });
3135 return cancelledRouteIds;
3136 }
3137
3138 // Opt in to capturing and reporting scroll positions during navigations,
3139 // used by the <ScrollRestoration> component
3140 function enableScrollRestoration(
3141 positions: Record<string, number>,
3142 getPosition: GetScrollPositionFunction,
3143 getKey?: GetScrollRestorationKeyFunction
3144 ) {
3145 savedScrollPositions = positions;
3146 getScrollPosition = getPosition;
3147 getScrollRestorationKey = getKey || null;
3148
3149 // Perform initial hydration scroll restoration, since we miss the boat on
3150 // the initial updateState() because we've not yet rendered <ScrollRestoration/>
3151 // and therefore have no savedScrollPositions available
3152 if (!initialScrollRestored && state.navigation === IDLE_NAVIGATION) {
3153 initialScrollRestored = true;
3154 let y = getSavedScrollPosition(state.location, state.matches);
3155 if (y != null) {
3156 updateState({ restoreScrollPosition: y });
3157 }
3158 }
3159
3160 return () => {
3161 savedScrollPositions = null;
3162 getScrollPosition = null;
3163 getScrollRestorationKey = null;
3164 };
3165 }
3166
3167 function getScrollKey(location: Location, matches: AgnosticDataRouteMatch[]) {
3168 if (getScrollRestorationKey) {
3169 let key = getScrollRestorationKey(
3170 location,
3171 matches.map((m) => convertRouteMatchToUiMatch(m, state.loaderData))
3172 );
3173 return key || location.key;
3174 }
3175 return location.key;
3176 }
3177
3178 function saveScrollPosition(
3179 location: Location,
3180 matches: AgnosticDataRouteMatch[]
3181 ): void {
3182 if (savedScrollPositions && getScrollPosition) {
3183 let key = getScrollKey(location, matches);
3184 savedScrollPositions[key] = getScrollPosition();
3185 }
3186 }
3187
3188 function getSavedScrollPosition(
3189 location: Location,
3190 matches: AgnosticDataRouteMatch[]
3191 ): number | null {
3192 if (savedScrollPositions) {
3193 let key = getScrollKey(location, matches);
3194 let y = savedScrollPositions[key];
3195 if (typeof y === "number") {
3196 return y;
3197 }
3198 }
3199 return null;
3200 }
3201
3202 function checkFogOfWar(
3203 matches: AgnosticDataRouteMatch[] | null,
3204 routesToUse: AgnosticDataRouteObject[],
3205 pathname: string
3206 ): { active: boolean; matches: AgnosticDataRouteMatch[] | null } {
3207 if (patchRoutesOnNavigationImpl) {
3208 if (!matches) {
3209 let fogMatches = matchRoutesImpl<AgnosticDataRouteObject>(
3210 routesToUse,
3211 pathname,
3212 basename,
3213 true
3214 );
3215
3216 return { active: true, matches: fogMatches || [] };
3217 } else {
3218 if (Object.keys(matches[0].params).length > 0) {
3219 // If we matched a dynamic param or a splat, it might only be because
3220 // we haven't yet discovered other routes that would match with a
3221 // higher score. Call patchRoutesOnNavigation just to be sure
3222 let partialMatches = matchRoutesImpl<AgnosticDataRouteObject>(
3223 routesToUse,
3224 pathname,
3225 basename,
3226 true
3227 );
3228 return { active: true, matches: partialMatches };
3229 }
3230 }
3231 }
3232
3233 return { active: false, matches: null };
3234 }
3235
3236 type DiscoverRoutesSuccessResult = {
3237 type: "success";
3238 matches: AgnosticDataRouteMatch[] | null;
3239 };
3240 type DiscoverRoutesErrorResult = {
3241 type: "error";
3242 error: any;
3243 partialMatches: AgnosticDataRouteMatch[];
3244 };
3245 type DiscoverRoutesAbortedResult = { type: "aborted" };
3246 type DiscoverRoutesResult =
3247 | DiscoverRoutesSuccessResult
3248 | DiscoverRoutesErrorResult
3249 | DiscoverRoutesAbortedResult;
3250
3251 async function discoverRoutes(
3252 matches: AgnosticDataRouteMatch[],
3253 pathname: string,
3254 signal: AbortSignal
3255 ): Promise<DiscoverRoutesResult> {
3256 if (!patchRoutesOnNavigationImpl) {
3257 return { type: "success", matches };
3258 }
3259
3260 let partialMatches: AgnosticDataRouteMatch[] | null = matches;
3261 while (true) {
3262 let isNonHMR = inFlightDataRoutes == null;
3263 let routesToUse = inFlightDataRoutes || dataRoutes;
3264 let localManifest = manifest;
3265 try {
3266 await patchRoutesOnNavigationImpl({
3267 path: pathname,
3268 matches: partialMatches,
3269 patch: (routeId, children) => {
3270 if (signal.aborted) return;
3271 patchRoutesImpl(
3272 routeId,
3273 children,
3274 routesToUse,
3275 localManifest,
3276 mapRouteProperties
3277 );
3278 },
3279 });
3280 } catch (e) {
3281 return { type: "error", error: e, partialMatches };
3282 } finally {
3283 // If we are not in the middle of an HMR revalidation and we changed the
3284 // routes, provide a new identity so when we `updateState` at the end of
3285 // this navigation/fetch `router.routes` will be a new identity and
3286 // trigger a re-run of memoized `router.routes` dependencies.
3287 // HMR will already update the identity and reflow when it lands
3288 // `inFlightDataRoutes` in `completeNavigation`
3289 if (isNonHMR && !signal.aborted) {
3290 dataRoutes = [...dataRoutes];
3291 }
3292 }
3293
3294 if (signal.aborted) {
3295 return { type: "aborted" };
3296 }
3297
3298 let newMatches = matchRoutes(routesToUse, pathname, basename);
3299 if (newMatches) {
3300 return { type: "success", matches: newMatches };
3301 }
3302
3303 let newPartialMatches = matchRoutesImpl<AgnosticDataRouteObject>(
3304 routesToUse,
3305 pathname,
3306 basename,
3307 true
3308 );
3309
3310 // Avoid loops if the second pass results in the same partial matches
3311 if (
3312 !newPartialMatches ||
3313 (partialMatches.length === newPartialMatches.length &&
3314 partialMatches.every(
3315 (m, i) => m.route.id === newPartialMatches![i].route.id
3316 ))
3317 ) {
3318 return { type: "success", matches: null };
3319 }
3320
3321 partialMatches = newPartialMatches;
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 dataStrategy,
3496 }: {
3497 requestContext?: unknown;
3498 skipLoaderErrorBubbling?: boolean;
3499 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 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 dataStrategy,
3597 }: {
3598 requestContext?: unknown;
3599 routeId?: string;
3600 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 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 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 dataStrategy,
3690 skipLoaderErrorBubbling,
3691 routeMatch != null
3692 );
3693 return result;
3694 }
3695
3696 let result = await loadRouteData(
3697 request,
3698 matches,
3699 requestContext,
3700 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 `DataStrategyResult` to bail out early
3714 // and then return or throw the raw Response here accordingly
3715 if (isDataStrategyResult(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 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 dataStrategy
3763 );
3764 result = results[actionMatch.route.id];
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 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 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 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 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 results,
3956 pendingActionResult,
3957 activeDeferreds,
3958 skipLoaderErrorBubbling
3959 );
3960
3961 // Add a null for any non-loader matches for proper revalidation on the client
3962 let executedLoaders = new Set<string>(
3963 matchesToLoad.map((match) => match.route.id)
3964 );
3965 matches.forEach((match) => {
3966 if (!executedLoaders.has(match.route.id)) {
3967 context.loaderData[match.route.id] = null;
3968 }
3969 });
3970
3971 return {
3972 ...context,
3973 matches,
3974 activeDeferreds:
3975 activeDeferreds.size > 0
3976 ? Object.fromEntries(activeDeferreds.entries())
3977 : null,
3978 };
3979 }
3980
3981 // Utility wrapper for calling dataStrategy server-side without having to
3982 // pass around the manifest, mapRouteProperties, etc.
3983 async function callDataStrategy(
3984 type: "loader" | "action",
3985 request: Request,
3986 matchesToLoad: AgnosticDataRouteMatch[],
3987 matches: AgnosticDataRouteMatch[],
3988 isRouteRequest: boolean,
3989 requestContext: unknown,
3990 dataStrategy: DataStrategyFunction | null
3991 ): Promise<Record<string, DataResult>> {
3992 let results = await callDataStrategyImpl(
3993 dataStrategy || defaultDataStrategy,
3994 type,
3995 null,
3996 request,
3997 matchesToLoad,
3998 matches,
3999 null,
4000 manifest,
4001 mapRouteProperties,
4002 requestContext
4003 );
4004
4005 let dataResults: Record<string, DataResult> = {};
4006 await Promise.all(
4007 matches.map(async (match) => {
4008 if (!(match.route.id in results)) {
4009 return;
4010 }
4011 let result = results[match.route.id];
4012 if (isRedirectDataStrategyResultResult(result)) {
4013 let response = result.result as Response;
4014 // Throw redirects and let the server handle them with an HTTP redirect
4015 throw normalizeRelativeRoutingRedirectResponse(
4016 response,
4017 request,
4018 match.route.id,
4019 matches,
4020 basename,
4021 future.v7_relativeSplatPath
4022 );
4023 }
4024 if (isResponse(result.result) && isRouteRequest) {
4025 // For SSR single-route requests, we want to hand Responses back
4026 // directly without unwrapping
4027 throw result;
4028 }
4029
4030 dataResults[match.route.id] =
4031 await convertDataStrategyResultToDataResult(result);
4032 })
4033 );
4034 return dataResults;
4035 }
4036
4037 return {
4038 dataRoutes,
4039 query,
4040 queryRoute,
4041 };
4042}
4043
4044//#endregion
4045
4046////////////////////////////////////////////////////////////////////////////////
4047//#region Helpers
4048////////////////////////////////////////////////////////////////////////////////
4049
4050/**
4051 * Given an existing StaticHandlerContext and an error thrown at render time,
4052 * provide an updated StaticHandlerContext suitable for a second SSR render
4053 */
4054export function getStaticContextFromError(
4055 routes: AgnosticDataRouteObject[],
4056 context: StaticHandlerContext,
4057 error: any
4058) {
4059 let newContext: StaticHandlerContext = {
4060 ...context,
4061 statusCode: isRouteErrorResponse(error) ? error.status : 500,
4062 errors: {
4063 [context._deepestRenderedBoundaryId || routes[0].id]: error,
4064 },
4065 };
4066 return newContext;
4067}
4068
4069function throwStaticHandlerAbortedError(
4070 request: Request,
4071 isRouteRequest: boolean,
4072 future: StaticHandlerFutureConfig
4073) {
4074 if (future.v7_throwAbortReason && request.signal.reason !== undefined) {
4075 throw request.signal.reason;
4076 }
4077
4078 let method = isRouteRequest ? "queryRoute" : "query";
4079 throw new Error(`${method}() call aborted: ${request.method} ${request.url}`);
4080}
4081
4082function isSubmissionNavigation(
4083 opts: BaseNavigateOrFetchOptions
4084): opts is SubmissionNavigateOptions {
4085 return (
4086 opts != null &&
4087 (("formData" in opts && opts.formData != null) ||
4088 ("body" in opts && opts.body !== undefined))
4089 );
4090}
4091
4092function normalizeTo(
4093 location: Path,
4094 matches: AgnosticDataRouteMatch[],
4095 basename: string,
4096 prependBasename: boolean,
4097 to: To | null,
4098 v7_relativeSplatPath: boolean,
4099 fromRouteId?: string,
4100 relative?: RelativeRoutingType
4101) {
4102 let contextualMatches: AgnosticDataRouteMatch[];
4103 let activeRouteMatch: AgnosticDataRouteMatch | undefined;
4104 if (fromRouteId) {
4105 // Grab matches up to the calling route so our route-relative logic is
4106 // relative to the correct source route
4107 contextualMatches = [];
4108 for (let match of matches) {
4109 contextualMatches.push(match);
4110 if (match.route.id === fromRouteId) {
4111 activeRouteMatch = match;
4112 break;
4113 }
4114 }
4115 } else {
4116 contextualMatches = matches;
4117 activeRouteMatch = matches[matches.length - 1];
4118 }
4119
4120 // Resolve the relative path
4121 let path = resolveTo(
4122 to ? to : ".",
4123 getResolveToMatches(contextualMatches, v7_relativeSplatPath),
4124 stripBasename(location.pathname, basename) || location.pathname,
4125 relative === "path"
4126 );
4127
4128 // When `to` is not specified we inherit search/hash from the current
4129 // location, unlike when to="." and we just inherit the path.
4130 // See https://github.com/remix-run/remix/issues/927
4131 if (to == null) {
4132 path.search = location.search;
4133 path.hash = location.hash;
4134 }
4135
4136 // Account for `?index` params when routing to the current location
4137 if ((to == null || to === "" || to === ".") && activeRouteMatch) {
4138 let nakedIndex = hasNakedIndexQuery(path.search);
4139 if (activeRouteMatch.route.index && !nakedIndex) {
4140 // Add one when we're targeting an index route
4141 path.search = path.search
4142 ? path.search.replace(/^\?/, "?index&")
4143 : "?index";
4144 } else if (!activeRouteMatch.route.index && nakedIndex) {
4145 // Remove existing ones when we're not
4146 let params = new URLSearchParams(path.search);
4147 let indexValues = params.getAll("index");
4148 params.delete("index");
4149 indexValues.filter((v) => v).forEach((v) => params.append("index", v));
4150 let qs = params.toString();
4151 path.search = qs ? `?${qs}` : "";
4152 }
4153 }
4154
4155 // If we're operating within a basename, prepend it to the pathname. If
4156 // this is a root navigation, then just use the raw basename which allows
4157 // the basename to have full control over the presence of a trailing slash
4158 // on root actions
4159 if (prependBasename && basename !== "/") {
4160 path.pathname =
4161 path.pathname === "/" ? basename : joinPaths([basename, path.pathname]);
4162 }
4163
4164 return createPath(path);
4165}
4166
4167// Normalize navigation options by converting formMethod=GET formData objects to
4168// URLSearchParams so they behave identically to links with query params
4169function normalizeNavigateOptions(
4170 normalizeFormMethod: boolean,
4171 isFetcher: boolean,
4172 path: string,
4173 opts?: BaseNavigateOrFetchOptions
4174): {
4175 path: string;
4176 submission?: Submission;
4177 error?: ErrorResponseImpl;
4178} {
4179 // Return location verbatim on non-submission navigations
4180 if (!opts || !isSubmissionNavigation(opts)) {
4181 return { path };
4182 }
4183
4184 if (opts.formMethod && !isValidMethod(opts.formMethod)) {
4185 return {
4186 path,
4187 error: getInternalRouterError(405, { method: opts.formMethod }),
4188 };
4189 }
4190
4191 let getInvalidBodyError = () => ({
4192 path,
4193 error: getInternalRouterError(400, { type: "invalid-body" }),
4194 });
4195
4196 // Create a Submission on non-GET navigations
4197 let rawFormMethod = opts.formMethod || "get";
4198 let formMethod = normalizeFormMethod
4199 ? (rawFormMethod.toUpperCase() as V7_FormMethod)
4200 : (rawFormMethod.toLowerCase() as FormMethod);
4201 let formAction = stripHashFromPath(path);
4202
4203 if (opts.body !== undefined) {
4204 if (opts.formEncType === "text/plain") {
4205 // text only support POST/PUT/PATCH/DELETE submissions
4206 if (!isMutationMethod(formMethod)) {
4207 return getInvalidBodyError();
4208 }
4209
4210 let text =
4211 typeof opts.body === "string"
4212 ? opts.body
4213 : opts.body instanceof FormData ||
4214 opts.body instanceof URLSearchParams
4215 ? // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#plain-text-form-data
4216 Array.from(opts.body.entries()).reduce(
4217 (acc, [name, value]) => `${acc}${name}=${value}\n`,
4218 ""
4219 )
4220 : String(opts.body);
4221
4222 return {
4223 path,
4224 submission: {
4225 formMethod,
4226 formAction,
4227 formEncType: opts.formEncType,
4228 formData: undefined,
4229 json: undefined,
4230 text,
4231 },
4232 };
4233 } else if (opts.formEncType === "application/json") {
4234 // json only supports POST/PUT/PATCH/DELETE submissions
4235 if (!isMutationMethod(formMethod)) {
4236 return getInvalidBodyError();
4237 }
4238
4239 try {
4240 let json =
4241 typeof opts.body === "string" ? JSON.parse(opts.body) : opts.body;
4242
4243 return {
4244 path,
4245 submission: {
4246 formMethod,
4247 formAction,
4248 formEncType: opts.formEncType,
4249 formData: undefined,
4250 json,
4251 text: undefined,
4252 },
4253 };
4254 } catch (e) {
4255 return getInvalidBodyError();
4256 }
4257 }
4258 }
4259
4260 invariant(
4261 typeof FormData === "function",
4262 "FormData is not available in this environment"
4263 );
4264
4265 let searchParams: URLSearchParams;
4266 let formData: FormData;
4267
4268 if (opts.formData) {
4269 searchParams = convertFormDataToSearchParams(opts.formData);
4270 formData = opts.formData;
4271 } else if (opts.body instanceof FormData) {
4272 searchParams = convertFormDataToSearchParams(opts.body);
4273 formData = opts.body;
4274 } else if (opts.body instanceof URLSearchParams) {
4275 searchParams = opts.body;
4276 formData = convertSearchParamsToFormData(searchParams);
4277 } else if (opts.body == null) {
4278 searchParams = new URLSearchParams();
4279 formData = new FormData();
4280 } else {
4281 try {
4282 searchParams = new URLSearchParams(opts.body);
4283 formData = convertSearchParamsToFormData(searchParams);
4284 } catch (e) {
4285 return getInvalidBodyError();
4286 }
4287 }
4288
4289 let submission: Submission = {
4290 formMethod,
4291 formAction,
4292 formEncType:
4293 (opts && opts.formEncType) || "application/x-www-form-urlencoded",
4294 formData,
4295 json: undefined,
4296 text: undefined,
4297 };
4298
4299 if (isMutationMethod(submission.formMethod)) {
4300 return { path, submission };
4301 }
4302
4303 // Flatten submission onto URLSearchParams for GET submissions
4304 let parsedPath = parsePath(path);
4305 // On GET navigation submissions we can drop the ?index param from the
4306 // resulting location since all loaders will run. But fetcher GET submissions
4307 // only run a single loader so we need to preserve any incoming ?index params
4308 if (isFetcher && parsedPath.search && hasNakedIndexQuery(parsedPath.search)) {
4309 searchParams.append("index", "");
4310 }
4311 parsedPath.search = `?${searchParams}`;
4312
4313 return { path: createPath(parsedPath), submission };
4314}
4315
4316// Filter out all routes at/below any caught error as they aren't going to
4317// render so we don't need to load them
4318function getLoaderMatchesUntilBoundary(
4319 matches: AgnosticDataRouteMatch[],
4320 boundaryId: string,
4321 includeBoundary = false
4322) {
4323 let index = matches.findIndex((m) => m.route.id === boundaryId);
4324 if (index >= 0) {
4325 return matches.slice(0, includeBoundary ? index + 1 : index);
4326 }
4327 return matches;
4328}
4329
4330function getMatchesToLoad(
4331 history: History,
4332 state: RouterState,
4333 matches: AgnosticDataRouteMatch[],
4334 submission: Submission | undefined,
4335 location: Location,
4336 initialHydration: boolean,
4337 skipActionErrorRevalidation: boolean,
4338 isRevalidationRequired: boolean,
4339 cancelledDeferredRoutes: string[],
4340 cancelledFetcherLoads: Set<string>,
4341 deletedFetchers: Set<string>,
4342 fetchLoadMatches: Map<string, FetchLoadMatch>,
4343 fetchRedirectIds: Set<string>,
4344 routesToUse: AgnosticDataRouteObject[],
4345 basename: string | undefined,
4346 pendingActionResult?: PendingActionResult
4347): [AgnosticDataRouteMatch[], RevalidatingFetcher[]] {
4348 let actionResult = pendingActionResult
4349 ? isErrorResult(pendingActionResult[1])
4350 ? pendingActionResult[1].error
4351 : pendingActionResult[1].data
4352 : undefined;
4353 let currentUrl = history.createURL(state.location);
4354 let nextUrl = history.createURL(location);
4355
4356 // Pick navigation matches that are net-new or qualify for revalidation
4357 let boundaryMatches = matches;
4358 if (initialHydration && state.errors) {
4359 // On initial hydration, only consider matches up to _and including_ the boundary.
4360 // This is inclusive to handle cases where a server loader ran successfully,
4361 // a child server loader bubbled up to this route, but this route has
4362 // `clientLoader.hydrate` so we want to still run the `clientLoader` so that
4363 // we have a complete version of `loaderData`
4364 boundaryMatches = getLoaderMatchesUntilBoundary(
4365 matches,
4366 Object.keys(state.errors)[0],
4367 true
4368 );
4369 } else if (pendingActionResult && isErrorResult(pendingActionResult[1])) {
4370 // If an action threw an error, we call loaders up to, but not including the
4371 // boundary
4372 boundaryMatches = getLoaderMatchesUntilBoundary(
4373 matches,
4374 pendingActionResult[0]
4375 );
4376 }
4377
4378 // Don't revalidate loaders by default after action 4xx/5xx responses
4379 // when the flag is enabled. They can still opt-into revalidation via
4380 // `shouldRevalidate` via `actionResult`
4381 let actionStatus = pendingActionResult
4382 ? pendingActionResult[1].statusCode
4383 : undefined;
4384 let shouldSkipRevalidation =
4385 skipActionErrorRevalidation && actionStatus && actionStatus >= 400;
4386
4387 let navigationMatches = boundaryMatches.filter((match, index) => {
4388 let { route } = match;
4389 if (route.lazy) {
4390 // We haven't loaded this route yet so we don't know if it's got a loader!
4391 return true;
4392 }
4393
4394 if (route.loader == null) {
4395 return false;
4396 }
4397
4398 if (initialHydration) {
4399 return shouldLoadRouteOnHydration(route, state.loaderData, state.errors);
4400 }
4401
4402 // Always call the loader on new route instances and pending defer cancellations
4403 if (
4404 isNewLoader(state.loaderData, state.matches[index], match) ||
4405 cancelledDeferredRoutes.some((id) => id === match.route.id)
4406 ) {
4407 return true;
4408 }
4409
4410 // This is the default implementation for when we revalidate. If the route
4411 // provides it's own implementation, then we give them full control but
4412 // provide this value so they can leverage it if needed after they check
4413 // their own specific use cases
4414 let currentRouteMatch = state.matches[index];
4415 let nextRouteMatch = match;
4416
4417 return shouldRevalidateLoader(match, {
4418 currentUrl,
4419 currentParams: currentRouteMatch.params,
4420 nextUrl,
4421 nextParams: nextRouteMatch.params,
4422 ...submission,
4423 actionResult,
4424 actionStatus,
4425 defaultShouldRevalidate: shouldSkipRevalidation
4426 ? false
4427 : // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate
4428 isRevalidationRequired ||
4429 currentUrl.pathname + currentUrl.search ===
4430 nextUrl.pathname + nextUrl.search ||
4431 // Search params affect all loaders
4432 currentUrl.search !== nextUrl.search ||
4433 isNewRouteInstance(currentRouteMatch, nextRouteMatch),
4434 });
4435 });
4436
4437 // Pick fetcher.loads that need to be revalidated
4438 let revalidatingFetchers: RevalidatingFetcher[] = [];
4439 fetchLoadMatches.forEach((f, key) => {
4440 // Don't revalidate:
4441 // - on initial hydration (shouldn't be any fetchers then anyway)
4442 // - if fetcher won't be present in the subsequent render
4443 // - no longer matches the URL (v7_fetcherPersist=false)
4444 // - was unmounted but persisted due to v7_fetcherPersist=true
4445 if (
4446 initialHydration ||
4447 !matches.some((m) => m.route.id === f.routeId) ||
4448 deletedFetchers.has(key)
4449 ) {
4450 return;
4451 }
4452
4453 let fetcherMatches = matchRoutes(routesToUse, f.path, basename);
4454
4455 // If the fetcher path no longer matches, push it in with null matches so
4456 // we can trigger a 404 in callLoadersAndMaybeResolveData. Note this is
4457 // currently only a use-case for Remix HMR where the route tree can change
4458 // at runtime and remove a route previously loaded via a fetcher
4459 if (!fetcherMatches) {
4460 revalidatingFetchers.push({
4461 key,
4462 routeId: f.routeId,
4463 path: f.path,
4464 matches: null,
4465 match: null,
4466 controller: null,
4467 });
4468 return;
4469 }
4470
4471 // Revalidating fetchers are decoupled from the route matches since they
4472 // load from a static href. They revalidate based on explicit revalidation
4473 // (submission, useRevalidator, or X-Remix-Revalidate)
4474 let fetcher = state.fetchers.get(key);
4475 let fetcherMatch = getTargetMatch(fetcherMatches, f.path);
4476
4477 let shouldRevalidate = false;
4478 if (fetchRedirectIds.has(key)) {
4479 // Never trigger a revalidation of an actively redirecting fetcher
4480 shouldRevalidate = false;
4481 } else if (cancelledFetcherLoads.has(key)) {
4482 // Always mark for revalidation if the fetcher was cancelled
4483 cancelledFetcherLoads.delete(key);
4484 shouldRevalidate = true;
4485 } else if (
4486 fetcher &&
4487 fetcher.state !== "idle" &&
4488 fetcher.data === undefined
4489 ) {
4490 // If the fetcher hasn't ever completed loading yet, then this isn't a
4491 // revalidation, it would just be a brand new load if an explicit
4492 // revalidation is required
4493 shouldRevalidate = isRevalidationRequired;
4494 } else {
4495 // Otherwise fall back on any user-defined shouldRevalidate, defaulting
4496 // to explicit revalidations only
4497 shouldRevalidate = shouldRevalidateLoader(fetcherMatch, {
4498 currentUrl,
4499 currentParams: state.matches[state.matches.length - 1].params,
4500 nextUrl,
4501 nextParams: matches[matches.length - 1].params,
4502 ...submission,
4503 actionResult,
4504 actionStatus,
4505 defaultShouldRevalidate: shouldSkipRevalidation
4506 ? false
4507 : isRevalidationRequired,
4508 });
4509 }
4510
4511 if (shouldRevalidate) {
4512 revalidatingFetchers.push({
4513 key,
4514 routeId: f.routeId,
4515 path: f.path,
4516 matches: fetcherMatches,
4517 match: fetcherMatch,
4518 controller: new AbortController(),
4519 });
4520 }
4521 });
4522
4523 return [navigationMatches, revalidatingFetchers];
4524}
4525
4526function shouldLoadRouteOnHydration(
4527 route: AgnosticDataRouteObject,
4528 loaderData: RouteData | null | undefined,
4529 errors: RouteData | null | undefined
4530) {
4531 // We dunno if we have a loader - gotta find out!
4532 if (route.lazy) {
4533 return true;
4534 }
4535
4536 // No loader, nothing to initialize
4537 if (!route.loader) {
4538 return false;
4539 }
4540
4541 let hasData = loaderData != null && loaderData[route.id] !== undefined;
4542 let hasError = errors != null && errors[route.id] !== undefined;
4543
4544 // Don't run if we error'd during SSR
4545 if (!hasData && hasError) {
4546 return false;
4547 }
4548
4549 // Explicitly opting-in to running on hydration
4550 if (typeof route.loader === "function" && route.loader.hydrate === true) {
4551 return true;
4552 }
4553
4554 // Otherwise, run if we're not yet initialized with anything
4555 return !hasData && !hasError;
4556}
4557
4558function isNewLoader(
4559 currentLoaderData: RouteData,
4560 currentMatch: AgnosticDataRouteMatch,
4561 match: AgnosticDataRouteMatch
4562) {
4563 let isNew =
4564 // [a] -> [a, b]
4565 !currentMatch ||
4566 // [a, b] -> [a, c]
4567 match.route.id !== currentMatch.route.id;
4568
4569 // Handle the case that we don't have data for a re-used route, potentially
4570 // from a prior error or from a cancelled pending deferred
4571 let isMissingData = currentLoaderData[match.route.id] === undefined;
4572
4573 // Always load if this is a net-new route or we don't yet have data
4574 return isNew || isMissingData;
4575}
4576
4577function isNewRouteInstance(
4578 currentMatch: AgnosticDataRouteMatch,
4579 match: AgnosticDataRouteMatch
4580) {
4581 let currentPath = currentMatch.route.path;
4582 return (
4583 // param change for this match, /users/123 -> /users/456
4584 currentMatch.pathname !== match.pathname ||
4585 // splat param changed, which is not present in match.path
4586 // e.g. /files/images/avatar.jpg -> files/finances.xls
4587 (currentPath != null &&
4588 currentPath.endsWith("*") &&
4589 currentMatch.params["*"] !== match.params["*"])
4590 );
4591}
4592
4593function shouldRevalidateLoader(
4594 loaderMatch: AgnosticDataRouteMatch,
4595 arg: ShouldRevalidateFunctionArgs
4596) {
4597 if (loaderMatch.route.shouldRevalidate) {
4598 let routeChoice = loaderMatch.route.shouldRevalidate(arg);
4599 if (typeof routeChoice === "boolean") {
4600 return routeChoice;
4601 }
4602 }
4603
4604 return arg.defaultShouldRevalidate;
4605}
4606
4607function patchRoutesImpl(
4608 routeId: string | null,
4609 children: AgnosticRouteObject[],
4610 routesToUse: AgnosticDataRouteObject[],
4611 manifest: RouteManifest,
4612 mapRouteProperties: MapRoutePropertiesFunction
4613) {
4614 let childrenToPatch: AgnosticDataRouteObject[];
4615 if (routeId) {
4616 let route = manifest[routeId];
4617 invariant(
4618 route,
4619 `No route found to patch children into: routeId = ${routeId}`
4620 );
4621 if (!route.children) {
4622 route.children = [];
4623 }
4624 childrenToPatch = route.children;
4625 } else {
4626 childrenToPatch = routesToUse;
4627 }
4628
4629 // Don't patch in routes we already know about so that `patch` is idempotent
4630 // to simplify user-land code. This is useful because we re-call the
4631 // `patchRoutesOnNavigation` function for matched routes with params.
4632 let uniqueChildren = children.filter(
4633 (newRoute) =>
4634 !childrenToPatch.some((existingRoute) =>
4635 isSameRoute(newRoute, existingRoute)
4636 )
4637 );
4638
4639 let newRoutes = convertRoutesToDataRoutes(
4640 uniqueChildren,
4641 mapRouteProperties,
4642 [routeId || "_", "patch", String(childrenToPatch?.length || "0")],
4643 manifest
4644 );
4645
4646 childrenToPatch.push(...newRoutes);
4647}
4648
4649function isSameRoute(
4650 newRoute: AgnosticRouteObject,
4651 existingRoute: AgnosticRouteObject
4652): boolean {
4653 // Most optimal check is by id
4654 if (
4655 "id" in newRoute &&
4656 "id" in existingRoute &&
4657 newRoute.id === existingRoute.id
4658 ) {
4659 return true;
4660 }
4661
4662 // Second is by pathing differences
4663 if (
4664 !(
4665 newRoute.index === existingRoute.index &&
4666 newRoute.path === existingRoute.path &&
4667 newRoute.caseSensitive === existingRoute.caseSensitive
4668 )
4669 ) {
4670 return false;
4671 }
4672
4673 // Pathless layout routes are trickier since we need to check children.
4674 // If they have no children then they're the same as far as we can tell
4675 if (
4676 (!newRoute.children || newRoute.children.length === 0) &&
4677 (!existingRoute.children || existingRoute.children.length === 0)
4678 ) {
4679 return true;
4680 }
4681
4682 // Otherwise, we look to see if every child in the new route is already
4683 // represented in the existing route's children
4684 return newRoute.children!.every((aChild, i) =>
4685 existingRoute.children?.some((bChild) => isSameRoute(aChild, bChild))
4686 );
4687}
4688
4689/**
4690 * Execute route.lazy() methods to lazily load route modules (loader, action,
4691 * shouldRevalidate) and update the routeManifest in place which shares objects
4692 * with dataRoutes so those get updated as well.
4693 */
4694async function loadLazyRouteModule(
4695 route: AgnosticDataRouteObject,
4696 mapRouteProperties: MapRoutePropertiesFunction,
4697 manifest: RouteManifest
4698) {
4699 if (!route.lazy) {
4700 return;
4701 }
4702
4703 let lazyRoute = await route.lazy();
4704
4705 // If the lazy route function was executed and removed by another parallel
4706 // call then we can return - first lazy() to finish wins because the return
4707 // value of lazy is expected to be static
4708 if (!route.lazy) {
4709 return;
4710 }
4711
4712 let routeToUpdate = manifest[route.id];
4713 invariant(routeToUpdate, "No route found in manifest");
4714
4715 // Update the route in place. This should be safe because there's no way
4716 // we could yet be sitting on this route as we can't get there without
4717 // resolving lazy() first.
4718 //
4719 // This is different than the HMR "update" use-case where we may actively be
4720 // on the route being updated. The main concern boils down to "does this
4721 // mutation affect any ongoing navigations or any current state.matches
4722 // values?". If not, it should be safe to update in place.
4723 let routeUpdates: Record<string, any> = {};
4724 for (let lazyRouteProperty in lazyRoute) {
4725 let staticRouteValue =
4726 routeToUpdate[lazyRouteProperty as keyof typeof routeToUpdate];
4727
4728 let isPropertyStaticallyDefined =
4729 staticRouteValue !== undefined &&
4730 // This property isn't static since it should always be updated based
4731 // on the route updates
4732 lazyRouteProperty !== "hasErrorBoundary";
4733
4734 warning(
4735 !isPropertyStaticallyDefined,
4736 `Route "${routeToUpdate.id}" has a static property "${lazyRouteProperty}" ` +
4737 `defined but its lazy function is also returning a value for this property. ` +
4738 `The lazy route property "${lazyRouteProperty}" will be ignored.`
4739 );
4740
4741 if (
4742 !isPropertyStaticallyDefined &&
4743 !immutableRouteKeys.has(lazyRouteProperty as ImmutableRouteKey)
4744 ) {
4745 routeUpdates[lazyRouteProperty] =
4746 lazyRoute[lazyRouteProperty as keyof typeof lazyRoute];
4747 }
4748 }
4749
4750 // Mutate the route with the provided updates. Do this first so we pass
4751 // the updated version to mapRouteProperties
4752 Object.assign(routeToUpdate, routeUpdates);
4753
4754 // Mutate the `hasErrorBoundary` property on the route based on the route
4755 // updates and remove the `lazy` function so we don't resolve the lazy
4756 // route again.
4757 Object.assign(routeToUpdate, {
4758 // To keep things framework agnostic, we use the provided
4759 // `mapRouteProperties` (or wrapped `detectErrorBoundary`) function to
4760 // set the framework-aware properties (`element`/`hasErrorBoundary`) since
4761 // the logic will differ between frameworks.
4762 ...mapRouteProperties(routeToUpdate),
4763 lazy: undefined,
4764 });
4765}
4766
4767// Default implementation of `dataStrategy` which fetches all loaders in parallel
4768async function defaultDataStrategy({
4769 matches,
4770}: DataStrategyFunctionArgs): ReturnType<DataStrategyFunction> {
4771 let matchesToLoad = matches.filter((m) => m.shouldLoad);
4772 let results = await Promise.all(matchesToLoad.map((m) => m.resolve()));
4773 return results.reduce(
4774 (acc, result, i) =>
4775 Object.assign(acc, { [matchesToLoad[i].route.id]: result }),
4776 {}
4777 );
4778}
4779
4780async function callDataStrategyImpl(
4781 dataStrategyImpl: DataStrategyFunction,
4782 type: "loader" | "action",
4783 state: RouterState | null,
4784 request: Request,
4785 matchesToLoad: AgnosticDataRouteMatch[],
4786 matches: AgnosticDataRouteMatch[],
4787 fetcherKey: string | null,
4788 manifest: RouteManifest,
4789 mapRouteProperties: MapRoutePropertiesFunction,
4790 requestContext?: unknown
4791): Promise<Record<string, DataStrategyResult>> {
4792 let loadRouteDefinitionsPromises = matches.map((m) =>
4793 m.route.lazy
4794 ? loadLazyRouteModule(m.route, mapRouteProperties, manifest)
4795 : undefined
4796 );
4797
4798 let dsMatches = matches.map((match, i) => {
4799 let loadRoutePromise = loadRouteDefinitionsPromises[i];
4800 let shouldLoad = matchesToLoad.some((m) => m.route.id === match.route.id);
4801 // `resolve` encapsulates route.lazy(), executing the loader/action,
4802 // and mapping return values/thrown errors to a `DataStrategyResult`. Users
4803 // can pass a callback to take fine-grained control over the execution
4804 // of the loader/action
4805 let resolve: DataStrategyMatch["resolve"] = async (handlerOverride) => {
4806 if (
4807 handlerOverride &&
4808 request.method === "GET" &&
4809 (match.route.lazy || match.route.loader)
4810 ) {
4811 shouldLoad = true;
4812 }
4813 return shouldLoad
4814 ? callLoaderOrAction(
4815 type,
4816 request,
4817 match,
4818 loadRoutePromise,
4819 handlerOverride,
4820 requestContext
4821 )
4822 : Promise.resolve({ type: ResultType.data, result: undefined });
4823 };
4824
4825 return {
4826 ...match,
4827 shouldLoad,
4828 resolve,
4829 };
4830 });
4831
4832 // Send all matches here to allow for a middleware-type implementation.
4833 // handler will be a no-op for unneeded routes and we filter those results
4834 // back out below.
4835 let results = await dataStrategyImpl({
4836 matches: dsMatches,
4837 request,
4838 params: matches[0].params,
4839 fetcherKey,
4840 context: requestContext,
4841 });
4842
4843 // Wait for all routes to load here but 'swallow the error since we want
4844 // it to bubble up from the `await loadRoutePromise` in `callLoaderOrAction` -
4845 // called from `match.resolve()`
4846 try {
4847 await Promise.all(loadRouteDefinitionsPromises);
4848 } catch (e) {
4849 // No-op
4850 }
4851
4852 return results;
4853}
4854
4855// Default logic for calling a loader/action is the user has no specified a dataStrategy
4856async function callLoaderOrAction(
4857 type: "loader" | "action",
4858 request: Request,
4859 match: AgnosticDataRouteMatch,
4860 loadRoutePromise: Promise<void> | undefined,
4861 handlerOverride: Parameters<DataStrategyMatch["resolve"]>[0],
4862 staticContext?: unknown
4863): Promise<DataStrategyResult> {
4864 let result: DataStrategyResult;
4865 let onReject: (() => void) | undefined;
4866
4867 let runHandler = (
4868 handler: AgnosticRouteObject["loader"] | AgnosticRouteObject["action"]
4869 ): Promise<DataStrategyResult> => {
4870 // Setup a promise we can race against so that abort signals short circuit
4871 let reject: () => void;
4872 // This will never resolve so safe to type it as Promise<DataStrategyResult> to
4873 // satisfy the function return value
4874 let abortPromise = new Promise<DataStrategyResult>((_, r) => (reject = r));
4875 onReject = () => reject();
4876 request.signal.addEventListener("abort", onReject);
4877
4878 let actualHandler = (ctx?: unknown) => {
4879 if (typeof handler !== "function") {
4880 return Promise.reject(
4881 new Error(
4882 `You cannot call the handler for a route which defines a boolean ` +
4883 `"${type}" [routeId: ${match.route.id}]`
4884 )
4885 );
4886 }
4887 return handler(
4888 {
4889 request,
4890 params: match.params,
4891 context: staticContext,
4892 },
4893 ...(ctx !== undefined ? [ctx] : [])
4894 );
4895 };
4896
4897 let handlerPromise: Promise<DataStrategyResult> = (async () => {
4898 try {
4899 let val = await (handlerOverride
4900 ? handlerOverride((ctx: unknown) => actualHandler(ctx))
4901 : actualHandler());
4902 return { type: "data", result: val };
4903 } catch (e) {
4904 return { type: "error", result: e };
4905 }
4906 })();
4907
4908 return Promise.race([handlerPromise, abortPromise]);
4909 };
4910
4911 try {
4912 let handler = match.route[type];
4913
4914 // If we have a route.lazy promise, await that first
4915 if (loadRoutePromise) {
4916 if (handler) {
4917 // Run statically defined handler in parallel with lazy()
4918 let handlerError;
4919 let [value] = await Promise.all([
4920 // If the handler throws, don't let it immediately bubble out,
4921 // since we need to let the lazy() execution finish so we know if this
4922 // route has a boundary that can handle the error
4923 runHandler(handler).catch((e) => {
4924 handlerError = e;
4925 }),
4926 loadRoutePromise,
4927 ]);
4928 if (handlerError !== undefined) {
4929 throw handlerError;
4930 }
4931 result = value!;
4932 } else {
4933 // Load lazy route module, then run any returned handler
4934 await loadRoutePromise;
4935
4936 handler = match.route[type];
4937 if (handler) {
4938 // Handler still runs even if we got interrupted to maintain consistency
4939 // with un-abortable behavior of handler execution on non-lazy or
4940 // previously-lazy-loaded routes
4941 result = await runHandler(handler);
4942 } else if (type === "action") {
4943 let url = new URL(request.url);
4944 let pathname = url.pathname + url.search;
4945 throw getInternalRouterError(405, {
4946 method: request.method,
4947 pathname,
4948 routeId: match.route.id,
4949 });
4950 } else {
4951 // lazy() route has no loader to run. Short circuit here so we don't
4952 // hit the invariant below that errors on returning undefined.
4953 return { type: ResultType.data, result: undefined };
4954 }
4955 }
4956 } else if (!handler) {
4957 let url = new URL(request.url);
4958 let pathname = url.pathname + url.search;
4959 throw getInternalRouterError(404, {
4960 pathname,
4961 });
4962 } else {
4963 result = await runHandler(handler);
4964 }
4965
4966 invariant(
4967 result.result !== undefined,
4968 `You defined ${type === "action" ? "an action" : "a loader"} for route ` +
4969 `"${match.route.id}" but didn't return anything from your \`${type}\` ` +
4970 `function. Please return a value or \`null\`.`
4971 );
4972 } catch (e) {
4973 // We should already be catching and converting normal handler executions to
4974 // DataStrategyResults and returning them, so anything that throws here is an
4975 // unexpected error we still need to wrap
4976 return { type: ResultType.error, result: e };
4977 } finally {
4978 if (onReject) {
4979 request.signal.removeEventListener("abort", onReject);
4980 }
4981 }
4982
4983 return result;
4984}
4985
4986async function convertDataStrategyResultToDataResult(
4987 dataStrategyResult: DataStrategyResult
4988): Promise<DataResult> {
4989 let { result, type } = dataStrategyResult;
4990
4991 if (isResponse(result)) {
4992 let data: any;
4993
4994 try {
4995 let contentType = result.headers.get("Content-Type");
4996 // Check between word boundaries instead of startsWith() due to the last
4997 // paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type
4998 if (contentType && /\bapplication\/json\b/.test(contentType)) {
4999 if (result.body == null) {
5000 data = null;
5001 } else {
5002 data = await result.json();
5003 }
5004 } else {
5005 data = await result.text();
5006 }
5007 } catch (e) {
5008 return { type: ResultType.error, error: e };
5009 }
5010
5011 if (type === ResultType.error) {
5012 return {
5013 type: ResultType.error,
5014 error: new ErrorResponseImpl(result.status, result.statusText, data),
5015 statusCode: result.status,
5016 headers: result.headers,
5017 };
5018 }
5019
5020 return {
5021 type: ResultType.data,
5022 data,
5023 statusCode: result.status,
5024 headers: result.headers,
5025 };
5026 }
5027
5028 if (type === ResultType.error) {
5029 if (isDataWithResponseInit(result)) {
5030 if (result.data instanceof Error) {
5031 return {
5032 type: ResultType.error,
5033 error: result.data,
5034 statusCode: result.init?.status,
5035 };
5036 }
5037
5038 // Convert thrown data() to ErrorResponse instances
5039 result = new ErrorResponseImpl(
5040 result.init?.status || 500,
5041 undefined,
5042 result.data
5043 );
5044 }
5045 return {
5046 type: ResultType.error,
5047 error: result,
5048 statusCode: isRouteErrorResponse(result) ? result.status : undefined,
5049 };
5050 }
5051
5052 if (isDeferredData(result)) {
5053 return {
5054 type: ResultType.deferred,
5055 deferredData: result,
5056 statusCode: result.init?.status,
5057 headers: result.init?.headers && new Headers(result.init.headers),
5058 };
5059 }
5060
5061 if (isDataWithResponseInit(result)) {
5062 return {
5063 type: ResultType.data,
5064 data: result.data,
5065 statusCode: result.init?.status,
5066 headers: result.init?.headers
5067 ? new Headers(result.init.headers)
5068 : undefined,
5069 };
5070 }
5071
5072 return { type: ResultType.data, data: result };
5073}
5074
5075// Support relative routing in internal redirects
5076function normalizeRelativeRoutingRedirectResponse(
5077 response: Response,
5078 request: Request,
5079 routeId: string,
5080 matches: AgnosticDataRouteMatch[],
5081 basename: string,
5082 v7_relativeSplatPath: boolean
5083) {
5084 let location = response.headers.get("Location");
5085 invariant(
5086 location,
5087 "Redirects returned/thrown from loaders/actions must have a Location header"
5088 );
5089
5090 if (!ABSOLUTE_URL_REGEX.test(location)) {
5091 let trimmedMatches = matches.slice(
5092 0,
5093 matches.findIndex((m) => m.route.id === routeId) + 1
5094 );
5095 location = normalizeTo(
5096 new URL(request.url),
5097 trimmedMatches,
5098 basename,
5099 true,
5100 location,
5101 v7_relativeSplatPath
5102 );
5103 response.headers.set("Location", location);
5104 }
5105
5106 return response;
5107}
5108
5109function normalizeRedirectLocation(
5110 location: string,
5111 currentUrl: URL,
5112 basename: string
5113): string {
5114 if (ABSOLUTE_URL_REGEX.test(location)) {
5115 // Strip off the protocol+origin for same-origin + same-basename absolute redirects
5116 let normalizedLocation = location;
5117 let url = normalizedLocation.startsWith("//")
5118 ? new URL(currentUrl.protocol + normalizedLocation)
5119 : new URL(normalizedLocation);
5120 let isSameBasename = stripBasename(url.pathname, basename) != null;
5121 if (url.origin === currentUrl.origin && isSameBasename) {
5122 return url.pathname + url.search + url.hash;
5123 }
5124 }
5125 return location;
5126}
5127
5128// Utility method for creating the Request instances for loaders/actions during
5129// client-side navigations and fetches. During SSR we will always have a
5130// Request instance from the static handler (query/queryRoute)
5131function createClientSideRequest(
5132 history: History,
5133 location: string | Location,
5134 signal: AbortSignal,
5135 submission?: Submission
5136): Request {
5137 let url = history.createURL(stripHashFromPath(location)).toString();
5138 let init: RequestInit = { signal };
5139
5140 if (submission && isMutationMethod(submission.formMethod)) {
5141 let { formMethod, formEncType } = submission;
5142 // Didn't think we needed this but it turns out unlike other methods, patch
5143 // won't be properly normalized to uppercase and results in a 405 error.
5144 // See: https://fetch.spec.whatwg.org/#concept-method
5145 init.method = formMethod.toUpperCase();
5146
5147 if (formEncType === "application/json") {
5148 init.headers = new Headers({ "Content-Type": formEncType });
5149 init.body = JSON.stringify(submission.json);
5150 } else if (formEncType === "text/plain") {
5151 // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
5152 init.body = submission.text;
5153 } else if (
5154 formEncType === "application/x-www-form-urlencoded" &&
5155 submission.formData
5156 ) {
5157 // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
5158 init.body = convertFormDataToSearchParams(submission.formData);
5159 } else {
5160 // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
5161 init.body = submission.formData;
5162 }
5163 }
5164
5165 return new Request(url, init);
5166}
5167
5168function convertFormDataToSearchParams(formData: FormData): URLSearchParams {
5169 let searchParams = new URLSearchParams();
5170
5171 for (let [key, value] of formData.entries()) {
5172 // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#converting-an-entry-list-to-a-list-of-name-value-pairs
5173 searchParams.append(key, typeof value === "string" ? value : value.name);
5174 }
5175
5176 return searchParams;
5177}
5178
5179function convertSearchParamsToFormData(
5180 searchParams: URLSearchParams
5181): FormData {
5182 let formData = new FormData();
5183 for (let [key, value] of searchParams.entries()) {
5184 formData.append(key, value);
5185 }
5186 return formData;
5187}
5188
5189function processRouteLoaderData(
5190 matches: AgnosticDataRouteMatch[],
5191 results: Record<string, DataResult>,
5192 pendingActionResult: PendingActionResult | undefined,
5193 activeDeferreds: Map<string, DeferredData>,
5194 skipLoaderErrorBubbling: boolean
5195): {
5196 loaderData: RouterState["loaderData"];
5197 errors: RouterState["errors"] | null;
5198 statusCode: number;
5199 loaderHeaders: Record<string, Headers>;
5200} {
5201 // Fill in loaderData/errors from our loaders
5202 let loaderData: RouterState["loaderData"] = {};
5203 let errors: RouterState["errors"] | null = null;
5204 let statusCode: number | undefined;
5205 let foundError = false;
5206 let loaderHeaders: Record<string, Headers> = {};
5207 let pendingError =
5208 pendingActionResult && isErrorResult(pendingActionResult[1])
5209 ? pendingActionResult[1].error
5210 : undefined;
5211
5212 // Process loader results into state.loaderData/state.errors
5213 matches.forEach((match) => {
5214 if (!(match.route.id in results)) {
5215 return;
5216 }
5217 let id = match.route.id;
5218 let result = results[id];
5219 invariant(
5220 !isRedirectResult(result),
5221 "Cannot handle redirect results in processLoaderData"
5222 );
5223 if (isErrorResult(result)) {
5224 let error = result.error;
5225 // If we have a pending action error, we report it at the highest-route
5226 // that throws a loader error, and then clear it out to indicate that
5227 // it was consumed
5228 if (pendingError !== undefined) {
5229 error = pendingError;
5230 pendingError = undefined;
5231 }
5232
5233 errors = errors || {};
5234
5235 if (skipLoaderErrorBubbling) {
5236 errors[id] = error;
5237 } else {
5238 // Look upwards from the matched route for the closest ancestor error
5239 // boundary, defaulting to the root match. Prefer higher error values
5240 // if lower errors bubble to the same boundary
5241 let boundaryMatch = findNearestBoundary(matches, id);
5242 if (errors[boundaryMatch.route.id] == null) {
5243 errors[boundaryMatch.route.id] = error;
5244 }
5245 }
5246
5247 // Clear our any prior loaderData for the throwing route
5248 loaderData[id] = undefined;
5249
5250 // Once we find our first (highest) error, we set the status code and
5251 // prevent deeper status codes from overriding
5252 if (!foundError) {
5253 foundError = true;
5254 statusCode = isRouteErrorResponse(result.error)
5255 ? result.error.status
5256 : 500;
5257 }
5258 if (result.headers) {
5259 loaderHeaders[id] = result.headers;
5260 }
5261 } else {
5262 if (isDeferredResult(result)) {
5263 activeDeferreds.set(id, result.deferredData);
5264 loaderData[id] = result.deferredData.data;
5265 // Error status codes always override success status codes, but if all
5266 // loaders are successful we take the deepest status code.
5267 if (
5268 result.statusCode != null &&
5269 result.statusCode !== 200 &&
5270 !foundError
5271 ) {
5272 statusCode = result.statusCode;
5273 }
5274 if (result.headers) {
5275 loaderHeaders[id] = result.headers;
5276 }
5277 } else {
5278 loaderData[id] = result.data;
5279 // Error status codes always override success status codes, but if all
5280 // loaders are successful we take the deepest status code.
5281 if (result.statusCode && result.statusCode !== 200 && !foundError) {
5282 statusCode = result.statusCode;
5283 }
5284 if (result.headers) {
5285 loaderHeaders[id] = result.headers;
5286 }
5287 }
5288 }
5289 });
5290
5291 // If we didn't consume the pending action error (i.e., all loaders
5292 // resolved), then consume it here. Also clear out any loaderData for the
5293 // throwing route
5294 if (pendingError !== undefined && pendingActionResult) {
5295 errors = { [pendingActionResult[0]]: pendingError };
5296 loaderData[pendingActionResult[0]] = undefined;
5297 }
5298
5299 return {
5300 loaderData,
5301 errors,
5302 statusCode: statusCode || 200,
5303 loaderHeaders,
5304 };
5305}
5306
5307function processLoaderData(
5308 state: RouterState,
5309 matches: AgnosticDataRouteMatch[],
5310 results: Record<string, DataResult>,
5311 pendingActionResult: PendingActionResult | undefined,
5312 revalidatingFetchers: RevalidatingFetcher[],
5313 fetcherResults: Record<string, DataResult>,
5314 activeDeferreds: Map<string, DeferredData>
5315): {
5316 loaderData: RouterState["loaderData"];
5317 errors?: RouterState["errors"];
5318} {
5319 let { loaderData, errors } = processRouteLoaderData(
5320 matches,
5321 results,
5322 pendingActionResult,
5323 activeDeferreds,
5324 false // This method is only called client side so we always want to bubble
5325 );
5326
5327 // Process results from our revalidating fetchers
5328 revalidatingFetchers.forEach((rf) => {
5329 let { key, match, controller } = rf;
5330 let result = fetcherResults[key];
5331 invariant(result, "Did not find corresponding fetcher result");
5332
5333 // Process fetcher non-redirect errors
5334 if (controller && controller.signal.aborted) {
5335 // Nothing to do for aborted fetchers
5336 return;
5337 } else if (isErrorResult(result)) {
5338 let boundaryMatch = findNearestBoundary(state.matches, match?.route.id);
5339 if (!(errors && errors[boundaryMatch.route.id])) {
5340 errors = {
5341 ...errors,
5342 [boundaryMatch.route.id]: result.error,
5343 };
5344 }
5345 state.fetchers.delete(key);
5346 } else if (isRedirectResult(result)) {
5347 // Should never get here, redirects should get processed above, but we
5348 // keep this to type narrow to a success result in the else
5349 invariant(false, "Unhandled fetcher revalidation redirect");
5350 } else if (isDeferredResult(result)) {
5351 // Should never get here, deferred data should be awaited for fetchers
5352 // in resolveDeferredResults
5353 invariant(false, "Unhandled fetcher deferred data");
5354 } else {
5355 let doneFetcher = getDoneFetcher(result.data);
5356 state.fetchers.set(key, doneFetcher);
5357 }
5358 });
5359
5360 return { loaderData, errors };
5361}
5362
5363function mergeLoaderData(
5364 loaderData: RouteData,
5365 newLoaderData: RouteData,
5366 matches: AgnosticDataRouteMatch[],
5367 errors: RouteData | null | undefined
5368): RouteData {
5369 let mergedLoaderData = { ...newLoaderData };
5370 for (let match of matches) {
5371 let id = match.route.id;
5372 if (newLoaderData.hasOwnProperty(id)) {
5373 if (newLoaderData[id] !== undefined) {
5374 mergedLoaderData[id] = newLoaderData[id];
5375 } else {
5376 // No-op - this is so we ignore existing data if we have a key in the
5377 // incoming object with an undefined value, which is how we unset a prior
5378 // loaderData if we encounter a loader error
5379 }
5380 } else if (loaderData[id] !== undefined && match.route.loader) {
5381 // Preserve existing keys not included in newLoaderData and where a loader
5382 // wasn't removed by HMR
5383 mergedLoaderData[id] = loaderData[id];
5384 }
5385
5386 if (errors && errors.hasOwnProperty(id)) {
5387 // Don't keep any loader data below the boundary
5388 break;
5389 }
5390 }
5391 return mergedLoaderData;
5392}
5393
5394function getActionDataForCommit(
5395 pendingActionResult: PendingActionResult | undefined
5396) {
5397 if (!pendingActionResult) {
5398 return {};
5399 }
5400 return isErrorResult(pendingActionResult[1])
5401 ? {
5402 // Clear out prior actionData on errors
5403 actionData: {},
5404 }
5405 : {
5406 actionData: {
5407 [pendingActionResult[0]]: pendingActionResult[1].data,
5408 },
5409 };
5410}
5411
5412// Find the nearest error boundary, looking upwards from the leaf route (or the
5413// route specified by routeId) for the closest ancestor error boundary,
5414// defaulting to the root match
5415function findNearestBoundary(
5416 matches: AgnosticDataRouteMatch[],
5417 routeId?: string
5418): AgnosticDataRouteMatch {
5419 let eligibleMatches = routeId
5420 ? matches.slice(0, matches.findIndex((m) => m.route.id === routeId) + 1)
5421 : [...matches];
5422 return (
5423 eligibleMatches.reverse().find((m) => m.route.hasErrorBoundary === true) ||
5424 matches[0]
5425 );
5426}
5427
5428function getShortCircuitMatches(routes: AgnosticDataRouteObject[]): {
5429 matches: AgnosticDataRouteMatch[];
5430 route: AgnosticDataRouteObject;
5431} {
5432 // Prefer a root layout route if present, otherwise shim in a route object
5433 let route =
5434 routes.length === 1
5435 ? routes[0]
5436 : routes.find((r) => r.index || !r.path || r.path === "/") || {
5437 id: `__shim-error-route__`,
5438 };
5439
5440 return {
5441 matches: [
5442 {
5443 params: {},
5444 pathname: "",
5445 pathnameBase: "",
5446 route,
5447 },
5448 ],
5449 route,
5450 };
5451}
5452
5453function getInternalRouterError(
5454 status: number,
5455 {
5456 pathname,
5457 routeId,
5458 method,
5459 type,
5460 message,
5461 }: {
5462 pathname?: string;
5463 routeId?: string;
5464 method?: string;
5465 type?: "defer-action" | "invalid-body";
5466 message?: string;
5467 } = {}
5468) {
5469 let statusText = "Unknown Server Error";
5470 let errorMessage = "Unknown @remix-run/router error";
5471
5472 if (status === 400) {
5473 statusText = "Bad Request";
5474 if (method && pathname && routeId) {
5475 errorMessage =
5476 `You made a ${method} request to "${pathname}" but ` +
5477 `did not provide a \`loader\` for route "${routeId}", ` +
5478 `so there is no way to handle the request.`;
5479 } else if (type === "defer-action") {
5480 errorMessage = "defer() is not supported in actions";
5481 } else if (type === "invalid-body") {
5482 errorMessage = "Unable to encode submission body";
5483 }
5484 } else if (status === 403) {
5485 statusText = "Forbidden";
5486 errorMessage = `Route "${routeId}" does not match URL "${pathname}"`;
5487 } else if (status === 404) {
5488 statusText = "Not Found";
5489 errorMessage = `No route matches URL "${pathname}"`;
5490 } else if (status === 405) {
5491 statusText = "Method Not Allowed";
5492 if (method && pathname && routeId) {
5493 errorMessage =
5494 `You made a ${method.toUpperCase()} request to "${pathname}" but ` +
5495 `did not provide an \`action\` for route "${routeId}", ` +
5496 `so there is no way to handle the request.`;
5497 } else if (method) {
5498 errorMessage = `Invalid request method "${method.toUpperCase()}"`;
5499 }
5500 }
5501
5502 return new ErrorResponseImpl(
5503 status || 500,
5504 statusText,
5505 new Error(errorMessage),
5506 true
5507 );
5508}
5509
5510// Find any returned redirect errors, starting from the lowest match
5511function findRedirect(
5512 results: Record<string, DataResult>
5513): { key: string; result: RedirectResult } | undefined {
5514 let entries = Object.entries(results);
5515 for (let i = entries.length - 1; i >= 0; i--) {
5516 let [key, result] = entries[i];
5517 if (isRedirectResult(result)) {
5518 return { key, result };
5519 }
5520 }
5521}
5522
5523function stripHashFromPath(path: To) {
5524 let parsedPath = typeof path === "string" ? parsePath(path) : path;
5525 return createPath({ ...parsedPath, hash: "" });
5526}
5527
5528function isHashChangeOnly(a: Location, b: Location): boolean {
5529 if (a.pathname !== b.pathname || a.search !== b.search) {
5530 return false;
5531 }
5532
5533 if (a.hash === "") {
5534 // /page -> /page#hash
5535 return b.hash !== "";
5536 } else if (a.hash === b.hash) {
5537 // /page#hash -> /page#hash
5538 return true;
5539 } else if (b.hash !== "") {
5540 // /page#hash -> /page#other
5541 return true;
5542 }
5543
5544 // If the hash is removed the browser will re-perform a request to the server
5545 // /page#hash -> /page
5546 return false;
5547}
5548
5549function isPromise<T = unknown>(val: unknown): val is Promise<T> {
5550 return typeof val === "object" && val != null && "then" in val;
5551}
5552
5553function isDataStrategyResult(result: unknown): result is DataStrategyResult {
5554 return (
5555 result != null &&
5556 typeof result === "object" &&
5557 "type" in result &&
5558 "result" in result &&
5559 (result.type === ResultType.data || result.type === ResultType.error)
5560 );
5561}
5562
5563function isRedirectDataStrategyResultResult(result: DataStrategyResult) {
5564 return (
5565 isResponse(result.result) && redirectStatusCodes.has(result.result.status)
5566 );
5567}
5568
5569function isDeferredResult(result: DataResult): result is DeferredResult {
5570 return result.type === ResultType.deferred;
5571}
5572
5573function isErrorResult(result: DataResult): result is ErrorResult {
5574 return result.type === ResultType.error;
5575}
5576
5577function isRedirectResult(result?: DataResult): result is RedirectResult {
5578 return (result && result.type) === ResultType.redirect;
5579}
5580
5581export function isDataWithResponseInit(
5582 value: any
5583): value is DataWithResponseInit<unknown> {
5584 return (
5585 typeof value === "object" &&
5586 value != null &&
5587 "type" in value &&
5588 "data" in value &&
5589 "init" in value &&
5590 value.type === "DataWithResponseInit"
5591 );
5592}
5593
5594export function isDeferredData(value: any): value is DeferredData {
5595 let deferred: DeferredData = value;
5596 return (
5597 deferred &&
5598 typeof deferred === "object" &&
5599 typeof deferred.data === "object" &&
5600 typeof deferred.subscribe === "function" &&
5601 typeof deferred.cancel === "function" &&
5602 typeof deferred.resolveData === "function"
5603 );
5604}
5605
5606function isResponse(value: any): value is Response {
5607 return (
5608 value != null &&
5609 typeof value.status === "number" &&
5610 typeof value.statusText === "string" &&
5611 typeof value.headers === "object" &&
5612 typeof value.body !== "undefined"
5613 );
5614}
5615
5616function isRedirectResponse(result: any): result is Response {
5617 if (!isResponse(result)) {
5618 return false;
5619 }
5620
5621 let status = result.status;
5622 let location = result.headers.get("Location");
5623 return status >= 300 && status <= 399 && location != null;
5624}
5625
5626function isValidMethod(method: string): method is FormMethod | V7_FormMethod {
5627 return validRequestMethods.has(method.toLowerCase() as FormMethod);
5628}
5629
5630function isMutationMethod(
5631 method: string
5632): method is MutationFormMethod | V7_MutationFormMethod {
5633 return validMutationMethods.has(method.toLowerCase() as MutationFormMethod);
5634}
5635
5636async function resolveNavigationDeferredResults(
5637 matches: (AgnosticDataRouteMatch | null)[],
5638 results: Record<string, DataResult>,
5639 signal: AbortSignal,
5640 currentMatches: AgnosticDataRouteMatch[],
5641 currentLoaderData: RouteData
5642) {
5643 let entries = Object.entries(results);
5644 for (let index = 0; index < entries.length; index++) {
5645 let [routeId, result] = entries[index];
5646 let match = matches.find((m) => m?.route.id === routeId);
5647 // If we don't have a match, then we can have a deferred result to do
5648 // anything with. This is for revalidating fetchers where the route was
5649 // removed during HMR
5650 if (!match) {
5651 continue;
5652 }
5653
5654 let currentMatch = currentMatches.find(
5655 (m) => m.route.id === match!.route.id
5656 );
5657 let isRevalidatingLoader =
5658 currentMatch != null &&
5659 !isNewRouteInstance(currentMatch, match) &&
5660 (currentLoaderData && currentLoaderData[match.route.id]) !== undefined;
5661
5662 if (isDeferredResult(result) && isRevalidatingLoader) {
5663 // Note: we do not have to touch activeDeferreds here since we race them
5664 // against the signal in resolveDeferredData and they'll get aborted
5665 // there if needed
5666 await resolveDeferredData(result, signal, false).then((result) => {
5667 if (result) {
5668 results[routeId] = result;
5669 }
5670 });
5671 }
5672 }
5673}
5674
5675async function resolveFetcherDeferredResults(
5676 matches: (AgnosticDataRouteMatch | null)[],
5677 results: Record<string, DataResult>,
5678 revalidatingFetchers: RevalidatingFetcher[]
5679) {
5680 for (let index = 0; index < revalidatingFetchers.length; index++) {
5681 let { key, routeId, controller } = revalidatingFetchers[index];
5682 let result = results[key];
5683 let match = matches.find((m) => m?.route.id === routeId);
5684 // If we don't have a match, then we can have a deferred result to do
5685 // anything with. This is for revalidating fetchers where the route was
5686 // removed during HMR
5687 if (!match) {
5688 continue;
5689 }
5690
5691 if (isDeferredResult(result)) {
5692 // Note: we do not have to touch activeDeferreds here since we race them
5693 // against the signal in resolveDeferredData and they'll get aborted
5694 // there if needed
5695 invariant(
5696 controller,
5697 "Expected an AbortController for revalidating fetcher deferred result"
5698 );
5699 await resolveDeferredData(result, controller.signal, true).then(
5700 (result) => {
5701 if (result) {
5702 results[key] = result;
5703 }
5704 }
5705 );
5706 }
5707 }
5708}
5709
5710async function resolveDeferredData(
5711 result: DeferredResult,
5712 signal: AbortSignal,
5713 unwrap = false
5714): Promise<SuccessResult | ErrorResult | undefined> {
5715 let aborted = await result.deferredData.resolveData(signal);
5716 if (aborted) {
5717 return;
5718 }
5719
5720 if (unwrap) {
5721 try {
5722 return {
5723 type: ResultType.data,
5724 data: result.deferredData.unwrappedData,
5725 };
5726 } catch (e) {
5727 // Handle any TrackedPromise._error values encountered while unwrapping
5728 return {
5729 type: ResultType.error,
5730 error: e,
5731 };
5732 }
5733 }
5734
5735 return {
5736 type: ResultType.data,
5737 data: result.deferredData.data,
5738 };
5739}
5740
5741function hasNakedIndexQuery(search: string): boolean {
5742 return new URLSearchParams(search).getAll("index").some((v) => v === "");
5743}
5744
5745function getTargetMatch(
5746 matches: AgnosticDataRouteMatch[],
5747 location: Location | string
5748) {
5749 let search =
5750 typeof location === "string" ? parsePath(location).search : location.search;
5751 if (
5752 matches[matches.length - 1].route.index &&
5753 hasNakedIndexQuery(search || "")
5754 ) {
5755 // Return the leaf index route when index is present
5756 return matches[matches.length - 1];
5757 }
5758 // Otherwise grab the deepest "path contributing" match (ignoring index and
5759 // pathless layout routes)
5760 let pathMatches = getPathContributingMatches(matches);
5761 return pathMatches[pathMatches.length - 1];
5762}
5763
5764function getSubmissionFromNavigation(
5765 navigation: Navigation
5766): Submission | undefined {
5767 let { formMethod, formAction, formEncType, text, formData, json } =
5768 navigation;
5769 if (!formMethod || !formAction || !formEncType) {
5770 return;
5771 }
5772
5773 if (text != null) {
5774 return {
5775 formMethod,
5776 formAction,
5777 formEncType,
5778 formData: undefined,
5779 json: undefined,
5780 text,
5781 };
5782 } else if (formData != null) {
5783 return {
5784 formMethod,
5785 formAction,
5786 formEncType,
5787 formData,
5788 json: undefined,
5789 text: undefined,
5790 };
5791 } else if (json !== undefined) {
5792 return {
5793 formMethod,
5794 formAction,
5795 formEncType,
5796 formData: undefined,
5797 json,
5798 text: undefined,
5799 };
5800 }
5801}
5802
5803function getLoadingNavigation(
5804 location: Location,
5805 submission?: Submission
5806): NavigationStates["Loading"] {
5807 if (submission) {
5808 let navigation: NavigationStates["Loading"] = {
5809 state: "loading",
5810 location,
5811 formMethod: submission.formMethod,
5812 formAction: submission.formAction,
5813 formEncType: submission.formEncType,
5814 formData: submission.formData,
5815 json: submission.json,
5816 text: submission.text,
5817 };
5818 return navigation;
5819 } else {
5820 let navigation: NavigationStates["Loading"] = {
5821 state: "loading",
5822 location,
5823 formMethod: undefined,
5824 formAction: undefined,
5825 formEncType: undefined,
5826 formData: undefined,
5827 json: undefined,
5828 text: undefined,
5829 };
5830 return navigation;
5831 }
5832}
5833
5834function getSubmittingNavigation(
5835 location: Location,
5836 submission: Submission
5837): NavigationStates["Submitting"] {
5838 let navigation: NavigationStates["Submitting"] = {
5839 state: "submitting",
5840 location,
5841 formMethod: submission.formMethod,
5842 formAction: submission.formAction,
5843 formEncType: submission.formEncType,
5844 formData: submission.formData,
5845 json: submission.json,
5846 text: submission.text,
5847 };
5848 return navigation;
5849}
5850
5851function getLoadingFetcher(
5852 submission?: Submission,
5853 data?: Fetcher["data"]
5854): FetcherStates["Loading"] {
5855 if (submission) {
5856 let fetcher: FetcherStates["Loading"] = {
5857 state: "loading",
5858 formMethod: submission.formMethod,
5859 formAction: submission.formAction,
5860 formEncType: submission.formEncType,
5861 formData: submission.formData,
5862 json: submission.json,
5863 text: submission.text,
5864 data,
5865 };
5866 return fetcher;
5867 } else {
5868 let fetcher: FetcherStates["Loading"] = {
5869 state: "loading",
5870 formMethod: undefined,
5871 formAction: undefined,
5872 formEncType: undefined,
5873 formData: undefined,
5874 json: undefined,
5875 text: undefined,
5876 data,
5877 };
5878 return fetcher;
5879 }
5880}
5881
5882function getSubmittingFetcher(
5883 submission: Submission,
5884 existingFetcher?: Fetcher
5885): FetcherStates["Submitting"] {
5886 let fetcher: FetcherStates["Submitting"] = {
5887 state: "submitting",
5888 formMethod: submission.formMethod,
5889 formAction: submission.formAction,
5890 formEncType: submission.formEncType,
5891 formData: submission.formData,
5892 json: submission.json,
5893 text: submission.text,
5894 data: existingFetcher ? existingFetcher.data : undefined,
5895 };
5896 return fetcher;
5897}
5898
5899function getDoneFetcher(data: Fetcher["data"]): FetcherStates["Idle"] {
5900 let fetcher: FetcherStates["Idle"] = {
5901 state: "idle",
5902 formMethod: undefined,
5903 formAction: undefined,
5904 formEncType: undefined,
5905 formData: undefined,
5906 json: undefined,
5907 text: undefined,
5908 data,
5909 };
5910 return fetcher;
5911}
5912
5913function restoreAppliedTransitions(
5914 _window: Window,
5915 transitions: Map<string, Set<string>>
5916) {
5917 try {
5918 let sessionPositions = _window.sessionStorage.getItem(
5919 TRANSITIONS_STORAGE_KEY
5920 );
5921 if (sessionPositions) {
5922 let json = JSON.parse(sessionPositions);
5923 for (let [k, v] of Object.entries(json || {})) {
5924 if (v && Array.isArray(v)) {
5925 transitions.set(k, new Set(v || []));
5926 }
5927 }
5928 }
5929 } catch (e) {
5930 // no-op, use default empty object
5931 }
5932}
5933
5934function persistAppliedTransitions(
5935 _window: Window,
5936 transitions: Map<string, Set<string>>
5937) {
5938 if (transitions.size > 0) {
5939 let json: Record<string, string[]> = {};
5940 for (let [k, v] of transitions) {
5941 json[k] = [...v];
5942 }
5943 try {
5944 _window.sessionStorage.setItem(
5945 TRANSITIONS_STORAGE_KEY,
5946 JSON.stringify(json)
5947 );
5948 } catch (error) {
5949 warning(
5950 false,
5951 `Failed to save applied view transitions in sessionStorage (${error}).`
5952 );
5953 }
5954 }
5955}
5956//#endregion
Note: See TracBrowser for help on using the repository browser.