UNPKG

react-router

Version:
1,288 lines • 123 kB
/** * 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) { if (route.element) warning(false, "You should not include both `Component` and `element` on your route - `Component` will be used."); Object.assign(updates, { element: React.createElement(route.Component), Component: void 0 }); } if (route.HydrateFallback) { if (route.hydrateFallbackElement) warning(false, "You should not include both `HydrateFallback` and `hydrateFallbackElement` on your route - `HydrateFallback` will be used."); Object.assign(updates, { hydrateFallbackElement: React.createElement(route.HydrateFallback), HydrateFallback: void 0 }); } if (route.ErrorBoundary) { if (route.errorElement) warning(false, "You should not include both `ErrorBoundary` and `errorElement` on your route - `ErrorBoundary` will be used."); 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,