| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 | |
| 6 | |
| 7 | |
| 8 | |
| 9 | |
| 10 |
|
| 11 | import { invariant, parsePath, warning } from "./history.js";
|
| 12 | import { decodePath, explodeOptionalSegments, joinPaths, matchPath, normalizePathname, stripBasename } from "./utils.js";
|
| 13 | import { createMultiMatcher } from "@remix-run/route-pattern/match";
|
| 14 | import { descending } from "@remix-run/route-pattern/specificity";
|
| 15 |
|
| 16 | var 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 | };
|
| 56 | function 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 | }
|
| 59 | function 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 | }
|
| 71 | function 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 | }
|
| 84 | function addOptionalTrailingSlash(routePattern) {
|
| 85 | return routePattern === "/" ? routePattern : `${routePattern.replace(/\/+$/, "")}(/)`;
|
| 86 | }
|
| 87 | function 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 | }
|
| 118 | function 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 | }
|
| 141 | function isOptionalRouteSegment(segment) {
|
| 142 | return segment.endsWith("?");
|
| 143 | }
|
| 144 | function 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 | }
|
| 154 | function escapeRoutePatternLiteral(value) {
|
| 155 | return value.replace(/[\\():*]/g, "\\$&");
|
| 156 | }
|
| 157 | function 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 | }
|
| 160 | function 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 | }
|
| 167 | function hasParamValidators(match) {
|
| 168 | return match.data.routesMeta.some((meta) => meta.route.unstable_validateParams != null && Object.keys(meta.route.unstable_validateParams).length > 0);
|
| 169 | }
|
| 170 | function 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 | }
|
| 176 | function 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 |
|
| 216 | export { RoutePatternDataRouteMatcher };
|