minify-pic-cli 1.2.0-beta.1 → 1.2.0-beta.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/index.js CHANGED
@@ -10,7 +10,7 @@ const ProgressDisplay = require("./lib/progress-display");
10
10
 
11
11
  // 默认配置参数
12
12
  const DEFAULT_CONFIG = {
13
- targetDir: process.cwd(), // 需要压缩的目录(可包含子目录)
13
+ targetDirs: [process.cwd()], // 需要压缩的目录数组(可包含子目录)
14
14
  outputDir: path.join(process.cwd(), "output"), // 输出目录
15
15
  quality: 80, // 压缩质量 0 - 100
16
16
  gifColours: 128, // GIF调色板最大数量
@@ -18,20 +18,68 @@ const DEFAULT_CONFIG = {
18
18
  };
19
19
 
20
20
  // 询问用户是否继续
21
- function askUserToContinue() {
21
+ function askUserToContinue(dirStats) {
22
22
  return new Promise((resolve) => {
23
23
  const rl = readline.createInterface({
24
24
  input: process.stdin,
25
25
  output: process.stdout,
26
26
  });
27
- console.log(`当前目录路径为: ${process.cwd()}`);
28
- rl.question("是否需要压缩当前目录的所有图片?Y/N:", (answer) => {
27
+
28
+ if (dirStats && dirStats.length > 0) {
29
+ console.log("\n检测到以下目录待压缩:");
30
+ dirStats.forEach((stat, index) => {
31
+ console.log(` ${index + 1}. ${stat.dir} (${stat.count} 张图片)`);
32
+ });
33
+ const totalCount = dirStats.reduce((sum, stat) => sum + stat.count, 0);
34
+ console.log(`总计: ${dirStats.length} 个目录, ${totalCount} 张图片\n`);
35
+ } else {
36
+ console.log(`当前目录路径为: ${process.cwd()}`);
37
+ }
38
+
39
+ rl.question("是否继续压缩? Y/N:", (answer) => {
29
40
  rl.close();
30
41
  resolve(answer.trim().toLowerCase() === "y");
31
42
  });
32
43
  });
33
44
  }
34
45
 
46
+ // 验证目录有效性
47
+ function validateDirectories(dirs) {
48
+ const validDirs = [];
49
+ const invalidDirs = [];
50
+
51
+ for (const dir of dirs) {
52
+ try {
53
+ if (!fs.existsSync(dir)) {
54
+ console.warn(`警告: 目录不存在,跳过: ${dir}`);
55
+ invalidDirs.push({ dir, reason: '目录不存在' });
56
+ continue;
57
+ }
58
+
59
+ const stat = fs.statSync(dir);
60
+ if (!stat.isDirectory()) {
61
+ console.warn(`警告: 路径不是目录,跳过: ${dir}`);
62
+ invalidDirs.push({ dir, reason: '不是目录' });
63
+ continue;
64
+ }
65
+
66
+ // 检查读取权限
67
+ try {
68
+ fs.accessSync(dir, fs.constants.R_OK);
69
+ validDirs.push(dir);
70
+ } catch (err) {
71
+ console.error(`错误: 目录无读取权限,跳过: ${dir}`);
72
+ invalidDirs.push({ dir, reason: '无读取权限' });
73
+ }
74
+ } catch (err) {
75
+ console.error(`错误: 验证目录失败,跳过: ${dir}`, err.message);
76
+ invalidDirs.push({ dir, reason: err.message });
77
+ }
78
+ }
79
+
80
+ return { validDirs, invalidDirs };
81
+ }
82
+
35
83
  // 获取文件大小(带单位,自动转换MB/KB)
36
84
  function getFileSizeWithUnit(filePath) {
37
85
  const stats = fs.statSync(filePath);
@@ -49,7 +97,7 @@ function getFileSizeWithUnit(filePath) {
49
97
  }
50
98
 
51
99
  // 压缩单张图片
52
- async function compressImage(filePath, config, baseDir = config.targetDir) {
100
+ async function compressImage(filePath, config, sourceDir) {
53
101
  const beforeSize = getFileSizeWithUnit(filePath);
54
102
  sharp.cache(false);
55
103
 
@@ -84,8 +132,10 @@ async function compressImage(filePath, config, baseDir = config.targetDir) {
84
132
  outputFileDir = path.dirname(outputFilePath);
85
133
  } else {
86
134
  // 保持目录结构输出到新目录
87
- const relativePath = path.relative(baseDir, filePath);
88
- outputFilePath = path.join(config.outputDir, relativePath);
135
+ // 多目录支持: 使用sourceDir的basename作为一级子目录
136
+ const relativePath = path.relative(sourceDir, filePath);
137
+ const sourceDirBasename = path.basename(sourceDir);
138
+ outputFilePath = path.join(config.outputDir, sourceDirBasename, relativePath);
89
139
  outputFileDir = path.dirname(outputFilePath);
90
140
  }
91
141
 
@@ -121,7 +171,7 @@ async function compressImage(filePath, config, baseDir = config.targetDir) {
121
171
  }
122
172
 
123
173
  // 递归扫描目录,收集所有图片文件
124
- function collectImageFiles(dir, config, baseDir = config.targetDir) {
174
+ function collectImageFiles(dir, config, sourceDir) {
125
175
  const files = fs.readdirSync(dir);
126
176
  const imageFiles = [];
127
177
 
@@ -137,7 +187,7 @@ function collectImageFiles(dir, config, baseDir = config.targetDir) {
137
187
 
138
188
  if (stat.isDirectory()) {
139
189
  if (!config.blackDirs.includes(file)) {
140
- const subFiles = collectImageFiles(filePath, config, baseDir);
190
+ const subFiles = collectImageFiles(filePath, config, sourceDir);
141
191
  imageFiles.push(...subFiles);
142
192
  } else {
143
193
  console.log(`跳过的目录: ${filePath}`);
@@ -145,7 +195,8 @@ function collectImageFiles(dir, config, baseDir = config.targetDir) {
145
195
  } else if (/\.(png|jpe?g|gif)$/i.test(filePath)) {
146
196
  imageFiles.push({
147
197
  filePath,
148
- size: stat.size
198
+ size: stat.size,
199
+ sourceDir: sourceDir // 记录所属源目录
149
200
  });
150
201
  }
151
202
  }
@@ -153,8 +204,34 @@ function collectImageFiles(dir, config, baseDir = config.targetDir) {
153
204
  return imageFiles;
154
205
  }
155
206
 
207
+ // 收集多个目录的图片文件
208
+ function collectImageFilesFromMultipleDirs(dirs, config) {
209
+ const allImageFiles = [];
210
+ const dirStats = [];
211
+
212
+ for (const dir of dirs) {
213
+ console.log(`正在扫描目录: ${dir}`);
214
+
215
+ // 如果是非原地替换模式,跳过与输出目录相同的源目录
216
+ if (!config.replace && path.resolve(dir) === path.resolve(config.outputDir)) {
217
+ console.warn(`警告: 源目录与输出目录相同,跳过: ${dir}`);
218
+ continue;
219
+ }
220
+
221
+ const imageFiles = collectImageFiles(dir, config, dir);
222
+ allImageFiles.push(...imageFiles);
223
+ dirStats.push({
224
+ dir: dir,
225
+ count: imageFiles.length
226
+ });
227
+ console.log(` 找到 ${imageFiles.length} 张图片`);
228
+ }
229
+
230
+ return { allImageFiles, dirStats };
231
+ }
232
+
156
233
  // 递归压缩目录下所有图片(单进程模式)
157
- async function compressFiles(dir, config, baseDir = config.targetDir) {
234
+ async function compressFiles(dir, config, sourceDir) {
158
235
  const files = fs.readdirSync(dir);
159
236
 
160
237
  for (const file of files) {
@@ -169,18 +246,18 @@ async function compressFiles(dir, config, baseDir = config.targetDir) {
169
246
 
170
247
  if (stat.isDirectory()) {
171
248
  if (!config.blackDirs.includes(file)) {
172
- await compressFiles(filePath, config, baseDir);
249
+ await compressFiles(filePath, config, sourceDir);
173
250
  } else {
174
251
  console.log(`跳过的目录: ${filePath}`);
175
252
  }
176
253
  } else if (/\.(png|jpe?g|gif)$/i.test(filePath)) {
177
- await compressImage(filePath, config, baseDir);
254
+ await compressImage(filePath, config, sourceDir);
178
255
  }
179
256
  }
180
257
  }
181
258
 
182
259
  // 多进程压缩
183
- async function compressFilesParallel(imageFiles, config, baseDir, setGlobalPool) {
260
+ async function compressFilesParallel(imageFiles, config, setGlobalPool) {
184
261
  const workerCount = WorkerPool.calculateWorkerCount(
185
262
  imageFiles.length,
186
263
  config.workers
@@ -201,10 +278,10 @@ async function compressFilesParallel(imageFiles, config, baseDir, setGlobalPool)
201
278
  for (const fileInfo of imageFiles) {
202
279
  try {
203
280
  const beforeSize = fileInfo.size;
204
- await compressImage(fileInfo.filePath, config, baseDir);
281
+ await compressImage(fileInfo.filePath, config, fileInfo.sourceDir);
205
282
  const afterSize = fs.statSync(
206
283
  config.replace ? fileInfo.filePath :
207
- path.join(config.outputDir, path.relative(baseDir, fileInfo.filePath))
284
+ path.join(config.outputDir, path.basename(fileInfo.sourceDir), path.relative(fileInfo.sourceDir, fileInfo.filePath))
208
285
  ).size;
209
286
 
210
287
  completed++;
@@ -252,7 +329,6 @@ async function compressFilesParallel(imageFiles, config, baseDir, setGlobalPool)
252
329
  gifColours: config.gifColours,
253
330
  outputDir: config.outputDir,
254
331
  replace: config.replace,
255
- baseDir: baseDir,
256
332
  onProgress: (data) => {
257
333
  progress.update(data);
258
334
  }
@@ -294,7 +370,7 @@ const program = new Command();
294
370
  program
295
371
  .name("mpic")
296
372
  .description("图片批量压缩工具")
297
- .option("-d, --dir <dir>", "需要压缩的目录", DEFAULT_CONFIG.targetDir)
373
+ .option("-d, --dir <dir>", "需要压缩的目录(逗号分隔多个目录)", val => val.split(",").map(d => d.trim()), DEFAULT_CONFIG.targetDirs)
298
374
  .option("-o, --output <output>", "输出目录", DEFAULT_CONFIG.outputDir)
299
375
  .option("-q, --quality <quality>", "压缩质量(0-100)", String(DEFAULT_CONFIG.quality))
300
376
  .option("-g, --gif-colours <colours>", "GIF调色板最大数量(2-256)", String(DEFAULT_CONFIG.gifColours))
@@ -329,9 +405,14 @@ program
329
405
  // 注册信号处理
330
406
  process.on('SIGINT', () => gracefulShutdown('SIGINT'));
331
407
  process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
332
- // 合并配置
408
+
409
+ // 合并配置 - 处理多目录
410
+ const inputDirs = Array.isArray(options.dir)
411
+ ? options.dir.map(d => path.resolve(process.cwd(), d))
412
+ : [path.resolve(process.cwd(), options.dir)];
413
+
333
414
  const config = {
334
- targetDir: path.resolve(process.cwd(), options.dir),
415
+ targetDirs: inputDirs,
335
416
  outputDir: options.replace ? null : path.resolve(process.cwd(), options.output),
336
417
  quality: parseInt(options.quality, 10),
337
418
  gifColours: parseInt(options.gifColours, 10),
@@ -340,29 +421,42 @@ program
340
421
  workers: options.workers,
341
422
  noParallel: options.noParallel || false,
342
423
  };
343
-
344
- let shouldContinue = options.yes;
345
- if (!shouldContinue) {
346
- shouldContinue = await askUserToContinue();
424
+
425
+ // 验证目录
426
+ console.log("正在验证目录...");
427
+ const { validDirs, invalidDirs } = validateDirectories(config.targetDirs);
428
+
429
+ if (invalidDirs.length > 0) {
430
+ console.log(`\n跳过了 ${invalidDirs.length} 个无效目录`);
347
431
  }
348
- if (!shouldContinue) {
349
- console.log("已取消操作。");
350
- process.exit(0);
432
+
433
+ if (validDirs.length === 0) {
434
+ console.error("错误: 所有目录都无效,程序退出");
435
+ process.exit(1);
351
436
  }
352
-
353
- const inputDir = config.targetDir;
437
+
438
+ console.log(`\n有效目录: ${validDirs.length} 个`);
354
439
 
355
440
  // 收集所有图片文件
356
- console.log("正在扫描图片文件...");
357
- const imageFiles = collectImageFiles(inputDir, config, inputDir);
441
+ console.log("\n正在扫描图片文件...");
442
+ const { allImageFiles, dirStats } = collectImageFilesFromMultipleDirs(validDirs, config);
358
443
 
359
- if (imageFiles.length === 0) {
444
+ if (allImageFiles.length === 0) {
360
445
  console.log("未找到需要压缩的图片文件");
361
446
  process.exit(0);
362
447
  }
363
448
 
449
+ let shouldContinue = options.yes;
450
+ if (!shouldContinue) {
451
+ shouldContinue = await askUserToContinue(dirStats);
452
+ }
453
+ if (!shouldContinue) {
454
+ console.log("已取消操作。");
455
+ process.exit(0);
456
+ }
457
+
364
458
  try {
365
- await compressFilesParallel(imageFiles, config, inputDir, (pool) => {
459
+ await compressFilesParallel(allImageFiles, config, (pool) => {
366
460
  globalPool = pool;
367
461
  });
368
462
 
@@ -250,13 +250,13 @@ class WorkerPool {
250
250
  type: "task",
251
251
  taskId,
252
252
  filePath: task.filePath,
253
+ sourceDir: task.sourceDir,
253
254
  config: {
254
255
  quality: this.config.quality,
255
256
  gifColours: this.config.gifColours,
256
257
  outputDir: this.config.outputDir,
257
258
  replace: this.config.replace
258
- },
259
- baseDir: this.config.baseDir
259
+ }
260
260
  });
261
261
  }
262
262
 
package/lib/worker.js CHANGED
@@ -13,7 +13,7 @@ function getFileSize(filePath) {
13
13
  }
14
14
 
15
15
  // 压缩单张图片
16
- async function compressImage(filePath, config, baseDir) {
16
+ async function compressImage(filePath, config, sourceDir) {
17
17
  const beforeSize = getFileSize(filePath);
18
18
  const startTime = Date.now();
19
19
 
@@ -50,8 +50,10 @@ async function compressImage(filePath, config, baseDir) {
50
50
  outputFileDir = path.dirname(outputFilePath);
51
51
  } else {
52
52
  // 保持目录结构输出到新目录
53
- const relativePath = path.relative(baseDir, filePath);
54
- outputFilePath = path.join(config.outputDir, relativePath);
53
+ // 多目录支持: 使用sourceDir的basename作为一级子目录
54
+ const relativePath = path.relative(sourceDir, filePath);
55
+ const sourceDirBasename = path.basename(sourceDir);
56
+ outputFilePath = path.join(config.outputDir, sourceDirBasename, relativePath);
55
57
  outputFileDir = path.dirname(outputFilePath);
56
58
  }
57
59
 
@@ -85,10 +87,10 @@ async function compressImage(filePath, config, baseDir) {
85
87
  // 工作进程消息处理
86
88
  process.on("message", async (message) => {
87
89
  if (message.type === "task") {
88
- const { taskId, filePath, config, baseDir } = message;
90
+ const { taskId, filePath, config, sourceDir } = message;
89
91
 
90
92
  try {
91
- const result = await compressImage(filePath, config, baseDir);
93
+ const result = await compressImage(filePath, config, sourceDir);
92
94
 
93
95
  process.send({
94
96
  type: "success",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minify-pic-cli",
3
- "version": "1.2.0-beta.1",
3
+ "version": "1.2.0-beta.2",
4
4
  "description": "一个简单易用的图片批量压缩命令行工具,支持 PNG、JPEG、GIF 格式,适合前端和设计师快速优化图片体积。",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "scripts": {
15
15
  "build": "node ./index.js",
16
- "test": "mpic -d ./test/src -o ./test/output -y"
16
+ "test": "mpic -d ./test/src/assets,./test/src/imgs -o ./test/output -y -w 2"
17
17
  },
18
18
  "keywords": [
19
19
  "图片压缩",