@mantiqh/image-optimizer 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/dist/index.js ADDED
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import fs from "fs/promises";
6
+ import path from "path";
7
+ import JSZip from "jszip";
8
+ import sharp from "sharp";
9
+ import chalk from "chalk";
10
+ import ora from "ora";
11
+ var program = new Command();
12
+ program.name("image-optimizer").description("Optimize images inside a zip file").version("1.0.0").requiredOption("-s, --source <path>", "Path to the input zip file").option("-o, --output <path>", "Path to the output zip file").option("-q, --quality <number>", "Quality of compression (1-100)", "80").option("-w, --width <number>", "Max width to resize images to", "1600").parse(process.argv);
13
+ var options = program.opts();
14
+ async function main() {
15
+ const spinner = ora("Starting optimization...").start();
16
+ try {
17
+ const sourcePath = path.resolve(process.cwd(), options.source);
18
+ let outputPath;
19
+ if (options.output) {
20
+ outputPath = path.resolve(process.cwd(), options.output);
21
+ } else {
22
+ const dir = path.dirname(sourcePath);
23
+ const ext = path.extname(sourcePath);
24
+ const name = path.basename(sourcePath, ext);
25
+ outputPath = path.join(dir, `${name}-optimized${ext}`);
26
+ }
27
+ if (!await fileExists(sourcePath)) {
28
+ spinner.fail(`Source file not found: ${sourcePath}`);
29
+ process.exit(1);
30
+ }
31
+ spinner.text = "Reading zip file...";
32
+ const zipData = await fs.readFile(sourcePath);
33
+ const zip = await JSZip.loadAsync(zipData);
34
+ const newZip = new JSZip();
35
+ const quality = parseInt(options.quality);
36
+ const maxWidth = parseInt(options.width);
37
+ const fileNames = Object.keys(zip.files);
38
+ let processedCount = 0;
39
+ let savedBytes = 0;
40
+ spinner.text = `Found ${fileNames.length} files. Processing...`;
41
+ for (const fileName of fileNames) {
42
+ const file = zip.files[fileName];
43
+ if (file.dir) {
44
+ newZip.folder(fileName);
45
+ continue;
46
+ }
47
+ const content = await file.async("nodebuffer");
48
+ const ext = path.extname(fileName).toLowerCase();
49
+ if ([".jpg", ".jpeg", ".png", ".webp"].includes(ext)) {
50
+ try {
51
+ spinner.text = `Optimizing: ${fileName}`;
52
+ let pipeline = sharp(content);
53
+ const metadata = await pipeline.metadata();
54
+ if (metadata.width && metadata.width > maxWidth) {
55
+ pipeline = pipeline.resize({ width: maxWidth });
56
+ }
57
+ if (ext === ".png") {
58
+ pipeline = pipeline.png({ quality, compressionLevel: 9 });
59
+ } else if (ext === ".webp") {
60
+ pipeline = pipeline.webp({ quality });
61
+ } else {
62
+ pipeline = pipeline.jpeg({ quality, mozjpeg: true });
63
+ }
64
+ const optimizedBuffer = await pipeline.toBuffer();
65
+ const diff = content.length - optimizedBuffer.length;
66
+ if (diff > 0) {
67
+ savedBytes += diff;
68
+ newZip.file(fileName, optimizedBuffer);
69
+ } else {
70
+ newZip.file(fileName, content);
71
+ }
72
+ processedCount++;
73
+ } catch (err) {
74
+ newZip.file(fileName, content);
75
+ }
76
+ } else {
77
+ newZip.file(fileName, content);
78
+ }
79
+ }
80
+ spinner.text = "Generating output zip...";
81
+ const outputBuffer = await newZip.generateAsync({
82
+ type: "nodebuffer",
83
+ compression: "DEFLATE",
84
+ compressionOptions: { level: 6 }
85
+ });
86
+ await fs.writeFile(outputPath, outputBuffer);
87
+ spinner.succeed(chalk.green("Optimization Complete!"));
88
+ console.log(`
89
+ \u{1F4C1} Output: ${chalk.cyan(outputPath)}`);
90
+ console.log(`\u{1F5BC}\uFE0F Images Processed: ${processedCount}`);
91
+ console.log(
92
+ `\u{1F4BE} Space Saved: ${chalk.bold(
93
+ (savedBytes / 1024 / 1024).toFixed(2)
94
+ )} MB
95
+ `
96
+ );
97
+ } catch (error) {
98
+ spinner.fail("Error occurred");
99
+ console.error(chalk.red(error.message));
100
+ process.exit(1);
101
+ }
102
+ }
103
+ async function fileExists(path2) {
104
+ try {
105
+ await fs.access(path2);
106
+ return true;
107
+ } catch {
108
+ return false;
109
+ }
110
+ }
111
+ main();
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@mantiqh/image-optimizer",
3
+ "version": "1.0.0",
4
+ "description": "CLI to optimize images inside a zip file",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "image-optimizer": "./dist/index.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "build": "tsup src/index.ts --format esm --clean",
12
+ "dev": "tsup src/index.ts --format esm --watch --onSuccess \"node dist/index.js\"",
13
+ "start": "node dist/index.js",
14
+ "prepublishOnly": "pnpm build"
15
+ },
16
+ "keywords": [],
17
+ "author": "",
18
+ "license": "ISC",
19
+ "packageManager": "pnpm@10.12.3",
20
+ "dependencies": {
21
+ "chalk": "^5.6.2",
22
+ "commander": "^14.0.2",
23
+ "jszip": "^3.10.1",
24
+ "ora": "^9.0.0",
25
+ "sharp": "^0.34.5"
26
+ },
27
+ "devDependencies": {
28
+ "@types/jszip": "^3.4.1",
29
+ "@types/node": "^25.0.3",
30
+ "@types/sharp": "^0.32.0",
31
+ "tsup": "^8.5.1",
32
+ "typescript": "^5.9.3"
33
+ }
34
+ }
package/src/index.ts ADDED
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import fs from "fs/promises";
4
+ import path from "path";
5
+ import JSZip from "jszip";
6
+ import sharp from "sharp";
7
+ import chalk from "chalk";
8
+ import ora from "ora";
9
+
10
+ const program = new Command();
11
+
12
+ program
13
+ .name("image-optimizer")
14
+ .description("Optimize images inside a zip file")
15
+ .version("1.0.0")
16
+ .requiredOption("-s, --source <path>", "Path to the input zip file")
17
+ .option("-o, --output <path>", "Path to the output zip file")
18
+ .option("-q, --quality <number>", "Quality of compression (1-100)", "80")
19
+ .option("-w, --width <number>", "Max width to resize images to", "1600")
20
+ .parse(process.argv);
21
+
22
+ const options = program.opts();
23
+
24
+ async function main() {
25
+ const spinner = ora("Starting optimization...").start();
26
+
27
+ try {
28
+ // 1. Resolve Paths
29
+ const sourcePath = path.resolve(process.cwd(), options.source);
30
+
31
+ // Default output name if not provided: input-optimized.zip
32
+ let outputPath: string;
33
+ if (options.output) {
34
+ outputPath = path.resolve(process.cwd(), options.output);
35
+ } else {
36
+ const dir = path.dirname(sourcePath);
37
+ const ext = path.extname(sourcePath);
38
+ const name = path.basename(sourcePath, ext);
39
+ outputPath = path.join(dir, `${name}-optimized${ext}`);
40
+ }
41
+
42
+ if (!(await fileExists(sourcePath))) {
43
+ spinner.fail(`Source file not found: ${sourcePath}`);
44
+ process.exit(1);
45
+ }
46
+
47
+ // 2. Read Zip
48
+ spinner.text = "Reading zip file...";
49
+ const zipData = await fs.readFile(sourcePath);
50
+ const zip = await JSZip.loadAsync(zipData);
51
+
52
+ const newZip = new JSZip();
53
+ const quality = parseInt(options.quality);
54
+ const maxWidth = parseInt(options.width);
55
+
56
+ // 3. Process Entries
57
+ const fileNames = Object.keys(zip.files);
58
+ let processedCount = 0;
59
+ let savedBytes = 0;
60
+
61
+ spinner.text = `Found ${fileNames.length} files. Processing...`;
62
+
63
+ // Process in parallel (batches) or sequence. Sequence is safer for memory.
64
+ for (const fileName of fileNames) {
65
+ const file = zip.files[fileName];
66
+
67
+ if (file.dir) {
68
+ newZip.folder(fileName);
69
+ continue;
70
+ }
71
+
72
+ const content = await file.async("nodebuffer");
73
+ const ext = path.extname(fileName).toLowerCase();
74
+
75
+ // Check if it's an image we can optimize
76
+ if ([".jpg", ".jpeg", ".png", ".webp"].includes(ext)) {
77
+ try {
78
+ spinner.text = `Optimizing: ${fileName}`;
79
+
80
+ // Sharp Optimization Pipeline
81
+ let pipeline = sharp(content);
82
+ const metadata = await pipeline.metadata();
83
+
84
+ // Resize if too big
85
+ if (metadata.width && metadata.width > maxWidth) {
86
+ pipeline = pipeline.resize({ width: maxWidth });
87
+ }
88
+
89
+ // Compress based on format
90
+ if (ext === ".png") {
91
+ pipeline = pipeline.png({ quality: quality, compressionLevel: 9 });
92
+ } else if (ext === ".webp") {
93
+ pipeline = pipeline.webp({ quality: quality });
94
+ } else {
95
+ // Default to jpeg/mozjpeg
96
+ pipeline = pipeline.jpeg({ quality: quality, mozjpeg: true });
97
+ }
98
+
99
+ const optimizedBuffer = await pipeline.toBuffer();
100
+
101
+ // Calculate savings
102
+ const diff = content.length - optimizedBuffer.length;
103
+ if (diff > 0) {
104
+ savedBytes += diff;
105
+ newZip.file(fileName, optimizedBuffer);
106
+ } else {
107
+ // If optimization made it bigger (rare), keep original
108
+ newZip.file(fileName, content);
109
+ }
110
+ processedCount++;
111
+ } catch (err) {
112
+ // If sharp fails (corrupt image?), keep original
113
+ newZip.file(fileName, content);
114
+ }
115
+ } else {
116
+ // Non-image files: just copy them
117
+ newZip.file(fileName, content);
118
+ }
119
+ }
120
+
121
+ // 4. Write Output Zip
122
+ spinner.text = "Generating output zip...";
123
+
124
+ // Generate zip as nodebuffer (works better for CLI files)
125
+ const outputBuffer = await newZip.generateAsync({
126
+ type: "nodebuffer",
127
+ compression: "DEFLATE",
128
+ compressionOptions: { level: 6 },
129
+ });
130
+
131
+ await fs.writeFile(outputPath, outputBuffer);
132
+
133
+ spinner.succeed(chalk.green("Optimization Complete!"));
134
+ console.log(`\nšŸ“ Output: ${chalk.cyan(outputPath)}`);
135
+ console.log(`šŸ–¼ļø Images Processed: ${processedCount}`);
136
+ console.log(
137
+ `šŸ’¾ Space Saved: ${chalk.bold(
138
+ (savedBytes / 1024 / 1024).toFixed(2)
139
+ )} MB\n`
140
+ );
141
+ } catch (error: any) {
142
+ spinner.fail("Error occurred");
143
+ console.error(chalk.red(error.message));
144
+ process.exit(1);
145
+ }
146
+ }
147
+
148
+ // Helper to check file existence
149
+ async function fileExists(path: string) {
150
+ try {
151
+ await fs.access(path);
152
+ return true;
153
+ } catch {
154
+ return false;
155
+ }
156
+ }
157
+
158
+ main();
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "esModuleInterop": true,
8
+ "forceConsistentCasingInFileNames": true,
9
+ "strict": true,
10
+ "skipLibCheck": true
11
+ },
12
+ "include": ["src/**/*"]
13
+ }