UNPKG

diginext-img-magic-cli

Version:
363 lines (362 loc) • 15.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const yargs_1 = __importDefault(require("yargs")); const promises_1 = __importDefault(require("fs/promises")); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const async_1 = require("async"); const string_1 = require("diginext-utils/string"); const getAllFiles_1 = __importDefault(require("./plugins/getAllFiles")); const sharp_1 = __importDefault(require("sharp")); const isImageSupported_1 = __importDefault(require("./plugins/isImageSupported")); const { version } = require("../package.json"); class ImageConverter { constructor(options) { this.outputDirWebp = ""; this.outputDirThumb = ""; this.results = []; this.processedCount = 0; this.options = options; this.setupOutputDirectories(); } setupOutputDirectories() { const normalizedDir = this.options.dir.replace(/\\/g, "/"); const folderName = (0, string_1.getFileNameWithoutExtension)(normalizedDir); const parentDir = path_1.default.dirname(normalizedDir); this.outputDirWebp = path_1.default.join(parentDir, `${folderName}-webp`); this.outputDirThumb = path_1.default.join(parentDir, `${folderName}-thumb-webp`); } async ensureDirectoryExists(dirPath) { try { if (fs_1.default.existsSync(dirPath)) { await promises_1.default.rm(dirPath, { recursive: true }); } await promises_1.default.mkdir(dirPath, { recursive: true }); } catch (error) { throw new Error(`Failed to create directory ${dirPath}: ${error}`); } } async validateDirectory() { try { const stats = await promises_1.default.lstat(this.options.dir); if (!stats.isDirectory()) { throw new Error("Path is not a directory"); } } catch (error) { throw new Error(`Invalid directory: ${this.options.dir}`); } } checkForDuplicateNames(files) { const nameMap = new Map(); files.forEach((file) => { if ((0, isImageSupported_1.default)(file)) { const nameWithoutExt = (0, string_1.getFileNameWithoutExtension)(file); const relativePath = path_1.default.dirname(file.replace(this.options.dir, "")); const key = path_1.default.join(relativePath, nameWithoutExt); if (!nameMap.has(key)) { nameMap.set(key, []); } nameMap.get(key).push(file); } }); const duplicates = []; nameMap.forEach((files, name) => { if (files.length > 1) { duplicates.push(...files); console.warn(`āš ļø Duplicate names found for: ${name}`); console.warn(` Files: ${files.join(", ")}`); } }); return duplicates; } async convertSingleImage(task) { var _a; const { inputPath, index, total } = task; const startTime = performance.now(); const startMemory = process.memoryUsage().heapUsed; try { // console.log(`šŸ“ø [${index}/${total}] Processing: ${path.basename(inputPath)}`); const normalizedPath = inputPath.replace(/\\/g, "/"); const fileExt = (0, string_1.getFileExtension)(normalizedPath); const isSupported = (0, isImageSupported_1.default)(normalizedPath); // Handle non-image files if (!isSupported) { const outputPath = normalizedPath.replace(this.options.dir, this.outputDirWebp); await promises_1.default.mkdir(path_1.default.dirname(outputPath), { recursive: true }); await promises_1.default.copyFile(normalizedPath, outputPath); const endTime = performance.now(); const endMemory = process.memoryUsage().heapUsed; return { success: true, input: inputPath, output: outputPath, metrics: { durationMs: endTime - startTime, memoryUsedMB: (endMemory - startMemory) / 1024 / 1024, }, }; } // Convert image to WebP const webpOutputPath = normalizedPath.replace(this.options.dir, this.outputDirWebp).replace(`.${fileExt}`, ".webp"); await promises_1.default.mkdir(path_1.default.dirname(webpOutputPath), { recursive: true }); await (0, sharp_1.default)(normalizedPath).webp({ quality: (_a = this.options.quality) !== null && _a !== void 0 ? _a : 90 }).toFile(webpOutputPath); const endTime = performance.now(); const endMemory = process.memoryUsage().heapUsed; return { success: true, input: inputPath, output: webpOutputPath, metrics: { durationMs: endTime - startTime, memoryUsedMB: (endMemory - startMemory) / 1024 / 1024, }, }; } catch (error) { const endTime = performance.now(); const endMemory = process.memoryUsage().heapUsed; return { success: false, input: inputPath, error: error instanceof Error ? error.message : String(error), metrics: { durationMs: endTime - startTime, memoryUsedMB: (endMemory - startMemory) / 1024 / 1024, }, }; } } createWorker() { return async (task) => { try { const result = await this.convertSingleImage(task); this.results.push(result); this.processedCount++; if (!result.success) { console.error(`āŒ Failed to convert ${task.inputPath}: ${result.error}`); } else { console.log(`āœ” [${this.processedCount}/${task.total}] Completed: ${path_1.default.basename(task.inputPath)}`); } } catch (error) { const errorResult = { success: false, input: task.inputPath, error: error instanceof Error ? error.message : String(error), }; this.results.push(errorResult); this.processedCount++; console.error(`āŒ Worker error for ${task.inputPath}:`, error); } }; } async processWithQueue(files) { return new Promise((resolve, reject) => { const concurrency = this.options.concurrency || 10; console.log(`šŸš€ Starting conversion of ${files.length} files with concurrency: ${concurrency}`); // Create the queue with our worker function const q = (0, async_1.queue)(this.createWorker(), concurrency); // Set up queue event handlers q.error((error, task) => { console.error("Queue error occurred:", error); console.error("Failed task:", task); }); q.drain(() => { console.log("\nšŸŽÆ All tasks completed!"); resolve(this.results); }); // Add progress monitoring // let lastProgress = 0; // q.saturated(() => { // console.log("šŸ”„ Queue is running at full capacity"); // }); q.empty(() => { console.log("šŸ“­ Queue is empty, waiting for workers to finish..."); }); // Create tasks and add them to the queue const tasks = files.map((file, index) => ({ inputPath: file, index: index + 1, total: files.length, })); // Add all tasks to the queue q.push(tasks, (error) => { if (error) { console.error("Task completion error:", error); } }); // Handle the case where there are no files if (files.length === 0) { resolve([]); } }); } async cleanup() { try { if (fs_1.default.existsSync(".temp")) { await promises_1.default.rm(".temp", { recursive: true }); } } catch (error) { console.warn("āš ļø Warning: Failed to cleanup temp directory:", error); } } printSummary(results, totalDurationMs) { const successful = results.filter((r) => r.success).length; const failed = results.filter((r) => !r.success).length; // Calculate metrics const totalProcessingTime = results.reduce((sum, r) => { var _a; return sum + (((_a = r.metrics) === null || _a === void 0 ? void 0 : _a.durationMs) || 0); }, 0); const avgTimePerImage = results.length > 0 ? totalProcessingTime / results.length : 0; const maxMemoryUsed = Math.max(...results.map((r) => { var _a; return ((_a = r.metrics) === null || _a === void 0 ? void 0 : _a.memoryUsedMB) || 0; }), 0); const currentMemory = process.memoryUsage(); console.log("\n" + "=".repeat(50)); console.log("šŸ“Š CONVERSION SUMMARY"); console.log("=".repeat(50)); console.log(`āœ” Successful: ${successful}`); console.log(`āŒ Failed: ${failed}`); console.log(`šŸ“ Output directory: ${this.outputDirWebp}`); if (this.options.thumb) { console.log(`šŸ–¼ļø Thumbnail directory: ${this.outputDirThumb}`); } console.log("\n" + "-".repeat(50)); console.log("ā±ļø PERFORMANCE METRICS"); console.log("-".repeat(50)); console.log(`ā±ļø Total wall time: ${(totalDurationMs / 1000).toFixed(2)}s`); console.log(`ā±ļø Total processing time: ${(totalProcessingTime / 1000).toFixed(2)}s`); console.log(`ā±ļø Avg time per image: ${avgTimePerImage.toFixed(0)}ms`); console.log(`šŸ’¾ Peak heap memory: ${(currentMemory.heapUsed / 1024 / 1024).toFixed(2)} MB`); console.log(`šŸ’¾ Heap total: ${(currentMemory.heapTotal / 1024 / 1024).toFixed(2)} MB`); console.log(`šŸ’¾ RSS (total process): ${(currentMemory.rss / 1024 / 1024).toFixed(2)} MB`); if (failed > 0) { console.log("\nāŒ Failed conversions:"); results.filter((r) => !r.success).forEach((r) => console.log(` • ${path_1.default.basename(r.input)}: ${r.error}`)); } console.log("=".repeat(50)); } async convert() { const conversionStartTime = performance.now(); try { // Reset state this.results = []; this.processedCount = 0; // Validate input directory await this.validateDirectory(); // Get all files const files = (0, getAllFiles_1.default)(this.options.dir, undefined, this.options.ignore); if (files.length === 0) { console.log("šŸ“‚ No files found in the specified directory."); return; } // Check for duplicate names const duplicates = this.checkForDuplicateNames(files); if (duplicates.length > 0) { throw new Error("Cannot proceed: Found files with duplicate names but different extensions."); } // Setup output directories await this.ensureDirectoryExists(this.outputDirWebp); if (this.options.thumb) { await this.ensureDirectoryExists(this.outputDirThumb); } // Process files using async queue const results = await this.processWithQueue(files); // Cleanup and summary await this.cleanup(); const totalDurationMs = performance.now() - conversionStartTime; this.printSummary(results, totalDurationMs); } catch (error) { console.error("šŸ’„ Conversion failed:", error instanceof Error ? error.message : String(error)); throw error; } } } async function parseArguments() { const argv = await (0, yargs_1.default)(process.argv.slice(2)) .usage("Usage: $0 [options]") .example('$0 --dir "./images" --thumb --maxsize 2048 --concurrency 5', "Convert images with 5 concurrent workers") .option("dir", { alias: "d", describe: "Source directory containing images", type: "string", demandOption: true, }) .option("thumb", { alias: "t", describe: "Generate thumbnails", type: "boolean", default: false, }) .option("maxsize", { alias: "m", describe: "Maximum size for converted images", type: "number", default: 4096, }) .option("quality", { alias: "q", describe: "WebP quality (1-100)", type: "number", default: 90, }) .option("concurrency", { alias: "c", describe: "Number of concurrent workers", type: "number", default: 12, }) .option("ignore", { alias: "i", describe: "Folder names to ignore (can specify multiple)", type: "string", array: true, default: [], }) .help("h") .alias("h", "help") .epilog(`Version: ${version}`) .parseAsync(); return { dir: argv.dir, thumb: argv.thumb, maxsize: argv.maxsize, quality: argv.quality, concurrency: argv.concurrency, ignore: argv.ignore, }; } async function main() { try { console.log(`šŸŽØ Image Converter v${version}`); console.log("=".repeat(30)); const options = await parseArguments(); console.log(`šŸ“ Source: ${options.dir}`); console.log(`šŸ“ Max size: ${options.maxsize}px`); console.log(`šŸŽÆ Quality: ${options.quality}`); console.log(`šŸ–¼ļø Generate thumbnails: ${options.thumb ? "Yes" : "No"}`); console.log(`⚔ Concurrency: ${options.concurrency}`); if (options.ignore && options.ignore.length > 0) { console.log(`🚫 Ignoring folders: ${options.ignore.join(", ")}`); } console.log("=".repeat(30)); const converter = new ImageConverter(options); await converter.convert(); console.log("šŸŽ‰ All done!"); } catch (error) { console.error("šŸ’„ Application error:", error instanceof Error ? error.message : String(error)); process.exit(1); } } // Handle unhandled promise rejections process.on("unhandledRejection", (reason, promise) => { console.error("Unhandled Rejection at:", promise, "reason:", reason); process.exit(1); }); // Run the application main();