@project-halycon/folder-map
Version:
A CLI tool to generate markdown documentation of folder structures with optional code content inclusion
203 lines (173 loc) • 5.31 kB
JavaScript
import fs from "fs";
import path from "path";
import { createIgnore, formatFileSize } from "./utils.js";
export async function generateStructure(
rootPath,
options = {
includeCode: false,
ignorePatterns: [],
maxDepth: Infinity,
showSize: false,
format: "md",
includeSysFiles: false,
}
) {
const defaultIgnore = options.includeSysFiles
? []
: [
// Build and cache directories
"node_modules/**",
".next/**",
"dist/**",
"build/**",
".cache/**",
"out/**",
".output/**",
".nuxt/**",
".svelte-kit/**",
// Version control
".git/**",
".svn/**",
".hg/**",
// Package managers
".yarn/**",
".pnpm/**",
"yarn.lock",
"package-lock.json",
"pnpm-lock.yaml",
// Environment and config files
".env*",
".env.local",
".env.*.local",
// IDE and editor files
".vscode/**",
".idea/**",
"*.swp",
"*.swo",
// OS files
".DS_Store",
"Thumbs.db",
// Logs
"*.log",
"logs/**",
// Test coverage
"coverage/**",
".nyc_output/**",
// Temporary files
"tmp/**",
"temp/**",
];
const ignore = createIgnore([
...defaultIgnore,
...(options.ignorePatterns || []),
]);
let output = "";
let fileContents = [];
if (options.format === "md") {
output += "# Project Structure\n\n";
output += "## Directory Tree\n\n";
output += "```text\n";
}
function processDirectory(currentPath, prefix = "", depth = 0) {
if (depth > options.maxDepth) return;
const items = fs.readdirSync(currentPath).sort((a, b) => {
const aStats = fs.statSync(path.join(currentPath, a));
const bStats = fs.statSync(path.join(currentPath, b));
if (aStats.isDirectory() && !bStats.isDirectory()) return -1;
if (!aStats.isDirectory() && bStats.isDirectory()) return 1;
return a.localeCompare(b);
});
items.forEach((item, index) => {
const fullPath = path.join(currentPath, item);
const relativePath = path.relative(rootPath, fullPath);
if (ignore.ignores(relativePath)) return;
const isLast = index === items.length - 1;
const stats = fs.statSync(fullPath);
const sizeInfo = options.showSize
? ` (${formatFileSize(stats.size)})`
: "";
if (stats.isDirectory()) {
output += `${prefix}${isLast ? "└── " : "├── "}${item}/${sizeInfo}\n`;
processDirectory(
fullPath,
`${prefix}${isLast ? " " : "│ "}`,
depth + 1
);
} else {
output += `${prefix}${isLast ? "└── " : "├── "}${item}${sizeInfo}\n`;
if (options.includeCode) {
try {
const content = fs.readFileSync(fullPath, "utf-8");
const extension = path.extname(item).slice(1);
fileContents.push({
path: relativePath,
content: content,
extension: extension,
});
} catch (error) {
fileContents.push({
path: relativePath,
content: "Unable to read file content",
extension: "text",
});
}
}
}
});
}
processDirectory(rootPath);
if (options.format === "md") {
output += "```\n\n";
if (options.includeCode && fileContents.length > 0) {
output += "## File Contents\n\n";
fileContents.forEach((file) => {
output += `### ${file.path}\n\n`;
output += "```" + file.extension + "\n";
output += file.content;
output += "\n```\n\n";
});
}
const stats = getDirectoryStats(rootPath, ignore);
output += "## Project Statistics\n\n";
output += "```text\n";
output += `Total Files: ${stats.files}\n`;
output += `Total Directories: ${stats.directories}\n`;
output += `Total Size: ${formatFileSize(stats.totalSize)}\n`;
output += "```\n";
} else {
if (options.includeCode && fileContents.length > 0) {
output += "\nFile Contents:\n\n";
fileContents.forEach((file) => {
output += `--- ${file.path} ---\n`;
output += file.content;
output += "\n\n";
});
}
const stats = getDirectoryStats(rootPath, ignore);
output += "\nProject Statistics:\n";
output += `Total Files: ${stats.files}\n`;
output += `Total Directories: ${stats.directories}\n`;
output += `Total Size: ${formatFileSize(stats.totalSize)}\n`;
}
return output;
}
function getDirectoryStats(dirPath, ignore) {
let stats = { files: 0, directories: 0, totalSize: 0 };
function processDir(currentPath) {
const items = fs.readdirSync(currentPath);
items.forEach((item) => {
const fullPath = path.join(currentPath, item);
if (ignore.ignores(path.relative(dirPath, fullPath))) return;
const itemStats = fs.statSync(fullPath);
if (itemStats.isDirectory()) {
stats.directories++;
processDir(fullPath);
} else {
stats.files++;
stats.totalSize += itemStats.size;
}
});
}
processDir(dirPath);
return stats;
}