diginext-img-magic-cli
Version:
README.md
381 lines (378 loc) • 13.4 kB
JavaScript
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();