UNPKG

analyze-project-structure

Version:

CLI tool for analyzing and printing the folder structure of a project, extracting details about files (such as functions, variables, routes, and imports/exports), and summarizing the project's code organization.

278 lines (244 loc) 10.4 kB
#!/usr/bin/env node const fs = require("fs"); const path = require("path"); const acorn = require("acorn"); // For parsing JavaScript files const { parse } = require("@typescript-eslint/typescript-estree"); // For parsing TypeScript files // Destructure command-line arguments to get the output file name (defaults to 'folder-structure-output.txt') const [, , outputFile = "folder-structure-output.txt"] = process.argv; // Function to print the folder structure and return it as a string const printFolderStructure = (dirPath, level = 0, output = "") => { const files = fs.readdirSync(dirPath); // Get list of files in the directory files.forEach((file) => { const fullPath = path.join(dirPath, file); // Get full path of the file const stats = fs.statSync(fullPath); // Get file stats (to check if it's a directory) // Append the file or directory name to the output with appropriate indentation output += " ".repeat(level * 2) + file + "\n"; // If it's a directory, recursively print its contents if (stats.isDirectory()) { output = printFolderStructure(fullPath, level + 1, output); // Recursion for directories } else if (fullPath.endsWith(".js") || fullPath.endsWith(".ts")) { output = extractFileDetails(fullPath, level + 1, output); // Process JavaScript/TypeScript files } }); return output; }; // Function to extract details from JavaScript or TypeScript files (functions, variables, routes, etc.) const extractFileDetails = (filePath, level, output) => { const code = fs.readFileSync(filePath, "utf8"); // Read the file content let ast; // Parse the file based on its extension (TypeScript or JavaScript) if (filePath.endsWith(".ts")) { ast = parse(code, { ecmaVersion: 2020, sourceType: "module", loc: true, // Include location info in AST }); } else { ast = acorn.parse(code, { ecmaVersion: 2020, sourceType: "module" }); } // Grouping parsed elements like functions, variables, routes, etc. const grouped = { classes: [], functions: [], arrowFunctions: [], variables: [], constants: [], imports: [], exports: [], routes: {}, // Stores route information (path and method) nested: {}, // Stores nested elements inside route handlers (functions, variables) }; // Recursive function to traverse the AST and extract relevant details const walk = (node, parent) => { // Handle function declarations (including function expressions and arrow functions) if ( node.type === "FunctionDeclaration" || node.type === "FunctionExpression" ) { const fnName = node.id ? node.id.name : "Anonymous Function"; if (parent === "route") { // If inside a route handler, store it as a nested function grouped.nested[fnName] = grouped.nested[fnName] || []; grouped.nested[fnName].push({ type: "function", name: fnName }); } else { grouped.functions.push(fnName); // Global function } } // Handle variable declarations (captures both normal variables and constants) if (node.type === "VariableDeclaration") { node.declarations.forEach((declaration) => { const name = declaration.id.name; if (parent === "route") { // Capture variables declared inside route handlers if (name !== "undefined" && name !== undefined) { grouped.nested[name] = grouped.nested[name] || []; grouped.nested[name].push({ type: "variable", name }); } } else { // Capture global variables if (name !== "undefined" && name !== undefined) { grouped.variables.push(name); } } // Track constants separately if ( declaration.kind === "const" && name !== "undefined" && name !== undefined ) { grouped.constants.push(name); } }); } // Handle class declarations if (node.type === "ClassDeclaration") { grouped.classes.push(node.id.name); } // Handle import declarations if (node.type === "ImportDeclaration") { const importPath = node.source.value; if (importPath !== "undefined" && importPath !== undefined) { grouped.imports.push(importPath); } } // Handle export declarations (both named and default) if ( node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration" ) { const exportName = node.declaration ? node.declaration.name : "default"; if (exportName !== "undefined" && exportName !== undefined) { grouped.exports.push(exportName); } } // Handle route (HTTP methods like GET, POST, PUT, DELETE) if ( node.type === "CallExpression" && node.callee && node.callee.type === "MemberExpression" ) { const methodMatch = node.callee.property.name; if (["get", "post", "put", "delete"].includes(methodMatch)) { const routePath = node.arguments[0]?.value; if (routePath) { // Initialize the route structure if it doesn't exist if (!grouped.routes[routePath]) { grouped.routes[routePath] = {}; } // Add the method (GET, POST, etc.) to the route const route = grouped.routes[routePath]; route[methodMatch] = route[methodMatch] || { variables: routePath.match(/:\w+/g) || [], handlerVariables: [], functions: [], }; // Capture handler variables (inside route handler) const handlerNode = node.arguments[1]; if (handlerNode && handlerNode.body && handlerNode.body.body) { handlerNode.body.body.forEach((stmt) => { if (stmt.type === "VariableDeclaration") { stmt.declarations.forEach((declaration) => { const handlerVarName = declaration.id.name; if ( handlerVarName !== "undefined" && handlerVarName !== undefined ) { route[methodMatch].handlerVariables.push(handlerVarName); } }); } }); } // Capture functions inside route handler if (handlerNode && handlerNode.body && handlerNode.body.body) { const handlerFunctions = handlerNode.body.body.filter( (stmt) => stmt.type === "FunctionDeclaration" || stmt.type === "FunctionExpression" ); handlerFunctions.forEach((fn) => { const fnName = fn.id ? fn.id.name : "Anonymous Function"; route[methodMatch].functions.push(fnName); }); } // Handle arrow functions inline in route handler if (handlerNode && handlerNode.type === "ArrowFunctionExpression") { const fnName = "Anonymous Arrow Function"; route[methodMatch].functions.push(fnName); } } } } // Recursively process child nodes for (const key in node) { if (node[key] && typeof node[key] === "object") { walk(node[key], parent); } } }; walk(ast); // Start traversing the AST // Helper function to print categories like imports, functions, constants, etc. const printCategory = (category, label) => { if (category.length > 0) { output += " ".repeat(level * 2) + `${label}:\n`; category.forEach((item) => { if (item !== "undefined" && item !== undefined) { output += " ".repeat((level + 1) * 2) + `-- ${item}\n`; } }); } }; // Helper function to print nested elements inside routes const printNested = (routeDetails, level) => { if (routeDetails.handlerVariables.length > 0) { output += " ".repeat((level + 3) * 2) + `-- Handler Variables:\n`; routeDetails.handlerVariables.forEach((varName) => { if (varName !== "undefined" && varName !== undefined) { output += " ".repeat((level + 4) * 2) + `-- ${varName}\n`; } }); } if (routeDetails.functions.length > 0) { output += " ".repeat((level + 3) * 2) + `-- Functions in Handler:\n`; routeDetails.functions.forEach((fn) => { if (fn !== "undefined" && fn !== undefined) { output += " ".repeat((level + 4) * 2) + `-- ${fn}\n`; } }); } }; // Print routes with details (methods, variables, nested functions) if (Object.keys(grouped.routes).length > 0) { Object.entries(grouped.routes).forEach(([routePath, methods]) => { output += " ".repeat(level * 2) + `-- Route: ${routePath}\n`; Object.entries(methods).forEach(([method, routeDetails]) => { output += " ".repeat((level + 1) * 2) + `-- Method: ${method.toUpperCase()}\n`; if (routeDetails.variables.length > 0) { output += " ".repeat((level + 2) * 2) + `-- Variables: ${routeDetails.variables.join(", ")}\n`; } // Print nested handler variables/functions printNested(routeDetails, level); }); }); } // Print other categories (imports, classes, functions, etc.) printCategory(grouped.imports, "imports"); printCategory(grouped.classes, "classes"); printCategory(grouped.functions, "functions"); printCategory(grouped.arrowFunctions, "arrow functions"); printCategory(grouped.constants, "constants"); printCategory(grouped.exports, "exports"); return output; }; // Start the process from the 'src' directory and write the output to a file const outputPath = path.join(process.cwd(), outputFile); // Output file path const sourcePath = path.join(process.cwd(), "src"); // Source directory path const output = printFolderStructure(sourcePath); // Print folder structure fs.writeFileSync(outputPath, output); // Save the output to the file // Log the success message console.log( `Folder structure and details have been saved to ${outputFile} in the current directory.` );