@regele/devtools
Version:
A collection of developer utilities for code processing and text analysis
445 lines (388 loc) • 18.2 kB
JavaScript
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);
}