You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					1357 lines
				
				37 KiB
			
		
		
			
		
	
	
					1357 lines
				
				37 KiB
			| 
											3 years ago
										 | import type { Location, Path, To } from "./history"; | ||
|  | import { invariant, parsePath } from "./history"; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Map of routeId -> data returned from a loader/action/error | ||
|  |  */ | ||
|  | export interface RouteData { | ||
|  |   [routeId: string]: any; | ||
|  | } | ||
|  | 
 | ||
|  | export enum ResultType { | ||
|  |   data = "data", | ||
|  |   deferred = "deferred", | ||
|  |   redirect = "redirect", | ||
|  |   error = "error", | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Successful result from a loader or action | ||
|  |  */ | ||
|  | export interface SuccessResult { | ||
|  |   type: ResultType.data; | ||
|  |   data: any; | ||
|  |   statusCode?: number; | ||
|  |   headers?: Headers; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Successful defer() result from a loader or action | ||
|  |  */ | ||
|  | export interface DeferredResult { | ||
|  |   type: ResultType.deferred; | ||
|  |   deferredData: DeferredData; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Redirect result from a loader or action | ||
|  |  */ | ||
|  | export interface RedirectResult { | ||
|  |   type: ResultType.redirect; | ||
|  |   status: number; | ||
|  |   location: string; | ||
|  |   revalidate: boolean; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Unsuccessful result from a loader or action | ||
|  |  */ | ||
|  | export interface ErrorResult { | ||
|  |   type: ResultType.error; | ||
|  |   error: any; | ||
|  |   headers?: Headers; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Result from a loader or action - potentially successful or unsuccessful | ||
|  |  */ | ||
|  | export type DataResult = | ||
|  |   | SuccessResult | ||
|  |   | DeferredResult | ||
|  |   | RedirectResult | ||
|  |   | ErrorResult; | ||
|  | 
 | ||
|  | export type MutationFormMethod = "post" | "put" | "patch" | "delete"; | ||
|  | export type FormMethod = "get" | MutationFormMethod; | ||
|  | 
 | ||
|  | export type FormEncType = | ||
|  |   | "application/x-www-form-urlencoded" | ||
|  |   | "multipart/form-data"; | ||
|  | 
 | ||
|  | /** | ||
|  |  * @private | ||
|  |  * Internal interface to pass around for action submissions, not intended for | ||
|  |  * external consumption | ||
|  |  */ | ||
|  | export interface Submission { | ||
|  |   formMethod: FormMethod; | ||
|  |   formAction: string; | ||
|  |   formEncType: FormEncType; | ||
|  |   formData: FormData; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * @private | ||
|  |  * Arguments passed to route loader/action functions.  Same for now but we keep | ||
|  |  * this as a private implementation detail in case they diverge in the future. | ||
|  |  */ | ||
|  | interface DataFunctionArgs { | ||
|  |   request: Request; | ||
|  |   params: Params; | ||
|  |   context?: any; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Arguments passed to loader functions | ||
|  |  */ | ||
|  | export interface LoaderFunctionArgs extends DataFunctionArgs {} | ||
|  | 
 | ||
|  | /** | ||
|  |  * Arguments passed to action functions | ||
|  |  */ | ||
|  | export interface ActionFunctionArgs extends DataFunctionArgs {} | ||
|  | 
 | ||
|  | /** | ||
|  |  * Route loader function signature | ||
|  |  */ | ||
|  | export interface LoaderFunction { | ||
|  |   (args: LoaderFunctionArgs): Promise<Response> | Response | Promise<any> | any; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Route action function signature | ||
|  |  */ | ||
|  | export interface ActionFunction { | ||
|  |   (args: ActionFunctionArgs): Promise<Response> | Response | Promise<any> | any; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Route shouldRevalidate function signature.  This runs after any submission | ||
|  |  * (navigation or fetcher), so we flatten the navigation/fetcher submission | ||
|  |  * onto the arguments.  It shouldn't matter whether it came from a navigation | ||
|  |  * or a fetcher, what really matters is the URLs and the formData since loaders | ||
|  |  * have to re-run based on the data models that were potentially mutated. | ||
|  |  */ | ||
|  | export interface ShouldRevalidateFunction { | ||
|  |   (args: { | ||
|  |     currentUrl: URL; | ||
|  |     currentParams: AgnosticDataRouteMatch["params"]; | ||
|  |     nextUrl: URL; | ||
|  |     nextParams: AgnosticDataRouteMatch["params"]; | ||
|  |     formMethod?: Submission["formMethod"]; | ||
|  |     formAction?: Submission["formAction"]; | ||
|  |     formEncType?: Submission["formEncType"]; | ||
|  |     formData?: Submission["formData"]; | ||
|  |     actionResult?: DataResult; | ||
|  |     defaultShouldRevalidate: boolean; | ||
|  |   }): boolean; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Base RouteObject with common props shared by all types of routes | ||
|  |  */ | ||
|  | type AgnosticBaseRouteObject = { | ||
|  |   caseSensitive?: boolean; | ||
|  |   path?: string; | ||
|  |   id?: string; | ||
|  |   loader?: LoaderFunction; | ||
|  |   action?: ActionFunction; | ||
|  |   hasErrorBoundary?: boolean; | ||
|  |   shouldRevalidate?: ShouldRevalidateFunction; | ||
|  |   handle?: any; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Index routes must not have children | ||
|  |  */ | ||
|  | export type AgnosticIndexRouteObject = AgnosticBaseRouteObject & { | ||
|  |   children?: undefined; | ||
|  |   index: true; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Non-index routes may have children, but cannot have index | ||
|  |  */ | ||
|  | export type AgnosticNonIndexRouteObject = AgnosticBaseRouteObject & { | ||
|  |   children?: AgnosticRouteObject[]; | ||
|  |   index?: false; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * A route object represents a logical route, with (optionally) its child | ||
|  |  * routes organized in a tree-like structure. | ||
|  |  */ | ||
|  | export type AgnosticRouteObject = | ||
|  |   | AgnosticIndexRouteObject | ||
|  |   | AgnosticNonIndexRouteObject; | ||
|  | 
 | ||
|  | export type AgnosticDataIndexRouteObject = AgnosticIndexRouteObject & { | ||
|  |   id: string; | ||
|  | }; | ||
|  | 
 | ||
|  | export type AgnosticDataNonIndexRouteObject = AgnosticNonIndexRouteObject & { | ||
|  |   children?: AgnosticDataRouteObject[]; | ||
|  |   id: string; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * A data route object, which is just a RouteObject with a required unique ID | ||
|  |  */ | ||
|  | export type AgnosticDataRouteObject = | ||
|  |   | AgnosticDataIndexRouteObject | ||
|  |   | AgnosticDataNonIndexRouteObject; | ||
|  | 
 | ||
|  | // Recursive helper for finding path parameters in the absence of wildcards
 | ||
|  | type _PathParam<Path extends string> = | ||
|  |   // split path into individual path segments
 | ||
|  |   Path extends `${infer L}/${infer R}` | ||
|  |     ? _PathParam<L> | _PathParam<R> | ||
|  |     : // find params after `:`
 | ||
|  |     Path extends `:${infer Param}` | ||
|  |     ? Param | ||
|  |     : // otherwise, there aren't any params present
 | ||
|  |       never; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Examples: | ||
|  |  * "/a/b/*" -> "*" | ||
|  |  * ":a" -> "a" | ||
|  |  * "/a/:b" -> "b" | ||
|  |  * "/a/blahblahblah:b" -> "b" | ||
|  |  * "/:a/:b" -> "a" | "b" | ||
|  |  * "/:a/b/:c/*" -> "a" | "c" | "*" | ||
|  |  */ | ||
|  | type PathParam<Path extends string> = | ||
|  |   // check if path is just a wildcard
 | ||
|  |   Path extends "*" | ||
|  |     ? "*" | ||
|  |     : // look for wildcard at the end of the path
 | ||
|  |     Path extends `${infer Rest}/*` | ||
|  |     ? "*" | _PathParam<Rest> | ||
|  |     : // look for params in the absence of wildcards
 | ||
|  |       _PathParam<Path>; | ||
|  | 
 | ||
|  | // Attempt to parse the given string segment. If it fails, then just return the
 | ||
|  | // plain string type as a default fallback. Otherwise return the union of the
 | ||
|  | // parsed string literals that were referenced as dynamic segments in the route.
 | ||
|  | export type ParamParseKey<Segment extends string> = | ||
|  |   // if could not find path params, fallback to `string`
 | ||
|  |   [PathParam<Segment>] extends [never] ? string : PathParam<Segment>; | ||
|  | 
 | ||
|  | /** | ||
|  |  * The parameters that were parsed from the URL path. | ||
|  |  */ | ||
|  | export type Params<Key extends string = string> = { | ||
|  |   readonly [key in Key]: string | undefined; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * A RouteMatch contains info about how a route matched a URL. | ||
|  |  */ | ||
|  | export interface AgnosticRouteMatch< | ||
|  |   ParamKey extends string = string, | ||
|  |   RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject | ||
|  | > { | ||
|  |   /** | ||
|  |    * The names and values of dynamic parameters in the URL. | ||
|  |    */ | ||
|  |   params: Params<ParamKey>; | ||
|  |   /** | ||
|  |    * The portion of the URL pathname that was matched. | ||
|  |    */ | ||
|  |   pathname: string; | ||
|  |   /** | ||
|  |    * The portion of the URL pathname that was matched before child routes. | ||
|  |    */ | ||
|  |   pathnameBase: string; | ||
|  |   /** | ||
|  |    * The route object that was used to match. | ||
|  |    */ | ||
|  |   route: RouteObjectType; | ||
|  | } | ||
|  | 
 | ||
|  | export interface AgnosticDataRouteMatch | ||
|  |   extends AgnosticRouteMatch<string, AgnosticDataRouteObject> {} | ||
|  | 
 | ||
|  | function isIndexRoute( | ||
|  |   route: AgnosticRouteObject | ||
|  | ): route is AgnosticIndexRouteObject { | ||
|  |   return route.index === true; | ||
|  | } | ||
|  | 
 | ||
|  | // Walk the route tree generating unique IDs where necessary so we are working
 | ||
|  | // solely with AgnosticDataRouteObject's within the Router
 | ||
|  | export function convertRoutesToDataRoutes( | ||
|  |   routes: AgnosticRouteObject[], | ||
|  |   parentPath: number[] = [], | ||
|  |   allIds: Set<string> = new Set<string>() | ||
|  | ): AgnosticDataRouteObject[] { | ||
|  |   return routes.map((route, index) => { | ||
|  |     let treePath = [...parentPath, index]; | ||
|  |     let id = typeof route.id === "string" ? route.id : treePath.join("-"); | ||
|  |     invariant( | ||
|  |       route.index !== true || !route.children, | ||
|  |       `Cannot specify children on an index route` | ||
|  |     ); | ||
|  |     invariant( | ||
|  |       !allIds.has(id), | ||
|  |       `Found a route id collision on id "${id}".  Route ` + | ||
|  |         "id's must be globally unique within Data Router usages" | ||
|  |     ); | ||
|  |     allIds.add(id); | ||
|  | 
 | ||
|  |     if (isIndexRoute(route)) { | ||
|  |       let indexRoute: AgnosticDataIndexRouteObject = { ...route, id }; | ||
|  |       return indexRoute; | ||
|  |     } else { | ||
|  |       let pathOrLayoutRoute: AgnosticDataNonIndexRouteObject = { | ||
|  |         ...route, | ||
|  |         id, | ||
|  |         children: route.children | ||
|  |           ? convertRoutesToDataRoutes(route.children, treePath, allIds) | ||
|  |           : undefined, | ||
|  |       }; | ||
|  |       return pathOrLayoutRoute; | ||
|  |     } | ||
|  |   }); | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Matches the given routes to a location and returns the match data. | ||
|  |  * | ||
|  |  * @see https://reactrouter.com/utils/match-routes
 | ||
|  |  */ | ||
|  | export function matchRoutes< | ||
|  |   RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject | ||
|  | >( | ||
|  |   routes: RouteObjectType[], | ||
|  |   locationArg: Partial<Location> | string, | ||
|  |   basename = "/" | ||
|  | ): AgnosticRouteMatch<string, RouteObjectType>[] | null { | ||
|  |   let location = | ||
|  |     typeof locationArg === "string" ? parsePath(locationArg) : locationArg; | ||
|  | 
 | ||
|  |   let pathname = stripBasename(location.pathname || "/", basename); | ||
|  | 
 | ||
|  |   if (pathname == null) { | ||
|  |     return null; | ||
|  |   } | ||
|  | 
 | ||
|  |   let branches = flattenRoutes(routes); | ||
|  |   rankRouteBranches(branches); | ||
|  | 
 | ||
|  |   let matches = null; | ||
|  |   for (let i = 0; matches == null && i < branches.length; ++i) { | ||
|  |     matches = matchRouteBranch<string, RouteObjectType>( | ||
|  |       branches[i], | ||
|  |       // Incoming pathnames are generally encoded from either window.location
 | ||
|  |       // or from router.navigate, but we want to match against the unencoded
 | ||
|  |       // paths in the route definitions.  Memory router locations won't be
 | ||
|  |       // encoded here but there also shouldn't be anything to decode so this
 | ||
|  |       // should be a safe operation.  This avoids needing matchRoutes to be
 | ||
|  |       // history-aware.
 | ||
|  |       safelyDecodeURI(pathname) | ||
|  |     ); | ||
|  |   } | ||
|  | 
 | ||
|  |   return matches; | ||
|  | } | ||
|  | 
 | ||
|  | interface RouteMeta< | ||
|  |   RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject | ||
|  | > { | ||
|  |   relativePath: string; | ||
|  |   caseSensitive: boolean; | ||
|  |   childrenIndex: number; | ||
|  |   route: RouteObjectType; | ||
|  | } | ||
|  | 
 | ||
|  | interface RouteBranch< | ||
|  |   RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject | ||
|  | > { | ||
|  |   path: string; | ||
|  |   score: number; | ||
|  |   routesMeta: RouteMeta<RouteObjectType>[]; | ||
|  | } | ||
|  | 
 | ||
|  | function flattenRoutes< | ||
|  |   RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject | ||
|  | >( | ||
|  |   routes: RouteObjectType[], | ||
|  |   branches: RouteBranch<RouteObjectType>[] = [], | ||
|  |   parentsMeta: RouteMeta<RouteObjectType>[] = [], | ||
|  |   parentPath = "" | ||
|  | ): RouteBranch<RouteObjectType>[] { | ||
|  |   let flattenRoute = ( | ||
|  |     route: RouteObjectType, | ||
|  |     index: number, | ||
|  |     relativePath?: string | ||
|  |   ) => { | ||
|  |     let meta: RouteMeta<RouteObjectType> = { | ||
|  |       relativePath: | ||
|  |         relativePath === undefined ? route.path || "" : relativePath, | ||
|  |       caseSensitive: route.caseSensitive === true, | ||
|  |       childrenIndex: index, | ||
|  |       route, | ||
|  |     }; | ||
|  | 
 | ||
|  |     if (meta.relativePath.startsWith("/")) { | ||
|  |       invariant( | ||
|  |         meta.relativePath.startsWith(parentPath), | ||
|  |         `Absolute route path "${meta.relativePath}" nested under path ` + | ||
|  |           `"${parentPath}" is not valid. An absolute child route path ` + | ||
|  |           `must start with the combined path of all its parent routes.` | ||
|  |       ); | ||
|  | 
 | ||
|  |       meta.relativePath = meta.relativePath.slice(parentPath.length); | ||
|  |     } | ||
|  | 
 | ||
|  |     let path = joinPaths([parentPath, meta.relativePath]); | ||
|  |     let routesMeta = parentsMeta.concat(meta); | ||
|  | 
 | ||
|  |     // Add the children before adding this route to the array so we traverse the
 | ||
|  |     // route tree depth-first and child routes appear before their parents in
 | ||
|  |     // the "flattened" version.
 | ||
|  |     if (route.children && route.children.length > 0) { | ||
|  |       invariant( | ||
|  |         // Our types know better, but runtime JS may not!
 | ||
|  |         // @ts-expect-error
 | ||
|  |         route.index !== true, | ||
|  |         `Index routes must not have child routes. Please remove ` + | ||
|  |           `all child routes from route path "${path}".` | ||
|  |       ); | ||
|  | 
 | ||
|  |       flattenRoutes(route.children, branches, routesMeta, path); | ||
|  |     } | ||
|  | 
 | ||
|  |     // Routes without a path shouldn't ever match by themselves unless they are
 | ||
|  |     // index routes, so don't add them to the list of possible branches.
 | ||
|  |     if (route.path == null && !route.index) { | ||
|  |       return; | ||
|  |     } | ||
|  | 
 | ||
|  |     branches.push({ | ||
|  |       path, | ||
|  |       score: computeScore(path, route.index), | ||
|  |       routesMeta, | ||
|  |     }); | ||
|  |   }; | ||
|  |   routes.forEach((route, index) => { | ||
|  |     // coarse-grain check for optional params
 | ||
|  |     if (route.path === "" || !route.path?.includes("?")) { | ||
|  |       flattenRoute(route, index); | ||
|  |     } else { | ||
|  |       for (let exploded of explodeOptionalSegments(route.path)) { | ||
|  |         flattenRoute(route, index, exploded); | ||
|  |       } | ||
|  |     } | ||
|  |   }); | ||
|  | 
 | ||
|  |   return branches; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Computes all combinations of optional path segments for a given path, | ||
|  |  * excluding combinations that are ambiguous and of lower priority. | ||
|  |  * | ||
|  |  * For example, `/one/:two?/three/:four?/:five?` explodes to: | ||
|  |  * - `/one/three` | ||
|  |  * - `/one/:two/three` | ||
|  |  * - `/one/three/:four` | ||
|  |  * - `/one/three/:five` | ||
|  |  * - `/one/:two/three/:four` | ||
|  |  * - `/one/:two/three/:five` | ||
|  |  * - `/one/three/:four/:five` | ||
|  |  * - `/one/:two/three/:four/:five` | ||
|  |  */ | ||
|  | function explodeOptionalSegments(path: string): string[] { | ||
|  |   let segments = path.split("/"); | ||
|  |   if (segments.length === 0) return []; | ||
|  | 
 | ||
|  |   let [first, ...rest] = segments; | ||
|  | 
 | ||
|  |   // Optional path segments are denoted by a trailing `?`
 | ||
|  |   let isOptional = first.endsWith("?"); | ||
|  |   // Compute the corresponding required segment: `foo?` -> `foo`
 | ||
|  |   let required = first.replace(/\?$/, ""); | ||
|  | 
 | ||
|  |   if (rest.length === 0) { | ||
|  |     // Intepret empty string as omitting an optional segment
 | ||
|  |     // `["one", "", "three"]` corresponds to omitting `:two` from `/one/:two?/three` -> `/one/three`
 | ||
|  |     return isOptional ? [required, ""] : [required]; | ||
|  |   } | ||
|  | 
 | ||
|  |   let restExploded = explodeOptionalSegments(rest.join("/")); | ||
|  | 
 | ||
|  |   let result: string[] = []; | ||
|  | 
 | ||
|  |   // All child paths with the prefix.  Do this for all children before the
 | ||
|  |   // optional version for all children so we get consistent ordering where the
 | ||
|  |   // parent optional aspect is preferred as required.  Otherwise, we can get
 | ||
|  |   // child sections interspersed where deeper optional segments are higher than
 | ||
|  |   // parent optional segments, where for example, /:two would explodes _earlier_
 | ||
|  |   // then /:one.  By always including the parent as required _for all children_
 | ||
|  |   // first, we avoid this issue
 | ||
|  |   result.push( | ||
|  |     ...restExploded.map((subpath) => | ||
|  |       subpath === "" ? required : [required, subpath].join("/") | ||
|  |     ) | ||
|  |   ); | ||
|  | 
 | ||
|  |   // Then if this is an optional value, add all child versions without
 | ||
|  |   if (isOptional) { | ||
|  |     result.push(...restExploded); | ||
|  |   } | ||
|  | 
 | ||
|  |   // for absolute paths, ensure `/` instead of empty segment
 | ||
|  |   return result.map((exploded) => | ||
|  |     path.startsWith("/") && exploded === "" ? "/" : exploded | ||
|  |   ); | ||
|  | } | ||
|  | 
 | ||
|  | function rankRouteBranches(branches: RouteBranch[]): void { | ||
|  |   branches.sort((a, b) => | ||
|  |     a.score !== b.score | ||
|  |       ? b.score - a.score // Higher score first
 | ||
|  |       : compareIndexes( | ||
|  |           a.routesMeta.map((meta) => meta.childrenIndex), | ||
|  |           b.routesMeta.map((meta) => meta.childrenIndex) | ||
|  |         ) | ||
|  |   ); | ||
|  | } | ||
|  | 
 | ||
|  | const paramRe = /^:\w+$/; | ||
|  | const dynamicSegmentValue = 3; | ||
|  | const indexRouteValue = 2; | ||
|  | const emptySegmentValue = 1; | ||
|  | const staticSegmentValue = 10; | ||
|  | const splatPenalty = -2; | ||
|  | const isSplat = (s: string) => s === "*"; | ||
|  | 
 | ||
|  | function computeScore(path: string, index: boolean | undefined): number { | ||
|  |   let segments = path.split("/"); | ||
|  |   let initialScore = segments.length; | ||
|  |   if (segments.some(isSplat)) { | ||
|  |     initialScore += splatPenalty; | ||
|  |   } | ||
|  | 
 | ||
|  |   if (index) { | ||
|  |     initialScore += indexRouteValue; | ||
|  |   } | ||
|  | 
 | ||
|  |   return segments | ||
|  |     .filter((s) => !isSplat(s)) | ||
|  |     .reduce( | ||
|  |       (score, segment) => | ||
|  |         score + | ||
|  |         (paramRe.test(segment) | ||
|  |           ? dynamicSegmentValue | ||
|  |           : segment === "" | ||
|  |           ? emptySegmentValue | ||
|  |           : staticSegmentValue), | ||
|  |       initialScore | ||
|  |     ); | ||
|  | } | ||
|  | 
 | ||
|  | function compareIndexes(a: number[], b: number[]): number { | ||
|  |   let siblings = | ||
|  |     a.length === b.length && a.slice(0, -1).every((n, i) => n === b[i]); | ||
|  | 
 | ||
|  |   return siblings | ||
|  |     ? // If two routes are siblings, we should try to match the earlier sibling
 | ||
|  |       // first. This allows people to have fine-grained control over the matching
 | ||
|  |       // behavior by simply putting routes with identical paths in the order they
 | ||
|  |       // want them tried.
 | ||
|  |       a[a.length - 1] - b[b.length - 1] | ||
|  |     : // Otherwise, it doesn't really make sense to rank non-siblings by index,
 | ||
|  |       // so they sort equally.
 | ||
|  |       0; | ||
|  | } | ||
|  | 
 | ||
|  | function matchRouteBranch< | ||
|  |   ParamKey extends string = string, | ||
|  |   RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject | ||
|  | >( | ||
|  |   branch: RouteBranch<RouteObjectType>, | ||
|  |   pathname: string | ||
|  | ): AgnosticRouteMatch<ParamKey, RouteObjectType>[] | null { | ||
|  |   let { routesMeta } = branch; | ||
|  | 
 | ||
|  |   let matchedParams = {}; | ||
|  |   let matchedPathname = "/"; | ||
|  |   let matches: AgnosticRouteMatch<ParamKey, RouteObjectType>[] = []; | ||
|  |   for (let i = 0; i < routesMeta.length; ++i) { | ||
|  |     let meta = routesMeta[i]; | ||
|  |     let end = i === routesMeta.length - 1; | ||
|  |     let remainingPathname = | ||
|  |       matchedPathname === "/" | ||
|  |         ? pathname | ||
|  |         : pathname.slice(matchedPathname.length) || "/"; | ||
|  |     let match = matchPath( | ||
|  |       { path: meta.relativePath, caseSensitive: meta.caseSensitive, end }, | ||
|  |       remainingPathname | ||
|  |     ); | ||
|  | 
 | ||
|  |     if (!match) return null; | ||
|  | 
 | ||
|  |     Object.assign(matchedParams, match.params); | ||
|  | 
 | ||
|  |     let route = meta.route; | ||
|  | 
 | ||
|  |     matches.push({ | ||
|  |       // TODO: Can this as be avoided?
 | ||
|  |       params: matchedParams as Params<ParamKey>, | ||
|  |       pathname: joinPaths([matchedPathname, match.pathname]), | ||
|  |       pathnameBase: normalizePathname( | ||
|  |         joinPaths([matchedPathname, match.pathnameBase]) | ||
|  |       ), | ||
|  |       route, | ||
|  |     }); | ||
|  | 
 | ||
|  |     if (match.pathnameBase !== "/") { | ||
|  |       matchedPathname = joinPaths([matchedPathname, match.pathnameBase]); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   return matches; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Returns a path with params interpolated. | ||
|  |  * | ||
|  |  * @see https://reactrouter.com/utils/generate-path
 | ||
|  |  */ | ||
|  | export function generatePath<Path extends string>( | ||
|  |   originalPath: Path, | ||
|  |   params: { | ||
|  |     [key in PathParam<Path>]: string; | ||
|  |   } = {} as any | ||
|  | ): string { | ||
|  |   let path = originalPath; | ||
|  |   if (path.endsWith("*") && path !== "*" && !path.endsWith("/*")) { | ||
|  |     warning( | ||
|  |       false, | ||
|  |       `Route path "${path}" will be treated as if it were ` + | ||
|  |         `"${path.replace(/\*$/, "/*")}" because the \`*\` character must ` + | ||
|  |         `always follow a \`/\` in the pattern. To get rid of this warning, ` + | ||
|  |         `please change the route path to "${path.replace(/\*$/, "/*")}".` | ||
|  |     ); | ||
|  |     path = path.replace(/\*$/, "/*") as Path; | ||
|  |   } | ||
|  | 
 | ||
|  |   return path | ||
|  |     .replace(/^:(\w+)/g, (_, key: PathParam<Path>) => { | ||
|  |       invariant(params[key] != null, `Missing ":${key}" param`); | ||
|  |       return params[key]!; | ||
|  |     }) | ||
|  |     .replace(/\/:(\w+)/g, (_, key: PathParam<Path>) => { | ||
|  |       invariant(params[key] != null, `Missing ":${key}" param`); | ||
|  |       return `/${params[key]!}`; | ||
|  |     }) | ||
|  |     .replace(/(\/?)\*/, (_, prefix, __, str) => { | ||
|  |       const star = "*" as PathParam<Path>; | ||
|  | 
 | ||
|  |       if (params[star] == null) { | ||
|  |         // If no splat was provided, trim the trailing slash _unless_ it's
 | ||
|  |         // the entire path
 | ||
|  |         return str === "/*" ? "/" : ""; | ||
|  |       } | ||
|  | 
 | ||
|  |       // Apply the splat
 | ||
|  |       return `${prefix}${params[star]}`; | ||
|  |     }); | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * A PathPattern is used to match on some portion of a URL pathname. | ||
|  |  */ | ||
|  | export interface PathPattern<Path extends string = string> { | ||
|  |   /** | ||
|  |    * A string to match against a URL pathname. May contain `:id`-style segments | ||
|  |    * to indicate placeholders for dynamic parameters. May also end with `/*` to | ||
|  |    * indicate matching the rest of the URL pathname. | ||
|  |    */ | ||
|  |   path: Path; | ||
|  |   /** | ||
|  |    * Should be `true` if the static portions of the `path` should be matched in | ||
|  |    * the same case. | ||
|  |    */ | ||
|  |   caseSensitive?: boolean; | ||
|  |   /** | ||
|  |    * Should be `true` if this pattern should match the entire URL pathname. | ||
|  |    */ | ||
|  |   end?: boolean; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * A PathMatch contains info about how a PathPattern matched on a URL pathname. | ||
|  |  */ | ||
|  | export interface PathMatch<ParamKey extends string = string> { | ||
|  |   /** | ||
|  |    * The names and values of dynamic parameters in the URL. | ||
|  |    */ | ||
|  |   params: Params<ParamKey>; | ||
|  |   /** | ||
|  |    * The portion of the URL pathname that was matched. | ||
|  |    */ | ||
|  |   pathname: string; | ||
|  |   /** | ||
|  |    * The portion of the URL pathname that was matched before child routes. | ||
|  |    */ | ||
|  |   pathnameBase: string; | ||
|  |   /** | ||
|  |    * The pattern that was used to match. | ||
|  |    */ | ||
|  |   pattern: PathPattern; | ||
|  | } | ||
|  | 
 | ||
|  | type Mutable<T> = { | ||
|  |   -readonly [P in keyof T]: T[P]; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Performs pattern matching on a URL pathname and returns information about | ||
|  |  * the match. | ||
|  |  * | ||
|  |  * @see https://reactrouter.com/utils/match-path
 | ||
|  |  */ | ||
|  | export function matchPath< | ||
|  |   ParamKey extends ParamParseKey<Path>, | ||
|  |   Path extends string | ||
|  | >( | ||
|  |   pattern: PathPattern<Path> | Path, | ||
|  |   pathname: string | ||
|  | ): PathMatch<ParamKey> | null { | ||
|  |   if (typeof pattern === "string") { | ||
|  |     pattern = { path: pattern, caseSensitive: false, end: true }; | ||
|  |   } | ||
|  | 
 | ||
|  |   let [matcher, paramNames] = compilePath( | ||
|  |     pattern.path, | ||
|  |     pattern.caseSensitive, | ||
|  |     pattern.end | ||
|  |   ); | ||
|  | 
 | ||
|  |   let match = pathname.match(matcher); | ||
|  |   if (!match) return null; | ||
|  | 
 | ||
|  |   let matchedPathname = match[0]; | ||
|  |   let pathnameBase = matchedPathname.replace(/(.)\/+$/, "$1"); | ||
|  |   let captureGroups = match.slice(1); | ||
|  |   let params: Params = paramNames.reduce<Mutable<Params>>( | ||
|  |     (memo, paramName, index) => { | ||
|  |       // We need to compute the pathnameBase here using the raw splat value
 | ||
|  |       // instead of using params["*"] later because it will be decoded then
 | ||
|  |       if (paramName === "*") { | ||
|  |         let splatValue = captureGroups[index] || ""; | ||
|  |         pathnameBase = matchedPathname | ||
|  |           .slice(0, matchedPathname.length - splatValue.length) | ||
|  |           .replace(/(.)\/+$/, "$1"); | ||
|  |       } | ||
|  | 
 | ||
|  |       memo[paramName] = safelyDecodeURIComponent( | ||
|  |         captureGroups[index] || "", | ||
|  |         paramName | ||
|  |       ); | ||
|  |       return memo; | ||
|  |     }, | ||
|  |     {} | ||
|  |   ); | ||
|  | 
 | ||
|  |   return { | ||
|  |     params, | ||
|  |     pathname: matchedPathname, | ||
|  |     pathnameBase, | ||
|  |     pattern, | ||
|  |   }; | ||
|  | } | ||
|  | 
 | ||
|  | function compilePath( | ||
|  |   path: string, | ||
|  |   caseSensitive = false, | ||
|  |   end = true | ||
|  | ): [RegExp, string[]] { | ||
|  |   warning( | ||
|  |     path === "*" || !path.endsWith("*") || path.endsWith("/*"), | ||
|  |     `Route path "${path}" will be treated as if it were ` + | ||
|  |       `"${path.replace(/\*$/, "/*")}" because the \`*\` character must ` + | ||
|  |       `always follow a \`/\` in the pattern. To get rid of this warning, ` + | ||
|  |       `please change the route path to "${path.replace(/\*$/, "/*")}".` | ||
|  |   ); | ||
|  | 
 | ||
|  |   let paramNames: string[] = []; | ||
|  |   let regexpSource = | ||
|  |     "^" + | ||
|  |     path | ||
|  |       .replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
 | ||
|  |       .replace(/^\/*/, "/") // Make sure it has a leading /
 | ||
|  |       .replace(/[\\.*+^$?{}|()[\]]/g, "\\$&") // Escape special regex chars
 | ||
|  |       .replace(/\/:(\w+)/g, (_: string, paramName: string) => { | ||
|  |         paramNames.push(paramName); | ||
|  |         return "/([^\\/]+)"; | ||
|  |       }); | ||
|  | 
 | ||
|  |   if (path.endsWith("*")) { | ||
|  |     paramNames.push("*"); | ||
|  |     regexpSource += | ||
|  |       path === "*" || path === "/*" | ||
|  |         ? "(.*)$" // Already matched the initial /, just match the rest
 | ||
|  |         : "(?:\\/(.+)|\\/*)$"; // Don't include the / in params["*"]
 | ||
|  |   } else if (end) { | ||
|  |     // When matching to the end, ignore trailing slashes
 | ||
|  |     regexpSource += "\\/*$"; | ||
|  |   } else if (path !== "" && path !== "/") { | ||
|  |     // If our path is non-empty and contains anything beyond an initial slash,
 | ||
|  |     // then we have _some_ form of path in our regex so we should expect to
 | ||
|  |     // match only if we find the end of this path segment.  Look for an optional
 | ||
|  |     // non-captured trailing slash (to match a portion of the URL) or the end
 | ||
|  |     // of the path (if we've matched to the end).  We used to do this with a
 | ||
|  |     // word boundary but that gives false positives on routes like
 | ||
|  |     // /user-preferences since `-` counts as a word boundary.
 | ||
|  |     regexpSource += "(?:(?=\\/|$))"; | ||
|  |   } else { | ||
|  |     // Nothing to match for "" or "/"
 | ||
|  |   } | ||
|  | 
 | ||
|  |   let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i"); | ||
|  | 
 | ||
|  |   return [matcher, paramNames]; | ||
|  | } | ||
|  | 
 | ||
|  | function safelyDecodeURI(value: string) { | ||
|  |   try { | ||
|  |     return decodeURI(value); | ||
|  |   } catch (error) { | ||
|  |     warning( | ||
|  |       false, | ||
|  |       `The URL path "${value}" could not be decoded because it is is a ` + | ||
|  |         `malformed URL segment. This is probably due to a bad percent ` + | ||
|  |         `encoding (${error}).` | ||
|  |     ); | ||
|  | 
 | ||
|  |     return value; | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function safelyDecodeURIComponent(value: string, paramName: string) { | ||
|  |   try { | ||
|  |     return decodeURIComponent(value); | ||
|  |   } catch (error) { | ||
|  |     warning( | ||
|  |       false, | ||
|  |       `The value for the URL param "${paramName}" will not be decoded because` + | ||
|  |         ` the string "${value}" is a malformed URL segment. This is probably` + | ||
|  |         ` due to a bad percent encoding (${error}).` | ||
|  |     ); | ||
|  | 
 | ||
|  |     return value; | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * @private | ||
|  |  */ | ||
|  | export function stripBasename( | ||
|  |   pathname: string, | ||
|  |   basename: string | ||
|  | ): string | null { | ||
|  |   if (basename === "/") return pathname; | ||
|  | 
 | ||
|  |   if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) { | ||
|  |     return null; | ||
|  |   } | ||
|  | 
 | ||
|  |   // We want to leave trailing slash behavior in the user's control, so if they
 | ||
|  |   // specify a basename with a trailing slash, we should support it
 | ||
|  |   let startIndex = basename.endsWith("/") | ||
|  |     ? basename.length - 1 | ||
|  |     : basename.length; | ||
|  |   let nextChar = pathname.charAt(startIndex); | ||
|  |   if (nextChar && nextChar !== "/") { | ||
|  |     // pathname does not start with basename/
 | ||
|  |     return null; | ||
|  |   } | ||
|  | 
 | ||
|  |   return pathname.slice(startIndex) || "/"; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * @private | ||
|  |  */ | ||
|  | export function warning(cond: any, message: string): void { | ||
|  |   if (!cond) { | ||
|  |     // eslint-disable-next-line no-console
 | ||
|  |     if (typeof console !== "undefined") console.warn(message); | ||
|  | 
 | ||
|  |     try { | ||
|  |       // Welcome to debugging React Router!
 | ||
|  |       //
 | ||
|  |       // This error is thrown as a convenience so you can more easily
 | ||
|  |       // find the source for a warning that appears in the console by
 | ||
|  |       // enabling "pause on exceptions" in your JavaScript debugger.
 | ||
|  |       throw new Error(message); | ||
|  |       // eslint-disable-next-line no-empty
 | ||
|  |     } catch (e) {} | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Returns a resolved path object relative to the given pathname. | ||
|  |  * | ||
|  |  * @see https://reactrouter.com/utils/resolve-path
 | ||
|  |  */ | ||
|  | export function resolvePath(to: To, fromPathname = "/"): Path { | ||
|  |   let { | ||
|  |     pathname: toPathname, | ||
|  |     search = "", | ||
|  |     hash = "", | ||
|  |   } = typeof to === "string" ? parsePath(to) : to; | ||
|  | 
 | ||
|  |   let pathname = toPathname | ||
|  |     ? toPathname.startsWith("/") | ||
|  |       ? toPathname | ||
|  |       : resolvePathname(toPathname, fromPathname) | ||
|  |     : fromPathname; | ||
|  | 
 | ||
|  |   return { | ||
|  |     pathname, | ||
|  |     search: normalizeSearch(search), | ||
|  |     hash: normalizeHash(hash), | ||
|  |   }; | ||
|  | } | ||
|  | 
 | ||
|  | function resolvePathname(relativePath: string, fromPathname: string): string { | ||
|  |   let segments = fromPathname.replace(/\/+$/, "").split("/"); | ||
|  |   let relativeSegments = relativePath.split("/"); | ||
|  | 
 | ||
|  |   relativeSegments.forEach((segment) => { | ||
|  |     if (segment === "..") { | ||
|  |       // Keep the root "" segment so the pathname starts at /
 | ||
|  |       if (segments.length > 1) segments.pop(); | ||
|  |     } else if (segment !== ".") { | ||
|  |       segments.push(segment); | ||
|  |     } | ||
|  |   }); | ||
|  | 
 | ||
|  |   return segments.length > 1 ? segments.join("/") : "/"; | ||
|  | } | ||
|  | 
 | ||
|  | function getInvalidPathError( | ||
|  |   char: string, | ||
|  |   field: string, | ||
|  |   dest: string, | ||
|  |   path: Partial<Path> | ||
|  | ) { | ||
|  |   return ( | ||
|  |     `Cannot include a '${char}' character in a manually specified ` + | ||
|  |     `\`to.${field}\` field [${JSON.stringify( | ||
|  |       path | ||
|  |     )}].  Please separate it out to the ` +
 | ||
|  |     `\`to.${dest}\` field. Alternatively you may provide the full path as ` + | ||
|  |     `a string in <Link to="..."> and the router will parse it for you.` | ||
|  |   ); | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * @private | ||
|  |  * | ||
|  |  * When processing relative navigation we want to ignore ancestor routes that | ||
|  |  * do not contribute to the path, such that index/pathless layout routes don't | ||
|  |  * interfere. | ||
|  |  * | ||
|  |  * For example, when moving a route element into an index route and/or a | ||
|  |  * pathless layout route, relative link behavior contained within should stay | ||
|  |  * the same.  Both of the following examples should link back to the root: | ||
|  |  * | ||
|  |  *   <Route path="/"> | ||
|  |  *     <Route path="accounts" element={<Link to=".."}> | ||
|  |  *   </Route> | ||
|  |  * | ||
|  |  *   <Route path="/"> | ||
|  |  *     <Route path="accounts"> | ||
|  |  *       <Route element={<AccountsLayout />}>       // <-- Does not contribute
 | ||
|  |  *         <Route index element={<Link to=".."} />  // <-- Does not contribute
 | ||
|  |  *       </Route | ||
|  |  *     </Route> | ||
|  |  *   </Route> | ||
|  |  */ | ||
|  | export function getPathContributingMatches< | ||
|  |   T extends AgnosticRouteMatch = AgnosticRouteMatch | ||
|  | >(matches: T[]) { | ||
|  |   return matches.filter( | ||
|  |     (match, index) => | ||
|  |       index === 0 || (match.route.path && match.route.path.length > 0) | ||
|  |   ); | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * @private | ||
|  |  */ | ||
|  | export function resolveTo( | ||
|  |   toArg: To, | ||
|  |   routePathnames: string[], | ||
|  |   locationPathname: string, | ||
|  |   isPathRelative = false | ||
|  | ): Path { | ||
|  |   let to: Partial<Path>; | ||
|  |   if (typeof toArg === "string") { | ||
|  |     to = parsePath(toArg); | ||
|  |   } else { | ||
|  |     to = { ...toArg }; | ||
|  | 
 | ||
|  |     invariant( | ||
|  |       !to.pathname || !to.pathname.includes("?"), | ||
|  |       getInvalidPathError("?", "pathname", "search", to) | ||
|  |     ); | ||
|  |     invariant( | ||
|  |       !to.pathname || !to.pathname.includes("#"), | ||
|  |       getInvalidPathError("#", "pathname", "hash", to) | ||
|  |     ); | ||
|  |     invariant( | ||
|  |       !to.search || !to.search.includes("#"), | ||
|  |       getInvalidPathError("#", "search", "hash", to) | ||
|  |     ); | ||
|  |   } | ||
|  | 
 | ||
|  |   let isEmptyPath = toArg === "" || to.pathname === ""; | ||
|  |   let toPathname = isEmptyPath ? "/" : to.pathname; | ||
|  | 
 | ||
|  |   let from: string; | ||
|  | 
 | ||
|  |   // Routing is relative to the current pathname if explicitly requested.
 | ||
|  |   //
 | ||
|  |   // If a pathname is explicitly provided in `to`, it should be relative to the
 | ||
|  |   // route context. This is explained in `Note on `<Link to>` values` in our
 | ||
|  |   // migration guide from v5 as a means of disambiguation between `to` values
 | ||
|  |   // that begin with `/` and those that do not. However, this is problematic for
 | ||
|  |   // `to` values that do not provide a pathname. `to` can simply be a search or
 | ||
|  |   // hash string, in which case we should assume that the navigation is relative
 | ||
|  |   // to the current location's pathname and *not* the route pathname.
 | ||
|  |   if (isPathRelative || toPathname == null) { | ||
|  |     from = locationPathname; | ||
|  |   } else { | ||
|  |     let routePathnameIndex = routePathnames.length - 1; | ||
|  | 
 | ||
|  |     if (toPathname.startsWith("..")) { | ||
|  |       let toSegments = toPathname.split("/"); | ||
|  | 
 | ||
|  |       // Each leading .. segment means "go up one route" instead of "go up one
 | ||
|  |       // URL segment".  This is a key difference from how <a href> works and a
 | ||
|  |       // major reason we call this a "to" value instead of a "href".
 | ||
|  |       while (toSegments[0] === "..") { | ||
|  |         toSegments.shift(); | ||
|  |         routePathnameIndex -= 1; | ||
|  |       } | ||
|  | 
 | ||
|  |       to.pathname = toSegments.join("/"); | ||
|  |     } | ||
|  | 
 | ||
|  |     // If there are more ".." segments than parent routes, resolve relative to
 | ||
|  |     // the root / URL.
 | ||
|  |     from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex] : "/"; | ||
|  |   } | ||
|  | 
 | ||
|  |   let path = resolvePath(to, from); | ||
|  | 
 | ||
|  |   // Ensure the pathname has a trailing slash if the original "to" had one
 | ||
|  |   let hasExplicitTrailingSlash = | ||
|  |     toPathname && toPathname !== "/" && toPathname.endsWith("/"); | ||
|  |   // Or if this was a link to the current path which has a trailing slash
 | ||
|  |   let hasCurrentTrailingSlash = | ||
|  |     (isEmptyPath || toPathname === ".") && locationPathname.endsWith("/"); | ||
|  |   if ( | ||
|  |     !path.pathname.endsWith("/") && | ||
|  |     (hasExplicitTrailingSlash || hasCurrentTrailingSlash) | ||
|  |   ) { | ||
|  |     path.pathname += "/"; | ||
|  |   } | ||
|  | 
 | ||
|  |   return path; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * @private | ||
|  |  */ | ||
|  | export function getToPathname(to: To): string | undefined { | ||
|  |   // Empty strings should be treated the same as / paths
 | ||
|  |   return to === "" || (to as Path).pathname === "" | ||
|  |     ? "/" | ||
|  |     : typeof to === "string" | ||
|  |     ? parsePath(to).pathname | ||
|  |     : to.pathname; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * @private | ||
|  |  */ | ||
|  | export const joinPaths = (paths: string[]): string => | ||
|  |   paths.join("/").replace(/\/\/+/g, "/"); | ||
|  | 
 | ||
|  | /** | ||
|  |  * @private | ||
|  |  */ | ||
|  | export const normalizePathname = (pathname: string): string => | ||
|  |   pathname.replace(/\/+$/, "").replace(/^\/*/, "/"); | ||
|  | 
 | ||
|  | /** | ||
|  |  * @private | ||
|  |  */ | ||
|  | export const normalizeSearch = (search: string): string => | ||
|  |   !search || search === "?" | ||
|  |     ? "" | ||
|  |     : search.startsWith("?") | ||
|  |     ? search | ||
|  |     : "?" + search; | ||
|  | 
 | ||
|  | /** | ||
|  |  * @private | ||
|  |  */ | ||
|  | export const normalizeHash = (hash: string): string => | ||
|  |   !hash || hash === "#" ? "" : hash.startsWith("#") ? hash : "#" + hash; | ||
|  | 
 | ||
|  | export type JsonFunction = <Data>( | ||
|  |   data: Data, | ||
|  |   init?: number | ResponseInit | ||
|  | ) => Response; | ||
|  | 
 | ||
|  | /** | ||
|  |  * This is a shortcut for creating `application/json` responses. Converts `data` | ||
|  |  * to JSON and sets the `Content-Type` header. | ||
|  |  */ | ||
|  | export const json: JsonFunction = (data, init = {}) => { | ||
|  |   let responseInit = typeof init === "number" ? { status: init } : init; | ||
|  | 
 | ||
|  |   let headers = new Headers(responseInit.headers); | ||
|  |   if (!headers.has("Content-Type")) { | ||
|  |     headers.set("Content-Type", "application/json; charset=utf-8"); | ||
|  |   } | ||
|  | 
 | ||
|  |   return new Response(JSON.stringify(data), { | ||
|  |     ...responseInit, | ||
|  |     headers, | ||
|  |   }); | ||
|  | }; | ||
|  | 
 | ||
|  | export interface TrackedPromise extends Promise<any> { | ||
|  |   _tracked?: boolean; | ||
|  |   _data?: any; | ||
|  |   _error?: any; | ||
|  | } | ||
|  | 
 | ||
|  | export class AbortedDeferredError extends Error {} | ||
|  | 
 | ||
|  | export class DeferredData { | ||
|  |   private pendingKeys: Set<string | number> = new Set<string | number>(); | ||
|  |   private controller: AbortController; | ||
|  |   private abortPromise: Promise<void>; | ||
|  |   private unlistenAbortSignal: () => void; | ||
|  |   private subscriber?: (aborted: boolean) => void = undefined; | ||
|  |   data: Record<string, unknown>; | ||
|  | 
 | ||
|  |   constructor(data: Record<string, unknown>) { | ||
|  |     invariant( | ||
|  |       data && typeof data === "object" && !Array.isArray(data), | ||
|  |       "defer() only accepts plain objects" | ||
|  |     ); | ||
|  | 
 | ||
|  |     // Set up an AbortController + Promise we can race against to exit early
 | ||
|  |     // cancellation
 | ||
|  |     let reject: (e: AbortedDeferredError) => void; | ||
|  |     this.abortPromise = new Promise((_, r) => (reject = r)); | ||
|  |     this.controller = new AbortController(); | ||
|  |     let onAbort = () => | ||
|  |       reject(new AbortedDeferredError("Deferred data aborted")); | ||
|  |     this.unlistenAbortSignal = () => | ||
|  |       this.controller.signal.removeEventListener("abort", onAbort); | ||
|  |     this.controller.signal.addEventListener("abort", onAbort); | ||
|  | 
 | ||
|  |     this.data = Object.entries(data).reduce( | ||
|  |       (acc, [key, value]) => | ||
|  |         Object.assign(acc, { | ||
|  |           [key]: this.trackPromise(key, value), | ||
|  |         }), | ||
|  |       {} | ||
|  |     ); | ||
|  |   } | ||
|  | 
 | ||
|  |   private trackPromise( | ||
|  |     key: string | number, | ||
|  |     value: Promise<unknown> | unknown | ||
|  |   ): TrackedPromise | unknown { | ||
|  |     if (!(value instanceof Promise)) { | ||
|  |       return value; | ||
|  |     } | ||
|  | 
 | ||
|  |     this.pendingKeys.add(key); | ||
|  | 
 | ||
|  |     // We store a little wrapper promise that will be extended with
 | ||
|  |     // _data/_error props upon resolve/reject
 | ||
|  |     let promise: TrackedPromise = Promise.race([value, this.abortPromise]).then( | ||
|  |       (data) => this.onSettle(promise, key, null, data as unknown), | ||
|  |       (error) => this.onSettle(promise, key, error as unknown) | ||
|  |     ); | ||
|  | 
 | ||
|  |     // Register rejection listeners to avoid uncaught promise rejections on
 | ||
|  |     // errors or aborted deferred values
 | ||
|  |     promise.catch(() => {}); | ||
|  | 
 | ||
|  |     Object.defineProperty(promise, "_tracked", { get: () => true }); | ||
|  |     return promise; | ||
|  |   } | ||
|  | 
 | ||
|  |   private onSettle( | ||
|  |     promise: TrackedPromise, | ||
|  |     key: string | number, | ||
|  |     error: unknown, | ||
|  |     data?: unknown | ||
|  |   ): unknown { | ||
|  |     if ( | ||
|  |       this.controller.signal.aborted && | ||
|  |       error instanceof AbortedDeferredError | ||
|  |     ) { | ||
|  |       this.unlistenAbortSignal(); | ||
|  |       Object.defineProperty(promise, "_error", { get: () => error }); | ||
|  |       return Promise.reject(error); | ||
|  |     } | ||
|  | 
 | ||
|  |     this.pendingKeys.delete(key); | ||
|  | 
 | ||
|  |     if (this.done) { | ||
|  |       // Nothing left to abort!
 | ||
|  |       this.unlistenAbortSignal(); | ||
|  |     } | ||
|  | 
 | ||
|  |     const subscriber = this.subscriber; | ||
|  |     if (error) { | ||
|  |       Object.defineProperty(promise, "_error", { get: () => error }); | ||
|  |       subscriber && subscriber(false); | ||
|  |       return Promise.reject(error); | ||
|  |     } | ||
|  | 
 | ||
|  |     Object.defineProperty(promise, "_data", { get: () => data }); | ||
|  |     subscriber && subscriber(false); | ||
|  |     return data; | ||
|  |   } | ||
|  | 
 | ||
|  |   subscribe(fn: (aborted: boolean) => void) { | ||
|  |     this.subscriber = fn; | ||
|  |   } | ||
|  | 
 | ||
|  |   cancel() { | ||
|  |     this.controller.abort(); | ||
|  |     this.pendingKeys.forEach((v, k) => this.pendingKeys.delete(k)); | ||
|  |     let subscriber = this.subscriber; | ||
|  |     subscriber && subscriber(true); | ||
|  |   } | ||
|  | 
 | ||
|  |   async resolveData(signal: AbortSignal) { | ||
|  |     let aborted = false; | ||
|  |     if (!this.done) { | ||
|  |       let onAbort = () => this.cancel(); | ||
|  |       signal.addEventListener("abort", onAbort); | ||
|  |       aborted = await new Promise((resolve) => { | ||
|  |         this.subscribe((aborted) => { | ||
|  |           signal.removeEventListener("abort", onAbort); | ||
|  |           if (aborted || this.done) { | ||
|  |             resolve(aborted); | ||
|  |           } | ||
|  |         }); | ||
|  |       }); | ||
|  |     } | ||
|  |     return aborted; | ||
|  |   } | ||
|  | 
 | ||
|  |   get done() { | ||
|  |     return this.pendingKeys.size === 0; | ||
|  |   } | ||
|  | 
 | ||
|  |   get unwrappedData() { | ||
|  |     invariant( | ||
|  |       this.data !== null && this.done, | ||
|  |       "Can only unwrap data on initialized and settled deferreds" | ||
|  |     ); | ||
|  | 
 | ||
|  |     return Object.entries(this.data).reduce( | ||
|  |       (acc, [key, value]) => | ||
|  |         Object.assign(acc, { | ||
|  |           [key]: unwrapTrackedPromise(value), | ||
|  |         }), | ||
|  |       {} | ||
|  |     ); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function isTrackedPromise(value: any): value is TrackedPromise { | ||
|  |   return ( | ||
|  |     value instanceof Promise && (value as TrackedPromise)._tracked === true | ||
|  |   ); | ||
|  | } | ||
|  | 
 | ||
|  | function unwrapTrackedPromise(value: any) { | ||
|  |   if (!isTrackedPromise(value)) { | ||
|  |     return value; | ||
|  |   } | ||
|  | 
 | ||
|  |   if (value._error) { | ||
|  |     throw value._error; | ||
|  |   } | ||
|  |   return value._data; | ||
|  | } | ||
|  | 
 | ||
|  | export function defer(data: Record<string, unknown>) { | ||
|  |   return new DeferredData(data); | ||
|  | } | ||
|  | 
 | ||
|  | export type RedirectFunction = ( | ||
|  |   url: string, | ||
|  |   init?: number | ResponseInit | ||
|  | ) => Response; | ||
|  | 
 | ||
|  | /** | ||
|  |  * A redirect response. Sets the status code and the `Location` header. | ||
|  |  * Defaults to "302 Found". | ||
|  |  */ | ||
|  | export const redirect: RedirectFunction = (url, init = 302) => { | ||
|  |   let responseInit = init; | ||
|  |   if (typeof responseInit === "number") { | ||
|  |     responseInit = { status: responseInit }; | ||
|  |   } else if (typeof responseInit.status === "undefined") { | ||
|  |     responseInit.status = 302; | ||
|  |   } | ||
|  | 
 | ||
|  |   let headers = new Headers(responseInit.headers); | ||
|  |   headers.set("Location", url); | ||
|  | 
 | ||
|  |   return new Response(null, { | ||
|  |     ...responseInit, | ||
|  |     headers, | ||
|  |   }); | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * @private | ||
|  |  * Utility class we use to hold auto-unwrapped 4xx/5xx Response bodies | ||
|  |  */ | ||
|  | export class ErrorResponse { | ||
|  |   status: number; | ||
|  |   statusText: string; | ||
|  |   data: any; | ||
|  |   error?: Error; | ||
|  |   internal: boolean; | ||
|  | 
 | ||
|  |   constructor( | ||
|  |     status: number, | ||
|  |     statusText: string | undefined, | ||
|  |     data: any, | ||
|  |     internal = false | ||
|  |   ) { | ||
|  |     this.status = status; | ||
|  |     this.statusText = statusText || ""; | ||
|  |     this.internal = internal; | ||
|  |     if (data instanceof Error) { | ||
|  |       this.data = data.toString(); | ||
|  |       this.error = data; | ||
|  |     } else { | ||
|  |       this.data = data; | ||
|  |     } | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Check if the given error is an ErrorResponse generated from a 4xx/5xx | ||
|  |  * Response throw from an action/loader | ||
|  |  */ | ||
|  | export function isRouteErrorResponse(e: any): e is ErrorResponse { | ||
|  |   return e instanceof ErrorResponse; | ||
|  | } |