#!/usr/bin/env node import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; class NodeModulesCleaner { constructor(rootPath = process.cwd()) { this.rootPath = path.resolve(rootPath); this.foundDirectories = []; this.deletedDirectories = []; this.errors = []; } async findNodeModules(dir = this.rootPath) { try { const entries = await fs.promises.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { if (entry.name === 'node_modules') { this.foundDirectories.push(fullPath); } else { // Skip common directories that shouldn't contain node_modules const skipDirs = ['.git', '.vscode', 'dist', 'build', 'coverage']; if (!skipDirs.includes(entry.name)) { await this.findNodeModules(fullPath); } } } } } catch (error) { this.errors.push({ path: dir, error: error.message }); } } async deleteDirectory(dirPath) { try { await fs.promises.rm(dirPath, { recursive: true, force: true }); this.deletedDirectories.push(dirPath); return true; } catch (error) { this.errors.push({ path: dirPath, error: error.message }); return false; } } async clean(dryRun = false) { console.log(`šŸ” Scanning for node_modules directories in: ${this.rootPath}`); await this.findNodeModules(); if (this.foundDirectories.length === 0) { console.log('āœ… No node_modules directories found.'); return; } console.log(`\nšŸ“ Found ${this.foundDirectories.length} node_modules directories:`); this.foundDirectories.forEach((dir, index) => { console.log(` ${index + 1}. ${path.relative(this.rootPath, dir)}`); }); if (dryRun) { console.log('\nšŸ” Dry run mode - no directories will be deleted.'); return; } console.log('\nšŸ—‘ļø Deleting directories...'); for (const dir of this.foundDirectories) { const relativePath = path.relative(this.rootPath, dir); process.stdout.write(` Deleting ${relativePath}... `); const success = await this.deleteDirectory(dir); console.log(success ? 'āœ…' : 'āŒ'); } // Summary console.log('\nšŸ“Š Summary:'); console.log(` āœ… Successfully deleted: ${this.deletedDirectories.length}`); console.log(` āŒ Failed to delete: ${this.errors.length}`); if (this.errors.length > 0) { console.log('\nāŒ Errors:'); for (const { path, error } of this.errors) { console.log(` ${path}: ${error}`); } } } async getSizeInfo() { const sizes = []; const isWindows = process.platform === 'win32'; for (const dir of this.foundDirectories) { try { let size; if (isWindows) { // Use PowerShell on Windows const sizeOutput = execSync( `powershell -Command "(Get-ChildItem -Path '${dir}' -Recurse -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum"`, { encoding: 'utf8' } ); const bytes = Number.parseInt(sizeOutput.trim(), 10) || 0; size = bytes > 0 ? `${(bytes / 1024 / 1024).toFixed(1)}M` : '0B'; } else { // Use du command on Unix-like systems const sizeOutput = execSync(`du -sh "${dir}" 2>/dev/null || echo "0B"`, { encoding: 'utf8', }); size = sizeOutput.trim().split('\t')[0]; } sizes.push({ path: dir, size }); } catch (_error) { sizes.push({ path: dir, size: 'Unknown' }); } } return sizes; } } // CLI interface async function main() { const args = process.argv.slice(2); const dryRun = args.includes('--dry-run') || args.includes('-n'); const showHelp = args.includes('--help') || args.includes('-h'); const showSizes = args.includes('--sizes') || args.includes('-s'); if (showHelp) { console.log(` Node Modules Cleaner - Tool for wiping 'node_module' folders recursively from a monorepo ==================== Usage: node clean-node-modules.mjs [options] [path] Options: --dry-run, -n Show what would be deleted without actually deleting --sizes, -s Show size information for each node_modules directory --help, -h Show this help message Examples: node clean-node-modules.mjs # Clean current directory node clean-node-modules.mjs /path/to/monorepo # Clean specific directory node clean-node-modules.mjs --dry-run # Preview what would be deleted node clean-node-modules.mjs --sizes # Show sizes before cleaning `); return; } // Get target directory (default to current directory) const targetPath = args.find((arg) => !arg.startsWith('--') && !arg.startsWith('-')) || process.cwd(); if (!fs.existsSync(targetPath)) { console.error(`āŒ Error: Directory '${targetPath}' does not exist.`); process.exit(1); } if (!fs.lstatSync(targetPath).isDirectory()) { console.error(`āŒ Error: '${targetPath}' is not a directory.`); process.exit(1); } const cleaner = new NodeModulesCleaner(targetPath); if (showSizes) { console.log('šŸ“Š Calculating sizes...'); await cleaner.findNodeModules(); const sizes = await cleaner.getSizeInfo(); if (sizes.length === 0) { console.log('āœ… No node_modules directories found.'); return; } console.log('\nšŸ“ Node modules directories and their sizes:'); for (const { path, size } of sizes) { console.log(` ${size.padStart(8)} ${path}`); } console.log(`\nšŸ“Š Total directories found: ${sizes.length}`); return; } await cleaner.clean(dryRun); } // Run the script main().catch((error) => { console.error('āŒ Unexpected error:', error.message); process.exit(1); }); export default NodeModulesCleaner;