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

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

Pred finalna verzija

  • Property mode set to 100644
File size: 47.7 KB
RevLine 
[d565449]1import type { Location, Path, To } from "./history";
2import { invariant, parsePath, warning } from "./history";
3
4/**
5 * Map of routeId -> data returned from a loader/action/error
6 */
7export interface RouteData {
8 [routeId: string]: any;
9}
10
11export enum ResultType {
12 data = "data",
13 deferred = "deferred",
14 redirect = "redirect",
15 error = "error",
16}
17
18/**
19 * Successful result from a loader or action
20 */
21export interface SuccessResult {
22 type: ResultType.data;
23 data: unknown;
24 statusCode?: number;
25 headers?: Headers;
26}
27
28/**
29 * Successful defer() result from a loader or action
30 */
31export interface DeferredResult {
32 type: ResultType.deferred;
33 deferredData: DeferredData;
34 statusCode?: number;
35 headers?: Headers;
36}
37
38/**
39 * Redirect result from a loader or action
40 */
41export interface RedirectResult {
42 type: ResultType.redirect;
43 // We keep the raw Response for redirects so we can return it verbatim
44 response: Response;
45}
46
47/**
48 * Unsuccessful result from a loader or action
49 */
50export interface ErrorResult {
51 type: ResultType.error;
52 error: unknown;
53 statusCode?: number;
54 headers?: Headers;
55}
56
57/**
58 * Result from a loader or action - potentially successful or unsuccessful
59 */
60export type DataResult =
61 | SuccessResult
62 | DeferredResult
63 | RedirectResult
64 | ErrorResult;
65
66type LowerCaseFormMethod = "get" | "post" | "put" | "patch" | "delete";
67type UpperCaseFormMethod = Uppercase<LowerCaseFormMethod>;
68
69/**
70 * Users can specify either lowercase or uppercase form methods on `<Form>`,
71 * useSubmit(), `<fetcher.Form>`, etc.
72 */
73export type HTMLFormMethod = LowerCaseFormMethod | UpperCaseFormMethod;
74
75/**
76 * Active navigation/fetcher form methods are exposed in lowercase on the
77 * RouterState
78 */
79export type FormMethod = LowerCaseFormMethod;
80export type MutationFormMethod = Exclude<FormMethod, "get">;
81
82/**
83 * In v7, active navigation/fetcher form methods are exposed in uppercase on the
84 * RouterState. This is to align with the normalization done via fetch().
85 */
86export type V7_FormMethod = UpperCaseFormMethod;
87export type V7_MutationFormMethod = Exclude<V7_FormMethod, "GET">;
88
89export type FormEncType =
90 | "application/x-www-form-urlencoded"
91 | "multipart/form-data"
92 | "application/json"
93 | "text/plain";
94
95// Thanks https://github.com/sindresorhus/type-fest!
96type JsonObject = { [Key in string]: JsonValue } & {
97 [Key in string]?: JsonValue | undefined;
98};
99type JsonArray = JsonValue[] | readonly JsonValue[];
100type JsonPrimitive = string | number | boolean | null;
101type JsonValue = JsonPrimitive | JsonObject | JsonArray;
102
103/**
104 * @private
105 * Internal interface to pass around for action submissions, not intended for
106 * external consumption
107 */
108export type Submission =
109 | {
110 formMethod: FormMethod | V7_FormMethod;
111 formAction: string;
112 formEncType: FormEncType;
113 formData: FormData;
114 json: undefined;
115 text: undefined;
116 }
117 | {
118 formMethod: FormMethod | V7_FormMethod;
119 formAction: string;
120 formEncType: FormEncType;
121 formData: undefined;
122 json: JsonValue;
123 text: undefined;
124 }
125 | {
126 formMethod: FormMethod | V7_FormMethod;
127 formAction: string;
128 formEncType: FormEncType;
129 formData: undefined;
130 json: undefined;
131 text: string;
132 };
133
134/**
135 * @private
136 * Arguments passed to route loader/action functions. Same for now but we keep
137 * this as a private implementation detail in case they diverge in the future.
138 */
139interface DataFunctionArgs<Context> {
140 request: Request;
141 params: Params;
142 context?: Context;
143}
144
145// TODO: (v7) Change the defaults from any to unknown in and remove Remix wrappers:
146// ActionFunction, ActionFunctionArgs, LoaderFunction, LoaderFunctionArgs
147// Also, make them a type alias instead of an interface
148
149/**
150 * Arguments passed to loader functions
151 */
152export interface LoaderFunctionArgs<Context = any>
153 extends DataFunctionArgs<Context> {}
154
155/**
156 * Arguments passed to action functions
157 */
158export interface ActionFunctionArgs<Context = any>
159 extends DataFunctionArgs<Context> {}
160
161/**
162 * Loaders and actions can return anything except `undefined` (`null` is a
163 * valid return value if there is no data to return). Responses are preferred
164 * and will ease any future migration to Remix
165 */
166type DataFunctionValue = Response | NonNullable<unknown> | null;
167
168type DataFunctionReturnValue = Promise<DataFunctionValue> | DataFunctionValue;
169
170/**
171 * Route loader function signature
172 */
173export type LoaderFunction<Context = any> = {
174 (
175 args: LoaderFunctionArgs<Context>,
176 handlerCtx?: unknown
177 ): DataFunctionReturnValue;
178} & { hydrate?: boolean };
179
180/**
181 * Route action function signature
182 */
183export interface ActionFunction<Context = any> {
184 (
185 args: ActionFunctionArgs<Context>,
186 handlerCtx?: unknown
187 ): DataFunctionReturnValue;
188}
189
190/**
191 * Arguments passed to shouldRevalidate function
192 */
193export interface ShouldRevalidateFunctionArgs {
194 currentUrl: URL;
195 currentParams: AgnosticDataRouteMatch["params"];
196 nextUrl: URL;
197 nextParams: AgnosticDataRouteMatch["params"];
198 formMethod?: Submission["formMethod"];
199 formAction?: Submission["formAction"];
200 formEncType?: Submission["formEncType"];
201 text?: Submission["text"];
202 formData?: Submission["formData"];
203 json?: Submission["json"];
204 actionStatus?: number;
205 actionResult?: any;
206 defaultShouldRevalidate: boolean;
207}
208
209/**
210 * Route shouldRevalidate function signature. This runs after any submission
211 * (navigation or fetcher), so we flatten the navigation/fetcher submission
212 * onto the arguments. It shouldn't matter whether it came from a navigation
213 * or a fetcher, what really matters is the URLs and the formData since loaders
214 * have to re-run based on the data models that were potentially mutated.
215 */
216export interface ShouldRevalidateFunction {
217 (args: ShouldRevalidateFunctionArgs): boolean;
218}
219
220/**
221 * Function provided by the framework-aware layers to set `hasErrorBoundary`
222 * from the framework-aware `errorElement` prop
223 *
224 * @deprecated Use `mapRouteProperties` instead
225 */
226export interface DetectErrorBoundaryFunction {
227 (route: AgnosticRouteObject): boolean;
228}
229
230export interface DataStrategyMatch
231 extends AgnosticRouteMatch<string, AgnosticDataRouteObject> {
232 shouldLoad: boolean;
233 resolve: (
234 handlerOverride?: (
235 handler: (ctx?: unknown) => DataFunctionReturnValue
[0c6b92a]236 ) => DataFunctionReturnValue
237 ) => Promise<DataStrategyResult>;
[d565449]238}
239
240export interface DataStrategyFunctionArgs<Context = any>
241 extends DataFunctionArgs<Context> {
242 matches: DataStrategyMatch[];
[0c6b92a]243 fetcherKey: string | null;
244}
245
246/**
247 * Result from a loader or action called via dataStrategy
248 */
249export interface DataStrategyResult {
250 type: "data" | "error";
251 result: unknown; // data, Error, Response, DeferredData, DataWithResponseInit
[d565449]252}
253
254export interface DataStrategyFunction {
[0c6b92a]255 (args: DataStrategyFunctionArgs): Promise<Record<string, DataStrategyResult>>;
[d565449]256}
257
[0c6b92a]258export type AgnosticPatchRoutesOnNavigationFunctionArgs<
259 O extends AgnosticRouteObject = AgnosticRouteObject,
[d565449]260 M extends AgnosticRouteMatch = AgnosticRouteMatch
[0c6b92a]261> = {
262 path: string;
263 matches: M[];
264 patch: (routeId: string | null, children: O[]) => void;
265};
266
267export type AgnosticPatchRoutesOnNavigationFunction<
268 O extends AgnosticRouteObject = AgnosticRouteObject,
269 M extends AgnosticRouteMatch = AgnosticRouteMatch
270> = (
271 opts: AgnosticPatchRoutesOnNavigationFunctionArgs<O, M>
272) => void | Promise<void>;
[d565449]273
274/**
275 * Function provided by the framework-aware layers to set any framework-specific
276 * properties from framework-agnostic properties
277 */
278export interface MapRoutePropertiesFunction {
279 (route: AgnosticRouteObject): {
280 hasErrorBoundary: boolean;
281 } & Record<string, any>;
282}
283
284/**
285 * Keys we cannot change from within a lazy() function. We spread all other keys
286 * onto the route. Either they're meaningful to the router, or they'll get
287 * ignored.
288 */
289export type ImmutableRouteKey =
290 | "lazy"
291 | "caseSensitive"
292 | "path"
293 | "id"
294 | "index"
295 | "children";
296
297export const immutableRouteKeys = new Set<ImmutableRouteKey>([
298 "lazy",
299 "caseSensitive",
300 "path",
301 "id",
302 "index",
303 "children",
304]);
305
306type RequireOne<T, Key = keyof T> = Exclude<
307 {
308 [K in keyof T]: K extends Key ? Omit<T, K> & Required<Pick<T, K>> : never;
309 }[keyof T],
310 undefined
311>;
312
313/**
314 * lazy() function to load a route definition, which can add non-matching
315 * related properties to a route
316 */
317export interface LazyRouteFunction<R extends AgnosticRouteObject> {
318 (): Promise<RequireOne<Omit<R, ImmutableRouteKey>>>;
319}
320
321/**
322 * Base RouteObject with common props shared by all types of routes
323 */
324type AgnosticBaseRouteObject = {
325 caseSensitive?: boolean;
326 path?: string;
327 id?: string;
328 loader?: LoaderFunction | boolean;
329 action?: ActionFunction | boolean;
330 hasErrorBoundary?: boolean;
331 shouldRevalidate?: ShouldRevalidateFunction;
332 handle?: any;
333 lazy?: LazyRouteFunction<AgnosticBaseRouteObject>;
334};
335
336/**
337 * Index routes must not have children
338 */
339export type AgnosticIndexRouteObject = AgnosticBaseRouteObject & {
340 children?: undefined;
341 index: true;
342};
343
344/**
345 * Non-index routes may have children, but cannot have index
346 */
347export type AgnosticNonIndexRouteObject = AgnosticBaseRouteObject & {
348 children?: AgnosticRouteObject[];
349 index?: false;
350};
351
352/**
353 * A route object represents a logical route, with (optionally) its child
354 * routes organized in a tree-like structure.
355 */
356export type AgnosticRouteObject =
357 | AgnosticIndexRouteObject
358 | AgnosticNonIndexRouteObject;
359
360export type AgnosticDataIndexRouteObject = AgnosticIndexRouteObject & {
361 id: string;
362};
363
364export type AgnosticDataNonIndexRouteObject = AgnosticNonIndexRouteObject & {
365 children?: AgnosticDataRouteObject[];
366 id: string;
367};
368
369/**
370 * A data route object, which is just a RouteObject with a required unique ID
371 */
372export type AgnosticDataRouteObject =
373 | AgnosticDataIndexRouteObject
374 | AgnosticDataNonIndexRouteObject;
375
376export type RouteManifest = Record<string, AgnosticDataRouteObject | undefined>;
377
378// Recursive helper for finding path parameters in the absence of wildcards
379type _PathParam<Path extends string> =
380 // split path into individual path segments
381 Path extends `${infer L}/${infer R}`
382 ? _PathParam<L> | _PathParam<R>
383 : // find params after `:`
384 Path extends `:${infer Param}`
385 ? Param extends `${infer Optional}?`
386 ? Optional
387 : Param
388 : // otherwise, there aren't any params present
389 never;
390
391/**
392 * Examples:
393 * "/a/b/*" -> "*"
394 * ":a" -> "a"
395 * "/a/:b" -> "b"
396 * "/a/blahblahblah:b" -> "b"
397 * "/:a/:b" -> "a" | "b"
398 * "/:a/b/:c/*" -> "a" | "c" | "*"
399 */
400export type PathParam<Path extends string> =
401 // check if path is just a wildcard
402 Path extends "*" | "/*"
403 ? "*"
404 : // look for wildcard at the end of the path
405 Path extends `${infer Rest}/*`
406 ? "*" | _PathParam<Rest>
407 : // look for params in the absence of wildcards
408 _PathParam<Path>;
409
410// Attempt to parse the given string segment. If it fails, then just return the
411// plain string type as a default fallback. Otherwise, return the union of the
412// parsed string literals that were referenced as dynamic segments in the route.
413export type ParamParseKey<Segment extends string> =
414 // if you could not find path params, fallback to `string`
415 [PathParam<Segment>] extends [never] ? string : PathParam<Segment>;
416
417/**
418 * The parameters that were parsed from the URL path.
419 */
420export type Params<Key extends string = string> = {
421 readonly [key in Key]: string | undefined;
422};
423
424/**
425 * A RouteMatch contains info about how a route matched a URL.
426 */
427export interface AgnosticRouteMatch<
428 ParamKey extends string = string,
429 RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject
430> {
431 /**
432 * The names and values of dynamic parameters in the URL.
433 */
434 params: Params<ParamKey>;
435 /**
436 * The portion of the URL pathname that was matched.
437 */
438 pathname: string;
439 /**
440 * The portion of the URL pathname that was matched before child routes.
441 */
442 pathnameBase: string;
443 /**
444 * The route object that was used to match.
445 */
446 route: RouteObjectType;
447}
448
449export interface AgnosticDataRouteMatch
450 extends AgnosticRouteMatch<string, AgnosticDataRouteObject> {}
451
452function isIndexRoute(
453 route: AgnosticRouteObject
454): route is AgnosticIndexRouteObject {
455 return route.index === true;
456}
457
458// Walk the route tree generating unique IDs where necessary, so we are working
459// solely with AgnosticDataRouteObject's within the Router
460export function convertRoutesToDataRoutes(
461 routes: AgnosticRouteObject[],
462 mapRouteProperties: MapRoutePropertiesFunction,
463 parentPath: string[] = [],
464 manifest: RouteManifest = {}
465): AgnosticDataRouteObject[] {
466 return routes.map((route, index) => {
467 let treePath = [...parentPath, String(index)];
468 let id = typeof route.id === "string" ? route.id : treePath.join("-");
469 invariant(
470 route.index !== true || !route.children,
471 `Cannot specify children on an index route`
472 );
473 invariant(
474 !manifest[id],
475 `Found a route id collision on id "${id}". Route ` +
476 "id's must be globally unique within Data Router usages"
477 );
478
479 if (isIndexRoute(route)) {
480 let indexRoute: AgnosticDataIndexRouteObject = {
481 ...route,
482 ...mapRouteProperties(route),
483 id,
484 };
485 manifest[id] = indexRoute;
486 return indexRoute;
487 } else {
488 let pathOrLayoutRoute: AgnosticDataNonIndexRouteObject = {
489 ...route,
490 ...mapRouteProperties(route),
491 id,
492 children: undefined,
493 };
494 manifest[id] = pathOrLayoutRoute;
495
496 if (route.children) {
497 pathOrLayoutRoute.children = convertRoutesToDataRoutes(
498 route.children,
499 mapRouteProperties,
500 treePath,
501 manifest
502 );
503 }
504
505 return pathOrLayoutRoute;
506 }
507 });
508}
509
510/**
511 * Matches the given routes to a location and returns the match data.
512 *
[0c6b92a]513 * @see https://reactrouter.com/v6/utils/match-routes
[d565449]514 */
515export function matchRoutes<
516 RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject
517>(
518 routes: RouteObjectType[],
519 locationArg: Partial<Location> | string,
520 basename = "/"
521): AgnosticRouteMatch<string, RouteObjectType>[] | null {
522 return matchRoutesImpl(routes, locationArg, basename, false);
523}
524
525export function matchRoutesImpl<
526 RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject
527>(
528 routes: RouteObjectType[],
529 locationArg: Partial<Location> | string,
530 basename: string,
531 allowPartial: boolean
532): AgnosticRouteMatch<string, RouteObjectType>[] | null {
533 let location =
534 typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
535
536 let pathname = stripBasename(location.pathname || "/", basename);
537
538 if (pathname == null) {
539 return null;
540 }
541
542 let branches = flattenRoutes(routes);
543 rankRouteBranches(branches);
544
545 let matches = null;
546 for (let i = 0; matches == null && i < branches.length; ++i) {
547 // Incoming pathnames are generally encoded from either window.location
548 // or from router.navigate, but we want to match against the unencoded
549 // paths in the route definitions. Memory router locations won't be
550 // encoded here but there also shouldn't be anything to decode so this
551 // should be a safe operation. This avoids needing matchRoutes to be
552 // history-aware.
553 let decoded = decodePath(pathname);
554 matches = matchRouteBranch<string, RouteObjectType>(
555 branches[i],
556 decoded,
557 allowPartial
558 );
559 }
560
561 return matches;
562}
563
564export interface UIMatch<Data = unknown, Handle = unknown> {
565 id: string;
566 pathname: string;
567 params: AgnosticRouteMatch["params"];
568 data: Data;
569 handle: Handle;
570}
571
572export function convertRouteMatchToUiMatch(
573 match: AgnosticDataRouteMatch,
574 loaderData: RouteData
575): UIMatch {
576 let { route, pathname, params } = match;
577 return {
578 id: route.id,
579 pathname,
580 params,
581 data: loaderData[route.id],
582 handle: route.handle,
583 };
584}
585
586interface RouteMeta<
587 RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject
588> {
589 relativePath: string;
590 caseSensitive: boolean;
591 childrenIndex: number;
592 route: RouteObjectType;
593}
594
595interface RouteBranch<
596 RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject
597> {
598 path: string;
599 score: number;
600 routesMeta: RouteMeta<RouteObjectType>[];
601}
602
603function flattenRoutes<
604 RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject
605>(
606 routes: RouteObjectType[],
607 branches: RouteBranch<RouteObjectType>[] = [],
608 parentsMeta: RouteMeta<RouteObjectType>[] = [],
609 parentPath = ""
610): RouteBranch<RouteObjectType>[] {
611 let flattenRoute = (
612 route: RouteObjectType,
613 index: number,
614 relativePath?: string
615 ) => {
616 let meta: RouteMeta<RouteObjectType> = {
617 relativePath:
618 relativePath === undefined ? route.path || "" : relativePath,
619 caseSensitive: route.caseSensitive === true,
620 childrenIndex: index,
621 route,
622 };
623
624 if (meta.relativePath.startsWith("/")) {
625 invariant(
626 meta.relativePath.startsWith(parentPath),
627 `Absolute route path "${meta.relativePath}" nested under path ` +
628 `"${parentPath}" is not valid. An absolute child route path ` +
629 `must start with the combined path of all its parent routes.`
630 );
631
632 meta.relativePath = meta.relativePath.slice(parentPath.length);
633 }
634
635 let path = joinPaths([parentPath, meta.relativePath]);
636 let routesMeta = parentsMeta.concat(meta);
637
638 // Add the children before adding this route to the array, so we traverse the
639 // route tree depth-first and child routes appear before their parents in
640 // the "flattened" version.
641 if (route.children && route.children.length > 0) {
642 invariant(
643 // Our types know better, but runtime JS may not!
644 // @ts-expect-error
645 route.index !== true,
646 `Index routes must not have child routes. Please remove ` +
647 `all child routes from route path "${path}".`
648 );
649 flattenRoutes(route.children, branches, routesMeta, path);
650 }
651
652 // Routes without a path shouldn't ever match by themselves unless they are
653 // index routes, so don't add them to the list of possible branches.
654 if (route.path == null && !route.index) {
655 return;
656 }
657
658 branches.push({
659 path,
660 score: computeScore(path, route.index),
661 routesMeta,
662 });
663 };
664 routes.forEach((route, index) => {
665 // coarse-grain check for optional params
666 if (route.path === "" || !route.path?.includes("?")) {
667 flattenRoute(route, index);
668 } else {
669 for (let exploded of explodeOptionalSegments(route.path)) {
670 flattenRoute(route, index, exploded);
671 }
672 }
673 });
674
675 return branches;
676}
677
678/**
679 * Computes all combinations of optional path segments for a given path,
680 * excluding combinations that are ambiguous and of lower priority.
681 *
682 * For example, `/one/:two?/three/:four?/:five?` explodes to:
683 * - `/one/three`
684 * - `/one/:two/three`
685 * - `/one/three/:four`
686 * - `/one/three/:five`
687 * - `/one/:two/three/:four`
688 * - `/one/:two/three/:five`
689 * - `/one/three/:four/:five`
690 * - `/one/:two/three/:four/:five`
691 */
692function explodeOptionalSegments(path: string): string[] {
693 let segments = path.split("/");
694 if (segments.length === 0) return [];
695
696 let [first, ...rest] = segments;
697
698 // Optional path segments are denoted by a trailing `?`
699 let isOptional = first.endsWith("?");
700 // Compute the corresponding required segment: `foo?` -> `foo`
701 let required = first.replace(/\?$/, "");
702
703 if (rest.length === 0) {
704 // Intepret empty string as omitting an optional segment
705 // `["one", "", "three"]` corresponds to omitting `:two` from `/one/:two?/three` -> `/one/three`
706 return isOptional ? [required, ""] : [required];
707 }
708
709 let restExploded = explodeOptionalSegments(rest.join("/"));
710
711 let result: string[] = [];
712
713 // All child paths with the prefix. Do this for all children before the
714 // optional version for all children, so we get consistent ordering where the
715 // parent optional aspect is preferred as required. Otherwise, we can get
716 // child sections interspersed where deeper optional segments are higher than
717 // parent optional segments, where for example, /:two would explode _earlier_
718 // then /:one. By always including the parent as required _for all children_
719 // first, we avoid this issue
720 result.push(
721 ...restExploded.map((subpath) =>
722 subpath === "" ? required : [required, subpath].join("/")
723 )
724 );
725
726 // Then, if this is an optional value, add all child versions without
727 if (isOptional) {
728 result.push(...restExploded);
729 }
730
731 // for absolute paths, ensure `/` instead of empty segment
732 return result.map((exploded) =>
733 path.startsWith("/") && exploded === "" ? "/" : exploded
734 );
735}
736
737function rankRouteBranches(branches: RouteBranch[]): void {
738 branches.sort((a, b) =>
739 a.score !== b.score
740 ? b.score - a.score // Higher score first
741 : compareIndexes(
742 a.routesMeta.map((meta) => meta.childrenIndex),
743 b.routesMeta.map((meta) => meta.childrenIndex)
744 )
745 );
746}
747
748const paramRe = /^:[\w-]+$/;
749const dynamicSegmentValue = 3;
750const indexRouteValue = 2;
751const emptySegmentValue = 1;
752const staticSegmentValue = 10;
753const splatPenalty = -2;
754const isSplat = (s: string) => s === "*";
755
756function computeScore(path: string, index: boolean | undefined): number {
757 let segments = path.split("/");
758 let initialScore = segments.length;
759 if (segments.some(isSplat)) {
760 initialScore += splatPenalty;
761 }
762
763 if (index) {
764 initialScore += indexRouteValue;
765 }
766
767 return segments
768 .filter((s) => !isSplat(s))
769 .reduce(
770 (score, segment) =>
771 score +
772 (paramRe.test(segment)
773 ? dynamicSegmentValue
774 : segment === ""
775 ? emptySegmentValue
776 : staticSegmentValue),
777 initialScore
778 );
779}
780
781function compareIndexes(a: number[], b: number[]): number {
782 let siblings =
783 a.length === b.length && a.slice(0, -1).every((n, i) => n === b[i]);
784
785 return siblings
786 ? // If two routes are siblings, we should try to match the earlier sibling
787 // first. This allows people to have fine-grained control over the matching
788 // behavior by simply putting routes with identical paths in the order they
789 // want them tried.
790 a[a.length - 1] - b[b.length - 1]
791 : // Otherwise, it doesn't really make sense to rank non-siblings by index,
792 // so they sort equally.
793 0;
794}
795
796function matchRouteBranch<
797 ParamKey extends string = string,
798 RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject
799>(
800 branch: RouteBranch<RouteObjectType>,
801 pathname: string,
802 allowPartial = false
803): AgnosticRouteMatch<ParamKey, RouteObjectType>[] | null {
804 let { routesMeta } = branch;
805
806 let matchedParams = {};
807 let matchedPathname = "/";
808 let matches: AgnosticRouteMatch<ParamKey, RouteObjectType>[] = [];
809 for (let i = 0; i < routesMeta.length; ++i) {
810 let meta = routesMeta[i];
811 let end = i === routesMeta.length - 1;
812 let remainingPathname =
813 matchedPathname === "/"
814 ? pathname
815 : pathname.slice(matchedPathname.length) || "/";
816 let match = matchPath(
817 { path: meta.relativePath, caseSensitive: meta.caseSensitive, end },
818 remainingPathname
819 );
820
821 let route = meta.route;
822
823 if (
824 !match &&
825 end &&
826 allowPartial &&
827 !routesMeta[routesMeta.length - 1].route.index
828 ) {
829 match = matchPath(
830 {
831 path: meta.relativePath,
832 caseSensitive: meta.caseSensitive,
833 end: false,
834 },
835 remainingPathname
836 );
837 }
838
839 if (!match) {
840 return null;
841 }
842
843 Object.assign(matchedParams, match.params);
844
845 matches.push({
846 // TODO: Can this as be avoided?
847 params: matchedParams as Params<ParamKey>,
848 pathname: joinPaths([matchedPathname, match.pathname]),
849 pathnameBase: normalizePathname(
850 joinPaths([matchedPathname, match.pathnameBase])
851 ),
852 route,
853 });
854
855 if (match.pathnameBase !== "/") {
856 matchedPathname = joinPaths([matchedPathname, match.pathnameBase]);
857 }
858 }
859
860 return matches;
861}
862
863/**
864 * Returns a path with params interpolated.
865 *
[0c6b92a]866 * @see https://reactrouter.com/v6/utils/generate-path
[d565449]867 */
868export function generatePath<Path extends string>(
869 originalPath: Path,
870 params: {
871 [key in PathParam<Path>]: string | null;
872 } = {} as any
873): string {
874 let path: string = originalPath;
875 if (path.endsWith("*") && path !== "*" && !path.endsWith("/*")) {
876 warning(
877 false,
878 `Route path "${path}" will be treated as if it were ` +
879 `"${path.replace(/\*$/, "/*")}" because the \`*\` character must ` +
880 `always follow a \`/\` in the pattern. To get rid of this warning, ` +
881 `please change the route path to "${path.replace(/\*$/, "/*")}".`
882 );
883 path = path.replace(/\*$/, "/*") as Path;
884 }
885
886 // ensure `/` is added at the beginning if the path is absolute
887 const prefix = path.startsWith("/") ? "/" : "";
888
889 const stringify = (p: any) =>
890 p == null ? "" : typeof p === "string" ? p : String(p);
891
892 const segments = path
893 .split(/\/+/)
894 .map((segment, index, array) => {
895 const isLastSegment = index === array.length - 1;
896
897 // only apply the splat if it's the last segment
898 if (isLastSegment && segment === "*") {
899 const star = "*" as PathParam<Path>;
900 // Apply the splat
901 return stringify(params[star]);
902 }
903
904 const keyMatch = segment.match(/^:([\w-]+)(\??)$/);
905 if (keyMatch) {
906 const [, key, optional] = keyMatch;
907 let param = params[key as PathParam<Path>];
908 invariant(optional === "?" || param != null, `Missing ":${key}" param`);
909 return stringify(param);
910 }
911
912 // Remove any optional markers from optional static segments
913 return segment.replace(/\?$/g, "");
914 })
915 // Remove empty segments
916 .filter((segment) => !!segment);
917
918 return prefix + segments.join("/");
919}
920
921/**
922 * A PathPattern is used to match on some portion of a URL pathname.
923 */
924export interface PathPattern<Path extends string = string> {
925 /**
926 * A string to match against a URL pathname. May contain `:id`-style segments
927 * to indicate placeholders for dynamic parameters. May also end with `/*` to
928 * indicate matching the rest of the URL pathname.
929 */
930 path: Path;
931 /**
932 * Should be `true` if the static portions of the `path` should be matched in
933 * the same case.
934 */
935 caseSensitive?: boolean;
936 /**
937 * Should be `true` if this pattern should match the entire URL pathname.
938 */
939 end?: boolean;
940}
941
942/**
943 * A PathMatch contains info about how a PathPattern matched on a URL pathname.
944 */
945export interface PathMatch<ParamKey extends string = string> {
946 /**
947 * The names and values of dynamic parameters in the URL.
948 */
949 params: Params<ParamKey>;
950 /**
951 * The portion of the URL pathname that was matched.
952 */
953 pathname: string;
954 /**
955 * The portion of the URL pathname that was matched before child routes.
956 */
957 pathnameBase: string;
958 /**
959 * The pattern that was used to match.
960 */
961 pattern: PathPattern;
962}
963
964type Mutable<T> = {
965 -readonly [P in keyof T]: T[P];
966};
967
968/**
969 * Performs pattern matching on a URL pathname and returns information about
970 * the match.
971 *
[0c6b92a]972 * @see https://reactrouter.com/v6/utils/match-path
[d565449]973 */
974export function matchPath<
975 ParamKey extends ParamParseKey<Path>,
976 Path extends string
977>(
978 pattern: PathPattern<Path> | Path,
979 pathname: string
980): PathMatch<ParamKey> | null {
981 if (typeof pattern === "string") {
982 pattern = { path: pattern, caseSensitive: false, end: true };
983 }
984
985 let [matcher, compiledParams] = compilePath(
986 pattern.path,
987 pattern.caseSensitive,
988 pattern.end
989 );
990
991 let match = pathname.match(matcher);
992 if (!match) return null;
993
994 let matchedPathname = match[0];
995 let pathnameBase = matchedPathname.replace(/(.)\/+$/, "$1");
996 let captureGroups = match.slice(1);
997 let params: Params = compiledParams.reduce<Mutable<Params>>(
998 (memo, { paramName, isOptional }, index) => {
999 // We need to compute the pathnameBase here using the raw splat value
1000 // instead of using params["*"] later because it will be decoded then
1001 if (paramName === "*") {
1002 let splatValue = captureGroups[index] || "";
1003 pathnameBase = matchedPathname
1004 .slice(0, matchedPathname.length - splatValue.length)
1005 .replace(/(.)\/+$/, "$1");
1006 }
1007
1008 const value = captureGroups[index];
1009 if (isOptional && !value) {
1010 memo[paramName] = undefined;
1011 } else {
1012 memo[paramName] = (value || "").replace(/%2F/g, "/");
1013 }
1014 return memo;
1015 },
1016 {}
1017 );
1018
1019 return {
1020 params,
1021 pathname: matchedPathname,
1022 pathnameBase,
1023 pattern,
1024 };
1025}
1026
1027type CompiledPathParam = { paramName: string; isOptional?: boolean };
1028
1029function compilePath(
1030 path: string,
1031 caseSensitive = false,
1032 end = true
1033): [RegExp, CompiledPathParam[]] {
1034 warning(
1035 path === "*" || !path.endsWith("*") || path.endsWith("/*"),
1036 `Route path "${path}" will be treated as if it were ` +
1037 `"${path.replace(/\*$/, "/*")}" because the \`*\` character must ` +
1038 `always follow a \`/\` in the pattern. To get rid of this warning, ` +
1039 `please change the route path to "${path.replace(/\*$/, "/*")}".`
1040 );
1041
1042 let params: CompiledPathParam[] = [];
1043 let regexpSource =
1044 "^" +
1045 path
1046 .replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
1047 .replace(/^\/*/, "/") // Make sure it has a leading /
1048 .replace(/[\\.*+^${}|()[\]]/g, "\\$&") // Escape special regex chars
1049 .replace(
1050 /\/:([\w-]+)(\?)?/g,
1051 (_: string, paramName: string, isOptional) => {
1052 params.push({ paramName, isOptional: isOptional != null });
1053 return isOptional ? "/?([^\\/]+)?" : "/([^\\/]+)";
1054 }
1055 );
1056
1057 if (path.endsWith("*")) {
1058 params.push({ paramName: "*" });
1059 regexpSource +=
1060 path === "*" || path === "/*"
1061 ? "(.*)$" // Already matched the initial /, just match the rest
1062 : "(?:\\/(.+)|\\/*)$"; // Don't include the / in params["*"]
1063 } else if (end) {
1064 // When matching to the end, ignore trailing slashes
1065 regexpSource += "\\/*$";
1066 } else if (path !== "" && path !== "/") {
1067 // If our path is non-empty and contains anything beyond an initial slash,
1068 // then we have _some_ form of path in our regex, so we should expect to
1069 // match only if we find the end of this path segment. Look for an optional
1070 // non-captured trailing slash (to match a portion of the URL) or the end
1071 // of the path (if we've matched to the end). We used to do this with a
1072 // word boundary but that gives false positives on routes like
1073 // /user-preferences since `-` counts as a word boundary.
1074 regexpSource += "(?:(?=\\/|$))";
1075 } else {
1076 // Nothing to match for "" or "/"
1077 }
1078
1079 let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");
1080
1081 return [matcher, params];
1082}
1083
1084export function decodePath(value: string) {
1085 try {
1086 return value
1087 .split("/")
1088 .map((v) => decodeURIComponent(v).replace(/\//g, "%2F"))
1089 .join("/");
1090 } catch (error) {
1091 warning(
1092 false,
1093 `The URL path "${value}" could not be decoded because it is is a ` +
1094 `malformed URL segment. This is probably due to a bad percent ` +
1095 `encoding (${error}).`
1096 );
1097
1098 return value;
1099 }
1100}
1101
1102/**
1103 * @private
1104 */
1105export function stripBasename(
1106 pathname: string,
1107 basename: string
1108): string | null {
1109 if (basename === "/") return pathname;
1110
1111 if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) {
1112 return null;
1113 }
1114
1115 // We want to leave trailing slash behavior in the user's control, so if they
1116 // specify a basename with a trailing slash, we should support it
1117 let startIndex = basename.endsWith("/")
1118 ? basename.length - 1
1119 : basename.length;
1120 let nextChar = pathname.charAt(startIndex);
1121 if (nextChar && nextChar !== "/") {
1122 // pathname does not start with basename/
1123 return null;
1124 }
1125
1126 return pathname.slice(startIndex) || "/";
1127}
1128
1129/**
1130 * Returns a resolved path object relative to the given pathname.
1131 *
[0c6b92a]1132 * @see https://reactrouter.com/v6/utils/resolve-path
[d565449]1133 */
1134export function resolvePath(to: To, fromPathname = "/"): Path {
1135 let {
1136 pathname: toPathname,
1137 search = "",
1138 hash = "",
1139 } = typeof to === "string" ? parsePath(to) : to;
1140
1141 let pathname = toPathname
1142 ? toPathname.startsWith("/")
1143 ? toPathname
1144 : resolvePathname(toPathname, fromPathname)
1145 : fromPathname;
1146
1147 return {
1148 pathname,
1149 search: normalizeSearch(search),
1150 hash: normalizeHash(hash),
1151 };
1152}
1153
1154function resolvePathname(relativePath: string, fromPathname: string): string {
1155 let segments = fromPathname.replace(/\/+$/, "").split("/");
1156 let relativeSegments = relativePath.split("/");
1157
1158 relativeSegments.forEach((segment) => {
1159 if (segment === "..") {
1160 // Keep the root "" segment so the pathname starts at /
1161 if (segments.length > 1) segments.pop();
1162 } else if (segment !== ".") {
1163 segments.push(segment);
1164 }
1165 });
1166
1167 return segments.length > 1 ? segments.join("/") : "/";
1168}
1169
1170function getInvalidPathError(
1171 char: string,
1172 field: string,
1173 dest: string,
1174 path: Partial<Path>
1175) {
1176 return (
1177 `Cannot include a '${char}' character in a manually specified ` +
1178 `\`to.${field}\` field [${JSON.stringify(
1179 path
1180 )}]. Please separate it out to the ` +
1181 `\`to.${dest}\` field. Alternatively you may provide the full path as ` +
1182 `a string in <Link to="..."> and the router will parse it for you.`
1183 );
1184}
1185
1186/**
1187 * @private
1188 *
1189 * When processing relative navigation we want to ignore ancestor routes that
1190 * do not contribute to the path, such that index/pathless layout routes don't
1191 * interfere.
1192 *
1193 * For example, when moving a route element into an index route and/or a
1194 * pathless layout route, relative link behavior contained within should stay
1195 * the same. Both of the following examples should link back to the root:
1196 *
1197 * <Route path="/">
1198 * <Route path="accounts" element={<Link to=".."}>
1199 * </Route>
1200 *
1201 * <Route path="/">
1202 * <Route path="accounts">
1203 * <Route element={<AccountsLayout />}> // <-- Does not contribute
1204 * <Route index element={<Link to=".."} /> // <-- Does not contribute
1205 * </Route
1206 * </Route>
1207 * </Route>
1208 */
1209export function getPathContributingMatches<
1210 T extends AgnosticRouteMatch = AgnosticRouteMatch
1211>(matches: T[]) {
1212 return matches.filter(
1213 (match, index) =>
1214 index === 0 || (match.route.path && match.route.path.length > 0)
1215 );
1216}
1217
1218// Return the array of pathnames for the current route matches - used to
1219// generate the routePathnames input for resolveTo()
1220export function getResolveToMatches<
1221 T extends AgnosticRouteMatch = AgnosticRouteMatch
1222>(matches: T[], v7_relativeSplatPath: boolean) {
1223 let pathMatches = getPathContributingMatches(matches);
1224
1225 // When v7_relativeSplatPath is enabled, use the full pathname for the leaf
1226 // match so we include splat values for "." links. See:
1227 // https://github.com/remix-run/react-router/issues/11052#issuecomment-1836589329
1228 if (v7_relativeSplatPath) {
1229 return pathMatches.map((match, idx) =>
1230 idx === pathMatches.length - 1 ? match.pathname : match.pathnameBase
1231 );
1232 }
1233
1234 return pathMatches.map((match) => match.pathnameBase);
1235}
1236
1237/**
1238 * @private
1239 */
1240export function resolveTo(
1241 toArg: To,
1242 routePathnames: string[],
1243 locationPathname: string,
1244 isPathRelative = false
1245): Path {
1246 let to: Partial<Path>;
1247 if (typeof toArg === "string") {
1248 to = parsePath(toArg);
1249 } else {
1250 to = { ...toArg };
1251
1252 invariant(
1253 !to.pathname || !to.pathname.includes("?"),
1254 getInvalidPathError("?", "pathname", "search", to)
1255 );
1256 invariant(
1257 !to.pathname || !to.pathname.includes("#"),
1258 getInvalidPathError("#", "pathname", "hash", to)
1259 );
1260 invariant(
1261 !to.search || !to.search.includes("#"),
1262 getInvalidPathError("#", "search", "hash", to)
1263 );
1264 }
1265
1266 let isEmptyPath = toArg === "" || to.pathname === "";
1267 let toPathname = isEmptyPath ? "/" : to.pathname;
1268
1269 let from: string;
1270
1271 // Routing is relative to the current pathname if explicitly requested.
1272 //
1273 // If a pathname is explicitly provided in `to`, it should be relative to the
1274 // route context. This is explained in `Note on `<Link to>` values` in our
1275 // migration guide from v5 as a means of disambiguation between `to` values
1276 // that begin with `/` and those that do not. However, this is problematic for
1277 // `to` values that do not provide a pathname. `to` can simply be a search or
1278 // hash string, in which case we should assume that the navigation is relative
1279 // to the current location's pathname and *not* the route pathname.
1280 if (toPathname == null) {
1281 from = locationPathname;
1282 } else {
1283 let routePathnameIndex = routePathnames.length - 1;
1284
1285 // With relative="route" (the default), each leading .. segment means
1286 // "go up one route" instead of "go up one URL segment". This is a key
1287 // difference from how <a href> works and a major reason we call this a
1288 // "to" value instead of a "href".
1289 if (!isPathRelative && toPathname.startsWith("..")) {
1290 let toSegments = toPathname.split("/");
1291
1292 while (toSegments[0] === "..") {
1293 toSegments.shift();
1294 routePathnameIndex -= 1;
1295 }
1296
1297 to.pathname = toSegments.join("/");
1298 }
1299
1300 from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex] : "/";
1301 }
1302
1303 let path = resolvePath(to, from);
1304
1305 // Ensure the pathname has a trailing slash if the original "to" had one
1306 let hasExplicitTrailingSlash =
1307 toPathname && toPathname !== "/" && toPathname.endsWith("/");
1308 // Or if this was a link to the current path which has a trailing slash
1309 let hasCurrentTrailingSlash =
1310 (isEmptyPath || toPathname === ".") && locationPathname.endsWith("/");
1311 if (
1312 !path.pathname.endsWith("/") &&
1313 (hasExplicitTrailingSlash || hasCurrentTrailingSlash)
1314 ) {
1315 path.pathname += "/";
1316 }
1317
1318 return path;
1319}
1320
1321/**
1322 * @private
1323 */
1324export function getToPathname(to: To): string | undefined {
1325 // Empty strings should be treated the same as / paths
1326 return to === "" || (to as Path).pathname === ""
1327 ? "/"
1328 : typeof to === "string"
1329 ? parsePath(to).pathname
1330 : to.pathname;
1331}
1332
1333/**
1334 * @private
1335 */
1336export const joinPaths = (paths: string[]): string =>
1337 paths.join("/").replace(/\/\/+/g, "/");
1338
1339/**
1340 * @private
1341 */
1342export const normalizePathname = (pathname: string): string =>
1343 pathname.replace(/\/+$/, "").replace(/^\/*/, "/");
1344
1345/**
1346 * @private
1347 */
1348export const normalizeSearch = (search: string): string =>
1349 !search || search === "?"
1350 ? ""
1351 : search.startsWith("?")
1352 ? search
1353 : "?" + search;
1354
1355/**
1356 * @private
1357 */
1358export const normalizeHash = (hash: string): string =>
1359 !hash || hash === "#" ? "" : hash.startsWith("#") ? hash : "#" + hash;
1360
1361export type JsonFunction = <Data>(
1362 data: Data,
1363 init?: number | ResponseInit
1364) => Response;
1365
1366/**
1367 * This is a shortcut for creating `application/json` responses. Converts `data`
1368 * to JSON and sets the `Content-Type` header.
[0c6b92a]1369 *
1370 * @deprecated The `json` method is deprecated in favor of returning raw objects.
1371 * This method will be removed in v7.
[d565449]1372 */
1373export const json: JsonFunction = (data, init = {}) => {
1374 let responseInit = typeof init === "number" ? { status: init } : init;
1375
1376 let headers = new Headers(responseInit.headers);
1377 if (!headers.has("Content-Type")) {
1378 headers.set("Content-Type", "application/json; charset=utf-8");
1379 }
1380
1381 return new Response(JSON.stringify(data), {
1382 ...responseInit,
1383 headers,
1384 });
1385};
1386
1387export class DataWithResponseInit<D> {
1388 type: string = "DataWithResponseInit";
1389 data: D;
1390 init: ResponseInit | null;
1391
1392 constructor(data: D, init?: ResponseInit) {
1393 this.data = data;
1394 this.init = init || null;
1395 }
1396}
1397
1398/**
1399 * Create "responses" that contain `status`/`headers` without forcing
1400 * serialization into an actual `Response` - used by Remix single fetch
1401 */
1402export function data<D>(data: D, init?: number | ResponseInit) {
1403 return new DataWithResponseInit(
1404 data,
1405 typeof init === "number" ? { status: init } : init
1406 );
1407}
1408
1409export interface TrackedPromise extends Promise<any> {
1410 _tracked?: boolean;
1411 _data?: any;
1412 _error?: any;
1413}
1414
1415export class AbortedDeferredError extends Error {}
1416
1417export class DeferredData {
1418 private pendingKeysSet: Set<string> = new Set<string>();
1419 private controller: AbortController;
1420 private abortPromise: Promise<void>;
1421 private unlistenAbortSignal: () => void;
1422 private subscribers: Set<(aborted: boolean, settledKey?: string) => void> =
1423 new Set();
1424 data: Record<string, unknown>;
1425 init?: ResponseInit;
1426 deferredKeys: string[] = [];
1427
1428 constructor(data: Record<string, unknown>, responseInit?: ResponseInit) {
1429 invariant(
1430 data && typeof data === "object" && !Array.isArray(data),
1431 "defer() only accepts plain objects"
1432 );
1433
1434 // Set up an AbortController + Promise we can race against to exit early
1435 // cancellation
1436 let reject: (e: AbortedDeferredError) => void;
1437 this.abortPromise = new Promise((_, r) => (reject = r));
1438 this.controller = new AbortController();
1439 let onAbort = () =>
1440 reject(new AbortedDeferredError("Deferred data aborted"));
1441 this.unlistenAbortSignal = () =>
1442 this.controller.signal.removeEventListener("abort", onAbort);
1443 this.controller.signal.addEventListener("abort", onAbort);
1444
1445 this.data = Object.entries(data).reduce(
1446 (acc, [key, value]) =>
1447 Object.assign(acc, {
1448 [key]: this.trackPromise(key, value),
1449 }),
1450 {}
1451 );
1452
1453 if (this.done) {
1454 // All incoming values were resolved
1455 this.unlistenAbortSignal();
1456 }
1457
1458 this.init = responseInit;
1459 }
1460
1461 private trackPromise(
1462 key: string,
1463 value: Promise<unknown> | unknown
1464 ): TrackedPromise | unknown {
1465 if (!(value instanceof Promise)) {
1466 return value;
1467 }
1468
1469 this.deferredKeys.push(key);
1470 this.pendingKeysSet.add(key);
1471
1472 // We store a little wrapper promise that will be extended with
1473 // _data/_error props upon resolve/reject
1474 let promise: TrackedPromise = Promise.race([value, this.abortPromise]).then(
1475 (data) => this.onSettle(promise, key, undefined, data as unknown),
1476 (error) => this.onSettle(promise, key, error as unknown)
1477 );
1478
1479 // Register rejection listeners to avoid uncaught promise rejections on
1480 // errors or aborted deferred values
1481 promise.catch(() => {});
1482
1483 Object.defineProperty(promise, "_tracked", { get: () => true });
1484 return promise;
1485 }
1486
1487 private onSettle(
1488 promise: TrackedPromise,
1489 key: string,
1490 error: unknown,
1491 data?: unknown
1492 ): unknown {
1493 if (
1494 this.controller.signal.aborted &&
1495 error instanceof AbortedDeferredError
1496 ) {
1497 this.unlistenAbortSignal();
1498 Object.defineProperty(promise, "_error", { get: () => error });
1499 return Promise.reject(error);
1500 }
1501
1502 this.pendingKeysSet.delete(key);
1503
1504 if (this.done) {
1505 // Nothing left to abort!
1506 this.unlistenAbortSignal();
1507 }
1508
1509 // If the promise was resolved/rejected with undefined, we'll throw an error as you
1510 // should always resolve with a value or null
1511 if (error === undefined && data === undefined) {
1512 let undefinedError = new Error(
1513 `Deferred data for key "${key}" resolved/rejected with \`undefined\`, ` +
1514 `you must resolve/reject with a value or \`null\`.`
1515 );
1516 Object.defineProperty(promise, "_error", { get: () => undefinedError });
1517 this.emit(false, key);
1518 return Promise.reject(undefinedError);
1519 }
1520
1521 if (data === undefined) {
1522 Object.defineProperty(promise, "_error", { get: () => error });
1523 this.emit(false, key);
1524 return Promise.reject(error);
1525 }
1526
1527 Object.defineProperty(promise, "_data", { get: () => data });
1528 this.emit(false, key);
1529 return data;
1530 }
1531
1532 private emit(aborted: boolean, settledKey?: string) {
1533 this.subscribers.forEach((subscriber) => subscriber(aborted, settledKey));
1534 }
1535
1536 subscribe(fn: (aborted: boolean, settledKey?: string) => void) {
1537 this.subscribers.add(fn);
1538 return () => this.subscribers.delete(fn);
1539 }
1540
1541 cancel() {
1542 this.controller.abort();
1543 this.pendingKeysSet.forEach((v, k) => this.pendingKeysSet.delete(k));
1544 this.emit(true);
1545 }
1546
1547 async resolveData(signal: AbortSignal) {
1548 let aborted = false;
1549 if (!this.done) {
1550 let onAbort = () => this.cancel();
1551 signal.addEventListener("abort", onAbort);
1552 aborted = await new Promise((resolve) => {
1553 this.subscribe((aborted) => {
1554 signal.removeEventListener("abort", onAbort);
1555 if (aborted || this.done) {
1556 resolve(aborted);
1557 }
1558 });
1559 });
1560 }
1561 return aborted;
1562 }
1563
1564 get done() {
1565 return this.pendingKeysSet.size === 0;
1566 }
1567
1568 get unwrappedData() {
1569 invariant(
1570 this.data !== null && this.done,
1571 "Can only unwrap data on initialized and settled deferreds"
1572 );
1573
1574 return Object.entries(this.data).reduce(
1575 (acc, [key, value]) =>
1576 Object.assign(acc, {
1577 [key]: unwrapTrackedPromise(value),
1578 }),
1579 {}
1580 );
1581 }
1582
1583 get pendingKeys() {
1584 return Array.from(this.pendingKeysSet);
1585 }
1586}
1587
1588function isTrackedPromise(value: any): value is TrackedPromise {
1589 return (
1590 value instanceof Promise && (value as TrackedPromise)._tracked === true
1591 );
1592}
1593
1594function unwrapTrackedPromise(value: any) {
1595 if (!isTrackedPromise(value)) {
1596 return value;
1597 }
1598
1599 if (value._error) {
1600 throw value._error;
1601 }
1602 return value._data;
1603}
1604
1605export type DeferFunction = (
1606 data: Record<string, unknown>,
1607 init?: number | ResponseInit
1608) => DeferredData;
1609
[0c6b92a]1610/**
1611 * @deprecated The `defer` method is deprecated in favor of returning raw
1612 * objects. This method will be removed in v7.
1613 */
[d565449]1614export const defer: DeferFunction = (data, init = {}) => {
1615 let responseInit = typeof init === "number" ? { status: init } : init;
1616
1617 return new DeferredData(data, responseInit);
1618};
1619
1620export type RedirectFunction = (
1621 url: string,
1622 init?: number | ResponseInit
1623) => Response;
1624
1625/**
1626 * A redirect response. Sets the status code and the `Location` header.
1627 * Defaults to "302 Found".
1628 */
1629export const redirect: RedirectFunction = (url, init = 302) => {
1630 let responseInit = init;
1631 if (typeof responseInit === "number") {
1632 responseInit = { status: responseInit };
1633 } else if (typeof responseInit.status === "undefined") {
1634 responseInit.status = 302;
1635 }
1636
1637 let headers = new Headers(responseInit.headers);
1638 headers.set("Location", url);
1639
1640 return new Response(null, {
1641 ...responseInit,
1642 headers,
1643 });
1644};
1645
1646/**
1647 * A redirect response that will force a document reload to the new location.
1648 * Sets the status code and the `Location` header.
1649 * Defaults to "302 Found".
1650 */
1651export const redirectDocument: RedirectFunction = (url, init) => {
1652 let response = redirect(url, init);
1653 response.headers.set("X-Remix-Reload-Document", "true");
1654 return response;
1655};
1656
1657/**
1658 * A redirect response that will perform a `history.replaceState` instead of a
1659 * `history.pushState` for client-side navigation redirects.
1660 * Sets the status code and the `Location` header.
1661 * Defaults to "302 Found".
1662 */
1663export const replace: RedirectFunction = (url, init) => {
1664 let response = redirect(url, init);
1665 response.headers.set("X-Remix-Replace", "true");
1666 return response;
1667};
1668
1669export type ErrorResponse = {
1670 status: number;
1671 statusText: string;
1672 data: any;
1673};
1674
1675/**
1676 * @private
1677 * Utility class we use to hold auto-unwrapped 4xx/5xx Response bodies
1678 *
1679 * We don't export the class for public use since it's an implementation
1680 * detail, but we export the interface above so folks can build their own
1681 * abstractions around instances via isRouteErrorResponse()
1682 */
1683export class ErrorResponseImpl implements ErrorResponse {
1684 status: number;
1685 statusText: string;
1686 data: any;
1687 private error?: Error;
1688 private internal: boolean;
1689
1690 constructor(
1691 status: number,
1692 statusText: string | undefined,
1693 data: any,
1694 internal = false
1695 ) {
1696 this.status = status;
1697 this.statusText = statusText || "";
1698 this.internal = internal;
1699 if (data instanceof Error) {
1700 this.data = data.toString();
1701 this.error = data;
1702 } else {
1703 this.data = data;
1704 }
1705 }
1706}
1707
1708/**
1709 * Check if the given error is an ErrorResponse generated from a 4xx/5xx
1710 * Response thrown from an action/loader
1711 */
1712export function isRouteErrorResponse(error: any): error is ErrorResponse {
1713 return (
1714 error != null &&
1715 typeof error.status === "number" &&
1716 typeof error.statusText === "string" &&
1717 typeof error.internal === "boolean" &&
1718 "data" in error
1719 );
1720}
Note: See TracBrowser for help on using the repository browser.