utils.ts 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413
  1. import type { Location, Path, To } from "./history";
  2. import { invariant, parsePath } 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: any;
  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. status: number;
  39. location: string;
  40. revalidate: boolean;
  41. }
  42. /**
  43. * Unsuccessful result from a loader or action
  44. */
  45. export interface ErrorResult {
  46. type: ResultType.error;
  47. error: any;
  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. export type MutationFormMethod = "post" | "put" | "patch" | "delete";
  59. export type FormMethod = "get" | MutationFormMethod;
  60. export type FormEncType =
  61. | "application/x-www-form-urlencoded"
  62. | "multipart/form-data";
  63. /**
  64. * @private
  65. * Internal interface to pass around for action submissions, not intended for
  66. * external consumption
  67. */
  68. export interface Submission {
  69. formMethod: FormMethod;
  70. formAction: string;
  71. formEncType: FormEncType;
  72. formData: FormData;
  73. }
  74. /**
  75. * @private
  76. * Arguments passed to route loader/action functions. Same for now but we keep
  77. * this as a private implementation detail in case they diverge in the future.
  78. */
  79. interface DataFunctionArgs {
  80. request: Request;
  81. params: Params;
  82. context?: any;
  83. }
  84. /**
  85. * Arguments passed to loader functions
  86. */
  87. export interface LoaderFunctionArgs extends DataFunctionArgs {}
  88. /**
  89. * Arguments passed to action functions
  90. */
  91. export interface ActionFunctionArgs extends DataFunctionArgs {}
  92. /**
  93. * Route loader function signature
  94. */
  95. export interface LoaderFunction {
  96. (args: LoaderFunctionArgs): Promise<Response> | Response | Promise<any> | any;
  97. }
  98. /**
  99. * Route action function signature
  100. */
  101. export interface ActionFunction {
  102. (args: ActionFunctionArgs): Promise<Response> | Response | Promise<any> | any;
  103. }
  104. /**
  105. * Route shouldRevalidate function signature. This runs after any submission
  106. * (navigation or fetcher), so we flatten the navigation/fetcher submission
  107. * onto the arguments. It shouldn't matter whether it came from a navigation
  108. * or a fetcher, what really matters is the URLs and the formData since loaders
  109. * have to re-run based on the data models that were potentially mutated.
  110. */
  111. export interface ShouldRevalidateFunction {
  112. (args: {
  113. currentUrl: URL;
  114. currentParams: AgnosticDataRouteMatch["params"];
  115. nextUrl: URL;
  116. nextParams: AgnosticDataRouteMatch["params"];
  117. formMethod?: Submission["formMethod"];
  118. formAction?: Submission["formAction"];
  119. formEncType?: Submission["formEncType"];
  120. formData?: Submission["formData"];
  121. actionResult?: DataResult;
  122. defaultShouldRevalidate: boolean;
  123. }): boolean;
  124. }
  125. /**
  126. * Base RouteObject with common props shared by all types of routes
  127. */
  128. type AgnosticBaseRouteObject = {
  129. caseSensitive?: boolean;
  130. path?: string;
  131. id?: string;
  132. loader?: LoaderFunction;
  133. action?: ActionFunction;
  134. hasErrorBoundary?: boolean;
  135. shouldRevalidate?: ShouldRevalidateFunction;
  136. handle?: any;
  137. };
  138. /**
  139. * Index routes must not have children
  140. */
  141. export type AgnosticIndexRouteObject = AgnosticBaseRouteObject & {
  142. children?: undefined;
  143. index: true;
  144. };
  145. /**
  146. * Non-index routes may have children, but cannot have index
  147. */
  148. export type AgnosticNonIndexRouteObject = AgnosticBaseRouteObject & {
  149. children?: AgnosticRouteObject[];
  150. index?: false;
  151. };
  152. /**
  153. * A route object represents a logical route, with (optionally) its child
  154. * routes organized in a tree-like structure.
  155. */
  156. export type AgnosticRouteObject =
  157. | AgnosticIndexRouteObject
  158. | AgnosticNonIndexRouteObject;
  159. export type AgnosticDataIndexRouteObject = AgnosticIndexRouteObject & {
  160. id: string;
  161. };
  162. export type AgnosticDataNonIndexRouteObject = AgnosticNonIndexRouteObject & {
  163. children?: AgnosticDataRouteObject[];
  164. id: string;
  165. };
  166. /**
  167. * A data route object, which is just a RouteObject with a required unique ID
  168. */
  169. export type AgnosticDataRouteObject =
  170. | AgnosticDataIndexRouteObject
  171. | AgnosticDataNonIndexRouteObject;
  172. // Recursive helper for finding path parameters in the absence of wildcards
  173. type _PathParam<Path extends string> =
  174. // split path into individual path segments
  175. Path extends `${infer L}/${infer R}`
  176. ? _PathParam<L> | _PathParam<R>
  177. : // find params after `:`
  178. Path extends `:${infer Param}`
  179. ? Param extends `${infer Optional}?`
  180. ? Optional
  181. : Param
  182. : // otherwise, there aren't any params present
  183. never;
  184. /**
  185. * Examples:
  186. * "/a/b/*" -> "*"
  187. * ":a" -> "a"
  188. * "/a/:b" -> "b"
  189. * "/a/blahblahblah:b" -> "b"
  190. * "/:a/:b" -> "a" | "b"
  191. * "/:a/b/:c/*" -> "a" | "c" | "*"
  192. */
  193. type PathParam<Path extends string> =
  194. // check if path is just a wildcard
  195. Path extends "*"
  196. ? "*"
  197. : // look for wildcard at the end of the path
  198. Path extends `${infer Rest}/*`
  199. ? "*" | _PathParam<Rest>
  200. : // look for params in the absence of wildcards
  201. _PathParam<Path>;
  202. // Attempt to parse the given string segment. If it fails, then just return the
  203. // plain string type as a default fallback. Otherwise return the union of the
  204. // parsed string literals that were referenced as dynamic segments in the route.
  205. export type ParamParseKey<Segment extends string> =
  206. // if could not find path params, fallback to `string`
  207. [PathParam<Segment>] extends [never] ? string : PathParam<Segment>;
  208. /**
  209. * The parameters that were parsed from the URL path.
  210. */
  211. export type Params<Key extends string = string> = {
  212. readonly [key in Key]: string | undefined;
  213. };
  214. /**
  215. * A RouteMatch contains info about how a route matched a URL.
  216. */
  217. export interface AgnosticRouteMatch<
  218. ParamKey extends string = string,
  219. RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject
  220. > {
  221. /**
  222. * The names and values of dynamic parameters in the URL.
  223. */
  224. params: Params<ParamKey>;
  225. /**
  226. * The portion of the URL pathname that was matched.
  227. */
  228. pathname: string;
  229. /**
  230. * The portion of the URL pathname that was matched before child routes.
  231. */
  232. pathnameBase: string;
  233. /**
  234. * The route object that was used to match.
  235. */
  236. route: RouteObjectType;
  237. }
  238. export interface AgnosticDataRouteMatch
  239. extends AgnosticRouteMatch<string, AgnosticDataRouteObject> {}
  240. function isIndexRoute(
  241. route: AgnosticRouteObject
  242. ): route is AgnosticIndexRouteObject {
  243. return route.index === true;
  244. }
  245. // Walk the route tree generating unique IDs where necessary so we are working
  246. // solely with AgnosticDataRouteObject's within the Router
  247. export function convertRoutesToDataRoutes(
  248. routes: AgnosticRouteObject[],
  249. parentPath: number[] = [],
  250. allIds: Set<string> = new Set<string>()
  251. ): AgnosticDataRouteObject[] {
  252. return routes.map((route, index) => {
  253. let treePath = [...parentPath, index];
  254. let id = typeof route.id === "string" ? route.id : treePath.join("-");
  255. invariant(
  256. route.index !== true || !route.children,
  257. `Cannot specify children on an index route`
  258. );
  259. invariant(
  260. !allIds.has(id),
  261. `Found a route id collision on id "${id}". Route ` +
  262. "id's must be globally unique within Data Router usages"
  263. );
  264. allIds.add(id);
  265. if (isIndexRoute(route)) {
  266. let indexRoute: AgnosticDataIndexRouteObject = { ...route, id };
  267. return indexRoute;
  268. } else {
  269. let pathOrLayoutRoute: AgnosticDataNonIndexRouteObject = {
  270. ...route,
  271. id,
  272. children: route.children
  273. ? convertRoutesToDataRoutes(route.children, treePath, allIds)
  274. : undefined,
  275. };
  276. return pathOrLayoutRoute;
  277. }
  278. });
  279. }
  280. /**
  281. * Matches the given routes to a location and returns the match data.
  282. *
  283. * @see https://reactrouter.com/utils/match-routes
  284. */
  285. export function matchRoutes<
  286. RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject
  287. >(
  288. routes: RouteObjectType[],
  289. locationArg: Partial<Location> | string,
  290. basename = "/"
  291. ): AgnosticRouteMatch<string, RouteObjectType>[] | null {
  292. let location =
  293. typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
  294. let pathname = stripBasename(location.pathname || "/", basename);
  295. if (pathname == null) {
  296. return null;
  297. }
  298. let branches = flattenRoutes(routes);
  299. rankRouteBranches(branches);
  300. let matches = null;
  301. for (let i = 0; matches == null && i < branches.length; ++i) {
  302. matches = matchRouteBranch<string, RouteObjectType>(
  303. branches[i],
  304. // Incoming pathnames are generally encoded from either window.location
  305. // or from router.navigate, but we want to match against the unencoded
  306. // paths in the route definitions. Memory router locations won't be
  307. // encoded here but there also shouldn't be anything to decode so this
  308. // should be a safe operation. This avoids needing matchRoutes to be
  309. // history-aware.
  310. safelyDecodeURI(pathname)
  311. );
  312. }
  313. return matches;
  314. }
  315. interface RouteMeta<
  316. RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject
  317. > {
  318. relativePath: string;
  319. caseSensitive: boolean;
  320. childrenIndex: number;
  321. route: RouteObjectType;
  322. }
  323. interface RouteBranch<
  324. RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject
  325. > {
  326. path: string;
  327. score: number;
  328. routesMeta: RouteMeta<RouteObjectType>[];
  329. }
  330. function flattenRoutes<
  331. RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject
  332. >(
  333. routes: RouteObjectType[],
  334. branches: RouteBranch<RouteObjectType>[] = [],
  335. parentsMeta: RouteMeta<RouteObjectType>[] = [],
  336. parentPath = ""
  337. ): RouteBranch<RouteObjectType>[] {
  338. let flattenRoute = (
  339. route: RouteObjectType,
  340. index: number,
  341. relativePath?: string
  342. ) => {
  343. let meta: RouteMeta<RouteObjectType> = {
  344. relativePath:
  345. relativePath === undefined ? route.path || "" : relativePath,
  346. caseSensitive: route.caseSensitive === true,
  347. childrenIndex: index,
  348. route,
  349. };
  350. if (meta.relativePath.startsWith("/")) {
  351. invariant(
  352. meta.relativePath.startsWith(parentPath),
  353. `Absolute route path "${meta.relativePath}" nested under path ` +
  354. `"${parentPath}" is not valid. An absolute child route path ` +
  355. `must start with the combined path of all its parent routes.`
  356. );
  357. meta.relativePath = meta.relativePath.slice(parentPath.length);
  358. }
  359. let path = joinPaths([parentPath, meta.relativePath]);
  360. let routesMeta = parentsMeta.concat(meta);
  361. // Add the children before adding this route to the array so we traverse the
  362. // route tree depth-first and child routes appear before their parents in
  363. // the "flattened" version.
  364. if (route.children && route.children.length > 0) {
  365. invariant(
  366. // Our types know better, but runtime JS may not!
  367. // @ts-expect-error
  368. route.index !== true,
  369. `Index routes must not have child routes. Please remove ` +
  370. `all child routes from route path "${path}".`
  371. );
  372. flattenRoutes(route.children, branches, routesMeta, path);
  373. }
  374. // Routes without a path shouldn't ever match by themselves unless they are
  375. // index routes, so don't add them to the list of possible branches.
  376. if (route.path == null && !route.index) {
  377. return;
  378. }
  379. branches.push({
  380. path,
  381. score: computeScore(path, route.index),
  382. routesMeta,
  383. });
  384. };
  385. routes.forEach((route, index) => {
  386. // coarse-grain check for optional params
  387. if (route.path === "" || !route.path?.includes("?")) {
  388. flattenRoute(route, index);
  389. } else {
  390. for (let exploded of explodeOptionalSegments(route.path)) {
  391. flattenRoute(route, index, exploded);
  392. }
  393. }
  394. });
  395. return branches;
  396. }
  397. /**
  398. * Computes all combinations of optional path segments for a given path,
  399. * excluding combinations that are ambiguous and of lower priority.
  400. *
  401. * For example, `/one/:two?/three/:four?/:five?` explodes to:
  402. * - `/one/three`
  403. * - `/one/:two/three`
  404. * - `/one/three/:four`
  405. * - `/one/three/:five`
  406. * - `/one/:two/three/:four`
  407. * - `/one/:two/three/:five`
  408. * - `/one/three/:four/:five`
  409. * - `/one/:two/three/:four/:five`
  410. */
  411. function explodeOptionalSegments(path: string): string[] {
  412. let segments = path.split("/");
  413. if (segments.length === 0) return [];
  414. let [first, ...rest] = segments;
  415. // Optional path segments are denoted by a trailing `?`
  416. let isOptional = first.endsWith("?");
  417. // Compute the corresponding required segment: `foo?` -> `foo`
  418. let required = first.replace(/\?$/, "");
  419. if (rest.length === 0) {
  420. // Intepret empty string as omitting an optional segment
  421. // `["one", "", "three"]` corresponds to omitting `:two` from `/one/:two?/three` -> `/one/three`
  422. return isOptional ? [required, ""] : [required];
  423. }
  424. let restExploded = explodeOptionalSegments(rest.join("/"));
  425. let result: string[] = [];
  426. // All child paths with the prefix. Do this for all children before the
  427. // optional version for all children so we get consistent ordering where the
  428. // parent optional aspect is preferred as required. Otherwise, we can get
  429. // child sections interspersed where deeper optional segments are higher than
  430. // parent optional segments, where for example, /:two would explodes _earlier_
  431. // then /:one. By always including the parent as required _for all children_
  432. // first, we avoid this issue
  433. result.push(
  434. ...restExploded.map((subpath) =>
  435. subpath === "" ? required : [required, subpath].join("/")
  436. )
  437. );
  438. // Then if this is an optional value, add all child versions without
  439. if (isOptional) {
  440. result.push(...restExploded);
  441. }
  442. // for absolute paths, ensure `/` instead of empty segment
  443. return result.map((exploded) =>
  444. path.startsWith("/") && exploded === "" ? "/" : exploded
  445. );
  446. }
  447. function rankRouteBranches(branches: RouteBranch[]): void {
  448. branches.sort((a, b) =>
  449. a.score !== b.score
  450. ? b.score - a.score // Higher score first
  451. : compareIndexes(
  452. a.routesMeta.map((meta) => meta.childrenIndex),
  453. b.routesMeta.map((meta) => meta.childrenIndex)
  454. )
  455. );
  456. }
  457. const paramRe = /^:\w+$/;
  458. const dynamicSegmentValue = 3;
  459. const indexRouteValue = 2;
  460. const emptySegmentValue = 1;
  461. const staticSegmentValue = 10;
  462. const splatPenalty = -2;
  463. const isSplat = (s: string) => s === "*";
  464. function computeScore(path: string, index: boolean | undefined): number {
  465. let segments = path.split("/");
  466. let initialScore = segments.length;
  467. if (segments.some(isSplat)) {
  468. initialScore += splatPenalty;
  469. }
  470. if (index) {
  471. initialScore += indexRouteValue;
  472. }
  473. return segments
  474. .filter((s) => !isSplat(s))
  475. .reduce(
  476. (score, segment) =>
  477. score +
  478. (paramRe.test(segment)
  479. ? dynamicSegmentValue
  480. : segment === ""
  481. ? emptySegmentValue
  482. : staticSegmentValue),
  483. initialScore
  484. );
  485. }
  486. function compareIndexes(a: number[], b: number[]): number {
  487. let siblings =
  488. a.length === b.length && a.slice(0, -1).every((n, i) => n === b[i]);
  489. return siblings
  490. ? // If two routes are siblings, we should try to match the earlier sibling
  491. // first. This allows people to have fine-grained control over the matching
  492. // behavior by simply putting routes with identical paths in the order they
  493. // want them tried.
  494. a[a.length - 1] - b[b.length - 1]
  495. : // Otherwise, it doesn't really make sense to rank non-siblings by index,
  496. // so they sort equally.
  497. 0;
  498. }
  499. function matchRouteBranch<
  500. ParamKey extends string = string,
  501. RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject
  502. >(
  503. branch: RouteBranch<RouteObjectType>,
  504. pathname: string
  505. ): AgnosticRouteMatch<ParamKey, RouteObjectType>[] | null {
  506. let { routesMeta } = branch;
  507. let matchedParams = {};
  508. let matchedPathname = "/";
  509. let matches: AgnosticRouteMatch<ParamKey, RouteObjectType>[] = [];
  510. for (let i = 0; i < routesMeta.length; ++i) {
  511. let meta = routesMeta[i];
  512. let end = i === routesMeta.length - 1;
  513. let remainingPathname =
  514. matchedPathname === "/"
  515. ? pathname
  516. : pathname.slice(matchedPathname.length) || "/";
  517. let match = matchPath(
  518. { path: meta.relativePath, caseSensitive: meta.caseSensitive, end },
  519. remainingPathname
  520. );
  521. if (!match) return null;
  522. Object.assign(matchedParams, match.params);
  523. let route = meta.route;
  524. matches.push({
  525. // TODO: Can this as be avoided?
  526. params: matchedParams as Params<ParamKey>,
  527. pathname: joinPaths([matchedPathname, match.pathname]),
  528. pathnameBase: normalizePathname(
  529. joinPaths([matchedPathname, match.pathnameBase])
  530. ),
  531. route,
  532. });
  533. if (match.pathnameBase !== "/") {
  534. matchedPathname = joinPaths([matchedPathname, match.pathnameBase]);
  535. }
  536. }
  537. return matches;
  538. }
  539. /**
  540. * Returns a path with params interpolated.
  541. *
  542. * @see https://reactrouter.com/utils/generate-path
  543. */
  544. export function generatePath<Path extends string>(
  545. originalPath: Path,
  546. params: {
  547. [key in PathParam<Path>]: string | null;
  548. } = {} as any
  549. ): string {
  550. let path = originalPath;
  551. if (path.endsWith("*") && path !== "*" && !path.endsWith("/*")) {
  552. warning(
  553. false,
  554. `Route path "${path}" will be treated as if it were ` +
  555. `"${path.replace(/\*$/, "/*")}" because the \`*\` character must ` +
  556. `always follow a \`/\` in the pattern. To get rid of this warning, ` +
  557. `please change the route path to "${path.replace(/\*$/, "/*")}".`
  558. );
  559. path = path.replace(/\*$/, "/*") as Path;
  560. }
  561. return (
  562. path
  563. .replace(
  564. /^:(\w+)(\??)/g,
  565. (_, key: PathParam<Path>, optional: string | undefined) => {
  566. let param = params[key];
  567. if (optional === "?") {
  568. return param == null ? "" : param;
  569. }
  570. if (param == null) {
  571. invariant(false, `Missing ":${key}" param`);
  572. }
  573. return param;
  574. }
  575. )
  576. .replace(
  577. /\/:(\w+)(\??)/g,
  578. (_, key: PathParam<Path>, optional: string | undefined) => {
  579. let param = params[key];
  580. if (optional === "?") {
  581. return param == null ? "" : `/${param}`;
  582. }
  583. if (param == null) {
  584. invariant(false, `Missing ":${key}" param`);
  585. }
  586. return `/${param}`;
  587. }
  588. )
  589. // Remove any optional markers from optional static segments
  590. .replace(/\?/g, "")
  591. .replace(/(\/?)\*/, (_, prefix, __, str) => {
  592. const star = "*" as PathParam<Path>;
  593. if (params[star] == null) {
  594. // If no splat was provided, trim the trailing slash _unless_ it's
  595. // the entire path
  596. return str === "/*" ? "/" : "";
  597. }
  598. // Apply the splat
  599. return `${prefix}${params[star]}`;
  600. })
  601. );
  602. }
  603. /**
  604. * A PathPattern is used to match on some portion of a URL pathname.
  605. */
  606. export interface PathPattern<Path extends string = string> {
  607. /**
  608. * A string to match against a URL pathname. May contain `:id`-style segments
  609. * to indicate placeholders for dynamic parameters. May also end with `/*` to
  610. * indicate matching the rest of the URL pathname.
  611. */
  612. path: Path;
  613. /**
  614. * Should be `true` if the static portions of the `path` should be matched in
  615. * the same case.
  616. */
  617. caseSensitive?: boolean;
  618. /**
  619. * Should be `true` if this pattern should match the entire URL pathname.
  620. */
  621. end?: boolean;
  622. }
  623. /**
  624. * A PathMatch contains info about how a PathPattern matched on a URL pathname.
  625. */
  626. export interface PathMatch<ParamKey extends string = string> {
  627. /**
  628. * The names and values of dynamic parameters in the URL.
  629. */
  630. params: Params<ParamKey>;
  631. /**
  632. * The portion of the URL pathname that was matched.
  633. */
  634. pathname: string;
  635. /**
  636. * The portion of the URL pathname that was matched before child routes.
  637. */
  638. pathnameBase: string;
  639. /**
  640. * The pattern that was used to match.
  641. */
  642. pattern: PathPattern;
  643. }
  644. type Mutable<T> = {
  645. -readonly [P in keyof T]: T[P];
  646. };
  647. /**
  648. * Performs pattern matching on a URL pathname and returns information about
  649. * the match.
  650. *
  651. * @see https://reactrouter.com/utils/match-path
  652. */
  653. export function matchPath<
  654. ParamKey extends ParamParseKey<Path>,
  655. Path extends string
  656. >(
  657. pattern: PathPattern<Path> | Path,
  658. pathname: string
  659. ): PathMatch<ParamKey> | null {
  660. if (typeof pattern === "string") {
  661. pattern = { path: pattern, caseSensitive: false, end: true };
  662. }
  663. let [matcher, paramNames] = compilePath(
  664. pattern.path,
  665. pattern.caseSensitive,
  666. pattern.end
  667. );
  668. let match = pathname.match(matcher);
  669. if (!match) return null;
  670. let matchedPathname = match[0];
  671. let pathnameBase = matchedPathname.replace(/(.)\/+$/, "$1");
  672. let captureGroups = match.slice(1);
  673. let params: Params = paramNames.reduce<Mutable<Params>>(
  674. (memo, paramName, index) => {
  675. // We need to compute the pathnameBase here using the raw splat value
  676. // instead of using params["*"] later because it will be decoded then
  677. if (paramName === "*") {
  678. let splatValue = captureGroups[index] || "";
  679. pathnameBase = matchedPathname
  680. .slice(0, matchedPathname.length - splatValue.length)
  681. .replace(/(.)\/+$/, "$1");
  682. }
  683. memo[paramName] = safelyDecodeURIComponent(
  684. captureGroups[index] || "",
  685. paramName
  686. );
  687. return memo;
  688. },
  689. {}
  690. );
  691. return {
  692. params,
  693. pathname: matchedPathname,
  694. pathnameBase,
  695. pattern,
  696. };
  697. }
  698. function compilePath(
  699. path: string,
  700. caseSensitive = false,
  701. end = true
  702. ): [RegExp, string[]] {
  703. warning(
  704. path === "*" || !path.endsWith("*") || path.endsWith("/*"),
  705. `Route path "${path}" will be treated as if it were ` +
  706. `"${path.replace(/\*$/, "/*")}" because the \`*\` character must ` +
  707. `always follow a \`/\` in the pattern. To get rid of this warning, ` +
  708. `please change the route path to "${path.replace(/\*$/, "/*")}".`
  709. );
  710. let paramNames: string[] = [];
  711. let regexpSource =
  712. "^" +
  713. path
  714. .replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
  715. .replace(/^\/*/, "/") // Make sure it has a leading /
  716. .replace(/[\\.*+^$?{}|()[\]]/g, "\\$&") // Escape special regex chars
  717. .replace(/\/:(\w+)/g, (_: string, paramName: string) => {
  718. paramNames.push(paramName);
  719. return "/([^\\/]+)";
  720. });
  721. if (path.endsWith("*")) {
  722. paramNames.push("*");
  723. regexpSource +=
  724. path === "*" || path === "/*"
  725. ? "(.*)$" // Already matched the initial /, just match the rest
  726. : "(?:\\/(.+)|\\/*)$"; // Don't include the / in params["*"]
  727. } else if (end) {
  728. // When matching to the end, ignore trailing slashes
  729. regexpSource += "\\/*$";
  730. } else if (path !== "" && path !== "/") {
  731. // If our path is non-empty and contains anything beyond an initial slash,
  732. // then we have _some_ form of path in our regex so we should expect to
  733. // match only if we find the end of this path segment. Look for an optional
  734. // non-captured trailing slash (to match a portion of the URL) or the end
  735. // of the path (if we've matched to the end). We used to do this with a
  736. // word boundary but that gives false positives on routes like
  737. // /user-preferences since `-` counts as a word boundary.
  738. regexpSource += "(?:(?=\\/|$))";
  739. } else {
  740. // Nothing to match for "" or "/"
  741. }
  742. let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");
  743. return [matcher, paramNames];
  744. }
  745. function safelyDecodeURI(value: string) {
  746. try {
  747. return decodeURI(value);
  748. } catch (error) {
  749. warning(
  750. false,
  751. `The URL path "${value}" could not be decoded because it is is a ` +
  752. `malformed URL segment. This is probably due to a bad percent ` +
  753. `encoding (${error}).`
  754. );
  755. return value;
  756. }
  757. }
  758. function safelyDecodeURIComponent(value: string, paramName: string) {
  759. try {
  760. return decodeURIComponent(value);
  761. } catch (error) {
  762. warning(
  763. false,
  764. `The value for the URL param "${paramName}" will not be decoded because` +
  765. ` the string "${value}" is a malformed URL segment. This is probably` +
  766. ` due to a bad percent encoding (${error}).`
  767. );
  768. return value;
  769. }
  770. }
  771. /**
  772. * @private
  773. */
  774. export function stripBasename(
  775. pathname: string,
  776. basename: string
  777. ): string | null {
  778. if (basename === "/") return pathname;
  779. if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) {
  780. return null;
  781. }
  782. // We want to leave trailing slash behavior in the user's control, so if they
  783. // specify a basename with a trailing slash, we should support it
  784. let startIndex = basename.endsWith("/")
  785. ? basename.length - 1
  786. : basename.length;
  787. let nextChar = pathname.charAt(startIndex);
  788. if (nextChar && nextChar !== "/") {
  789. // pathname does not start with basename/
  790. return null;
  791. }
  792. return pathname.slice(startIndex) || "/";
  793. }
  794. /**
  795. * @private
  796. */
  797. export function warning(cond: any, message: string): void {
  798. if (!cond) {
  799. // eslint-disable-next-line no-console
  800. if (typeof console !== "undefined") console.warn(message);
  801. try {
  802. // Welcome to debugging @remix-run/router!
  803. //
  804. // This error is thrown as a convenience so you can more easily
  805. // find the source for a warning that appears in the console by
  806. // enabling "pause on exceptions" in your JavaScript debugger.
  807. throw new Error(message);
  808. // eslint-disable-next-line no-empty
  809. } catch (e) {}
  810. }
  811. }
  812. /**
  813. * Returns a resolved path object relative to the given pathname.
  814. *
  815. * @see https://reactrouter.com/utils/resolve-path
  816. */
  817. export function resolvePath(to: To, fromPathname = "/"): Path {
  818. let {
  819. pathname: toPathname,
  820. search = "",
  821. hash = "",
  822. } = typeof to === "string" ? parsePath(to) : to;
  823. let pathname = toPathname
  824. ? toPathname.startsWith("/")
  825. ? toPathname
  826. : resolvePathname(toPathname, fromPathname)
  827. : fromPathname;
  828. return {
  829. pathname,
  830. search: normalizeSearch(search),
  831. hash: normalizeHash(hash),
  832. };
  833. }
  834. function resolvePathname(relativePath: string, fromPathname: string): string {
  835. let segments = fromPathname.replace(/\/+$/, "").split("/");
  836. let relativeSegments = relativePath.split("/");
  837. relativeSegments.forEach((segment) => {
  838. if (segment === "..") {
  839. // Keep the root "" segment so the pathname starts at /
  840. if (segments.length > 1) segments.pop();
  841. } else if (segment !== ".") {
  842. segments.push(segment);
  843. }
  844. });
  845. return segments.length > 1 ? segments.join("/") : "/";
  846. }
  847. function getInvalidPathError(
  848. char: string,
  849. field: string,
  850. dest: string,
  851. path: Partial<Path>
  852. ) {
  853. return (
  854. `Cannot include a '${char}' character in a manually specified ` +
  855. `\`to.${field}\` field [${JSON.stringify(
  856. path
  857. )}]. Please separate it out to the ` +
  858. `\`to.${dest}\` field. Alternatively you may provide the full path as ` +
  859. `a string in <Link to="..."> and the router will parse it for you.`
  860. );
  861. }
  862. /**
  863. * @private
  864. *
  865. * When processing relative navigation we want to ignore ancestor routes that
  866. * do not contribute to the path, such that index/pathless layout routes don't
  867. * interfere.
  868. *
  869. * For example, when moving a route element into an index route and/or a
  870. * pathless layout route, relative link behavior contained within should stay
  871. * the same. Both of the following examples should link back to the root:
  872. *
  873. * <Route path="/">
  874. * <Route path="accounts" element={<Link to=".."}>
  875. * </Route>
  876. *
  877. * <Route path="/">
  878. * <Route path="accounts">
  879. * <Route element={<AccountsLayout />}> // <-- Does not contribute
  880. * <Route index element={<Link to=".."} /> // <-- Does not contribute
  881. * </Route
  882. * </Route>
  883. * </Route>
  884. */
  885. export function getPathContributingMatches<
  886. T extends AgnosticRouteMatch = AgnosticRouteMatch
  887. >(matches: T[]) {
  888. return matches.filter(
  889. (match, index) =>
  890. index === 0 || (match.route.path && match.route.path.length > 0)
  891. );
  892. }
  893. /**
  894. * @private
  895. */
  896. export function resolveTo(
  897. toArg: To,
  898. routePathnames: string[],
  899. locationPathname: string,
  900. isPathRelative = false
  901. ): Path {
  902. let to: Partial<Path>;
  903. if (typeof toArg === "string") {
  904. to = parsePath(toArg);
  905. } else {
  906. to = { ...toArg };
  907. invariant(
  908. !to.pathname || !to.pathname.includes("?"),
  909. getInvalidPathError("?", "pathname", "search", to)
  910. );
  911. invariant(
  912. !to.pathname || !to.pathname.includes("#"),
  913. getInvalidPathError("#", "pathname", "hash", to)
  914. );
  915. invariant(
  916. !to.search || !to.search.includes("#"),
  917. getInvalidPathError("#", "search", "hash", to)
  918. );
  919. }
  920. let isEmptyPath = toArg === "" || to.pathname === "";
  921. let toPathname = isEmptyPath ? "/" : to.pathname;
  922. let from: string;
  923. // Routing is relative to the current pathname if explicitly requested.
  924. //
  925. // If a pathname is explicitly provided in `to`, it should be relative to the
  926. // route context. This is explained in `Note on `<Link to>` values` in our
  927. // migration guide from v5 as a means of disambiguation between `to` values
  928. // that begin with `/` and those that do not. However, this is problematic for
  929. // `to` values that do not provide a pathname. `to` can simply be a search or
  930. // hash string, in which case we should assume that the navigation is relative
  931. // to the current location's pathname and *not* the route pathname.
  932. if (isPathRelative || toPathname == null) {
  933. from = locationPathname;
  934. } else {
  935. let routePathnameIndex = routePathnames.length - 1;
  936. if (toPathname.startsWith("..")) {
  937. let toSegments = toPathname.split("/");
  938. // Each leading .. segment means "go up one route" instead of "go up one
  939. // URL segment". This is a key difference from how <a href> works and a
  940. // major reason we call this a "to" value instead of a "href".
  941. while (toSegments[0] === "..") {
  942. toSegments.shift();
  943. routePathnameIndex -= 1;
  944. }
  945. to.pathname = toSegments.join("/");
  946. }
  947. // If there are more ".." segments than parent routes, resolve relative to
  948. // the root / URL.
  949. from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex] : "/";
  950. }
  951. let path = resolvePath(to, from);
  952. // Ensure the pathname has a trailing slash if the original "to" had one
  953. let hasExplicitTrailingSlash =
  954. toPathname && toPathname !== "/" && toPathname.endsWith("/");
  955. // Or if this was a link to the current path which has a trailing slash
  956. let hasCurrentTrailingSlash =
  957. (isEmptyPath || toPathname === ".") && locationPathname.endsWith("/");
  958. if (
  959. !path.pathname.endsWith("/") &&
  960. (hasExplicitTrailingSlash || hasCurrentTrailingSlash)
  961. ) {
  962. path.pathname += "/";
  963. }
  964. return path;
  965. }
  966. /**
  967. * @private
  968. */
  969. export function getToPathname(to: To): string | undefined {
  970. // Empty strings should be treated the same as / paths
  971. return to === "" || (to as Path).pathname === ""
  972. ? "/"
  973. : typeof to === "string"
  974. ? parsePath(to).pathname
  975. : to.pathname;
  976. }
  977. /**
  978. * @private
  979. */
  980. export const joinPaths = (paths: string[]): string =>
  981. paths.join("/").replace(/\/\/+/g, "/");
  982. /**
  983. * @private
  984. */
  985. export const normalizePathname = (pathname: string): string =>
  986. pathname.replace(/\/+$/, "").replace(/^\/*/, "/");
  987. /**
  988. * @private
  989. */
  990. export const normalizeSearch = (search: string): string =>
  991. !search || search === "?"
  992. ? ""
  993. : search.startsWith("?")
  994. ? search
  995. : "?" + search;
  996. /**
  997. * @private
  998. */
  999. export const normalizeHash = (hash: string): string =>
  1000. !hash || hash === "#" ? "" : hash.startsWith("#") ? hash : "#" + hash;
  1001. export type JsonFunction = <Data>(
  1002. data: Data,
  1003. init?: number | ResponseInit
  1004. ) => Response;
  1005. /**
  1006. * This is a shortcut for creating `application/json` responses. Converts `data`
  1007. * to JSON and sets the `Content-Type` header.
  1008. */
  1009. export const json: JsonFunction = (data, init = {}) => {
  1010. let responseInit = typeof init === "number" ? { status: init } : init;
  1011. let headers = new Headers(responseInit.headers);
  1012. if (!headers.has("Content-Type")) {
  1013. headers.set("Content-Type", "application/json; charset=utf-8");
  1014. }
  1015. return new Response(JSON.stringify(data), {
  1016. ...responseInit,
  1017. headers,
  1018. });
  1019. };
  1020. export interface TrackedPromise extends Promise<any> {
  1021. _tracked?: boolean;
  1022. _data?: any;
  1023. _error?: any;
  1024. }
  1025. export class AbortedDeferredError extends Error {}
  1026. export class DeferredData {
  1027. private pendingKeysSet: Set<string> = new Set<string>();
  1028. private controller: AbortController;
  1029. private abortPromise: Promise<void>;
  1030. private unlistenAbortSignal: () => void;
  1031. private subscribers: Set<(aborted: boolean, settledKey?: string) => void> =
  1032. new Set();
  1033. data: Record<string, unknown>;
  1034. init?: ResponseInit;
  1035. deferredKeys: string[] = [];
  1036. constructor(data: Record<string, unknown>, responseInit?: ResponseInit) {
  1037. invariant(
  1038. data && typeof data === "object" && !Array.isArray(data),
  1039. "defer() only accepts plain objects"
  1040. );
  1041. // Set up an AbortController + Promise we can race against to exit early
  1042. // cancellation
  1043. let reject: (e: AbortedDeferredError) => void;
  1044. this.abortPromise = new Promise((_, r) => (reject = r));
  1045. this.controller = new AbortController();
  1046. let onAbort = () =>
  1047. reject(new AbortedDeferredError("Deferred data aborted"));
  1048. this.unlistenAbortSignal = () =>
  1049. this.controller.signal.removeEventListener("abort", onAbort);
  1050. this.controller.signal.addEventListener("abort", onAbort);
  1051. this.data = Object.entries(data).reduce(
  1052. (acc, [key, value]) =>
  1053. Object.assign(acc, {
  1054. [key]: this.trackPromise(key, value),
  1055. }),
  1056. {}
  1057. );
  1058. if (this.done) {
  1059. // All incoming values were resolved
  1060. this.unlistenAbortSignal();
  1061. }
  1062. this.init = responseInit;
  1063. }
  1064. private trackPromise(
  1065. key: string,
  1066. value: Promise<unknown> | unknown
  1067. ): TrackedPromise | unknown {
  1068. if (!(value instanceof Promise)) {
  1069. return value;
  1070. }
  1071. this.deferredKeys.push(key);
  1072. this.pendingKeysSet.add(key);
  1073. // We store a little wrapper promise that will be extended with
  1074. // _data/_error props upon resolve/reject
  1075. let promise: TrackedPromise = Promise.race([value, this.abortPromise]).then(
  1076. (data) => this.onSettle(promise, key, null, data as unknown),
  1077. (error) => this.onSettle(promise, key, error as unknown)
  1078. );
  1079. // Register rejection listeners to avoid uncaught promise rejections on
  1080. // errors or aborted deferred values
  1081. promise.catch(() => {});
  1082. Object.defineProperty(promise, "_tracked", { get: () => true });
  1083. return promise;
  1084. }
  1085. private onSettle(
  1086. promise: TrackedPromise,
  1087. key: string,
  1088. error: unknown,
  1089. data?: unknown
  1090. ): unknown {
  1091. if (
  1092. this.controller.signal.aborted &&
  1093. error instanceof AbortedDeferredError
  1094. ) {
  1095. this.unlistenAbortSignal();
  1096. Object.defineProperty(promise, "_error", { get: () => error });
  1097. return Promise.reject(error);
  1098. }
  1099. this.pendingKeysSet.delete(key);
  1100. if (this.done) {
  1101. // Nothing left to abort!
  1102. this.unlistenAbortSignal();
  1103. }
  1104. if (error) {
  1105. Object.defineProperty(promise, "_error", { get: () => error });
  1106. this.emit(false, key);
  1107. return Promise.reject(error);
  1108. }
  1109. Object.defineProperty(promise, "_data", { get: () => data });
  1110. this.emit(false, key);
  1111. return data;
  1112. }
  1113. private emit(aborted: boolean, settledKey?: string) {
  1114. this.subscribers.forEach((subscriber) => subscriber(aborted, settledKey));
  1115. }
  1116. subscribe(fn: (aborted: boolean, settledKey?: string) => void) {
  1117. this.subscribers.add(fn);
  1118. return () => this.subscribers.delete(fn);
  1119. }
  1120. cancel() {
  1121. this.controller.abort();
  1122. this.pendingKeysSet.forEach((v, k) => this.pendingKeysSet.delete(k));
  1123. this.emit(true);
  1124. }
  1125. async resolveData(signal: AbortSignal) {
  1126. let aborted = false;
  1127. if (!this.done) {
  1128. let onAbort = () => this.cancel();
  1129. signal.addEventListener("abort", onAbort);
  1130. aborted = await new Promise((resolve) => {
  1131. this.subscribe((aborted) => {
  1132. signal.removeEventListener("abort", onAbort);
  1133. if (aborted || this.done) {
  1134. resolve(aborted);
  1135. }
  1136. });
  1137. });
  1138. }
  1139. return aborted;
  1140. }
  1141. get done() {
  1142. return this.pendingKeysSet.size === 0;
  1143. }
  1144. get unwrappedData() {
  1145. invariant(
  1146. this.data !== null && this.done,
  1147. "Can only unwrap data on initialized and settled deferreds"
  1148. );
  1149. return Object.entries(this.data).reduce(
  1150. (acc, [key, value]) =>
  1151. Object.assign(acc, {
  1152. [key]: unwrapTrackedPromise(value),
  1153. }),
  1154. {}
  1155. );
  1156. }
  1157. get pendingKeys() {
  1158. return Array.from(this.pendingKeysSet);
  1159. }
  1160. }
  1161. function isTrackedPromise(value: any): value is TrackedPromise {
  1162. return (
  1163. value instanceof Promise && (value as TrackedPromise)._tracked === true
  1164. );
  1165. }
  1166. function unwrapTrackedPromise(value: any) {
  1167. if (!isTrackedPromise(value)) {
  1168. return value;
  1169. }
  1170. if (value._error) {
  1171. throw value._error;
  1172. }
  1173. return value._data;
  1174. }
  1175. export type DeferFunction = (
  1176. data: Record<string, unknown>,
  1177. init?: number | ResponseInit
  1178. ) => DeferredData;
  1179. export const defer: DeferFunction = (data, init = {}) => {
  1180. let responseInit = typeof init === "number" ? { status: init } : init;
  1181. return new DeferredData(data, responseInit);
  1182. };
  1183. export type RedirectFunction = (
  1184. url: string,
  1185. init?: number | ResponseInit
  1186. ) => Response;
  1187. /**
  1188. * A redirect response. Sets the status code and the `Location` header.
  1189. * Defaults to "302 Found".
  1190. */
  1191. export const redirect: RedirectFunction = (url, init = 302) => {
  1192. let responseInit = init;
  1193. if (typeof responseInit === "number") {
  1194. responseInit = { status: responseInit };
  1195. } else if (typeof responseInit.status === "undefined") {
  1196. responseInit.status = 302;
  1197. }
  1198. let headers = new Headers(responseInit.headers);
  1199. headers.set("Location", url);
  1200. return new Response(null, {
  1201. ...responseInit,
  1202. headers,
  1203. });
  1204. };
  1205. /**
  1206. * @private
  1207. * Utility class we use to hold auto-unwrapped 4xx/5xx Response bodies
  1208. */
  1209. export class ErrorResponse {
  1210. status: number;
  1211. statusText: string;
  1212. data: any;
  1213. error?: Error;
  1214. internal: boolean;
  1215. constructor(
  1216. status: number,
  1217. statusText: string | undefined,
  1218. data: any,
  1219. internal = false
  1220. ) {
  1221. this.status = status;
  1222. this.statusText = statusText || "";
  1223. this.internal = internal;
  1224. if (data instanceof Error) {
  1225. this.data = data.toString();
  1226. this.error = data;
  1227. } else {
  1228. this.data = data;
  1229. }
  1230. }
  1231. }
  1232. /**
  1233. * Check if the given error is an ErrorResponse generated from a 4xx/5xx
  1234. * Response thrown from an action/loader
  1235. */
  1236. export function isRouteErrorResponse(error: any): error is ErrorResponse {
  1237. return (
  1238. error != null &&
  1239. typeof error.status === "number" &&
  1240. typeof error.statusText === "string" &&
  1241. typeof error.internal === "boolean" &&
  1242. "data" in error
  1243. );
  1244. }