utils.ts 48 KB

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