lazywebp 1.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Keiver Hernandez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # towebp - Lazy webp converter
2
+
3
+ <p align="center">
4
+ <img src="assets/lazywebp-towebp-start.webp" alt="Lazy Webp — drop zone" width="420" />
5
+ </p>
6
+
7
+ CLI tool and native macOS app to batch convert images to WebP format. Handles single files, multiple files, and entire directories with concurrent processing, atomic writes, and mtime-based skip logic.
8
+
9
+ ## Quick Start
10
+
11
+ ```bash
12
+ npx towebp photo.png
13
+ ```
14
+
15
+ Converts `photo.png` to `photo.webp` in the same directory.
16
+
17
+ ## Install
18
+
19
+ **Global** (adds `towebp` to your PATH):
20
+
21
+ ```bash
22
+ npm install -g towebp
23
+ ```
24
+
25
+ **Run without installing** (via npx):
26
+
27
+ ```bash
28
+ npx towebp photo.png
29
+ ```
30
+
31
+ **From source:**
32
+
33
+ ```bash
34
+ git clone https://github.com/keiver/towebp.git
35
+ cd towebp
36
+ npm install && npm run build && npm link
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ```bash
42
+ # Convert a single file (output next to source)
43
+ towebp photo.png
44
+
45
+ # Convert multiple files at once
46
+ towebp photo.png banner.jpg logo.gif
47
+
48
+ # Convert all images in a directory
49
+ towebp images/
50
+
51
+ # Convert to a separate output directory
52
+ towebp -o output/ images/
53
+
54
+ # Custom quality (1-100, default: 90)
55
+ towebp -q 80 photo.png
56
+
57
+ # Recursive subdirectory processing
58
+ towebp -r images/
59
+
60
+ # Combine flags
61
+ towebp -q 75 -r -o dist/ src/assets/
62
+ ```
63
+
64
+ ## Options
65
+
66
+ | Flag | Description |
67
+ |---|---|
68
+ | `-q, --quality <n>` | WebP quality 1-100 (default: 90) |
69
+ | `-o, --output <dir>` | Output directory (default: next to source) |
70
+ | `-r, --recursive` | Process subdirectories recursively |
71
+ | `-h, --help` | Show help message |
72
+ | `-v, --version` | Show version number |
73
+
74
+ ## Features
75
+
76
+ - Accepts multiple input files and directories in a single command
77
+ - Skips files that haven't changed (compares mtime)
78
+ - Gracefully skips non-image files with a warning
79
+ - Atomic writes via temp file + rename
80
+ - Concurrent processing (up to 4 workers)
81
+ - Color space conversion (display-p3 / RGB to sRGB)
82
+ - Auto-rotation based on EXIF data
83
+ - Recursive subdirectory support with mirrored output structure
84
+
85
+ ## Supported Formats
86
+
87
+ JPG, JPEG, PNG, GIF, BMP, TIFF, WebP
88
+
89
+ ## macOS App — Lazy Webp
90
+
91
+ Native SwiftUI app that wraps the CLI. Drag and drop images or folders to convert them — output is generated next to each source file.
92
+
93
+ <p align="center">
94
+ <img src="assets/lazywebp-towebp-folder.png.webp" alt="Lazy Webp — folder batch conversion" width="400" />
95
+ </p>
96
+
97
+ ### Requirements
98
+
99
+ - macOS 14+
100
+ - Swift 6.2+
101
+ - The `towebp` CLI must be installed and available in your PATH
102
+
103
+ ### Run in development
104
+
105
+ ```bash
106
+ cd app
107
+ swift run
108
+ ```
109
+
110
+ ### Install to /Applications
111
+
112
+ ```bash
113
+ cd app
114
+ ./install-app.sh
115
+ ```
116
+
117
+ This builds a release binary, creates an app bundle at `/Applications/Lazy Webp.app` (`dev.keiver.lazywebp`), and signs it locally.
118
+
119
+ ### App Features
120
+
121
+ - Drag-and-drop files and folders
122
+ - File picker dialog
123
+ - Quality slider (1-100)
124
+ - Per-file size and savings display
125
+ - Live progress tracking with cancel support
126
+ - Always-on-top floating window
127
+ - Menu bar icon with quick access
128
+ - Launch at login option
129
+ - Install to /Applications from the menu bar
130
+
131
+ ## Requirements
132
+
133
+ | Component | Requires |
134
+ |---|---|
135
+ | CLI | Node.js 18+, Sharp |
136
+ | macOS App | macOS 14+, Swift 6.2+ |
137
+
138
+ ## npm Scripts
139
+
140
+ | Script | Description |
141
+ |---|---|
142
+ | `npm run dev` | Run directly from TypeScript source |
143
+ | `npm run build` | Compile to JavaScript in `dist/` |
144
+ | `npm start` | Run compiled build |
145
+ | `npm test` | Run tests |
146
+ | `npm run test:watch` | Run tests in watch mode |
147
+
148
+ ## License
149
+
150
+ MIT
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cli.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/cli.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,98 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+ import fs from "node:fs/promises";
5
+ import path from "node:path";
6
+ import os from "node:os";
7
+ import sharp from "sharp";
8
+ const exec = promisify(execFile);
9
+ // Use tsx to run the TypeScript source directly
10
+ const CLI = path.resolve("src/index.ts");
11
+ const TSX = path.resolve("node_modules/.bin/tsx");
12
+ function tmpDir() {
13
+ return path.join(os.tmpdir(), `towebp-cli-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
14
+ }
15
+ async function createTestPng(filePath) {
16
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
17
+ await sharp({
18
+ create: { width: 10, height: 10, channels: 3, background: { r: 255, g: 0, b: 0 } },
19
+ })
20
+ .png()
21
+ .toFile(filePath);
22
+ }
23
+ async function cleanup(dir) {
24
+ try {
25
+ await fs.rm(dir, { recursive: true, force: true });
26
+ }
27
+ catch {
28
+ // ignore
29
+ }
30
+ }
31
+ describe("CLI", () => {
32
+ let workDir;
33
+ beforeEach(async () => {
34
+ workDir = tmpDir();
35
+ await fs.mkdir(workDir, { recursive: true });
36
+ });
37
+ afterEach(async () => {
38
+ await cleanup(workDir);
39
+ });
40
+ it("prints help with --help", async () => {
41
+ const { stdout } = await exec(TSX, [CLI, "--help"]);
42
+ expect(stdout).toContain("towebp");
43
+ expect(stdout).toContain("Usage:");
44
+ expect(stdout).toContain("--quality");
45
+ });
46
+ it("prints version with --version", async () => {
47
+ const { stdout } = await exec(TSX, [CLI, "--version"]);
48
+ expect(stdout.trim()).toMatch(/^\d+\.\d+\.\d+$/);
49
+ });
50
+ it("exits with error on no arguments", async () => {
51
+ try {
52
+ await exec(TSX, [CLI]);
53
+ expect.fail("should have thrown");
54
+ }
55
+ catch (err) {
56
+ const error = err;
57
+ expect(error.code).toBe(1);
58
+ expect(error.stderr).toContain("no input");
59
+ }
60
+ });
61
+ it("converts a single file via CLI", async () => {
62
+ const inputPath = path.join(workDir, "cli-test.png");
63
+ await createTestPng(inputPath);
64
+ const { stdout } = await exec(TSX, [CLI, inputPath]);
65
+ expect(stdout).toContain("Processed:");
66
+ const stat = await fs.stat(path.join(workDir, "cli-test.webp"));
67
+ expect(stat.size).toBeGreaterThan(0);
68
+ });
69
+ it("converts a directory via CLI", async () => {
70
+ await createTestPng(path.join(workDir, "a.png"));
71
+ await createTestPng(path.join(workDir, "b.png"));
72
+ const { stdout } = await exec(TSX, [CLI, workDir]);
73
+ expect(stdout).toContain("Total files:");
74
+ expect(stdout).toContain("2");
75
+ });
76
+ it("exits with error on invalid input", async () => {
77
+ try {
78
+ await exec(TSX, [CLI, "/nonexistent/path"]);
79
+ expect.fail("should have thrown");
80
+ }
81
+ catch (err) {
82
+ const error = err;
83
+ expect(error.code).toBe(1);
84
+ }
85
+ });
86
+ it("exits with error on unknown flag", async () => {
87
+ try {
88
+ await exec(TSX, [CLI, "--badopt"]);
89
+ expect.fail("should have thrown");
90
+ }
91
+ catch (err) {
92
+ const error = err;
93
+ expect(error.code).toBe(1);
94
+ expect(error.stderr).toContain("unknown option");
95
+ }
96
+ });
97
+ });
98
+ //# sourceMappingURL=cli.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.test.js","sourceRoot":"","sources":["../../src/__tests__/cli.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=converter.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"converter.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/converter.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,188 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { ImageConverter } from "../converter.js";
3
+ import sharp from "sharp";
4
+ import fs from "node:fs/promises";
5
+ import path from "node:path";
6
+ import os from "node:os";
7
+ function tmpDir() {
8
+ return path.join(os.tmpdir(), `towebp-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
9
+ }
10
+ async function createTestImage(filePath, format = "png", width = 10, height = 10) {
11
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
12
+ const background = format === "png" ? { r: 255, g: 0, b: 0 } : { r: 0, g: 255, b: 0 };
13
+ await sharp({
14
+ create: { width, height, channels: 3, background },
15
+ })
16
+ .toFormat(format)
17
+ .toFile(filePath);
18
+ }
19
+ async function cleanup(dir) {
20
+ try {
21
+ await fs.rm(dir, { recursive: true, force: true });
22
+ }
23
+ catch {
24
+ // ignore
25
+ }
26
+ }
27
+ describe("ImageConverter", () => {
28
+ let workDir;
29
+ beforeEach(async () => {
30
+ workDir = tmpDir();
31
+ await fs.mkdir(workDir, { recursive: true });
32
+ });
33
+ afterEach(async () => {
34
+ await cleanup(workDir);
35
+ });
36
+ describe("single file conversion", () => {
37
+ it("converts a PNG to WebP next to the source", async () => {
38
+ const inputPath = path.join(workDir, "test.png");
39
+ await createTestImage(inputPath);
40
+ const converter = new ImageConverter();
41
+ const result = await converter.run(inputPath);
42
+ expect(result.processed).toBe(1);
43
+ expect(result.skipped).toBe(0);
44
+ expect(result.failed.length).toBe(0);
45
+ const outputPath = path.join(workDir, "test.webp");
46
+ const stat = await fs.stat(outputPath);
47
+ expect(stat.size).toBeGreaterThan(0);
48
+ });
49
+ it("converts a JPG to WebP in a separate output directory", async () => {
50
+ const inputPath = path.join(workDir, "photo.jpg");
51
+ const outDir = path.join(workDir, "out");
52
+ await createTestImage(inputPath, "jpeg");
53
+ const converter = new ImageConverter();
54
+ const result = await converter.run(inputPath, outDir);
55
+ expect(result.processed).toBe(1);
56
+ const outputPath = path.join(outDir, "photo.webp");
57
+ const stat = await fs.stat(outputPath);
58
+ expect(stat.size).toBeGreaterThan(0);
59
+ });
60
+ it("skips when source is .webp and output would be same path", async () => {
61
+ const inputPath = path.join(workDir, "already.webp");
62
+ // Create a small webp
63
+ await sharp({
64
+ create: { width: 10, height: 10, channels: 3, background: { r: 0, g: 0, b: 255 } },
65
+ })
66
+ .webp()
67
+ .toFile(inputPath);
68
+ const converter = new ImageConverter();
69
+ const result = await converter.run(inputPath);
70
+ expect(result.skipped).toBe(1);
71
+ expect(result.processed).toBe(0);
72
+ });
73
+ it("skips non-image files", async () => {
74
+ const inputPath = path.join(workDir, "readme.txt");
75
+ await fs.writeFile(inputPath, "hello");
76
+ const converter = new ImageConverter();
77
+ const result = await converter.run(inputPath);
78
+ expect(result.totalFiles).toBe(1);
79
+ expect(result.skipped).toBe(1);
80
+ expect(result.processed).toBe(0);
81
+ });
82
+ });
83
+ describe("directory conversion", () => {
84
+ it("converts all images in a directory (same-dir output)", async () => {
85
+ await createTestImage(path.join(workDir, "a.png"));
86
+ await createTestImage(path.join(workDir, "b.jpg"), "jpeg");
87
+ const converter = new ImageConverter();
88
+ const result = await converter.run(workDir);
89
+ expect(result.totalFiles).toBe(2);
90
+ expect(result.processed).toBe(2);
91
+ const aWebp = await fs.stat(path.join(workDir, "a.webp"));
92
+ const bWebp = await fs.stat(path.join(workDir, "b.webp"));
93
+ expect(aWebp.size).toBeGreaterThan(0);
94
+ expect(bWebp.size).toBeGreaterThan(0);
95
+ });
96
+ it("converts directory to separate output directory", async () => {
97
+ await createTestImage(path.join(workDir, "c.png"));
98
+ const outDir = path.join(workDir, "output");
99
+ const converter = new ImageConverter();
100
+ const result = await converter.run(workDir, outDir);
101
+ expect(result.processed).toBe(1);
102
+ const stat = await fs.stat(path.join(outDir, "c.webp"));
103
+ expect(stat.size).toBeGreaterThan(0);
104
+ });
105
+ it("skips unchanged files on second run", async () => {
106
+ await createTestImage(path.join(workDir, "d.png"));
107
+ const outDir = path.join(workDir, "output");
108
+ const converter1 = new ImageConverter();
109
+ await converter1.run(workDir, outDir);
110
+ const converter2 = new ImageConverter();
111
+ const result = await converter2.run(workDir, outDir);
112
+ expect(result.skipped).toBe(1);
113
+ expect(result.processed).toBe(0);
114
+ });
115
+ it("throws on empty directory", async () => {
116
+ const emptyDir = path.join(workDir, "empty");
117
+ await fs.mkdir(emptyDir, { recursive: true });
118
+ const converter = new ImageConverter();
119
+ await expect(converter.run(emptyDir)).rejects.toThrow("No valid image files found");
120
+ });
121
+ });
122
+ describe("recursive mode", () => {
123
+ it("processes images in subdirectories", async () => {
124
+ await createTestImage(path.join(workDir, "top.png"));
125
+ await createTestImage(path.join(workDir, "sub", "nested.png"));
126
+ const converter = new ImageConverter();
127
+ const result = await converter.run(workDir, undefined, true);
128
+ expect(result.totalFiles).toBe(2);
129
+ expect(result.processed).toBe(2);
130
+ const topWebp = await fs.stat(path.join(workDir, "top.webp"));
131
+ const nestedWebp = await fs.stat(path.join(workDir, "sub", "nested.webp"));
132
+ expect(topWebp.size).toBeGreaterThan(0);
133
+ expect(nestedWebp.size).toBeGreaterThan(0);
134
+ });
135
+ it("mirrors subdirectory structure in separate output", async () => {
136
+ await createTestImage(path.join(workDir, "a", "deep.png"));
137
+ const outDir = path.join(workDir, "output");
138
+ const converter = new ImageConverter();
139
+ const result = await converter.run(workDir, outDir, true);
140
+ expect(result.processed).toBe(1);
141
+ const stat = await fs.stat(path.join(outDir, "a", "deep.webp"));
142
+ expect(stat.size).toBeGreaterThan(0);
143
+ });
144
+ });
145
+ describe("quality option", () => {
146
+ it("uses custom quality", async () => {
147
+ const inputPath = path.join(workDir, "q.png");
148
+ // Use a large noisy image so quality difference is measurable
149
+ const size = 500;
150
+ const channels = 3;
151
+ const noise = Buffer.alloc(size * size * channels);
152
+ for (let i = 0; i < noise.length; i++) {
153
+ noise[i] = Math.floor(Math.random() * 256);
154
+ }
155
+ await fs.mkdir(path.dirname(inputPath), { recursive: true });
156
+ await sharp(noise, { raw: { width: size, height: size, channels } })
157
+ .png()
158
+ .toFile(inputPath);
159
+ const converterHigh = new ImageConverter(100);
160
+ await converterHigh.run(inputPath);
161
+ const highSize = (await fs.stat(path.join(workDir, "q.webp"))).size;
162
+ // Remove output for second run
163
+ await fs.unlink(path.join(workDir, "q.webp"));
164
+ const converterLow = new ImageConverter(1);
165
+ await converterLow.run(inputPath);
166
+ const lowSize = (await fs.stat(path.join(workDir, "q.webp"))).size;
167
+ // Lower quality should produce smaller file with noisy content
168
+ expect(lowSize).toBeLessThan(highSize);
169
+ });
170
+ it("clamps quality to valid range", () => {
171
+ const converterLow = new ImageConverter(0);
172
+ expect(converterLow.config.quality).toBe(1);
173
+ const converterHigh = new ImageConverter(200);
174
+ expect(converterHigh.config.quality).toBe(100);
175
+ });
176
+ });
177
+ describe("error handling", () => {
178
+ it("records failed files without crashing", async () => {
179
+ const inputPath = path.join(workDir, "corrupt.png");
180
+ await fs.writeFile(inputPath, "not a real image");
181
+ const converter = new ImageConverter();
182
+ const result = await converter.run(workDir);
183
+ expect(result.failed.length).toBe(1);
184
+ expect(result.failed[0].file).toBe(path.join(workDir, "corrupt.png"));
185
+ });
186
+ });
187
+ });
188
+ //# sourceMappingURL=converter.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"converter.test.js","sourceRoot":"","sources":["../../src/__tests__/converter.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { ConversionConfig, ConversionStats, ConversionResult, FileConversionResult } from "./types.js";
2
+ export declare class ImageConverter {
3
+ config: ConversionConfig;
4
+ stats: ConversionStats;
5
+ constructor(quality?: number);
6
+ private resetStats;
7
+ run(input: string, outputDir?: string, recursive?: boolean): Promise<ConversionResult>;
8
+ runAll(inputs: string[], outputDir?: string, recursive?: boolean): Promise<ConversionResult>;
9
+ private collectFileTask;
10
+ private collectDirectoryTasks;
11
+ private skipFile;
12
+ private isSameFile;
13
+ private resolveOutputPath;
14
+ private validatePaths;
15
+ private getDirectorySize;
16
+ private shouldProcessImage;
17
+ convertImage(inputPath: string, outputPath: string): Promise<FileConversionResult>;
18
+ private processInBatches;
19
+ private buildResult;
20
+ }
21
+ //# sourceMappingURL=converter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"converter.d.ts","sourceRoot":"","sources":["../src/converter.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,gBAAgB,EAAkB,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAE5H,qBAAa,cAAc;IACzB,MAAM,EAAE,gBAAgB,CAAC;IACzB,KAAK,EAAE,eAAe,CAAC;gBAEX,OAAO,SAAK;IAoBxB,OAAO,CAAC,UAAU;IAaZ,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,SAAS,UAAQ,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAIpF,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,SAAS,UAAQ,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAgChG,OAAO,CAAC,eAAe;YAgBT,qBAAqB;IAkCnC,OAAO,CAAC,QAAQ;IAMhB,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,iBAAiB;YAQX,aAAa;YAkBb,gBAAgB;YAehB,kBAAkB;IAoB1B,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;YAuF1E,gBAAgB;IAwB9B,OAAO,CAAC,WAAW;CAoBpB"}
@@ -0,0 +1,278 @@
1
+ import sharp from "sharp";
2
+ import path from "node:path";
3
+ import fs from "node:fs/promises";
4
+ import fsSync from "node:fs";
5
+ import crypto from "node:crypto";
6
+ import os from "node:os";
7
+ import { formatBytes, formatDuration, getDiskSpace, isImageFile } from "./utils.js";
8
+ export class ImageConverter {
9
+ config;
10
+ stats;
11
+ constructor(quality = 90) {
12
+ this.config = {
13
+ quality: Math.max(1, Math.min(quality, 100)),
14
+ maxConcurrent: Math.max(1, Math.min(os.cpus().length - 1, 4)),
15
+ };
16
+ this.stats = {
17
+ processed: 0,
18
+ skipped: 0,
19
+ failed: [],
20
+ totalFiles: 0,
21
+ totalBytes: 0,
22
+ savedBytes: 0,
23
+ startTime: null,
24
+ endTime: null,
25
+ };
26
+ sharp.cache({ memory: 512 });
27
+ }
28
+ resetStats() {
29
+ this.stats = {
30
+ processed: 0,
31
+ skipped: 0,
32
+ failed: [],
33
+ totalFiles: 0,
34
+ totalBytes: 0,
35
+ savedBytes: 0,
36
+ startTime: null,
37
+ endTime: null,
38
+ };
39
+ }
40
+ async run(input, outputDir, recursive = false) {
41
+ return this.runAll([input], outputDir, recursive);
42
+ }
43
+ async runAll(inputs, outputDir, recursive = false) {
44
+ this.resetStats();
45
+ this.stats.startTime = Date.now();
46
+ if (outputDir) {
47
+ await fs.mkdir(outputDir, { recursive: true });
48
+ }
49
+ // Collect all tasks from files and directories
50
+ const tasks = [];
51
+ for (const input of inputs) {
52
+ const stat = await fs.stat(input);
53
+ if (stat.isFile()) {
54
+ this.collectFileTask(input, outputDir, tasks);
55
+ }
56
+ else if (stat.isDirectory()) {
57
+ await this.collectDirectoryTasks(input, outputDir, recursive, tasks);
58
+ }
59
+ else {
60
+ throw new Error(`Input is neither a file nor a directory: ${input}`);
61
+ }
62
+ }
63
+ if (this.stats.totalFiles === 0) {
64
+ throw new Error("No valid image files found");
65
+ }
66
+ await this.processInBatches(tasks);
67
+ this.stats.endTime = Date.now();
68
+ return this.buildResult();
69
+ }
70
+ collectFileTask(inputPath, outputDir, tasks) {
71
+ if (!isImageFile(inputPath)) {
72
+ this.skipFile(`Skipping: not a supported image file: ${inputPath}`);
73
+ return;
74
+ }
75
+ const outputPath = this.resolveOutputPath(inputPath, outputDir);
76
+ if (this.isSameFile(inputPath, outputPath)) {
77
+ return;
78
+ }
79
+ this.stats.totalFiles++;
80
+ tasks.push({ inputPath, outputPath });
81
+ }
82
+ async collectDirectoryTasks(inputDir, outputDir, recursive, tasks) {
83
+ const sameDir = !outputDir;
84
+ const resolvedOutput = outputDir ?? inputDir;
85
+ if (!sameDir) {
86
+ await this.validatePaths(inputDir, resolvedOutput);
87
+ }
88
+ const entries = recursive
89
+ ? await fs.readdir(inputDir, { recursive: true })
90
+ : await fs.readdir(inputDir);
91
+ const imageFiles = entries.filter((entry) => isImageFile(entry));
92
+ for (const file of imageFiles) {
93
+ const inputPath = path.join(inputDir, file);
94
+ let outputPath;
95
+ if (sameDir) {
96
+ outputPath = path.join(path.dirname(inputPath), `${path.parse(file).name}.webp`);
97
+ }
98
+ else {
99
+ const relDir = path.dirname(file);
100
+ outputPath = path.join(resolvedOutput, relDir, `${path.parse(file).name}.webp`);
101
+ }
102
+ if (this.isSameFile(inputPath, outputPath)) {
103
+ continue;
104
+ }
105
+ this.stats.totalFiles++;
106
+ tasks.push({ inputPath, outputPath });
107
+ }
108
+ }
109
+ skipFile(message) {
110
+ console.warn(message);
111
+ this.stats.totalFiles++;
112
+ this.stats.skipped++;
113
+ }
114
+ isSameFile(inputPath, outputPath) {
115
+ if (path.resolve(inputPath) === path.resolve(outputPath)) {
116
+ this.skipFile(`Skipping: source and output are the same file: ${inputPath}`);
117
+ return true;
118
+ }
119
+ return false;
120
+ }
121
+ resolveOutputPath(inputPath, outputDir) {
122
+ const name = path.parse(inputPath).name;
123
+ if (outputDir) {
124
+ return path.join(outputDir, `${name}.webp`);
125
+ }
126
+ return path.join(path.dirname(inputPath), `${name}.webp`);
127
+ }
128
+ async validatePaths(inputDir, outputDir) {
129
+ const inputStats = await fs.stat(inputDir);
130
+ if (!inputStats.isDirectory()) {
131
+ throw new Error("Input path is not a directory");
132
+ }
133
+ await fs.access(inputDir, fsSync.constants.R_OK);
134
+ await fs.mkdir(outputDir, { recursive: true });
135
+ await fs.access(outputDir, fsSync.constants.W_OK);
136
+ const { available } = await getDiskSpace(outputDir);
137
+ const requiredSpace = await this.getDirectorySize(inputDir);
138
+ if (available < requiredSpace * 1.2) {
139
+ throw new Error("Insufficient disk space");
140
+ }
141
+ }
142
+ async getDirectorySize(dir) {
143
+ const files = await fs.readdir(dir, { recursive: true });
144
+ const sizes = await Promise.all(files.map(async (file) => {
145
+ try {
146
+ const stats = await fs.stat(path.join(dir, file));
147
+ return stats.isFile() ? stats.size : 0;
148
+ }
149
+ catch {
150
+ return 0;
151
+ }
152
+ }));
153
+ return sizes.reduce((acc, size) => acc + size, 0);
154
+ }
155
+ async shouldProcessImage(inputPath, outputPath) {
156
+ try {
157
+ const outputExists = await fs
158
+ .access(outputPath)
159
+ .then(() => true)
160
+ .catch(() => false);
161
+ if (!outputExists)
162
+ return true;
163
+ const [inputStats, outputStats] = await Promise.all([
164
+ fs.stat(inputPath),
165
+ fs.stat(outputPath),
166
+ ]);
167
+ return inputStats.mtime > outputStats.mtime || outputStats.size === 0;
168
+ }
169
+ catch {
170
+ return true;
171
+ }
172
+ }
173
+ async convertImage(inputPath, outputPath) {
174
+ let tempOutput;
175
+ try {
176
+ const needsProcessing = await this.shouldProcessImage(inputPath, outputPath);
177
+ if (!needsProcessing) {
178
+ this.stats.skipped++;
179
+ return { success: true, skipped: true };
180
+ }
181
+ const inputSize = (await fs.stat(inputPath)).size;
182
+ tempOutput = path.join(path.dirname(outputPath), `.towebp-${crypto.randomBytes(8).toString("hex")}.webp`);
183
+ // Ensure output directory exists (for recursive mode with subdirs)
184
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
185
+ const pipeline = sharp(inputPath, {
186
+ failOnError: true,
187
+ limitInputPixels: 268402689, // 16384 x 16384
188
+ sequentialRead: true,
189
+ });
190
+ const metadata = await pipeline.metadata();
191
+ let sharpInstance = pipeline.rotate(); // Auto-rotate based on EXIF
192
+ const space = metadata.space;
193
+ if (space === "rgb" || space === "display-p3") {
194
+ sharpInstance = sharpInstance.toColorspace("srgb");
195
+ }
196
+ await sharpInstance
197
+ .toFormat("webp", {
198
+ quality: this.config.quality,
199
+ effort: 6,
200
+ alphaQuality: 100,
201
+ lossless: false,
202
+ smartSubsample: true,
203
+ nearLossless: false,
204
+ })
205
+ .toFile(tempOutput);
206
+ const tempStats = await fs.stat(tempOutput);
207
+ if (tempStats.size === 0) {
208
+ throw new Error("Generated file is empty");
209
+ }
210
+ // Guard against symlink at output path
211
+ try {
212
+ const outputLstat = await fs.lstat(outputPath);
213
+ if (outputLstat.isSymbolicLink()) {
214
+ throw new Error("Output path is a symbolic link — refusing to overwrite");
215
+ }
216
+ }
217
+ catch (e) {
218
+ if (e.code !== "ENOENT")
219
+ throw e;
220
+ }
221
+ await fs.rename(tempOutput, outputPath);
222
+ this.stats.processed++;
223
+ this.stats.totalBytes += inputSize;
224
+ this.stats.savedBytes += inputSize - tempStats.size;
225
+ return { success: true, skipped: false };
226
+ }
227
+ catch (err) {
228
+ if (tempOutput) {
229
+ try {
230
+ await fs.unlink(tempOutput);
231
+ }
232
+ catch {
233
+ // ignore cleanup errors
234
+ }
235
+ }
236
+ const message = err instanceof Error ? err.message : String(err);
237
+ this.stats.failed.push({
238
+ file: inputPath,
239
+ error: message,
240
+ });
241
+ return { success: false, error: message };
242
+ }
243
+ }
244
+ async processInBatches(tasks) {
245
+ const results = [];
246
+ for (let i = 0; i < tasks.length; i += this.config.maxConcurrent) {
247
+ const batch = tasks.slice(i, i + this.config.maxConcurrent);
248
+ const batchResults = await Promise.all(batch.map((task) => this.convertImage(task.inputPath, task.outputPath)));
249
+ results.push(...batchResults);
250
+ const progress = (((i + batch.length) / tasks.length) * 100).toFixed(1);
251
+ const savedMB = (this.stats.savedBytes / 1024 / 1024).toFixed(2);
252
+ process.stdout.write(`\rProgress: ${progress}% (${i + batch.length}/${tasks.length}) | Saved: ${savedMB}MB`);
253
+ }
254
+ if (tasks.length > 0) {
255
+ process.stdout.write("\n");
256
+ }
257
+ return results;
258
+ }
259
+ buildResult() {
260
+ const duration = this.stats.startTime && this.stats.endTime
261
+ ? formatDuration(this.stats.endTime - this.stats.startTime)
262
+ : "0s";
263
+ const ratio = this.stats.totalBytes > 0
264
+ ? ((this.stats.savedBytes / this.stats.totalBytes) * 100).toFixed(2) + "%"
265
+ : "0%";
266
+ return {
267
+ totalFiles: this.stats.totalFiles,
268
+ processed: this.stats.processed,
269
+ skipped: this.stats.skipped,
270
+ failed: this.stats.failed,
271
+ duration,
272
+ totalSize: formatBytes(this.stats.totalBytes),
273
+ savedSize: formatBytes(this.stats.savedBytes),
274
+ compressionRatio: ratio,
275
+ };
276
+ }
277
+ }
278
+ //# sourceMappingURL=converter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"converter.js","sourceRoot":"","sources":["../src/converter.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,117 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { ImageConverter } from "./converter.js";
4
+ const require = createRequire(import.meta.url);
5
+ const { version: VERSION } = require("../package.json");
6
+ const HELP = `
7
+ towebp v${VERSION} — Convert images to WebP format
8
+
9
+ Usage:
10
+ towebp <file...> Convert file(s), output next to source
11
+ towebp <dir> Convert all images in dir, output next to sources
12
+ towebp -o <outputDir> <input...> Convert to separate output directory
13
+ towebp -q 80 <file> Custom quality (default: 90)
14
+ towebp -r <dir> Recursive subdirectory processing
15
+
16
+ Options:
17
+ -q, --quality <n> WebP quality 1-100 (default: 90)
18
+ -o, --output <dir> Output directory (default: next to source)
19
+ -r, --recursive Process subdirectories recursively
20
+ -h, --help Show this help message
21
+ -v, --version Show version number
22
+
23
+ Supported formats: jpg, jpeg, png, gif, bmp, tiff, webp
24
+ `.trim();
25
+ function parseArgs(argv) {
26
+ const args = argv.slice(2);
27
+ const result = {
28
+ inputs: [],
29
+ quality: 90,
30
+ recursive: false,
31
+ help: false,
32
+ version: false,
33
+ };
34
+ for (let i = 0; i < args.length; i++) {
35
+ const arg = args[i];
36
+ if (arg === "-h" || arg === "--help") {
37
+ result.help = true;
38
+ return result;
39
+ }
40
+ if (arg === "-v" || arg === "--version") {
41
+ result.version = true;
42
+ return result;
43
+ }
44
+ if (arg === "-r" || arg === "--recursive") {
45
+ result.recursive = true;
46
+ continue;
47
+ }
48
+ if (arg === "-q" || arg === "--quality") {
49
+ const next = args[++i];
50
+ if (next === undefined) {
51
+ console.error("Error: --quality requires a numeric argument");
52
+ process.exit(1);
53
+ }
54
+ const val = parseInt(next, 10);
55
+ if (isNaN(val)) {
56
+ console.error(`Error: invalid quality value: ${next}`);
57
+ process.exit(1);
58
+ }
59
+ // Clamp to 1-100
60
+ result.quality = Math.max(1, Math.min(val, 100));
61
+ continue;
62
+ }
63
+ if (arg === "-o" || arg === "--output") {
64
+ const next = args[++i];
65
+ if (next === undefined) {
66
+ console.error("Error: --output requires a directory argument");
67
+ process.exit(1);
68
+ }
69
+ result.outputDir = next;
70
+ continue;
71
+ }
72
+ if (arg.startsWith("-")) {
73
+ console.error(`Error: unknown option: ${arg}`);
74
+ console.error("Run towebp --help for usage");
75
+ process.exit(1);
76
+ }
77
+ result.inputs.push(arg);
78
+ }
79
+ return result;
80
+ }
81
+ async function main() {
82
+ const parsed = parseArgs(process.argv);
83
+ if (parsed.help) {
84
+ console.log(HELP);
85
+ return;
86
+ }
87
+ if (parsed.version) {
88
+ console.log(VERSION);
89
+ return;
90
+ }
91
+ if (parsed.inputs.length === 0) {
92
+ console.error("Error: no input file or directory specified");
93
+ console.error("Run towebp --help for usage");
94
+ process.exit(1);
95
+ }
96
+ const converter = new ImageConverter(parsed.quality);
97
+ const results = await converter.runAll(parsed.inputs, parsed.outputDir, parsed.recursive);
98
+ console.log("\nConversion completed:");
99
+ console.log(` Total files: ${results.totalFiles}`);
100
+ console.log(` Processed: ${results.processed}`);
101
+ console.log(` Skipped: ${results.skipped}`);
102
+ console.log(` Failed: ${results.failed.length}`);
103
+ console.log(` Duration: ${results.duration}`);
104
+ console.log(` Total size: ${results.totalSize}`);
105
+ console.log(` Saved: ${results.savedSize}`);
106
+ console.log(` Compression: ${results.compressionRatio}`);
107
+ if (results.failed.length > 0) {
108
+ console.log("\nFailed conversions:");
109
+ results.failed.forEach((f) => console.log(` - ${f.file}: ${f.error}`));
110
+ process.exit(1);
111
+ }
112
+ }
113
+ main().catch((err) => {
114
+ console.error("Error:", err instanceof Error ? err.message : String(err));
115
+ process.exit(1);
116
+ });
117
+ //# 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,46 @@
1
+ export interface ConversionConfig {
2
+ quality: number;
3
+ maxConcurrent: number;
4
+ }
5
+ export interface ConversionStats {
6
+ processed: number;
7
+ skipped: number;
8
+ failed: FailedFile[];
9
+ totalFiles: number;
10
+ totalBytes: number;
11
+ savedBytes: number;
12
+ startTime: number | null;
13
+ endTime: number | null;
14
+ }
15
+ export interface ConversionResult {
16
+ totalFiles: number;
17
+ processed: number;
18
+ skipped: number;
19
+ failed: FailedFile[];
20
+ duration: string;
21
+ totalSize: string;
22
+ savedSize: string;
23
+ compressionRatio: string;
24
+ }
25
+ export interface FailedFile {
26
+ file: string;
27
+ error: string;
28
+ }
29
+ export interface FileConversionResult {
30
+ success: boolean;
31
+ skipped?: boolean;
32
+ error?: string;
33
+ }
34
+ export interface ConversionTask {
35
+ inputPath: string;
36
+ outputPath: string;
37
+ }
38
+ export interface ParsedArgs {
39
+ inputs: string[];
40
+ outputDir?: string;
41
+ quality: number;
42
+ recursive: boolean;
43
+ help: boolean;
44
+ version: boolean;
45
+ }
46
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,7 @@
1
+ export declare function isImageFile(filePath: string): boolean;
2
+ export declare function formatBytes(bytes: number): string;
3
+ export declare function formatDuration(ms: number): string;
4
+ export declare function getDiskSpace(dir: string): Promise<{
5
+ available: number;
6
+ }>;
7
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAKA,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGrD;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAWjD;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAIjD;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAU9E"}
package/dist/utils.js ADDED
@@ -0,0 +1,35 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs/promises";
3
+ const INPUT_FORMATS = ["jpg", "jpeg", "png", "gif", "bmp", "tiff", "webp"];
4
+ export function isImageFile(filePath) {
5
+ const ext = path.extname(filePath).toLowerCase().slice(1);
6
+ return INPUT_FORMATS.includes(ext);
7
+ }
8
+ export function formatBytes(bytes) {
9
+ const units = ["B", "KB", "MB", "GB"];
10
+ let size = bytes;
11
+ let unit = 0;
12
+ while (size >= 1024 && unit < units.length - 1) {
13
+ size /= 1024;
14
+ unit++;
15
+ }
16
+ return `${size.toFixed(2)} ${units[unit]}`;
17
+ }
18
+ export function formatDuration(ms) {
19
+ const seconds = Math.floor(ms / 1000);
20
+ const minutes = Math.floor(seconds / 60);
21
+ return minutes > 0 ? `${minutes}m ${seconds % 60}s` : `${seconds}s`;
22
+ }
23
+ export async function getDiskSpace(dir) {
24
+ const target = process.platform === "win32" ? path.parse(dir).root : dir;
25
+ try {
26
+ const stats = await fs.statfs(target);
27
+ return { available: stats.bavail * stats.bsize };
28
+ }
29
+ catch (err) {
30
+ const message = err instanceof Error ? err.message : String(err);
31
+ console.warn(`Warning: could not check disk space for ${dir}: ${message}`);
32
+ return { available: Infinity };
33
+ }
34
+ }
35
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAElC,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAE3E,MAAM,UAAU,WAAW,CAAC,QAAgB;IAC1C,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC1D,OAAO,aAAa,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,MAAM,KAAK,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IACtC,IAAI,IAAI,GAAG,KAAK,CAAC;IACjB,IAAI,IAAI,GAAG,CAAC,CAAC;IAEb,OAAO,IAAI,IAAI,IAAI,IAAI,IAAI,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC/C,IAAI,IAAI,IAAI,CAAC;QACb,IAAI,EAAE,CAAC;IACT,CAAC;IAED,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,EAAU;IACvC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;IACtC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC;IACzC,OAAO,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,KAAK,OAAO,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,OAAO,GAAG,CAAC;AACtE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,GAAW;IAC5C,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC;IACzE,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACtC,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC;IACnD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,OAAO,CAAC,IAAI,CAAC,2CAA2C,GAAG,KAAK,OAAO,EAAE,CAAC,CAAC;QAC3E,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;IACjC,CAAC;AACH,CAAC"}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "lazywebp",
3
+ "version": "1.3.0",
4
+ "description": "CLI tool to batch convert images to WebP format using Sharp",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "bin": {
15
+ "towebp": "dist/index.js"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "LICENSE",
20
+ "README.md"
21
+ ],
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "scripts": {
26
+ "build": "tsc",
27
+ "dev": "tsx src/index.ts",
28
+ "start": "node dist/index.js",
29
+ "test": "vitest run",
30
+ "test:watch": "vitest",
31
+ "prepublishOnly": "npm run build"
32
+ },
33
+ "keywords": [
34
+ "webp",
35
+ "image",
36
+ "converter",
37
+ "sharp",
38
+ "batch",
39
+ "cli",
40
+ "image-processing"
41
+ ],
42
+ "author": "Keiver Hernandez (https://keiver.dev)",
43
+ "license": "MIT",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/keiver/towebp.git"
47
+ },
48
+ "bugs": {
49
+ "url": "https://github.com/keiver/towebp/issues"
50
+ },
51
+ "homepage": "https://keiver.dev/towebp",
52
+ "dependencies": {
53
+ "sharp": "^0.33.5"
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "^22.10.0",
57
+ "tsx": "^4.19.0",
58
+ "typescript": "^5.7.0",
59
+ "vitest": "^4.0.18"
60
+ }
61
+ }