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
JavaScript
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.`
);