UNPKG

9.07 kBJavaScriptView Raw
1/**
2 * react-router v8.4.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 { invariant, parsePath, warning } from "./history.js";
12import { decodePath, explodeOptionalSegments, joinPaths, matchPath, normalizePathname, stripBasename } from "./utils.js";
13import { createMultiMatcher } from "@remix-run/route-pattern/match";
14import { descending } from "@remix-run/route-pattern/specificity";
15//#region lib/router/matcher-route-pattern.ts
16var RoutePatternDataRouteMatcher = class {
17 #state;
18 #basename;
19 constructor(basename) {
20 this.#basename = basename;
21 }
22 update(routes) {
23 let branches = flattenRoutes(routes);
24 let matcher = createMultiMatcher({ ignoreCase: true });
25 let partialMatcher = createMultiMatcher({ ignoreCase: true });
26 for (let branch of branches) {
27 let routePattern = convertReactRouterPathToRoutePattern(branch.path);
28 validateRoutePatternSplat(branch.path, routePattern);
29 routePattern = addOptionalTrailingSlash(routePattern);
30 matcher.add(routePattern, branch);
31 if (!branch.routesMeta[branch.routesMeta.length - 1].route.index) partialMatcher.add(routePattern === "/" ? `/*__rr_partial` : `${routePattern}(/*__rr_partial)`, branch);
32 }
33 this.#state = {
34 branches,
35 matcher,
36 partialMatcher
37 };
38 return branches;
39 }
40 match(locationArg, allowPartial = false) {
41 let pathname = stripBasename((typeof locationArg === "string" ? parsePath(locationArg) : locationArg).pathname || "/", this.#basename);
42 if (pathname == null) return null;
43 let decoded = decodePath(pathname);
44 let url = new URL("http://reactrouter.local");
45 url.pathname = pathname.endsWith("/") ? pathname : `${pathname}/`;
46 invariant(this.#state, "Route pattern routes must be initialized before matching.");
47 let matches = this.#state.matcher.matchAll(url);
48 if (allowPartial) matches.push(...this.#state.partialMatcher.matchAll(url));
49 for (let match of prioritizeValidatedMatches(matches)) {
50 let routeMatches = convertRoutePatternMatchToRouteMatches(match, decoded, allowPartial);
51 if (routeMatches && validateRouteMatchParams(routeMatches)) return routeMatches;
52 }
53 return null;
54 }
55};
56function validateRoutePatternSplat(routePath, routePattern) {
57 invariant(getRoutePatternSplatIndexes(routePattern).length <= 1 && isTerminalRoutePatternSplat(routePattern), `Route path "${routePath}" is not supported with \`future.unstable_routePatternMatching\` because React Router only supports a single splat at the end of a route path.`);
58}
59function getRoutePatternSplatIndexes(routePattern) {
60 let splatIndexes = [];
61 for (let i = 0; i < routePattern.length; i++) {
62 let char = routePattern[i];
63 if (char === "\\") {
64 i++;
65 continue;
66 }
67 if (char === "*") splatIndexes.push(i);
68 }
69 return splatIndexes;
70}
71function isTerminalRoutePatternSplat(routePattern) {
72 for (let i = 0; i < routePattern.length; i++) {
73 let char = routePattern[i];
74 if (char === "\\") {
75 i++;
76 continue;
77 }
78 if (char !== "*") continue;
79 let suffix = routePattern.slice(i + 1);
80 return /^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(suffix) || suffix === "";
81 }
82 return true;
83}
84function addOptionalTrailingSlash(routePattern) {
85 return routePattern === "/" ? routePattern : `${routePattern.replace(/\/+$/, "")}(/)`;
86}
87function flattenRoutes(routes, branches = [], parentsMeta = [], parentPath = "") {
88 routes.forEach((route, index) => {
89 invariant(route.caseSensitive !== true, "`caseSensitive` routes are not supported with `future.unstable_routePatternMatching`.");
90 let meta = {
91 relativePath: route.path || "",
92 caseSensitive: false,
93 childrenIndex: index,
94 route
95 };
96 let absolutePath;
97 if (meta.relativePath.startsWith("/")) {
98 absolutePath = meta.relativePath;
99 let parentPathMatch = explodeOptionalSegments(parentPath).find((path) => meta.relativePath.startsWith(path));
100 invariant(parentPathMatch != null, `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.`);
101 meta.relativePath = meta.relativePath.slice(parentPathMatch.length);
102 }
103 let routesMeta = parentsMeta.concat(meta);
104 let path = absolutePath ?? (meta.relativePath ? joinPaths([parentPath, meta.relativePath]) : parentPath);
105 if (route.children && route.children.length > 0) {
106 invariant(route.index !== true, `Index routes must not have child routes. Please remove all child routes from route path "${path}".`);
107 flattenRoutes(route.children, branches, routesMeta, path);
108 if (route.children?.some((child) => child.index)) return;
109 }
110 if (route.path != null || route.index) branches.push({
111 path,
112 score: 0,
113 routesMeta
114 });
115 });
116 return branches;
117}
118function convertReactRouterPathToRoutePattern(path) {
119 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(/\*$/, "/*")}".`);
120 if (path.endsWith("*") && path !== "*" && !path.endsWith("/*")) path = path.replace(/\*$/, "/*");
121 path = path.replace(/^\/*/, "/");
122 if (path === "") return "/";
123 let segments = path.split("/");
124 let pattern = segments.shift() || "";
125 for (let i = 0; i < segments.length;) {
126 let optionalRun = [];
127 while (i < segments.length && isOptionalRouteSegment(segments[i])) {
128 optionalRun.push(convertRouteSegment(segments[i].slice(0, -1)));
129 i++;
130 }
131 if (optionalRun.length > 0) {
132 let nested = optionalRun.reduceRight((child, segment) => `/${segment}${child ? `(${child})` : ""}`, "");
133 pattern += `(${nested})`;
134 continue;
135 }
136 pattern += `/${convertRouteSegment(segments[i])}`;
137 i++;
138 }
139 return pattern || "/";
140}
141function isOptionalRouteSegment(segment) {
142 return segment.endsWith("?");
143}
144function convertRouteSegment(segment) {
145 if (segment === "*") return "*__rr_splat";
146 let paramMatch = segment.match(/^:([\w-]+)(\?)?(.*)$/);
147 if (paramMatch) {
148 let [, paramName, optional, suffix] = paramMatch;
149 let converted = `:${paramName}${escapeRoutePatternLiteral(suffix)}`;
150 return optional ? `(:${paramName})${escapeRoutePatternLiteral(suffix)}` : converted;
151 }
152 return escapeRoutePatternLiteral(segment);
153}
154function escapeRoutePatternLiteral(value) {
155 return value.replace(/[\\():*]/g, "\\$&");
156}
157function validateRouteMatchParams(matches) {
158 return matches.every(({ route, params }) => route.unstable_validateParams == null || Object.keys(route.unstable_validateParams).length === 0 || Object.entries(route.unstable_validateParams).every(([param, re]) => params[param] != null ? new RegExp(re.source, re.flags).test(params[param]) : true));
159}
160function prioritizeValidatedMatches(matches) {
161 return matches.sort((a, b) => {
162 let specificity = descending(a, b);
163 if (specificity !== 0) return specificity;
164 return Number(hasParamValidators(b)) - Number(hasParamValidators(a));
165 });
166}
167function hasParamValidators(match) {
168 return match.data.routesMeta.some((meta) => meta.route.unstable_validateParams != null && Object.keys(meta.route.unstable_validateParams).length > 0);
169}
170function convertRoutePatternMatchToRouteMatches(match, pathname, allowPartial) {
171 let result = matchRoutePatternBranch(match.data, pathname, allowPartial, 0, "/", {});
172 if (result == null) return null;
173 for (let routeMatch of result.matches) routeMatch.params = result.params;
174 return result.matches;
175}
176function matchRoutePatternBranch(branch, pathname, allowPartial, metaIndex, matchedPathname, matchedParams) {
177 let meta = branch.routesMeta[metaIndex];
178 let end = metaIndex === branch.routesMeta.length - 1;
179 let remainingPathname = matchedPathname === "/" ? pathname : pathname.slice(matchedPathname.length) || "/";
180 for (let relativePath of explodeOptionalSegments(meta.relativePath)) {
181 let pattern = {
182 path: relativePath,
183 caseSensitive: false,
184 end
185 };
186 let pathMatch = matchPath(pattern, remainingPathname);
187 if (!pathMatch && end && allowPartial && !meta.route.index) pathMatch = matchPath({
188 ...pattern,
189 end: false
190 }, remainingPathname);
191 if (!pathMatch) continue;
192 let params = {
193 ...matchedParams,
194 ...pathMatch.params
195 };
196 let routeMatch = {
197 params,
198 pathname: joinPaths([matchedPathname, pathMatch.pathname]),
199 pathnameBase: normalizePathname(joinPaths([matchedPathname, pathMatch.pathnameBase])),
200 route: meta.route
201 };
202 let nextMatchedPathname = pathMatch.pathnameBase === "/" ? matchedPathname : joinPaths([matchedPathname, pathMatch.pathnameBase]);
203 if (end) return {
204 matches: [routeMatch],
205 params
206 };
207 let childResult = matchRoutePatternBranch(branch, pathname, allowPartial, metaIndex + 1, nextMatchedPathname, params);
208 if (childResult) return {
209 matches: [routeMatch, ...childResult.matches],
210 params: childResult.params
211 };
212 }
213 return null;
214}
215//#endregion
216export { RoutePatternDataRouteMatcher };