UNPKG

@regele/devtools

Version:

A collection of developer utilities for code processing and text analysis

445 lines (388 loc) 18.2 kB
#!/usr/bin/env node try { // Direct implementation of comment removal const fs = require('fs'); const path = require('path'); const { Command } = require('commander'); const chalk = require('chalk'); const ora = require('ora'); const fg = require('fast-glob'); // Ensure ora is properly imported const createSpinner = ora.default || ora; // Function to remove comments from JavaScript code function removeJavaScriptComments(code) { let result = code; try { // First, preserve strings and regexes to avoid modifying comments inside them const preservedItems = []; let preservedCount = 0; // Temporarily replace strings and regexes with placeholders result = result.replace( /(["'`])(?:\\[\s\S]|(?!\1)[^\\])*\1|\/(?![*+?])(?:[^\r\n\[/\\]|\\.|\[(?:[^\r\n\]\\]|\\.)*\])+\/(?:[gimuy]+\b)?/g, (match) => { const placeholder = `__PRESERVED_ITEM_${preservedCount}__`; preservedItems[preservedCount] = match; preservedCount++; return placeholder; } ); // Remove JSX comments result = result.replace(/\{\/\*[\s\S]*?\*\/\}/g, ''); // Remove multi-line comments result = result.replace(/\/\*[\s\S]*?\*\//g, ''); // Remove single-line comments // Handle comments at start of line result = result.replace(/^[ \t]*\/\/.*$/gm, ''); // Handle comments after code (but not in URLs) result = result.replace(/([^:/])\/\/.*$/gm, '$1'); // Restore preserved strings and regexes for (let i = 0; i < preservedCount; i++) { const placeholder = `__PRESERVED_ITEM_${i}__`; const regex = new RegExp(placeholder, 'g'); result = result.replace(regex, () => preservedItems[i]); } // Remove empty lines // Remove lines that contain only whitespace result = result.replace(/^\s*[\r\n]/gm, ''); // Remove consecutive empty lines (preserving indentation) result = result.replace(/\n(\s*)\n(\s*)\n/g, '\n$2\n'); // Remove empty lines at the beginning and end of the file result = result.replace(/^(\s*\n)+/, ''); result = result.replace(/(\n\s*)+$/, ''); return result.trim(); } catch (error) { console.error(`JavaScript comment removal failed: ${error.message}`); return code; } } // Create the clean command const command = new Command('devtools-clean'); command .description('Remove comments from code files') .argument('<patterns...>', 'File patterns to clean (e.g., "src/**/*.js")') .option('--single-line', 'Remove single-line comments', true) .option('--multi-line', 'Remove multi-line comments', true) .option('--jsx', 'Remove JSX comments', true) .option('--empty-lines', 'Remove empty lines', true) .option('--ignore <pattern>', 'Files to ignore (comma-separated)', value => value.split(',').map(item => item.trim())) .option('--write', 'Write changes to files', true) .option('--silent', 'Suppress output', false) .action(async (patterns, options) => { const silent = options.silent; try { // Start spinner const spinner = createSpinner({ text: 'Removing comments...', color: 'cyan', spinner: 'dots' }).start(); // Process each file let results = []; // Normalize patterns for Windows paths const normalizedPatterns = patterns.map(pattern => { // Convert Windows backslashes to forward slashes for glob return pattern.replace(/\\/g, '/'); }); // Default ignore patterns const defaultIgnore = [ '**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**', '**/package-lock.json', '**/yarn.lock', '**/*.min.js', '**/*.min.css', '**/*.bundle.js' ]; // Combine with user-provided ignore patterns const ignorePatterns = options.ignore ? [...defaultIgnore, ...options.ignore] : defaultIgnore; // Find all files matching the patterns const files = await fg(normalizedPatterns, { ignore: ignorePatterns, onlyFiles: true, absolute: true, // Use absolute paths to handle Windows paths better followSymbolicLinks: false // Don't follow symlinks to avoid circular references }); for (const file of files) { try { // Read the file const content = fs.readFileSync(file, 'utf8'); const originalSize = content.length; // Process the file based on extension const ext = path.extname(file).toLowerCase(); let newContent; // JavaScript/TypeScript family if (['.js', '.jsx', '.ts', '.tsx', '.json', '.mjs', '.cjs', '.vue', '.svelte', '.astro'].includes(ext)) { newContent = removeJavaScriptComments(content); } // HTML/XML family else if (['.html', '.htm', '.xml', '.svg', '.xhtml', '.jsp', '.asp', '.aspx', '.cshtml', '.razor', '.ejs', '.hbs', '.handlebars', '.pug', '.jade'].includes(ext)) { try { // For HTML files, remove HTML comments newContent = content.replace(/<!--[\s\S]*?-->/g, ''); // Also process script and style tags for JS and CSS comments newContent = newContent.replace(/(<script\b[^>]*>)([\s\S]*?)(<\/script>)/gi, (match, openTag, scriptContent, closeTag) => { return openTag + removeJavaScriptComments(scriptContent) + closeTag; }); newContent = newContent.replace(/(<style\b[^>]*>)([\s\S]*?)(<\/style>)/gi, (match, openTag, styleContent, closeTag) => { // Remove CSS comments let cleanedStyle = styleContent.replace(/\/\*[\s\S]*?\*\//g, ''); // Remove empty lines cleanedStyle = cleanedStyle.replace(/^\s*[\r\n]/gm, ''); cleanedStyle = cleanedStyle.replace(/\n\s*\n/g, '\n'); return openTag + cleanedStyle + closeTag; }); } catch (error) { console.error(`Error processing HTML file ${file}: ${error.message}`); newContent = content; // Keep original content on error } } // CSS family else if (['.css', '.scss', '.sass', '.less', '.styl', '.stylus', '.pcss', '.postcss'].includes(ext)) { try { // For CSS files, remove CSS comments newContent = content.replace(/\/\*[\s\S]*?\*\//g, ''); // Remove empty lines newContent = newContent.replace(/^\s*[\r\n]/gm, ''); newContent = newContent.replace(/\n\s*\n/g, '\n'); } catch (error) { console.error(`Error processing CSS file ${file}: ${error.message}`); newContent = content; // Keep original content on error } } // C-style languages (C, C++, C#, Java, etc.) else if (['.c', '.h', '.cpp', '.hpp', '.cc', '.cxx', '.c++', '.cs', '.java', '.go', '.swift', '.kt', '.scala', '.php', '.php5', '.phtml', '.inc'].includes(ext)) { try { // C-style comments are similar to JavaScript newContent = removeJavaScriptComments(content); } catch (error) { console.error(`Error processing C-style file ${file}: ${error.message}`); newContent = content; // Keep original content on error } } // Python, Ruby, Perl, etc. else if (['.py', '.pyw', '.rb', '.pl', '.pm', '.r', '.jl'].includes(ext)) { try { // Preserve strings const preservedItems = []; let preservedCount = 0; // Temporarily replace strings with placeholders let result = content.replace( /(["'`])(?:\\[\s\S]|(?!\1)[^\\])*\1|"""[\s\S]*?"""|'''[\s\S]*?'''/g, (match) => { const placeholder = `__PRESERVED_ITEM_${preservedCount}__`; preservedItems[preservedCount] = match; preservedCount++; return placeholder; } ); // Remove single-line comments (# in Python, Ruby, etc.) result = result.replace(/^[ \t]*#.*$/gm, ''); result = result.replace(/([^\\])#.*$/gm, '$1'); // Restore preserved strings for (let i = 0; i < preservedCount; i++) { const placeholder = `__PRESERVED_ITEM_${i}__`; const regex = new RegExp(placeholder, 'g'); result = result.replace(regex, () => preservedItems[i]); } // Remove empty lines result = result.replace(/^\s*[\r\n]/gm, ''); result = result.replace(/\n\s*\n/g, '\n'); newContent = result; } catch (error) { console.error(`Error processing Python/Ruby file ${file}: ${error.message}`); newContent = content; // Keep original content on error } } // Shell scripts else if (['.sh', '.bash', '.zsh', '.fish', '.ksh', '.bat', '.cmd', '.ps1'].includes(ext)) { try { // Preserve strings const preservedItems = []; let preservedCount = 0; // Temporarily replace strings with placeholders let result = content.replace( /(["'`])(?:\\[\s\S]|(?!\1)[^\\])*\1/g, (match) => { const placeholder = `__PRESERVED_ITEM_${preservedCount}__`; preservedItems[preservedCount] = match; preservedCount++; return placeholder; } ); // Remove single-line comments (# in shell scripts) result = result.replace(/^[ \t]*#.*$/gm, ''); result = result.replace(/([^\\])#.*$/gm, '$1'); // For batch files, remove REM comments if (['.bat', '.cmd'].includes(ext)) { result = result.replace(/^[ \t]*REM.*$/gim, ''); } // For PowerShell, remove # and <# #> comments if (['.ps1'].includes(ext)) { result = result.replace(/^[ \t]*#.*$/gm, ''); result = result.replace(/<#[\s\S]*?#>/g, ''); } // Restore preserved strings for (let i = 0; i < preservedCount; i++) { const placeholder = `__PRESERVED_ITEM_${i}__`; const regex = new RegExp(placeholder, 'g'); result = result.replace(regex, () => preservedItems[i]); } // Remove empty lines result = result.replace(/^\s*[\r\n]/gm, ''); result = result.replace(/\n\s*\n/g, '\n'); newContent = result; } catch (error) { console.error(`Error processing shell script ${file}: ${error.message}`); newContent = content; // Keep original content on error } } // SQL else if (['.sql', '.mysql', '.pgsql', '.sqlite', '.plsql'].includes(ext)) { try { // Preserve strings const preservedItems = []; let preservedCount = 0; // Temporarily replace strings with placeholders let result = content.replace( /(["'`])(?:\\[\s\S]|(?!\1)[^\\])*\1/g, (match) => { const placeholder = `__PRESERVED_ITEM_${preservedCount}__`; preservedItems[preservedCount] = match; preservedCount++; return placeholder; } ); // Remove single-line comments (-- in SQL) result = result.replace(/^[ \t]*--.*$/gm, ''); result = result.replace(/([^:-])--.*$/gm, '$1'); // Remove multi-line comments (/* */ in SQL) result = result.replace(/\/\*[\s\S]*?\*\//g, ''); // Restore preserved strings for (let i = 0; i < preservedCount; i++) { const placeholder = `__PRESERVED_ITEM_${i}__`; const regex = new RegExp(placeholder, 'g'); result = result.replace(regex, () => preservedItems[i]); } // Remove empty lines result = result.replace(/^\s*[\r\n]/gm, ''); result = result.replace(/\n\s*\n/g, '\n'); newContent = result; } catch (error) { console.error(`Error processing SQL file ${file}: ${error.message}`); newContent = content; // Keep original content on error } } // Default fallback else { // Try to detect the language based on content if (content.includes('<?php')) { // PHP-like syntax newContent = removeJavaScriptComments(content); } else if (content.includes('<!DOCTYPE') || content.includes('<html')) { // HTML-like syntax newContent = content.replace(/<!--[\s\S]*?-->/g, ''); } else if (content.match(/^[ \t]*import\s|^[ \t]*from\s|^[ \t]*export\s|^[ \t]*class\s|^[ \t]*function\s/m)) { // JavaScript/TypeScript-like syntax newContent = removeJavaScriptComments(content); } else if (content.match(/^[ \t]*#include\s|^[ \t]*#define\s|^[ \t]*#ifndef\s/m)) { // C/C++-like syntax newContent = removeJavaScriptComments(content); } else if (content.match(/^[ \t]*def\s|^[ \t]*class\s|^[ \t]*import\s/m) && content.includes('#')) { // Python-like syntax let result = content; // Remove single-line comments (# in Python) result = result.replace(/^[ \t]*#.*$/gm, ''); result = result.replace(/([^\\])#.*$/gm, '$1'); newContent = result; } else { // Default to JavaScript comment removal as a fallback newContent = removeJavaScriptComments(content); } } const newSize = newContent.length; const diffSize = originalSize - newSize; const diffPercentage = originalSize > 0 ? (diffSize / originalSize) * 100 : 0; // Write the file if requested if (options.write) { fs.writeFileSync(file, newContent, 'utf8'); } results.push({ path: file, success: true, originalSize, newSize, diffSize, diffPercentage }); } catch (error) { results.push({ path: file, success: false, error: error.message }); } } // Stop spinner spinner.succeed(`Processed ${results.length} files`); // Log results if (!silent) { const successCount = results.filter(r => r.success).length; const errorCount = results.length - successCount; console.log('\n' + chalk.bold('Results:')); console.log(`${chalk.green(`${successCount} files`)} processed successfully`); if (errorCount > 0) { console.log(`${chalk.red(`${errorCount} files`)} failed to process`); console.log('\n' + chalk.bold('Errors:')); results .filter(r => !r.success) .forEach(result => { console.log(` ${chalk.red('✗')} ${result.path}: ${result.error}`); }); } // Log size changes const successResults = results.filter(r => r.success); if (successResults.length > 0) { const totalOriginalSize = successResults.reduce((sum, r) => sum + (r.originalSize || 0), 0); const totalNewSize = successResults.reduce((sum, r) => sum + (r.newSize || 0), 0); const totalDiffSize = totalOriginalSize - totalNewSize; const percentChange = totalOriginalSize > 0 ? (totalDiffSize / totalOriginalSize) * 100 : 0; console.log('\n' + chalk.bold('Size Changes:')); console.log(` Original: ${formatSize(totalOriginalSize)}`); console.log(` New: ${formatSize(totalNewSize)}`); if (totalDiffSize > 0) { console.log(` Reduced by: ${chalk.green(formatSize(totalDiffSize))} (${percentChange.toFixed(2)}%)`); } else if (totalDiffSize < 0) { console.log(` Increased by: ${chalk.yellow(formatSize(Math.abs(totalDiffSize)))} (${Math.abs(percentChange).toFixed(2)}%)`); } else { console.log(` No size change`); } } console.log('\n' + chalk.bold.green(`Comment removal completed.`)); } } catch (error) { console.error(chalk.red('✗ ') + error.message); process.exit(1); } }); // Format size in bytes to a human-readable string function formatSize(bytes) { if (bytes < 1024) { return `${bytes} B`; } else if (bytes < 1024 * 1024) { return `${(bytes / 1024).toFixed(2)} KB`; } else { return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; } } // Execute the command command.parse(process.argv); } catch (error) { console.error('Error loading command:', error.message); process.exit(1); }