140 lines
5.4 KiB
JavaScript
140 lines
5.4 KiB
JavaScript
/**
|
|
* 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 ctx = {
|
|
opts: opts || {},
|
|
router,
|
|
data: []
|
|
};
|
|
const compiled = compileRouteMatch(ctx);
|
|
return new Function(...ctx.data.map((_, i) => `$${i}`), `return(m,p)=>{${compiled}}`)(...ctx.data);
|
|
}
|
|
/**
|
|
* 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 or you can pass custom `serialize` function in options.
|
|
*
|
|
* @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 ctx = {
|
|
opts: opts || {},
|
|
router,
|
|
data: [],
|
|
compileToString: true
|
|
};
|
|
let compiled = `(m,p)=>{${compileRouteMatch(ctx)}}`;
|
|
if (ctx.data.length > 0) compiled = `/* @__PURE__ */ (() => { ${`const ${ctx.data.map((v, i) => `$${i}=${v}`).join(",")};`}; return ${compiled}})()`;
|
|
return functionName ? `const ${functionName}=${compiled};` : compiled;
|
|
}
|
|
function compileRouteMatch(ctx) {
|
|
let code = "";
|
|
const staticNodes = /* @__PURE__ */ new Set();
|
|
for (const key in ctx.router.static) {
|
|
const node = ctx.router.static[key];
|
|
if (node?.methods) {
|
|
staticNodes.add(node);
|
|
code += `if(p===${JSON.stringify(key.replace(/\/$/, "") || "/")}){${compileMethodMatch(ctx, node.methods, [], -1)}}`;
|
|
}
|
|
}
|
|
const match = compileNode(ctx, ctx.router.root, [], 0, staticNodes);
|
|
if (match) code += `let s=p.split("/"),l=s.length-1;${match}`;
|
|
if (!code) return ctx.opts?.matchAll ? `return [];` : "";
|
|
return `${ctx.opts?.matchAll ? `let r=[];` : ""}if(p.charCodeAt(p.length-1)===47)p=p.slice(0,-1)||"/";${code}${ctx.opts?.matchAll ? "return r;" : ""}`;
|
|
}
|
|
function compileMethodMatch(ctx, methods, params, currentIdx) {
|
|
let code = "";
|
|
for (const key in methods) {
|
|
const matchers = methods[key];
|
|
if (matchers && matchers?.length > 0) {
|
|
if (key !== "") code += `if(m==="${key}")${matchers.length > 1 ? "{" : ""}`;
|
|
const _matchers = matchers.map((m) => compileFinalMatch(ctx, m, currentIdx, params)).sort((a, b) => b.weight - a.weight);
|
|
for (const matcher of _matchers) code += matcher.code;
|
|
if (key !== "") code += matchers.length > 1 ? "}" : "";
|
|
}
|
|
}
|
|
return code;
|
|
}
|
|
function compileFinalMatch(ctx, data, currentIdx, params) {
|
|
let ret = `{data:${serializeData(ctx, data.data)}`;
|
|
const conditions = [];
|
|
const { paramsMap, paramsRegexp } = data;
|
|
if (paramsMap && paramsMap.length > 0) {
|
|
if (!paramsMap[paramsMap.length - 1][2] && currentIdx !== -1) conditions.push(`l>=${currentIdx}`);
|
|
for (let i = 0; i < paramsRegexp.length; i++) {
|
|
const regexp = paramsRegexp[i];
|
|
if (!regexp) continue;
|
|
conditions.push(`${regexp.toString()}.test(s[${i + 1}])`);
|
|
}
|
|
ret += ",params:{";
|
|
for (let i = 0; i < paramsMap.length; i++) {
|
|
const map = paramsMap[i];
|
|
ret += typeof map[1] === "string" ? `${JSON.stringify(map[1])}:${params[i]},` : `...(${map[1].toString()}.exec(${params[i]}))?.groups,`;
|
|
}
|
|
ret += "}";
|
|
}
|
|
return {
|
|
code: (conditions.length > 0 ? `if(${conditions.join("&&")})` : "") + (ctx.opts?.matchAll ? `r.unshift(${ret}});` : `return ${ret}};`),
|
|
weight: conditions.length
|
|
};
|
|
}
|
|
function compileNode(ctx, node, params, startIdx, staticNodes) {
|
|
let code = "";
|
|
if (node.methods && !staticNodes.has(node)) {
|
|
const match = compileMethodMatch(ctx, node.methods, params, node.key === "*" ? startIdx : -1);
|
|
if (match) {
|
|
const hasLastOptionalParam = node.key === "*";
|
|
code += `if(l===${startIdx}${hasLastOptionalParam ? `||l===${startIdx - 1}` : ""}){${match}}`;
|
|
}
|
|
}
|
|
if (node.static) for (const key in node.static) {
|
|
const match = compileNode(ctx, node.static[key], params, startIdx + 1, staticNodes);
|
|
if (match) code += `if(s[${startIdx + 1}]===${JSON.stringify(key)}){${match}}`;
|
|
}
|
|
if (node.param) {
|
|
const match = compileNode(ctx, node.param, [...params, `s[${startIdx + 1}]`], startIdx + 1, staticNodes);
|
|
if (match) code += match;
|
|
}
|
|
if (node.wildcard) {
|
|
const { wildcard } = node;
|
|
if (wildcard.static || wildcard.param || wildcard.wildcard) throw new Error("Compiler mode does not support patterns after wildcard");
|
|
if (wildcard.methods) {
|
|
const match = compileMethodMatch(ctx, wildcard.methods, [...params, `s.slice(${startIdx + 1}).join('/')`], startIdx);
|
|
if (match) code += match;
|
|
}
|
|
}
|
|
return code;
|
|
}
|
|
function serializeData(ctx, value) {
|
|
if (ctx.compileToString) if (ctx.opts?.serialize) value = ctx.opts.serialize(value);
|
|
else if (typeof value?.toJSON === "function") value = value.toJSON();
|
|
else value = JSON.stringify(value);
|
|
let index = ctx.data.indexOf(value);
|
|
if (index === -1) {
|
|
ctx.data.push(value);
|
|
index = ctx.data.length - 1;
|
|
}
|
|
return `$${index}`;
|
|
}
|
|
|
|
export { compileRouter, compileRouterToString }; |