INDIE UNPKG

34.7 kBJavaScriptView Raw
1/**
2 * react-router v8.5.0
3 *
4 * Copyright (c) Remix Software Inc.
5 *
6 * This source code is licensed under the MIT license found in the
7 * LICENSE.md file in the root directory of this source tree.
8 *
9 * @license MIT
10 */
11import { ABSOLUTE_URL_REGEX, PROTOCOL_RELATIVE_URL_REGEX, normalizeProtocolRelativeUrl } from "./url.js";
12import { invariant, parsePath, warning } from "./history.js";
13import * as React$1 from "react";
14//#region lib/router/utils.ts
15/**
16* Creates a type-safe {@link RouterContext} object that can be used to
17* store and retrieve arbitrary values in [`action`](../../start/framework/route-module#action)s,
18* [`loader`](../../start/framework/route-module#loader)s, and [middleware](../../how-to/middleware).
19* Similar to React's [`createContext`](https://react.dev/reference/react/createContext),
20* but specifically designed for React Router's request/response lifecycle.
21*
22* If a `defaultValue` is provided, it will be returned from `context.get()`
23* when no value has been set for the context. Otherwise, reading this context
24* when no value has been set will throw an error.
25*
26* ```tsx filename=app/context.ts
27* import { createContext } from "react-router";
28*
29* // Create a context for user data
30* export const userContext =
31* createContext<User | null>(null);
32* ```
33*
34* ```tsx filename=app/middleware/auth.ts
35* import { getUserFromSession } from "~/auth.server";
36* import { userContext } from "~/context";
37*
38* export const authMiddleware = async ({
39* context,
40* request,
41* }) => {
42* const user = await getUserFromSession(request);
43* context.set(userContext, user);
44* };
45* ```
46*
47* ```tsx filename=app/routes/profile.tsx
48* import { userContext } from "~/context";
49*
50* export async function loader({
51* context,
52* }: Route.LoaderArgs) {
53* const user = context.get(userContext);
54*
55* if (!user) {
56* throw new Response("Unauthorized", { status: 401 });
57* }
58*
59* return { user };
60* }
61* ```
62*
63* @public
64* @category Utils
65* @mode framework
66* @mode data
67* @param defaultValue An optional default value for the context. This value
68* will be returned if no value has been set for this context.
69* @returns A {@link RouterContext} object that can be used with
70* `context.get()` and `context.set()` in [`action`](../../start/framework/route-module#action)s,
71* [`loader`](../../start/framework/route-module#loader)s, and [middleware](../../how-to/middleware).
72*/
73function createContext(defaultValue) {
74 return { defaultValue };
75}
76/**
77* Provides methods for writing/reading values in application context in a
78* type-safe way. Primarily for usage with [middleware](../../how-to/middleware).
79*
80* @example
81* import {
82* createContext,
83* RouterContextProvider
84* } from "react-router";
85*
86* const userContext = createContext<User | null>(null);
87* const contextProvider = new RouterContextProvider();
88* contextProvider.set(userContext, getUser());
89* // ^ Type-safe
90* const user = contextProvider.get(userContext);
91* // ^ User
92*
93* @public
94* @category Utils
95* @mode framework
96* @mode data
97*/
98var RouterContextProvider = class {
99 #map = /* @__PURE__ */ new Map();
100 /**
101 * Create a new `RouterContextProvider` instance
102 * @param init An optional initial context map to populate the provider with
103 */
104 constructor(init) {
105 if (init) for (let [context, value] of init) this.set(context, value);
106 }
107 /**
108 * Access a value from the context. If no value has been set for the context,
109 * it will return the context's `defaultValue` if provided, or throw an error
110 * if no `defaultValue` was set.
111 * @param context The context to get the value for
112 * @returns The value for the context, or the context's `defaultValue` if no
113 * value was set
114 */
115 get(context) {
116 if (this.#map.has(context)) return this.#map.get(context);
117 if (context.defaultValue !== void 0) return context.defaultValue;
118 throw new Error("No value found for context");
119 }
120 /**
121 * Set a value for the context. If the context already has a value set, this
122 * will overwrite it.
123 *
124 * @param context The context to set the value for
125 * @param value The value to set for the context
126 * @returns {void}
127 */
128 set(context, value) {
129 this.#map.set(context, value);
130 }
131};
132var DiscoveryCancelledError = class extends Error {};
133const unsupportedLazyRouteObjectKeys = new Set([
134 "lazy",
135 "caseSensitive",
136 "path",
137 "id",
138 "index",
139 "children",
140 "unstable_validateParams"
141]);
142function isUnsupportedLazyRouteObjectKey(key) {
143 return unsupportedLazyRouteObjectKeys.has(key);
144}
145const unsupportedLazyRouteFunctionKeys = new Set([
146 "lazy",
147 "caseSensitive",
148 "path",
149 "id",
150 "index",
151 "middleware",
152 "children",
153 "unstable_validateParams"
154]);
155function isUnsupportedLazyRouteFunctionKey(key) {
156 return unsupportedLazyRouteFunctionKeys.has(key);
157}
158function isIndexRoute(route) {
159 return route.index === true;
160}
161function defaultMapRouteProperties(route) {
162 let updates = {};
163 if (route.Component) {
164 if (route.element) warning(false, "You should not include both `Component` and `element` on your route - `Component` will be used.");
165 Object.assign(updates, {
166 element: React$1.createElement(route.Component),
167 Component: void 0
168 });
169 }
170 if (route.HydrateFallback) {
171 if (route.hydrateFallbackElement) warning(false, "You should not include both `HydrateFallback` and `hydrateFallbackElement` on your route - `HydrateFallback` will be used.");
172 Object.assign(updates, {
173 hydrateFallbackElement: React$1.createElement(route.HydrateFallback),
174 HydrateFallback: void 0
175 });
176 }
177 if (route.ErrorBoundary) {
178 if (route.errorElement) warning(false, "You should not include both `ErrorBoundary` and `errorElement` on your route - `ErrorBoundary` will be used.");
179 Object.assign(updates, {
180 errorElement: React$1.createElement(route.ErrorBoundary),
181 ErrorBoundary: void 0
182 });
183 }
184 return updates;
185}
186function convertRoutesToDataRoutes(routes, mapRouteProperties = defaultMapRouteProperties, parentPath = [], manifest = {}, allowInPlaceMutations = false) {
187 return routes.map((route, index) => {
188 let treePath = [...parentPath, String(index)];
189 let id = typeof route.id === "string" ? route.id : treePath.join("-");
190 invariant(route.index !== true || !route.children, `Cannot specify children on an index route`);
191 invariant(allowInPlaceMutations || !manifest[id], `Found a route id collision on id "${id}". Route id's must be globally unique within Data Router usages`);
192 if (isIndexRoute(route)) {
193 let indexRoute = {
194 ...route,
195 id
196 };
197 manifest[id] = mergeRouteUpdates(indexRoute, mapRouteProperties(indexRoute));
198 return indexRoute;
199 } else {
200 let pathOrLayoutRoute = {
201 ...route,
202 id,
203 children: void 0
204 };
205 manifest[id] = mergeRouteUpdates(pathOrLayoutRoute, mapRouteProperties(pathOrLayoutRoute));
206 if (route.children) pathOrLayoutRoute.children = convertRoutesToDataRoutes(route.children, mapRouteProperties, treePath, manifest, allowInPlaceMutations);
207 return pathOrLayoutRoute;
208 }
209 });
210}
211function mergeRouteUpdates(route, updates) {
212 return Object.assign(route, {
213 ...updates,
214 ...typeof updates.lazy === "object" && updates.lazy != null ? { lazy: {
215 ...route.lazy,
216 ...updates.lazy
217 } } : {}
218 });
219}
220/**
221* Matches the given routes to a location and returns the match data.
222*
223* @example
224* import { matchRoutes } from "react-router";
225*
226* let routes = [{
227* path: "/",
228* Component: Root,
229* children: [{
230* path: "dashboard",
231* Component: Dashboard,
232* }]
233* }];
234*
235* matchRoutes(routes, "/dashboard"); // [rootMatch, dashboardMatch]
236*
237* @public
238* @category Utils
239* @param routes The array of route objects to match against.
240* @param locationArg The location to match against, either a string path or a
241* partial {@link Location} object
242* @param basename Optional base path to strip from the location before matching.
243* Defaults to `/`.
244* @returns An array of matched routes, or `null` if no matches were found.
245*/
246function matchRoutes(routes, locationArg, basename = "/") {
247 return matchRoutesImpl(routes, locationArg, basename, false);
248}
249function matchRoutesImpl(routes, locationArg, basename, allowPartial, precomputedBranches) {
250 let pathname = stripBasename((typeof locationArg === "string" ? parsePath(locationArg) : locationArg).pathname || "/", basename);
251 if (pathname == null) return null;
252 let branches = precomputedBranches ?? flattenAndRankRoutes(routes);
253 let matches = null;
254 let decoded = decodePath(pathname);
255 for (let i = 0; matches == null && i < branches.length; ++i) matches = matchRouteBranch(branches[i], decoded, allowPartial);
256 return matches;
257}
258function convertRouteMatchToUiMatch(match, loaderData) {
259 let { route, pathname, params } = match;
260 return {
261 id: route.id,
262 pathname,
263 params,
264 loaderData: loaderData[route.id],
265 handle: route.handle
266 };
267}
268function flattenAndRankRoutes(routes) {
269 let branches = flattenRoutes(routes);
270 rankRouteBranches(branches);
271 return branches;
272}
273function flattenRoutes(routes, branches = [], parentsMeta = [], parentPath = "", _hasParentOptionalSegments = false) {
274 let flattenRoute = (route, index, hasParentOptionalSegments = _hasParentOptionalSegments, relativePath) => {
275 let meta = {
276 relativePath: relativePath === void 0 ? route.path || "" : relativePath,
277 caseSensitive: route.caseSensitive === true,
278 childrenIndex: index,
279 route
280 };
281 if (meta.relativePath.startsWith("/")) {
282 if (!meta.relativePath.startsWith(parentPath) && hasParentOptionalSegments) return;
283 invariant(meta.relativePath.startsWith(parentPath), `Absolute route path "${meta.relativePath}" nested under path "${parentPath}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`);
284 meta.relativePath = meta.relativePath.slice(parentPath.length);
285 }
286 let path = joinPaths([parentPath, meta.relativePath]);
287 let routesMeta = parentsMeta.concat(meta);
288 if (route.children && route.children.length > 0) {
289 invariant(route.index !== true, `Index routes must not have child routes. Please remove all child routes from route path "${path}".`);
290 flattenRoutes(route.children, branches, routesMeta, path, hasParentOptionalSegments);
291 }
292 if (route.path == null && !route.index) return;
293 branches.push({
294 path,
295 score: computeScore(path, route.index),
296 routesMeta: routesMeta.map((meta, i) => {
297 let [matcher, params] = compilePath(meta.relativePath, meta.caseSensitive, i === routesMeta.length - 1);
298 return {
299 ...meta,
300 matcher,
301 compiledParams: params
302 };
303 })
304 });
305 };
306 routes.forEach((route, index) => {
307 if (route.path === "" || !route.path?.includes("?")) flattenRoute(route, index);
308 else for (let exploded of explodeOptionalSegments(route.path)) flattenRoute(route, index, true, exploded);
309 });
310 return branches;
311}
312function explodeOptionalSegments(path) {
313 let segments = path.split("/");
314 if (segments.length === 0) return [];
315 let [first, ...rest] = segments;
316 let isOptional = first.endsWith("?");
317 let required = first.replace(/\?$/, "");
318 if (rest.length === 0) return isOptional ? [required, ""] : [required];
319 let restExploded = explodeOptionalSegments(rest.join("/"));
320 let result = [];
321 result.push(...restExploded.map((subpath) => subpath === "" ? required : [required, subpath].join("/")));
322 if (isOptional) result.push(...restExploded);
323 return result.map((exploded) => path.startsWith("/") && exploded === "" ? "/" : exploded);
324}
325function rankRouteBranches(branches) {
326 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)));
327}
328const paramRe = /^:[\w-]+$/;
329const partialParamRe = /^:[\w-]+/;
330const partialDynamicSegmentValue = 3.5;
331const dynamicSegmentValue = 3;
332const indexRouteValue = 2;
333const emptySegmentValue = 1;
334const staticSegmentValue = 10;
335const splatPenalty = -2;
336const isSplat = (s) => s === "*";
337function computeScore(path, index) {
338 let segments = path.split("/");
339 let initialScore = segments.length;
340 if (segments.some(isSplat)) initialScore += splatPenalty;
341 if (index) initialScore += indexRouteValue;
342 return segments.filter((s) => !isSplat(s)).reduce((score, segment) => score + (paramRe.test(segment) ? dynamicSegmentValue : partialParamRe.test(segment) ? partialDynamicSegmentValue : segment === "" ? emptySegmentValue : staticSegmentValue), initialScore);
343}
344function compareIndexes(a, b) {
345 return a.length === b.length && a.slice(0, -1).every((n, i) => n === b[i]) ? a[a.length - 1] - b[b.length - 1] : 0;
346}
347function matchRouteBranch(branch, pathname, allowPartial = false) {
348 let { routesMeta } = branch;
349 let matchedParams = {};
350 let matchedPathname = "/";
351 let matches = [];
352 for (let i = 0; i < routesMeta.length; ++i) {
353 let meta = routesMeta[i];
354 let end = i === routesMeta.length - 1;
355 let remainingPathname = matchedPathname === "/" ? pathname : pathname.slice(matchedPathname.length) || "/";
356 let pattern = {
357 path: meta.relativePath,
358 caseSensitive: meta.caseSensitive,
359 end
360 };
361 let match = meta.matcher && meta.compiledParams ? matchPathImpl(pattern, remainingPathname, meta.matcher, meta.compiledParams) : matchPath(pattern, remainingPathname);
362 let route = meta.route;
363 if (!match && end && allowPartial && !routesMeta[routesMeta.length - 1].route.index) match = matchPath({
364 path: meta.relativePath,
365 caseSensitive: meta.caseSensitive,
366 end: false
367 }, remainingPathname);
368 if (!match) return null;
369 Object.assign(matchedParams, match.params);
370 matches.push({
371 params: matchedParams,
372 pathname: joinPaths([matchedPathname, match.pathname]),
373 pathnameBase: normalizePathname(joinPaths([matchedPathname, match.pathnameBase])),
374 route
375 });
376 if (match.pathnameBase !== "/") matchedPathname = joinPaths([matchedPathname, match.pathnameBase]);
377 }
378 return matches;
379}
380/**
381* Characters that `encodeURIComponent` escapes but that are valid literally in
382* a URL path segment. Per RFC 3986 §3.3, a path segment is made of `pchar`:
383*
384* ```
385* pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
386* sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
387* ```
388*
389* `encodeURIComponent` targets query-string values, where `$ & + , ; = : @`
390* are delimiters and must be escaped — but in a path segment they carry no
391* special meaning, and browsers keep them literal in `location.pathname`.
392* (`! ' ( ) *` and the unreserved set are already left alone by
393* `encodeURIComponent`, so they need no restoring.)
394*/
395const PATH_PARAM_OVERESCAPED = {
396 "%24": "$",
397 "%26": "&",
398 "%2B": "+",
399 "%2C": ",",
400 "%3A": ":",
401 "%3B": ";",
402 "%3D": "=",
403 "%40": "@"
404};
405/**
406* Encodes a param value for interpolation into a single URL path segment.
407*
408* Escapes characters that would break the path (`/ ? # %`, whitespace,
409* non-ASCII, …) while leaving characters that RFC 3986 permits literally in a
410* path segment untouched. Escaping those would needlessly rewrite URLs — e.g.
411* a semver build param `1.0.0+1` would become `1.0.0%2B1` even though browsers
412* display and match the `+` literally in `location.pathname`.
413*
414* See [RFC 3986 §3.3](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3))
415*
416* @param value The param value to encode.
417* @returns The encoded value, safe for use as a single path segment.
418*/
419function encodePathParam(value) {
420 return encodeURIComponent(value).replace(/%(?:24|26|2B|2C|3A|3B|3D|40)/g, (match) => PATH_PARAM_OVERESCAPED[match]);
421}
422/**
423* Returns a path with params interpolated.
424*
425* Param values are percent-encoded for use in a path segment: characters that
426* would change the URL structure (`/`, `?`, `#`, `%`, whitespace, non-ASCII)
427* are escaped, while characters that RFC 3986 allows literally in a path
428* segment (`$ & + , ; = : @`) are kept as-is. Note this differs from query-string
429* encoding (`encodeURIComponent`/`URLSearchParams`), where those characters are
430* delimiters and must be escaped. Splat (`*`) values are encoded per segment,
431* preserving `/` separators.
432*
433* See [RFC 3986 §3.3](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3)
434*
435* @example
436* import { generatePath } from "react-router";
437*
438* generatePath("/users/:id", { id: "123" }); // "/users/123"
439* generatePath("/files/:name", { name: "a b" }); // "/files/a%20b"
440* generatePath("/releases/:v", { v: "1.0.0+1" }); // "/releases/1.0.0+1"
441*
442* @public
443* @category Utils
444* @param originalPath The original path to generate.
445* @param params The parameters to interpolate into the path.
446* @returns The generated path with parameters interpolated.
447*/
448function generatePath(originalPath, params = {}) {
449 let path = originalPath;
450 if (path.endsWith("*") && path !== "*" && !path.endsWith("/*")) {
451 warning(false, `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(/\*$/, "/*")}".`);
452 path = path.replace(/\*$/, "/*");
453 }
454 const prefix = path.startsWith("/") ? "/" : "";
455 const stringify = (p) => p == null ? "" : typeof p === "string" ? p : String(p);
456 return prefix + path.split(/\/+/).map((segment, index, array) => {
457 if (index === array.length - 1 && segment === "*") return stringify(params["*"]);
458 const keyMatch = segment.match(/^:([\w-]+)(\??)(.*)/);
459 if (keyMatch) {
460 const [, key, optional, suffix] = keyMatch;
461 let param = params[key];
462 invariant(optional === "?" || param != null, `Missing ":${key}" param`);
463 return encodePathParam(stringify(param)) + suffix;
464 }
465 return segment.replace(/\?$/g, "");
466 }).filter((segment) => !!segment).join("/");
467}
468/**
469* Performs pattern matching on a URL pathname and returns information about
470* the match.
471*
472* @public
473* @category Utils
474* @param pattern The pattern to match against the URL pathname. This can be a
475* string or a {@link PathPattern} object. If a string is provided, it will be
476* treated as a pattern with `caseSensitive` set to `false` and `end` set to
477* `true`.
478* @param pathname The URL pathname to match against the pattern.
479* @returns A path match object if the pattern matches the pathname,
480* or `null` if it does not match.
481*/
482function matchPath(pattern, pathname) {
483 if (typeof pattern === "string") pattern = {
484 path: pattern,
485 caseSensitive: false,
486 end: true
487 };
488 let [matcher, compiledParams] = compilePath(pattern.path, pattern.caseSensitive, pattern.end);
489 return matchPathImpl(pattern, pathname, matcher, compiledParams);
490}
491function matchPathImpl(pattern, pathname, matcher, compiledParams) {
492 let match = pathname.match(matcher);
493 if (!match) return null;
494 let matchedPathname = match[0];
495 let pathnameBase = removeTrailingSlash(matchedPathname, 1);
496 let captureGroups = match.slice(1);
497 return {
498 params: compiledParams.reduce((memo, { paramName, isOptional }, index) => {
499 if (paramName === "*") {
500 let splatValue = captureGroups[index] || "";
501 pathnameBase = removeTrailingSlash(matchedPathname.slice(0, matchedPathname.length - splatValue.length), 1);
502 }
503 const value = captureGroups[index];
504 if (isOptional && !value) memo[paramName] = void 0;
505 else memo[paramName] = (value || "").replace(/%2F/g, "/");
506 return memo;
507 }, {}),
508 pathname: matchedPathname,
509 pathnameBase,
510 pattern
511 };
512}
513function compilePath(path, caseSensitive = false, end = true) {
514 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(/\*$/, "/*")}".`);
515 let params = [];
516 let regexpSource = "^" + path.replace(/\/*\*?$/, "").replace(/^\/*/, "/").replace(/[\\.*+^${}|()[\]]/g, "\\$&").replace(/\/:([\w-]+)(\?)?/g, (match, paramName, isOptional, index, str) => {
517 params.push({
518 paramName,
519 isOptional: isOptional != null
520 });
521 if (isOptional) {
522 let nextChar = str.charAt(index + match.length);
523 if (nextChar && nextChar !== "/") return "/([^\\/]*)";
524 return "(?:/([^\\/]*))?";
525 }
526 return "/([^\\/]+)";
527 }).replace(/\/((?:[^/?()[\]*^\\]|\\.)+)\?(?=\/|$|\()/g, "(?:/$1)?");
528 if (path.endsWith("*")) {
529 params.push({ paramName: "*" });
530 regexpSource += path === "*" || path === "/*" ? "(.*)$" : "(?:\\/(.+)|\\/*)$";
531 } else if (end) regexpSource += "\\/*$";
532 else if (path !== "" && path !== "/") regexpSource += "(?:(?=\\/|$))";
533 return [new RegExp(regexpSource, caseSensitive ? void 0 : "i"), params];
534}
535function decodePath(value) {
536 try {
537 return value.split("/").map((v) => decodeURIComponent(v).replace(/\//g, "%2F")).join("/");
538 } catch (error) {
539 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}).`);
540 return value;
541 }
542}
543function stripBasename(pathname, basename) {
544 if (basename === "/") return pathname;
545 if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) return null;
546 let startIndex = basename.endsWith("/") ? basename.length - 1 : basename.length;
547 let nextChar = pathname.charAt(startIndex);
548 if (nextChar && nextChar !== "/") return null;
549 return pathname.slice(startIndex) || "/";
550}
551function prependBasename({ basename, pathname }) {
552 return pathname === "/" ? basename : joinPaths([basename, pathname]);
553}
554const isAbsoluteUrl = (url) => ABSOLUTE_URL_REGEX.test(url);
555/**
556* Returns a resolved {@link Path} object relative to the given pathname.
557*
558* @public
559* @category Utils
560* @param to The path to resolve, either a string or a partial {@link Path}
561* object.
562* @param fromPathname The pathname to resolve the path from. Defaults to `/`.
563* @returns A {@link Path} object with the resolved pathname, search, and hash.
564*/
565function resolvePath(to, fromPathname = "/") {
566 let { pathname: toPathname, search = "", hash = "" } = typeof to === "string" ? parsePath(to) : to;
567 let pathname;
568 if (toPathname) {
569 toPathname = removeDoubleSlashes(toPathname);
570 if (toPathname.startsWith("/") || toPathname.startsWith("\\")) pathname = resolvePathname(toPathname.substring(1), "/");
571 else pathname = resolvePathname(toPathname, fromPathname);
572 } else pathname = fromPathname;
573 return {
574 pathname,
575 search: normalizeSearch(search),
576 hash: normalizeHash(hash)
577 };
578}
579function resolvePathname(relativePath, fromPathname) {
580 let segments = removeTrailingSlash(fromPathname).split("/");
581 relativePath.split("/").forEach((segment) => {
582 if (segment === "..") {
583 if (segments.length > 1) segments.pop();
584 } else if (segment !== ".") segments.push(segment);
585 });
586 return segments.length > 1 ? segments.join("/") : "/";
587}
588function getInvalidPathError(char, field, dest, path) {
589 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.`;
590}
591function getPathContributingMatches(matches) {
592 return matches.filter((match, index) => index === 0 || match.route.path && match.route.path.length > 0);
593}
594function getResolveToMatches(matches) {
595 let pathMatches = getPathContributingMatches(matches);
596 return pathMatches.map((match, idx) => idx === pathMatches.length - 1 ? match.pathname : match.pathnameBase);
597}
598function resolveTo(toArg, routePathnames, locationPathname, isPathRelative = false) {
599 let to;
600 if (typeof toArg === "string") to = parsePath(toArg);
601 else {
602 to = { ...toArg };
603 invariant(!to.pathname || !to.pathname.includes("?"), getInvalidPathError("?", "pathname", "search", to));
604 invariant(!to.pathname || !to.pathname.includes("#"), getInvalidPathError("#", "pathname", "hash", to));
605 invariant(!to.search || !to.search.includes("#"), getInvalidPathError("#", "search", "hash", to));
606 }
607 let isEmptyPath = toArg === "" || to.pathname === "";
608 let toPathname = isEmptyPath ? "/" : to.pathname;
609 let from;
610 if (toPathname == null) from = locationPathname;
611 else {
612 let routePathnameIndex = routePathnames.length - 1;
613 if (!isPathRelative && toPathname.startsWith("..")) {
614 let toSegments = toPathname.split("/");
615 while (toSegments[0] === "..") {
616 toSegments.shift();
617 routePathnameIndex -= 1;
618 }
619 to.pathname = toSegments.join("/");
620 }
621 from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex] : "/";
622 }
623 let path = resolvePath(to, from);
624 let hasExplicitTrailingSlash = toPathname && toPathname !== "/" && toPathname.endsWith("/");
625 let hasCurrentTrailingSlash = (isEmptyPath || toPathname === ".") && locationPathname.endsWith("/");
626 if (!path.pathname.endsWith("/") && (hasExplicitTrailingSlash || hasCurrentTrailingSlash)) path.pathname += "/";
627 return path;
628}
629const removeDoubleSlashes = (path) => path.replace(/[\\/]{2,}/g, "/");
630const joinPaths = (paths) => removeDoubleSlashes(paths.join("/"));
631function removeTrailingSlash(path, minLength = 0) {
632 let end = path.length;
633 while (end > minLength && path.charCodeAt(end - 1) === 47) end--;
634 return end === path.length ? path : path.slice(0, end);
635}
636const normalizePathname = (pathname) => removeTrailingSlash(pathname).replace(/^\/*/, "/");
637const normalizeSearch = (search) => !search || search === "?" ? "" : search.startsWith("?") ? search : "?" + search;
638const normalizeHash = (hash) => !hash || hash === "#" ? "" : hash.startsWith("#") ? hash : "#" + hash;
639var DataWithResponseInit = class {
640 type = "DataWithResponseInit";
641 data;
642 init;
643 constructor(data, init) {
644 this.data = data;
645 this.init = init || null;
646 }
647};
648/**
649* Create "responses" that contain `headers`/`status` without forcing
650* serialization into an actual [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
651*
652* @example
653* import { data } from "react-router";
654*
655* export async function action({ request }: Route.ActionArgs) {
656* let formData = await request.formData();
657* let item = await createItem(formData);
658* return data(item, {
659* headers: { "X-Custom-Header": "value" }
660* status: 201,
661* });
662* }
663*
664* @public
665* @category Utils
666* @mode framework
667* @mode data
668* @param data The data to be included in the response.
669* @param init The status code or a `ResponseInit` object to be included in the
670* response.
671* @returns A {@link DataWithResponseInit} instance containing the data and
672* response init.
673*/
674function data(data, init) {
675 return new DataWithResponseInit(data, typeof init === "number" ? { status: init } : init);
676}
677/**
678* A redirect [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response).
679* Sets the status code and the [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)
680* header. Defaults to [`302 Found`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302).
681*
682* This utility accepts absolute URLs and can navigate to external domains, so
683* the application should validate any user-supplied inputs to redirects.
684*
685* @example
686* import { redirect } from "react-router";
687*
688* export async function loader({ request }: Route.LoaderArgs) {
689* if (!isLoggedIn(request))
690* throw redirect("/login");
691* }
692*
693* // ...
694* }
695*
696* @public
697* @category Utils
698* @mode framework
699* @mode data
700* @param url The URL to redirect to.
701* @param init The status code or a `ResponseInit` object to be included in the
702* response.
703* @returns A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
704* object with the redirect status and [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)
705* header.
706*/
707const redirect = (url, init = 302) => {
708 let responseInit = init;
709 if (typeof responseInit === "number") responseInit = { status: responseInit };
710 else if (typeof responseInit.status === "undefined") responseInit.status = 302;
711 let headers = new Headers(responseInit.headers);
712 headers.set("Location", url);
713 return new Response(null, {
714 ...responseInit,
715 headers
716 });
717};
718/**
719* A redirect [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
720* that will force a document reload to the new location. Sets the status code
721* and the [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)
722* header. Defaults to [`302 Found`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302).
723*
724* This utility accepts absolute URLs and can navigate to external domains, so
725* the application should validate any user-supplied inputs to redirects.
726*
727* ```tsx filename=routes/logout.tsx
728* import { redirectDocument } from "react-router";
729*
730* import { destroySession } from "../sessions.server";
731*
732* export async function action({ request }: Route.ActionArgs) {
733* let session = await getSession(request.headers.get("Cookie"));
734* return redirectDocument("/", {
735* headers: { "Set-Cookie": await destroySession(session) }
736* });
737* }
738* ```
739*
740* @public
741* @category Utils
742* @mode framework
743* @mode data
744* @param url The URL to redirect to.
745* @param init The status code or a `ResponseInit` object to be included in the
746* response.
747* @returns A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
748* object with the redirect status and [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)
749* header.
750*/
751const redirectDocument = (url, init) => {
752 let response = redirect(url, init);
753 response.headers.set("X-Remix-Reload-Document", "true");
754 return response;
755};
756/**
757* A redirect [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
758* that will perform a [`history.replaceState`](https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState)
759* instead of a [`history.pushState`](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState)
760* for client-side navigation redirects. Sets the status code and the [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)
761* header. Defaults to [`302 Found`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302).
762*
763* @example
764* import { replace } from "react-router";
765*
766* export async function loader() {
767* return replace("/new-location");
768* }
769*
770* @public
771* @category Utils
772* @mode framework
773* @mode data
774* @param url The URL to redirect to.
775* @param init The status code or a `ResponseInit` object to be included in the
776* response.
777* @returns A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
778* object with the redirect status and [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)
779* header.
780*/
781const replace = (url, init) => {
782 let response = redirect(url, init);
783 response.headers.set("X-Remix-Replace", "true");
784 return response;
785};
786const SUPPORTED_ERROR_TYPES = [
787 "EvalError",
788 "RangeError",
789 "ReferenceError",
790 "SyntaxError",
791 "TypeError",
792 "URIError"
793];
794var ErrorResponseImpl = class {
795 status;
796 statusText;
797 data;
798 error;
799 internal;
800 constructor(status, statusText, data, internal = false) {
801 this.status = status;
802 this.statusText = statusText || "";
803 this.internal = internal;
804 if (data instanceof Error) {
805 this.data = data.toString();
806 this.error = data;
807 } else this.data = data;
808 }
809};
810/**
811* Check if the given error is an {@link ErrorResponse} generated from a 4xx/5xx
812* [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
813* thrown from an [`action`](../../start/framework/route-module#action) or
814* [`loader`](../../start/framework/route-module#loader) function.
815*
816* @example
817* import { isRouteErrorResponse } from "react-router";
818*
819* export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
820* if (isRouteErrorResponse(error)) {
821* return (
822* <>
823* <p>Error: `${error.status}: ${error.statusText}`</p>
824* <p>{error.data}</p>
825* </>
826* );
827* }
828*
829* return (
830* <p>Error: {error instanceof Error ? error.message : "Unknown Error"}</p>
831* );
832* }
833*
834* @public
835* @category Utils
836* @mode framework
837* @mode data
838* @param error The error to check.
839* @returns `true` if the error is an {@link ErrorResponse}, `false` otherwise.
840*/
841function isRouteErrorResponse(error) {
842 return error != null && typeof error.status === "number" && typeof error.statusText === "string" && typeof error.internal === "boolean" && "data" in error;
843}
844function getRoutePattern(matches) {
845 return joinPaths(matches.map((m) => m.route.path).filter(Boolean)) || "/";
846}
847function createDataFunctionUrl(request, path) {
848 let url = new URL(typeof request === "string" || request instanceof URL ? request : request.url);
849 let parsed = typeof path === "string" ? parsePath(path) : path;
850 url.pathname = parsed.pathname || "/";
851 if (parsed.search) {
852 let searchParams = new URLSearchParams(parsed.search);
853 let indexValues = searchParams.getAll("index");
854 searchParams.delete("index");
855 for (let value of indexValues.filter(Boolean)) searchParams.append("index", value);
856 let search = searchParams.toString();
857 url.search = search ? `?${search}` : "";
858 } else url.search = "";
859 url.hash = parsed.hash || "";
860 return url;
861}
862const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined";
863function parseToInfo(_to, basename) {
864 let to = _to;
865 if (typeof to !== "string" || !ABSOLUTE_URL_REGEX.test(to)) return {
866 absoluteURL: void 0,
867 isExternal: false,
868 to
869 };
870 let absoluteURL = to;
871 let isExternal = false;
872 if (isBrowser) try {
873 let currentUrl = new URL(window.location.href);
874 let targetUrl = PROTOCOL_RELATIVE_URL_REGEX.test(to) ? new URL(normalizeProtocolRelativeUrl(to, currentUrl.protocol)) : new URL(to);
875 let path = stripBasename(targetUrl.pathname, basename);
876 if (targetUrl.origin === currentUrl.origin && path != null) to = path + targetUrl.search + targetUrl.hash;
877 else isExternal = true;
878 } catch {
879 warning(false, `<Link to="${to}"> contains an invalid URL which will probably break when clicked - please update to a valid URL path.`);
880 }
881 return {
882 absoluteURL,
883 isExternal,
884 to
885 };
886}
887//#endregion
888export { DiscoveryCancelledError, ErrorResponseImpl, RouterContextProvider, SUPPORTED_ERROR_TYPES, compilePath, convertRouteMatchToUiMatch, convertRoutesToDataRoutes, createContext, createDataFunctionUrl, data, decodePath, defaultMapRouteProperties, encodePathParam, explodeOptionalSegments, flattenAndRankRoutes, generatePath, getPathContributingMatches, getResolveToMatches, getRoutePattern, isAbsoluteUrl, isBrowser, isRouteErrorResponse, isUnsupportedLazyRouteFunctionKey, isUnsupportedLazyRouteObjectKey, joinPaths, matchPath, matchRoutes, matchRoutesImpl, normalizePathname, parseToInfo, prependBasename, redirect, redirectDocument, removeDoubleSlashes, removeTrailingSlash, replace, resolvePath, resolveTo, stripBasename };