UNPKG

codebase-asset-optimizer

Version:

Professional CLI development tool for optimizing and managing assets in codebases. Detects unused assets, optimizes images to WebP, optimizes videos, and automatically replaces asset references. GIFs are preserved unchanged to maintain animation functiona

537 lines • 22.7 kB
#!/usr/bin/env node import { Command } from "commander"; import inquirer from "inquirer"; import chalk from "chalk"; import figlet from "figlet"; import path from "path"; import fs from "fs-extra"; import { AssetOptimizer } from "./optimizer.js"; import { createDefaultConfig, detectProjectStructure } from "./config.js"; /** * Display welcome banner */ function displayBanner() { console.log(chalk.cyan(figlet.textSync("Asset Optimizer", { horizontalLayout: "full" }))); console.log(chalk.gray("šŸš€ Professional asset optimization for modern codebases\n")); } /** * Interactive project setup */ async function setupProject(projectDir) { console.log(chalk.blue("šŸ”§ Setting up asset optimization for your project...\n")); // Detect project structure const detectedConfig = detectProjectStructure(projectDir); const baseConfig = createDefaultConfig(projectDir); console.log(chalk.green("āœ“ Project structure detected:")); console.log(` šŸ“ Project root: ${projectDir}`); console.log(` šŸ“ Public directory: ${detectedConfig.publicDir || baseConfig.publicDir}`); console.log(` šŸ“ Source directory: ${baseConfig.sourceDir}\n`); const answers = await inquirer.prompt([ { type: "confirm", name: "useDetectedStructure", message: "Use the detected project structure?", default: true, }, { type: "input", name: "customPublicDir", message: "Enter custom public directory path:", when: (answers) => !answers.useDetectedStructure, validate: (input) => { const fullPath = path.resolve(projectDir, input); return fs.existsSync(fullPath) ? true : "Directory does not exist"; }, }, ]); let config = { ...baseConfig, ...detectedConfig }; if (!answers.useDetectedStructure && answers.customPublicDir) { config.publicDir = path.resolve(projectDir, answers.customPublicDir); } return config; } /** * Interactive audit results display */ async function displayAuditResults(optimizer) { const results = optimizer.auditResults; console.log(chalk.blue("\nšŸ“Š AUDIT RESULTS")); console.log("=".repeat(60)); // Quick stats console.log(chalk.white(`šŸ“ Total Assets: ${results.totalAssets} (${formatBytes(results.totalSize)})`)); console.log(chalk.green(`āœ… Used: ${results.summary.usedCount} (${formatBytes(results.summary.usedSize)})`)); console.log(chalk.red(`āŒ Unused: ${results.summary.unusedCount} (${formatBytes(results.summary.unusedSize)})`)); console.log(chalk.yellow(`šŸŽÆ Optimizable: ${results.summary.optimizableCount} (${formatBytes(results.summary.optimizableSize)})`)); // Asset type breakdown console.log(chalk.blue("\nšŸ“Š By Type:")); console.log(` šŸ–¼ļø Images: ${results.summary.assetsByType.images.total} total, ${results.summary.assetsByType.images.optimizable} optimizable`); console.log(` šŸŽ¬ Videos: ${results.summary.assetsByType.videos.total} total, ${results.summary.assetsByType.videos.optimizable} optimizable`); // Potential savings if (results.summary.unusedCount > 0 || results.summary.optimizableCount > 0) { const potentialSavings = results.summary.unusedSize + results.summary.optimizableSize * 0.3; // Estimate 30% optimization console.log(chalk.green(`\nšŸ’° Potential Savings: ${formatBytes(potentialSavings)}`)); } // Show top opportunities if (results.unusedAssets.length > 0) { console.log(chalk.red(`\nšŸ—‘ļø Top Unused Assets:`)); results.unusedAssets.slice(0, 3).forEach((asset, i) => { console.log(` ${i + 1}. ${asset.filename} (${formatBytes(asset.size)})`); }); } if (results.optimizableAssets.length > 0) { console.log(chalk.yellow(`\nšŸŽÆ Top Optimization Opportunities:`)); results.optimizableAssets.slice(0, 3).forEach((asset, i) => { const type = asset.assetType === "video" ? "ļæ½ Video" : "šŸ–¼ļø Image"; console.log(` ${i + 1}. ${type} ${asset.filename} (${formatBytes(asset.size)})`); }); } // Interactive action selection const choices = []; if (results.summary.optimizableCount > 0) { choices.push({ name: `šŸŽÆ Optimize ${results.summary.optimizableCount} assets (recommended)`, value: "optimize", }); } if (results.summary.unusedCount > 0) { choices.push({ name: `šŸ—‘ļø Remove ${results.summary.unusedCount} unused assets (${formatBytes(results.summary.unusedSize)} savings)`, value: "remove", }); } choices.push({ name: "šŸ“‹ View detailed report", value: "report" }, { name: "šŸ”„ Run audit again", value: "reaudit" }, { name: "āŒ Exit", value: "exit" }); const { action } = await inquirer.prompt([ { type: "list", name: "action", message: "\nWhat would you like to do?", choices, }, ]); return action; } /** * Interactive optimization flow */ async function runOptimization(optimizer) { const optimizableAssets = optimizer.optimizableAssets; if (optimizableAssets.length === 0) { console.log(chalk.green("āœ… No assets need optimization!")); return; } console.log(chalk.yellow(`\nšŸŽÆ Ready to optimize ${optimizableAssets.length} assets\n`)); // Show what will happen const images = optimizableAssets.filter((a) => a.assetType === "image"); const videos = optimizableAssets.filter((a) => a.assetType === "video"); if (images.length > 0) { console.log(chalk.blue(`šŸ–¼ļø ${images.length} images will be converted to WebP format`)); console.log(chalk.gray(" • Only optimized if size reduction is achieved")); } if (videos.length > 0) { console.log(chalk.magenta(`ļæ½ ${videos.length} videos will be optimized for web`)); console.log(chalk.gray(" • Re-encoded with optimized compression settings")); console.log(chalk.gray(" • Only optimized if size reduction is achieved")); } // Safety confirmation const { proceed, createBackup } = await inquirer.prompt([ { type: "confirm", name: "createBackup", message: "Create backup before optimization? (recommended)", default: true, }, { type: "confirm", name: "proceed", message: "Proceed with optimization?", default: true, }, ]); if (!proceed) { console.log(chalk.yellow("ā¹ļø Optimization cancelled")); return; } console.log(chalk.blue("\nšŸš€ Starting optimization...\n")); try { const stats = await optimizer.optimizeAssets(createBackup); // Success summary console.log(chalk.green("\nšŸŽ‰ Optimization Complete!")); console.log("=".repeat(40)); console.log(`āœ… Successfully optimized: ${stats.optimized}/${stats.processed} assets`); console.log(`šŸ’¾ Space saved: ${formatBytes(stats.sizeBefore - stats.sizeAfter)}`); console.log(`ā±ļø Time taken: ${(stats.duration / 1000).toFixed(1)}s`); if (stats.failed > 0) { console.log(chalk.yellow(`āš ļø Failed to optimize: ${stats.failed} assets`)); } } catch (error) { console.error(chalk.red(`āŒ Optimization failed: ${error}`)); } } /** * Interactive removal flow */ async function runRemoval(optimizer) { const unusedAssets = optimizer.unusedAssets; if (unusedAssets.length === 0) { console.log(chalk.green("āœ… No unused assets found!")); return; } console.log(chalk.red(`\nšŸ—‘ļø Found ${unusedAssets.length} unused assets\n`)); // Show top unused assets console.log("Top unused assets:"); unusedAssets.slice(0, 10).forEach((asset, i) => { console.log(` ${i + 1}. ${asset.filename} (${formatBytes(asset.size)})`); }); if (unusedAssets.length > 10) { console.log(` ... and ${unusedAssets.length - 10} more`); } const totalSize = unusedAssets.reduce((sum, asset) => sum + asset.size, 0); console.log(chalk.yellow(`\nšŸ’° Total space to be freed: ${formatBytes(totalSize)}`)); const { proceed, createBackup } = await inquirer.prompt([ { type: "confirm", name: "createBackup", message: "Create backup before removal? (recommended)", default: true, }, { type: "confirm", name: "proceed", message: "Are you sure you want to remove these unused assets?", default: false, }, ]); if (!proceed) { console.log(chalk.yellow("ā¹ļø Removal cancelled")); return; } try { const result = await optimizer.removeUnusedAssets(createBackup); console.log(chalk.green("\nšŸŽ‰ Cleanup Complete!")); console.log(`āœ… Removed: ${result.removed} assets`); console.log(`šŸ’¾ Space freed: ${formatBytes(result.savedSpace)}`); if (result.backupPath) { console.log(`šŸ“¦ Backup created: ${result.backupPath}`); } } catch (error) { console.error(chalk.red(`āŒ Removal failed: ${error}`)); } } /** * Main interactive flow */ async function runInteractive(projectDir) { displayBanner(); const config = await setupProject(projectDir); const optimizer = new AssetOptimizer(config); console.log(chalk.blue("šŸ” Running asset audit...\n")); await optimizer.audit(); while (true) { const action = await displayAuditResults(optimizer); switch (action) { case "optimize": await runOptimization(optimizer); break; case "remove": await runRemoval(optimizer); break; case "report": optimizer.displayResults(); break; case "reaudit": console.log(chalk.blue("\nšŸ”„ Re-running audit...\n")); await optimizer.audit(); continue; case "exit": console.log(chalk.green("\nšŸ‘‹ Thanks for using Asset Optimizer!")); return; } // Ask if user wants to continue const { continueSession } = await inquirer.prompt([ { type: "confirm", name: "continueSession", message: "\nWould you like to perform another action?", default: true, }, ]); if (!continueSession) { console.log(chalk.green("\nšŸ‘‹ Thanks for using Asset Optimizer!")); break; } // Re-run audit after any changes console.log(chalk.blue("\nšŸ”„ Refreshing audit...\n")); await optimizer.audit(); } } /** * Format bytes to human readable string */ function formatBytes(bytes) { if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; } /** * CLI Program Setup */ const program = new Command(); program .name("codebase-asset-optimizer") .description("šŸš€ Professional asset optimization for modern codebases") .version("1.1.1"); // Interactive mode (default and recommended) program .command("interactive [directory]", { isDefault: true }) .alias("i") .description("šŸŽÆ Interactive asset optimization (recommended)") .action(async (directory) => { const projectDir = path.resolve(directory || process.cwd()); await runInteractive(projectDir); }); // Quick audit command program .command("audit [directory]") .description("šŸ” Run asset audit and display results") .option("-p, --public-dir <dir>", "Public assets directory") .action(async (directory, options) => { const projectDir = path.resolve(directory || process.cwd()); let config = createDefaultConfig(projectDir); if (options?.publicDir) { config.publicDir = path.resolve(projectDir, options.publicDir); } const optimizer = new AssetOptimizer(config); console.log(chalk.blue("šŸ” Running asset audit...\n")); await optimizer.audit(); optimizer.displayResults(); }); // Quick optimize command program .command("optimize [directory]") .description("šŸŽÆ Optimize assets automatically") .option("-p, --public-dir <dir>", "Public assets directory") .option("--no-backup", "Skip creating backup") .option("-f, --force", "Skip confirmations") .option("--dry-run", "Show what would be optimized without making changes") .action(async (directory, options) => { const projectDir = path.resolve(directory || process.cwd()); let config = createDefaultConfig(projectDir); if (options?.publicDir) { config.publicDir = path.resolve(projectDir, options.publicDir); } const optimizer = new AssetOptimizer(config); console.log(chalk.blue("šŸ” Running asset audit...\n")); await optimizer.audit(); const optimizableCount = optimizer.optimizableAssets.length; if (optimizableCount === 0) { console.log(chalk.green("āœ… No assets need optimization!")); return; } // Handle dry-run mode if (options?.dryRun) { console.log(chalk.yellow("šŸ” DRY RUN MODE - No files will be modified\n")); console.log(chalk.blue("šŸ“‹ Assets that would be optimized:")); optimizer.optimizableAssets.forEach((asset, index) => { const sizeDisplay = formatBytes(asset.size); const typeInfo = asset.assetType === 'image' ? 'šŸ–¼ļø' : 'šŸŽ¬'; console.log(` ${index + 1}. ${typeInfo} ${chalk.cyan(asset.relativePath)} - ${chalk.green(sizeDisplay)}`); }); const totalSize = optimizer.optimizableAssets.reduce((sum, asset) => sum + asset.size, 0); const imageCount = optimizer.optimizableAssets.filter(a => a.assetType === 'image').length; const videoCount = optimizer.optimizableAssets.filter(a => a.assetType === 'video').length; console.log(chalk.blue("\nšŸ“Š Summary:")); console.log(` šŸ“ Total assets: ${optimizableCount}`); console.log(` ļæ½ļø Images: ${imageCount} (would be converted to WebP)`); console.log(` šŸŽ¬ Videos: ${videoCount} (would be optimized for web)`); console.log(` ļæ½ Current size: ${formatBytes(totalSize)}`); console.log(` šŸ’° Estimated savings: 30-70% typical for images, 20-90% for videos`); console.log(chalk.yellow("\nšŸ’” Run without --dry-run to apply optimizations")); return; } if (!options?.force) { const { proceed } = await inquirer.prompt([ { type: "confirm", name: "proceed", message: `Optimize ${optimizableCount} assets?`, default: true, }, ]); if (!proceed) { console.log(chalk.yellow("ā¹ļø Optimization cancelled")); return; } } const stats = await optimizer.optimizeAssets(options?.backup !== false); console.log(chalk.green("\nšŸŽ‰ Optimization Complete!")); console.log(`āœ… Successfully optimized: ${stats.optimized}/${stats.processed} assets`); console.log(`šŸ’¾ Space saved: ${formatBytes(stats.sizeBefore - stats.sizeAfter)}`); }); // Clean unused assets program .command("clean [directory]") .description("šŸ—‘ļø Remove unused assets") .option("-p, --public-dir <dir>", "Public assets directory") .option("--no-backup", "Skip creating backup") .option("-f, --force", "Skip confirmations") .action(async (directory, options) => { const projectDir = path.resolve(directory || process.cwd()); let config = createDefaultConfig(projectDir); if (options?.publicDir) { config.publicDir = path.resolve(projectDir, options.publicDir); } const optimizer = new AssetOptimizer(config); console.log(chalk.blue("šŸ” Running asset audit...\n")); await optimizer.audit(); const unusedCount = optimizer.unusedAssets.length; if (unusedCount === 0) { console.log(chalk.green("āœ… No unused assets found!")); return; } if (!options?.force) { const totalSize = optimizer.unusedAssets.reduce((sum, asset) => sum + asset.size, 0); const { proceed } = await inquirer.prompt([ { type: "confirm", name: "proceed", message: `Remove ${unusedCount} unused assets (${formatBytes(totalSize)})?`, default: false, }, ]); if (!proceed) { console.log(chalk.yellow("ā¹ļø Removal cancelled")); return; } } const result = await optimizer.removeUnusedAssets(options?.backup !== false); console.log(chalk.green("\nšŸŽ‰ Cleanup Complete!")); console.log(`āœ… Removed: ${result.removed} assets`); console.log(`šŸ’¾ Space freed: ${formatBytes(result.savedSpace)}`); }); // Setup command for adding npm scripts program .command("setup") .description("šŸ“‹ Add convenient npm scripts to package.json") .action(async () => { try { console.log(chalk.cyan('\nšŸš€ Codebase Asset Optimizer Setup\n')); const packageJsonPath = findPackageJson(); if (!packageJsonPath) { console.log(chalk.red('āŒ No package.json found in current directory or parent directories.')); console.log(chalk.gray(' Make sure you are in a Node.js project directory.\n')); return; } console.log(chalk.green(`šŸ“¦ Found package.json: ${path.relative(process.cwd(), packageJsonPath)}`)); // Read existing package.json let packageJson; try { const packageJsonContent = await fs.readFile(packageJsonPath, 'utf8'); packageJson = JSON.parse(packageJsonContent); } catch (error) { console.log(chalk.red('āŒ Error reading package.json:'), error); return; } // Define suggested scripts const suggestedScripts = { 'optimize:assets': 'npx codebase-asset-optimizer optimize', 'optimize:assets:dry': 'npx codebase-asset-optimizer optimize --dry-run', 'audit:assets': 'npx codebase-asset-optimizer audit', 'clean:assets': 'npx codebase-asset-optimizer clean', 'assets:interactive': 'npx codebase-asset-optimizer interactive' }; const scriptDescriptions = { 'optimize:assets': 'Optimize all assets (images → WebP, video compression)', 'optimize:assets:dry': 'Preview what would be optimized (safe, no changes)', 'audit:assets': 'Analyze asset usage and optimization opportunities', 'clean:assets': 'Remove unused assets from your project', 'assets:interactive': 'Interactive mode with guided optimization steps' }; // Check which scripts already exist const existingScripts = packageJson.scripts || {}; const availableScripts = Object.entries(suggestedScripts).filter(([scriptName]) => !existingScripts[scriptName]); if (availableScripts.length === 0) { console.log(chalk.green('āœ… All suggested scripts already exist in your package.json!')); console.log(chalk.cyan('\nYou can run:')); Object.keys(suggestedScripts).forEach(scriptName => { console.log(chalk.white(` npm run ${scriptName}`)); }); return; } // Show available scripts console.log(chalk.yellow('\nšŸš€ Available npm scripts to add:\n')); availableScripts.forEach(([scriptName]) => { console.log(chalk.white(` npm run ${scriptName}`)); console.log(chalk.gray(` └─ ${scriptDescriptions[scriptName]}\n`)); }); const { addScripts } = await inquirer.prompt([ { type: 'confirm', name: 'addScripts', message: 'Add these scripts to your package.json?', default: true } ]); if (!addScripts) { console.log(chalk.yellow('ā¹ļø Setup cancelled. You can run this command again anytime.')); return; } // Add scripts to package.json if (!packageJson.scripts) { packageJson.scripts = {}; } let addedCount = 0; availableScripts.forEach(([scriptName, command]) => { packageJson.scripts[scriptName] = command; addedCount++; }); // Write updated package.json try { await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n'); console.log(chalk.green(`\nāœ… Added ${addedCount} npm scripts to package.json!`)); console.log(chalk.cyan('\nšŸ’” Install as dev dependency:')); console.log(chalk.white(' npm install --save-dev codebase-asset-optimizer')); console.log(chalk.cyan('\nšŸŽÆ Quick Start:')); console.log(chalk.white(' npm run audit:assets')); console.log(chalk.gray(' └─ See what assets can be optimized')); console.log(chalk.white(' npm run optimize:assets:dry')); console.log(chalk.gray(' └─ Preview changes before applying them')); console.log(chalk.white(' npm run optimize:assets')); console.log(chalk.gray(' └─ Optimize your assets\n')); } catch (error) { console.log(chalk.red('āŒ Error writing package.json:'), error); } } catch (error) { console.log(chalk.red('āŒ Setup failed:'), error); } }); /** * Find package.json in current directory or parent directories */ function findPackageJson() { let currentDir = process.cwd(); const root = path.parse(currentDir).root; while (currentDir !== root) { const packageJsonPath = path.join(currentDir, 'package.json'); if (fs.existsSync(packageJsonPath)) { return packageJsonPath; } currentDir = path.dirname(currentDir); } return null; } // Error handling program.configureHelp({ sortSubcommands: true, }); program.showHelpAfterError(); // Parse CLI arguments program.parse(); //# sourceMappingURL=cli.js.map