import * as React from “react”;
import type {
Hash,
History,
InitialEntry,
Location,
MemoryHistory,
Path,
Pathname,
Search,
To,
} from “history”;
import {
Action as NavigationType,
createMemoryHistory,
parsePath,
createPath,
} from “history”;
export { parsePath, createPath, NavigationType };
export type { Hash, Location, Path, Pathname, Search, To };
function invariant(cond: any, message: string): asserts cond {
if (!cond) throw new Error(message);
}
function warning(cond: any, message: string): void {
if (!cond) {
// eslint-disable-next-line no-console
if (typeof console !== “undefined”) console.warn(message);
try {
// Welcome to debugging React Router!
//
// This error is thrown as a convenience so you can more easily
// find the source for a warning that appears in the console by
// enabling “pause on exceptions” in your JavaScript debugger.
throw new Error(message);
// eslint-disable-next-line no-empty
} catch (e) {}
}
}
const alreadyWarned: Record = {};
function warningOnce(key: string, cond: boolean, message: string) {
if (!cond && !alreadyWarned[key]) {
alreadyWarned[key] = true;
warning(false, message);
}
}
///////////////////////////////////////////////////////////////////////////////
// CONTEXT
///////////////////////////////////////////////////////////////////////////////
/**
* A Navigator is a “location changer”; it’s how you get to different locations.
*
* Every history instance conforms to the Navigator interface, but the
* distinction is useful primarily when it comes to the low-level API
* where both the location and a navigator must be provided separately in order
* to avoid “tearing” that may occur in a suspense-enabled app if the action
* and/or location were to be read directly from the history instance.
*/
export type Navigator = Pick;
interface NavigationContextObject {
basename: string;
navigator: Navigator;
static: boolean;
}
const NavigationContext = React.createContext(null!);
if (__DEV__) {
NavigationContext.displayName = “Navigation”;
}
interface LocationContextObject {
location: Location;
navigationType: NavigationType;
}
const LocationContext = React.createContext(null!);
if (__DEV__) {
LocationContext.displayName = “Location”;
}
interface RouteContextObject {
outlet: React.ReactElement | null;
matches: RouteMatch[];
}
const RouteContext = React.createContext({
outlet: null,
matches: [],
});
if (__DEV__) {
RouteContext.displayName = “Route”;
}
///////////////////////////////////////////////////////////////////////////////
// COMPONENTS
///////////////////////////////////////////////////////////////////////////////
export interface MemoryRouterProps {
basename?: string;
children?: React.ReactNode;
initialEntries?: InitialEntry[];
initialIndex?: number;
}
/**
* A that stores all entries in memory.
*
* @see https://reactrouter.com/docs/en/v6/api#memoryrouter
*/
export function MemoryRouter({
basename,
children,
initialEntries,
initialIndex,
}: MemoryRouterProps): React.ReactElement {
let historyRef = React.useRef();
if (historyRef.current == null) {
historyRef.current = createMemoryHistory({ initialEntries, initialIndex });
}
let history = historyRef.current;
let [state, setState] = React.useState({
action: history.action,
location: history.location,
});
React.useLayoutEffect(() => history.listen(setState), [history]);
return (
);
}
export interface NavigateProps {
to: To;
replace?: boolean;
state?: any;
}
/**
* Changes the current location.
*
* Note: This API is mostly useful in React.Component subclasses that are not
* able to use hooks. In functional components, we recommend you use the
* `useNavigate` hook instead.
*
* @see https://reactrouter.com/docs/en/v6/api#navigate
*/
export function Navigate({ to, replace, state }: NavigateProps): null {
invariant(
useInRouterContext(),
// TODO: This error is probably because they somehow have 2 versions of
// the router loaded. We can help them understand how to avoid that.
` may be used only in the context of a component.`
);
warning(
!React.useContext(NavigationContext).static,
` must not be used on the initial render in a . ` +
`This is a no-op, but you should modify your code so the is ` +
`only ever rendered in response to some user interaction or state change.`
);
let navigate = useNavigate();
React.useEffect(() => {
navigate(to, { replace, state });
});
return null;
}
export interface OutletProps {
context?: unknown;
}
/**
* Renders the child route’s element, if there is one.
*
* @see https://reactrouter.com/docs/en/v6/api#outlet
*/
export function Outlet(props: OutletProps): React.ReactElement | null {
return useOutlet(props.context);
}
export interface RouteProps {
caseSensitive?: boolean;
children?: React.ReactNode;
element?: React.ReactNode | null;
index?: boolean;
path?: string;
}
export interface PathRouteProps {
caseSensitive?: boolean;
children?: React.ReactNode;
element?: React.ReactNode | null;
index?: false;
path: string;
}
export interface LayoutRouteProps {
children?: React.ReactNode;
element?: React.ReactNode | null;
}
export interface IndexRouteProps {
element?: React.ReactNode | null;
index: true;
}
/**
* Declares an element that should be rendered at a certain URL path.
*
* @see https://reactrouter.com/docs/en/v6/api#route
*/
export function Route(
_props: PathRouteProps | LayoutRouteProps | IndexRouteProps
): React.ReactElement | null {
invariant(
false,
`A is only ever to be used as the child of element, ` +
`never rendered directly. Please wrap your in a .`
);
}
export interface RouterProps {
basename?: string;
children?: React.ReactNode;
location: Partial | string;
navigationType?: NavigationType;
navigator: Navigator;
static?: boolean;
}
/**
* Provides location context for the rest of the app.
*
* Note: You usually won’t render a directly. Instead, you’ll render a
* router that is more specific to your environment such as a
* in web browsers or a for server rendering.
*
* @see https://reactrouter.com/docs/en/v6/api#router
*/
export function Router({
basename: basenameProp = “/”,
children = null,
location: locationProp,
navigationType = NavigationType.Pop,
navigator,
static: staticProp = false,
}: RouterProps): React.ReactElement | null {
invariant(
!useInRouterContext(),
`You cannot render a inside another .` +
` You should never have more than one in your app.`
);
let basename = normalizePathname(basenameProp);
let navigationContext = React.useMemo(
() => ({ basename, navigator, static: staticProp }),
[basename, navigator, staticProp]
);
if (typeof locationProp === “string”) {
locationProp = parsePath(locationProp);
}
let {
pathname = “/”,
search = “”,
hash = “”,
state = null,
key = “default”,
} = locationProp;
let location = React.useMemo(() => {
let trailingPathname = stripBasename(pathname, basename);
if (trailingPathname == null) {
return null;
}
return {
pathname: trailingPathname,
search,
hash,
state,
key,
};
}, [basename, pathname, search, hash, state, key]);
warning(
location != null,
` is not able to match the URL ` +
`”${pathname}${search}${hash}” because it does not start with the ` +
`basename, so the won’t render anything.`
);
if (location == null) {
return null;
}
return (
);
}
export interface RoutesProps {
children?: React.ReactNode;
location?: Partial | string;
}
/**
* A container for a nested tree of elements that renders the branch
* that best matches the current location.
*
* @see https://reactrouter.com/docs/en/v6/api#routes
*/
export function Routes({
children,
location,
}: RoutesProps): React.ReactElement | null {
return useRoutes(createRoutesFromChildren(children), location);
}
///////////////////////////////////////////////////////////////////////////////
// HOOKS
///////////////////////////////////////////////////////////////////////////////
/**
* Returns the full href for the given “to” value. This is useful for building
* custom links that are also accessible and preserve right-click behavior.
*
* @see https://reactrouter.com/docs/en/v6/api#usehref
*/
export function useHref(to: To): string {
invariant(
useInRouterContext(),
// TODO: This error is probably because they somehow have 2 versions of the
// router loaded. We can help them understand how to avoid that.
`useHref() may be used only in the context of a component.`
);
let { basename, navigator } = React.useContext(NavigationContext);
let { hash, pathname, search } = useResolvedPath(to);
let joinedPathname = pathname;
if (basename !== “/”) {
let toPathname = getToPathname(to);
let endsWithSlash = toPathname != null && toPathname.endsWith(“/”);
joinedPathname =
pathname === “/”
? basename + (endsWithSlash ? “/” : “”)
: joinPaths([basename, pathname]);
}
return navigator.createHref({ pathname: joinedPathname, search, hash });
}
/**
* Returns true if this component is a descendant of a .
*
* @see https://reactrouter.com/docs/en/v6/api#useinroutercontext
*/
export function useInRouterContext(): boolean {
return React.useContext(LocationContext) != null;
}
/**
* Returns the current location object, which represents the current URL in web
* browsers.
*
* Note: If you’re using this it may mean you’re doing some of your own
* “routing” in your app, and we’d like to know what your use case is. We may
* be able to provide something higher-level to better suit your needs.
*
* @see https://reactrouter.com/docs/en/v6/api#uselocation
*/
export function useLocation(): Location {
invariant(
useInRouterContext(),
// TODO: This error is probably because they somehow have 2 versions of the
// router loaded. We can help them understand how to avoid that.
`useLocation() may be used only in the context of a component.`
);
return React.useContext(LocationContext).location;
}
type ParamParseFailed = { failed: true };
type ParamParseSegment =
// Check here if there exists a forward slash in the string.
Segment extends `${infer LeftSegment}/${infer RightSegment}`
? // If there is a forward slash, then attempt to parse each side of the
// forward slash.
ParamParseSegment extends infer LeftResult
? ParamParseSegment extends infer RightResult
? LeftResult extends string
? // If the left side is successfully parsed as a param, then check if
// the right side can be successfully parsed as well. If both sides
// can be parsed, then the result is a union of the two sides
// (read: “foo” | “bar”).
RightResult extends string
? LeftResult | RightResult
: LeftResult
: // If the left side is not successfully parsed as a param, then check
// if only the right side can be successfully parse as a param. If it
// can, then the result is just right, else it’s a failure.
RightResult extends string
? RightResult
: ParamParseFailed
: ParamParseFailed
: // If the left side didn’t parse into a param, then just check the right
// side.
ParamParseSegment extends infer RightResult
? RightResult extends string
? RightResult
: ParamParseFailed
: ParamParseFailed
: // If there’s no forward slash, then check if this segment starts with a
// colon. If it does, then this is a dynamic segment, so the result is
// just the remainder of the string. Otherwise, it’s a failure.
Segment extends `:${infer Remaining}`
? Remaining
: ParamParseFailed;
// Attempt to parse the given string segment. If it fails, then just return the
// plain string type as a default fallback. Otherwise return the union of the
// parsed string literals that were referenced as dynamic segments in the route.
type ParamParseKey =
ParamParseSegment extends string
? ParamParseSegment
: string;
/**
* Returns the current navigation action which describes how the router came to
* the current location, either by a pop, push, or replace on the history stack.
*
* @see https://reactrouter.com/docs/en/v6/api#usenavigationtype
*/
export function useNavigationType(): NavigationType {
return React.useContext(LocationContext).navigationType;
}
/**
* Returns true if the URL for the given “to” value matches the current URL.
* This is useful for components that need to know “active” state, e.g.
* .
*
* @see https://reactrouter.com/docs/en/v6/api#usematch
*/
export function useMatch<
ParamKey extends ParamParseKey,
Path extends string
>(pattern: PathPattern | Path): PathMatch | null {
invariant(
useInRouterContext(),
// TODO: This error is probably because they somehow have 2 versions of the
// router loaded. We can help them understand how to avoid that.
`useMatch() may be used only in the context of a component.`
);
let { pathname } = useLocation();
return React.useMemo(
() => matchPath(pattern, pathname),
[pathname, pattern]
);
}
/**
* The interface for the navigate() function returned from useNavigate().
*/
export interface NavigateFunction {
(to: To, options?: NavigateOptions): void;
(delta: number): void;
}
export interface NavigateOptions {
replace?: boolean;
state?: any;
}
/**
* Returns an imperative method for changing the location. Used by s, but
* may also be used by other elements to change the location.
*
* @see https://reactrouter.com/docs/en/v6/api#usenavigate
*/
export function useNavigate(): NavigateFunction {
invariant(
useInRouterContext(),
// TODO: This error is probably because they somehow have 2 versions of the
// router loaded. We can help them understand how to avoid that.
`useNavigate() may be used only in the context of a component.`
);
let { basename, navigator } = React.useContext(NavigationContext);
let { matches } = React.useContext(RouteContext);
let { pathname: locationPathname } = useLocation();
let routePathnamesJson = JSON.stringify(
matches.map((match) => match.pathnameBase)
);
let activeRef = React.useRef(false);
React.useEffect(() => {
activeRef.current = true;
});
let navigate: NavigateFunction = React.useCallback(
(to: To | number, options: NavigateOptions = {}) => {
warning(
activeRef.current,
`You should call navigate() in a React.useEffect(), not when ` +
`your component is first rendered.`
);
if (!activeRef.current) return;
if (typeof to === “number”) {
navigator.go(to);
return;
}
let path = resolveTo(
to,
JSON.parse(routePathnamesJson),
locationPathname
);
if (basename !== “/”) {
path.pathname = joinPaths([basename, path.pathname]);
}
(!!options.replace ? navigator.replace : navigator.push)(
path,
options.state
);
},
[basename, navigator, routePathnamesJson, locationPathname]
);
return navigate;
}
const OutletContext = React.createContext(null);
/**
* Returns the context (if provided) for the child route at this level of the route
* hierarchy.
* @see https://reactrouter.com/docs/en/v6/api#useoutletcontext
*/
export function useOutletContext(): Context {
return React.useContext(OutletContext) as Context;
}
/**
* Returns the element for the child route at this level of the route
* hierarchy. Used internally by to render child routes.
*
* @see https://reactrouter.com/docs/en/v6/api#useoutlet
*/
export function useOutlet(context?: unknown): React.ReactElement | null {
let outlet = React.useContext(RouteContext).outlet;
if (outlet) {
return (
{outlet}
);
}
return outlet;
}
/**
* Returns an object of key/value pairs of the dynamic params from the current
* URL that were matched by the route path.
*
* @see https://reactrouter.com/docs/en/v6/api#useparams
*/
export function useParams<
ParamsOrKey extends string | Record = string
>(): Readonly<
[ParamsOrKey] extends [string] ? Params : Partial
> {
let { matches } = React.useContext(RouteContext);
let routeMatch = matches[matches.length – 1];
return routeMatch ? (routeMatch.params as any) : {};
}
/**
* Resolves the pathname of the given `to` value against the current location.
*
* @see https://reactrouter.com/docs/en/v6/api#useresolvedpath
*/
export function useResolvedPath(to: To): Path {
let { matches } = React.useContext(RouteContext);
let { pathname: locationPathname } = useLocation();
let routePathnamesJson = JSON.stringify(
matches.map((match) => match.pathnameBase)
);
return React.useMemo(
() => resolveTo(to, JSON.parse(routePathnamesJson), locationPathname),
[to, routePathnamesJson, locationPathname]
);
}
/**
* Returns the element of the route that matched the current location, prepared
* with the correct context to render the remainder of the route tree. Route
* elements in the tree must render an to render their child route’s
* element.
*
* @see https://reactrouter.com/docs/en/v6/api#useroutes
*/
export function useRoutes(
routes: RouteObject[],
locationArg?: Partial | string
): React.ReactElement | null {
invariant(
useInRouterContext(),
// TODO: This error is probably because they somehow have 2 versions of the
// router loaded. We can help them understand how to avoid that.
`useRoutes() may be used only in the context of a component.`
);
let { matches: parentMatches } = React.useContext(RouteContext);
let routeMatch = parentMatches[parentMatches.length – 1];
let parentParams = routeMatch ? routeMatch.params : {};
let parentPathname = routeMatch ? routeMatch.pathname : “/”;
let parentPathnameBase = routeMatch ? routeMatch.pathnameBase : “/”;
let parentRoute = routeMatch && routeMatch.route;
if (__DEV__) {
// You won’t get a warning about 2 different under a
// without a trailing *, but this is a best-effort warning anyway since we
// cannot even give the warning unless they land at the parent route.
//
// Example:
//
//
// {/* This route path MUST end with /* because otherwise
// it will never match /blog/post/123 */}
// } />
// } />
//
//
// function Blog() {
// return (
//
// } />
//
// );
// }
let parentPath = (parentRoute && parentRoute.path) || “”;
warningOnce(
parentPathname,
!parentRoute || parentPath.endsWith(“*”),
`You rendered descendant (or called \`useRoutes()\`) at ` +
`”${parentPathname}” (under ) but the ` +
`parent route path has no trailing “*”. This means if you navigate ` +
`deeper, the parent won’t match anymore and therefore the child ` +
`routes will never render.\n\n` +
`Please change the parent to .`
);
}
let locationFromContext = useLocation();
let location;
if (locationArg) {
let parsedLocationArg =
typeof locationArg === “string” ? parsePath(locationArg) : locationArg;
invariant(
parentPathnameBase === “/” ||
parsedLocationArg.pathname?.startsWith(parentPathnameBase),
`When overriding the location using \`\` or \`useRoutes(routes, location)\`, ` +
`the location pathname must begin with the portion of the URL pathname that was ` +
`matched by all parent routes. The current pathname base is “${parentPathnameBase}” ` +
`but pathname “${parsedLocationArg.pathname}” was given in the \`location\` prop.`
);
location = parsedLocationArg;
} else {
location = locationFromContext;
}
let pathname = location.pathname || “/”;
let remainingPathname =
parentPathnameBase === “/”
? pathname
: pathname.slice(parentPathnameBase.length) || “/”;
let matches = matchRoutes(routes, { pathname: remainingPathname });
if (__DEV__) {
warning(
parentRoute || matches != null,
`No routes matched location “${location.pathname}${location.search}${location.hash}” `
);
warning(
matches == null ||
matches[matches.length – 1].route.element !== undefined,
`Matched leaf route at location “${location.pathname}${location.search}${location.hash}” does not have an element. ` +
`This means it will render an with a null value by default resulting in an “empty” page.`
);
}
return _renderMatches(
matches &&
matches.map((match) =>
Object.assign({}, match, {
params: Object.assign({}, parentParams, match.params),
pathname: joinPaths([parentPathnameBase, match.pathname]),
pathnameBase:
match.pathnameBase === “/”
? parentPathnameBase
: joinPaths([parentPathnameBase, match.pathnameBase]),
})
),
parentMatches
);
}
///////////////////////////////////////////////////////////////////////////////
// UTILS
///////////////////////////////////////////////////////////////////////////////
/**
* Creates a route config from a React “children” object, which is usually
* either a `` element or an array of them. Used internally by
* `` to create a route config from its children.
*
* @see https://reactrouter.com/docs/en/v6/api#createroutesfromchildren
*/
export function createRoutesFromChildren(
children: React.ReactNode
): RouteObject[] {
let routes: RouteObject[] = [];
React.Children.forEach(children, (element) => {
if (!React.isValidElement(element)) {
// Ignore non-elements. This allows people to more easily inline
// conditionals in their route config.
return;
}
if (element.type === React.Fragment) {
// Transparently support React.Fragment and its children.
routes.push.apply(
routes,
createRoutesFromChildren(element.props.children)
);
return;
}
invariant(
element.type === Route,
`[${
typeof element.type === “string” ? element.type : element.type.name
}] is not a component. All component children of must be a or `
);
let route: RouteObject = {
caseSensitive: element.props.caseSensitive,
element: element.props.element,
index: element.props.index,
path: element.props.path,
};
if (element.props.children) {
route.children = createRoutesFromChildren(element.props.children);
}
routes.push(route);
});
return routes;
}
/**
* The parameters that were parsed from the URL path.
*/
export type Params = {
readonly [key in Key]: string | undefined;
};
/**
* A route object represents a logical route, with (optionally) its child
* routes organized in a tree-like structure.
*/
export interface RouteObject {
caseSensitive?: boolean;
children?: RouteObject[];
element?: React.ReactNode;
index?: boolean;
path?: string;
}
/**
* Returns a path with params interpolated.
*
* @see https://reactrouter.com/docs/en/v6/api#generatepath
*/
export function generatePath(path: string, params: Params = {}): string {
return path
.replace(/:(\w+)/g, (_, key) => {
invariant(params[key] != null, `Missing “:${key}” param`);
return params[key]!;
})
.replace(/\/*\*$/, (_) =>
params[“*”] == null ? “” : params[“*”].replace(/^\/*/, “/”)
);
}
/**
* A RouteMatch contains info about how a route matched a URL.
*/
export interface RouteMatch {
/**
* The names and values of dynamic parameters in the URL.
*/
params: Params;
/**
* The portion of the URL pathname that was matched.
*/
pathname: string;
/**
* The portion of the URL pathname that was matched before child routes.
*/
pathnameBase: string;
/**
* The route object that was used to match.
*/
route: RouteObject;
}
/**
* Matches the given routes to a location and returns the match data.
*
* @see https://reactrouter.com/docs/en/v6/api#matchroutes
*/
export function matchRoutes(
routes: RouteObject[],
locationArg: Partial | string,
basename = “/”
): RouteMatch[] | null {
let location =
typeof locationArg === “string” ? parsePath(locationArg) : locationArg;
let pathname = stripBasename(location.pathname || “/”, basename);
if (pathname == null) {
return null;
}
let branches = flattenRoutes(routes);
rankRouteBranches(branches);
let matches = null;
for (let i = 0; matches == null && i < branches.length; ++i) {
matches = matchRouteBranch(branches[i], pathname);
}
return matches;
}
interface RouteMeta {
relativePath: string;
caseSensitive: boolean;
childrenIndex: number;
route: RouteObject;
}
interface RouteBranch {
path: string;
score: number;
routesMeta: RouteMeta[];
}
function flattenRoutes(
routes: RouteObject[],
branches: RouteBranch[] = [],
parentsMeta: RouteMeta[] = [],
parentPath = ""
): RouteBranch[] {
routes.forEach((route, index) => {
let meta: RouteMeta = {
relativePath: route.path || “”,
caseSensitive: route.caseSensitive === true,
childrenIndex: index,
route,
};
if (meta.relativePath.startsWith(“/”)) {
invariant(
meta.relativePath.startsWith(parentPath),
`Absolute route path “${meta.relativePath}” nested under path ` +
`”${parentPath}” is not valid. An absolute child route path ` +
`must start with the combined path of all its parent routes.`
);
meta.relativePath = meta.relativePath.slice(parentPath.length);
}
let path = joinPaths([parentPath, meta.relativePath]);
let routesMeta = parentsMeta.concat(meta);
// Add the children before adding this route to the array so we traverse the
// route tree depth-first and child routes appear before their parents in
// the “flattened” version.
if (route.children && route.children.length > 0) {
invariant(
route.index !== true,
`Index routes must not have child routes. Please remove ` +
`all child routes from route path “${path}”.`
);
flattenRoutes(route.children, branches, routesMeta, path);
}
// Routes without a path shouldn’t ever match by themselves unless they are
// index routes, so don’t add them to the list of possible branches.
if (route.path == null && !route.index) {
return;
}
branches.push({ path, score: computeScore(path, route.index), routesMeta });
});
return branches;
}
function rankRouteBranches(branches: RouteBranch[]): void {
branches.sort((a, b) =>
a.score !== b.score
? b.score – a.score // Higher score first
: compareIndexes(
a.routesMeta.map((meta) => meta.childrenIndex),
b.routesMeta.map((meta) => meta.childrenIndex)
)
);
}
const paramRe = /^:\w+$/;
const dynamicSegmentValue = 3;
const indexRouteValue = 2;
const emptySegmentValue = 1;
const staticSegmentValue = 10;
const splatPenalty = -2;
const isSplat = (s: string) => s === “*”;
function computeScore(path: string, index: boolean | undefined): number {
let segments = path.split(“/”);
let initialScore = segments.length;
if (segments.some(isSplat)) {
initialScore += splatPenalty;
}
if (index) {
initialScore += indexRouteValue;
}
return segments
.filter((s) => !isSplat(s))
.reduce(
(score, segment) =>
score +
(paramRe.test(segment)
? dynamicSegmentValue
: segment === “”
? emptySegmentValue
: staticSegmentValue),
initialScore
);
}
function compareIndexes(a: number[], b: number[]): number {
let siblings =
a.length === b.length && a.slice(0, -1).every((n, i) => n === b[i]);
return siblings
? // If two routes are siblings, we should try to match the earlier sibling
// first. This allows people to have fine-grained control over the matching
// behavior by simply putting routes with identical paths in the order they
// want them tried.
a[a.length – 1] – b[b.length – 1]
: // Otherwise, it doesn’t really make sense to rank non-siblings by index,
// so they sort equally.
0;
}
function matchRouteBranch(
branch: RouteBranch,
pathname: string
): RouteMatch[] | null {
let { routesMeta } = branch;
let matchedParams = {};
let matchedPathname = “/”;
let matches: RouteMatch[] = [];
for (let i = 0; i < routesMeta.length; ++i) {
let meta = routesMeta[i];
let end = i === routesMeta.length - 1;
let remainingPathname =
matchedPathname === "/"
? pathname
: pathname.slice(matchedPathname.length) || "/";
let match = matchPath(
{ path: meta.relativePath, caseSensitive: meta.caseSensitive, end },
remainingPathname
);
if (!match) return null;
Object.assign(matchedParams, match.params);
let route = meta.route;
matches.push({
params: matchedParams,
pathname: joinPaths([matchedPathname, match.pathname]),
pathnameBase: normalizePathname(
joinPaths([matchedPathname, match.pathnameBase])
),
route,
});
if (match.pathnameBase !== "/") {
matchedPathname = joinPaths([matchedPathname, match.pathnameBase]);
}
}
return matches;
}
/**
* Renders the result of `matchRoutes()` into a React element.
*/
export function renderMatches(
matches: RouteMatch[] | null
): React.ReactElement | null {
return _renderMatches(matches);
}
function _renderMatches(
matches: RouteMatch[] | null,
parentMatches: RouteMatch[] = []
): React.ReactElement | null {
if (matches == null) return null;
return matches.reduceRight((outlet, match, index) => {
return (
);
}, null as React.ReactElement | null);
}
/**
* A PathPattern is used to match on some portion of a URL pathname.
*/
export interface PathPattern {
/**
* A string to match against a URL pathname. May contain `:id`-style segments
* to indicate placeholders for dynamic parameters. May also end with `/*` to
* indicate matching the rest of the URL pathname.
*/
path: Path;
/**
* Should be `true` if the static portions of the `path` should be matched in
* the same case.
*/
caseSensitive?: boolean;
/**
* Should be `true` if this pattern should match the entire URL pathname.
*/
end?: boolean;
}
/**
* A PathMatch contains info about how a PathPattern matched on a URL pathname.
*/
export interface PathMatch {
/**
* The names and values of dynamic parameters in the URL.
*/
params: Params;
/**
* The portion of the URL pathname that was matched.
*/
pathname: string;
/**
* The portion of the URL pathname that was matched before child routes.
*/
pathnameBase: string;
/**
* The pattern that was used to match.
*/
pattern: PathPattern;
}
type Mutable = {
-readonly [P in keyof T]: T[P];
};
/**
* Performs pattern matching on a URL pathname and returns information about
* the match.
*
* @see https://reactrouter.com/docs/en/v6/api#matchpath
*/
export function matchPath<
ParamKey extends ParamParseKey,
Path extends string
>(
pattern: PathPattern | Path,
pathname: string
): PathMatch | null {
if (typeof pattern === “string”) {
pattern = { path: pattern, caseSensitive: false, end: true };
}
let [matcher, paramNames] = compilePath(
pattern.path,
pattern.caseSensitive,
pattern.end
);
let match = pathname.match(matcher);
if (!match) return null;
let matchedPathname = match[0];
let pathnameBase = matchedPathname.replace(/(.)\/+$/, “$1”);
let captureGroups = match.slice(1);
let params: Params = paramNames.reduce>(
(memo, paramName, index) => {
// We need to compute the pathnameBase here using the raw splat value
// instead of using params[“*”] later because it will be decoded then
if (paramName === “*”) {
let splatValue = captureGroups[index] || “”;
pathnameBase = matchedPathname
.slice(0, matchedPathname.length – splatValue.length)
.replace(/(.)\/+$/, “$1”);
}
memo[paramName] = safelyDecodeURIComponent(
captureGroups[index] || “”,
paramName
);
return memo;
},
{}
);
return {
params,
pathname: matchedPathname,
pathnameBase,
pattern,
};
}
function compilePath(
path: string,
caseSensitive = false,
end = true
): [RegExp, string[]] {
warning(
path === “*” || !path.endsWith(“*”) || path.endsWith(“/*”),
`Route path “${path}” will be treated as if it were ` +
`”${path.replace(/\*$/, “/*”)}” because the \`*\` character must ` +
`always follow a \`/\` in the pattern. To get rid of this warning, ` +
`please change the route path to “${path.replace(/\*$/, “/*”)}”.`
);
let paramNames: string[] = [];
let regexpSource =
“^” +
path
.replace(/\/*\*?$/, “”) // Ignore trailing / and /*, we’ll handle it below
.replace(/^\/*/, “/”) // Make sure it has a leading /
.replace(/[\\.*+^$?{}|()[\]]/g, “\\$&”) // Escape special regex chars
.replace(/:(\w+)/g, (_: string, paramName: string) => {
paramNames.push(paramName);
return “([^\\/]+)”;
});
if (path.endsWith(“*”)) {
paramNames.push(“*”);
regexpSource +=
path === “*” || path === “/*”
? “(.*)$” // Already matched the initial /, just match the rest
: “(?:\\/(.+)|\\/*)$”; // Don’t include the / in params[“*”]
} else {
regexpSource += end
? “\\/*$” // When matching to the end, ignore trailing slashes
: // Otherwise, match a word boundary or a proceeding /. The word boundary restricts
// parent routes to matching only their own words and nothing more, e.g. parent
// route “/home” should not match “/home2”.
// Additionally, allow paths starting with `.`, `-`, `~`, and url-encoded entities,
// but do not consume the character in the matched path so they can match against
// nested paths.
“(?:(?=[.~-]|%[0-9A-F]{2})|\\b|\\/|$)”;
}
let matcher = new RegExp(regexpSource, caseSensitive ? undefined : “i”);
return [matcher, paramNames];
}
function safelyDecodeURIComponent(value: string, paramName: string) {
try {
return decodeURIComponent(value);
} catch (error) {
warning(
false,
`The value for the URL param “${paramName}” will not be decoded because` +
` the string “${value}” is a malformed URL segment. This is probably` +
` due to a bad percent encoding (${error}).`
);
return value;
}
}
/**
* Returns a resolved path object relative to the given pathname.
*
* @see https://reactrouter.com/docs/en/v6/api#resolvepath
*/
export function resolvePath(to: To, fromPathname = “/”): Path {
let {
pathname: toPathname,
search = “”,
hash = “”,
} = typeof to === “string” ? parsePath(to) : to;
let pathname = toPathname
? toPathname.startsWith(“/”)
? toPathname
: resolvePathname(toPathname, fromPathname)
: fromPathname;
return {
pathname,
search: normalizeSearch(search),
hash: normalizeHash(hash),
};
}
function resolvePathname(relativePath: string, fromPathname: string): string {
let segments = fromPathname.replace(/\/+$/, “”).split(“/”);
let relativeSegments = relativePath.split(“/”);
relativeSegments.forEach((segment) => {
if (segment === “..”) {
// Keep the root “” segment so the pathname starts at /
if (segments.length > 1) segments.pop();
} else if (segment !== “.”) {
segments.push(segment);
}
});
return segments.length > 1 ? segments.join(“/”) : “/”;
}
function resolveTo(
toArg: To,
routePathnames: string[],
locationPathname: string
): Path {
let to = typeof toArg === “string” ? parsePath(toArg) : toArg;
let toPathname = toArg === “” || to.pathname === “” ? “/” : to.pathname;
// If a pathname is explicitly provided in `to`, it should be relative to the
// route context. This is explained in `Note on `` values` in our
// migration guide from v5 as a means of disambiguation between `to` values
// that begin with `/` and those that do not. However, this is problematic for
// `to` values that do not provide a pathname. `to` can simply be a search or
// hash string, in which case we should assume that the navigation is relative
// to the current location’s pathname and *not* the route pathname.
let from: string;
if (toPathname == null) {
from = locationPathname;
} else {
let routePathnameIndex = routePathnames.length – 1;
if (toPathname.startsWith(“..”)) {
let toSegments = toPathname.split(“/”);
// Each leading .. segment means “go up one route” instead of “go up one
// URL segment”. This is a key difference from how works and a
// major reason we call this a “to” value instead of a “href”.
while (toSegments[0] === “..”) {
toSegments.shift();
routePathnameIndex -= 1;
}
to.pathname = toSegments.join(“/”);
}
// If there are more “..” segments than parent routes, resolve relative to
// the root / URL.
from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex] : “/”;
}
let path = resolvePath(to, from);
// Ensure the pathname has a trailing slash if the original to value had one.
if (
toPathname &&
toPathname !== “/” &&
toPathname.endsWith(“/”) &&
!path.pathname.endsWith(“/”)
) {
path.pathname += “/”;
}
return path;
}
function getToPathname(to: To): string | undefined {
// Empty strings should be treated the same as / paths
return to === “” || (to as Path).pathname === “”
? “/”
: typeof to === “string”
? parsePath(to).pathname
: to.pathname;
}
function stripBasename(pathname: string, basename: string): string | null {
if (basename === “/”) return pathname;
if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) {
return null;
}
let nextChar = pathname.charAt(basename.length);
if (nextChar && nextChar !== “/”) {
// pathname does not start with basename/
return null;
}
return pathname.slice(basename.length) || “/”;
}
const joinPaths = (paths: string[]): string =>
paths.join(“/”).replace(/\/\/+/g, “/”);
const normalizePathname = (pathname: string): string =>
pathname.replace(/\/+$/, “”).replace(/^\/*/, “/”);
const normalizeSearch = (search: string): string =>
!search || search === “?”
? “”
: search.startsWith(“?”)
? search
: “?” + search;
const normalizeHash = (hash: string): string =>
!hash || hash === “#” ? “” : hash.startsWith(“#”) ? hash : “#” + hash;
///////////////////////////////////////////////////////////////////////////////
// DANGER! PLEASE READ ME!
// We provide these exports as an escape hatch in the event that you need any
// routing data that we don’t provide an explicit API for. With that said, we
// want to cover your use case if we can, so if you feel the need to use these
// we want to hear from you. Let us know what you’re building and we’ll do our
// best to make sure we can support you!
//
// We consider these exports an implementation detail and do not guarantee
// against any breaking changes, regardless of the semver release. Use with
// extreme caution and only if you understand the consequences. Godspeed.
///////////////////////////////////////////////////////////////////////////////
/** @internal */
export {
NavigationContext as UNSAFE_NavigationContext,
LocationContext as UNSAFE_LocationContext,
RouteContext as UNSAFE_RouteContext,
};