react-router
Version:
Declarative routing for React
1,287 lines • 122 kB
JavaScript
/**
* react-router v8.0.0
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
import { AsyncLocalStorage } from "node:async_hooks";
import * as React from "react";
import { parse, serialize, splitSetCookieString } from "cookie-es";
import { BrowserRouter, Form, HashRouter, Link, Links, MemoryRouter, Meta, NavLink, Navigate, Outlet, Outlet as Outlet$1, Route, Router, RouterProvider, Routes, ScrollRestoration, StaticRouter, StaticRouterProvider, UNSAFE_AwaitContextProvider, UNSAFE_WithComponentProps, UNSAFE_WithErrorBoundaryProps, UNSAFE_WithHydrateFallbackProps, unstable_HistoryRouter } from "react-router/internal/react-server-client";
//#region lib/router/url.ts
const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|[\\/]{2})/i;
//#endregion
//#region lib/router/history.ts
function invariant$1(value, message) {
if (value === false || value === null || typeof value === "undefined") throw new Error(message);
}
function warning(cond, message) {
if (!cond) {
if (typeof console !== "undefined") console.warn(message);
try {
throw new Error(message);
} catch (e) {}
}
}
function createKey$1() {
return Math.random().toString(36).substring(2, 10);
}
/**
* Creates a Location object with a unique key from the given Path
*/
function createLocation(current, to, state = null, key, mask) {
return {
pathname: typeof current === "string" ? current : current.pathname,
search: "",
hash: "",
...typeof to === "string" ? parsePath(to) : to,
state,
key: to && to.key || key || createKey$1(),
mask
};
}
/**
* Creates a string URL path from the given pathname, search, and hash components.
*
* @category Utils
*/
function createPath({ pathname = "/", search = "", hash = "" }) {
if (search && search !== "?") pathname += search.charAt(0) === "?" ? search : "?" + search;
if (hash && hash !== "#") pathname += hash.charAt(0) === "#" ? hash : "#" + hash;
return pathname;
}
/**
* Parses a string URL path into its separate pathname, search, and hash components.
*
* @category Utils
*/
function parsePath(path) {
let parsedPath = {};
if (path) {
let hashIndex = path.indexOf("#");
if (hashIndex >= 0) {
parsedPath.hash = path.substring(hashIndex);
path = path.substring(0, hashIndex);
}
let searchIndex = path.indexOf("?");
if (searchIndex >= 0) {
parsedPath.search = path.substring(searchIndex);
path = path.substring(0, searchIndex);
}
if (path) parsedPath.pathname = path;
}
return parsedPath;
}
//#endregion
//#region lib/router/instrumentation.ts
const UninstrumentedSymbol = Symbol("Uninstrumented");
function getRouteInstrumentationUpdates(fns, route) {
let aggregated = {
lazy: [],
"lazy.loader": [],
"lazy.action": [],
"lazy.middleware": [],
middleware: [],
loader: [],
action: []
};
fns.forEach((fn) => fn({
id: route.id,
index: route.index,
path: route.path,
instrument(i) {
let keys = Object.keys(aggregated);
for (let key of keys) if (i[key]) aggregated[key].push(i[key]);
}
}));
let updates = {};
if (typeof route.lazy === "function" && aggregated.lazy.length > 0) {
let instrumented = wrapImpl(aggregated.lazy, route.lazy, () => void 0);
if (instrumented) updates.lazy = instrumented;
}
if (typeof route.lazy === "object") {
let lazyObject = route.lazy;
[
"middleware",
"loader",
"action"
].forEach((key) => {
let lazyFn = lazyObject[key];
let instrumentations = aggregated[`lazy.${key}`];
if (typeof lazyFn === "function" && instrumentations.length > 0) {
let instrumented = wrapImpl(instrumentations, lazyFn, () => void 0);
if (instrumented) updates.lazy = Object.assign(updates.lazy || {}, { [key]: instrumented });
}
});
}
["loader", "action"].forEach((key) => {
let handler = route[key];
if (typeof handler === "function" && aggregated[key].length > 0) {
let original = handler[UninstrumentedSymbol] ?? handler;
let instrumented = wrapImpl(aggregated[key], original, (...args) => getHandlerInfo(args[0]));
if (instrumented) {
if (key === "loader" && original.hydrate === true) instrumented.hydrate = true;
instrumented[UninstrumentedSymbol] = original;
updates[key] = instrumented;
}
}
});
if (route.middleware && route.middleware.length > 0 && aggregated.middleware.length > 0) updates.middleware = route.middleware.map((middleware) => {
let original = middleware[UninstrumentedSymbol] ?? middleware;
let instrumented = wrapImpl(aggregated.middleware, original, (...args) => getHandlerInfo(args[0]));
if (instrumented) {
instrumented[UninstrumentedSymbol] = original;
return instrumented;
}
return middleware;
});
return updates;
}
function wrapImpl(impls, handler, getInfo) {
if (impls.length === 0) return null;
return async (...args) => {
let result = await recurseRight(impls, getInfo(...args), () => handler(...args), impls.length - 1);
if (result.type === "error") throw result.value;
return result.value;
};
}
async function recurseRight(impls, info, handler, index) {
let impl = impls[index];
let result;
if (!impl) try {
result = {
type: "success",
value: await handler()
};
} catch (e) {
result = {
type: "error",
value: e
};
}
else {
let handlerPromise = void 0;
let callHandler = async () => {
if (handlerPromise) console.error("You cannot call instrumented handlers more than once");
else handlerPromise = recurseRight(impls, info, handler, index - 1);
result = await handlerPromise;
invariant$1(result, "Expected a result");
if (result.type === "error" && result.value instanceof Error) return {
status: "error",
error: result.value
};
return {
status: "success",
error: void 0
};
};
try {
await impl(callHandler, info);
} catch (e) {
console.error("An instrumentation function threw an error:", e);
}
if (!handlerPromise) await callHandler();
await handlerPromise;
}
if (result) return result;
return {
type: "error",
value: /* @__PURE__ */ new Error("No result assigned in instrumentation chain.")
};
}
function getHandlerInfo(args) {
let { request, context, params, pattern } = args;
return {
request: getReadonlyRequest(request),
params: { ...params },
pattern,
context: getReadonlyContext(context)
};
}
function getReadonlyRequest(request) {
return {
method: request.method,
url: request.url,
headers: { get: (...args) => request.headers.get(...args) }
};
}
function getReadonlyContext(context) {
return { get: (ctx) => context.get(ctx) };
}
//#endregion
//#region lib/router/utils.ts
/**
* Creates a type-safe {@link RouterContext} object that can be used to
* store and retrieve arbitrary values in [`action`](../../start/framework/route-module#action)s,
* [`loader`](../../start/framework/route-module#loader)s, and [middleware](../../how-to/middleware).
* Similar to React's [`createContext`](https://react.dev/reference/react/createContext),
* but specifically designed for React Router's request/response lifecycle.
*
* If a `defaultValue` is provided, it will be returned from `context.get()`
* when no value has been set for the context. Otherwise, reading this context
* when no value has been set will throw an error.
*
* ```tsx filename=app/context.ts
* import { createContext } from "react-router";
*
* // Create a context for user data
* export const userContext =
* createContext<User | null>(null);
* ```
*
* ```tsx filename=app/middleware/auth.ts
* import { getUserFromSession } from "~/auth.server";
* import { userContext } from "~/context";
*
* export const authMiddleware = async ({
* context,
* request,
* }) => {
* const user = await getUserFromSession(request);
* context.set(userContext, user);
* };
* ```
*
* ```tsx filename=app/routes/profile.tsx
* import { userContext } from "~/context";
*
* export async function loader({
* context,
* }: Route.LoaderArgs) {
* const user = context.get(userContext);
*
* if (!user) {
* throw new Response("Unauthorized", { status: 401 });
* }
*
* return { user };
* }
* ```
*
* @public
* @category Utils
* @mode framework
* @mode data
* @param defaultValue An optional default value for the context. This value
* will be returned if no value has been set for this context.
* @returns A {@link RouterContext} object that can be used with
* `context.get()` and `context.set()` in [`action`](../../start/framework/route-module#action)s,
* [`loader`](../../start/framework/route-module#loader)s, and [middleware](../../how-to/middleware).
*/
function createContext(defaultValue) {
return { defaultValue };
}
/**
* Provides methods for writing/reading values in application context in a
* type-safe way. Primarily for usage with [middleware](../../how-to/middleware).
*
* @example
* import {
* createContext,
* RouterContextProvider
* } from "react-router";
*
* const userContext = createContext<User | null>(null);
* const contextProvider = new RouterContextProvider();
* contextProvider.set(userContext, getUser());
* // ^ Type-safe
* const user = contextProvider.get(userContext);
* // ^ User
*
* @public
* @category Utils
* @mode framework
* @mode data
*/
var RouterContextProvider = class {
#map = /* @__PURE__ */ new Map();
/**
* Create a new `RouterContextProvider` instance
* @param init An optional initial context map to populate the provider with
*/
constructor(init) {
if (init) for (let [context, value] of init) this.set(context, value);
}
/**
* Access a value from the context. If no value has been set for the context,
* it will return the context's `defaultValue` if provided, or throw an error
* if no `defaultValue` was set.
* @param context The context to get the value for
* @returns The value for the context, or the context's `defaultValue` if no
* value was set
*/
get(context) {
if (this.#map.has(context)) return this.#map.get(context);
if (context.defaultValue !== void 0) return context.defaultValue;
throw new Error("No value found for context");
}
/**
* Set a value for the context. If the context already has a value set, this
* will overwrite it.
*
* @param context The context to set the value for
* @param value The value to set for the context
* @returns {void}
*/
set(context, value) {
this.#map.set(context, value);
}
};
const unsupportedLazyRouteObjectKeys = new Set([
"lazy",
"caseSensitive",
"path",
"id",
"index",
"children"
]);
function isUnsupportedLazyRouteObjectKey(key) {
return unsupportedLazyRouteObjectKeys.has(key);
}
const unsupportedLazyRouteFunctionKeys = new Set([
"lazy",
"caseSensitive",
"path",
"id",
"index",
"middleware",
"children"
]);
function isUnsupportedLazyRouteFunctionKey(key) {
return unsupportedLazyRouteFunctionKeys.has(key);
}
function isIndexRoute(route) {
return route.index === true;
}
function defaultMapRouteProperties(route) {
let updates = {};
if (route.Component) Object.assign(updates, {
element: React.createElement(route.Component),
Component: void 0
});
if (route.HydrateFallback) Object.assign(updates, {
hydrateFallbackElement: React.createElement(route.HydrateFallback),
HydrateFallback: void 0
});
if (route.ErrorBoundary) Object.assign(updates, {
errorElement: React.createElement(route.ErrorBoundary),
ErrorBoundary: void 0
});
return updates;
}
function convertRoutesToDataRoutes(routes, mapRouteProperties = defaultMapRouteProperties, parentPath = [], manifest = {}, allowInPlaceMutations = false) {
return routes.map((route, index) => {
let treePath = [...parentPath, String(index)];
let id = typeof route.id === "string" ? route.id : treePath.join("-");
invariant$1(route.index !== true || !route.children, `Cannot specify children on an index route`);
invariant$1(allowInPlaceMutations || !manifest[id], `Found a route id collision on id "${id}". Route id's must be globally unique within Data Router usages`);
if (isIndexRoute(route)) {
let indexRoute = {
...route,
id
};
manifest[id] = mergeRouteUpdates(indexRoute, mapRouteProperties(indexRoute));
return indexRoute;
} else {
let pathOrLayoutRoute = {
...route,
id,
children: void 0
};
manifest[id] = mergeRouteUpdates(pathOrLayoutRoute, mapRouteProperties(pathOrLayoutRoute));
if (route.children) pathOrLayoutRoute.children = convertRoutesToDataRoutes(route.children, mapRouteProperties, treePath, manifest, allowInPlaceMutations);
return pathOrLayoutRoute;
}
});
}
function mergeRouteUpdates(route, updates) {
return Object.assign(route, {
...updates,
...typeof updates.lazy === "object" && updates.lazy != null ? { lazy: {
...route.lazy,
...updates.lazy
} } : {}
});
}
/**
* Matches the given routes to a location and returns the match data.
*
* @example
* import { matchRoutes } from "react-router";
*
* let routes = [{
* path: "/",
* Component: Root,
* children: [{
* path: "dashboard",
* Component: Dashboard,
* }]
* }];
*
* matchRoutes(routes, "/dashboard"); // [rootMatch, dashboardMatch]
*
* @public
* @category Utils
* @param routes The array of route objects to match against.
* @param locationArg The location to match against, either a string path or a
* partial {@link Location} object
* @param basename Optional base path to strip from the location before matching.
* Defaults to `/`.
* @returns An array of matched routes, or `null` if no matches were found.
*/
function matchRoutes(routes, locationArg, basename = "/") {
return matchRoutesImpl(routes, locationArg, basename, false);
}
function matchRoutesImpl(routes, locationArg, basename, allowPartial, precomputedBranches) {
let pathname = stripBasename((typeof locationArg === "string" ? parsePath(locationArg) : locationArg).pathname || "/", basename);
if (pathname == null) return null;
let branches = precomputedBranches ?? flattenAndRankRoutes(routes);
let matches = null;
let decoded = decodePath(pathname);
for (let i = 0; matches == null && i < branches.length; ++i) matches = matchRouteBranch(branches[i], decoded, allowPartial);
return matches;
}
function convertRouteMatchToUiMatch(match, loaderData) {
let { route, pathname, params } = match;
return {
id: route.id,
pathname,
params,
loaderData: loaderData[route.id],
handle: route.handle
};
}
function flattenAndRankRoutes(routes) {
let branches = flattenRoutes(routes);
rankRouteBranches(branches);
return branches;
}
function flattenRoutes(routes, branches = [], parentsMeta = [], parentPath = "", _hasParentOptionalSegments = false) {
let flattenRoute = (route, index, hasParentOptionalSegments = _hasParentOptionalSegments, relativePath) => {
let meta = {
relativePath: relativePath === void 0 ? route.path || "" : relativePath,
caseSensitive: route.caseSensitive === true,
childrenIndex: index,
route
};
if (meta.relativePath.startsWith("/")) {
if (!meta.relativePath.startsWith(parentPath) && hasParentOptionalSegments) return;
invariant$1(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);
if (route.children && route.children.length > 0) {
invariant$1(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, hasParentOptionalSegments);
}
if (route.path == null && !route.index) return;
branches.push({
path,
score: computeScore(path, route.index),
routesMeta: routesMeta.map((meta, i) => {
let [matcher, params] = compilePath(meta.relativePath, meta.caseSensitive, i === routesMeta.length - 1);
return {
...meta,
matcher,
compiledParams: params
};
})
});
};
routes.forEach((route, index) => {
if (route.path === "" || !route.path?.includes("?")) flattenRoute(route, index);
else for (let exploded of explodeOptionalSegments(route.path)) flattenRoute(route, index, true, exploded);
});
return branches;
}
function explodeOptionalSegments(path) {
let segments = path.split("/");
if (segments.length === 0) return [];
let [first, ...rest] = segments;
let isOptional = first.endsWith("?");
let required = first.replace(/\?$/, "");
if (rest.length === 0) return isOptional ? [required, ""] : [required];
let restExploded = explodeOptionalSegments(rest.join("/"));
let result = [];
result.push(...restExploded.map((subpath) => subpath === "" ? required : [required, subpath].join("/")));
if (isOptional) result.push(...restExploded);
return result.map((exploded) => path.startsWith("/") && exploded === "" ? "/" : exploded);
}
function rankRouteBranches(branches) {
branches.sort((a, b) => a.score !== b.score ? b.score - a.score : 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) => s === "*";
function computeScore(path, index) {
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, b) {
return a.length === b.length && a.slice(0, -1).every((n, i) => n === b[i]) ? a[a.length - 1] - b[b.length - 1] : 0;
}
function matchRouteBranch(branch, pathname, allowPartial = false) {
let { routesMeta } = branch;
let matchedParams = {};
let matchedPathname = "/";
let matches = [];
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 pattern = {
path: meta.relativePath,
caseSensitive: meta.caseSensitive,
end
};
let match = meta.matcher && meta.compiledParams ? matchPathImpl(pattern, remainingPathname, meta.matcher, meta.compiledParams) : matchPath(pattern, remainingPathname);
let route = meta.route;
if (!match && end && allowPartial && !routesMeta[routesMeta.length - 1].route.index) match = matchPath({
path: meta.relativePath,
caseSensitive: meta.caseSensitive,
end: false
}, remainingPathname);
if (!match) return null;
Object.assign(matchedParams, match.params);
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;
}
/**
* Performs pattern matching on a URL pathname and returns information about
* the match.
*
* @public
* @category Utils
* @param pattern The pattern to match against the URL pathname. This can be a
* string or a {@link PathPattern} object. If a string is provided, it will be
* treated as a pattern with `caseSensitive` set to `false` and `end` set to
* `true`.
* @param pathname The URL pathname to match against the pattern.
* @returns A path match object if the pattern matches the pathname,
* or `null` if it does not match.
*/
function matchPath(pattern, pathname) {
if (typeof pattern === "string") pattern = {
path: pattern,
caseSensitive: false,
end: true
};
let [matcher, compiledParams] = compilePath(pattern.path, pattern.caseSensitive, pattern.end);
return matchPathImpl(pattern, pathname, matcher, compiledParams);
}
function matchPathImpl(pattern, pathname, matcher, compiledParams) {
let match = pathname.match(matcher);
if (!match) return null;
let matchedPathname = match[0];
let pathnameBase = matchedPathname.replace(/(.)\/+$/, "$1");
let captureGroups = match.slice(1);
return {
params: compiledParams.reduce((memo, { paramName, isOptional }, index) => {
if (paramName === "*") {
let splatValue = captureGroups[index] || "";
pathnameBase = matchedPathname.slice(0, matchedPathname.length - splatValue.length).replace(/(.)\/+$/, "$1");
}
const value = captureGroups[index];
if (isOptional && !value) memo[paramName] = void 0;
else memo[paramName] = (value || "").replace(/%2F/g, "/");
return memo;
}, {}),
pathname: matchedPathname,
pathnameBase,
pattern
};
}
function compilePath(path, caseSensitive = false, end = true) {
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 params = [];
let regexpSource = "^" + path.replace(/\/*\*?$/, "").replace(/^\/*/, "/").replace(/[\\.*+^${}|()[\]]/g, "\\$&").replace(/\/:([\w-]+)(\?)?/g, (match, paramName, isOptional, index, str) => {
params.push({
paramName,
isOptional: isOptional != null
});
if (isOptional) {
let nextChar = str.charAt(index + match.length);
if (nextChar && nextChar !== "/") return "/([^\\/]*)";
return "(?:/([^\\/]*))?";
}
return "/([^\\/]+)";
}).replace(/\/([\w-]+)\?(\/|$)/g, "(/$1)?$2");
if (path.endsWith("*")) {
params.push({ paramName: "*" });
regexpSource += path === "*" || path === "/*" ? "(.*)$" : "(?:\\/(.+)|\\/*)$";
} else if (end) regexpSource += "\\/*$";
else if (path !== "" && path !== "/") regexpSource += "(?:(?=\\/|$))";
return [new RegExp(regexpSource, caseSensitive ? void 0 : "i"), params];
}
function decodePath(value) {
try {
return value.split("/").map((v) => decodeURIComponent(v).replace(/\//g, "%2F")).join("/");
} catch (error) {
warning(false, `The URL path "${value}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${error}).`);
return value;
}
}
function stripBasename(pathname, basename) {
if (basename === "/") return pathname;
if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) return null;
let startIndex = basename.endsWith("/") ? basename.length - 1 : basename.length;
let nextChar = pathname.charAt(startIndex);
if (nextChar && nextChar !== "/") return null;
return pathname.slice(startIndex) || "/";
}
function prependBasename({ basename, pathname }) {
return pathname === "/" ? basename : joinPaths([basename, pathname]);
}
const isAbsoluteUrl = (url) => ABSOLUTE_URL_REGEX.test(url);
/**
* Returns a resolved {@link Path} object relative to the given pathname.
*
* @public
* @category Utils
* @param to The path to resolve, either a string or a partial {@link Path}
* object.
* @param fromPathname The pathname to resolve the path from. Defaults to `/`.
* @returns A {@link Path} object with the resolved pathname, search, and hash.
*/
function resolvePath(to, fromPathname = "/") {
let { pathname: toPathname, search = "", hash = "" } = typeof to === "string" ? parsePath(to) : to;
let pathname;
if (toPathname) {
toPathname = removeDoubleSlashes(toPathname);
if (toPathname.startsWith("/")) pathname = resolvePathname(toPathname.substring(1), "/");
else pathname = resolvePathname(toPathname, fromPathname);
} else pathname = fromPathname;
return {
pathname,
search: normalizeSearch(search),
hash: normalizeHash(hash)
};
}
function resolvePathname(relativePath, fromPathname) {
let segments = removeTrailingSlash(fromPathname).split("/");
relativePath.split("/").forEach((segment) => {
if (segment === "..") {
if (segments.length > 1) segments.pop();
} else if (segment !== ".") segments.push(segment);
});
return segments.length > 1 ? segments.join("/") : "/";
}
function getInvalidPathError(char, field, dest, path) {
return `Cannot include a '${char}' character in a manually specified \`to.${field}\` field [${JSON.stringify(path)}]. Please separate it out to the \`to.${dest}\` field. Alternatively you may provide the full path as a string in <Link to="..."> and the router will parse it for you.`;
}
function getPathContributingMatches(matches) {
return matches.filter((match, index) => index === 0 || match.route.path && match.route.path.length > 0);
}
function getResolveToMatches(matches) {
let pathMatches = getPathContributingMatches(matches);
return pathMatches.map((match, idx) => idx === pathMatches.length - 1 ? match.pathname : match.pathnameBase);
}
function resolveTo(toArg, routePathnames, locationPathname, isPathRelative = false) {
let to;
if (typeof toArg === "string") to = parsePath(toArg);
else {
to = { ...toArg };
invariant$1(!to.pathname || !to.pathname.includes("?"), getInvalidPathError("?", "pathname", "search", to));
invariant$1(!to.pathname || !to.pathname.includes("#"), getInvalidPathError("#", "pathname", "hash", to));
invariant$1(!to.search || !to.search.includes("#"), getInvalidPathError("#", "search", "hash", to));
}
let isEmptyPath = toArg === "" || to.pathname === "";
let toPathname = isEmptyPath ? "/" : to.pathname;
let from;
if (toPathname == null) from = locationPathname;
else {
let routePathnameIndex = routePathnames.length - 1;
if (!isPathRelative && toPathname.startsWith("..")) {
let toSegments = toPathname.split("/");
while (toSegments[0] === "..") {
toSegments.shift();
routePathnameIndex -= 1;
}
to.pathname = toSegments.join("/");
}
from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex] : "/";
}
let path = resolvePath(to, from);
let hasExplicitTrailingSlash = toPathname && toPathname !== "/" && toPathname.endsWith("/");
let hasCurrentTrailingSlash = (isEmptyPath || toPathname === ".") && locationPathname.endsWith("/");
if (!path.pathname.endsWith("/") && (hasExplicitTrailingSlash || hasCurrentTrailingSlash)) path.pathname += "/";
return path;
}
const removeDoubleSlashes = (path) => path.replace(/[\\/]{2,}/g, "/");
const joinPaths = (paths) => removeDoubleSlashes(paths.join("/"));
const removeTrailingSlash = (path) => path.replace(/\/+$/, "");
const normalizePathname = (pathname) => removeTrailingSlash(pathname).replace(/^\/*/, "/");
const normalizeSearch = (search) => !search || search === "?" ? "" : search.startsWith("?") ? search : "?" + search;
const normalizeHash = (hash) => !hash || hash === "#" ? "" : hash.startsWith("#") ? hash : "#" + hash;
var DataWithResponseInit = class {
type = "DataWithResponseInit";
data;
init;
constructor(data, init) {
this.data = data;
this.init = init || null;
}
};
/**
* Create "responses" that contain `headers`/`status` without forcing
* serialization into an actual [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
*
* @example
* import { data } from "react-router";
*
* export async function action({ request }: Route.ActionArgs) {
* let formData = await request.formData();
* let item = await createItem(formData);
* return data(item, {
* headers: { "X-Custom-Header": "value" }
* status: 201,
* });
* }
*
* @public
* @category Utils
* @mode framework
* @mode data
* @param data The data to be included in the response.
* @param init The status code or a `ResponseInit` object to be included in the
* response.
* @returns A {@link DataWithResponseInit} instance containing the data and
* response init.
*/
function data(data, init) {
return new DataWithResponseInit(data, typeof init === "number" ? { status: init } : init);
}
/**
* A redirect [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response).
* Sets the status code and the [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)
* header. Defaults to [`302 Found`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302).
*
* This utility accepts absolute URLs and can navigate to external domains, so
* the application should validate any user-supplied inputs to redirects.
*
* @example
* import { redirect } from "react-router";
*
* export async function loader({ request }: Route.LoaderArgs) {
* if (!isLoggedIn(request))
* throw redirect("/login");
* }
*
* // ...
* }
*
* @public
* @category Utils
* @mode framework
* @mode data
* @param url The URL to redirect to.
* @param init The status code or a `ResponseInit` object to be included in the
* response.
* @returns A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
* object with the redirect status and [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)
* header.
*/
const redirect$1 = (url, init = 302) => {
let responseInit = init;
if (typeof responseInit === "number") responseInit = { status: responseInit };
else if (typeof responseInit.status === "undefined") responseInit.status = 302;
let headers = new Headers(responseInit.headers);
headers.set("Location", url);
return new Response(null, {
...responseInit,
headers
});
};
/**
* A redirect [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
* that will force a document reload to the new location. Sets the status code
* and the [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)
* header. Defaults to [`302 Found`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302).
*
* This utility accepts absolute URLs and can navigate to external domains, so
* the application should validate any user-supplied inputs to redirects.
*
* ```tsx filename=routes/logout.tsx
* import { redirectDocument } from "react-router";
*
* import { destroySession } from "../sessions.server";
*
* export async function action({ request }: Route.ActionArgs) {
* let session = await getSession(request.headers.get("Cookie"));
* return redirectDocument("/", {
* headers: { "Set-Cookie": await destroySession(session) }
* });
* }
* ```
*
* @public
* @category Utils
* @mode framework
* @mode data
* @param url The URL to redirect to.
* @param init The status code or a `ResponseInit` object to be included in the
* response.
* @returns A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
* object with the redirect status and [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)
* header.
*/
const redirectDocument$1 = (url, init) => {
let response = redirect$1(url, init);
response.headers.set("X-Remix-Reload-Document", "true");
return response;
};
/**
* A redirect [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
* that will perform a [`history.replaceState`](https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState)
* instead of a [`history.pushState`](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState)
* for client-side navigation redirects. Sets the status code and the [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)
* header. Defaults to [`302 Found`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302).
*
* @example
* import { replace } from "react-router";
*
* export async function loader() {
* return replace("/new-location");
* }
*
* @public
* @category Utils
* @mode framework
* @mode data
* @param url The URL to redirect to.
* @param init The status code or a `ResponseInit` object to be included in the
* response.
* @returns A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
* object with the redirect status and [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)
* header.
*/
const replace$1 = (url, init) => {
let response = redirect$1(url, init);
response.headers.set("X-Remix-Replace", "true");
return response;
};
var ErrorResponseImpl = class {
status;
statusText;
data;
error;
internal;
constructor(status, statusText, data, internal = false) {
this.status = status;
this.statusText = statusText || "";
this.internal = internal;
if (data instanceof Error) {
this.data = data.toString();
this.error = data;
} else this.data = data;
}
};
/**
* Check if the given error is an {@link ErrorResponse} generated from a 4xx/5xx
* [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
* thrown from an [`action`](../../start/framework/route-module#action) or
* [`loader`](../../start/framework/route-module#loader) function.
*
* @example
* import { isRouteErrorResponse } from "react-router";
*
* export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
* if (isRouteErrorResponse(error)) {
* return (
* <>
* <p>Error: `${error.status}: ${error.statusText}`</p>
* <p>{error.data}</p>
* </>
* );
* }
*
* return (
* <p>Error: {error instanceof Error ? error.message : "Unknown Error"}</p>
* );
* }
*
* @public
* @category Utils
* @mode framework
* @mode data
* @param error The error to check.
* @returns `true` if the error is an {@link ErrorResponse}, `false` otherwise.
*/
function isRouteErrorResponse(error) {
return error != null && typeof error.status === "number" && typeof error.statusText === "string" && typeof error.internal === "boolean" && "data" in error;
}
function getRoutePattern(matches) {
return joinPaths(matches.map((m) => m.route.path).filter(Boolean)) || "/";
}
typeof window !== "undefined" && typeof window.document !== "undefined" && window.document.createElement;
//#endregion
//#region lib/router/router.ts
const validMutationMethodsArr = [
"POST",
"PUT",
"PATCH",
"DELETE"
];
const validMutationMethods = new Set(validMutationMethodsArr);
const validRequestMethodsArr = ["GET", ...validMutationMethodsArr];
const validRequestMethods = new Set(validRequestMethodsArr);
const redirectStatusCodes = new Set([
301,
302,
303,
307,
308
]);
const ResetLoaderDataSymbol = Symbol("ResetLoaderData");
/**
* Create a static handler to perform server-side data loading
*
* @example
* export async function handleRequest(request: Request) {
* let { query, dataRoutes } = createStaticHandler(routes);
* let context = await query(request);
*
* if (context instanceof Response) {
* return context;
* }
*
* let router = createStaticRouter(dataRoutes, context);
* return new Response(
* ReactDOMServer.renderToString(<StaticRouterProvider ... />),
* { headers: { "Content-Type": "text/html" } }
* );
* }
*
* @public
* @category Data Routers
* @mode data
* @param routes The {@link RouteObject | route objects} to create a static
* handler for
* @param opts Options
* @param opts.basename The base URL for the static handler (default: `/`)
* @param opts.future Future flags for the static handler
* @returns A static handler that can be used to query data for the provided
* routes
*/
function createStaticHandler(routes, opts) {
invariant$1(routes.length > 0, "You must provide a non-empty routes array to createStaticHandler");
let manifest = {};
let basename = (opts ? opts.basename : null) || "/";
let _mapRouteProperties = opts?.mapRouteProperties;
let mapRouteProperties = _mapRouteProperties ? _mapRouteProperties : () => ({});
({ ...opts?.future });
if (opts?.instrumentations) {
let instrumentations = opts.instrumentations;
mapRouteProperties = (route) => {
return {
..._mapRouteProperties?.(route),
...getRouteInstrumentationUpdates(instrumentations.map((i) => i.route).filter(Boolean), route)
};
};
}
let dataRoutes = convertRoutesToDataRoutes(routes, mapRouteProperties, void 0, manifest);
let routeBranches = flattenAndRankRoutes(dataRoutes);
/**
* The query() method is intended for document requests, in which we want to
* call an optional action and potentially multiple loaders for all nested
* routes. It returns a StaticHandlerContext object, which is very similar
* to the router state (location, loaderData, actionData, errors, etc.) and
* also adds SSR-specific information such as the statusCode and headers
* from action/loaders Responses.
*
* It _should_ never throw and should report all errors through the
* returned handlerContext.errors object, properly associating errors to
* their error boundary. Additionally, it tracks _deepestRenderedBoundaryId
* which can be used to emulate React error boundaries during SSR by performing
* a second pass only down to the boundaryId.
*
* The one exception where we do not return a StaticHandlerContext is when a
* redirect response is returned or thrown from any action/loader. We
* propagate that out and return the raw Response so the HTTP server can
* return it directly.
*
* - `opts.requestContext` is an optional server context that will be passed
* to actions/loaders in the `context` parameter
* - `opts.skipLoaderErrorBubbling` is an optional parameter that will prevent
* the bubbling of errors which allows single-fetch-type implementations
* where the client will handle the bubbling and we may need to return data
* for the handling route
*/
async function query(request, { requestContext, filterMatchesToLoad, skipLoaderErrorBubbling, skipRevalidation, dataStrategy, generateMiddlewareResponse, normalizePath } = {}) {
let normalizePathImpl = normalizePath || defaultNormalizePath;
let method = request.method;
let location = createLocation("", normalizePathImpl(request), null, "default");
let matches = matchRoutesImpl(dataRoutes, location, basename, false, routeBranches);
requestContext = requestContext != null ? requestContext : new RouterContextProvider();
if (!isValidMethod(method) && method !== "HEAD") {
let error = getInternalRouterError(405, { method });
let { matches: methodNotAllowedMatches, route } = getShortCircuitMatches(dataRoutes);
let staticContext = {
basename,
location,
matches: methodNotAllowedMatches,
loaderData: {},
actionData: null,
errors: { [route.id]: error },
statusCode: error.status,
loaderHeaders: {},
actionHeaders: {}
};
return generateMiddlewareResponse ? generateMiddlewareResponse(() => Promise.resolve(staticContext)) : staticContext;
} else if (!matches) {
let error = getInternalRouterError(404, { pathname: location.pathname });
let { matches: notFoundMatches, route } = getShortCircuitMatches(dataRoutes);
let staticContext = {
basename,
location,
matches: notFoundMatches,
loaderData: {},
actionData: null,
errors: { [route.id]: error },
statusCode: error.status,
loaderHeaders: {},
actionHeaders: {}
};
return generateMiddlewareResponse ? generateMiddlewareResponse(() => Promise.resolve(staticContext)) : staticContext;
}
if (generateMiddlewareResponse) {
invariant$1(requestContext instanceof RouterContextProvider, "When using middleware in `staticHandler.query()`, any provided `requestContext` must be an instance of `RouterContextProvider`");
try {
await loadLazyMiddlewareForMatches(matches, manifest, mapRouteProperties);
let renderedStaticContext;
let response = await runServerMiddlewarePipeline({
request,
url: createDataFunctionUrl(request, location),
pattern: getRoutePattern(matches),
matches,
params: matches[0].params,
context: requestContext
}, async () => {
return await generateMiddlewareResponse(async (revalidationRequest, opts = {}) => {
let result = await queryImpl(revalidationRequest, location, matches, requestContext, dataStrategy || null, skipLoaderErrorBubbling === true, null, "filterMatchesToLoad" in opts ? opts.filterMatchesToLoad ?? null : filterMatchesToLoad ?? null, skipRevalidation === true);
if (isResponse(result)) return result;
renderedStaticContext = {
location,
basename,
...result
};
return renderedStaticContext;
});
}, async (error, routeId) => {
if (isRedirectResponse(error)) return error;
if (isResponse(error)) try {
error = new ErrorResponseImpl(error.status, error.statusText, await parseResponseBody(error));
} catch (e) {
error = e;
}
if (isDataWithResponseInit(error)) error = dataWithResponseInitToErrorResponse(error);
if (renderedStaticContext) {
if (routeId in renderedStaticContext.loaderData) renderedStaticContext.loaderData[routeId] = void 0;
let staticContext = getStaticContextFromError(dataRoutes, renderedStaticContext, error, skipLoaderErrorBubbling ? routeId : findNearestBoundary(matches, routeId).route.id);
return generateMiddlewareResponse(() => Promise.resolve(staticContext));
} else {
let staticContext = {
matches,
location,
basename,
loaderData: {},
actionData: null,
errors: { [skipLoaderErrorBubbling ? routeId : findNearestBoundary(matches, matches.find((m) => m.route.id === routeId || m.route.loader)?.route.id || routeId).route.id]: error },
statusCode: isRouteErrorResponse(error) ? error.status : 500,
actionHeaders: {},
loaderHeaders: {}
};
return generateMiddlewareResponse(() => Promise.resolve(staticContext));
}
});
invariant$1(isResponse(response), "Expected a response in query()");
return response;
} catch (e) {
if (isResponse(e)) return e;
throw e;
}
}
let result = await queryImpl(request, location, matches, requestContext, dataStrategy || null, skipLoaderErrorBubbling === true, null, filterMatchesToLoad || null, skipRevalidation === true);
if (isResponse(result)) return result;
return {
location,
basename,
...result
};
}
/**
* The queryRoute() method is intended for targeted route requests, either
* for fetch ?_data requests or resource route requests. In this case, we
* are only ever calling a single action or loader, and we are returning the
* returned value directly. In most cases, this will be a Response returned
* from the action/loader, but it may be a primitive or other value as well -
* and in such cases the calling context should handle that accordingly.
*
* We do respect the throw/return differentiation, so if an action/loader
* throws, then this method will throw the value. This is important so we
* can do proper boundary identification in Remix where a thrown Response
* must go to the Catch Boundary but a returned Response is happy-path.
*
* One thing to note is that any Router-initiated Errors that make sense
* to associate with a status code will be thrown as an ErrorResponse
* instance which include the raw Error, such that the calling context can
* serialize the error as they see fit while including the proper response
* code. Examples here are 404 and 405 errors that occur prior to reaching
* any user-defined loaders.
*
* - `opts.routeId` allows you to specify the specific route handler to call.
* If not provided the handler will determine the proper route by matching
* against `request.url`
* - `opts.requestContext` is an optional server context that will be passed
* to actions/loaders in the `context` parameter
*/
async function queryRoute(request, { routeId, requestContext, dataStrategy, generateMiddlewareResponse, normalizePath } = {}) {
let normalizePathImpl = normalizePath || defaultNormalizePath;
let method = request.method;
let location = createLocation("", normalizePathImpl(request), null, "default");
let matches = matchRoutesImpl(dataRoutes, location, basename, false, routeBranches);
requestContext = requestContext != null ? requestContext : new RouterContextProvider();
if (!isValidMethod(method) && method !== "HEAD" && method !== "OPTIONS") throw getInternalRouterError(405, { method });
else if (!matches) throw getInternalRouterError(404, { pathname: location.pathname });
let match = routeId ? matches.find((m) => m.route.id === routeId) : getTargetMatch(matches, location);
if (routeId && !match) throw getInternalRouterError(403, {
pathname: location.pathname,
routeId
});
else if (!match) throw getInternalRouterError(404, { pathname: location.pathname });
if (generateMiddlewareResponse) {
invariant$1(requestContext instanceof RouterContextProvider, "When using middleware in `staticHandler.queryRoute()`, any provided `requestContext` must be an instance of `RouterContextProvider`");
await loadLazyMiddlewareForMatches(matches, manifest, mapRouteProperties);
return await runServerMiddlewarePipeline({
request,
url: createDataFunctionUrl(request, location),
pattern: getRoutePattern(matches),
matches,
params: matches[0].params,
context: requestContext
}, async () => {
return await generateMiddlewareResponse(async (innerRequest) => {
let processed = handleQueryResult(await queryImpl(innerRequest, location, matches, requestContext, dataStrategy || null, false, match, null, false));
return isResponse(processed) ? processed : typeof processed === "string" ? new Response(processed) : Response.json(processed);
});
}, (error) => {
if (isDataWithResponseInit(error)) return Promise.resolve(dataWithResponseInitToResponse(error));
if (isResponse(error)) return Promise.resolve(error);
throw error;
});
}
return handleQueryResult(await queryImpl(request, location, matches, requestContext, dataStrategy || null, false, match, null, false));
function handleQueryResult(result) {
if (isResponse(result)) return result;
let error = result.errors ? Object.values(result.errors)[0] : void 0;
if (error !== void 0) throw error;
if (result.actionData) return Object.values(result.actionData)[0];
if (result.loaderData) return Object.values(result.loaderData)[0];
}
}
async function queryImpl(request, location, matches, requestContext, dataStrategy, skipLoaderErrorBubbling, routeMatch, filterMatchesToLoad, skipRevalidation) {
invariant$1(request.signal, "query()/queryRoute() requests must contain an AbortController signal");
try {
if (isMutationMethod(request.method)) return await submit(request, location, matches, routeMatch || getTargetMatch(matches, location), requestContext, dataStrategy, skipLoaderErrorBubbling, routeMatch != null, filterMatchesToLoad, skipRevalidation);
let result = await loadRouteData(request, location, matches, requestContext, dataStrategy, skipLoaderErrorBubbling, routeMatch, filterMatchesToLoad);
return isResponse(result) ? result : {
...result,
actionData: null,
actionHeaders: {}
};
} catch (e) {
if (isDataStrategyResult(e) && isResponse(e.result)) {
if (e.type === "error") throw e.result;
return e.result;
}
if (isRedirectResponse(e)) return e;
throw e;
}
}
async function submit(request, location, matches, actionMatch, requestContext, dataStrategy, skipLoaderErrorBubbling, isRouteRequest, filterMatchesToLoad, skipRevalidation) {
let result;
if (!actionMatch.route.action && !actionMatch.route.lazy) {
let error = getInternalRouterError(405, {
method: request.method,
pathname: new URL(request.url).pathname,
routeId: actionMatch.route.id
});
if (isRouteRequest) throw error;
result = {
type: "error",
error
};
} else {
result = (await callDataStrategy(request, location, getTargetedDataStrategyMatches(mapRouteProperties, manifest, request, location, matches, actionMatch, [], requestContext), isRouteRequest, requestContext, dataStrategy))[actionMatch.route.id];
if (request.signal.aborted) throwStaticHandlerAbortedError(request, isRouteRequest);
}
if (isRedirectResult(result)) throw new Response(null, {
status: result.response.status,
headers: { Location: result.response.headers.get("Location") }
});
if (isRouteRequest) {
if (isErrorResult(result)) throw result.error;
return {
matches: [actionMatch],
loaderData: {},
actionData: { [actionMatch.route.id]: result.data },
errors: null,
statusCode: 200,
loaderHeaders: {},
actionHeaders: {}
};
}
if (skipRevalidation) if (isErrorResult(result)) {
let boundaryMatch = skipLoaderErrorBubbling ? actionMatch : findNearestBoundary(matches, actionMatch.route.id);
return {
statusCode: isRouteErrorResponse(result.error) ? result.error.status : result.statusCode != null ? result.statusCode : 500,
actionData: null,
actionHeaders: { ...result.headers ? { [actionMatch.route.id]: result.headers } : {} },
matches,
loaderData: {},
errors: { [boundaryMatch.route.id]: result.error },
loaderHeaders: {}
};
} else return {
actionData: { [actionMatch.route.id]: result.data },
actionHeaders: result.headers ? { [actionMatch.route.id]: result.headers } : {},
matches,
loaderData: {},
errors: null,
statusCode: result.statusCode || 200,
loaderHeaders: {}
};
let loaderRequest = new Request(request.url, {
headers: request.headers,
redirect: request.redirect,
signal: request.signal
});
if (isErrorResult(result)) return {
...await loadRouteData(loaderRequest, location, matches, requestContext, dataStrategy, skipLoaderErrorBubbling, null, filterMatchesToLoad, [(skipLoaderErrorBubbling ? actionMatch : findNearestBoundary(matches, actionMatch.route.id)).route.id, result]),
statusCode: isRouteErrorResponse(result.error) ? result.error.status : result.statusCode != null ? result.statusCode : 500,
actionData: null,
actionHeaders: { ...result.headers ? { [actionMatch.route.id]: result.headers } : {} }
};
return {
...await loadRouteData(loaderRequest, location, matches, requestContext, dataStrategy, skipLoaderErrorBubbling, null, filterMatchesToLoad),
actionData: { [actionMatch.route.id]: result.data },
...result.statusCode ? { statusC