UNPKG

@tscircuit/routematch

Version:

TypeScript route matcher with Next.js-style dynamic routes

177 lines 5.07 kB
// 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