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
JavaScript
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