import UrlPattern from "url-pattern"; import queryString from "query-string"; type Routes = { [key: string]: UrlPattern }; type Paths = { [key: string]: string }; export type ViewState = { // route name, e.g. "home" route: string | null; // path name // e.g. "/dashboard" // e.g. "/users/abc123" pathname: string; // params from URL (:) or query (?) // e.g. "/users/:id" // e.g. "/redirect?url" params: { [key: string]: string }; // arbitrary data to pass between routes // eslint-disable-next-line @typescript-eslint/no-explicit-any data?: { [key: string]: any }; }; export default class APIRouter { private readonly routes: Routes; constructor(paths: Paths) { this.routes = {}; for (const [name, route] of Object.entries(paths)) { this.routes[name] = new UrlPattern(route); } } match(relativePath: string): ViewState { for (const [name, pattern] of Object.entries(this.routes)) { const [path, qs = ""] = relativePath.split("?"); const match = pattern.match(path); if (match) { const queryParams = queryString.parse(qs, { // Only decode if needed, or else `+` in invite emails // may be incorrectly decoded decode: qs.includes("%"), }); const params = { ...match, ...queryParams, }; return { route: name, pathname: relativePath, params }; } } return { route: null, pathname: relativePath, params: {} }; } }