rou3
Version:
Lightweight and fast router for JavaScript.
133 lines (132 loc) • 4.93 kB
JavaScript
//#region src/compiler.ts
/**
* Compiles the router instance into a faster route-matching function.
*
* **IMPORTANT:** `compileRouter` requires eval support with `new Function()` in the runtime for JIT compilation.
*
* @example
* import { createRouter, addRoute } from "rou3";
* import { compileRouter } from "rou3/compiler";
* const router = createRouter();
* // [add some routes]
* const findRoute = compileRouter(router);
* const matchAll = compileRouter(router, { matchAll: true });
* findRoute("GET", "/path/foo/bar");
*
* @param router - The router context to compile.
*/
function compileRouter(router, opts) {
const deps = [];
const compiled = compileRouteMatch(router, deps, opts?.matchAll);
return new Function(...deps.map((_, i) => "d" + (i + 1)), `return(m,p)=>{${compiled}}`)(...deps);
}
/**
* Compile the router instance into a compact runnable code.
*
* **IMPORTANT:** Route data must be serializable to JSON (i.e., no functions or classes) or implement the `toJSON()` method to render custom code.
*
* @example
* import { createRouter, addRoute } from "rou3";
* import { compileRouterToString } from "rou3/compiler";
* const router = createRouter();
* // [add some routes with serializable data]
* const compilerCode = compileRouterToString(router, "findRoute");
* // "const findRoute=(m, p) => {}"
*/
function compileRouterToString(router, functionName, opts) {
const compiled = `(m,p)=>{${compileRouteMatch(router, void 0, opts?.matchAll)}}`;
return functionName ? `const ${functionName}=${compiled};` : compiled;
}
/**
* Compile a router to pattern matching statements
* @param router
* @param deps - Dependencies of the function scope
*/
function compileRouteMatch(router, deps, matchAll) {
let str = `${matchAll ? `let r=[];` : ""}if(p[p.length-1]==='/')p=p.slice(0,-1)||'/';`;
const staticNodes = new Set();
for (const key in router.static) {
const node = router.static[key];
if (node?.methods) {
staticNodes.add(node);
str += `if(p===${JSON.stringify(key.replace(/\/$/, "") || "/")}){${compileMethodMatch(node.methods, [], deps, -1, matchAll)[1]}}`;
}
}
const [existsTail, tail] = compileNode(router.root, [], 0, deps, false, staticNodes, matchAll);
return str + (existsTail ? "let s=p.split('/'),l=s.length-1;" + tail + (matchAll ? ";return r" : "") : "");
}
function compileMethodMatch(methods, params, deps, currentIdx, matchAll) {
let str = "";
let exists = false;
for (const key in methods) {
const data = methods[key];
if (data && data?.length > 0) {
exists = true;
if (key !== "") str += `if(m==='${key}')`;
const dataValue = data[0].data;
let res = deps ? `{data:d${deps.push(dataValue)}` : `{data:${typeof dataValue?.toJSON === "function" ? dataValue.toJSON() : JSON.stringify(dataValue)}`;
const { paramsMap } = data[0];
if (paramsMap && paramsMap.length > 0) {
const required = !paramsMap[paramsMap.length - 1][2] && currentIdx !== -1;
if (required) str += `if(l>=${currentIdx})`;
res += ",params:{";
for (let i = 0; i < paramsMap.length; i++) {
const map = paramsMap[i];
res += typeof map[1] === "string" ? `${JSON.stringify(map[1])}:${params[i]},` : `...(${map[1].toString()}.exec(${params[i]}))?.groups,`;
}
res += "}";
}
str += matchAll ? `r.unshift(${res}});` : `return ${res}};`;
}
}
return [exists, str];
}
/**
* Compile a node to matcher logic
*/
function compileNode(node, params, startIdx, deps, isParamNode, staticNodes, matchAll) {
let str = "";
let exists = false;
if (node.methods && !staticNodes.has(node)) {
const [existsChild, match] = compileMethodMatch(node.methods, params, deps, isParamNode ? startIdx : -1, matchAll);
if (existsChild) {
exists = true;
str += `if(l===${startIdx}${isParamNode ? `||l===${startIdx - 1}` : ""}){${match}}`;
}
}
if (node.static) for (const key in node.static) {
const [existsChild, match] = compileNode(node.static[key], params, startIdx + 1, deps, false, staticNodes, matchAll);
if (existsChild) {
exists = true;
str += `if(s[${startIdx + 1}]===${JSON.stringify(key)}){${match}}`;
}
}
if (node.param) {
const [existsChild, match] = compileNode(node.param, [...params, `s[${startIdx + 1}]`], startIdx + 1, deps, true, staticNodes, matchAll);
if (existsChild) {
exists = true;
str += match;
}
}
if (node.wildcard) {
const { wildcard } = node;
if (hasChild(wildcard)) throw new Error("Compiler mode does not support patterns after wildcard");
if (wildcard.methods) {
const [existsChild, match] = compileMethodMatch(wildcard.methods, [...params, `s.slice(${startIdx + 1}).join('/')`], deps, startIdx, matchAll);
if (existsChild) {
exists = true;
str += match;
}
}
}
return [exists, str];
}
/**
* Whether the current node has children nodes
* @param n
*/
function hasChild(n) {
return !!(n.static || n.param || n.wildcard);
}
//#endregion
export { compileRouter, compileRouterToString };