UNPKG

comment-strip-cli

Version:

A powerful CLI tool to strip comments from source code files while preserving strings and important metadata

341 lines (287 loc) 10.7 kB
const fs = require('fs').promises; const path = require('path'); function parseArgs(args) { const options = { input: null, type: ['all'], dryRun: false, backup: false, recursive: true, extensions: [ 'js', 'mjs', 'jsx', 'ts', 'tsx', 'c', 'h', 'cpp', 'cxx', 'cc', 'hpp', 'hxx', 'py', 'pyw', 'sol', 'rs', 'go', 'java', 'kt', 'kts', 'swift', 'dart', 'scala', 'sc', 'php', 'phtml', 'css', 'scss', 'rb', 'rbw', 'pl', 'pm', 'r', 'R', 'sh', 'bash', 'zsh', 'dockerfile', 'cmake', 'toml', 'yml', 'yaml', 'ini', 'cfg', 'conf', 'makefile', 'mk', 'asm', 's', 'sql', 'lua', 'hs', 'lhs', 'json', 'jsonc' ] }; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === '--help' || arg === '-h') { showHelp(); return { help: true }; } else if (arg.startsWith('--type=') || arg.startsWith('-t=')) { const typeValue = arg.split('=')[1]; if (typeValue) { if (typeValue.includes(',')) { options.type = typeValue.split(',').map(t => t.trim()); } else { options.type = [typeValue]; } } else { console.error('❌Error: --type flag requires a value'); return { error: true }; } } else if (arg === '--type' || arg === '-t') { if (i + 1 < args.length) { i++; const typeValue = args[i]; if (typeValue.includes(',')) { options.type = typeValue.split(',').map(t => t.trim()); } else { options.type = [typeValue]; } } else { console.error('❌Error: --type flag requires a value'); return { error: true }; } } else if (arg === '--dry-run' || arg === '-d') { options.dryRun = true; } else if (arg === '--backup' || arg === '-b') { options.backup = true; } else if (arg.startsWith('--extensions=') || arg.startsWith('-e=')) { const extensionsValue = arg.split('=')[1]; if (extensionsValue) { options.extensions = extensionsValue.split(',').map(ext => ext.trim()); } else { console.error('❌Error: --extensions flag requires a value'); return { error: true }; } } else if (arg === '--extensions' || arg === '-e') { if (i + 1 < args.length) { i++; const extensionsValue = args[i]; options.extensions = extensionsValue.split(',').map(ext => ext.trim()); } else { console.error('❌Error: --extensions flag requires a value'); return { error: true }; } } else if (!arg.startsWith('--') && !arg.startsWith('-') && !options.input) { options.input = arg; } } return options; } function showHelp() { console.log(` Comment Stripper CLI Tool Usage: comment-strip <file|directory> [options] Options: --type, -t <type> Comment type to strip: '//', '#', '/* */', '--', ';', etc. (default: all) --dry-run, -d Preview changes without modifying files --backup, -b Create backup files before modification --extensions, -e <ext> File extensions to process (comma-separated) --help, -h Show this help message Examples: comment-strip app.js # Strip all comments from app.js comment-strip src/ --type="//" # Strip only // comments from src/ comment-strip . --dry-run # Preview all changes comment-strip code/ --backup --type="#" # Strip # comments with backup comment-strip project/ --extensions=js,py,cpp # Process only specific extensions Supported Languages: Programming: JavaScript/TypeScript, C/C++, Java, Python, Solidity, Rust, Go, Kotlin, Swift, Dart, Scala, PHP, Ruby, Perl, R, Lua, Haskell Web: CSS, SCSS, HTML, JSON Config: Docker, CMake, TOML, YAML, INI, Makefile Database: SQL Assembly: ASM, S And many more! `); } async function main(args) { try { const options = parseArgs(args); if (options.help) { return { success: true }; } if (options.error) { return { success: false }; } if (!options.input) { console.error('❌Error: Please provide a file or directory to process'); console.error('❌Use --help for usage information'); return { success: false }; } const typeDisplay = options.type.join(', '); console.log(`------> Options: type=${typeDisplay}, backup=${options.backup}`); // Resolve the input path to absolute path const inputPath = path.resolve(options.input); console.log(`------> Checking path: ${inputPath}`); // Dry-run specific message if (options.dryRun) { console.log(`------> Comments with type ${typeDisplay} will be stripped and the code will be autoformatted`); } // Check if input exists try { const stats = await fs.stat(inputPath); if (stats.isDirectory()) { return await processDirectory(inputPath, options); } else { return await processFile(inputPath, options); } } catch (error) { console.error(`❌Error: File or directory '${options.input}' not found`); console.error(`❌Resolved path: ${inputPath}`); console.error(`❌Error details: ${error.message}`); return { success: false }; } } catch (error) { console.error('❌Unexpected error:', error.message); return { success: false }; } } async function processDirectory(dirPath, options) { let processedCount = 0; let skippedCount = 0; let errorCount = 0; let noChangesCount = 0; console.log(`------> Processing directory: ${dirPath}`); try { const files = await getFilesRecursively(dirPath, options.extensions); if (files.length === 0) { console.log(`------> No files found with supported extensions or special files`); return { success: true, processedCount: 0, skippedCount: 0, errorCount: 0 }; } console.log(`------> Found ${files.length} files to process`); for (const file of files) { try { const result = await processFile(file, options); if (result.skipped) { skippedCount++; } else if (result.modified === false) { noChangesCount++; } else if (result.modified !== false) { processedCount++; } } catch (error) { console.error(`❌Error processing ${file}: ${error.message}`); errorCount++; } } console.log(`------> Summary: ${processedCount} files processed, ${noChangesCount} unchanged, ${skippedCount} skipped, ${errorCount} errors`); return { success: errorCount === 0, processedCount, noChangesCount, skippedCount, errorCount }; } catch (error) { console.error(`❌Error reading directory: ${error.message}`); return { success: false }; } } async function processFile(filePath, options) { try { // Read file content const content = await fs.readFile(filePath, 'utf8'); // Get file extension and filename to determine language const ext = path.extname(filePath).slice(1).toLowerCase(); const filename = path.basename(filePath); // Check if this file should be processed if (!shouldProcessFile(filename, ext, options.extensions)) { return { success: true, skipped: true }; } // Import and use the parser const { stripComments } = require('./parser.js'); let strippedContent; try { const commentTypes = Array.isArray(options.type) ? options.type : [options.type]; strippedContent = stripComments(content, ext, commentTypes, filename); } catch (parserError) { throw new Error(`Parser error: ${parserError.message}`); } if (typeof strippedContent !== 'string') { throw new Error('Parser returned invalid result format'); } const typeDisplay = options.type.join(', '); if (content === strippedContent) { if (!options.dryRun) { console.log(`>>>>>> No comments found at ${filePath} of type ${typeDisplay}`); } return { success: true, modified: false }; } if (options.dryRun) { return { success: true, preview: true }; } // Create backup if requested if (options.backup) { try { const backupPath = filePath + '.bak'; await fs.writeFile(backupPath, content, 'utf8'); console.log(`======> Backup for your file ${filePath} has been created`); } catch (backupError) { console.warn(`Warning: Could not create backup for ${filePath}: ${backupError.message}`); } } // Write the modified content await fs.writeFile(filePath, strippedContent, 'utf8'); console.log(`------> Comments of type ${typeDisplay} have been stripped and the code has been formatted`); return { success: true, modified: true }; } catch (error) { throw new Error(`Failed to process ${filePath}: ${error.message}`); } } function shouldProcessFile(filename, ext, allowedExtensions) { // Check for special files without extensions const specialFiles = [ 'dockerfile', 'Dockerfile', 'makefile', 'Makefile', 'GNUmakefile', 'CMakeLists.txt', 'cmakelists.txt' ]; const lowerName = filename.toLowerCase(); // Check if it's a special file if (specialFiles.some(special => lowerName === special.toLowerCase())) { return true; } // Check if it starts with special patterns if (lowerName.startsWith('dockerfile.') || lowerName.startsWith('makefile.')) { return true; } // Check normal extensions if (ext && allowedExtensions.includes(ext)) { return true; } return false; } async function getFilesRecursively(dir, extensions) { const files = []; async function scanDir(currentDir) { const entries = await fs.readdir(currentDir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentDir, entry.name); if (entry.isDirectory()) { // Skip node_modules, .git, and other hidden directories if (!entry.name.startsWith('.') && entry.name !== 'node_modules' && entry.name !== 'dist' && entry.name !== 'build' && entry.name !== 'coverage' && entry.name !== 'target' && entry.name !== 'vendor') { await scanDir(fullPath); } } else if (entry.isFile()) { const ext = path.extname(entry.name).slice(1).toLowerCase(); if (shouldProcessFile(entry.name, ext, extensions)) { files.push(fullPath); } } } } await scanDir(dir); return files; } module.exports = { main };