@tscircuit/routematch
Version:
TypeScript route matcher with Next.js-style dynamic routes
177 lines • 5.07 kB
JavaScript
// lib/index.ts
function escapeRegex(str) {
return str.replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&");
}
function parseParameter(param) {
const optional = param.startsWith("[") && param.endsWith("]");
if (optional) {
param = param.slice(1, -1);
}
const repeat = param.startsWith("...");
if (repeat) {
param = param.slice(3);
}
return { key: param, repeat, optional };
}
function decodeParam(param) {
try {
return decodeURIComponent(param);
} catch {
throw new Error(`Failed to decode parameter: ${param}`);
}
}
function getParametrizedRoute(route) {
const segments = (route.replace(/\/$/, "") || "/").slice(1).split("/");
const groups = {};
let groupIndex = 1;
const parameterizedRoute = segments.map((segment) => {
if (segment.startsWith("[") && segment.endsWith("]")) {
const { key, optional, repeat } = parseParameter(segment.slice(1, -1));
groups[key] = { pos: groupIndex++, repeat, optional };
return repeat ? optional ? "(?:/(.+?))?" : "/(.+?)" : "/([^/]+?)";
} else {
return `/${escapeRegex(segment)}`;
}
}).join("");
if (typeof globalThis.window === "undefined") {
let routeKeyCharCode = 97;
let routeKeyCharLength = 1;
const getSafeRouteKey = () => {
let routeKey = "";
for (let i = 0; i < routeKeyCharLength; i++) {
routeKey += String.fromCharCode(routeKeyCharCode);
routeKeyCharCode++;
if (routeKeyCharCode > 122) {
routeKeyCharLength++;
routeKeyCharCode = 97;
}
}
return routeKey;
};
const routeKeys = {};
const namedParameterizedRoute = segments.map((segment) => {
if (segment.startsWith("[") && segment.endsWith("]")) {
const { key, optional, repeat } = parseParameter(segment.slice(1, -1));
let cleanedKey = key.replace(/\W/g, "");
let invalidKey = false;
if (cleanedKey.length === 0 || cleanedKey.length > 30) {
invalidKey = true;
}
if (!Number.isNaN(parseInt(cleanedKey.substring(0, 1)))) {
invalidKey = true;
}
if (invalidKey) {
cleanedKey = getSafeRouteKey();
}
routeKeys[cleanedKey] = key;
return repeat ? optional ? `(?:/(?<${cleanedKey}>.+?))?` : `/(?<${cleanedKey}>.+?)` : `/(?<${cleanedKey}>[^/]+?)`;
} else {
return `/${escapeRegex(segment)}`;
}
}).join("");
return {
parameterizedRoute,
namedParameterizedRoute,
groups,
routeKeys
};
}
return {
parameterizedRoute,
groups
};
}
function getRouteRegex(normalizedRoute) {
const result = getParametrizedRoute(normalizedRoute);
if ("routeKeys" in result) {
return {
re: new RegExp(`^${result.parameterizedRoute}(?:/)?$`),
groups: result.groups,
routeKeys: result.routeKeys,
namedRegex: `^${result.namedParameterizedRoute}(?:/)?$`
};
}
return {
re: new RegExp(`^${result.parameterizedRoute}(?:/)?$`),
groups: result.groups
};
}
function createRouteMatcherFunc(routeRegex) {
const { re, groups } = routeRegex;
return (pathname) => {
const routeMatch = re.exec(pathname);
if (!routeMatch) {
return false;
}
const params = {};
for (const [slugName, group] of Object.entries(groups)) {
const match = routeMatch[group.pos];
if (match !== void 0) {
params[slugName] = match.includes("/") ? match.split("/").map(decodeParam) : group.repeat ? [decodeParam(match)] : decodeParam(match);
}
}
return params;
};
}
function createRouteMatcher(routes) {
if (!Array.isArray(routes) || routes.length === 0) {
throw new Error("Routes array cannot be empty");
}
const routeEntries = routes.map((route) => {
if (typeof route !== "string") {
throw new Error("All routes must be strings");
}
const regex = getRouteRegex(route);
const matcher = createRouteMatcherFunc(regex);
const paramCount = Object.keys(regex.groups).length;
const hasWildcard = route.includes("[...");
const priority = hasWildcard ? -1e3 - paramCount : -paramCount;
return {
route,
regex,
matcher,
priority
};
});
routeEntries.sort((a, b) => b.priority - a.priority);
return (path) => {
if (typeof path !== "string") {
return null;
}
for (const entry of routeEntries) {
const params = entry.matcher(path);
if (params !== false) {
return {
matchedRoute: entry.route,
routeParams: params
};
}
}
return null;
};
}
var index_default = createRouteMatcher;
function isValidRoute(route) {
try {
getRouteRegex(route);
return true;
} catch {
return false;
}
}
function getRouteParameters(route) {
try {
const regex = getRouteRegex(route);
return Object.keys(regex.groups);
} catch {
return [];
}
}
export {
createRouteMatcher,
index_default as default,
createRouteMatcher as getRouteMatcher,
getRouteParameters,
isValidRoute
};
//# sourceMappingURL=index.js.map