@mantiqh/image-optimizer 1.1.1 → 1.2.2

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 CHANGED
@@ -9,7 +9,9 @@ Built for developers to quickly reduce asset sizes before deployment without com
9
9
 
10
10
  - **Universal Input:** Works on `.zip` files, local folders, or single images.
11
11
  - **Recursive:** Process entire directory trees; copies non-image files (CSS, JS) unchanged.
12
- - **Expanded Support:** Optimizes `JPG`, `PNG`, `WebP`, `AVIF`, `GIF`, `TIFF`, and `SVG`.
12
+ - **Expanded Support:** Optimizes `JPG`, `PNG`, `WebP`, `AVIF`, `GIF`, `TIFF`. SVGs are skipped (copied as-is).
13
+ - **Fast:** Parallel processing — optimizes up to 5 images concurrently per directory.
14
+ - **Progress Tracking:** Shows `[N/total]` progress while optimizing.
13
15
  - **Smart Output:** Creates optimized versions _next to_ your source files by default.
14
16
  - **Safe:** If an optimized image is larger than the original, it keeps the original.
15
17
 
package/dist/index.js CHANGED
@@ -1,13 +1,57 @@
1
1
  #!/usr/bin/env node
2
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";
3
+ // src/optimizer.ts
8
4
  import sharp from "sharp";
5
+ async function optimizeBuffer(buffer, ext, config) {
6
+ const extension = ext.toLowerCase();
7
+ try {
8
+ let pipeline = sharp(buffer, { animated: true });
9
+ const metadata = await pipeline.metadata();
10
+ if (metadata.width && metadata.width > config.width) {
11
+ pipeline = pipeline.resize({ width: config.width });
12
+ }
13
+ switch (extension) {
14
+ case ".svg":
15
+ return buffer;
16
+ case ".jpeg":
17
+ case ".jpg":
18
+ pipeline = pipeline.jpeg({ quality: config.quality, mozjpeg: true });
19
+ break;
20
+ case ".png":
21
+ pipeline = pipeline.png({
22
+ quality: config.quality,
23
+ compressionLevel: 9,
24
+ palette: true
25
+ });
26
+ break;
27
+ case ".webp":
28
+ pipeline = pipeline.webp({ quality: config.quality });
29
+ break;
30
+ case ".gif":
31
+ pipeline = pipeline.gif({ colors: 128 });
32
+ break;
33
+ case ".avif":
34
+ pipeline = pipeline.avif({ quality: config.quality });
35
+ break;
36
+ case ".tiff":
37
+ pipeline = pipeline.tiff({ quality: config.quality });
38
+ break;
39
+ }
40
+ const outputBuffer = await pipeline.toBuffer();
41
+ if (outputBuffer.length >= buffer.length) {
42
+ return buffer;
43
+ }
44
+ return outputBuffer;
45
+ } catch {
46
+ return buffer;
47
+ }
48
+ }
49
+
50
+ // src/helpers.ts
51
+ import path from "path";
9
52
  import chalk from "chalk";
10
- import ora from "ora";
53
+
54
+ // src/constants.ts
11
55
  var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
12
56
  ".jpg",
13
57
  ".jpeg",
@@ -18,111 +62,93 @@ var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
18
62
  ".tiff",
19
63
  ".svg"
20
64
  ]);
