@remix-run/router 0.0.0-experimental-48058118

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/router.ts ADDED
@@ -0,0 +1,3851 @@
1
+ import type { Action, History, Location, Path, To } from "./history";
2
+ import {
3
+ Action as HistoryAction,
4
+ createLocation,
5
+ createPath,
6
+ invariant,
7
+ parsePath,
8
+ } from "./history";
9
+ import type {
10
+ ActionFunction,
11
+ ActionFunctionWithMiddleware,
12
+ AgnosticDataRouteMatch,
13
+ AgnosticDataRouteObject,
14
+ AgnosticRouteMatch,
15
+ AgnosticRouteObject,
16
+ DataResult,
17
+ DeferredResult,
18
+ ErrorResult,
19
+ FormEncType,
20
+ FormMethod,
21
+ LoaderFunction,
22
+ LoaderFunctionWithMiddleware,
23
+ MiddlewareContext,
24
+ MutationFormMethod,
25
+ Params,
26
+ RedirectResult,
27
+ RouteData,
28
+ ShouldRevalidateFunction,
29
+ Submission,
30
+ SuccessResult,
31
+ } from "./utils";
32
+ import {
33
+ convertRoutesToDataRoutes,
34
+ createMiddlewareStore,
35
+ DeferredData,
36
+ ErrorResponse,
37
+ getPathContributingMatches,
38
+ isRouteErrorResponse,
39
+ joinPaths,
40
+ matchRoutes,
41
+ resolveTo,
42
+ ResultType,
43
+ warning,
44
+ } from "./utils";
45
+
46
+ ////////////////////////////////////////////////////////////////////////////////
47
+ //#region Types and Constants
48
+ ////////////////////////////////////////////////////////////////////////////////
49
+
50
+ /**
51
+ * A Router instance manages all navigation and data loading/mutations
52
+ */
53
+ export interface Router {
54
+ /**
55
+ * @internal
56
+ * PRIVATE - DO NOT USE
57
+ *
58
+ * Return the basename for the router
59
+ */
60
+ get basename(): RouterInit["basename"];
61
+
62
+ /**
63
+ * @internal
64
+ * PRIVATE - DO NOT USE
65
+ *
66
+ * Return the current state of the router
67
+ */
68
+ get state(): RouterState;
69
+
70
+ /**
71
+ * @internal
72
+ * PRIVATE - DO NOT USE
73
+ *
74
+ * Return the routes for this router instance
75
+ */
76
+ get routes(): AgnosticDataRouteObject[];
77
+
78
+ /**
79
+ * @internal
80
+ * PRIVATE - DO NOT USE
81
+ *
82
+ * Initialize the router, including adding history listeners and kicking off
83
+ * initial data fetches. Returns a function to cleanup listeners and abort
84
+ * any in-progress loads
85
+ */
86
+ initialize(): Router;
87
+
88
+ /**
89
+ * @internal
90
+ * PRIVATE - DO NOT USE
91
+ *
92
+ * Subscribe to router.state updates
93
+ *
94
+ * @param fn function to call with the new state
95
+ */
96
+ subscribe(fn: RouterSubscriber): () => void;
97
+
98
+ /**
99
+ * @internal
100
+ * PRIVATE - DO NOT USE
101
+ *
102
+ * Enable scroll restoration behavior in the router
103
+ *
104
+ * @param savedScrollPositions Object that will manage positions, in case
105
+ * it's being restored from sessionStorage
106
+ * @param getScrollPosition Function to get the active Y scroll position
107
+ * @param getKey Function to get the key to use for restoration
108
+ */
109
+ enableScrollRestoration(
110
+ savedScrollPositions: Record<string, number>,
111
+ getScrollPosition: GetScrollPositionFunction,
112
+ getKey?: GetScrollRestorationKeyFunction
113
+ ): () => void;
114
+
115
+ /**
116
+ * @internal
117
+ * PRIVATE - DO NOT USE
118
+ *
119
+ * Navigate forward/backward in the history stack
120
+ * @param to Delta to move in the history stack
121
+ */
122
+ navigate(to: number): Promise<void>;
123
+
124
+ /**
125
+ * Navigate to the given path
126
+ * @param to Path to navigate to
127
+ * @param opts Navigation options (method, submission, etc.)
128
+ */
129
+ navigate(to: To, opts?: RouterNavigateOptions): Promise<void>;
130
+
131
+ /**
132
+ * @internal
133
+ * PRIVATE - DO NOT USE
134
+ *
135
+ * Trigger a fetcher load/submission
136
+ *
137
+ * @param key Fetcher key
138
+ * @param routeId Route that owns the fetcher
139
+ * @param href href to fetch
140
+ * @param opts Fetcher options, (method, submission, etc.)
141
+ */
142
+ fetch(
143
+ key: string,
144
+ routeId: string,
145
+ href: string,
146
+ opts?: RouterNavigateOptions
147
+ ): void;
148
+
149
+ /**
150
+ * @internal
151
+ * PRIVATE - DO NOT USE
152
+ *
153
+ * Trigger a revalidation of all current route loaders and fetcher loads
154
+ */
155
+ revalidate(): void;
156
+
157
+ /**
158
+ * @internal
159
+ * PRIVATE - DO NOT USE
160
+ *
161
+ * Utility function to create an href for the given location
162
+ * @param location
163
+ */
164
+ createHref(location: Location | URL): string;
165
+
166
+ /**
167
+ * @internal
168
+ * PRIVATE - DO NOT USE
169
+ *
170
+ * Utility function to URL encode a destination path according to the internal
171
+ * history implementation
172
+ * @param to
173
+ */
174
+ encodeLocation(to: To): Path;
175
+
176
+ /**
177
+ * @internal
178
+ * PRIVATE - DO NOT USE
179
+ *
180
+ * Get/create a fetcher for the given key
181
+ * @param key
182
+ */
183
+ getFetcher<TData = any>(key?: string): Fetcher<TData>;
184
+
185
+ /**
186
+ * @internal
187
+ * PRIVATE - DO NOT USE
188
+ *
189
+ * Delete the fetcher for a given key
190
+ * @param key
191
+ */
192
+ deleteFetcher(key?: string): void;
193
+
194
+ /**
195
+ * @internal
196
+ * PRIVATE - DO NOT USE
197
+ *
198
+ * Cleanup listeners and abort any in-progress loads
199
+ */
200
+ dispose(): void;
201
+
202
+ /**
203
+ * @internal
204
+ * PRIVATE - DO NOT USE
205
+ *
206
+ * Get a navigation blocker
207
+ * @param key The identifier for the blocker
208
+ * @param fn The blocker function implementation
209
+ */
210
+ getBlocker(key: string, fn: BlockerFunction): Blocker;
211
+
212
+ /**
213
+ * @internal
214
+ * PRIVATE - DO NOT USE
215
+ *
216
+ * Delete a navigation blocker
217
+ * @param key The identifier for the blocker
218
+ */
219
+ deleteBlocker(key: string): void;
220
+
221
+ /**
222
+ * @internal
223
+ * PRIVATE - DO NOT USE
224
+ *
225
+ * Internal fetch AbortControllers accessed by unit tests
226
+ */
227
+ _internalFetchControllers: Map<string, AbortController>;
228
+
229
+ /**
230
+ * @internal
231
+ * PRIVATE - DO NOT USE
232
+ *
233
+ * Internal pending DeferredData instances accessed by unit tests
234
+ */
235
+ _internalActiveDeferreds: Map<string, DeferredData>;
236
+ }
237
+
238
+ /**
239
+ * State maintained internally by the router. During a navigation, all states
240
+ * reflect the the "old" location unless otherwise noted.
241
+ */
242
+ export interface RouterState {
243
+ /**
244
+ * The action of the most recent navigation
245
+ */
246
+ historyAction: HistoryAction;
247
+
248
+ /**
249
+ * The current location reflected by the router
250
+ */
251
+ location: Location;
252
+
253
+ /**
254
+ * The current set of route matches
255
+ */
256
+ matches: AgnosticDataRouteMatch[];
257
+
258
+ /**
259
+ * Tracks whether we've completed our initial data load
260
+ */
261
+ initialized: boolean;
262
+
263
+ /**
264
+ * Current scroll position we should start at for a new view
265
+ * - number -> scroll position to restore to
266
+ * - false -> do not restore scroll at all (used during submissions)
267
+ * - null -> don't have a saved position, scroll to hash or top of page
268
+ */
269
+ restoreScrollPosition: number | false | null;
270
+
271
+ /**
272
+ * Indicate whether this navigation should skip resetting the scroll position
273
+ * if we are unable to restore the scroll position
274
+ */
275
+ preventScrollReset: boolean;
276
+
277
+ /**
278
+ * Tracks the state of the current navigation
279
+ */
280
+ navigation: Navigation;
281
+
282
+ /**
283
+ * Tracks any in-progress revalidations
284
+ */
285
+ revalidation: RevalidationState;
286
+
287
+ /**
288
+ * Data from the loaders for the current matches
289
+ */
290
+ loaderData: RouteData;
291
+
292
+ /**
293
+ * Data from the action for the current matches
294
+ */
295
+ actionData: RouteData | null;
296
+
297
+ /**
298
+ * Errors caught from loaders for the current matches
299
+ */
300
+ errors: RouteData | null;
301
+
302
+ /**
303
+ * Map of current fetchers
304
+ */
305
+ fetchers: Map<string, Fetcher>;
306
+
307
+ /**
308
+ * Map of current blockers
309
+ */
310
+ blockers: Map<string, Blocker>;
311
+ }
312
+
313
+ /**
314
+ * Data that can be passed into hydrate a Router from SSR
315
+ */
316
+ export type HydrationState = Partial<
317
+ Pick<RouterState, "loaderData" | "actionData" | "errors">
318
+ >;
319
+
320
+ /**
321
+ * Future flags to toggle on new feature behavior
322
+ */
323
+ export interface FutureConfig {
324
+ unstable_middleware: boolean;
325
+ }
326
+
327
+ /**
328
+ * Initialization options for createRouter
329
+ */
330
+ export interface RouterInit {
331
+ basename?: string;
332
+ routes: AgnosticRouteObject[];
333
+ history: History;
334
+ hydrationData?: HydrationState;
335
+ future?: FutureConfig;
336
+ }
337
+
338
+ /**
339
+ * State returned from a server-side query() call
340
+ */
341
+ export interface StaticHandlerContext {
342
+ basename: Router["basename"];
343
+ location: RouterState["location"];
344
+ matches: RouterState["matches"];
345
+ loaderData: RouterState["loaderData"];
346
+ actionData: RouterState["actionData"];
347
+ errors: RouterState["errors"];
348
+ statusCode: number;
349
+ loaderHeaders: Record<string, Headers>;
350
+ actionHeaders: Record<string, Headers>;
351
+ activeDeferreds: Record<string, DeferredData> | null;
352
+ _deepestRenderedBoundaryId?: string | null;
353
+ }
354
+
355
+ interface StaticHandlerQueryOpts {
356
+ requestContext?: unknown;
357
+ middlewareContext?: MiddlewareContext;
358
+ }
359
+
360
+ interface StaticHandlerQueryRouteOpts extends StaticHandlerQueryOpts {
361
+ routeId?: string;
362
+ }
363
+
364
+ /**
365
+ * A StaticHandler instance manages a singular SSR navigation/fetch event
366
+ */
367
+ export interface StaticHandler {
368
+ dataRoutes: AgnosticDataRouteObject[];
369
+ query(
370
+ request: Request,
371
+ opts?: StaticHandlerQueryOpts
372
+ ): Promise<StaticHandlerContext | Response>;
373
+ queryRoute(
374
+ request: Request,
375
+ opts?: StaticHandlerQueryRouteOpts
376
+ ): Promise<any>;
377
+ }
378
+
379
+ /**
380
+ * Subscriber function signature for changes to router state
381
+ */
382
+ export interface RouterSubscriber {
383
+ (state: RouterState): void;
384
+ }
385
+
386
+ interface UseMatchesMatch {
387
+ id: string;
388
+ pathname: string;
389
+ params: AgnosticRouteMatch["params"];
390
+ data: unknown;
391
+ handle: unknown;
392
+ }
393
+
394
+ /**
395
+ * Function signature for determining the key to be used in scroll restoration
396
+ * for a given location
397
+ */
398
+ export interface GetScrollRestorationKeyFunction {
399
+ (location: Location, matches: UseMatchesMatch[]): string | null;
400
+ }
401
+
402
+ /**
403
+ * Function signature for determining the current scroll position
404
+ */
405
+ export interface GetScrollPositionFunction {
406
+ (): number;
407
+ }
408
+
409
+ /**
410
+ * Options for a navigate() call for a Link navigation
411
+ */
412
+ type LinkNavigateOptions = {
413
+ replace?: boolean;
414
+ state?: any;
415
+ preventScrollReset?: boolean;
416
+ };
417
+
418
+ /**
419
+ * Options for a navigate() call for a Form navigation
420
+ */
421
+ type SubmissionNavigateOptions = {
422
+ replace?: boolean;
423
+ state?: any;
424
+ preventScrollReset?: boolean;
425
+ formMethod?: FormMethod;
426
+ formEncType?: FormEncType;
427
+ formData: FormData;
428
+ };
429
+
430
+ /**
431
+ * Options to pass to navigate() for either a Link or Form navigation
432
+ */
433
+ export type RouterNavigateOptions =
434
+ | LinkNavigateOptions
435
+ | SubmissionNavigateOptions;
436
+
437
+ /**
438
+ * Options to pass to fetch()
439
+ */
440
+ export type RouterFetchOptions =
441
+ | Omit<LinkNavigateOptions, "replace">
442
+ | Omit<SubmissionNavigateOptions, "replace">;
443
+
444
+ /**
445
+ * Potential states for state.navigation
446
+ */
447
+ export type NavigationStates = {
448
+ Idle: {
449
+ state: "idle";
450
+ location: undefined;
451
+ formMethod: undefined;
452
+ formAction: undefined;
453
+ formEncType: undefined;
454
+ formData: undefined;
455
+ };
456
+ Loading: {
457
+ state: "loading";
458
+ location: Location;
459
+ formMethod: FormMethod | undefined;
460
+ formAction: string | undefined;
461
+ formEncType: FormEncType | undefined;
462
+ formData: FormData | undefined;
463
+ };
464
+ Submitting: {
465
+ state: "submitting";
466
+ location: Location;
467
+ formMethod: FormMethod;
468
+ formAction: string;
469
+ formEncType: FormEncType;
470
+ formData: FormData;
471
+ };
472
+ };
473
+
474
+ export type Navigation = NavigationStates[keyof NavigationStates];
475
+
476
+ export type RevalidationState = "idle" | "loading";
477
+
478
+ /**
479
+ * Potential states for fetchers
480
+ */
481
+ type FetcherStates<TData = any> = {
482
+ Idle: {
483
+ state: "idle";
484
+ formMethod: undefined;
485
+ formAction: undefined;
486
+ formEncType: undefined;
487
+ formData: undefined;
488
+ data: TData | undefined;
489
+ " _hasFetcherDoneAnything "?: boolean;
490
+ };
491
+ Loading: {
492
+ state: "loading";
493
+ formMethod: FormMethod | undefined;
494
+ formAction: string | undefined;
495
+ formEncType: FormEncType | undefined;
496
+ formData: FormData | undefined;
497
+ data: TData | undefined;
498
+ " _hasFetcherDoneAnything "?: boolean;
499
+ };
500
+ Submitting: {
501
+ state: "submitting";
502
+ formMethod: FormMethod;
503
+ formAction: string;
504
+ formEncType: FormEncType;
505
+ formData: FormData;
506
+ data: TData | undefined;
507
+ " _hasFetcherDoneAnything "?: boolean;
508
+ };
509
+ };
510
+
511
+ export type Fetcher<TData = any> =
512
+ FetcherStates<TData>[keyof FetcherStates<TData>];
513
+
514
+ interface BlockerBlocked {
515
+ state: "blocked";
516
+ reset(): void;
517
+ proceed(): void;
518
+ location: Location;
519
+ }
520
+
521
+ interface BlockerUnblocked {
522
+ state: "unblocked";
523
+ reset: undefined;
524
+ proceed: undefined;
525
+ location: undefined;
526
+ }
527
+
528
+ interface BlockerProceeding {
529
+ state: "proceeding";
530
+ reset: undefined;
531
+ proceed: undefined;
532
+ location: Location;
533
+ }
534
+
535
+ export type Blocker = BlockerUnblocked | BlockerBlocked | BlockerProceeding;
536
+
537
+ export type BlockerFunction = (args: {
538
+ currentLocation: Location;
539
+ nextLocation: Location;
540
+ historyAction: HistoryAction;
541
+ }) => boolean;
542
+
543
+ interface ShortCircuitable {
544
+ /**
545
+ * startNavigation does not need to complete the navigation because we
546
+ * redirected or got interrupted
547
+ */
548
+ shortCircuited?: boolean;
549
+ }
550
+
551
+ interface HandleActionResult extends ShortCircuitable {
552
+ /**
553
+ * Error thrown from the current action, keyed by the route containing the
554
+ * error boundary to render the error. To be committed to the state after
555
+ * loaders have completed
556
+ */
557
+ pendingActionError?: RouteData;
558
+ /**
559
+ * Data returned from the current action, keyed by the route owning the action.
560
+ * To be committed to the state after loaders have completed
561
+ */
562
+ pendingActionData?: RouteData;
563
+ }
564
+
565
+ interface HandleLoadersResult extends ShortCircuitable {
566
+ /**
567
+ * loaderData returned from the current set of loaders
568
+ */
569
+ loaderData?: RouterState["loaderData"];
570
+ /**
571
+ * errors thrown from the current set of loaders
572
+ */
573
+ errors?: RouterState["errors"];
574
+ }
575
+
576
+ /**
577
+ * Cached info for active fetcher.load() instances so they can participate
578
+ * in revalidation
579
+ */
580
+ interface FetchLoadMatch {
581
+ routeId: string;
582
+ path: string;
583
+ match: AgnosticDataRouteMatch;
584
+ matches: AgnosticDataRouteMatch[];
585
+ }
586
+
587
+ /**
588
+ * Identified fetcher.load() calls that need to be revalidated
589
+ */
590
+ interface RevalidatingFetcher extends FetchLoadMatch {
591
+ key: string;
592
+ }
593
+
594
+ /**
595
+ * Wrapper object to allow us to throw any response out from callLoaderOrAction
596
+ * for queryRouter while preserving whether or not it was thrown or returned
597
+ * from the loader/action
598
+ */
599
+ interface QueryRouteResponse {
600
+ type: ResultType.data | ResultType.error;
601
+ response: Response;
602
+ }
603
+
604
+ const defaultFutureConfig: FutureConfig = { unstable_middleware: false };
605
+ const validMutationMethodsArr: MutationFormMethod[] = [
606
+ "post",
607
+ "put",
608
+ "patch",
609
+ "delete",
610
+ ];
611
+ const validMutationMethods = new Set<MutationFormMethod>(
612
+ validMutationMethodsArr
613
+ );
614
+
615
+ const validRequestMethodsArr: FormMethod[] = [
616
+ "get",
617
+ ...validMutationMethodsArr,
618
+ ];
619
+ const validRequestMethods = new Set<FormMethod>(validRequestMethodsArr);
620
+
621
+ const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
622
+ const redirectPreserveMethodStatusCodes = new Set([307, 308]);
623
+
624
+ export const IDLE_NAVIGATION: NavigationStates["Idle"] = {
625
+ state: "idle",
626
+ location: undefined,
627
+ formMethod: undefined,
628
+ formAction: undefined,
629
+ formEncType: undefined,
630
+ formData: undefined,
631
+ };
632
+
633
+ export const IDLE_FETCHER: FetcherStates["Idle"] = {
634
+ state: "idle",
635
+ data: undefined,
636
+ formMethod: undefined,
637
+ formAction: undefined,
638
+ formEncType: undefined,
639
+ formData: undefined,
640
+ };
641
+
642
+ export const IDLE_BLOCKER: BlockerUnblocked = {
643
+ state: "unblocked",
644
+ proceed: undefined,
645
+ reset: undefined,
646
+ location: undefined,
647
+ };
648
+
649
+ const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
650
+
651
+ const isBrowser =
652
+ typeof window !== "undefined" &&
653
+ typeof window.document !== "undefined" &&
654
+ typeof window.document.createElement !== "undefined";
655
+ const isServer = !isBrowser;
656
+ //#endregion
657
+
658
+ ////////////////////////////////////////////////////////////////////////////////
659
+ //#region createRouter
660
+ ////////////////////////////////////////////////////////////////////////////////
661
+
662
+ /**
663
+ * Create a router and listen to history POP navigations
664
+ */
665
+ export function createRouter(init: RouterInit): Router {
666
+ invariant(
667
+ init.routes.length > 0,
668
+ "You must provide a non-empty routes array to createRouter"
669
+ );
670
+
671
+ let dataRoutes = convertRoutesToDataRoutes(init.routes);
672
+ let future: FutureConfig = { ...defaultFutureConfig, ...init.future };
673
+
674
+ // Cleanup function for history
675
+ let unlistenHistory: (() => void) | null = null;
676
+ // Externally-provided functions to call on all state changes
677
+ let subscribers = new Set<RouterSubscriber>();
678
+ // Externally-provided object to hold scroll restoration locations during routing
679
+ let savedScrollPositions: Record<string, number> | null = null;
680
+ // Externally-provided function to get scroll restoration keys
681
+ let getScrollRestorationKey: GetScrollRestorationKeyFunction | null = null;
682
+ // Externally-provided function to get current scroll position
683
+ let getScrollPosition: GetScrollPositionFunction | null = null;
684
+ // One-time flag to control the initial hydration scroll restoration. Because
685
+ // we don't get the saved positions from <ScrollRestoration /> until _after_
686
+ // the initial render, we need to manually trigger a separate updateState to
687
+ // send along the restoreScrollPosition
688
+ // Set to true if we have `hydrationData` since we assume we were SSR'd and that
689
+ // SSR did the initial scroll restoration.
690
+ let initialScrollRestored = init.hydrationData != null;
691
+
692
+ let initialMatches = matchRoutes(
693
+ dataRoutes,
694
+ init.history.location,
695
+ init.basename
696
+ );
697
+ let initialErrors: RouteData | null = null;
698
+
699
+ if (initialMatches == null) {
700
+ // If we do not match a user-provided-route, fall back to the root
701
+ // to allow the error boundary to take over
702
+ let error = getInternalRouterError(404, {
703
+ pathname: init.history.location.pathname,
704
+ });
705
+ let { matches, route } = getShortCircuitMatches(dataRoutes);
706
+ initialMatches = matches;
707
+ initialErrors = { [route.id]: error };
708
+ }
709
+
710
+ let initialized =
711
+ !initialMatches.some((m) => m.route.loader) || init.hydrationData != null;
712
+
713
+ let router: Router;
714
+ let state: RouterState = {
715
+ historyAction: init.history.action,
716
+ location: init.history.location,
717
+ matches: initialMatches,
718
+ initialized,
719
+ navigation: IDLE_NAVIGATION,
720
+ // Don't restore on initial updateState() if we were SSR'd
721
+ restoreScrollPosition: init.hydrationData != null ? false : null,
722
+ preventScrollReset: false,
723
+ revalidation: "idle",
724
+ loaderData: (init.hydrationData && init.hydrationData.loaderData) || {},
725
+ actionData: (init.hydrationData && init.hydrationData.actionData) || null,
726
+ errors: (init.hydrationData && init.hydrationData.errors) || initialErrors,
727
+ fetchers: new Map(),
728
+ blockers: new Map(),
729
+ };
730
+
731
+ // -- Stateful internal variables to manage navigations --
732
+ // Current navigation in progress (to be committed in completeNavigation)
733
+ let pendingAction: HistoryAction = HistoryAction.Pop;
734
+
735
+ // Should the current navigation prevent the scroll reset if scroll cannot
736
+ // be restored?
737
+ let pendingPreventScrollReset = false;
738
+
739
+ // AbortController for the active navigation
740
+ let pendingNavigationController: AbortController | null;
741
+
742
+ // We use this to avoid touching history in completeNavigation if a
743
+ // revalidation is entirely uninterrupted
744
+ let isUninterruptedRevalidation = false;
745
+
746
+ // Use this internal flag to force revalidation of all loaders:
747
+ // - submissions (completed or interrupted)
748
+ // - useRevalidate()
749
+ // - X-Remix-Revalidate (from redirect)
750
+ let isRevalidationRequired = false;
751
+
752
+ // Use this internal array to capture routes that require revalidation due
753
+ // to a cancelled deferred on action submission
754
+ let cancelledDeferredRoutes: string[] = [];
755
+
756
+ // Use this internal array to capture fetcher loads that were cancelled by an
757
+ // action navigation and require revalidation
758
+ let cancelledFetcherLoads: string[] = [];
759
+
760
+ // AbortControllers for any in-flight fetchers
761
+ let fetchControllers = new Map<string, AbortController>();
762
+
763
+ // Track loads based on the order in which they started
764
+ let incrementingLoadId = 0;
765
+
766
+ // Track the outstanding pending navigation data load to be compared against
767
+ // the globally incrementing load when a fetcher load lands after a completed
768
+ // navigation
769
+ let pendingNavigationLoadId = -1;
770
+
771
+ // Fetchers that triggered data reloads as a result of their actions
772
+ let fetchReloadIds = new Map<string, number>();
773
+
774
+ // Fetchers that triggered redirect navigations from their actions
775
+ let fetchRedirectIds = new Set<string>();
776
+
777
+ // Most recent href/match for fetcher.load calls for fetchers
778
+ let fetchLoadMatches = new Map<string, FetchLoadMatch>();
779
+
780
+ // Store DeferredData instances for active route matches. When a
781
+ // route loader returns defer() we stick one in here. Then, when a nested
782
+ // promise resolves we update loaderData. If a new navigation starts we
783
+ // cancel active deferreds for eliminated routes.
784
+ let activeDeferreds = new Map<string, DeferredData>();
785
+
786
+ // Store blocker functions in a separate Map outside of router state since
787
+ // we don't need to update UI state if they change
788
+ let blockerFunctions = new Map<string, BlockerFunction>();
789
+
790
+ // Flag to ignore the next history update, so we can revert the URL change on
791
+ // a POP navigation that was blocked by the user without touching router state
792
+ let ignoreNextHistoryUpdate = false;
793
+
794
+ // Initialize the router, all side effects should be kicked off from here.
795
+ // Implemented as a Fluent API for ease of:
796
+ // let router = createRouter(init).initialize();
797
+ function initialize() {
798
+ // If history informs us of a POP navigation, start the navigation but do not update
799
+ // state. We'll update our own state once the navigation completes
800
+ unlistenHistory = init.history.listen(
801
+ ({ action: historyAction, location, delta }) => {
802
+ // Ignore this event if it was just us resetting the URL from a
803
+ // blocked POP navigation
804
+ if (ignoreNextHistoryUpdate) {
805
+ ignoreNextHistoryUpdate = false;
806
+ return;
807
+ }
808
+
809
+ warning(
810
+ blockerFunctions.size === 0 || delta != null,
811
+ "You are trying to use a blocker on a POP navigation to a location " +
812
+ "that was not created by @remix-run/router. This will fail silently in " +
813
+ "production. This can happen if you are navigating outside the router " +
814
+ "via `window.history.pushState`/`window.location.hash` instead of using " +
815
+ "router navigation APIs. This can also happen if you are using " +
816
+ "createHashRouter and the user manually changes the URL."
817
+ );
818
+
819
+ let blockerKey = shouldBlockNavigation({
820
+ currentLocation: state.location,
821
+ nextLocation: location,
822
+ historyAction,
823
+ });
824
+
825
+ if (blockerKey && delta != null) {
826
+ // Restore the URL to match the current UI, but don't update router state
827
+ ignoreNextHistoryUpdate = true;
828
+ init.history.go(delta * -1);
829
+
830
+ // Put the blocker into a blocked state
831
+ updateBlocker(blockerKey, {
832
+ state: "blocked",
833
+ location,
834
+ proceed() {
835
+ updateBlocker(blockerKey!, {
836
+ state: "proceeding",
837
+ proceed: undefined,
838
+ reset: undefined,
839
+ location,
840
+ });
841
+ // Re-do the same POP navigation we just blocked
842
+ init.history.go(delta);
843
+ },
844
+ reset() {
845
+ deleteBlocker(blockerKey!);
846
+ updateState({ blockers: new Map(router.state.blockers) });
847
+ },
848
+ });
849
+ return;
850
+ }
851
+
852
+ return startNavigation(historyAction, location);
853
+ }
854
+ );
855
+
856
+ // Kick off initial data load if needed. Use Pop to avoid modifying history
857
+ if (!state.initialized) {
858
+ startNavigation(HistoryAction.Pop, state.location);
859
+ }
860
+
861
+ return router;
862
+ }
863
+
864
+ // Clean up a router and it's side effects
865
+ function dispose() {
866
+ if (unlistenHistory) {
867
+ unlistenHistory();
868
+ }
869
+ subscribers.clear();
870
+ pendingNavigationController && pendingNavigationController.abort();
871
+ state.fetchers.forEach((_, key) => deleteFetcher(key));
872
+ state.blockers.forEach((_, key) => deleteBlocker(key));
873
+ }
874
+
875
+ // Subscribe to state updates for the router
876
+ function subscribe(fn: RouterSubscriber) {
877
+ subscribers.add(fn);
878
+ return () => subscribers.delete(fn);
879
+ }
880
+
881
+ // Update our state and notify the calling context of the change
882
+ function updateState(newState: Partial<RouterState>): void {
883
+ state = {
884
+ ...state,
885
+ ...newState,
886
+ };
887
+ subscribers.forEach((subscriber) => subscriber(state));
888
+ }
889
+
890
+ // Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION
891
+ // and setting state.[historyAction/location/matches] to the new route.
892
+ // - Location is a required param
893
+ // - Navigation will always be set to IDLE_NAVIGATION
894
+ // - Can pass any other state in newState
895
+ function completeNavigation(
896
+ location: Location,
897
+ newState: Partial<Omit<RouterState, "action" | "location" | "navigation">>
898
+ ): void {
899
+ // Deduce if we're in a loading/actionReload state:
900
+ // - We have committed actionData in the store
901
+ // - The current navigation was a mutation submission
902
+ // - We're past the submitting state and into the loading state
903
+ // - The location being loaded is not the result of a redirect
904
+ let isActionReload =
905
+ state.actionData != null &&
906
+ state.navigation.formMethod != null &&
907
+ isMutationMethod(state.navigation.formMethod) &&
908
+ state.navigation.state === "loading" &&
909
+ location.state?._isRedirect !== true;
910
+
911
+ let actionData: RouteData | null;
912
+ if (newState.actionData) {
913
+ if (Object.keys(newState.actionData).length > 0) {
914
+ actionData = newState.actionData;
915
+ } else {
916
+ // Empty actionData -> clear prior actionData due to an action error
917
+ actionData = null;
918
+ }
919
+ } else if (isActionReload) {
920
+ // Keep the current data if we're wrapping up the action reload
921
+ actionData = state.actionData;
922
+ } else {
923
+ // Clear actionData on any other completed navigations
924
+ actionData = null;
925
+ }
926
+
927
+ // Always preserve any existing loaderData from re-used routes
928
+ let loaderData = newState.loaderData
929
+ ? mergeLoaderData(
930
+ state.loaderData,
931
+ newState.loaderData,
932
+ newState.matches || [],
933
+ newState.errors
934
+ )
935
+ : state.loaderData;
936
+
937
+ // On a successful navigation we can assume we got through all blockers
938
+ // so we can start fresh
939
+ for (let [key] of blockerFunctions) {
940
+ deleteBlocker(key);
941
+ }
942
+
943
+ // Always respect the user flag. Otherwise don't reset on mutation
944
+ // submission navigations unless they redirect
945
+ let preventScrollReset =
946
+ pendingPreventScrollReset === true ||
947
+ (state.navigation.formMethod != null &&
948
+ isMutationMethod(state.navigation.formMethod) &&
949
+ location.state?._isRedirect !== true);
950
+
951
+ updateState({
952
+ ...newState, // matches, errors, fetchers go through as-is
953
+ actionData,
954
+ loaderData,
955
+ historyAction: pendingAction,
956
+ location,
957
+ initialized: true,
958
+ navigation: IDLE_NAVIGATION,
959
+ revalidation: "idle",
960
+ restoreScrollPosition: getSavedScrollPosition(
961
+ location,
962
+ newState.matches || state.matches
963
+ ),
964
+ preventScrollReset,
965
+ blockers: new Map(state.blockers),
966
+ });
967
+
968
+ if (isUninterruptedRevalidation) {
969
+ // If this was an uninterrupted revalidation then do not touch history
970
+ } else if (pendingAction === HistoryAction.Pop) {
971
+ // Do nothing for POP - URL has already been updated
972
+ } else if (pendingAction === HistoryAction.Push) {
973
+ init.history.push(location, location.state);
974
+ } else if (pendingAction === HistoryAction.Replace) {
975
+ init.history.replace(location, location.state);
976
+ }
977
+
978
+ // Reset stateful navigation vars
979
+ pendingAction = HistoryAction.Pop;
980
+ pendingPreventScrollReset = false;
981
+ isUninterruptedRevalidation = false;
982
+ isRevalidationRequired = false;
983
+ cancelledDeferredRoutes = [];
984
+ cancelledFetcherLoads = [];
985
+ }
986
+
987
+ // Trigger a navigation event, which can either be a numerical POP or a PUSH
988
+ // replace with an optional submission
989
+ async function navigate(
990
+ to: number | To,
991
+ opts?: RouterNavigateOptions
992
+ ): Promise<void> {
993
+ if (typeof to === "number") {
994
+ init.history.go(to);
995
+ return;
996
+ }
997
+
998
+ let { path, submission, error } = normalizeNavigateOptions(to, opts);
999
+
1000
+ let currentLocation = state.location;
1001
+ let nextLocation = createLocation(state.location, path, opts && opts.state);
1002
+
1003
+ // When using navigate as a PUSH/REPLACE we aren't reading an already-encoded
1004
+ // URL from window.location, so we need to encode it here so the behavior
1005
+ // remains the same as POP and non-data-router usages. new URL() does all
1006
+ // the same encoding we'd get from a history.pushState/window.location read
1007
+ // without having to touch history
1008
+ nextLocation = {
1009
+ ...nextLocation,
1010
+ ...init.history.encodeLocation(nextLocation),
1011
+ };
1012
+
1013
+ let userReplace = opts && opts.replace != null ? opts.replace : undefined;
1014
+
1015
+ let historyAction = HistoryAction.Push;
1016
+
1017
+ if (userReplace === true) {
1018
+ historyAction = HistoryAction.Replace;
1019
+ } else if (userReplace === false) {
1020
+ // no-op
1021
+ } else if (
1022
+ submission != null &&
1023
+ isMutationMethod(submission.formMethod) &&
1024
+ submission.formAction === state.location.pathname + state.location.search
1025
+ ) {
1026
+ // By default on submissions to the current location we REPLACE so that
1027
+ // users don't have to double-click the back button to get to the prior
1028
+ // location. If the user redirects to a different location from the
1029
+ // action/loader this will be ignored and the redirect will be a PUSH
1030
+ historyAction = HistoryAction.Replace;
1031
+ }
1032
+
1033
+ let preventScrollReset =
1034
+ opts && "preventScrollReset" in opts
1035
+ ? opts.preventScrollReset === true
1036
+ : undefined;
1037
+
1038
+ let blockerKey = shouldBlockNavigation({
1039
+ currentLocation,
1040
+ nextLocation,
1041
+ historyAction,
1042
+ });
1043
+ if (blockerKey) {
1044
+ // Put the blocker into a blocked state
1045
+ updateBlocker(blockerKey, {
1046
+ state: "blocked",
1047
+ location: nextLocation,
1048
+ proceed() {
1049
+ updateBlocker(blockerKey!, {
1050
+ state: "proceeding",
1051
+ proceed: undefined,
1052
+ reset: undefined,
1053
+ location: nextLocation,
1054
+ });
1055
+ // Send the same navigation through
1056
+ navigate(to, opts);
1057
+ },
1058
+ reset() {
1059
+ deleteBlocker(blockerKey!);
1060
+ updateState({ blockers: new Map(state.blockers) });
1061
+ },
1062
+ });
1063
+ return;
1064
+ }
1065
+
1066
+ return await startNavigation(historyAction, nextLocation, {
1067
+ submission,
1068
+ // Send through the formData serialization error if we have one so we can
1069
+ // render at the right error boundary after we match routes
1070
+ pendingError: error,
1071
+ preventScrollReset,
1072
+ replace: opts && opts.replace,
1073
+ });
1074
+ }
1075
+
1076
+ // Revalidate all current loaders. If a navigation is in progress or if this
1077
+ // is interrupted by a navigation, allow this to "succeed" by calling all
1078
+ // loaders during the next loader round
1079
+ function revalidate() {
1080
+ interruptActiveLoads();
1081
+ updateState({ revalidation: "loading" });
1082
+
1083
+ // If we're currently submitting an action, we don't need to start a new
1084
+ // navigation, we'll just let the follow up loader execution call all loaders
1085
+ if (state.navigation.state === "submitting") {
1086
+ return;
1087
+ }
1088
+
1089
+ // If we're currently in an idle state, start a new navigation for the current
1090
+ // action/location and mark it as uninterrupted, which will skip the history
1091
+ // update in completeNavigation
1092
+ if (state.navigation.state === "idle") {
1093
+ startNavigation(state.historyAction, state.location, {
1094
+ startUninterruptedRevalidation: true,
1095
+ });
1096
+ return;
1097
+ }
1098
+
1099
+ // Otherwise, if we're currently in a loading state, just start a new
1100
+ // navigation to the navigation.location but do not trigger an uninterrupted
1101
+ // revalidation so that history correctly updates once the navigation completes
1102
+ startNavigation(
1103
+ pendingAction || state.historyAction,
1104
+ state.navigation.location,
1105
+ { overrideNavigation: state.navigation }
1106
+ );
1107
+ }
1108
+
1109
+ // Start a navigation to the given action/location. Can optionally provide a
1110
+ // overrideNavigation which will override the normalLoad in the case of a redirect
1111
+ // navigation
1112
+ async function startNavigation(
1113
+ historyAction: HistoryAction,
1114
+ location: Location,
1115
+ opts?: {
1116
+ submission?: Submission;
1117
+ overrideNavigation?: Navigation;
1118
+ pendingError?: ErrorResponse;
1119
+ startUninterruptedRevalidation?: boolean;
1120
+ preventScrollReset?: boolean;
1121
+ replace?: boolean;
1122
+ }
1123
+ ): Promise<void> {
1124
+ // Abort any in-progress navigations and start a new one. Unset any ongoing
1125
+ // uninterrupted revalidations unless told otherwise, since we want this
1126
+ // new navigation to update history normally
1127
+ pendingNavigationController && pendingNavigationController.abort();
1128
+ pendingNavigationController = null;
1129
+ pendingAction = historyAction;
1130
+ isUninterruptedRevalidation =
1131
+ (opts && opts.startUninterruptedRevalidation) === true;
1132
+
1133
+ // Save the current scroll position every time we start a new navigation,
1134
+ // and track whether we should reset scroll on completion
1135
+ saveScrollPosition(state.location, state.matches);
1136
+ pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
1137
+
1138
+ let loadingNavigation = opts && opts.overrideNavigation;
1139
+ let matches = matchRoutes(dataRoutes, location, init.basename);
1140
+
1141
+ // Short circuit with a 404 on the root error boundary if we match nothing
1142
+ if (!matches) {
1143
+ let error = getInternalRouterError(404, { pathname: location.pathname });
1144
+ let { matches: notFoundMatches, route } =
1145
+ getShortCircuitMatches(dataRoutes);
1146
+ // Cancel all pending deferred on 404s since we don't keep any routes
1147
+ cancelActiveDeferreds();
1148
+ completeNavigation(location, {
1149
+ matches: notFoundMatches,
1150
+ loaderData: {},
1151
+ errors: {
1152
+ [route.id]: error,
1153
+ },
1154
+ });
1155
+ return;
1156
+ }
1157
+
1158
+ // Short circuit if it's only a hash change and not a mutation submission
1159
+ // For example, on /page#hash and submit a <Form method="post"> which will
1160
+ // default to a navigation to /page
1161
+ if (
1162
+ isHashChangeOnly(state.location, location) &&
1163
+ !(opts && opts.submission && isMutationMethod(opts.submission.formMethod))
1164
+ ) {
1165
+ completeNavigation(location, { matches });
1166
+ return;
1167
+ }
1168
+
1169
+ // Create a controller/Request for this navigation
1170
+ pendingNavigationController = new AbortController();
1171
+ let request = createClientSideRequest(
1172
+ init.history,
1173
+ location,
1174
+ pendingNavigationController.signal,
1175
+ opts && opts.submission
1176
+ );
1177
+ let pendingActionData: RouteData | undefined;
1178
+ let pendingError: RouteData | undefined;
1179
+
1180
+ if (opts && opts.pendingError) {
1181
+ // If we have a pendingError, it means the user attempted a GET submission
1182
+ // with binary FormData so assign here and skip to handleLoaders. That
1183
+ // way we handle calling loaders above the boundary etc. It's not really
1184
+ // different from an actionError in that sense.
1185
+ pendingError = {
1186
+ [findNearestBoundary(matches).route.id]: opts.pendingError,
1187
+ };
1188
+ } else if (
1189
+ opts &&
1190
+ opts.submission &&
1191
+ isMutationMethod(opts.submission.formMethod)
1192
+ ) {
1193
+ // Call action if we received an action submission
1194
+ let actionOutput = await handleAction(
1195
+ request,
1196
+ location,
1197
+ opts.submission,
1198
+ matches,
1199
+ { replace: opts.replace }
1200
+ );
1201
+
1202
+ if (actionOutput.shortCircuited) {
1203
+ return;
1204
+ }
1205
+
1206
+ pendingActionData = actionOutput.pendingActionData;
1207
+ pendingError = actionOutput.pendingActionError;
1208
+
1209
+ let navigation: NavigationStates["Loading"] = {
1210
+ state: "loading",
1211
+ location,
1212
+ ...opts.submission,
1213
+ };
1214
+ loadingNavigation = navigation;
1215
+
1216
+ // Create a GET request for the loaders
1217
+ request = new Request(request.url, { signal: request.signal });
1218
+ }
1219
+
1220
+ // Call loaders
1221
+ let { shortCircuited, loaderData, errors } = await handleLoaders(
1222
+ request,
1223
+ location,
1224
+ matches,
1225
+ loadingNavigation,
1226
+ opts && opts.submission,
1227
+ opts && opts.replace,
1228
+ pendingActionData,
1229
+ pendingError
1230
+ );
1231
+
1232
+ if (shortCircuited) {
1233
+ return;
1234
+ }
1235
+
1236
+ // Clean up now that the action/loaders have completed. Don't clean up if
1237
+ // we short circuited because pendingNavigationController will have already
1238
+ // been assigned to a new controller for the next navigation
1239
+ pendingNavigationController = null;
1240
+
1241
+ completeNavigation(location, {
1242
+ matches,
1243
+ ...(pendingActionData ? { actionData: pendingActionData } : {}),
1244
+ loaderData,
1245
+ errors,
1246
+ });
1247
+ }
1248
+
1249
+ // Call the action matched by the leaf route for this navigation and handle
1250
+ // redirects/errors
1251
+ async function handleAction(
1252
+ request: Request,
1253
+ location: Location,
1254
+ submission: Submission,
1255
+ matches: AgnosticDataRouteMatch[],
1256
+ opts?: { replace?: boolean }
1257
+ ): Promise<HandleActionResult> {
1258
+ interruptActiveLoads();
1259
+
1260
+ // Put us in a submitting state
1261
+ let navigation: NavigationStates["Submitting"] = {
1262
+ state: "submitting",
1263
+ location,
1264
+ ...submission,
1265
+ };
1266
+ updateState({ navigation });
1267
+
1268
+ // Call our action and get the result
1269
+ let result: DataResult;
1270
+ let actionMatch = getTargetMatch(matches, location);
1271
+
1272
+ if (!actionMatch.route.action) {
1273
+ result = {
1274
+ type: ResultType.error,
1275
+ error: getInternalRouterError(405, {
1276
+ method: request.method,
1277
+ pathname: location.pathname,
1278
+ routeId: actionMatch.route.id,
1279
+ }),
1280
+ };
1281
+ } else {
1282
+ result = await callLoaderOrAction(
1283
+ "action",
1284
+ request,
1285
+ actionMatch,
1286
+ matches,
1287
+ router.basename,
1288
+ future.unstable_middleware
1289
+ );
1290
+
1291
+ if (request.signal.aborted) {
1292
+ return { shortCircuited: true };
1293
+ }
1294
+ }
1295
+
1296
+ if (isRedirectResult(result)) {
1297
+ let replace: boolean;
1298
+ if (opts && opts.replace != null) {
1299
+ replace = opts.replace;
1300
+ } else {
1301
+ // If the user didn't explicity indicate replace behavior, replace if
1302
+ // we redirected to the exact same location we're currently at to avoid
1303
+ // double back-buttons
1304
+ replace =
1305
+ result.location === state.location.pathname + state.location.search;
1306
+ }
1307
+ await startRedirectNavigation(state, result, { submission, replace });
1308
+ return { shortCircuited: true };
1309
+ }
1310
+
1311
+ if (isErrorResult(result)) {
1312
+ // Store off the pending error - we use it to determine which loaders
1313
+ // to call and will commit it when we complete the navigation
1314
+ let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
1315
+
1316
+ // By default, all submissions are REPLACE navigations, but if the
1317
+ // action threw an error that'll be rendered in an errorElement, we fall
1318
+ // back to PUSH so that the user can use the back button to get back to
1319
+ // the pre-submission form location to try again
1320
+ if ((opts && opts.replace) !== true) {
1321
+ pendingAction = HistoryAction.Push;
1322
+ }
1323
+
1324
+ return {
1325
+ // Send back an empty object we can use to clear out any prior actionData
1326
+ pendingActionData: {},
1327
+ pendingActionError: { [boundaryMatch.route.id]: result.error },
1328
+ };
1329
+ }
1330
+
1331
+ if (isDeferredResult(result)) {
1332
+ throw getInternalRouterError(400, { type: "defer-action" });
1333
+ }
1334
+
1335
+ return {
1336
+ pendingActionData: { [actionMatch.route.id]: result.data },
1337
+ };
1338
+ }
1339
+
1340
+ // Call all applicable loaders for the given matches, handling redirects,
1341
+ // errors, etc.
1342
+ async function handleLoaders(
1343
+ request: Request,
1344
+ location: Location,
1345
+ matches: AgnosticDataRouteMatch[],
1346
+ overrideNavigation?: Navigation,
1347
+ submission?: Submission,
1348
+ replace?: boolean,
1349
+ pendingActionData?: RouteData,
1350
+ pendingError?: RouteData
1351
+ ): Promise<HandleLoadersResult> {
1352
+ // Figure out the right navigation we want to use for data loading
1353
+ let loadingNavigation = overrideNavigation;
1354
+ if (!loadingNavigation) {
1355
+ let navigation: NavigationStates["Loading"] = {
1356
+ state: "loading",
1357
+ location,
1358
+ formMethod: undefined,
1359
+ formAction: undefined,
1360
+ formEncType: undefined,
1361
+ formData: undefined,
1362
+ ...submission,
1363
+ };
1364
+ loadingNavigation = navigation;
1365
+ }
1366
+
1367
+ // If this was a redirect from an action we don't have a "submission" but
1368
+ // we have it on the loading navigation so use that if available
1369
+ let activeSubmission = submission
1370
+ ? submission
1371
+ : loadingNavigation.formMethod &&
1372
+ loadingNavigation.formAction &&
1373
+ loadingNavigation.formData &&
1374
+ loadingNavigation.formEncType
1375
+ ? {
1376
+ formMethod: loadingNavigation.formMethod,
1377
+ formAction: loadingNavigation.formAction,
1378
+ formData: loadingNavigation.formData,
1379
+ formEncType: loadingNavigation.formEncType,
1380
+ }
1381
+ : undefined;
1382
+
1383
+ let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(
1384
+ init.history,
1385
+ state,
1386
+ matches,
1387
+ activeSubmission,
1388
+ location,
1389
+ isRevalidationRequired,
1390
+ cancelledDeferredRoutes,
1391
+ cancelledFetcherLoads,
1392
+ pendingActionData,
1393
+ pendingError,
1394
+ fetchLoadMatches
1395
+ );
1396
+
1397
+ // Cancel pending deferreds for no-longer-matched routes or routes we're
1398
+ // about to reload. Note that if this is an action reload we would have
1399
+ // already cancelled all pending deferreds so this would be a no-op
1400
+ cancelActiveDeferreds(
1401
+ (routeId) =>
1402
+ !(matches && matches.some((m) => m.route.id === routeId)) ||
1403
+ (matchesToLoad && matchesToLoad.some((m) => m.route.id === routeId))
1404
+ );
1405
+
1406
+ // Short circuit if we have no loaders to run
1407
+ if (matchesToLoad.length === 0 && revalidatingFetchers.length === 0) {
1408
+ completeNavigation(location, {
1409
+ matches,
1410
+ loaderData: {},
1411
+ // Commit pending error if we're short circuiting
1412
+ errors: pendingError || null,
1413
+ ...(pendingActionData ? { actionData: pendingActionData } : {}),
1414
+ });
1415
+ return { shortCircuited: true };
1416
+ }
1417
+
1418
+ // If this is an uninterrupted revalidation, we remain in our current idle
1419
+ // state. If not, we need to switch to our loading state and load data,
1420
+ // preserving any new action data or existing action data (in the case of
1421
+ // a revalidation interrupting an actionReload)
1422
+ if (!isUninterruptedRevalidation) {
1423
+ revalidatingFetchers.forEach((rf) => {
1424
+ let fetcher = state.fetchers.get(rf.key);
1425
+ let revalidatingFetcher: FetcherStates["Loading"] = {
1426
+ state: "loading",
1427
+ data: fetcher && fetcher.data,
1428
+ formMethod: undefined,
1429
+ formAction: undefined,
1430
+ formEncType: undefined,
1431
+ formData: undefined,
1432
+ " _hasFetcherDoneAnything ": true,
1433
+ };
1434
+ state.fetchers.set(rf.key, revalidatingFetcher);
1435
+ });
1436
+ let actionData = pendingActionData || state.actionData;
1437
+ updateState({
1438
+ navigation: loadingNavigation,
1439
+ ...(actionData
1440
+ ? Object.keys(actionData).length === 0
1441
+ ? { actionData: null }
1442
+ : { actionData }
1443
+ : {}),
1444
+ ...(revalidatingFetchers.length > 0
1445
+ ? { fetchers: new Map(state.fetchers) }
1446
+ : {}),
1447
+ });
1448
+ }
1449
+
1450
+ pendingNavigationLoadId = ++incrementingLoadId;
1451
+ revalidatingFetchers.forEach((rf) =>
1452
+ fetchControllers.set(rf.key, pendingNavigationController!)
1453
+ );
1454
+
1455
+ let { results, loaderResults, fetcherResults } =
1456
+ await callLoadersAndMaybeResolveData(
1457
+ state.matches,
1458
+ matches,
1459
+ matchesToLoad,
1460
+ revalidatingFetchers,
1461
+ request
1462
+ );
1463
+
1464
+ if (request.signal.aborted) {
1465
+ return { shortCircuited: true };
1466
+ }
1467
+
1468
+ // Clean up _after_ loaders have completed. Don't clean up if we short
1469
+ // circuited because fetchControllers would have been aborted and
1470
+ // reassigned to new controllers for the next navigation
1471
+ revalidatingFetchers.forEach((rf) => fetchControllers.delete(rf.key));
1472
+
1473
+ // If any loaders returned a redirect Response, start a new REPLACE navigation
1474
+ let redirect = findRedirect(results);
1475
+ if (redirect) {
1476
+ await startRedirectNavigation(state, redirect, { replace });
1477
+ return { shortCircuited: true };
1478
+ }
1479
+
1480
+ // Process and commit output from loaders
1481
+ let { loaderData, errors } = processLoaderData(
1482
+ state,
1483
+ matches,
1484
+ matchesToLoad,
1485
+ loaderResults,
1486
+ pendingError,
1487
+ revalidatingFetchers,
1488
+ fetcherResults,
1489
+ activeDeferreds
1490
+ );
1491
+
1492
+ // Wire up subscribers to update loaderData as promises settle
1493
+ activeDeferreds.forEach((deferredData, routeId) => {
1494
+ deferredData.subscribe((aborted) => {
1495
+ // Note: No need to updateState here since the TrackedPromise on
1496
+ // loaderData is stable across resolve/reject
1497
+ // Remove this instance if we were aborted or if promises have settled
1498
+ if (aborted || deferredData.done) {
1499
+ activeDeferreds.delete(routeId);
1500
+ }
1501
+ });
1502
+ });
1503
+
1504
+ markFetchRedirectsDone();
1505
+ let didAbortFetchLoads = abortStaleFetchLoads(pendingNavigationLoadId);
1506
+
1507
+ return {
1508
+ loaderData,
1509
+ errors,
1510
+ ...(didAbortFetchLoads || revalidatingFetchers.length > 0
1511
+ ? { fetchers: new Map(state.fetchers) }
1512
+ : {}),
1513
+ };
1514
+ }
1515
+
1516
+ function getFetcher<TData = any>(key: string): Fetcher<TData> {
1517
+ return state.fetchers.get(key) || IDLE_FETCHER;
1518
+ }
1519
+
1520
+ // Trigger a fetcher load/submit for the given fetcher key
1521
+ function fetch(
1522
+ key: string,
1523
+ routeId: string,
1524
+ href: string,
1525
+ opts?: RouterFetchOptions
1526
+ ) {
1527
+ if (isServer) {
1528
+ throw new Error(
1529
+ "router.fetch() was called during the server render, but it shouldn't be. " +
1530
+ "You are likely calling a useFetcher() method in the body of your component. " +
1531
+ "Try moving it to a useEffect or a callback."
1532
+ );
1533
+ }
1534
+
1535
+ if (fetchControllers.has(key)) abortFetcher(key);
1536
+
1537
+ let matches = matchRoutes(dataRoutes, href, init.basename);
1538
+ if (!matches) {
1539
+ setFetcherError(
1540
+ key,
1541
+ routeId,
1542
+ getInternalRouterError(404, { pathname: href })
1543
+ );
1544
+ return;
1545
+ }
1546
+
1547
+ let { path, submission } = normalizeNavigateOptions(href, opts, true);
1548
+ let match = getTargetMatch(matches, path);
1549
+
1550
+ pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
1551
+
1552
+ if (submission && isMutationMethod(submission.formMethod)) {
1553
+ return handleFetcherAction(
1554
+ key,
1555
+ routeId,
1556
+ path,
1557
+ match,
1558
+ matches,
1559
+ submission
1560
+ );
1561
+ }
1562
+
1563
+ // Store off the match so we can call it's shouldRevalidate on subsequent
1564
+ // revalidations
1565
+ fetchLoadMatches.set(key, { routeId, path, match, matches });
1566
+ return handleFetcherLoader(key, routeId, path, match, matches, submission);
1567
+ }
1568
+
1569
+ // Call the action for the matched fetcher.submit(), and then handle redirects,
1570
+ // errors, and revalidation
1571
+ async function handleFetcherAction(
1572
+ key: string,
1573
+ routeId: string,
1574
+ path: string,
1575
+ match: AgnosticDataRouteMatch,
1576
+ requestMatches: AgnosticDataRouteMatch[],
1577
+ submission: Submission
1578
+ ) {
1579
+ interruptActiveLoads();
1580
+ fetchLoadMatches.delete(key);
1581
+
1582
+ if (!match.route.action) {
1583
+ let error = getInternalRouterError(405, {
1584
+ method: submission.formMethod,
1585
+ pathname: path,
1586
+ routeId: routeId,
1587
+ });
1588
+ setFetcherError(key, routeId, error);
1589
+ return;
1590
+ }
1591
+
1592
+ // Put this fetcher into it's submitting state
1593
+ let existingFetcher = state.fetchers.get(key);
1594
+ let fetcher: FetcherStates["Submitting"] = {
1595
+ state: "submitting",
1596
+ ...submission,
1597
+ data: existingFetcher && existingFetcher.data,
1598
+ " _hasFetcherDoneAnything ": true,
1599
+ };
1600
+ state.fetchers.set(key, fetcher);
1601
+ updateState({ fetchers: new Map(state.fetchers) });
1602
+
1603
+ // Call the action for the fetcher
1604
+ let abortController = new AbortController();
1605
+ let fetchRequest = createClientSideRequest(
1606
+ init.history,
1607
+ path,
1608
+ abortController.signal,
1609
+ submission
1610
+ );
1611
+ fetchControllers.set(key, abortController);
1612
+
1613
+ let actionResult = await callLoaderOrAction(
1614
+ "action",
1615
+ fetchRequest,
1616
+ match,
1617
+ requestMatches,
1618
+ router.basename,
1619
+ future.unstable_middleware
1620
+ );
1621
+
1622
+ if (fetchRequest.signal.aborted) {
1623
+ // We can delete this so long as we weren't aborted by ou our own fetcher
1624
+ // re-submit which would have put _new_ controller is in fetchControllers
1625
+ if (fetchControllers.get(key) === abortController) {
1626
+ fetchControllers.delete(key);
1627
+ }
1628
+ return;
1629
+ }
1630
+
1631
+ if (isRedirectResult(actionResult)) {
1632
+ fetchControllers.delete(key);
1633
+ fetchRedirectIds.add(key);
1634
+ let loadingFetcher: FetcherStates["Loading"] = {
1635
+ state: "loading",
1636
+ ...submission,
1637
+ data: undefined,
1638
+ " _hasFetcherDoneAnything ": true,
1639
+ };
1640
+ state.fetchers.set(key, loadingFetcher);
1641
+ updateState({ fetchers: new Map(state.fetchers) });
1642
+
1643
+ return startRedirectNavigation(state, actionResult, {
1644
+ isFetchActionRedirect: true,
1645
+ });
1646
+ }
1647
+
1648
+ // Process any non-redirect errors thrown
1649
+ if (isErrorResult(actionResult)) {
1650
+ setFetcherError(key, routeId, actionResult.error);
1651
+ return;
1652
+ }
1653
+
1654
+ if (isDeferredResult(actionResult)) {
1655
+ throw getInternalRouterError(400, { type: "defer-action" });
1656
+ }
1657
+
1658
+ // Start the data load for current matches, or the next location if we're
1659
+ // in the middle of a navigation
1660
+ let nextLocation = state.navigation.location || state.location;
1661
+ let revalidationRequest = createClientSideRequest(
1662
+ init.history,
1663
+
1664
+ nextLocation,
1665
+ abortController.signal
1666
+ );
1667
+ let matches =
1668
+ state.navigation.state !== "idle"
1669
+ ? matchRoutes(dataRoutes, state.navigation.location, init.basename)
1670
+ : state.matches;
1671
+
1672
+ invariant(matches, "Didn't find any matches after fetcher action");
1673
+
1674
+ let loadId = ++incrementingLoadId;
1675
+ fetchReloadIds.set(key, loadId);
1676
+
1677
+ let loadFetcher: FetcherStates["Loading"] = {
1678
+ state: "loading",
1679
+ data: actionResult.data,
1680
+ ...submission,
1681
+ " _hasFetcherDoneAnything ": true,
1682
+ };
1683
+ state.fetchers.set(key, loadFetcher);
1684
+
1685
+ let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(
1686
+ init.history,
1687
+ state,
1688
+ matches,
1689
+ submission,
1690
+ nextLocation,
1691
+ isRevalidationRequired,
1692
+ cancelledDeferredRoutes,
1693
+ cancelledFetcherLoads,
1694
+ { [match.route.id]: actionResult.data },
1695
+ undefined, // No need to send through errors since we short circuit above
1696
+ fetchLoadMatches
1697
+ );
1698
+
1699
+ // Put all revalidating fetchers into the loading state, except for the
1700
+ // current fetcher which we want to keep in it's current loading state which
1701
+ // contains it's action submission info + action data
1702
+ revalidatingFetchers
1703
+ .filter((rf) => rf.key !== key)
1704
+ .forEach((rf) => {
1705
+ let staleKey = rf.key;
1706
+ let existingFetcher = state.fetchers.get(staleKey);
1707
+ let revalidatingFetcher: FetcherStates["Loading"] = {
1708
+ state: "loading",
1709
+ data: existingFetcher && existingFetcher.data,
1710
+ formMethod: undefined,
1711
+ formAction: undefined,
1712
+ formEncType: undefined,
1713
+ formData: undefined,
1714
+ " _hasFetcherDoneAnything ": true,
1715
+ };
1716
+ state.fetchers.set(staleKey, revalidatingFetcher);
1717
+ fetchControllers.set(staleKey, abortController);
1718
+ });
1719
+
1720
+ updateState({ fetchers: new Map(state.fetchers) });
1721
+
1722
+ let { results, loaderResults, fetcherResults } =
1723
+ await callLoadersAndMaybeResolveData(
1724
+ state.matches,
1725
+ matches,
1726
+ matchesToLoad,
1727
+ revalidatingFetchers,
1728
+ revalidationRequest
1729
+ );
1730
+
1731
+ if (abortController.signal.aborted) {
1732
+ return;
1733
+ }
1734
+
1735
+ fetchReloadIds.delete(key);
1736
+ fetchControllers.delete(key);
1737
+ revalidatingFetchers.forEach((r) => fetchControllers.delete(r.key));
1738
+
1739
+ let redirect = findRedirect(results);
1740
+ if (redirect) {
1741
+ return startRedirectNavigation(state, redirect);
1742
+ }
1743
+
1744
+ // Process and commit output from loaders
1745
+ let { loaderData, errors } = processLoaderData(
1746
+ state,
1747
+ state.matches,
1748
+ matchesToLoad,
1749
+ loaderResults,
1750
+ undefined,
1751
+ revalidatingFetchers,
1752
+ fetcherResults,
1753
+ activeDeferreds
1754
+ );
1755
+
1756
+ let doneFetcher: FetcherStates["Idle"] = {
1757
+ state: "idle",
1758
+ data: actionResult.data,
1759
+ formMethod: undefined,
1760
+ formAction: undefined,
1761
+ formEncType: undefined,
1762
+ formData: undefined,
1763
+ " _hasFetcherDoneAnything ": true,
1764
+ };
1765
+ state.fetchers.set(key, doneFetcher);
1766
+
1767
+ let didAbortFetchLoads = abortStaleFetchLoads(loadId);
1768
+
1769
+ // If we are currently in a navigation loading state and this fetcher is
1770
+ // more recent than the navigation, we want the newer data so abort the
1771
+ // navigation and complete it with the fetcher data
1772
+ if (
1773
+ state.navigation.state === "loading" &&
1774
+ loadId > pendingNavigationLoadId
1775
+ ) {
1776
+ invariant(pendingAction, "Expected pending action");
1777
+ pendingNavigationController && pendingNavigationController.abort();
1778
+
1779
+ completeNavigation(state.navigation.location, {
1780
+ matches,
1781
+ loaderData,
1782
+ errors,
1783
+ fetchers: new Map(state.fetchers),
1784
+ });
1785
+ } else {
1786
+ // otherwise just update with the fetcher data, preserving any existing
1787
+ // loaderData for loaders that did not need to reload. We have to
1788
+ // manually merge here since we aren't going through completeNavigation
1789
+ updateState({
1790
+ errors,
1791
+ loaderData: mergeLoaderData(
1792
+ state.loaderData,
1793
+ loaderData,
1794
+ matches,
1795
+ errors
1796
+ ),
1797
+ ...(didAbortFetchLoads ? { fetchers: new Map(state.fetchers) } : {}),
1798
+ });
1799
+ isRevalidationRequired = false;
1800
+ }
1801
+ }
1802
+
1803
+ // Call the matched loader for fetcher.load(), handling redirects, errors, etc.
1804
+ async function handleFetcherLoader(
1805
+ key: string,
1806
+ routeId: string,
1807
+ path: string,
1808
+ match: AgnosticDataRouteMatch,
1809
+ matches: AgnosticDataRouteMatch[],
1810
+ submission?: Submission
1811
+ ) {
1812
+ let existingFetcher = state.fetchers.get(key);
1813
+ // Put this fetcher into it's loading state
1814
+ let loadingFetcher: FetcherStates["Loading"] = {
1815
+ state: "loading",
1816
+ formMethod: undefined,
1817
+ formAction: undefined,
1818
+ formEncType: undefined,
1819
+ formData: undefined,
1820
+ ...submission,
1821
+ data: existingFetcher && existingFetcher.data,
1822
+ " _hasFetcherDoneAnything ": true,
1823
+ };
1824
+ state.fetchers.set(key, loadingFetcher);
1825
+ updateState({ fetchers: new Map(state.fetchers) });
1826
+
1827
+ // Call the loader for this fetcher route match
1828
+ let abortController = new AbortController();
1829
+ let fetchRequest = createClientSideRequest(
1830
+ init.history,
1831
+ path,
1832
+ abortController.signal
1833
+ );
1834
+ fetchControllers.set(key, abortController);
1835
+
1836
+ let result: DataResult = await callLoaderOrAction(
1837
+ "loader",
1838
+ fetchRequest,
1839
+ match,
1840
+ matches,
1841
+ router.basename,
1842
+ future.unstable_middleware
1843
+ );
1844
+
1845
+ // Deferred isn't supported for fetcher loads, await everything and treat it
1846
+ // as a normal load. resolveDeferredData will return undefined if this
1847
+ // fetcher gets aborted, so we just leave result untouched and short circuit
1848
+ // below if that happens
1849
+ if (isDeferredResult(result)) {
1850
+ result =
1851
+ (await resolveDeferredData(result, fetchRequest.signal, true)) ||
1852
+ result;
1853
+ }
1854
+
1855
+ // We can delete this so long as we weren't aborted by ou our own fetcher
1856
+ // re-load which would have put _new_ controller is in fetchControllers
1857
+ if (fetchControllers.get(key) === abortController) {
1858
+ fetchControllers.delete(key);
1859
+ }
1860
+
1861
+ if (fetchRequest.signal.aborted) {
1862
+ return;
1863
+ }
1864
+
1865
+ // If the loader threw a redirect Response, start a new REPLACE navigation
1866
+ if (isRedirectResult(result)) {
1867
+ await startRedirectNavigation(state, result);
1868
+ return;
1869
+ }
1870
+
1871
+ // Process any non-redirect errors thrown
1872
+ if (isErrorResult(result)) {
1873
+ let boundaryMatch = findNearestBoundary(state.matches, routeId);
1874
+ state.fetchers.delete(key);
1875
+ // TODO: In remix, this would reset to IDLE_NAVIGATION if it was a catch -
1876
+ // do we need to behave any differently with our non-redirect errors?
1877
+ // What if it was a non-redirect Response?
1878
+ updateState({
1879
+ fetchers: new Map(state.fetchers),
1880
+ errors: {
1881
+ [boundaryMatch.route.id]: result.error,
1882
+ },
1883
+ });
1884
+ return;
1885
+ }
1886
+
1887
+ invariant(!isDeferredResult(result), "Unhandled fetcher deferred data");
1888
+
1889
+ // Put the fetcher back into an idle state
1890
+ let doneFetcher: FetcherStates["Idle"] = {
1891
+ state: "idle",
1892
+ data: result.data,
1893
+ formMethod: undefined,
1894
+ formAction: undefined,
1895
+ formEncType: undefined,
1896
+ formData: undefined,
1897
+ " _hasFetcherDoneAnything ": true,
1898
+ };
1899
+ state.fetchers.set(key, doneFetcher);
1900
+ updateState({ fetchers: new Map(state.fetchers) });
1901
+ }
1902
+
1903
+ /**
1904
+ * Utility function to handle redirects returned from an action or loader.
1905
+ * Normally, a redirect "replaces" the navigation that triggered it. So, for
1906
+ * example:
1907
+ *
1908
+ * - user is on /a
1909
+ * - user clicks a link to /b
1910
+ * - loader for /b redirects to /c
1911
+ *
1912
+ * In a non-JS app the browser would track the in-flight navigation to /b and
1913
+ * then replace it with /c when it encountered the redirect response. In
1914
+ * the end it would only ever update the URL bar with /c.
1915
+ *
1916
+ * In client-side routing using pushState/replaceState, we aim to emulate
1917
+ * this behavior and we also do not update history until the end of the
1918
+ * navigation (including processed redirects). This means that we never
1919
+ * actually touch history until we've processed redirects, so we just use
1920
+ * the history action from the original navigation (PUSH or REPLACE).
1921
+ */
1922
+ async function startRedirectNavigation(
1923
+ state: RouterState,
1924
+ redirect: RedirectResult,
1925
+ {
1926
+ submission,
1927
+ replace,
1928
+ isFetchActionRedirect,
1929
+ }: {
1930
+ submission?: Submission;
1931
+ replace?: boolean;
1932
+ isFetchActionRedirect?: boolean;
1933
+ } = {}
1934
+ ) {
1935
+ if (redirect.revalidate) {
1936
+ isRevalidationRequired = true;
1937
+ }
1938
+
1939
+ let redirectLocation = createLocation(
1940
+ state.location,
1941
+ redirect.location,
1942
+ // TODO: This can be removed once we get rid of useTransition in Remix v2
1943
+ {
1944
+ _isRedirect: true,
1945
+ ...(isFetchActionRedirect ? { _isFetchActionRedirect: true } : {}),
1946
+ }
1947
+ );
1948
+ invariant(
1949
+ redirectLocation,
1950
+ "Expected a location on the redirect navigation"
1951
+ );
1952
+
1953
+ // Check if this an absolute external redirect that goes to a new origin
1954
+ if (
1955
+ ABSOLUTE_URL_REGEX.test(redirect.location) &&
1956
+ isBrowser &&
1957
+ typeof window?.location !== "undefined"
1958
+ ) {
1959
+ let newOrigin = init.history.createURL(redirect.location).origin;
1960
+ if (window.location.origin !== newOrigin) {
1961
+ if (replace) {
1962
+ window.location.replace(redirect.location);
1963
+ } else {
1964
+ window.location.assign(redirect.location);
1965
+ }
1966
+ return;
1967
+ }
1968
+ }
1969
+
1970
+ // There's no need to abort on redirects, since we don't detect the
1971
+ // redirect until the action/loaders have settled
1972
+ pendingNavigationController = null;
1973
+
1974
+ let redirectHistoryAction =
1975
+ replace === true ? HistoryAction.Replace : HistoryAction.Push;
1976
+
1977
+ // Use the incoming submission if provided, fallback on the active one in
1978
+ // state.navigation
1979
+ let { formMethod, formAction, formEncType, formData } = state.navigation;
1980
+ if (!submission && formMethod && formAction && formData && formEncType) {
1981
+ submission = {
1982
+ formMethod,
1983
+ formAction,
1984
+ formEncType,
1985
+ formData,
1986
+ };
1987
+ }
1988
+
1989
+ // If this was a 307/308 submission we want to preserve the HTTP method and
1990
+ // re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the
1991
+ // redirected location
1992
+ if (
1993
+ redirectPreserveMethodStatusCodes.has(redirect.status) &&
1994
+ submission &&
1995
+ isMutationMethod(submission.formMethod)
1996
+ ) {
1997
+ await startNavigation(redirectHistoryAction, redirectLocation, {
1998
+ submission: {
1999
+ ...submission,
2000
+ formAction: redirect.location,
2001
+ },
2002
+ // Preserve this flag across redirects
2003
+ preventScrollReset: pendingPreventScrollReset,
2004
+ });
2005
+ } else {
2006
+ // Otherwise, we kick off a new loading navigation, preserving the
2007
+ // submission info for the duration of this navigation
2008
+ await startNavigation(redirectHistoryAction, redirectLocation, {
2009
+ overrideNavigation: {
2010
+ state: "loading",
2011
+ location: redirectLocation,
2012
+ formMethod: submission ? submission.formMethod : undefined,
2013
+ formAction: submission ? submission.formAction : undefined,
2014
+ formEncType: submission ? submission.formEncType : undefined,
2015
+ formData: submission ? submission.formData : undefined,
2016
+ },
2017
+ // Preserve this flag across redirects
2018
+ preventScrollReset: pendingPreventScrollReset,
2019
+ });
2020
+ }
2021
+ }
2022
+
2023
+ async function callLoadersAndMaybeResolveData(
2024
+ currentMatches: AgnosticDataRouteMatch[],
2025
+ matches: AgnosticDataRouteMatch[],
2026
+ matchesToLoad: AgnosticDataRouteMatch[],
2027
+ fetchersToLoad: RevalidatingFetcher[],
2028
+ request: Request
2029
+ ) {
2030
+ // Call all navigation loaders and revalidating fetcher loaders in parallel,
2031
+ // then slice off the results into separate arrays so we can handle them
2032
+ // accordingly
2033
+ let results = await Promise.all([
2034
+ ...matchesToLoad.map((match) =>
2035
+ callLoaderOrAction(
2036
+ "loader",
2037
+ request,
2038
+ match,
2039
+ matches,
2040
+ router.basename,
2041
+ future.unstable_middleware
2042
+ )
2043
+ ),
2044
+ ...fetchersToLoad.map((f) =>
2045
+ callLoaderOrAction(
2046
+ "loader",
2047
+ createClientSideRequest(init.history, f.path, request.signal),
2048
+ f.match,
2049
+ f.matches,
2050
+ router.basename,
2051
+ future.unstable_middleware
2052
+ )
2053
+ ),
2054
+ ]);
2055
+ let loaderResults = results.slice(0, matchesToLoad.length);
2056
+ let fetcherResults = results.slice(matchesToLoad.length);
2057
+
2058
+ await Promise.all([
2059
+ resolveDeferredResults(
2060
+ currentMatches,
2061
+ matchesToLoad,
2062
+ loaderResults,
2063
+ request.signal,
2064
+ false,
2065
+ state.loaderData
2066
+ ),
2067
+ resolveDeferredResults(
2068
+ currentMatches,
2069
+ fetchersToLoad.map((f) => f.match),
2070
+ fetcherResults,
2071
+ request.signal,
2072
+ true
2073
+ ),
2074
+ ]);
2075
+
2076
+ return { results, loaderResults, fetcherResults };
2077
+ }
2078
+
2079
+ function interruptActiveLoads() {
2080
+ // Every interruption triggers a revalidation
2081
+ isRevalidationRequired = true;
2082
+
2083
+ // Cancel pending route-level deferreds and mark cancelled routes for
2084
+ // revalidation
2085
+ cancelledDeferredRoutes.push(...cancelActiveDeferreds());
2086
+
2087
+ // Abort in-flight fetcher loads
2088
+ fetchLoadMatches.forEach((_, key) => {
2089
+ if (fetchControllers.has(key)) {
2090
+ cancelledFetcherLoads.push(key);
2091
+ abortFetcher(key);
2092
+ }
2093
+ });
2094
+ }
2095
+
2096
+ function setFetcherError(key: string, routeId: string, error: any) {
2097
+ let boundaryMatch = findNearestBoundary(state.matches, routeId);
2098
+ deleteFetcher(key);
2099
+ updateState({
2100
+ errors: {
2101
+ [boundaryMatch.route.id]: error,
2102
+ },
2103
+ fetchers: new Map(state.fetchers),
2104
+ });
2105
+ }
2106
+
2107
+ function deleteFetcher(key: string): void {
2108
+ if (fetchControllers.has(key)) abortFetcher(key);
2109
+ fetchLoadMatches.delete(key);
2110
+ fetchReloadIds.delete(key);
2111
+ fetchRedirectIds.delete(key);
2112
+ state.fetchers.delete(key);
2113
+ }
2114
+
2115
+ function abortFetcher(key: string) {
2116
+ let controller = fetchControllers.get(key);
2117
+ invariant(controller, `Expected fetch controller: ${key}`);
2118
+ controller.abort();
2119
+ fetchControllers.delete(key);
2120
+ }
2121
+
2122
+ function markFetchersDone(keys: string[]) {
2123
+ for (let key of keys) {
2124
+ let fetcher = getFetcher(key);
2125
+ let doneFetcher: FetcherStates["Idle"] = {
2126
+ state: "idle",
2127
+ data: fetcher.data,
2128
+ formMethod: undefined,
2129
+ formAction: undefined,
2130
+ formEncType: undefined,
2131
+ formData: undefined,
2132
+ " _hasFetcherDoneAnything ": true,
2133
+ };
2134
+ state.fetchers.set(key, doneFetcher);
2135
+ }
2136
+ }
2137
+
2138
+ function markFetchRedirectsDone(): void {
2139
+ let doneKeys = [];
2140
+ for (let key of fetchRedirectIds) {
2141
+ let fetcher = state.fetchers.get(key);
2142
+ invariant(fetcher, `Expected fetcher: ${key}`);
2143
+ if (fetcher.state === "loading") {
2144
+ fetchRedirectIds.delete(key);
2145
+ doneKeys.push(key);
2146
+ }
2147
+ }
2148
+ markFetchersDone(doneKeys);
2149
+ }
2150
+
2151
+ function abortStaleFetchLoads(landedId: number): boolean {
2152
+ let yeetedKeys = [];
2153
+ for (let [key, id] of fetchReloadIds) {
2154
+ if (id < landedId) {
2155
+ let fetcher = state.fetchers.get(key);
2156
+ invariant(fetcher, `Expected fetcher: ${key}`);
2157
+ if (fetcher.state === "loading") {
2158
+ abortFetcher(key);
2159
+ fetchReloadIds.delete(key);
2160
+ yeetedKeys.push(key);
2161
+ }
2162
+ }
2163
+ }
2164
+ markFetchersDone(yeetedKeys);
2165
+ return yeetedKeys.length > 0;
2166
+ }
2167
+
2168
+ function getBlocker(key: string, fn: BlockerFunction) {
2169
+ let blocker: Blocker = state.blockers.get(key) || IDLE_BLOCKER;
2170
+
2171
+ if (blockerFunctions.get(key) !== fn) {
2172
+ blockerFunctions.set(key, fn);
2173
+ }
2174
+
2175
+ return blocker;
2176
+ }
2177
+
2178
+ function deleteBlocker(key: string) {
2179
+ state.blockers.delete(key);
2180
+ blockerFunctions.delete(key);
2181
+ }
2182
+
2183
+ // Utility function to update blockers, ensuring valid state transitions
2184
+ function updateBlocker(key: string, newBlocker: Blocker) {
2185
+ let blocker = state.blockers.get(key) || IDLE_BLOCKER;
2186
+
2187
+ // Poor mans state machine :)
2188
+ // https://mermaid.live/edit#pako:eNqVkc9OwzAMxl8l8nnjAYrEtDIOHEBIgwvKJTReGy3_lDpIqO27k6awMG0XcrLlnz87nwdonESogKXXBuE79rq75XZO3-yHds0RJVuv70YrPlUrCEe2HfrORS3rubqZfuhtpg5C9wk5tZ4VKcRUq88q9Z8RS0-48cE1iHJkL0ugbHuFLus9L6spZy8nX9MP2CNdomVaposqu3fGayT8T8-jJQwhepo_UtpgBQaDEUom04dZhAN1aJBDlUKJBxE1ceB2Smj0Mln-IBW5AFU2dwUiktt_2Qaq2dBfaKdEup85UV7Yd-dKjlnkabl2Pvr0DTkTreM
2189
+ invariant(
2190
+ (blocker.state === "unblocked" && newBlocker.state === "blocked") ||
2191
+ (blocker.state === "blocked" && newBlocker.state === "blocked") ||
2192
+ (blocker.state === "blocked" && newBlocker.state === "proceeding") ||
2193
+ (blocker.state === "blocked" && newBlocker.state === "unblocked") ||
2194
+ (blocker.state === "proceeding" && newBlocker.state === "unblocked"),
2195
+ `Invalid blocker state transition: ${blocker.state} -> ${newBlocker.state}`
2196
+ );
2197
+
2198
+ state.blockers.set(key, newBlocker);
2199
+ updateState({ blockers: new Map(state.blockers) });
2200
+ }
2201
+
2202
+ function shouldBlockNavigation({
2203
+ currentLocation,
2204
+ nextLocation,
2205
+ historyAction,
2206
+ }: {
2207
+ currentLocation: Location;
2208
+ nextLocation: Location;
2209
+ historyAction: HistoryAction;
2210
+ }): string | undefined {
2211
+ if (blockerFunctions.size === 0) {
2212
+ return;
2213
+ }
2214
+
2215
+ // We ony support a single active blocker at the moment since we don't have
2216
+ // any compelling use cases for multi-blocker yet
2217
+ if (blockerFunctions.size > 1) {
2218
+ warning(false, "A router only supports one blocker at a time");
2219
+ }
2220
+
2221
+ let entries = Array.from(blockerFunctions.entries());
2222
+ let [blockerKey, blockerFunction] = entries[entries.length - 1];
2223
+ let blocker = state.blockers.get(blockerKey);
2224
+
2225
+ if (blocker && blocker.state === "proceeding") {
2226
+ // If the blocker is currently proceeding, we don't need to re-check
2227
+ // it and can let this navigation continue
2228
+ return;
2229
+ }
2230
+
2231
+ // At this point, we know we're unblocked/blocked so we need to check the
2232
+ // user-provided blocker function
2233
+ if (blockerFunction({ currentLocation, nextLocation, historyAction })) {
2234
+ return blockerKey;
2235
+ }
2236
+ }
2237
+
2238
+ function cancelActiveDeferreds(
2239
+ predicate?: (routeId: string) => boolean
2240
+ ): string[] {
2241
+ let cancelledRouteIds: string[] = [];
2242
+ activeDeferreds.forEach((dfd, routeId) => {
2243
+ if (!predicate || predicate(routeId)) {
2244
+ // Cancel the deferred - but do not remove from activeDeferreds here -
2245
+ // we rely on the subscribers to do that so our tests can assert proper
2246
+ // cleanup via _internalActiveDeferreds
2247
+ dfd.cancel();
2248
+ cancelledRouteIds.push(routeId);
2249
+ activeDeferreds.delete(routeId);
2250
+ }
2251
+ });
2252
+ return cancelledRouteIds;
2253
+ }
2254
+
2255
+ // Opt in to capturing and reporting scroll positions during navigations,
2256
+ // used by the <ScrollRestoration> component
2257
+ function enableScrollRestoration(
2258
+ positions: Record<string, number>,
2259
+ getPosition: GetScrollPositionFunction,
2260
+ getKey?: GetScrollRestorationKeyFunction
2261
+ ) {
2262
+ savedScrollPositions = positions;
2263
+ getScrollPosition = getPosition;
2264
+ getScrollRestorationKey = getKey || ((location) => location.key);
2265
+
2266
+ // Perform initial hydration scroll restoration, since we miss the boat on
2267
+ // the initial updateState() because we've not yet rendered <ScrollRestoration/>
2268
+ // and therefore have no savedScrollPositions available
2269
+ if (!initialScrollRestored && state.navigation === IDLE_NAVIGATION) {
2270
+ initialScrollRestored = true;
2271
+ let y = getSavedScrollPosition(state.location, state.matches);
2272
+ if (y != null) {
2273
+ updateState({ restoreScrollPosition: y });
2274
+ }
2275
+ }
2276
+
2277
+ return () => {
2278
+ savedScrollPositions = null;
2279
+ getScrollPosition = null;
2280
+ getScrollRestorationKey = null;
2281
+ };
2282
+ }
2283
+
2284
+ function saveScrollPosition(
2285
+ location: Location,
2286
+ matches: AgnosticDataRouteMatch[]
2287
+ ): void {
2288
+ if (savedScrollPositions && getScrollRestorationKey && getScrollPosition) {
2289
+ let userMatches = matches.map((m) =>
2290
+ createUseMatchesMatch(m, state.loaderData)
2291
+ );
2292
+ let key = getScrollRestorationKey(location, userMatches) || location.key;
2293
+ savedScrollPositions[key] = getScrollPosition();
2294
+ }
2295
+ }
2296
+
2297
+ function getSavedScrollPosition(
2298
+ location: Location,
2299
+ matches: AgnosticDataRouteMatch[]
2300
+ ): number | null {
2301
+ if (savedScrollPositions && getScrollRestorationKey && getScrollPosition) {
2302
+ let userMatches = matches.map((m) =>
2303
+ createUseMatchesMatch(m, state.loaderData)
2304
+ );
2305
+ let key = getScrollRestorationKey(location, userMatches) || location.key;
2306
+ let y = savedScrollPositions[key];
2307
+ if (typeof y === "number") {
2308
+ return y;
2309
+ }
2310
+ }
2311
+ return null;
2312
+ }
2313
+
2314
+ router = {
2315
+ get basename() {
2316
+ return init.basename;
2317
+ },
2318
+ get state() {
2319
+ return state;
2320
+ },
2321
+ get routes() {
2322
+ return dataRoutes;
2323
+ },
2324
+ initialize,
2325
+ subscribe,
2326
+ enableScrollRestoration,
2327
+ navigate,
2328
+ fetch,
2329
+ revalidate,
2330
+ // Passthrough to history-aware createHref used by useHref so we get proper
2331
+ // hash-aware URLs in DOM paths
2332
+ createHref: (to: To) => init.history.createHref(to),
2333
+ encodeLocation: (to: To) => init.history.encodeLocation(to),
2334
+ getFetcher,
2335
+ deleteFetcher,
2336
+ dispose,
2337
+ getBlocker,
2338
+ deleteBlocker,
2339
+ _internalFetchControllers: fetchControllers,
2340
+ _internalActiveDeferreds: activeDeferreds,
2341
+ };
2342
+
2343
+ return router;
2344
+ }
2345
+ //#endregion
2346
+
2347
+ ////////////////////////////////////////////////////////////////////////////////
2348
+ //#region createStaticHandler
2349
+ ////////////////////////////////////////////////////////////////////////////////
2350
+
2351
+ export const UNSAFE_DEFERRED_SYMBOL = Symbol("deferred");
2352
+
2353
+ export interface StaticHandlerInit {
2354
+ basename?: string;
2355
+ future?: FutureConfig;
2356
+ }
2357
+
2358
+ export function createStaticHandler(
2359
+ routes: AgnosticRouteObject[],
2360
+ init?: StaticHandlerInit
2361
+ ): StaticHandler {
2362
+ invariant(
2363
+ routes.length > 0,
2364
+ "You must provide a non-empty routes array to createStaticHandler"
2365
+ );
2366
+
2367
+ let dataRoutes = convertRoutesToDataRoutes(routes);
2368
+ let basename = (init ? init.basename : null) || "/";
2369
+ let future: FutureConfig = {
2370
+ ...defaultFutureConfig,
2371
+ ...(init && init.future ? init.future : null),
2372
+ };
2373
+
2374
+ /**
2375
+ * The query() method is intended for document requests, in which we want to
2376
+ * call an optional action and potentially multiple loaders for all nested
2377
+ * routes. It returns a StaticHandlerContext object, which is very similar
2378
+ * to the router state (location, loaderData, actionData, errors, etc.) and
2379
+ * also adds SSR-specific information such as the statusCode and headers
2380
+ * from action/loaders Responses.
2381
+ *
2382
+ * It _should_ never throw and should report all errors through the
2383
+ * returned context.errors object, properly associating errors to their error
2384
+ * boundary. Additionally, it tracks _deepestRenderedBoundaryId which can be
2385
+ * used to emulate React error boundaries during SSr by performing a second
2386
+ * pass only down to the boundaryId.
2387
+ *
2388
+ * The one exception where we do not return a StaticHandlerContext is when a
2389
+ * redirect response is returned or thrown from any action/loader. We
2390
+ * propagate that out and return the raw Response so the HTTP server can
2391
+ * return it directly.
2392
+ */
2393
+ async function query(
2394
+ request: Request,
2395
+ { requestContext, middlewareContext }: StaticHandlerQueryOpts = {}
2396
+ ): Promise<StaticHandlerContext | Response> {
2397
+ let url = new URL(request.url);
2398
+ let method = request.method.toLowerCase();
2399
+ let location = createLocation("", createPath(url), null, "default");
2400
+ let matches = matchRoutes(dataRoutes, location, basename);
2401
+
2402
+ // SSR supports HEAD requests while SPA doesn't
2403
+ if (!isValidMethod(method) && method !== "head") {
2404
+ let error = getInternalRouterError(405, { method });
2405
+ let { matches: methodNotAllowedMatches, route } =
2406
+ getShortCircuitMatches(dataRoutes);
2407
+ return {
2408
+ basename,
2409
+ location,
2410
+ matches: methodNotAllowedMatches,
2411
+ loaderData: {},
2412
+ actionData: null,
2413
+ errors: {
2414
+ [route.id]: error,
2415
+ },
2416
+ statusCode: error.status,
2417
+ loaderHeaders: {},
2418
+ actionHeaders: {},
2419
+ activeDeferreds: null,
2420
+ };
2421
+ } else if (!matches) {
2422
+ let error = getInternalRouterError(404, { pathname: location.pathname });
2423
+ let { matches: notFoundMatches, route } =
2424
+ getShortCircuitMatches(dataRoutes);
2425
+ return {
2426
+ basename,
2427
+ location,
2428
+ matches: notFoundMatches,
2429
+ loaderData: {},
2430
+ actionData: null,
2431
+ errors: {
2432
+ [route.id]: error,
2433
+ },
2434
+ statusCode: error.status,
2435
+ loaderHeaders: {},
2436
+ actionHeaders: {},
2437
+ activeDeferreds: null,
2438
+ };
2439
+ }
2440
+
2441
+ let result = await queryImpl(
2442
+ request,
2443
+ location,
2444
+ matches,
2445
+ requestContext,
2446
+ middlewareContext
2447
+ );
2448
+ if (isResponse(result)) {
2449
+ return result;
2450
+ }
2451
+
2452
+ // When returning StaticHandlerContext, we patch back in the location here
2453
+ // since we need it for React Context. But this helps keep our submit and
2454
+ // loadRouteData operating on a Request instead of a Location
2455
+ return { location, basename, ...result };
2456
+ }
2457
+
2458
+ /**
2459
+ * The queryRoute() method is intended for targeted route requests, either
2460
+ * for fetch ?_data requests or resource route requests. In this case, we
2461
+ * are only ever calling a single action or loader, and we are returning the
2462
+ * returned value directly. In most cases, this will be a Response returned
2463
+ * from the action/loader, but it may be a primitive or other value as well -
2464
+ * and in such cases the calling context should handle that accordingly.
2465
+ *
2466
+ * We do respect the throw/return differentiation, so if an action/loader
2467
+ * throws, then this method will throw the value. This is important so we
2468
+ * can do proper boundary identification in Remix where a thrown Response
2469
+ * must go to the Catch Boundary but a returned Response is happy-path.
2470
+ *
2471
+ * One thing to note is that any Router-initiated Errors that make sense
2472
+ * to associate with a status code will be thrown as an ErrorResponse
2473
+ * instance which include the raw Error, such that the calling context can
2474
+ * serialize the error as they see fit while including the proper response
2475
+ * code. Examples here are 404 and 405 errors that occur prior to reaching
2476
+ * any user-defined loaders.
2477
+ */
2478
+ async function queryRoute(
2479
+ request: Request,
2480
+ {
2481
+ routeId,
2482
+ requestContext,
2483
+ middlewareContext,
2484
+ }: StaticHandlerQueryRouteOpts = {}
2485
+ ): Promise<any> {
2486
+ let url = new URL(request.url);
2487
+ let method = request.method.toLowerCase();
2488
+ let location = createLocation("", createPath(url), null, "default");
2489
+ let matches = matchRoutes(dataRoutes, location, basename);
2490
+
2491
+ // SSR supports HEAD requests while SPA doesn't
2492
+ if (!isValidMethod(method) && method !== "head" && method !== "options") {
2493
+ throw getInternalRouterError(405, { method });
2494
+ } else if (!matches) {
2495
+ throw getInternalRouterError(404, { pathname: location.pathname });
2496
+ }
2497
+
2498
+ let match = routeId
2499
+ ? matches.find((m) => m.route.id === routeId)
2500
+ : getTargetMatch(matches, location);
2501
+
2502
+ if (routeId && !match) {
2503
+ throw getInternalRouterError(403, {
2504
+ pathname: location.pathname,
2505
+ routeId,
2506
+ });
2507
+ } else if (!match) {
2508
+ // This should never hit I don't think?
2509
+ throw getInternalRouterError(404, { pathname: location.pathname });
2510
+ }
2511
+
2512
+ let result = await queryImpl(
2513
+ request,
2514
+ location,
2515
+ matches,
2516
+ requestContext,
2517
+ middlewareContext,
2518
+ match
2519
+ );
2520
+ if (isResponse(result)) {
2521
+ return result;
2522
+ }
2523
+
2524
+ let error = result.errors ? Object.values(result.errors)[0] : undefined;
2525
+ if (error !== undefined) {
2526
+ // If we got back result.errors, that means the loader/action threw
2527
+ // _something_ that wasn't a Response, but it's not guaranteed/required
2528
+ // to be an `instanceof Error` either, so we have to use throw here to
2529
+ // preserve the "error" state outside of queryImpl.
2530
+ throw error;
2531
+ }
2532
+
2533
+ // Pick off the right state value to return
2534
+ if (result.actionData) {
2535
+ return Object.values(result.actionData)[0];
2536
+ }
2537
+
2538
+ if (result.loaderData) {
2539
+ let data = Object.values(result.loaderData)[0];
2540
+ if (result.activeDeferreds?.[match.route.id]) {
2541
+ data[UNSAFE_DEFERRED_SYMBOL] = result.activeDeferreds[match.route.id];
2542
+ }
2543
+ return data;
2544
+ }
2545
+
2546
+ return undefined;
2547
+ }
2548
+
2549
+ async function queryImpl(
2550
+ request: Request,
2551
+ location: Location,
2552
+ matches: AgnosticDataRouteMatch[],
2553
+ requestContext: unknown,
2554
+ middlewareContext?: MiddlewareContext,
2555
+ routeMatch?: AgnosticDataRouteMatch
2556
+ ): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
2557
+ invariant(
2558
+ request.signal,
2559
+ "query()/queryRoute() requests must contain an AbortController signal"
2560
+ );
2561
+
2562
+ try {
2563
+ if (isMutationMethod(request.method.toLowerCase())) {
2564
+ let result = await submit(
2565
+ request,
2566
+ matches,
2567
+ routeMatch || getTargetMatch(matches, location),
2568
+ requestContext,
2569
+ middlewareContext,
2570
+ routeMatch != null
2571
+ );
2572
+ return result;
2573
+ }
2574
+
2575
+ let result = await loadRouteData(
2576
+ request,
2577
+ matches,
2578
+ requestContext,
2579
+ middlewareContext,
2580
+ routeMatch
2581
+ );
2582
+ return isResponse(result)
2583
+ ? result
2584
+ : {
2585
+ ...result,
2586
+ actionData: null,
2587
+ actionHeaders: {},
2588
+ };
2589
+ } catch (e) {
2590
+ // If the user threw/returned a Response in callLoaderOrAction, we throw
2591
+ // it to bail out and then return or throw here based on whether the user
2592
+ // returned or threw
2593
+ if (isQueryRouteResponse(e)) {
2594
+ if (e.type === ResultType.error && !isRedirectResponse(e.response)) {
2595
+ throw e.response;
2596
+ }
2597
+ return e.response;
2598
+ }
2599
+ // Redirects are always returned since they don't propagate to catch
2600
+ // boundaries
2601
+ if (isRedirectResponse(e)) {
2602
+ return e;
2603
+ }
2604
+ throw e;
2605
+ }
2606
+ }
2607
+
2608
+ async function submit(
2609
+ request: Request,
2610
+ matches: AgnosticDataRouteMatch[],
2611
+ actionMatch: AgnosticDataRouteMatch,
2612
+ requestContext: unknown,
2613
+ middlewareContext: MiddlewareContext | undefined,
2614
+ isRouteRequest: boolean
2615
+ ): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
2616
+ let result: DataResult;
2617
+
2618
+ if (!actionMatch.route.action) {
2619
+ let error = getInternalRouterError(405, {
2620
+ method: request.method,
2621
+ pathname: new URL(request.url).pathname,
2622
+ routeId: actionMatch.route.id,
2623
+ });
2624
+ if (isRouteRequest) {
2625
+ throw error;
2626
+ }
2627
+ result = {
2628
+ type: ResultType.error,
2629
+ error,
2630
+ };
2631
+ } else {
2632
+ result = await callLoaderOrAction(
2633
+ "action",
2634
+ request,
2635
+ actionMatch,
2636
+ matches,
2637
+ basename,
2638
+ future.unstable_middleware,
2639
+ true,
2640
+ isRouteRequest,
2641
+ requestContext,
2642
+ middlewareContext
2643
+ );
2644
+
2645
+ if (request.signal.aborted) {
2646
+ let method = isRouteRequest ? "queryRoute" : "query";
2647
+ throw new Error(`${method}() call aborted`);
2648
+ }
2649
+ }
2650
+
2651
+ if (isRedirectResult(result)) {
2652
+ // Uhhhh - this should never happen, we should always throw these from
2653
+ // callLoaderOrAction, but the type narrowing here keeps TS happy and we
2654
+ // can get back on the "throw all redirect responses" train here should
2655
+ // this ever happen :/
2656
+ throw new Response(null, {
2657
+ status: result.status,
2658
+ headers: {
2659
+ Location: result.location,
2660
+ },
2661
+ });
2662
+ }
2663
+
2664
+ if (isDeferredResult(result)) {
2665
+ let error = getInternalRouterError(400, { type: "defer-action" });
2666
+ if (isRouteRequest) {
2667
+ throw error;
2668
+ }
2669
+ result = {
2670
+ type: ResultType.error,
2671
+ error,
2672
+ };
2673
+ }
2674
+
2675
+ if (isRouteRequest) {
2676
+ // Note: This should only be non-Response values if we get here, since
2677
+ // isRouteRequest should throw any Response received in callLoaderOrAction
2678
+ if (isErrorResult(result)) {
2679
+ throw result.error;
2680
+ }
2681
+
2682
+ return {
2683
+ matches: [actionMatch],
2684
+ loaderData: {},
2685
+ actionData: { [actionMatch.route.id]: result.data },
2686
+ errors: null,
2687
+ // Note: statusCode + headers are unused here since queryRoute will
2688
+ // return the raw Response or value
2689
+ statusCode: 200,
2690
+ loaderHeaders: {},
2691
+ actionHeaders: {},
2692
+ activeDeferreds: null,
2693
+ };
2694
+ }
2695
+
2696
+ if (isErrorResult(result)) {
2697
+ // Store off the pending error - we use it to determine which loaders
2698
+ // to call and will commit it when we complete the navigation
2699
+ let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
2700
+ let context = await loadRouteData(
2701
+ request,
2702
+ matches,
2703
+ requestContext,
2704
+ middlewareContext,
2705
+ undefined,
2706
+ {
2707
+ [boundaryMatch.route.id]: result.error,
2708
+ }
2709
+ );
2710
+
2711
+ // action status codes take precedence over loader status codes
2712
+ return {
2713
+ ...context,
2714
+ statusCode: isRouteErrorResponse(result.error)
2715
+ ? result.error.status
2716
+ : 500,
2717
+ actionData: null,
2718
+ actionHeaders: {
2719
+ ...(result.headers ? { [actionMatch.route.id]: result.headers } : {}),
2720
+ },
2721
+ };
2722
+ }
2723
+
2724
+ // Create a GET request for the loaders
2725
+ let loaderRequest = new Request(request.url, {
2726
+ headers: request.headers,
2727
+ redirect: request.redirect,
2728
+ signal: request.signal,
2729
+ });
2730
+ let context = await loadRouteData(
2731
+ loaderRequest,
2732
+ matches,
2733
+ requestContext,
2734
+ middlewareContext
2735
+ );
2736
+
2737
+ return {
2738
+ ...context,
2739
+ // action status codes take precedence over loader status codes
2740
+ ...(result.statusCode ? { statusCode: result.statusCode } : {}),
2741
+ actionData: {
2742
+ [actionMatch.route.id]: result.data,
2743
+ },
2744
+ actionHeaders: {
2745
+ ...(result.headers ? { [actionMatch.route.id]: result.headers } : {}),
2746
+ },
2747
+ };
2748
+ }
2749
+
2750
+ async function loadRouteData(
2751
+ request: Request,
2752
+ matches: AgnosticDataRouteMatch[],
2753
+ requestContext: unknown,
2754
+ middlewareContext: MiddlewareContext | undefined,
2755
+ routeMatch?: AgnosticDataRouteMatch,
2756
+ pendingActionError?: RouteData
2757
+ ): Promise<
2758
+ | Omit<
2759
+ StaticHandlerContext,
2760
+ "location" | "basename" | "actionData" | "actionHeaders"
2761
+ >
2762
+ | Response
2763
+ > {
2764
+ let isRouteRequest = routeMatch != null;
2765
+
2766
+ // Short circuit if we have no loaders to run (queryRoute())
2767
+ if (isRouteRequest && !routeMatch?.route.loader) {
2768
+ throw getInternalRouterError(400, {
2769
+ method: request.method,
2770
+ pathname: new URL(request.url).pathname,
2771
+ routeId: routeMatch?.route.id,
2772
+ });
2773
+ }
2774
+
2775
+ let requestMatches = routeMatch
2776
+ ? [routeMatch]
2777
+ : getLoaderMatchesUntilBoundary(
2778
+ matches,
2779
+ Object.keys(pendingActionError || {})[0]
2780
+ );
2781
+ let matchesToLoad = requestMatches.filter((m) => m.route.loader);
2782
+
2783
+ // Short circuit if we have no loaders to run (query())
2784
+ if (matchesToLoad.length === 0) {
2785
+ return {
2786
+ matches,
2787
+ // Add a null for all matched routes for proper revalidation on the client
2788
+ loaderData: matches.reduce(
2789
+ (acc, m) => Object.assign(acc, { [m.route.id]: null }),
2790
+ {}
2791
+ ),
2792
+ errors: pendingActionError || null,
2793
+ statusCode: 200,
2794
+ loaderHeaders: {},
2795
+ activeDeferreds: null,
2796
+ };
2797
+ }
2798
+
2799
+ let results = await Promise.all([
2800
+ ...matchesToLoad.map((match) =>
2801
+ callLoaderOrAction(
2802
+ "loader",
2803
+ request,
2804
+ match,
2805
+ matches,
2806
+ basename,
2807
+ future.unstable_middleware,
2808
+ true,
2809
+ isRouteRequest,
2810
+ requestContext,
2811
+ middlewareContext
2812
+ )
2813
+ ),
2814
+ ]);
2815
+
2816
+ if (request.signal.aborted) {
2817
+ let method = isRouteRequest ? "queryRoute" : "query";
2818
+ throw new Error(`${method}() call aborted`);
2819
+ }
2820
+
2821
+ // Process and commit output from loaders
2822
+ let activeDeferreds = new Map<string, DeferredData>();
2823
+ let context = processRouteLoaderData(
2824
+ matches,
2825
+ matchesToLoad,
2826
+ results,
2827
+ pendingActionError,
2828
+ activeDeferreds
2829
+ );
2830
+
2831
+ // Add a null for any non-loader matches for proper revalidation on the client
2832
+ let executedLoaders = new Set<string>(
2833
+ matchesToLoad.map((match) => match.route.id)
2834
+ );
2835
+ matches.forEach((match) => {
2836
+ if (!executedLoaders.has(match.route.id)) {
2837
+ context.loaderData[match.route.id] = null;
2838
+ }
2839
+ });
2840
+
2841
+ return {
2842
+ ...context,
2843
+ matches,
2844
+ activeDeferreds:
2845
+ activeDeferreds.size > 0
2846
+ ? Object.fromEntries(activeDeferreds.entries())
2847
+ : null,
2848
+ };
2849
+ }
2850
+
2851
+ return {
2852
+ dataRoutes,
2853
+ query,
2854
+ queryRoute,
2855
+ };
2856
+ }
2857
+
2858
+ //#endregion
2859
+
2860
+ ////////////////////////////////////////////////////////////////////////////////
2861
+ //#region Helpers
2862
+ ////////////////////////////////////////////////////////////////////////////////
2863
+
2864
+ /**
2865
+ * Given an existing StaticHandlerContext and an error thrown at render time,
2866
+ * provide an updated StaticHandlerContext suitable for a second SSR render
2867
+ */
2868
+ export function getStaticContextFromError(
2869
+ routes: AgnosticDataRouteObject[],
2870
+ context: StaticHandlerContext,
2871
+ error: any
2872
+ ) {
2873
+ let newContext: StaticHandlerContext = {
2874
+ ...context,
2875
+ statusCode: 500,
2876
+ errors: {
2877
+ [context._deepestRenderedBoundaryId || routes[0].id]: error,
2878
+ },
2879
+ };
2880
+ return newContext;
2881
+ }
2882
+
2883
+ function isSubmissionNavigation(
2884
+ opts: RouterNavigateOptions
2885
+ ): opts is SubmissionNavigateOptions {
2886
+ return opts != null && "formData" in opts;
2887
+ }
2888
+
2889
+ // Normalize navigation options by converting formMethod=GET formData objects to
2890
+ // URLSearchParams so they behave identically to links with query params
2891
+ function normalizeNavigateOptions(
2892
+ to: To,
2893
+ opts?: RouterNavigateOptions,
2894
+ isFetcher = false
2895
+ ): {
2896
+ path: string;
2897
+ submission?: Submission;
2898
+ error?: ErrorResponse;
2899
+ } {
2900
+ let path = typeof to === "string" ? to : createPath(to);
2901
+
2902
+ // Return location verbatim on non-submission navigations
2903
+ if (!opts || !isSubmissionNavigation(opts)) {
2904
+ return { path };
2905
+ }
2906
+
2907
+ if (opts.formMethod && !isValidMethod(opts.formMethod)) {
2908
+ return {
2909
+ path,
2910
+ error: getInternalRouterError(405, { method: opts.formMethod }),
2911
+ };
2912
+ }
2913
+
2914
+ // Create a Submission on non-GET navigations
2915
+ let submission: Submission | undefined;
2916
+ if (opts.formData) {
2917
+ submission = {
2918
+ formMethod: opts.formMethod || "get",
2919
+ formAction: stripHashFromPath(path),
2920
+ formEncType:
2921
+ (opts && opts.formEncType) || "application/x-www-form-urlencoded",
2922
+ formData: opts.formData,
2923
+ };
2924
+
2925
+ if (isMutationMethod(submission.formMethod)) {
2926
+ return { path, submission };
2927
+ }
2928
+ }
2929
+
2930
+ // Flatten submission onto URLSearchParams for GET submissions
2931
+ let parsedPath = parsePath(path);
2932
+ let searchParams = convertFormDataToSearchParams(opts.formData);
2933
+ // Since fetcher GET submissions only run a single loader (as opposed to
2934
+ // navigation GET submissions which run all loaders), we need to preserve
2935
+ // any incoming ?index params
2936
+ if (isFetcher && parsedPath.search && hasNakedIndexQuery(parsedPath.search)) {
2937
+ searchParams.append("index", "");
2938
+ }
2939
+ parsedPath.search = `?${searchParams}`;
2940
+
2941
+ return { path: createPath(parsedPath), submission };
2942
+ }
2943
+
2944
+ // Filter out all routes below any caught error as they aren't going to
2945
+ // render so we don't need to load them
2946
+ function getLoaderMatchesUntilBoundary(
2947
+ matches: AgnosticDataRouteMatch[],
2948
+ boundaryId?: string
2949
+ ) {
2950
+ let boundaryMatches = matches;
2951
+ if (boundaryId) {
2952
+ let index = matches.findIndex((m) => m.route.id === boundaryId);
2953
+ if (index >= 0) {
2954
+ boundaryMatches = matches.slice(0, index);
2955
+ }
2956
+ }
2957
+ return boundaryMatches;
2958
+ }
2959
+
2960
+ function getMatchesToLoad(
2961
+ history: History,
2962
+ state: RouterState,
2963
+ matches: AgnosticDataRouteMatch[],
2964
+ submission: Submission | undefined,
2965
+ location: Location,
2966
+ isRevalidationRequired: boolean,
2967
+ cancelledDeferredRoutes: string[],
2968
+ cancelledFetcherLoads: string[],
2969
+ pendingActionData?: RouteData,
2970
+ pendingError?: RouteData,
2971
+ fetchLoadMatches?: Map<string, FetchLoadMatch>
2972
+ ): [AgnosticDataRouteMatch[], RevalidatingFetcher[]] {
2973
+ let actionResult = pendingError
2974
+ ? Object.values(pendingError)[0]
2975
+ : pendingActionData
2976
+ ? Object.values(pendingActionData)[0]
2977
+ : undefined;
2978
+
2979
+ let currentUrl = history.createURL(state.location);
2980
+ let nextUrl = history.createURL(location);
2981
+
2982
+ let defaultShouldRevalidate =
2983
+ // Forced revalidation due to submission, useRevalidate, or X-Remix-Revalidate
2984
+ isRevalidationRequired ||
2985
+ // Clicked the same link, resubmitted a GET form
2986
+ currentUrl.toString() === nextUrl.toString() ||
2987
+ // Search params affect all loaders
2988
+ currentUrl.search !== nextUrl.search;
2989
+
2990
+ // Pick navigation matches that are net-new or qualify for revalidation
2991
+ let boundaryId = pendingError ? Object.keys(pendingError)[0] : undefined;
2992
+ let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId);
2993
+
2994
+ let navigationMatches = boundaryMatches.filter((match, index) => {
2995
+ if (match.route.loader == null) {
2996
+ return false;
2997
+ }
2998
+
2999
+ // Always call the loader on new route instances and pending defer cancellations
3000
+ if (
3001
+ isNewLoader(state.loaderData, state.matches[index], match) ||
3002
+ cancelledDeferredRoutes.some((id) => id === match.route.id)
3003
+ ) {
3004
+ return true;
3005
+ }
3006
+
3007
+ // This is the default implementation for when we revalidate. If the route
3008
+ // provides it's own implementation, then we give them full control but
3009
+ // provide this value so they can leverage it if needed after they check
3010
+ // their own specific use cases
3011
+ let currentRouteMatch = state.matches[index];
3012
+ let nextRouteMatch = match;
3013
+
3014
+ return shouldRevalidateLoader(match, {
3015
+ currentUrl,
3016
+ currentParams: currentRouteMatch.params,
3017
+ nextUrl,
3018
+ nextParams: nextRouteMatch.params,
3019
+ ...submission,
3020
+ actionResult,
3021
+ defaultShouldRevalidate:
3022
+ defaultShouldRevalidate ||
3023
+ isNewRouteInstance(currentRouteMatch, nextRouteMatch),
3024
+ });
3025
+ });
3026
+
3027
+ // Pick fetcher.loads that need to be revalidated
3028
+ let revalidatingFetchers: RevalidatingFetcher[] = [];
3029
+ fetchLoadMatches &&
3030
+ fetchLoadMatches.forEach((f, key) => {
3031
+ if (!matches.some((m) => m.route.id === f.routeId)) {
3032
+ // This fetcher is not going to be present in the subsequent render so
3033
+ // there's no need to revalidate it
3034
+ return;
3035
+ } else if (cancelledFetcherLoads.includes(key)) {
3036
+ // This fetcher was cancelled from a prior action submission - force reload
3037
+ revalidatingFetchers.push({ key, ...f });
3038
+ } else {
3039
+ // Revalidating fetchers are decoupled from the route matches since they
3040
+ // hit a static href, so they _always_ check shouldRevalidate and the
3041
+ // default is strictly if a revalidation is explicitly required (action
3042
+ // submissions, useRevalidator, X-Remix-Revalidate).
3043
+ let shouldRevalidate = shouldRevalidateLoader(f.match, {
3044
+ currentUrl,
3045
+ currentParams: state.matches[state.matches.length - 1].params,
3046
+ nextUrl,
3047
+ nextParams: matches[matches.length - 1].params,
3048
+ ...submission,
3049
+ actionResult,
3050
+ defaultShouldRevalidate,
3051
+ });
3052
+ if (shouldRevalidate) {
3053
+ revalidatingFetchers.push({ key, ...f });
3054
+ }
3055
+ }
3056
+ });
3057
+
3058
+ return [navigationMatches, revalidatingFetchers];
3059
+ }
3060
+
3061
+ function isNewLoader(
3062
+ currentLoaderData: RouteData,
3063
+ currentMatch: AgnosticDataRouteMatch,
3064
+ match: AgnosticDataRouteMatch
3065
+ ) {
3066
+ let isNew =
3067
+ // [a] -> [a, b]
3068
+ !currentMatch ||
3069
+ // [a, b] -> [a, c]
3070
+ match.route.id !== currentMatch.route.id;
3071
+
3072
+ // Handle the case that we don't have data for a re-used route, potentially
3073
+ // from a prior error or from a cancelled pending deferred
3074
+ let isMissingData = currentLoaderData[match.route.id] === undefined;
3075
+
3076
+ // Always load if this is a net-new route or we don't yet have data
3077
+ return isNew || isMissingData;
3078
+ }
3079
+
3080
+ function isNewRouteInstance(
3081
+ currentMatch: AgnosticDataRouteMatch,
3082
+ match: AgnosticDataRouteMatch
3083
+ ) {
3084
+ let currentPath = currentMatch.route.path;
3085
+ return (
3086
+ // param change for this match, /users/123 -> /users/456
3087
+ currentMatch.pathname !== match.pathname ||
3088
+ // splat param changed, which is not present in match.path
3089
+ // e.g. /files/images/avatar.jpg -> files/finances.xls
3090
+ (currentPath != null &&
3091
+ currentPath.endsWith("*") &&
3092
+ currentMatch.params["*"] !== match.params["*"])
3093
+ );
3094
+ }
3095
+
3096
+ function shouldRevalidateLoader(
3097
+ loaderMatch: AgnosticDataRouteMatch,
3098
+ arg: Parameters<ShouldRevalidateFunction>[0]
3099
+ ) {
3100
+ if (loaderMatch.route.shouldRevalidate) {
3101
+ let routeChoice = loaderMatch.route.shouldRevalidate(arg);
3102
+ if (typeof routeChoice === "boolean") {
3103
+ return routeChoice;
3104
+ }
3105
+ }
3106
+
3107
+ return arg.defaultShouldRevalidate;
3108
+ }
3109
+
3110
+ async function callRouteSubPipeline(
3111
+ request: Request,
3112
+ matches: AgnosticDataRouteMatch[],
3113
+ params: Params<string>,
3114
+ middlewareContext: MiddlewareContext,
3115
+ handler:
3116
+ | LoaderFunction
3117
+ | ActionFunction
3118
+ | LoaderFunctionWithMiddleware
3119
+ | ActionFunctionWithMiddleware
3120
+ ): Promise<ReturnType<LoaderFunction>> {
3121
+ if (request.signal.aborted) {
3122
+ throw new Error("Request aborted");
3123
+ }
3124
+
3125
+ if (matches.length === 0) {
3126
+ // We reached the end of our middlewares, call the handler
3127
+ middlewareContext.next = () => {
3128
+ throw new Error(
3129
+ "You may only call `next()` once per middleware and you may not call " +
3130
+ "it in an action or loader"
3131
+ );
3132
+ };
3133
+ return handler({
3134
+ request,
3135
+ params,
3136
+ context: middlewareContext,
3137
+ });
3138
+ }
3139
+
3140
+ // We've still got matches, continue on the middleware train. The `next()`
3141
+ // function will "bubble" back up the middlewares after handlers have executed
3142
+ let nextCalled = false;
3143
+ let next: MiddlewareContext["next"] = () => {
3144
+ nextCalled = true;
3145
+ return callRouteSubPipeline(
3146
+ request,
3147
+ matches.slice(1),
3148
+ params,
3149
+ middlewareContext,
3150
+ handler
3151
+ );
3152
+ };
3153
+
3154
+ if (!matches[0].route.middleware) {
3155
+ return next();
3156
+ }
3157
+
3158
+ middlewareContext.next = next;
3159
+ let res = await matches[0].route.middleware({
3160
+ request,
3161
+ params,
3162
+ context: middlewareContext,
3163
+ });
3164
+
3165
+ if (nextCalled) {
3166
+ return res;
3167
+ } else {
3168
+ return next();
3169
+ }
3170
+ }
3171
+
3172
+ function disabledMiddlewareFn() {
3173
+ throw new Error(
3174
+ "Middleware must be enabled via the `future.unstable_middleware` flag)"
3175
+ );
3176
+ }
3177
+
3178
+ const disabledMiddlewareContext: MiddlewareContext = {
3179
+ // @ts-expect-error
3180
+ get: disabledMiddlewareFn,
3181
+ set: disabledMiddlewareFn,
3182
+ next: disabledMiddlewareFn,
3183
+ };
3184
+
3185
+ async function callLoaderOrAction(
3186
+ type: "loader" | "action",
3187
+ request: Request,
3188
+ match: AgnosticDataRouteMatch,
3189
+ matches: AgnosticDataRouteMatch[],
3190
+ basename = "/",
3191
+ enableMiddleware: boolean,
3192
+ isStaticRequest: boolean = false,
3193
+ isRouteRequest: boolean = false,
3194
+ requestContext?: unknown,
3195
+ middlewareContext?: MiddlewareContext
3196
+ ): Promise<DataResult> {
3197
+ let resultType;
3198
+ let result;
3199
+
3200
+ // Setup a promise we can race against so that abort signals short circuit
3201
+ let reject: () => void;
3202
+ let abortPromise = new Promise((_, r) => (reject = r));
3203
+ let onReject = () => reject();
3204
+ request.signal.addEventListener("abort", onReject);
3205
+
3206
+ try {
3207
+ let handler = match.route[type];
3208
+ invariant(
3209
+ handler,
3210
+ `Could not find the ${type} to run on the "${match.route.id}" route`
3211
+ );
3212
+
3213
+ // Only call the pipeline for the matches up to this specific match
3214
+ let idx = matches.findIndex((m) => m.route.id === match.route.id);
3215
+ let dataPromise;
3216
+
3217
+ if (enableMiddleware) {
3218
+ dataPromise = callRouteSubPipeline(
3219
+ request,
3220
+ matches.slice(0, idx + 1),
3221
+ matches[0].params,
3222
+ createMiddlewareStore(middlewareContext),
3223
+ handler
3224
+ );
3225
+ } else {
3226
+ dataPromise = (handler as LoaderFunction | ActionFunction)({
3227
+ request,
3228
+ params: match.params,
3229
+ context: requestContext || disabledMiddlewareContext,
3230
+ });
3231
+ }
3232
+
3233
+ result = await Promise.race([dataPromise, abortPromise]);
3234
+
3235
+ invariant(
3236
+ result !== undefined,
3237
+ `You defined ${type === "action" ? "an action" : "a loader"} for route ` +
3238
+ `"${match.route.id}" but didn't return anything from your \`${type}\` ` +
3239
+ `function. Please return a value or \`null\`.`
3240
+ );
3241
+ } catch (e) {
3242
+ resultType = ResultType.error;
3243
+ result = e;
3244
+ } finally {
3245
+ request.signal.removeEventListener("abort", onReject);
3246
+ }
3247
+
3248
+ if (isResponse(result)) {
3249
+ let status = result.status;
3250
+
3251
+ // Process redirects
3252
+ if (redirectStatusCodes.has(status)) {
3253
+ let location = result.headers.get("Location");
3254
+ invariant(
3255
+ location,
3256
+ "Redirects returned/thrown from loaders/actions must have a Location header"
3257
+ );
3258
+
3259
+ // Support relative routing in internal redirects
3260
+ if (!ABSOLUTE_URL_REGEX.test(location)) {
3261
+ let activeMatches = matches.slice(0, matches.indexOf(match) + 1);
3262
+ let routePathnames = getPathContributingMatches(activeMatches).map(
3263
+ (match) => match.pathnameBase
3264
+ );
3265
+ let resolvedLocation = resolveTo(
3266
+ location,
3267
+ routePathnames,
3268
+ new URL(request.url).pathname
3269
+ );
3270
+ invariant(
3271
+ createPath(resolvedLocation),
3272
+ `Unable to resolve redirect location: ${location}`
3273
+ );
3274
+
3275
+ // Prepend the basename to the redirect location if we have one
3276
+ if (basename) {
3277
+ let path = resolvedLocation.pathname;
3278
+ resolvedLocation.pathname =
3279
+ path === "/" ? basename : joinPaths([basename, path]);
3280
+ }
3281
+
3282
+ location = createPath(resolvedLocation);
3283
+ } else if (!isStaticRequest) {
3284
+ // Strip off the protocol+origin for same-origin absolute redirects.
3285
+ // If this is a static reques, we can let it go back to the browser
3286
+ // as-is
3287
+ let currentUrl = new URL(request.url);
3288
+ let url = location.startsWith("//")
3289
+ ? new URL(currentUrl.protocol + location)
3290
+ : new URL(location);
3291
+ if (url.origin === currentUrl.origin) {
3292
+ location = url.pathname + url.search + url.hash;
3293
+ }
3294
+ }
3295
+
3296
+ // Don't process redirects in the router during static requests requests.
3297
+ // Instead, throw the Response and let the server handle it with an HTTP
3298
+ // redirect. We also update the Location header in place in this flow so
3299
+ // basename and relative routing is taken into account
3300
+ if (isStaticRequest) {
3301
+ result.headers.set("Location", location);
3302
+ throw result;
3303
+ }
3304
+
3305
+ return {
3306
+ type: ResultType.redirect,
3307
+ status,
3308
+ location,
3309
+ revalidate: result.headers.get("X-Remix-Revalidate") !== null,
3310
+ };
3311
+ }
3312
+
3313
+ // For SSR single-route requests, we want to hand Responses back directly
3314
+ // without unwrapping. We do this with the QueryRouteResponse wrapper
3315
+ // interface so we can know whether it was returned or thrown
3316
+ if (isRouteRequest) {
3317
+ // eslint-disable-next-line no-throw-literal
3318
+ throw {
3319
+ type: resultType || ResultType.data,
3320
+ response: result,
3321
+ };
3322
+ }
3323
+
3324
+ let data: any;
3325
+ let contentType = result.headers.get("Content-Type");
3326
+ // Check between word boundaries instead of startsWith() due to the last
3327
+ // paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type
3328
+ if (contentType && /\bapplication\/json\b/.test(contentType)) {
3329
+ data = await result.json();
3330
+ } else {
3331
+ data = await result.text();
3332
+ }
3333
+
3334
+ if (resultType === ResultType.error) {
3335
+ return {
3336
+ type: resultType,
3337
+ error: new ErrorResponse(status, result.statusText, data),
3338
+ headers: result.headers,
3339
+ };
3340
+ }
3341
+
3342
+ return {
3343
+ type: ResultType.data,
3344
+ data,
3345
+ statusCode: result.status,
3346
+ headers: result.headers,
3347
+ };
3348
+ }
3349
+
3350
+ if (resultType === ResultType.error) {
3351
+ return { type: resultType, error: result };
3352
+ }
3353
+
3354
+ if (result instanceof DeferredData) {
3355
+ return { type: ResultType.deferred, deferredData: result };
3356
+ }
3357
+
3358
+ return { type: ResultType.data, data: result };
3359
+ }
3360
+
3361
+ // Utility method for creating the Request instances for loaders/actions during
3362
+ // client-side navigations and fetches. During SSR we will always have a
3363
+ // Request instance from the static handler (query/queryRoute)
3364
+ function createClientSideRequest(
3365
+ history: History,
3366
+ location: string | Location,
3367
+ signal: AbortSignal,
3368
+ submission?: Submission
3369
+ ): Request {
3370
+ let url = history.createURL(stripHashFromPath(location)).toString();
3371
+ let init: RequestInit = { signal };
3372
+
3373
+ if (submission && isMutationMethod(submission.formMethod)) {
3374
+ let { formMethod, formEncType, formData } = submission;
3375
+ init.method = formMethod.toUpperCase();
3376
+ init.body =
3377
+ formEncType === "application/x-www-form-urlencoded"
3378
+ ? convertFormDataToSearchParams(formData)
3379
+ : formData;
3380
+ }
3381
+
3382
+ // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
3383
+ return new Request(url, init);
3384
+ }
3385
+
3386
+ function convertFormDataToSearchParams(formData: FormData): URLSearchParams {
3387
+ let searchParams = new URLSearchParams();
3388
+
3389
+ for (let [key, value] of formData.entries()) {
3390
+ // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#converting-an-entry-list-to-a-list-of-name-value-pairs
3391
+ searchParams.append(key, value instanceof File ? value.name : value);
3392
+ }
3393
+
3394
+ return searchParams;
3395
+ }
3396
+
3397
+ function processRouteLoaderData(
3398
+ matches: AgnosticDataRouteMatch[],
3399
+ matchesToLoad: AgnosticDataRouteMatch[],
3400
+ results: DataResult[],
3401
+ pendingError: RouteData | undefined,
3402
+ activeDeferreds: Map<string, DeferredData>
3403
+ ): {
3404
+ loaderData: RouterState["loaderData"];
3405
+ errors: RouterState["errors"] | null;
3406
+ statusCode: number;
3407
+ loaderHeaders: Record<string, Headers>;
3408
+ } {
3409
+ // Fill in loaderData/errors from our loaders
3410
+ let loaderData: RouterState["loaderData"] = {};
3411
+ let errors: RouterState["errors"] | null = null;
3412
+ let statusCode: number | undefined;
3413
+ let foundError = false;
3414
+ let loaderHeaders: Record<string, Headers> = {};
3415
+
3416
+ // Process loader results into state.loaderData/state.errors
3417
+ results.forEach((result, index) => {
3418
+ let id = matchesToLoad[index].route.id;
3419
+ invariant(
3420
+ !isRedirectResult(result),
3421
+ "Cannot handle redirect results in processLoaderData"
3422
+ );
3423
+ if (isErrorResult(result)) {
3424
+ // Look upwards from the matched route for the closest ancestor
3425
+ // error boundary, defaulting to the root match
3426
+ let boundaryMatch = findNearestBoundary(matches, id);
3427
+ let error = result.error;
3428
+ // If we have a pending action error, we report it at the highest-route
3429
+ // that throws a loader error, and then clear it out to indicate that
3430
+ // it was consumed
3431
+ if (pendingError) {
3432
+ error = Object.values(pendingError)[0];
3433
+ pendingError = undefined;
3434
+ }
3435
+
3436
+ errors = errors || {};
3437
+
3438
+ // Prefer higher error values if lower errors bubble to the same boundary
3439
+ if (errors[boundaryMatch.route.id] == null) {
3440
+ errors[boundaryMatch.route.id] = error;
3441
+ }
3442
+
3443
+ // Clear our any prior loaderData for the throwing route
3444
+ loaderData[id] = undefined;
3445
+
3446
+ // Once we find our first (highest) error, we set the status code and
3447
+ // prevent deeper status codes from overriding
3448
+ if (!foundError) {
3449
+ foundError = true;
3450
+ statusCode = isRouteErrorResponse(result.error)
3451
+ ? result.error.status
3452
+ : 500;
3453
+ }
3454
+ if (result.headers) {
3455
+ loaderHeaders[id] = result.headers;
3456
+ }
3457
+ } else {
3458
+ if (isDeferredResult(result)) {
3459
+ activeDeferreds.set(id, result.deferredData);
3460
+ loaderData[id] = result.deferredData.data;
3461
+ } else {
3462
+ loaderData[id] = result.data;
3463
+ }
3464
+
3465
+ // Error status codes always override success status codes, but if all
3466
+ // loaders are successful we take the deepest status code.
3467
+ if (
3468
+ result.statusCode != null &&
3469
+ result.statusCode !== 200 &&
3470
+ !foundError
3471
+ ) {
3472
+ statusCode = result.statusCode;
3473
+ }
3474
+ if (result.headers) {
3475
+ loaderHeaders[id] = result.headers;
3476
+ }
3477
+ }
3478
+ });
3479
+
3480
+ // If we didn't consume the pending action error (i.e., all loaders
3481
+ // resolved), then consume it here. Also clear out any loaderData for the
3482
+ // throwing route
3483
+ if (pendingError) {
3484
+ errors = pendingError;
3485
+ loaderData[Object.keys(pendingError)[0]] = undefined;
3486
+ }
3487
+
3488
+ return {
3489
+ loaderData,
3490
+ errors,
3491
+ statusCode: statusCode || 200,
3492
+ loaderHeaders,
3493
+ };
3494
+ }
3495
+
3496
+ function processLoaderData(
3497
+ state: RouterState,
3498
+ matches: AgnosticDataRouteMatch[],
3499
+ matchesToLoad: AgnosticDataRouteMatch[],
3500
+ results: DataResult[],
3501
+ pendingError: RouteData | undefined,
3502
+ revalidatingFetchers: RevalidatingFetcher[],
3503
+ fetcherResults: DataResult[],
3504
+ activeDeferreds: Map<string, DeferredData>
3505
+ ): {
3506
+ loaderData: RouterState["loaderData"];
3507
+ errors?: RouterState["errors"];
3508
+ } {
3509
+ let { loaderData, errors } = processRouteLoaderData(
3510
+ matches,
3511
+ matchesToLoad,
3512
+ results,
3513
+ pendingError,
3514
+ activeDeferreds
3515
+ );
3516
+
3517
+ // Process results from our revalidating fetchers
3518
+ for (let index = 0; index < revalidatingFetchers.length; index++) {
3519
+ let { key, match } = revalidatingFetchers[index];
3520
+ invariant(
3521
+ fetcherResults !== undefined && fetcherResults[index] !== undefined,
3522
+ "Did not find corresponding fetcher result"
3523
+ );
3524
+ let result = fetcherResults[index];
3525
+
3526
+ // Process fetcher non-redirect errors
3527
+ if (isErrorResult(result)) {
3528
+ let boundaryMatch = findNearestBoundary(state.matches, match.route.id);
3529
+ if (!(errors && errors[boundaryMatch.route.id])) {
3530
+ errors = {
3531
+ ...errors,
3532
+ [boundaryMatch.route.id]: result.error,
3533
+ };
3534
+ }
3535
+ state.fetchers.delete(key);
3536
+ } else if (isRedirectResult(result)) {
3537
+ // Should never get here, redirects should get processed above, but we
3538
+ // keep this to type narrow to a success result in the else
3539
+ invariant(false, "Unhandled fetcher revalidation redirect");
3540
+ } else if (isDeferredResult(result)) {
3541
+ // Should never get here, deferred data should be awaited for fetchers
3542
+ // in resolveDeferredResults
3543
+ invariant(false, "Unhandled fetcher deferred data");
3544
+ } else {
3545
+ let doneFetcher: FetcherStates["Idle"] = {
3546
+ state: "idle",
3547
+ data: result.data,
3548
+ formMethod: undefined,
3549
+ formAction: undefined,
3550
+ formEncType: undefined,
3551
+ formData: undefined,
3552
+ " _hasFetcherDoneAnything ": true,
3553
+ };
3554
+ state.fetchers.set(key, doneFetcher);
3555
+ }
3556
+ }
3557
+
3558
+ return { loaderData, errors };
3559
+ }
3560
+
3561
+ function mergeLoaderData(
3562
+ loaderData: RouteData,
3563
+ newLoaderData: RouteData,
3564
+ matches: AgnosticDataRouteMatch[],
3565
+ errors: RouteData | null | undefined
3566
+ ): RouteData {
3567
+ let mergedLoaderData = { ...newLoaderData };
3568
+ for (let match of matches) {
3569
+ let id = match.route.id;
3570
+ if (newLoaderData.hasOwnProperty(id)) {
3571
+ if (newLoaderData[id] !== undefined) {
3572
+ mergedLoaderData[id] = newLoaderData[id];
3573
+ } else {
3574
+ // No-op - this is so we ignore existing data if we have a key in the
3575
+ // incoming object with an undefined value, which is how we unset a prior
3576
+ // loaderData if we encounter a loader error
3577
+ }
3578
+ } else if (loaderData[id] !== undefined) {
3579
+ mergedLoaderData[id] = loaderData[id];
3580
+ }
3581
+
3582
+ if (errors && errors.hasOwnProperty(id)) {
3583
+ // Don't keep any loader data below the boundary
3584
+ break;
3585
+ }
3586
+ }
3587
+ return mergedLoaderData;
3588
+ }
3589
+
3590
+ // Find the nearest error boundary, looking upwards from the leaf route (or the
3591
+ // route specified by routeId) for the closest ancestor error boundary,
3592
+ // defaulting to the root match
3593
+ function findNearestBoundary(
3594
+ matches: AgnosticDataRouteMatch[],
3595
+ routeId?: string
3596
+ ): AgnosticDataRouteMatch {
3597
+ let eligibleMatches = routeId
3598
+ ? matches.slice(0, matches.findIndex((m) => m.route.id === routeId) + 1)
3599
+ : [...matches];
3600
+ return (
3601
+ eligibleMatches.reverse().find((m) => m.route.hasErrorBoundary === true) ||
3602
+ matches[0]
3603
+ );
3604
+ }
3605
+
3606
+ function getShortCircuitMatches(routes: AgnosticDataRouteObject[]): {
3607
+ matches: AgnosticDataRouteMatch[];
3608
+ route: AgnosticDataRouteObject;
3609
+ } {
3610
+ // Prefer a root layout route if present, otherwise shim in a route object
3611
+ let route = routes.find((r) => r.index || !r.path || r.path === "/") || {
3612
+ id: `__shim-error-route__`,
3613
+ };
3614
+
3615
+ return {
3616
+ matches: [
3617
+ {
3618
+ params: {},
3619
+ pathname: "",
3620
+ pathnameBase: "",
3621
+ route,
3622
+ },
3623
+ ],
3624
+ route,
3625
+ };
3626
+ }
3627
+
3628
+ function getInternalRouterError(
3629
+ status: number,
3630
+ {
3631
+ pathname,
3632
+ routeId,
3633
+ method,
3634
+ type,
3635
+ }: {
3636
+ pathname?: string;
3637
+ routeId?: string;
3638
+ method?: string;
3639
+ type?: "defer-action";
3640
+ } = {}
3641
+ ) {
3642
+ let statusText = "Unknown Server Error";
3643
+ let errorMessage = "Unknown @remix-run/router error";
3644
+
3645
+ if (status === 400) {
3646
+ statusText = "Bad Request";
3647
+ if (method && pathname && routeId) {
3648
+ errorMessage =
3649
+ `You made a ${method} request to "${pathname}" but ` +
3650
+ `did not provide a \`loader\` for route "${routeId}", ` +
3651
+ `so there is no way to handle the request.`;
3652
+ } else if (type === "defer-action") {
3653
+ errorMessage = "defer() is not supported in actions";
3654
+ }
3655
+ } else if (status === 403) {
3656
+ statusText = "Forbidden";
3657
+ errorMessage = `Route "${routeId}" does not match URL "${pathname}"`;
3658
+ } else if (status === 404) {
3659
+ statusText = "Not Found";
3660
+ errorMessage = `No route matches URL "${pathname}"`;
3661
+ } else if (status === 405) {
3662
+ statusText = "Method Not Allowed";
3663
+ if (method && pathname && routeId) {
3664
+ errorMessage =
3665
+ `You made a ${method.toUpperCase()} request to "${pathname}" but ` +
3666
+ `did not provide an \`action\` for route "${routeId}", ` +
3667
+ `so there is no way to handle the request.`;
3668
+ } else if (method) {
3669
+ errorMessage = `Invalid request method "${method.toUpperCase()}"`;
3670
+ }
3671
+ }
3672
+
3673
+ return new ErrorResponse(
3674
+ status || 500,
3675
+ statusText,
3676
+ new Error(errorMessage),
3677
+ true
3678
+ );
3679
+ }
3680
+
3681
+ // Find any returned redirect errors, starting from the lowest match
3682
+ function findRedirect(results: DataResult[]): RedirectResult | undefined {
3683
+ for (let i = results.length - 1; i >= 0; i--) {
3684
+ let result = results[i];
3685
+ if (isRedirectResult(result)) {
3686
+ return result;
3687
+ }
3688
+ }
3689
+ }
3690
+
3691
+ function stripHashFromPath(path: To) {
3692
+ let parsedPath = typeof path === "string" ? parsePath(path) : path;
3693
+ return createPath({ ...parsedPath, hash: "" });
3694
+ }
3695
+
3696
+ function isHashChangeOnly(a: Location, b: Location): boolean {
3697
+ return (
3698
+ a.pathname === b.pathname && a.search === b.search && a.hash !== b.hash
3699
+ );
3700
+ }
3701
+
3702
+ function isDeferredResult(result: DataResult): result is DeferredResult {
3703
+ return result.type === ResultType.deferred;
3704
+ }
3705
+
3706
+ function isErrorResult(result: DataResult): result is ErrorResult {
3707
+ return result.type === ResultType.error;
3708
+ }
3709
+
3710
+ function isRedirectResult(result?: DataResult): result is RedirectResult {
3711
+ return (result && result.type) === ResultType.redirect;
3712
+ }
3713
+
3714
+ function isResponse(value: any): value is Response {
3715
+ return (
3716
+ value != null &&
3717
+ typeof value.status === "number" &&
3718
+ typeof value.statusText === "string" &&
3719
+ typeof value.headers === "object" &&
3720
+ typeof value.body !== "undefined"
3721
+ );
3722
+ }
3723
+
3724
+ function isRedirectResponse(result: any): result is Response {
3725
+ if (!isResponse(result)) {
3726
+ return false;
3727
+ }
3728
+
3729
+ let status = result.status;
3730
+ let location = result.headers.get("Location");
3731
+ return status >= 300 && status <= 399 && location != null;
3732
+ }
3733
+
3734
+ function isQueryRouteResponse(obj: any): obj is QueryRouteResponse {
3735
+ return (
3736
+ obj &&
3737
+ isResponse(obj.response) &&
3738
+ (obj.type === ResultType.data || ResultType.error)
3739
+ );
3740
+ }
3741
+
3742
+ function isValidMethod(method: string): method is FormMethod {
3743
+ return validRequestMethods.has(method as FormMethod);
3744
+ }
3745
+
3746
+ function isMutationMethod(method?: string): method is MutationFormMethod {
3747
+ return validMutationMethods.has(method as MutationFormMethod);
3748
+ }
3749
+
3750
+ async function resolveDeferredResults(
3751
+ currentMatches: AgnosticDataRouteMatch[],
3752
+ matchesToLoad: AgnosticDataRouteMatch[],
3753
+ results: DataResult[],
3754
+ signal: AbortSignal,
3755
+ isFetcher: boolean,
3756
+ currentLoaderData?: RouteData
3757
+ ) {
3758
+ for (let index = 0; index < results.length; index++) {
3759
+ let result = results[index];
3760
+ let match = matchesToLoad[index];
3761
+ let currentMatch = currentMatches.find(
3762
+ (m) => m.route.id === match.route.id
3763
+ );
3764
+ let isRevalidatingLoader =
3765
+ currentMatch != null &&
3766
+ !isNewRouteInstance(currentMatch, match) &&
3767
+ (currentLoaderData && currentLoaderData[match.route.id]) !== undefined;
3768
+
3769
+ if (isDeferredResult(result) && (isFetcher || isRevalidatingLoader)) {
3770
+ // Note: we do not have to touch activeDeferreds here since we race them
3771
+ // against the signal in resolveDeferredData and they'll get aborted
3772
+ // there if needed
3773
+ await resolveDeferredData(result, signal, isFetcher).then((result) => {
3774
+ if (result) {
3775
+ results[index] = result || results[index];
3776
+ }
3777
+ });
3778
+ }
3779
+ }
3780
+ }
3781
+
3782
+ async function resolveDeferredData(
3783
+ result: DeferredResult,
3784
+ signal: AbortSignal,
3785
+ unwrap = false
3786
+ ): Promise<SuccessResult | ErrorResult | undefined> {
3787
+ let aborted = await result.deferredData.resolveData(signal);
3788
+ if (aborted) {
3789
+ return;
3790
+ }
3791
+
3792
+ if (unwrap) {
3793
+ try {
3794
+ return {
3795
+ type: ResultType.data,
3796
+ data: result.deferredData.unwrappedData,
3797
+ };
3798
+ } catch (e) {
3799
+ // Handle any TrackedPromise._error values encountered while unwrapping
3800
+ return {
3801
+ type: ResultType.error,
3802
+ error: e,
3803
+ };
3804
+ }
3805
+ }
3806
+
3807
+ return {
3808
+ type: ResultType.data,
3809
+ data: result.deferredData.data,
3810
+ };
3811
+ }
3812
+
3813
+ function hasNakedIndexQuery(search: string): boolean {
3814
+ return new URLSearchParams(search).getAll("index").some((v) => v === "");
3815
+ }
3816
+
3817
+ // Note: This should match the format exported by useMatches, so if you change
3818
+ // this please also change that :) Eventually we'll DRY this up
3819
+ function createUseMatchesMatch(
3820
+ match: AgnosticDataRouteMatch,
3821
+ loaderData: RouteData
3822
+ ): UseMatchesMatch {
3823
+ let { route, pathname, params } = match;
3824
+ return {
3825
+ id: route.id,
3826
+ pathname,
3827
+ params,
3828
+ data: loaderData[route.id] as unknown,
3829
+ handle: route.handle as unknown,
3830
+ };
3831
+ }
3832
+
3833
+ function getTargetMatch(
3834
+ matches: AgnosticDataRouteMatch[],
3835
+ location: Location | string
3836
+ ) {
3837
+ let search =
3838
+ typeof location === "string" ? parsePath(location).search : location.search;
3839
+ if (
3840
+ matches[matches.length - 1].route.index &&
3841
+ hasNakedIndexQuery(search || "")
3842
+ ) {
3843
+ // Return the leaf index route when index is present
3844
+ return matches[matches.length - 1];
3845
+ }
3846
+ // Otherwise grab the deepest "path contributing" match (ignoring index and
3847
+ // pathless layout routes)
3848
+ let pathMatches = getPathContributingMatches(matches);
3849
+ return pathMatches[pathMatches.length - 1];
3850
+ }
3851
+ //#endregion