UNPKG

diginext-img-magic-cli

Version:
381 lines (378 loc) 13.4 kB
import { getAllFiles_default } from "./chunk-F2OZKBDD.js"; import { isImageSupported_default } from "./chunk-PFOUXJUO.js"; import { __commonJS } from "./chunk-FTA5RKYX.js"; // package.json var require_package = __commonJS({ "package.json"(exports, module) { module.exports = { name: "diginext-img-magic-cli", version: "0.4.0", sideEffects: false, readme: "README.md", bin: { towebp: "bin/towebp" }, homepage: "https://wearetopgroup.com", author: { name: "TOP GROUP (a.k.a Digitop)", email: "dev@wearetopgroup.com" }, keywords: [ "utils", "plugins", "diginext", "diginext-img-magic-cli", "utilities", "helper", "extra" ], main: "./dist/index.js", files: [ "bin", "dist", "esm" ], scripts: { build: "tsc --module commonjs --outDir dist && tsc --module esnext --outDir esm ", build_tsup: "tsup src/**/*.ts --format esm,cjs --dts --legacy-output", "prebuild:win32": "rd /S /Q dist esm", "prebuild:darwin:linux": "rm -rf dist && rm -rf esm", "prebuild:default": "rm -rf dist && rm -rf esm", release: "npm run build && npm publish", dev: 'npm run prebuild && concurrently "tsc --module commonjs --outDir dist --watch "', lint: "TIMING=1 eslint src --fix", clean: "rm -rf .turbo && rm -rf node_modules && rm -rf dist" }, dependencies: { async: "3.2.6", dayjs: "1.11.7", "diginext-utils": "4.1.3", dotenv: "16.6.1", sharp: "0.34.5", yargs: "17.7.1" }, publishConfig: { access: "public" }, devDependencies: { "@types/async": "3.2.24", "@types/jest": "29.4.4", "@types/node": "20.15.0", "@types/yargs": "17.0.22", concurrently: "7.6.0", eslint: "8.36.0", "eslint-plugin-import": "2.27.5", esm: "3.2.25", tsup: "8.5.1", typescript: "4.9.5" } }; } }); // src/index.ts import yargs from "yargs"; import fs from "fs/promises"; import fsSync from "fs"; import path from "path"; import { queue } from "async"; import { getFileExtension, getFileNameWithoutExtension } from "diginext-utils/string"; import sharp from "sharp"; var { version } = require_package(); var ImageConverter = class { 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 = getFileNameWithoutExtension(normalizedDir); const parentDir = path.dirname(normalizedDir); this.outputDirWebp = path.join(parentDir, `${folderName}-webp`); this.outputDirThumb = path.join(parentDir, `${folderName}-thumb-webp`); } async ensureDirectoryExists(dirPath) { try { if (fsSync.existsSync(dirPath)) { await fs.rm(dirPath, { recursive: true }); } await fs.mkdir(dirPath, { recursive: true }); } catch (error) { throw new Error(`Failed to create directory ${dirPath}: ${error}`); } } async validateDirectory() { try { const stats = await fs.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 = /* @__PURE__ */ new Map(); files.forEach((file) => { if (isImageSupported_default(file)) { const nameWithoutExt = getFileNameWithoutExtension(file); const relativePath = path.dirname(file.replace(this.options.dir, "")); const key = path.join(relativePath, nameWithoutExt); if (!nameMap.has(key)) { nameMap.set(key, []); } nameMap.get(key).push(file); } }); const duplicates = []; nameMap.forEach((files2, name) => { if (files2.length > 1) { duplicates.push(...files2); console.warn(`\u26A0\uFE0F Duplicate names found for: ${name}`); console.warn(` Files: ${files2.join(", ")}`); } }); return duplicates; } async convertSingleImage(task) { const { inputPath, index, total } = task; const startTime = performance.now(); const startMemory = process.memoryUsage().heapUsed; try { const normalizedPath = inputPath.replace(/\\/g, "/"); const fileExt = getFileExtension(normalizedPath); const isSupported = isImageSupported_default(normalizedPath); if (!isSupported) { const outputPath = normalizedPath.replace(this.options.dir, this.outputDirWebp); await fs.mkdir(path.dirname(outputPath), { recursive: true }); await fs.copyFile(normalizedPath, outputPath); const endTime2 = performance.now(); const endMemory2 = process.memoryUsage().heapUsed; return { success: true, input: inputPath, output: outputPath, metrics: { durationMs: endTime2 - startTime, memoryUsedMB: (endMemory2 - startMemory) / 1024 / 1024 } }; } const webpOutputPath = normalizedPath.replace(this.options.dir, this.outputDirWebp).replace(`.${fileExt}`, ".webp"); await fs.mkdir(path.dirname(webpOutputPath), { recursive: true }); await sharp(normalizedPath).webp().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(`\u274C Failed to convert ${task.inputPath}: ${result.error}`); } else { console.log(`\u2714 [${this.processedCount}/${task.total}] Completed: ${path.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(`\u274C Worker error for ${task.inputPath}:`, error); } }; } async processWithQueue(files) { return new Promise((resolve, reject) => { const concurrency = this.options.concurrency || 10; console.log(`\u{1F680} Starting conversion of ${files.length} files with concurrency: ${concurrency}`); const q = queue(this.createWorker(), concurrency); q.error((error, task) => { console.error("Queue error occurred:", error); console.error("Failed task:", task); }); q.drain(() => { console.log("\n\u{1F3AF} All tasks completed!"); resolve(this.results); }); q.empty(() => { console.log("\u{1F4ED} Queue is empty, waiting for workers to finish..."); }); const tasks = files.map((file, index) => ({ inputPath: file, index: index + 1, total: files.length })); q.push(tasks, (error) => { if (error) { console.error("Task completion error:", error); } }); if (files.length === 0) { resolve([]); } }); } async cleanup() { try { if (fsSync.existsSync(".temp")) { await fs.rm(".temp", { recursive: true }); } } catch (error) { console.warn("\u26A0\uFE0F 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; const totalProcessingTime = results.reduce((sum, r) => { var _a; return sum + (((_a = r.metrics) == null ? 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 ? void 0 : _a.memoryUsedMB) || 0; }), 0); const currentMemory = process.memoryUsage(); console.log("\n" + "=".repeat(50)); console.log("\u{1F4CA} CONVERSION SUMMARY"); console.log("=".repeat(50)); console.log(`\u2714 Successful: ${successful}`); console.log(`\u274C Failed: ${failed}`); console.log(`\u{1F4C1} Output directory: ${this.outputDirWebp}`); if (this.options.thumb) { console.log(`\u{1F5BC}\uFE0F Thumbnail directory: ${this.outputDirThumb}`); } console.log("\n" + "-".repeat(50)); console.log("\u23F1\uFE0F PERFORMANCE METRICS"); console.log("-".repeat(50)); console.log(`\u23F1\uFE0F Total wall time: ${(totalDurationMs / 1e3).toFixed(2)}s`); console.log(`\u23F1\uFE0F Total processing time: ${(totalProcessingTime / 1e3).toFixed(2)}s`); console.log(`\u23F1\uFE0F Avg time per image: ${avgTimePerImage.toFixed(0)}ms`); console.log(`\u{1F4BE} Peak heap memory: ${(currentMemory.heapUsed / 1024 / 1024).toFixed(2)} MB`); console.log(`\u{1F4BE} Heap total: ${(currentMemory.heapTotal / 1024 / 1024).toFixed(2)} MB`); console.log(`\u{1F4BE} RSS (total process): ${(currentMemory.rss / 1024 / 1024).toFixed(2)} MB`); if (failed > 0) { console.log("\n\u274C Failed conversions:"); results.filter((r) => !r.success).forEach((r) => console.log(` \u2022 ${path.basename(r.input)}: ${r.error}`)); } console.log("=".repeat(50)); } async convert() { const conversionStartTime = performance.now(); try { this.results = []; this.processedCount = 0; await this.validateDirectory(); const files = getAllFiles_default(this.options.dir); if (files.length === 0) { console.log("\u{1F4C2} No files found in the specified directory."); return; } const duplicates = this.checkForDuplicateNames(files); if (duplicates.length > 0) { throw new Error("Cannot proceed: Found files with duplicate names but different extensions."); } await this.ensureDirectoryExists(this.outputDirWebp); if (this.options.thumb) { await this.ensureDirectoryExists(this.outputDirThumb); } const results = await this.processWithQueue(files); await this.cleanup(); const totalDurationMs = performance.now() - conversionStartTime; this.printSummary(results, totalDurationMs); } catch (error) { console.error("\u{1F4A5} Conversion failed:", error instanceof Error ? error.message : String(error)); throw error; } } }; async function parseArguments() { const argv = await yargs(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("concurrency", { alias: "c", describe: "Number of concurrent workers", type: "number", default: 12 }).help("h").alias("h", "help").epilog(`Version: ${version}`).parseAsync(); return { dir: argv.dir, thumb: argv.thumb, maxsize: argv.maxsize, concurrency: argv.concurrency }; } async function main() { try { console.log(`\u{1F3A8} Image Converter v${version}`); console.log("=".repeat(30)); const options = await parseArguments(); console.log(`\u{1F4C1} Source: ${options.dir}`); console.log(`\u{1F4CF} Max size: ${options.maxsize}px`); console.log(`\u{1F5BC}\uFE0F Generate thumbnails: ${options.thumb ? "Yes" : "No"}`); console.log(`\u26A1 Concurrency: ${options.concurrency}`); console.log("=".repeat(30)); const converter = new ImageConverter(options); await converter.convert(); console.log("\u{1F389} All done!"); } catch (error) { console.error("\u{1F4A5} Application error:", error instanceof Error ? error.message : String(error)); process.exit(1); } } process.on("unhandledRejection", (reason, promise) => { console.error("Unhandled Rejection at:", promise, "reason:", reason); process.exit(1); }); main();