express-router-dependency-graph
Version:
Create dependency graph for express routing.
135 lines • 5.02 kB
JavaScript
import { parse } from "@babel/parser";
import path from "node:path";
import fs from "node:fs/promises";
import query from "esquery";
import { markdownTable } from "markdown-table";
const findRouting = async ({ AST, fileContent }) => {
try {
const search = (method, AST) => {
const selector = `CallExpression:has(MemberExpression > Identifier[name="${method}"])`;
const results = query(AST, selector);
// router.{get,post,delete,put,use}
return results.flatMap((node) => {
// TODO: improve query to avoid this check
// res.set("X-Content-Type-Options", req.get("test")));
if (node.callee.property.name !== method) {
return [];
}
// single argument should be ignored
// req.get("host"); it is not routing
if (node.arguments.length === 1) {
return [];
}
const pathValue = node.arguments[0] !== undefined &&
node.arguments[0].type === "StringLiteral" &&
node.arguments[0].value;
if (!pathValue) {
return []; // skip: it will only includes middleware
}
const middlewareArguments = method === "use"
? // @ts-ignore
node.arguments?.slice(1) ?? []
: // @ts-ignore
node.arguments?.slice(1, node.arguments.length - 1) ?? [];
const middlewares = middlewareArguments.map((arg) => {
// app.use(() => {});
if (arg.type === "ArrowFunctionExpression") {
return "Anonymous Function";
}
// app.use(function () {});
if (arg.type === "FunctionExpression") {
// @ts-ignore
return arg?.id?.name ?? "Anonymous Function";
}
return fileContent.slice(arg.start, arg.end);
});
return [
{
method,
path: pathValue,
middlewares,
// @ts-ignore
range: [node.start, node.end],
// @ts-ignore
loc: node.loc
}
];
});
};
const methods = ["get", "post", "delete", "put", "use"];
return methods.flatMap((method) => {
return search(method, AST);
});
}
catch {
return [];
}
};
const toAbsolute = (cwd, f) => {
return path.resolve(cwd, f);
};
const hasImportExpress = (AST) => {
// import express from "express";
if (query(AST, "ImportDeclaration[source.value='express']").length > 0) {
return true;
}
// const express = require("express");
if (query(AST, "CallExpression[callee.name='require'][arguments.0.value='express']").length > 0) {
return true;
}
// const express = await import("express");
if (query(AST, "ImportExpression[source.value='express']").length > 0) {
return true;
}
return false;
};
export async function analyzeDependency({ filePath }) {
const fileContent = await fs.readFile(filePath, "utf-8");
try {
const AST = parse(fileContent, {
sourceType: "module",
plugins: ["jsx", "typescript"]
});
if (!hasImportExpress(AST)) {
return [];
}
return findRouting({ AST, fileContent });
}
catch (e) {
console.error("Error while analyzing", filePath);
console.error(e);
return [];
}
}
export async function analyzeDependencies({ filePaths, cwd }) {
return Promise.all(filePaths.map(async (filePath) => {
const absoluteFilePath = toAbsolute(cwd, filePath);
return {
filePath: absoluteFilePath,
routers: await analyzeDependency({ filePath: absoluteFilePath })
};
}));
}
export const formatMarkdown = ({ cwd, results, rootBaseUrl }) => {
const toRelative = (f) => {
return path.relative(cwd, f);
};
const table = [["File", "Method", "Routing", "Middlewares", "FilePath"]];
for (const result of results) {
if (result.routers.length === 0) {
continue;
}
table.push([`${rootBaseUrl}${toRelative(result.filePath)}`]);
result.routers.forEach((router) => {
table.push([
"",
router.method,
router.path,
router.middlewares.join(", ").split(/\r?\n/g).join(" "),
`${rootBaseUrl}${toRelative(result.filePath)}#L${router.loc.start.line}-L${router.loc.end.line}`
]);
});
}
return markdownTable(table);
};
//# sourceMappingURL=index.js.map