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

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

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