21
- var program = new Command();
22
- program.name("image-optimizer").description(
23
- chalk.cyan(
24
- "\u{1F680} Universal CLI to optimize images (File, Folder, or Zip). Supports JPG, PNG, WebP, AVIF, GIF, TIFF, SVG."
25
- )
26
- ).version("1.1.1").requiredOption(
27
- "-s, --source <path>",
28
- "Path to the input file, folder, or zip"
29
- ).option(
30
- "-o, --output <path>",
31
- "Path to the output (default: source location + -optimized)"
32
- ).option("-q, --quality <number>", "Quality of compression (1-100)", "80").option("-w, --width <number>", "Max width to resize images to", "1600").parse(process.argv);
33
- var options = program.opts();
34
- async function main() {
35
- const spinner = ora("Analyzing source...").start();
36
- try {
37
- const sourcePath = path.resolve(process.cwd(), options.source);
38
- try {
39
- await fs.access(sourcePath);
40
- } catch {
41
- spinner.fail(`Source not found: ${sourcePath}`);
42
- process.exit(1);
43
- }
44
- const stats = await fs.stat(sourcePath);
45
- const config = {
46
- quality: parseInt(options.quality),
47
- width: parseInt(options.width),
48
- spinner
49
- };
50
- if (stats.isDirectory()) {
51
- const outputPath = determineOutputPath(
52
- sourcePath,
53
- options.output,
54
- "-optimized"
55
- );
56
- spinner.text = "Processing Directory...";
57
- await processDirectory(sourcePath, outputPath, config);
58
- } else if (sourcePath.endsWith(".zip")) {
59
- const outputPath = determineOutputPath(
60
- sourcePath,
61
- options.output,
62
- "-optimized.zip"
63
- );
64
- spinner.text = "Processing Zip...";
65
- await processZip(sourcePath, outputPath, config);
66
- } else if (isSupportedImage(sourcePath)) {
67
- const ext = path.extname(sourcePath);
68
- const outputPath = determineOutputPath(
69
- sourcePath,
70
- options.output,
71
- `-optimized${ext}`
72
- );
73
- spinner.text = "Processing Single File...";
74
- await processSingleFile(sourcePath, outputPath, config);
75
- } else {
76
- spinner.fail(
77
- "Unsupported file type. Please provide a Folder, Zip, or supported Image."
78
- );
79
- process.exit(1);
80
- }
81
- spinner.succeed(chalk.green("Optimization Complete!"));
82
- } catch (error) {
83
- spinner.fail("Error occurred");
84
- console.error(chalk.red(error.message));
85
- process.exit(1);
65
+
66
+ // src/helpers.ts
67
+ function logOutputPath(outputPath) {
68
+ console.log(`
69
+ \u{1F4C1} Output: ${chalk.cyan(outputPath)}`);
70
+ }
71
+ function isSupportedImage(filePath) {
72
+ const ext = path.extname(filePath).toLowerCase();
73
+ return SUPPORTED_EXTENSIONS.has(ext);
74
+ }
75
+ function determineOutputPath(source, userOutput, suffix) {
76
+ if (userOutput) {
77
+ return path.resolve(process.cwd(), userOutput);
86
78
  }
79
+ const dir = path.dirname(source);
80
+ const ext = path.extname(source);
81
+ const name = path.basename(source, ext);
82
+ return path.join(dir, `${name}${suffix}`);
87
83
  }
84
+
85
+ // src/processors.ts
86
+ import fs from "fs/promises";
87
+ import path2 from "path";
88
+ import JSZip from "jszip";
89
+ import chalk2 from "chalk";
88
90
  async function processDirectory(source, destination, config) {
89
91
  if (source === destination) {
90
92
  destination += "-1";
91
93
  }
92
94
  await fs.mkdir(destination, { recursive: true });
93
95
  const entries = await fs.readdir(source, { withFileTypes: true });
94
- let processedCount = 0;
95
- for (const entry of entries) {
96
- const srcPath = path.join(source, entry.name);
97
- const destPath = path.join(destination, entry.name);
98
- if (entry.isDirectory()) {
99
- await processDirectory(srcPath, destPath, config);
100
- } else if (entry.isFile()) {
101
- if (isSupportedImage(entry.name)) {
102
- config.spinner.text = `Optimizing: ${entry.name}`;
103
- const buffer = await fs.readFile(srcPath);
104
- const optimizedBuffer = await optimizeBuffer(
105
- buffer,
106
- path.extname(entry.name),
107
- config
108
- );
109
- await fs.writeFile(destPath, optimizedBuffer);
110
- processedCount++;
111
- } else {
112
- await fs.copyFile(srcPath, destPath);
113
- }
114
- }
96
+ const dirEntries = entries.filter((e) => e.isDirectory());
97
+ const imageEntries = entries.filter(
98
+ (e) => e.isFile() && isSupportedImage(e.name) && path2.extname(e.name).toLowerCase() !== ".svg"
99
+ );
100
+ const nonImageEntries = entries.filter(
101
+ (e) => e.isFile() && (!isSupportedImage(e.name) || path2.extname(e.name).toLowerCase() === ".svg")
102
+ );
103
+ for (const entry of dirEntries) {
104
+ await processDirectory(
105
+ path2.join(source, entry.name),
106
+ path2.join(destination, entry.name),
107
+ config
108
+ );
115
109
  }
116
- if (processedCount > 0) {
110
+ let processedCount = 0;
111
+ const batchSize = 5;
112
+ const totalImages = imageEntries.length;
113
+ for (let i = 0; i < imageEntries.length; i += batchSize) {
114
+ const batch = imageEntries.slice(i, Math.min(i + batchSize, imageEntries.length));
115
+ const results = await Promise.all(
116
+ batch.map(async (entry, j) => {
117
+ const srcPath = path2.join(source, entry.name);
118
+ const destPath = path2.join(destination, entry.name);
119
+ const num = i + j + 1;
120
+ config.spinner.text = `Optimizing [${num}/${totalImages}]: ${entry.name}`;
121
+ try {
122
+ const buffer = await fs.readFile(srcPath);
123
+ const optimizedBuffer = await optimizeBuffer(buffer, path2.extname(entry.name), config);
124
+ await fs.writeFile(destPath, optimizedBuffer);
125
+ return 1;
126
+ } catch (error) {
127
+ console.error(chalk2.red(`Failed to optimize ${entry.name}: ${error.message}`));
128
+ await fs.copyFile(srcPath, destPath);
129
+ return 1;
130
+ }
131
+ })
132
+ );
133
+ processedCount += results.length;
117
134
  }
118
- console.log(`
119
- \u{1F4C1} Output: ${chalk.cyan(destination)}`);
135
+ await Promise.all(
136
+ nonImageEntries.map((entry) => {
137
+ return fs.copyFile(path2.join(source, entry.name), path2.join(destination, entry.name));
138
+ })
139
+ );
140
+ return processedCount;
120
141
  }
121
142
  async function processZip(source, destination, config) {
122
143
  const zipData = await fs.readFile(source);
123
144
  const zip = await JSZip.loadAsync(zipData);
124
145
  const newZip = new JSZip();
125
146
  const fileNames = Object.keys(zip.files);
147
+ const imageFiles = fileNames.filter(
148
+ (f) => !zip.files[f].dir && isSupportedImage(f) && path2.extname(f).toLowerCase() !== ".svg"
149
+ );
150
+ const totalImages = imageFiles.length;
151
+ let imageIndex = 0;
126
152
  for (const fileName of fileNames) {
127
153
  const file = zip.files[fileName];
128
154
  if (file.dir) {
@@ -130,14 +156,19 @@ async function processZip(source, destination, config) {
130
156
  continue;
131
157
  }
132
158
  const content = await file.async("nodebuffer");
133
- if (isSupportedImage(fileName)) {
134
- config.spinner.text = `Optimizing inside zip: ${fileName}`;
135
- const optimized = await optimizeBuffer(
136
- content,
137
- path.extname(fileName),
138
- config
139
- );
140
- newZip.file(fileName, optimized);
159
+ const ext = path2.extname(fileName).toLowerCase();
160
+ if (ext === ".svg") {
161
+ newZip.file(fileName, content);
162
+ } else if (isSupportedImage(fileName)) {
163
+ imageIndex++;
164
+ config.spinner.text = `Optimizing in zip [${imageIndex}/${totalImages}]: ${fileName}`;
165
+ try {
166
+ const optimized = await optimizeBuffer(content, path2.extname(fileName), config);
167
+ newZip.file(fileName, optimized);
168
+ } catch (error) {
169
+ console.error(chalk2.red(`Failed to optimize ${fileName}: ${error.message}`));
170
+ newZip.file(fileName, content);
171
+ }
141
172
  } else {
142
173
  newZip.file(fileName, content);
143
174
  }
@@ -149,69 +180,84 @@ async function processZip(source, destination, config) {
149
180
  compressionOptions: { level: 6 }
150
181
  });
151
182
  await fs.writeFile(destination, outputBuffer);
152
- console.log(`
153
- \u{1F4C1} Output: ${chalk.cyan(destination)}`);
154
183
  }
155
184
  async function processSingleFile(source, destination, config) {
156
185
  const buffer = await fs.readFile(source);
157
- const optimized = await optimizeBuffer(buffer, path.extname(source), config);
186
+ const optimized = await optimizeBuffer(buffer, path2.extname(source), config);
158
187
  await fs.writeFile(destination, optimized);
159
- console.log(`
160
- \u{1F4C1} Output: ${chalk.cyan(destination)}`);
161
188
  }
162
- async function optimizeBuffer(buffer, ext, config) {
163
- const extension = ext.toLowerCase();
189
+
190
+ // src/cli.ts
191
+ import { createRequire } from "module";
192
+ import { Command } from "commander";
193
+ import fs2 from "fs/promises";
194
+ import path3 from "path";
195
+ import chalk3 from "chalk";
196
+ import ora from "ora";
197
+ var require2 = createRequire(import.meta.url);
198
+ var { version } = require2("../package.json");
199
+ async function main() {
200
+ const program = new Command();
201
+ program.name("image-optimizer").description(
202
+ chalk3.cyan(
203
+ "\u{1F680} Universal CLI to optimize images (File, Folder, or Zip). Supports JPG, PNG, WebP, AVIF, GIF, TIFF, SVG."
204
+ )
205
+ ).version(version).requiredOption("-s, --source <path>", "Path to the input file, folder, or zip").option("-o, --output <path>", "Path to the output (default: source location + -optimized)").option("-q, --quality <number>", "Quality of compression (1-100)", "80").option("-w, --width <number>", "Max width to resize images to", "1600").parse(process.argv);
206
+ const options = program.opts();
207
+ const spinner = ora("Analyzing source...").start();
164
208
  try {
165
- let pipeline = sharp(buffer, { animated: true });
166
- const metadata = await pipeline.metadata();
167
- if (metadata.width && metadata.width > config.width) {
168
- pipeline = pipeline.resize({ width: config.width });
169
- }
170
- switch (extension) {
171
- case ".jpeg":
172
- case ".jpg":
173
- pipeline = pipeline.jpeg({ quality: config.quality, mozjpeg: true });
174
- break;
175
- case ".png":
176
- pipeline = pipeline.png({
177
- quality: config.quality,
178
- compressionLevel: 9,
179
- palette: true
180
- });
181
- break;
182
- case ".webp":
183
- pipeline = pipeline.webp({ quality: config.quality });
184
- break;
185
- case ".gif":
186
- pipeline = pipeline.gif({ colors: 128 });
187
- break;
188
- case ".avif":
189
- pipeline = pipeline.avif({ quality: config.quality });
190
- break;
191
- case ".tiff":
192
- pipeline = pipeline.tiff({ quality: config.quality });
193
- break;
209
+ const sourcePath = path3.resolve(process.cwd(), options.source);
210
+ try {
211
+ await fs2.access(sourcePath);
212
+ } catch {
213
+ spinner.fail(`Source not found: ${sourcePath}`);
214
+ process.exit(1);
194
215
  }
195
- const outputBuffer = await pipeline.toBuffer();
196
- if (outputBuffer.length >= buffer.length) {
197
- return buffer;
216
+ const stats = await fs2.stat(sourcePath);
217
+ const config = {
218
+ quality: parseInt(options.quality),
219
+ width: parseInt(options.width),
220
+ spinner
221
+ };
222
+ if (stats.isDirectory()) {
223
+ const outputPath = determineOutputPath(sourcePath, options.output, "-optimized");
224
+ spinner.text = "Processing Directory...";
225
+ await processDirectory(sourcePath, outputPath, config);
226
+ logOutputPath(outputPath);
227
+ } else if (sourcePath.endsWith(".zip")) {
228
+ const outputPath = determineOutputPath(sourcePath, options.output, "-optimized.zip");
229
+ spinner.text = "Processing Zip...";
230
+ await processZip(sourcePath, outputPath, config);
231
+ logOutputPath(outputPath);
232
+ } else if (isSupportedImage(sourcePath)) {
233
+ const ext = path3.extname(sourcePath);
234
+ const outputPath = determineOutputPath(sourcePath, options.output, `-optimized${ext}`);
235
+ spinner.text = "Processing Single File...";
236
+ await processSingleFile(sourcePath, outputPath, config);
237
+ logOutputPath(outputPath);
238
+ } else {
239
+ spinner.fail("Unsupported file type. Please provide a Folder, Zip, or supported Image.");
240
+ process.exit(1);
198
241
  }
199
- return outputBuffer;
200
- } catch (err) {
201
- return buffer;
242
+ spinner.succeed(chalk3.green("Optimization Complete!"));
243
+ } catch (error) {
244
+ spinner.fail("Error occurred");
245
+ console.error(chalk3.red(error.message));
246
+ process.exit(1);
202
247
  }
203
248
  }
204
- function isSupportedImage(filePath) {
205
- const ext = path.extname(filePath).toLowerCase();
206
- return SUPPORTED_EXTENSIONS.has(ext);
207
- }
208
- function determineOutputPath(source, userOutput, suffix) {
209
- if (userOutput) {
210
- return path.resolve(process.cwd(), userOutput);
211
- }
212
- const dir = path.dirname(source);
213
- const ext = path.extname(source);
214
- const name = path.basename(source, ext);
215
- return path.join(dir, `${name}${suffix}`);
249
+
250
+ // src/index.ts
251
+ import { fileURLToPath } from "url";
252
+ var isDirectRun = process.argv[1] === fileURLToPath(import.meta.url);
253
+ if (isDirectRun) {
254
+ main();
216
255
  }
217
- main();
256
+ export {
257
+ determineOutputPath,
258
+ isSupportedImage,
259
+ optimizeBuffer,
260
+ processDirectory,
261
+ processSingleFile,
262
+ processZip
263
+ };
package/package.json CHANGED
@@ -1,22 +1,35 @@
1
1
  {
2
2
  "name": "@mantiqh/image-optimizer",
3
- "version": "1.1.1",
3
+ "version": "1.2.2",
4
4
  "description": "CLI to optimize images.",
5
+ "type": "module",
5
6
  "main": "dist/index.js",
7
+ "exports": {
8
+ ".": "./dist/index.js"
9
+ },
6
10
  "bin": {
7
11
  "image-optimizer": "./dist/index.js"
8
12
  },
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"
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18"
15
18
  },
16
- "keywords": [],
19
+ "keywords": [
20
+ "image",
21
+ "optimizer",
22
+ "compress",
23
+ "resize",
24
+ "sharp",
25
+ "cli",
26
+ "png",
27
+ "jpg",
28
+ "webp",
29
+ "avif"
30
+ ],
17
31
  "author": "Muqtadir A.",
18
32
  "license": "MIT",
19
- "packageManager": "pnpm@10.12.3",
20
33
  "dependencies": {
21
34
  "chalk": "^5.6.2",
22
35
  "commander": "^14.0.2",
@@ -25,10 +38,28 @@
25
38
  "sharp": "^0.34.5"
26
39
  },
27
40
  "devDependencies": {
28
- "@types/jszip": "^3.4.1",
41
+ "@eslint/js": "^9.0.0",
29
42
  "@types/node": "^25.0.3",
30
- "@types/sharp": "^0.32.0",
43
+ "eslint": "^9.0.0",
44
+ "eslint-config-prettier": "^10.0.0",
45
+ "eslint-plugin-prettier": "^5.0.0",
46
+ "np": "^10.0.0",
47
+ "prettier": "^3.0.0",
31
48
  "tsup": "^8.5.1",
32
- "typescript": "^5.9.3"
49
+ "typescript": "^5.9.3",
50
+ "typescript-eslint": "^8.0.0",
51
+ "vitest": "^3.0.0"
52
+ },
53
+ "scripts": {
54
+ "build": "tsup src/index.ts --format esm --clean",
55
+ "dev": "tsup src/index.ts --format esm --watch --onSuccess \"node dist/index.js\"",
56
+ "start": "node dist/index.js",
57
+ "test": "vitest run",
58
+ "test:watch": "vitest",
59
+ "lint": "eslint src/ tests/",
60
+ "lint:fix": "eslint src/ tests/ --fix",
61
+ "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
62
+ "format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"",
63
+ "release": "np"
33
64
  }
34
- }
65
+ }
package/src/index.ts DELETED
@@ -1,302 +0,0 @@
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
- // --- Configuration ---
11
- const SUPPORTED_EXTENSIONS = new Set([
12
- ".jpg",
13
- ".jpeg",
14
- ".png",
15
- ".webp",
16
- ".gif",
17
- ".avif",
18
- ".tiff",
19
- ".svg",
20
- ]);
21
-
22
- const program = new Command();
23
-
24
- program
25
- .name("image-optimizer")
26
- .description(
27
- chalk.cyan(
28
- "šŸš€ Universal CLI to optimize images (File, Folder, or Zip). Supports JPG, PNG, WebP, AVIF, GIF, TIFF, SVG."
29
- )
30
- )
31
- .version("1.1.1")
32
- .requiredOption(
33
- "-s, --source <path>",
34
- "Path to the input file, folder, or zip"
35
- )
36
- .option(
37
- "-o, --output <path>",
38
- "Path to the output (default: source location + -optimized)"
39
- )
40
- .option("-q, --quality <number>", "Quality of compression (1-100)", "80")
41
- .option("-w, --width <number>", "Max width to resize images to", "1600")
42
- .parse(process.argv);
43
-
44
- const options = program.opts();
45
-
46
- async function main() {
47
- const spinner = ora("Analyzing source...").start();
48
-
49
- try {
50
- // Resolve absolute path of the source immediately
51
- const sourcePath = path.resolve(process.cwd(), options.source);
52
-
53
- // Check if source exists
54
- try {
55
- await fs.access(sourcePath);
56
- } catch {
57
- spinner.fail(`Source not found: ${sourcePath}`);
58
- process.exit(1);
59
- }
60
-
61
- const stats = await fs.stat(sourcePath);
62
- const config = {
63
- quality: parseInt(options.quality),
64
- width: parseInt(options.width),
65
- spinner,
66
- };
67
-
68
- if (stats.isDirectory()) {
69
- // MODE: Folder
70
- // Fix: Ensure output is next to source, not CWD
71
- const outputPath = determineOutputPath(
72
- sourcePath,
73
- options.output,
74
- "-optimized"
75
- );
76
- spinner.text = "Processing Directory...";
77
- await processDirectory(sourcePath, outputPath, config);
78
- } else if (sourcePath.endsWith(".zip")) {
79
- // MODE: Zip
80
- const outputPath = determineOutputPath(
81
- sourcePath,
82
- options.output,
83
- "-optimized.zip"
84
- );
85
- spinner.text = "Processing Zip...";
86
- await processZip(sourcePath, outputPath, config);
87
- } else if (isSupportedImage(sourcePath)) {
88
- // MODE: Single File
89
- const ext = path.extname(sourcePath);
90
- const outputPath = determineOutputPath(
91
- sourcePath,
92
- options.output,
93
- `-optimized${ext}`
94
- );
95
- spinner.text = "Processing Single File...";
96
- await processSingleFile(sourcePath, outputPath, config);
97
- } else {
98
- spinner.fail(
99
- "Unsupported file type. Please provide a Folder, Zip, or supported Image."
100
- );
101
- process.exit(1);
102
- }
103
-
104
- spinner.succeed(chalk.green("Optimization Complete!"));
105
- } catch (error: any) {
106
- spinner.fail("Error occurred");
107
- console.error(chalk.red(error.message));
108
- process.exit(1);
109
- }
110
- }
111
-
112
- // --- Processors ---
113
-
114
- async function processDirectory(
115
- source: string,
116
- destination: string,
117
- config: any
118
- ) {
119
- // 1. Create Destination Folder
120
- // If user pointed source as destination (rare error), avoid loop
121
- if (source === destination) {
122
- destination += "-1";
123
- }
124
- await fs.mkdir(destination, { recursive: true });
125
-
126
- // 2. Read Directory
127
- const entries = await fs.readdir(source, { withFileTypes: true });
128
- let processedCount = 0;
129
-
130
- for (const entry of entries) {
131
- const srcPath = path.join(source, entry.name);
132
- const destPath = path.join(destination, entry.name);
133
-
134
- if (entry.isDirectory()) {
135
- // Recursive call
136
- await processDirectory(srcPath, destPath, config);
137
- } else if (entry.isFile()) {
138
- if (isSupportedImage(entry.name)) {
139
- config.spinner.text = `Optimizing: ${entry.name}`;
140
- const buffer = await fs.readFile(srcPath);
141
- const optimizedBuffer = await optimizeBuffer(
142
- buffer,
143
- path.extname(entry.name),
144
- config
145
- );
146
- await fs.writeFile(destPath, optimizedBuffer);
147
- processedCount++;
148
- } else {
149
- // Copy non-images
150
- await fs.copyFile(srcPath, destPath);
151
- }
152
- }
153
- }
154
- // Added: Log output location for folders
155
- if (processedCount > 0) {
156
- // Only log the root output folder once (check logic if recursive)
157
- // Actually, since this is recursive, we should only log in the main caller.
158
- // But since we can't easily detect "root" here without extra args,
159
- // we'll rely on the main function logging or log here only if it looks like the root.
160
- // Better approach: Let's log it in main?
161
- // No, processDirectory is recursive.
162
- // Let's just log it once at the top level call.
163
- }
164
- // Log strictly for the user visibility (Moved logic to ensure visibility)
165
- console.log(`\nšŸ“ Output: ${chalk.cyan(destination)}`);
166
- }
167
-
168
- async function processZip(source: string, destination: string, config: any) {
169
- const zipData = await fs.readFile(source);
170
- const zip = await JSZip.loadAsync(zipData);
171
- const newZip = new JSZip();
172
-
173
- const fileNames = Object.keys(zip.files);
174
-
175
- for (const fileName of fileNames) {
176
- const file = zip.files[fileName];
177
- if (file.dir) {
178
- newZip.folder(fileName);
179
- continue;
180
- }
181
-
182
- const content = await file.async("nodebuffer");
183
- if (isSupportedImage(fileName)) {
184
- config.spinner.text = `Optimizing inside zip: ${fileName}`;
185
- const optimized = await optimizeBuffer(
186
- content,
187
- path.extname(fileName),
188
- config
189
- );
190
- newZip.file(fileName, optimized);
191
- } else {
192
- newZip.file(fileName, content);
193
- }
194
- }
195
-
196
- config.spinner.text = "Generating Output Zip...";
197
- const outputBuffer = await newZip.generateAsync({
198
- type: "nodebuffer",
199
- compression: "DEFLATE",
200
- compressionOptions: { level: 6 },
201
- });
202
- await fs.writeFile(destination, outputBuffer);
203
- console.log(`\nšŸ“ Output: ${chalk.cyan(destination)}`);
204
- }
205
-
206
- async function processSingleFile(
207
- source: string,
208
- destination: string,
209
- config: any
210
- ) {
211
- const buffer = await fs.readFile(source);
212
- const optimized = await optimizeBuffer(buffer, path.extname(source), config);
213
- await fs.writeFile(destination, optimized);
214
- console.log(`\nšŸ“ Output: ${chalk.cyan(destination)}`);
215
- }
216
-
217
- // --- Core Optimizer ---
218
-
219
- async function optimizeBuffer(
220
- buffer: Buffer,
221
- ext: string,
222
- config: any
223
- ): Promise<Buffer> {
224
- const extension = ext.toLowerCase();
225
-
226
- try {
227
- let pipeline = sharp(buffer, { animated: true });
228
- const metadata = await pipeline.metadata();
229
-
230
- // Resize
231
- if (metadata.width && metadata.width > config.width) {
232
- pipeline = pipeline.resize({ width: config.width });
233
- }
234
-
235
- // Compress based on format
236
- switch (extension) {
237
- case ".jpeg":
238
- case ".jpg":
239
- pipeline = pipeline.jpeg({ quality: config.quality, mozjpeg: true });
240
- break;
241
- case ".png":
242
- pipeline = pipeline.png({
243
- quality: config.quality,
244
- compressionLevel: 9,
245
- palette: true,
246
- });
247
- break;
248
- case ".webp":
249
- pipeline = pipeline.webp({ quality: config.quality });
250
- break;
251
- case ".gif":
252
- pipeline = pipeline.gif({ colors: 128 });
253
- break;
254
- case ".avif":
255
- pipeline = pipeline.avif({ quality: config.quality });
256
- break;
257
- case ".tiff":
258
- pipeline = pipeline.tiff({ quality: config.quality });
259
- break;
260
- }
261
-
262
- const outputBuffer = await pipeline.toBuffer();
263
-
264
- // Safety Check: If optimized is bigger, return original
265
- if (outputBuffer.length >= buffer.length) {
266
- return buffer;
267
- }
268
-
269
- return outputBuffer;
270
- } catch (err) {
271
- return buffer;
272
- }
273
- }
274
-
275
- // --- Helpers ---
276
-
277
- function isSupportedImage(filePath: string): boolean {
278
- const ext = path.extname(filePath).toLowerCase();
279
- return SUPPORTED_EXTENSIONS.has(ext);
280
- }
281
-
282
- function determineOutputPath(
283
- source: string,
284
- userOutput: string | undefined,
285
- suffix: string
286
- ): string {
287
- // If user provided a path, use it relative to CWD (standard CLI behavior)
288
- if (userOutput) {
289
- return path.resolve(process.cwd(), userOutput);
290
- }
291
-
292
- // If no output provided, put it NEXT TO THE SOURCE
293
- const dir = path.dirname(source); // Gets the folder containing the source
294
- const ext = path.extname(source);
295
- const name = path.basename(source, ext); // Filename without extension
296
-
297
- // If we are processing a folder, 'name' is the folder name
298
- // If we are processing a file, 'name' is the filename
299
- return path.join(dir, `${name}${suffix}`);
300
- }
301
-
302
- main();
package/tsconfig.json DELETED
@@ -1,13 +0,0 @@
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
- }