UNPKG

rou3

Version:

Lightweight and fast router for JavaScript.

258 lines (250 loc) 7.81 kB
//#region src/object.ts const NullProtoObj = /* @__PURE__ */ (() => { const e = function() {}; return e.prototype = Object.create(null), Object.freeze(e.prototype), e; })(); //#endregion //#region src/context.ts /** * Create a new router context. */ function createRouter() { const ctx = { root: { key: "" }, static: new NullProtoObj() }; return ctx; } //#endregion //#region src/operations/_utils.ts function splitPath(path) { const [_, ...s] = path.split("/"); return s[s.length - 1] === "" ? s.slice(0, -1) : s; } function getMatchParams(segments, paramsMap) { const params = new NullProtoObj(); for (const [index, name] of paramsMap) { const segment = index < 0 ? segments.slice(-1 * index).join("/") : segments[index]; if (typeof name === "string") params[name] = segment; else { const match = segment.match(name); if (match) for (const key in match.groups) params[key] = match.groups[key]; } } return params; } //#endregion //#region src/operations/add.ts /** * Add a route to the router context. */ function addRoute(ctx, method = "", path, data) { const segments = splitPath(path); let node = ctx.root; let _unnamedParamIndex = 0; const paramsMap = []; for (let i = 0; i < segments.length; i++) { const segment = segments[i]; if (segment.startsWith("**")) { if (!node.wildcard) node.wildcard = { key: "**" }; node = node.wildcard; paramsMap.push([ -i, segment.split(":")[1] || "_", segment.length === 2 ]); break; } if (segment === "*" || segment.includes(":")) { if (!node.param) node.param = { key: "*" }; node = node.param; const isOptional = segment === "*"; paramsMap.push([ i, isOptional ? `_${_unnamedParamIndex++}` : _getParamMatcher(segment), isOptional ]); continue; } const child = node.static?.[segment]; if (child) node = child; else { const staticNode = { key: segment }; if (!node.static) node.static = new NullProtoObj(); node.static[segment] = staticNode; node = staticNode; } } const hasParams = paramsMap.length > 0; if (!node.methods) node.methods = new NullProtoObj(); if (!node.methods[method]) node.methods[method] = []; node.methods[method].push({ data: data || null, paramsMap: hasParams ? paramsMap : void 0 }); if (!hasParams) ctx.static[path] = node; } function _getParamMatcher(segment) { if (!segment.includes(":", 1)) return segment.slice(1); const regex = segment.replace(/:(\w+)/g, (_, id) => `(?<${id}>[^/]+)`).replace(/\./g, "\\."); return new RegExp(`^${regex}$`); } //#endregion //#region src/operations/find.ts /** * Find a route by path. */ function findRoute(ctx, method = "", path, opts) { if (path[path.length - 1] === "/") path = path.slice(0, -1); const staticNode = ctx.static[path]; if (staticNode && staticNode.methods) { const staticMatch = staticNode.methods[method] || staticNode.methods[""]; if (staticMatch !== void 0) return staticMatch[0]; } const segments = splitPath(path); const match = _lookupTree(ctx, ctx.root, method, segments, 0)?.[0]; if (match === void 0) return; if (opts?.params === false) return match; return { data: match.data, params: match.paramsMap ? getMatchParams(segments, match.paramsMap) : void 0 }; } function _lookupTree(ctx, node, method, segments, index) { if (index === segments.length) { if (node.methods) { const match = node.methods[method] || node.methods[""]; if (match) return match; } if (node.param && node.param.methods) { const match = node.param.methods[method] || node.param.methods[""]; if (match) { const pMap = match[0].paramsMap; if (pMap?.[pMap?.length - 1]?.[2]) return match; } } if (node.wildcard && node.wildcard.methods) { const match = node.wildcard.methods[method] || node.wildcard.methods[""]; if (match) { const pMap = match[0].paramsMap; if (pMap?.[pMap?.length - 1]?.[2]) return match; } } return void 0; } const segment = segments[index]; if (node.static) { const staticChild = node.static[segment]; if (staticChild) { const match = _lookupTree(ctx, staticChild, method, segments, index + 1); if (match) return match; } } if (node.param) { const match = _lookupTree(ctx, node.param, method, segments, index + 1); if (match) return match; } if (node.wildcard && node.wildcard.methods) return node.wildcard.methods[method] || node.wildcard.methods[""]; return; } //#endregion //#region src/operations/remove.ts /** * Remove a route from the router context. */ function removeRoute(ctx, method, path) { const segments = splitPath(path); return _remove(ctx.root, method || "", segments, 0); } function _remove(node, method, segments, index) { if (index === segments.length) { if (node.methods && method in node.methods) { delete node.methods[method]; if (Object.keys(node.methods).length === 0) node.methods = void 0; } return; } const segment = segments[index]; if (segment === "*") { if (node.param) { _remove(node.param, method, segments, index + 1); if (_isEmptyNode(node.param)) node.param = void 0; } return; } if (segment.startsWith("**")) { if (node.wildcard) { _remove(node.wildcard, method, segments, index + 1); if (_isEmptyNode(node.wildcard)) node.wildcard = void 0; } return; } const childNode = node.static?.[segment]; if (childNode) { _remove(childNode, method, segments, index + 1); if (_isEmptyNode(childNode)) { delete node.static[segment]; if (Object.keys(node.static).length === 0) node.static = void 0; } } } function _isEmptyNode(node) { return node.methods === void 0 && node.static === void 0 && node.param === void 0 && node.wildcard === void 0; } //#endregion //#region src/operations/find-all.ts /** * Find all route patterns that match the given path. */ function findAllRoutes(ctx, method = "", path, opts) { if (path[path.length - 1] === "/") path = path.slice(0, -1); const segments = splitPath(path); const matches = _findAll(ctx, ctx.root, method, segments, 0); if (opts?.params === false) return matches; return matches.map((m) => { return { data: m.data, params: m.paramsMap ? getMatchParams(segments, m.paramsMap) : void 0 }; }); } function _findAll(ctx, node, method, segments, index, matches = []) { const segment = segments[index]; if (node.wildcard && node.wildcard.methods) { const match = node.wildcard.methods[method] || node.wildcard.methods[""]; if (match) matches.push(...match); } if (node.param) { _findAll(ctx, node.param, method, segments, index + 1, matches); if (index === segments.length && node.param.methods) { const match = node.param.methods[method] || node.param.methods[""]; if (match) { const pMap = match[0].paramsMap; if (pMap?.[pMap?.length - 1]?.[2]) matches.push(...match); } } } const staticChild = node.static?.[segment]; if (staticChild) _findAll(ctx, staticChild, method, segments, index + 1, matches); if (index === segments.length && node.methods) { const match = node.methods[method] || node.methods[""]; if (match) matches.push(...match); } return matches; } //#endregion //#region src/regexp.ts function routeToRegExp(route = "/") { const reSegments = []; let idCtr = 0; for (const segment of route.split("/")) { if (!segment) continue; if (segment === "*") reSegments.push(`(?<_${idCtr++}>[^/]*)`); else if (segment.startsWith("**")) reSegments.push(segment === "**" ? "?(?<_>.*)" : `?(?<${segment.slice(3)}>.+)`); else if (segment.includes(":")) reSegments.push(segment.replace(/:(\w+)/g, (_, id) => `(?<${id}>[^/]+)`).replace(/\./g, "\\.")); else reSegments.push(segment); } return new RegExp(`^/${reSegments.join("/")}/?$`); } //#endregion export { NullProtoObj, addRoute, createRouter, findAllRoutes, findRoute, removeRoute, routeToRegExp };