webpocalypse 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # webpocalypse
2
+
3
+ > Batch convert images to WebP/AVIF — fully local, no server, no API calls.
4
+
5
+ ```
6
+ npx webpocalypse ./public --format webp --quality 80 --max-width 1920
7
+ ```
8
+
9
+ Recursively scans a directory, converts every `.jpg`, `.jpeg`, `.png` to WebP
10
+ and/or AVIF using [sharp](https://sharp.pixelplumbing.com/), preserves the full
11
+ folder structure, and reports per-file savings.
12
+
13
+ ---
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install -g webpocalypse
19
+ # or run without installing:
20
+ npx webpocalypse <input> [options]
21
+ ```
22
+
23
+ ---
24
+
25
+ ## Usage
26
+
27
+ ```
28
+ webpocalypse <input> [options]
29
+
30
+ Arguments:
31
+ input Directory of images to convert
32
+
33
+ Options:
34
+ -f, --format <format> Output format: webp | avif | both (default: webp)
35
+ -q, --quality <number> Compression quality 1–100 (default: 80)
36
+ --lossless Lossless compression
37
+ --max-width <px> Maximum output width (no upscaling)
38
+ --max-height <px> Maximum output height (no upscaling)
39
+ -o, --out <path> Output directory (default: <input>-optimized)
40
+ --in-place Replace source directory safely via temp dir
41
+ -h, --help Show help
42
+ -V, --version Show version
43
+ ```
44
+
45
+ ---
46
+
47
+ ## Examples
48
+
49
+ ```bash
50
+ # WebP at quality 80 (default)
51
+ webpocalypse ./images
52
+
53
+ # Both WebP + AVIF
54
+ webpocalypse ./images --format both --quality 75
55
+
56
+ # Resize + convert
57
+ webpocalypse ./public/photos --format webp --max-width 1920 --quality 85
58
+
59
+ # Lossless WebP
60
+ webpocalypse ./assets --format webp --lossless
61
+
62
+ # Custom output directory
63
+ webpocalypse ./src/images --format avif --out ./dist/images
64
+
65
+ # Overwrite source in-place (safe: uses temp dir, rolls back on failure)
66
+ webpocalypse ./public --format webp --in-place
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Output
72
+
73
+ ```
74
+ webpocalypse v1.0.0
75
+ Input: /home/user/project/public/images
76
+ Output: /home/user/project/public/images-optimized
77
+ Format: both Quality: 80
78
+ Files: 42 images found → 84 outputs
79
+
80
+ File Original Converted Savings
81
+ ─────────────────────────────────────────────────────────────────────────────
82
+ hero.webp 1.2 MB 149 KB 88%
83
+ hero.avif 1.2 MB 218 KB 82%
84
+ icons/logo.webp 45 KB 12 KB 73%
85
+ icons/logo.avif 45 KB 9 KB 80%
86
+ ...
87
+ ─────────────────────────────────────────────────────────────────────────────
88
+
89
+ ✔ 84 files converted
90
+ ✔ 56.2 MB → 9.1 MB (84% saved)
91
+
92
+ Output: /home/user/project/public/images-optimized
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Behavior
98
+
99
+ | Extension | Action |
100
+ |------------------|-------------------------------|
101
+ | `.jpg` / `.jpeg` | Re-encoded via sharp |
102
+ | `.png` | Re-encoded via sharp |
103
+ | `.webp` / `.avif`| Copied as-is (no re-encoding) |
104
+
105
+ ### `--in-place` safety
106
+
107
+ 1. All files are written to a temporary directory first.
108
+ 2. **Only if every conversion succeeds**, the source directory is replaced atomically.
109
+ 3. On any failure the source is left completely untouched and the temp dir is cleaned up.
110
+
111
+ ### Exit codes
112
+
113
+ | Code | Meaning |
114
+ |------|--------------------------|
115
+ | `0` | All files processed OK |
116
+ | `1` | One or more files failed |
117
+
118
+ ---
119
+
120
+ ## Project structure
121
+
122
+ ```
123
+ src/
124
+ index.ts Entry point & orchestration
125
+ cli.ts Argument parsing (commander)
126
+ scan.ts Recursive directory traversal
127
+ convert.ts sharp encoding logic + p-limit concurrency
128
+ writer.ts Output & in-place replacement logic
129
+ logger.ts Table, summary, formatting helpers
130
+ types.ts Shared TypeScript types
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Requirements
136
+
137
+ - Node.js ≥ 18
138
+ - Runs on Linux, macOS, Windows (sharp ships pre-built binaries)
package/dist/cli.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { CliOptions } from './types.js';
2
+ export declare function parseArgs(argv: string[]): {
3
+ inputDir: string;
4
+ options: CliOptions;
5
+ };
6
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAgB,MAAM,YAAY,CAAC;AAI3D,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,UAAU,CAAA;CAAE,CAiEnF"}
package/dist/cli.js ADDED
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.parseArgs = parseArgs;
7
+ const commander_1 = require("commander");
8
+ const path_1 = __importDefault(require("path"));
9
+ const VALID_FORMATS = ['webp', 'avif', 'both'];
10
+ function parseArgs(argv) {
11
+ const program = new commander_1.Command();
12
+ program
13
+ .name('webpocalypse')
14
+ .description('Batch convert images to WebP/AVIF with quality control and directory structure preservation')
15
+ .version('1.0.0')
16
+ .argument('<input>', 'Input directory containing images to convert')
17
+ .option('-f, --format <format>', 'Output format: webp, avif, or both', 'webp')
18
+ .option('-q, --quality <number>', 'Compression quality (50–100)', '80')
19
+ .option('--lossless', 'Use lossless compression', false)
20
+ .option('--max-width <number>', 'Maximum output width in pixels (no upscaling)')
21
+ .option('--max-height <number>', 'Maximum output height in pixels (no upscaling)')
22
+ .option('-o, --out <path>', 'Output directory (default: <input>-optimized)')
23
+ .option('--in-place', 'Overwrite source directory safely via temp dir', false);
24
+ program.parse(argv);
25
+ const opts = program.opts();
26
+ const [inputArg] = program.args;
27
+ if (!inputArg) {
28
+ program.error('Missing required argument: <input>');
29
+ }
30
+ const format = opts.format;
31
+ if (!VALID_FORMATS.includes(format)) {
32
+ program.error(`Invalid format "${format}". Must be one of: ${VALID_FORMATS.join(', ')}`);
33
+ }
34
+ const quality = parseInt(opts.quality, 10);
35
+ if (isNaN(quality) || quality < 1 || quality > 100) {
36
+ program.error(`Invalid quality "${opts.quality}". Must be a number between 1 and 100`);
37
+ }
38
+ let maxWidth;
39
+ if (opts.maxWidth !== undefined) {
40
+ maxWidth = parseInt(opts.maxWidth, 10);
41
+ if (isNaN(maxWidth) || maxWidth <= 0) {
42
+ program.error(`Invalid --max-width "${opts.maxWidth}". Must be a positive integer`);
43
+ }
44
+ }
45
+ let maxHeight;
46
+ if (opts.maxHeight !== undefined) {
47
+ maxHeight = parseInt(opts.maxHeight, 10);
48
+ if (isNaN(maxHeight) || maxHeight <= 0) {
49
+ program.error(`Invalid --max-height "${opts.maxHeight}". Must be a positive integer`);
50
+ }
51
+ }
52
+ const inputDir = path_1.default.resolve(inputArg);
53
+ return {
54
+ inputDir,
55
+ options: {
56
+ format: format,
57
+ quality,
58
+ lossless: opts.lossless,
59
+ maxWidth,
60
+ maxHeight,
61
+ out: opts.out ? path_1.default.resolve(opts.out) : undefined,
62
+ inPlace: opts.inPlace,
63
+ },
64
+ };
65
+ }
66
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { FileEntry, CliOptions, ConversionResult } from './types.js';
2
+ /**
3
+ * Convert all files and write directly to outputDir.
4
+ * Calls onProgress after each file finishes (success or failure).
5
+ */
6
+ export declare function convertFiles(files: FileEntry[], outputDir: string, options: CliOptions, onProgress: (result: ConversionResult) => void): Promise<ConversionResult[]>;
7
+ //# sourceMappingURL=convert.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"convert.d.ts","sourceRoot":"","sources":["../src/convert.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAgF1E;;;GAGG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,SAAS,EAAE,EAClB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,UAAU,EACnB,UAAU,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,GAC7C,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAuD7B"}
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.convertFiles = convertFiles;
7
+ const sharp_1 = __importDefault(require("sharp"));
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const fs_extra_1 = __importDefault(require("fs-extra"));
11
+ const p_limit_1 = __importDefault(require("p-limit"));
12
+ const scan_js_1 = require("./scan.js");
13
+ function applyResize(image, options) {
14
+ if (!options.maxWidth && !options.maxHeight)
15
+ return image;
16
+ return image.resize({
17
+ width: options.maxWidth,
18
+ height: options.maxHeight,
19
+ fit: 'inside',
20
+ withoutEnlargement: true,
21
+ });
22
+ }
23
+ async function encodeToFile(file, format, outputDir, options) {
24
+ const inputBuffer = fs_1.default.readFileSync(file.inputPath);
25
+ const originalSize = inputBuffer.length;
26
+ const outputRelativePath = (0, scan_js_1.replaceExtension)(file.relativePath, format);
27
+ const outputPath = path_1.default.join(outputDir, outputRelativePath);
28
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(outputPath));
29
+ let image = (0, sharp_1.default)(inputBuffer);
30
+ image = applyResize(image, options);
31
+ if (format === 'webp') {
32
+ await (options.lossless
33
+ ? image.webp({ lossless: true })
34
+ : image.webp({ quality: options.quality })).toFile(outputPath);
35
+ }
36
+ else {
37
+ // effort 2 (scale 0–9): ~10× faster than default 4 with negligible quality
38
+ // loss for web use. The default effort level locks the CPU.
39
+ await (options.lossless
40
+ ? image.avif({ lossless: true, effort: 2 })
41
+ : image.avif({ quality: options.quality, effort: 2 })).toFile(outputPath);
42
+ }
43
+ const convertedSize = fs_1.default.statSync(outputPath).size;
44
+ return {
45
+ inputPath: file.inputPath,
46
+ relativePath: file.relativePath,
47
+ outputPath,
48
+ outputRelativePath,
49
+ originalSize,
50
+ convertedSize,
51
+ success: true,
52
+ skipped: false,
53
+ };
54
+ }
55
+ async function passthroughFile(file, outputDir) {
56
+ const outputPath = path_1.default.join(outputDir, file.relativePath);
57
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(outputPath));
58
+ await fs_extra_1.default.copy(file.inputPath, outputPath);
59
+ const size = fs_1.default.statSync(file.inputPath).size;
60
+ return {
61
+ inputPath: file.inputPath,
62
+ relativePath: file.relativePath,
63
+ outputPath,
64
+ outputRelativePath: file.relativePath,
65
+ originalSize: size,
66
+ convertedSize: size,
67
+ success: true,
68
+ skipped: true,
69
+ };
70
+ }
71
+ /**
72
+ * Convert all files and write directly to outputDir.
73
+ * Calls onProgress after each file finishes (success or failure).
74
+ */
75
+ async function convertFiles(files, outputDir, options, onProgress) {
76
+ // AVIF is much heavier to encode — reduce concurrency to avoid CPU saturation
77
+ const concurrency = options.format === 'avif' ? 3
78
+ : options.format === 'both' ? 4
79
+ : 6;
80
+ const limit = (0, p_limit_1.default)(concurrency);
81
+ const allResults = [];
82
+ const tasks = files.flatMap((file) => {
83
+ if ((0, scan_js_1.isPassthrough)(file.inputPath)) {
84
+ return [
85
+ limit(async () => {
86
+ const result = await passthroughFile(file, outputDir);
87
+ allResults.push(result);
88
+ onProgress(result);
89
+ }),
90
+ ];
91
+ }
92
+ const formats = options.format === 'both' ? ['webp', 'avif'] : [options.format];
93
+ return formats.map((fmt) => limit(async () => {
94
+ let result;
95
+ try {
96
+ result = await encodeToFile(file, fmt, outputDir, options);
97
+ }
98
+ catch (err) {
99
+ const stat = (() => {
100
+ try {
101
+ return fs_1.default.statSync(file.inputPath);
102
+ }
103
+ catch {
104
+ return null;
105
+ }
106
+ })();
107
+ result = {
108
+ inputPath: file.inputPath,
109
+ relativePath: file.relativePath,
110
+ outputPath: '',
111
+ outputRelativePath: (0, scan_js_1.replaceExtension)(file.relativePath, fmt),
112
+ originalSize: stat?.size ?? 0,
113
+ convertedSize: 0,
114
+ success: false,
115
+ skipped: false,
116
+ error: err instanceof Error ? err.message : String(err),
117
+ };
118
+ }
119
+ allResults.push(result);
120
+ onProgress(result);
121
+ }));
122
+ });
123
+ await Promise.all(tasks);
124
+ return allResults;
125
+ }
126
+ //# sourceMappingURL=convert.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"convert.js","sourceRoot":"","sources":["../src/convert.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const ora_1 = __importDefault(require("ora"));
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ const cli_js_1 = require("./cli.js");
11
+ const scan_js_1 = require("./scan.js");
12
+ const writer_js_1 = require("./writer.js");
13
+ const logger_js_1 = require("./logger.js");
14
+ async function main() {
15
+ const { inputDir, options } = (0, cli_js_1.parseArgs)(process.argv);
16
+ // ── Validate input directory ──────────────────────────────────────────────
17
+ if (!fs_1.default.existsSync(inputDir)) {
18
+ console.error(chalk_1.default.red(`✖ Input directory not found: ${inputDir}`));
19
+ process.exit(1);
20
+ }
21
+ const stat = fs_1.default.statSync(inputDir);
22
+ if (!stat.isDirectory()) {
23
+ console.error(chalk_1.default.red(`✖ Input path is not a directory: ${inputDir}`));
24
+ process.exit(1);
25
+ }
26
+ // ── Scan ──────────────────────────────────────────────────────────────────
27
+ const spinner = (0, ora_1.default)({ text: 'Scanning for images…', stream: process.stderr }).start();
28
+ const files = (0, scan_js_1.scanDirectory)(inputDir);
29
+ spinner.stop();
30
+ if (files.length === 0) {
31
+ console.log(chalk_1.default.yellow('⚠ No supported images found in: ') + inputDir);
32
+ console.log(chalk_1.default.dim(' Supported formats: .jpg, .jpeg, .png, .webp, .avif'));
33
+ process.exit(0);
34
+ }
35
+ // Each file may produce 1 or 2 output files (when --format both)
36
+ const totalOutputs = files.reduce((sum, f) => {
37
+ const isPassthrough = /\.(webp|avif)$/i.test(f.inputPath);
38
+ return sum + (isPassthrough ? 1 : options.format === 'both' ? 2 : 1);
39
+ }, 0);
40
+ const outputDir = options.inPlace
41
+ ? inputDir
42
+ : (options.out ?? `${inputDir}-optimized`);
43
+ console.log();
44
+ console.log(chalk_1.default.bold('webpocalypse') +
45
+ chalk_1.default.dim(` v${getVersion()}`));
46
+ console.log(chalk_1.default.dim(' Input: ') + inputDir);
47
+ console.log(chalk_1.default.dim(' Output: ') + (options.inPlace ? chalk_1.default.yellow(outputDir + ' (in-place)') : outputDir));
48
+ console.log(chalk_1.default.dim(' Format: ') + options.format +
49
+ chalk_1.default.dim(' Quality: ') + (options.lossless ? 'lossless' : String(options.quality)) +
50
+ (options.maxWidth ? chalk_1.default.dim(' Max-W: ') + options.maxWidth : '') +
51
+ (options.maxHeight ? chalk_1.default.dim(' Max-H: ') + options.maxHeight : ''));
52
+ console.log(chalk_1.default.dim(` Files: ${files.length} image${files.length !== 1 ? 's' : ''} found → ${totalOutputs} output${totalOutputs !== 1 ? 's' : ''}`));
53
+ // ── Convert ───────────────────────────────────────────────────────────────
54
+ (0, logger_js_1.printTableHeader)();
55
+ let doneCount = 0;
56
+ const isTTY = Boolean(process.stderr.isTTY);
57
+ // Only spin in interactive terminals; in CI/piped output just print rows.
58
+ const progressSpinner = isTTY
59
+ ? (0, ora_1.default)({ text: (0, logger_js_1.progressText)(0, totalOutputs, ''), stream: process.stderr }).start()
60
+ : null;
61
+ const allResults = [];
62
+ function onProgress(result) {
63
+ doneCount++;
64
+ if (progressSpinner) {
65
+ progressSpinner.clear();
66
+ }
67
+ (0, logger_js_1.printResultRow)(result);
68
+ allResults.push(result);
69
+ if (progressSpinner && doneCount < totalOutputs) {
70
+ progressSpinner.text = (0, logger_js_1.progressText)(doneCount, totalOutputs, result.outputRelativePath);
71
+ progressSpinner.render();
72
+ }
73
+ }
74
+ try {
75
+ const { results, outputDir: finalOutputDir } = await (0, writer_js_1.writeOutput)(inputDir, files, options, onProgress);
76
+ progressSpinner?.stop();
77
+ // Sort results by path for a stable display order
78
+ results.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
79
+ const summary = (0, logger_js_1.buildSummary)(results);
80
+ (0, logger_js_1.printSummary)(summary, finalOutputDir);
81
+ process.exit(summary.failureCount > 0 ? 1 : 0);
82
+ }
83
+ catch (err) {
84
+ progressSpinner?.stop();
85
+ console.error();
86
+ console.error(chalk_1.default.red('✖ Fatal error: ') + (err instanceof Error ? err.message : String(err)));
87
+ process.exit(1);
88
+ }
89
+ }
90
+ function getVersion() {
91
+ try {
92
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
93
+ const pkg = require('../package.json');
94
+ return pkg.version;
95
+ }
96
+ catch {
97
+ return '?';
98
+ }
99
+ }
100
+ main();
101
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { ConversionResult, ConversionSummary } from './types.js';
2
+ export declare function formatBytes(bytes: number): string;
3
+ export declare function printTableHeader(): void;
4
+ export declare function printResultRow(result: ConversionResult): void;
5
+ export declare function buildSummary(results: ConversionResult[]): ConversionSummary;
6
+ export declare function printSummary(summary: ConversionSummary, outputDir: string): void;
7
+ export declare function progressText(done: number, total: number, currentFile: string): string;
8
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAItE,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMjD;AAgCD,wBAAgB,gBAAgB,IAAI,IAAI,CAIvC;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAwC7D;AAID,wBAAgB,YAAY,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,iBAAiB,CA8B3E;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAwChF;AAID,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAIrF"}
package/dist/logger.js ADDED
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.formatBytes = formatBytes;
7
+ exports.printTableHeader = printTableHeader;
8
+ exports.printResultRow = printResultRow;
9
+ exports.buildSummary = buildSummary;
10
+ exports.printSummary = printSummary;
11
+ exports.progressText = progressText;
12
+ const chalk_1 = __importDefault(require("chalk"));
13
+ // ─── Formatting helpers ──────────────────────────────────────────────────────
14
+ function formatBytes(bytes) {
15
+ if (bytes === 0)
16
+ return '0 B';
17
+ const units = ['B', 'KB', 'MB', 'GB'];
18
+ const exp = Math.min(Math.floor(Math.log2(bytes) / 10), units.length - 1);
19
+ const value = bytes / Math.pow(1024, exp);
20
+ return `${value.toFixed(value < 10 ? 1 : 0)} ${units[exp]}`;
21
+ }
22
+ function pct(original, converted) {
23
+ if (original === 0)
24
+ return '—';
25
+ const saved = ((original - converted) / original) * 100;
26
+ return `${saved >= 0 ? saved.toFixed(0) : '+' + Math.abs(saved).toFixed(0)}%`;
27
+ }
28
+ function truncatePath(p, maxLen) {
29
+ if (p.length <= maxLen)
30
+ return p.padEnd(maxLen);
31
+ return '…' + p.slice(-(maxLen - 1));
32
+ }
33
+ // ─── Results table ───────────────────────────────────────────────────────────
34
+ const COL = {
35
+ file: 40,
36
+ original: 10,
37
+ converted: 11,
38
+ savings: 10,
39
+ };
40
+ const HEADER = chalk_1.default.bold(truncatePath('File', COL.file) + ' ' +
41
+ 'Original'.padStart(COL.original) + ' ' +
42
+ 'Converted'.padStart(COL.converted) + ' ' +
43
+ 'Savings'.padStart(COL.savings));
44
+ const DIVIDER = '─'.repeat(COL.file + COL.original + COL.converted + COL.savings + 6);
45
+ function printTableHeader() {
46
+ console.log();
47
+ console.log(HEADER);
48
+ console.log(chalk_1.default.dim(DIVIDER));
49
+ }
50
+ function printResultRow(result) {
51
+ if (!result.success) {
52
+ console.log(chalk_1.default.red('✖ ') +
53
+ chalk_1.default.red(truncatePath(result.relativePath, COL.file - 2)) +
54
+ ' ' +
55
+ chalk_1.default.dim(`Failed: ${result.error ?? 'unknown error'}`));
56
+ return;
57
+ }
58
+ const displayPath = result.skipped
59
+ ? result.relativePath + chalk_1.default.dim(' (copy)')
60
+ : result.outputRelativePath;
61
+ const fileCol = truncatePath(displayPath, COL.file);
62
+ const savings = pct(result.originalSize, result.convertedSize);
63
+ const savingsNum = result.originalSize
64
+ ? ((result.originalSize - result.convertedSize) / result.originalSize) * 100
65
+ : 0;
66
+ let savingsStr;
67
+ if (result.skipped) {
68
+ savingsStr = chalk_1.default.dim('—');
69
+ }
70
+ else if (savingsNum >= 20) {
71
+ savingsStr = chalk_1.default.green(savings.padStart(COL.savings));
72
+ }
73
+ else if (savingsNum >= 0) {
74
+ savingsStr = chalk_1.default.yellow(savings.padStart(COL.savings));
75
+ }
76
+ else {
77
+ savingsStr = chalk_1.default.red(savings.padStart(COL.savings));
78
+ }
79
+ console.log(chalk_1.default.dim(' ') +
80
+ fileCol.padEnd(COL.file) + ' ' +
81
+ chalk_1.default.dim(formatBytes(result.originalSize).padStart(COL.original)) + ' ' +
82
+ chalk_1.default.cyan(formatBytes(result.convertedSize).padStart(COL.converted)) + ' ' +
83
+ savingsStr);
84
+ }
85
+ // ─── Summary ─────────────────────────────────────────────────────────────────
86
+ function buildSummary(results) {
87
+ let totalOriginalBytes = 0;
88
+ let totalConvertedBytes = 0;
89
+ let successCount = 0;
90
+ let failureCount = 0;
91
+ let skippedCount = 0;
92
+ for (const r of results) {
93
+ if (!r.success) {
94
+ failureCount++;
95
+ totalOriginalBytes += r.originalSize;
96
+ }
97
+ else if (r.skipped) {
98
+ skippedCount++;
99
+ totalOriginalBytes += r.originalSize;
100
+ totalConvertedBytes += r.convertedSize;
101
+ }
102
+ else {
103
+ successCount++;
104
+ totalOriginalBytes += r.originalSize;
105
+ totalConvertedBytes += r.convertedSize;
106
+ }
107
+ }
108
+ return {
109
+ totalFiles: results.length,
110
+ successCount,
111
+ failureCount,
112
+ skippedCount,
113
+ totalOriginalBytes,
114
+ totalConvertedBytes,
115
+ };
116
+ }
117
+ function printSummary(summary, outputDir) {
118
+ console.log(chalk_1.default.dim(DIVIDER));
119
+ console.log();
120
+ const { successCount, failureCount, skippedCount, totalOriginalBytes, totalConvertedBytes } = summary;
121
+ if (successCount > 0) {
122
+ const savings = totalOriginalBytes > 0
123
+ ? Math.round(((totalOriginalBytes - totalConvertedBytes) / totalOriginalBytes) * 100)
124
+ : 0;
125
+ console.log(chalk_1.default.green('✔') + ' ' +
126
+ chalk_1.default.bold(`${successCount} file${successCount !== 1 ? 's' : ''} converted`));
127
+ console.log(chalk_1.default.green('✔') + ' ' +
128
+ `${formatBytes(totalOriginalBytes)} → ${formatBytes(totalConvertedBytes)} ` +
129
+ chalk_1.default.bold.green(`(${savings}% saved)`));
130
+ }
131
+ if (skippedCount > 0) {
132
+ console.log(chalk_1.default.dim('·') + ' ' +
133
+ chalk_1.default.dim(`${skippedCount} file${skippedCount !== 1 ? 's' : ''} copied as-is`));
134
+ }
135
+ if (failureCount > 0) {
136
+ console.log(chalk_1.default.red('✖') + ' ' +
137
+ chalk_1.default.red.bold(`${failureCount} file${failureCount !== 1 ? 's' : ''} failed`));
138
+ }
139
+ console.log();
140
+ console.log(chalk_1.default.dim('Output: ') + chalk_1.default.cyan(outputDir));
141
+ console.log();
142
+ }
143
+ // ─── Spinner helpers ─────────────────────────────────────────────────────────
144
+ function progressText(done, total, currentFile) {
145
+ const pctDone = total > 0 ? Math.round((done / total) * 100) : 0;
146
+ const truncated = currentFile.length > 40 ? '…' + currentFile.slice(-39) : currentFile;
147
+ return `[${done}/${total}] ${pctDone}% — ${truncated}`;
148
+ }
149
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"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"}
package/dist/scan.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { FileEntry } from './types.js';
2
+ export declare function isSupportedImage(filename: string): boolean;
3
+ export declare function isPassthrough(filename: string): boolean;
4
+ export declare function replaceExtension(filePath: string, newExt: string): string;
5
+ /**
6
+ * Recursively walk a directory and collect all supported image files.
7
+ * Returns paths relative to the given root so that directory structure
8
+ * can be replicated in the output.
9
+ */
10
+ export declare function scanDirectory(rootDir: string): FileEntry[];
11
+ //# sourceMappingURL=scan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAU5C,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAG1D;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGvD;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAIzE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,CAoB1D"}
package/dist/scan.js ADDED
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isSupportedImage = isSupportedImage;
7
+ exports.isPassthrough = isPassthrough;
8
+ exports.replaceExtension = replaceExtension;
9
+ exports.scanDirectory = scanDirectory;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ // Extensions that will be re-encoded by sharp
13
+ const CONVERTIBLE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png']);
14
+ // Extensions that are already in a target format — copy as-is
15
+ const PASSTHROUGH_EXTENSIONS = new Set(['.webp', '.avif']);
16
+ const ALL_SUPPORTED = new Set([...CONVERTIBLE_EXTENSIONS, ...PASSTHROUGH_EXTENSIONS]);
17
+ function isSupportedImage(filename) {
18
+ const ext = filename.toLowerCase().slice(filename.lastIndexOf('.'));
19
+ return ALL_SUPPORTED.has(ext);
20
+ }
21
+ function isPassthrough(filename) {
22
+ const ext = filename.toLowerCase().slice(filename.lastIndexOf('.'));
23
+ return PASSTHROUGH_EXTENSIONS.has(ext);
24
+ }
25
+ function replaceExtension(filePath, newExt) {
26
+ const dotIndex = filePath.lastIndexOf('.');
27
+ const base = dotIndex !== -1 ? filePath.slice(0, dotIndex) : filePath;
28
+ return `${base}.${newExt}`;
29
+ }
30
+ /**
31
+ * Recursively walk a directory and collect all supported image files.
32
+ * Returns paths relative to the given root so that directory structure
33
+ * can be replicated in the output.
34
+ */
35
+ function scanDirectory(rootDir) {
36
+ const entries = [];
37
+ function walk(dir) {
38
+ const items = fs_1.default.readdirSync(dir, { withFileTypes: true });
39
+ for (const item of items) {
40
+ const fullPath = path_1.default.join(dir, item.name);
41
+ if (item.isDirectory()) {
42
+ walk(fullPath);
43
+ }
44
+ else if (item.isFile() && isSupportedImage(item.name)) {
45
+ const relativePath = path_1.default.relative(rootDir, fullPath);
46
+ entries.push({ inputPath: fullPath, relativePath });
47
+ }
48
+ }
49
+ }
50
+ walk(rootDir);
51
+ return entries;
52
+ }
53
+ //# sourceMappingURL=scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.js","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":";;;;;AAYA,4CAGC;AAED,sCAGC;AAED,4CAIC;AAOD,sCAoBC;AArDD,4CAAoB;AACpB,gDAAwB;AAGxB,8CAA8C;AAC9C,MAAM,sBAAsB,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;AAElE,8DAA8D;AAC9D,MAAM,sBAAsB,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;AAE3D,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,sBAAsB,EAAE,GAAG,sBAAsB,CAAC,CAAC,CAAC;AAEtF,SAAgB,gBAAgB,CAAC,QAAgB;IAC/C,MAAM,GAAG,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;IACpE,OAAO,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAChC,CAAC;AAED,SAAgB,aAAa,CAAC,QAAgB;IAC5C,MAAM,GAAG,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;IACpE,OAAO,sBAAsB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AACzC,CAAC;AAED,SAAgB,gBAAgB,CAAC,QAAgB,EAAE,MAAc;IAC/D,MAAM,QAAQ,GAAG,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAC3C,MAAM,IAAI,GAAG,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;IACtE,OAAO,GAAG,IAAI,IAAI,MAAM,EAAE,CAAC;AAC7B,CAAC;AAED;;;;GAIG;AACH,SAAgB,aAAa,CAAC,OAAe;IAC3C,MAAM,OAAO,GAAgB,EAAE,CAAC;IAEhC,SAAS,IAAI,CAAC,GAAW;QACvB,MAAM,KAAK,GAAG,YAAE,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QAE3D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;YAE3C,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;gBACvB,IAAI,CAAC,QAAQ,CAAC,CAAC;YACjB,CAAC;iBAAM,IAAI,IAAI,CAAC,MAAM,EAAE,IAAI,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBACxD,MAAM,YAAY,GAAG,cAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;gBACtD,OAAO,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC,CAAC;YACtD,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,CAAC,OAAO,CAAC,CAAC;IACd,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,34 @@
1
+ export type OutputFormat = 'webp' | 'avif' | 'both';
2
+ export interface CliOptions {
3
+ format: OutputFormat;
4
+ quality: number;
5
+ lossless: boolean;
6
+ maxWidth?: number;
7
+ maxHeight?: number;
8
+ out?: string;
9
+ inPlace: boolean;
10
+ }
11
+ export interface FileEntry {
12
+ inputPath: string;
13
+ relativePath: string;
14
+ }
15
+ export interface ConversionResult {
16
+ inputPath: string;
17
+ relativePath: string;
18
+ outputPath: string;
19
+ outputRelativePath: string;
20
+ originalSize: number;
21
+ convertedSize: number;
22
+ success: boolean;
23
+ skipped: boolean;
24
+ error?: string;
25
+ }
26
+ export interface ConversionSummary {
27
+ totalFiles: number;
28
+ successCount: number;
29
+ failureCount: number;
30
+ skippedCount: number;
31
+ totalOriginalBytes: number;
32
+ totalConvertedBytes: number;
33
+ }
34
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEpD,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;CAC7B"}
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,25 @@
1
+ import type { CliOptions, ConversionResult } from './types.js';
2
+ import type { FileEntry } from './types.js';
3
+ /**
4
+ * Determine the final output directory from CLI options.
5
+ * - `--out <path>` wins if provided
6
+ * - `--in-place` uses a temp dir (caller must finalize after success)
7
+ * - Default: `<inputDir>-optimized`
8
+ */
9
+ export declare function resolveOutputDir(inputDir: string, options: CliOptions): string;
10
+ /**
11
+ * Run conversion and write output.
12
+ *
13
+ * For --in-place:
14
+ * 1. Write everything to a temporary directory.
15
+ * 2. Only if ALL conversions succeed, atomically replace the source directory.
16
+ * 3. On any failure, leave the source untouched and clean up the temp dir.
17
+ *
18
+ * For normal output:
19
+ * Write directly to the resolved output directory.
20
+ */
21
+ export declare function writeOutput(inputDir: string, files: FileEntry[], options: CliOptions, onProgress: (result: ConversionResult) => void): Promise<{
22
+ results: ConversionResult[];
23
+ outputDir: string;
24
+ }>;
25
+ //# sourceMappingURL=writer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../src/writer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,MAAM,CAI9E;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,SAAS,EAAE,EAClB,OAAO,EAAE,UAAU,EACnB,UAAU,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,GAC7C,OAAO,CAAC;IAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAS7D"}
package/dist/writer.js ADDED
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveOutputDir = resolveOutputDir;
7
+ exports.writeOutput = writeOutput;
8
+ const path_1 = __importDefault(require("path"));
9
+ const os_1 = __importDefault(require("os"));
10
+ const fs_extra_1 = __importDefault(require("fs-extra"));
11
+ const convert_js_1 = require("./convert.js");
12
+ /**
13
+ * Determine the final output directory from CLI options.
14
+ * - `--out <path>` wins if provided
15
+ * - `--in-place` uses a temp dir (caller must finalize after success)
16
+ * - Default: `<inputDir>-optimized`
17
+ */
18
+ function resolveOutputDir(inputDir, options) {
19
+ if (options.out)
20
+ return options.out;
21
+ if (options.inPlace)
22
+ return ''; // placeholder; writer uses a temp dir internally
23
+ return `${inputDir}-optimized`;
24
+ }
25
+ /**
26
+ * Run conversion and write output.
27
+ *
28
+ * For --in-place:
29
+ * 1. Write everything to a temporary directory.
30
+ * 2. Only if ALL conversions succeed, atomically replace the source directory.
31
+ * 3. On any failure, leave the source untouched and clean up the temp dir.
32
+ *
33
+ * For normal output:
34
+ * Write directly to the resolved output directory.
35
+ */
36
+ async function writeOutput(inputDir, files, options, onProgress) {
37
+ if (options.inPlace) {
38
+ return runInPlace(inputDir, files, options, onProgress);
39
+ }
40
+ const outputDir = resolveOutputDir(inputDir, options);
41
+ await fs_extra_1.default.ensureDir(outputDir);
42
+ const results = await (0, convert_js_1.convertFiles)(files, outputDir, options, onProgress);
43
+ return { results, outputDir };
44
+ }
45
+ async function runInPlace(inputDir, files, options, onProgress) {
46
+ const tempDir = path_1.default.join(os_1.default.tmpdir(), `webpocalypse-${process.pid}-${Date.now()}`);
47
+ try {
48
+ await fs_extra_1.default.ensureDir(tempDir);
49
+ const results = await (0, convert_js_1.convertFiles)(files, tempDir, options, onProgress);
50
+ const hasFailures = results.some((r) => !r.success);
51
+ if (hasFailures) {
52
+ // Leave source untouched; clean up temp
53
+ await fs_extra_1.default.remove(tempDir);
54
+ return { results, outputDir: inputDir };
55
+ }
56
+ // All succeeded — replace source directory atomically
57
+ const backupDir = `${inputDir}.bak-${Date.now()}`;
58
+ await fs_extra_1.default.move(inputDir, backupDir);
59
+ try {
60
+ await fs_extra_1.default.move(tempDir, inputDir);
61
+ await fs_extra_1.default.remove(backupDir);
62
+ }
63
+ catch (moveErr) {
64
+ // Restore from backup on failure
65
+ await fs_extra_1.default.move(backupDir, inputDir);
66
+ await fs_extra_1.default.remove(tempDir).catch(() => undefined);
67
+ throw moveErr;
68
+ }
69
+ return { results, outputDir: inputDir };
70
+ }
71
+ catch (err) {
72
+ await fs_extra_1.default.remove(tempDir).catch(() => undefined);
73
+ throw err;
74
+ }
75
+ }
76
+ //# sourceMappingURL=writer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writer.js","sourceRoot":"","sources":["../src/writer.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "webpocalypse",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool for batch image conversion to WebP/AVIF with quality control and directory structure preservation",
5
+ "keywords": ["image", "conversion", "webp", "avif", "optimization", "cli"],
6
+ "author": "",
7
+ "license": "MIT",
8
+ "main": "dist/index.js",
9
+ "bin": {
10
+ "webpocalypse": "dist/index.js"
11
+ },
12
+ "files": ["dist"],
13
+ "engines": {
14
+ "node": ">=18.0.0"
15
+ },
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "dev": "ts-node src/index.ts",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "dependencies": {
22
+ "chalk": "^5.3.0",
23
+ "commander": "^12.1.0",
24
+ "fs-extra": "^11.2.0",
25
+ "ora": "^8.1.0",
26
+ "p-limit": "^6.1.0",
27
+ "sharp": "^0.33.4"
28
+ },
29
+ "devDependencies": {
30
+ "@types/fs-extra": "^11.0.4",
31
+ "@types/node": "^20.14.0",
32
+ "typescript": "^5.4.5"
33
+ }
34
+ }