// https://github.com/vercel/next.js/blob/e65c56e7e6c16c3c971984ce7acd161bca6b9c2f/packages/next/shared/lib/router/utils/route-regex.ts interface Group { pos: number repeat: boolean optional: boolean } // this isn't importing the escape-string-regex module // to reduce bytes function escapeRegex(str: string) { return str.replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&") } function parseParameter(param: string) { 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 } } export function getParametrizedRoute(route: string) { const segments = (route.replace(/\/$/, "") || "/").slice(1).split("/") const groups: { [groupName: string]: Group } = {} 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("") // dead code eliminate for browser since it's only needed // while generating routes-manifest if (typeof window === "undefined") { let routeKeyCharCode = 97 let routeKeyCharLength = 1 // builds a minimal routeKey using only a-z and minimal number of characters 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: { [named: string]: string } = {} let namedParameterizedRoute = segments .map((segment) => { if (segment.startsWith("[") && segment.endsWith("]")) { const { key, optional, repeat } = parseParameter(segment.slice(1, -1)) // replace any non-word characters since they can break // the named regex let cleanedKey = key.replace(/\W/g, "") let invalidKey = false // check if the key is still invalid and fallback to using a known // safe key if (cleanedKey.length === 0 || cleanedKey.length > 30) { invalidKey = true } if (!isNaN(parseInt(cleanedKey.substr(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, } } export interface RouteRegex { groups: { [groupName: string]: Group } namedRegex?: string re: RegExp routeKeys?: { [named: string]: string } } export function getRouteRegex(normalizedRoute: string): RouteRegex { 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, } }