UNPKG

expodoc

Version:

A tool to generate API documentation automatically for Express.js applications.

362 lines (361 loc) 15.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RouteParser = void 0; const node_fs_1 = require("node:fs"); const node_path_1 = require("node:path"); const defaults_1 = require("../../core/config/defaults"); const stringUtils_1 = require("../utils/stringUtils"); class RouteParser { constructor(projectPath = ".", config = defaults_1.ExpressDocGenConfig) { this.projectPath = (0, node_path_1.resolve)(projectPath); this.routerBasePaths = {}; this.routes = []; this.config = config; } loadConfigFromAppFile() { const appFilePath = (0, node_path_1.join)(this.projectPath, this.config.ROUTER_CONFIGURED_IN_APP_FILE); if (!(0, node_fs_1.existsSync)(appFilePath)) return; const content = (0, node_fs_1.readFileSync)(appFilePath, "utf-8"); const pattern = /app\.use\((["'])(.*?)\1\s*,\s*([a-zA-Z0-9_]+)\)/g; let match; match = pattern.exec(content); while (match !== null) { match = pattern.exec(content); if (match === null) break; const basePath = match[2]; const routerVar = match[3]; this.routerBasePaths[routerVar] = basePath; } } findRouterFiles() { var _a, _b; const routerFiles = []; for (const folderPattern of this.config.ROUTER_FOLDERS) { const folderPath = (0, node_path_1.join)(this.projectPath, folderPattern); if (!(0, node_fs_1.existsSync)(folderPath)) continue; for (const filePattern of this.config .ROUTER_FILE_NAMING_PATTERNS) { const files = this.globFiles(folderPath, filePattern); for (const filePath of files) { if ((_a = this.config.IGNORED_FOLDERS) === null || _a === void 0 ? void 0 : _a.some((ignored) => filePath.includes(ignored))) continue; if ((_b = this.config.IGNORED_FILES) === null || _b === void 0 ? void 0 : _b.includes((0, node_path_1.basename)(filePath))) continue; routerFiles.push(filePath); } } } return routerFiles; } globFiles(dir, pattern) { const files = []; const entries = (0, node_fs_1.readdirSync)(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = (0, node_path_1.join)(dir, entry.name); if (entry.isDirectory()) { files.push(...this.globFiles(fullPath, pattern)); } else if (this.matchPattern(entry.name, pattern)) { files.push(fullPath); } } return files; } matchPattern(filename, pattern) { const regex = new RegExp(`^${pattern.replace(/\*/g, ".*")}$`); return regex.test(filename); } parseRouterFile(filePath) { const content = (0, node_fs_1.readFileSync)(filePath, "utf-8"); const routerVarMatch = /(?:const|let|var)\s+([a-zA-Z0-9_]+)\s*=\s*Router\(\)|default\s+Router\(\)/.exec(content); if (!routerVarMatch) return; const routerVar = routerVarMatch[1] || (0, node_path_1.basename)(filePath).replace(".route", "Router"); const basePath = this.routerBasePaths[routerVar] || ""; this.processRouteDefinitions(content, basePath, routerVar); } processRouteDefinitions(content, basePath, routerVar) { var _a; // const cleanedContent = content // .replace(/\/\/.*?\n|\/\*.*?\*\//gs, '') // .replace(/\s+/g, ' '); const cleanedContent = content .replace(/\/\/.*?\n/g, "") // Remove single-line comments .replace(/\/\*[\s\S]*?\*\//g, "") // Remove multi-line comments .replace(/\s+/g, " "); // Replace multiple spaces with a single space const routePattern = /([a-zA-Z0-9_]+)\.(?:route\((["'])(.*?)\2\)|(get|post|put|delete|patch|options|head|all)\((["'])(.*?)\5)\s*\)?/g; let match; match = routePattern.exec(cleanedContent); while (match !== null) { match = routePattern.exec(cleanedContent); if (match === null) break; const routerVarMatch = match[1]; const routePath = match[3] || match[6]; const httpMethod = (_a = match[4]) === null || _a === void 0 ? void 0 : _a.toLowerCase(); const chainStart = match.index; const chainEnd = this.findEndOfChain(cleanedContent, chainStart); const chainContent = cleanedContent.slice(chainStart, chainEnd); if (!httpMethod) { this.processRouteChain(chainContent, basePath, routePath, routerVarMatch); } else { this.processSingleRoute(chainContent, basePath, routePath, httpMethod, routerVarMatch); } } } findEndOfChain(content, startPos) { let balance = 0; for (let i = startPos; i < content.length; i++) { if (content[i] === "(") balance++; else if (content[i] === ")") { balance--; if (balance === 0) { for (let j = i; j < content.length; j++) { if (content[j] === ";" || content[j] === "\n") return j + 1; } return i + 1; } } } return content.length; } tagControllerWithScope(controller, middlewares) { for (const [scope, scopeMiddlewares] of Object.entries(this.config.ROUTER_MIDDLEWARE_SCOPE_CONFIG)) { if (scopeMiddlewares.some((mw) => middlewares.includes(mw))) { return `${controller} (${scope})`; } } return controller; } processRouteChain(chainContent, basePath, routePath, routerVar) { const methodPattern = /\.(get|post|put|delete|patch|options|head|all)\(/g; let methodMatch; methodMatch = methodPattern.exec(chainContent); while (methodMatch !== null) { methodMatch = methodPattern.exec(chainContent); if (methodMatch === null) break; const httpMethod = methodMatch[1].toLowerCase(); let startPos = methodMatch.index + methodMatch[0].length; let balance = 1; let endPos = startPos; while (endPos < chainContent.length && balance > 0) { if (chainContent[endPos] === "(") balance++; else if (chainContent[endPos] === ")") balance--; endPos++; } const argsContent = chainContent.slice(startPos, endPos - 1); this.processRouteArgs(argsContent, basePath, routePath, httpMethod, routerVar); } } processSingleRoute(chainContent, basePath, routePath, httpMethod, routerVar) { const argsStart = chainContent.indexOf("(") + 1; const argsEnd = chainContent.lastIndexOf(")"); const argsContent = chainContent.slice(argsStart, argsEnd); this.processRouteArgs(argsContent, basePath, routePath, httpMethod, routerVar); } processRouteArgs(argsContent, basePath, routePath, httpMethod, routerVar) { const args = this.splitArgsWithBalance(argsContent); if (!args.length) return; let controller = null; const middlewares = []; for (const arg of args) { const trimmedArg = arg.trim(); if (!trimmedArg) continue; if (!controller && (middlewares.length < args.length - 1 || trimmedArg.includes("("))) { middlewares.push(trimmedArg); } else { controller = trimmedArg.replace(/,\s+$/, ""); } } if (!controller) return; const fullPath = `${basePath}/${routePath}` .replace("//", "/") .replace(/\/$/, ""); const fullUrl = `${this.config.SERVER_URL}${fullPath}`; controller = this.normalizeCaseToSentence(controller); controller = this.tagControllerWithScope(controller, middlewares); const normalizedRouterVar = this.normalizeCaseToSentence(routerVar.replace(/Router/g, "").trim()); this.routes.push({ router: normalizedRouterVar, method: httpMethod.toUpperCase(), url: fullUrl, controller, middlewares, }); } normalizeCaseToSentence(inputStr) { if (!inputStr || typeof inputStr !== "string") return inputStr; let normalized = (0, stringUtils_1.normalizeToSentenceCase)(inputStr); if (normalized) { const words = normalized.split(" "); if (words[0]) { words[0] = words[0][0].toUpperCase() + words[0].slice(1).toLowerCase(); } for (let i = 1; i < words.length; i++) { if (words[i].length > 1 && words[i] === words[i].toUpperCase()) continue; words[i] = words[i].toLowerCase(); } normalized = words.join(" "); } return normalized; } splitArgsWithBalance(argsContent) { const args = []; let currentArg = ""; let parenBalance = 0; let bracketBalance = 0; let inString = false; let stringChar = null; for (const char of argsContent) { if ((char === '"' || char === "'") && !inString) { inString = true; stringChar = char; } else if (char === stringChar && inString) { inString = false; stringChar = null; } if (!inString) { if (char === "," && parenBalance === 0 && bracketBalance === 0) { args.push(currentArg.trim()); currentArg = ""; continue; } else if (char === "(") parenBalance++; else if (char === ")") parenBalance--; else if (char === "{") bracketBalance++; else if (char === "}") bracketBalance--; } currentArg += char; } if (currentArg.trim()) args.push(currentArg.trim()); return args; } getPostmanCollectionServerUrl() { var _a, _b; return ((_a = this.config.POSTMAN_CONFIG) === null || _a === void 0 ? void 0 : _a.SERVER_URL.VARIABLE) ? `{{${this.config.POSTMAN_CONFIG.SERVER_URL.NAME}}}` : (_b = this.config.POSTMAN_CONFIG) === null || _b === void 0 ? void 0 : _b.SERVER_URL.NAME; } generatePostmanCollection(baseUrlVar, outputDir, postmanFilename) { var _a, _b, _c, _d, _e, _f; if (baseUrlVar === void 0) { baseUrlVar = this.getPostmanCollectionServerUrl(); } if (outputDir === void 0) { outputDir = (_a = this.config.POSTMAN_CONFIG) === null || _a === void 0 ? void 0 : _a.OUTPUT_DIR; } if (postmanFilename === void 0) { postmanFilename = (_b = this.config.POSTMAN_CONFIG) === null || _b === void 0 ? void 0 : _b.POSTMAN_FILENAME; } if (!this.routes || !this.routes.length) { throw new Error("Routes are not available or are empty."); } const folders = {}; for (const route of this.routes) { const { router, method, url: fullUrl, controller } = route; // const parsedUrl = url.parse(fullUrl); const parsedUrl = new URL(fullUrl); const pathOnly = parsedUrl.pathname || ""; const pathParts = []; const variables = []; for (const part of pathOnly.split("/").filter(Boolean)) { if (part.startsWith(":")) { const key = part.slice(1); const defaultVal = key.toLowerCase().includes("id") || key.toLowerCase().includes("phone") ? "1" : "John"; pathParts.push(`:${key}`); variables.push({ key, value: defaultVal }); } else { pathParts.push(part); } } const rawUrl = `${baseUrlVar}/${pathParts.join("/")}`; const urlObj = { raw: rawUrl, host: [baseUrlVar], path: pathParts, }; if (variables.length) urlObj.variable = variables; const requestObj = { method, header: [], url: urlObj, }; if (["POST", "PUT", "PATCH"].includes(method)) { requestObj.body = { mode: "raw", raw: '{\n "example_key": "example_value"\n}', options: { raw: { language: "json" } }, }; } folders[router] = folders[router] || []; folders[router].push({ name: controller, request: requestObj, response: [], }); } const postmanJson = { info: { _postman_id: "generated-id-1234", name: (_c = this.config.POSTMAN_CONFIG) === null || _c === void 0 ? void 0 : _c.COLLECTION_NAME, description: (_d = this.config.POSTMAN_CONFIG) === null || _d === void 0 ? void 0 : _d.COLLECTION_DESCRIPTION, schema: (_e = this.config.POSTMAN_CONFIG) === null || _e === void 0 ? void 0 : _e.SCHEMA, }, item: Object.entries(folders).map(([name, items]) => ({ name, item: items, })), variable: [ { key: (_f = this.config.POSTMAN_CONFIG) === null || _f === void 0 ? void 0 : _f.SERVER_URL.NAME, value: this.config.SERVER_URL, type: "string", }, ], }; (0, node_fs_1.mkdirSync)(outputDir, { recursive: true }); const postmanPath = (0, node_path_1.join)(outputDir, postmanFilename); (0, node_fs_1.writeFileSync)(postmanPath, JSON.stringify(postmanJson, null, 4), "utf-8"); return postmanPath; } parseAllRoutes() { this.loadConfigFromAppFile(); const routerFiles = this.findRouterFiles(); for (const routerFile of routerFiles) { this.parseRouterFile(routerFile); } this.routes.sort((a, b) => a.url.localeCompare(b.url)); } getRoutesJson() { return JSON.stringify(this.routes, null, 4); } createPostmanFile() { return this.generatePostmanCollection(undefined, "prompts"); } } exports.RouteParser = RouteParser;