auto-image-converter 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +154 -28
- package/bin/index.js +50 -19
- package/bin/onetimeresizer.mjs +93 -0
- package/bin/watcher.mjs +52 -35
- package/image-converter.config.mjs +19 -7
- package/lib/ConvertImages.js +68 -0
- package/lib/CreateSrcSet.js +60 -0
- package/lib/FileManager.js +205 -0
- package/lib/MarkerFile.js +98 -0
- package/lib/Pipeline.js +378 -0
- package/lib/Queue.js +41 -0
- package/lib/ResizeImages.js +105 -0
- package/lib/converter.js +135 -50
- package/lib/pipelines/ConvertationPipeline.js +182 -0
- package/lib/steps/ConvertationStep.js +493 -0
- package/package.json +29 -27
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
import { ConvertImages } from "../ConvertImages.js";
|
|
5
|
+
import { FileManager } from "../FileManager.js";
|
|
6
|
+
import { MarkerFile } from "../MarkerFile.js";
|
|
7
|
+
|
|
8
|
+
export class ConvertationStep {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.supportedExtensions = this.#parseExtensions(
|
|
12
|
+
config.converted ||
|
|
13
|
+
"*.{png,jpg,jpeg,webp,avif,tiff}"
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Парсим расширения из паттерна converted
|
|
18
|
+
#parseExtensions(pattern) {
|
|
19
|
+
const match = pattern.match(/\*\.\{(.+?)\}/);
|
|
20
|
+
if (match) {
|
|
21
|
+
return match[1]
|
|
22
|
+
.split(",")
|
|
23
|
+
.map((ext) => `.${ext.trim()}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Если паттерн типа "*.png"
|
|
27
|
+
const simpleMatch = pattern.match(/\*\.(\w+)/);
|
|
28
|
+
if (simpleMatch) {
|
|
29
|
+
return [`.${simpleMatch[1]}`];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Fallback
|
|
33
|
+
return [
|
|
34
|
+
".jpg",
|
|
35
|
+
".jpeg",
|
|
36
|
+
".png",
|
|
37
|
+
".webp",
|
|
38
|
+
".avif",
|
|
39
|
+
".tiff",
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Основной метод - конвертация одного файла
|
|
44
|
+
async execute(filePath) {
|
|
45
|
+
const initialPath = filePath; // сохраняем начальный путь
|
|
46
|
+
let skipOriginalCheck = false; // флаг что мы только что переименовали .processed → .original
|
|
47
|
+
let wasRenamed = false; // флаг что файл был переименован
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// 1. Проверка маркеров (пропускаем обработанные файлы)
|
|
51
|
+
if (!this.config.force) {
|
|
52
|
+
const marker = new MarkerFile(filePath);
|
|
53
|
+
|
|
54
|
+
// Проверяем файлы с маркером .processed
|
|
55
|
+
if (marker.isMarkProcessed()) {
|
|
56
|
+
const currentFormat = path
|
|
57
|
+
.extname(filePath)
|
|
58
|
+
.slice(1)
|
|
59
|
+
.toLowerCase();
|
|
60
|
+
const targetFormat = (
|
|
61
|
+
this.config.format || "webp"
|
|
62
|
+
).toLowerCase();
|
|
63
|
+
|
|
64
|
+
// Если формат совпадает - пропускаем
|
|
65
|
+
if (currentFormat === targetFormat) {
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
skipped: true,
|
|
69
|
+
reason: "already_processed",
|
|
70
|
+
originalPath: filePath,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Формат не совпадает - нужна переконвертация
|
|
75
|
+
// Проверяем, есть ли .original файл с таким же именем
|
|
76
|
+
const cleanName =
|
|
77
|
+
marker.removeAllMarkers();
|
|
78
|
+
const dir = path.dirname(filePath);
|
|
79
|
+
const ext = path.extname(filePath);
|
|
80
|
+
const originalPath = path.join(
|
|
81
|
+
dir,
|
|
82
|
+
`${cleanName}.original${ext}`
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const fm = new FileManager(filePath);
|
|
86
|
+
const originalExists = await fm.exists(
|
|
87
|
+
originalPath
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (originalExists) {
|
|
91
|
+
// Есть .original - это устаревший .processed
|
|
92
|
+
// Просто удаляем и пропускаем (оригинал будет обработан отдельно)
|
|
93
|
+
await fm.deleteFile(filePath);
|
|
94
|
+
return {
|
|
95
|
+
success: false,
|
|
96
|
+
skipped: true,
|
|
97
|
+
reason: "outdated_processed_removed",
|
|
98
|
+
originalPath: filePath,
|
|
99
|
+
};
|
|
100
|
+
} else {
|
|
101
|
+
// Нет .original с таким же расширением
|
|
102
|
+
// Проверяем, нет ли .original с другим расширением
|
|
103
|
+
const hasAnyOriginal =
|
|
104
|
+
await this.#checkAnyOriginalExists(
|
|
105
|
+
filePath,
|
|
106
|
+
cleanName
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (hasAnyOriginal) {
|
|
110
|
+
// Есть другой .original - это устаревший .processed, удаляем
|
|
111
|
+
await fm.deleteFile(filePath);
|
|
112
|
+
return {
|
|
113
|
+
success: false,
|
|
114
|
+
skipped: true,
|
|
115
|
+
reason: "outdated_processed_removed_other_original_exists",
|
|
116
|
+
originalPath: filePath,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Нет ни одного .original - этот .processed становится оригиналом
|
|
121
|
+
const newPath =
|
|
122
|
+
await marker.addMarker(
|
|
123
|
+
MarkerFile.MARKERS.ORIGINAL
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Обновляем filePath и marker для продолжения конвертации
|
|
127
|
+
filePath = newPath;
|
|
128
|
+
wasRenamed = true; // помечаем что файл был переименован
|
|
129
|
+
marker.filePath = newPath;
|
|
130
|
+
marker.dir = path.dirname(newPath);
|
|
131
|
+
marker.ext = path.extname(newPath);
|
|
132
|
+
marker.nameWithoutExt =
|
|
133
|
+
path.basename(
|
|
134
|
+
newPath,
|
|
135
|
+
marker.ext
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
skipOriginalCheck = true; // пропускаем проверку .original ниже
|
|
139
|
+
// НЕ возвращаем - продолжаем выполнение!
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Если файл с маркером .original - проверяем, существует ли результат
|
|
144
|
+
if (
|
|
145
|
+
!skipOriginalCheck &&
|
|
146
|
+
this.config.markerOriginal &&
|
|
147
|
+
marker.isMarkOriginal()
|
|
148
|
+
) {
|
|
149
|
+
const targetFormat = (
|
|
150
|
+
this.config.format || "webp"
|
|
151
|
+
).toLowerCase();
|
|
152
|
+
const cleanName =
|
|
153
|
+
marker.removeAllMarkers();
|
|
154
|
+
|
|
155
|
+
const fm = new FileManager(filePath, {
|
|
156
|
+
outputDir: this.config.outputDir,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const expectedOutputPath =
|
|
160
|
+
fm.resolvePath({
|
|
161
|
+
name: cleanName, // чистое имя без маркеров
|
|
162
|
+
ext: `.${targetFormat}`,
|
|
163
|
+
marker: this.config
|
|
164
|
+
.markerProcessed
|
|
165
|
+
? "processed"
|
|
166
|
+
: null,
|
|
167
|
+
outputDir:
|
|
168
|
+
this.config.outputDir,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Если результат существует - пропускаем
|
|
172
|
+
if (
|
|
173
|
+
await fm.exists(expectedOutputPath)
|
|
174
|
+
) {
|
|
175
|
+
// Если нужно удалять оригинал - удаляем его
|
|
176
|
+
if (this.config.removeOriginal) {
|
|
177
|
+
await fm.deleteFile(filePath);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
success: false,
|
|
182
|
+
skipped: true,
|
|
183
|
+
reason: "already_converted_output_exists",
|
|
184
|
+
originalPath: filePath,
|
|
185
|
+
outputPath: expectedOutputPath,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Результата нет - продолжаем конвертацию
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 2. Проверка расширения (пропускаем если уже в нужном формате)
|
|
194
|
+
const currentExt = path
|
|
195
|
+
.extname(filePath)
|
|
196
|
+
.slice(1)
|
|
197
|
+
.toLowerCase();
|
|
198
|
+
const targetFormat = (
|
|
199
|
+
this.config.format || "webp"
|
|
200
|
+
).toLowerCase();
|
|
201
|
+
|
|
202
|
+
if (currentExt === targetFormat) {
|
|
203
|
+
// Если это .original файл уже в целевом формате
|
|
204
|
+
const marker = new MarkerFile(filePath);
|
|
205
|
+
if (
|
|
206
|
+
marker.isMarkOriginal() &&
|
|
207
|
+
this.config.removeOriginal
|
|
208
|
+
) {
|
|
209
|
+
// Удаляем, т.к. результат уже есть
|
|
210
|
+
const fm = new FileManager(filePath);
|
|
211
|
+
await fm.deleteFile(filePath);
|
|
212
|
+
return {
|
|
213
|
+
success: false,
|
|
214
|
+
skipped: true,
|
|
215
|
+
reason: "original_in_target_format_removed",
|
|
216
|
+
originalPath: filePath,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
success: false,
|
|
222
|
+
skipped: true,
|
|
223
|
+
reason: "already_target_format",
|
|
224
|
+
originalPath: filePath,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 3. Конвертация
|
|
229
|
+
const quality = this.config.quality || 80;
|
|
230
|
+
|
|
231
|
+
// Читаем файл в буфер сразу, чтобы освободить дескриптор
|
|
232
|
+
const sourceBuffer = await fs.readFile(
|
|
233
|
+
filePath
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const converter = new ConvertImages(
|
|
237
|
+
sourceBuffer, // передаем буфер, а не путь к файлу
|
|
238
|
+
targetFormat,
|
|
239
|
+
quality
|
|
240
|
+
);
|
|
241
|
+
const buffer = await this.#convertByFormat(
|
|
242
|
+
converter,
|
|
243
|
+
targetFormat
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// 4. Подготовка к сохранению
|
|
247
|
+
// Убираем все маркеры из имени файла перед созданием результата
|
|
248
|
+
const originalMarker = new MarkerFile(filePath);
|
|
249
|
+
const cleanName =
|
|
250
|
+
originalMarker.removeAllMarkers();
|
|
251
|
+
|
|
252
|
+
const fm = new FileManager(filePath, {
|
|
253
|
+
outputDir: this.config.outputDir,
|
|
254
|
+
outputPattern: this.config.pattern,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const desiredPath = fm.resolvePath({
|
|
258
|
+
name: cleanName, // используем чистое имя без маркеров
|
|
259
|
+
ext: `.${targetFormat}`,
|
|
260
|
+
marker: this.config.markerProcessed
|
|
261
|
+
? "processed"
|
|
262
|
+
: null,
|
|
263
|
+
outputDir: this.config.outputDir,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Проверка: если файл .original и результат уже существует - удаляем оригинал
|
|
267
|
+
if (
|
|
268
|
+
originalMarker.isMarkOriginal() &&
|
|
269
|
+
(await fm.exists(desiredPath))
|
|
270
|
+
) {
|
|
271
|
+
if (this.config.removeOriginal) {
|
|
272
|
+
await fm.deleteFile(filePath);
|
|
273
|
+
return {
|
|
274
|
+
success: false,
|
|
275
|
+
skipped: true,
|
|
276
|
+
reason: "original_removed_result_exists",
|
|
277
|
+
originalPath: filePath,
|
|
278
|
+
outputPath: desiredPath,
|
|
279
|
+
};
|
|
280
|
+
} else {
|
|
281
|
+
return {
|
|
282
|
+
success: false,
|
|
283
|
+
skipped: true,
|
|
284
|
+
reason: "result_already_exists",
|
|
285
|
+
originalPath: filePath,
|
|
286
|
+
outputPath: desiredPath,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 5. Проверка дубликатов
|
|
292
|
+
const { finalPath, duplicate } =
|
|
293
|
+
await this.#checkDuplicate(
|
|
294
|
+
desiredPath,
|
|
295
|
+
buffer
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
if (duplicate) {
|
|
299
|
+
// Удаляем оригинал-дубликат если нужно
|
|
300
|
+
if (this.config.removeOriginal) {
|
|
301
|
+
await fm.deleteFile(filePath);
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
success: false,
|
|
305
|
+
skipped: true,
|
|
306
|
+
reason: "duplicate",
|
|
307
|
+
originalPath: filePath,
|
|
308
|
+
outputPath: finalPath,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// 6. Сохранение
|
|
313
|
+
const dir = path.dirname(finalPath);
|
|
314
|
+
await fs.mkdir(dir, { recursive: true });
|
|
315
|
+
await fs.writeFile(finalPath, buffer);
|
|
316
|
+
|
|
317
|
+
// 7. Добавление маркера к оригиналу (если включено)
|
|
318
|
+
if (
|
|
319
|
+
this.config.markerOriginal &&
|
|
320
|
+
!this.config.removeOriginal
|
|
321
|
+
) {
|
|
322
|
+
const originalMarker = new MarkerFile(
|
|
323
|
+
filePath
|
|
324
|
+
);
|
|
325
|
+
await originalMarker.markOriginal();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// 8. Удаление оригинала (если включено)
|
|
329
|
+
if (this.config.removeOriginal) {
|
|
330
|
+
console.log(
|
|
331
|
+
`[DEBUG] Deleting original: ${filePath}`
|
|
332
|
+
);
|
|
333
|
+
await fm.deleteFile(filePath);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
success: true,
|
|
338
|
+
skipped: false,
|
|
339
|
+
outputPath: finalPath,
|
|
340
|
+
originalPath: filePath,
|
|
341
|
+
format: targetFormat,
|
|
342
|
+
};
|
|
343
|
+
} catch (error) {
|
|
344
|
+
return {
|
|
345
|
+
success: false,
|
|
346
|
+
skipped: false,
|
|
347
|
+
error: error.message,
|
|
348
|
+
originalPath: filePath,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Проверить существование .original файлов с любым расширением
|
|
354
|
+
async #checkAnyOriginalExists(filePath, cleanName) {
|
|
355
|
+
const dir = path.dirname(filePath);
|
|
356
|
+
|
|
357
|
+
for (const ext of this.supportedExtensions) {
|
|
358
|
+
const candidatePath = path.join(
|
|
359
|
+
dir,
|
|
360
|
+
`${cleanName}.original${ext}`
|
|
361
|
+
);
|
|
362
|
+
const fm = new FileManager(candidatePath);
|
|
363
|
+
|
|
364
|
+
if (await fm.exists(candidatePath)) {
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Приватный метод конвертации по формату
|
|
373
|
+
async #convertByFormat(converter, format) {
|
|
374
|
+
switch (format.toLowerCase()) {
|
|
375
|
+
case "webp":
|
|
376
|
+
return converter.toWebp();
|
|
377
|
+
case "avif":
|
|
378
|
+
return converter.toAvif();
|
|
379
|
+
case "png":
|
|
380
|
+
return converter.toPng();
|
|
381
|
+
case "jpg":
|
|
382
|
+
case "jpeg":
|
|
383
|
+
return converter.toJpg();
|
|
384
|
+
case "tiff":
|
|
385
|
+
return converter.toTiff();
|
|
386
|
+
default:
|
|
387
|
+
throw new Error(
|
|
388
|
+
`Unsupported format: ${format}`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Приватный метод проверки дубликатов (гибридный: размер + хеш)
|
|
394
|
+
async #checkDuplicate(desiredPath, buffer) {
|
|
395
|
+
try {
|
|
396
|
+
const stat = await fs.stat(desiredPath);
|
|
397
|
+
|
|
398
|
+
// 1. Быстрая проверка по размеру
|
|
399
|
+
if (stat.size !== buffer.length) {
|
|
400
|
+
// Размеры разные - точно не дубликат
|
|
401
|
+
// Ищем уникальное имя с (N)
|
|
402
|
+
return await this.#findUniqueName(
|
|
403
|
+
desiredPath,
|
|
404
|
+
buffer
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// 2. Размеры совпали - проверяем хеш (точная проверка)
|
|
409
|
+
const existingFm = new FileManager(desiredPath);
|
|
410
|
+
const existingHash =
|
|
411
|
+
await existingFm.getFileHash();
|
|
412
|
+
|
|
413
|
+
const bufferHash = crypto
|
|
414
|
+
.createHash("sha256")
|
|
415
|
+
.update(buffer)
|
|
416
|
+
.digest("hex");
|
|
417
|
+
|
|
418
|
+
if (existingHash === bufferHash) {
|
|
419
|
+
// Хеши совпали - это точно дубликат
|
|
420
|
+
return {
|
|
421
|
+
finalPath: desiredPath,
|
|
422
|
+
duplicate: true,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Размеры одинаковые, но хеши разные - разные файлы
|
|
427
|
+
return await this.#findUniqueName(
|
|
428
|
+
desiredPath,
|
|
429
|
+
buffer
|
|
430
|
+
);
|
|
431
|
+
} catch {
|
|
432
|
+
// Файл не существует - используем оригинальное имя
|
|
433
|
+
return {
|
|
434
|
+
finalPath: desiredPath,
|
|
435
|
+
duplicate: false,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Вспомогательный метод поиска уникального имени
|
|
441
|
+
async #findUniqueName(desiredPath, buffer) {
|
|
442
|
+
const dir = path.dirname(desiredPath);
|
|
443
|
+
const ext = path.extname(desiredPath);
|
|
444
|
+
const name = path.basename(desiredPath, ext);
|
|
445
|
+
|
|
446
|
+
const bufferHash = crypto
|
|
447
|
+
.createHash("sha256")
|
|
448
|
+
.update(buffer)
|
|
449
|
+
.digest("hex");
|
|
450
|
+
|
|
451
|
+
let counter = 1;
|
|
452
|
+
while (true) {
|
|
453
|
+
const candidate = path.join(
|
|
454
|
+
dir,
|
|
455
|
+
`${name}(${counter})${ext}`
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
const candidateStat = await fs.stat(
|
|
460
|
+
candidate
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
// Проверяем размер
|
|
464
|
+
if (candidateStat.size !== buffer.length) {
|
|
465
|
+
counter++;
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Размер совпал - проверяем хеш
|
|
470
|
+
const candidateFm = new FileManager(
|
|
471
|
+
candidate
|
|
472
|
+
);
|
|
473
|
+
const candidateHash =
|
|
474
|
+
await candidateFm.getFileHash();
|
|
475
|
+
|
|
476
|
+
if (candidateHash === bufferHash) {
|
|
477
|
+
return {
|
|
478
|
+
finalPath: candidate,
|
|
479
|
+
duplicate: true,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
counter++;
|
|
484
|
+
} catch {
|
|
485
|
+
// Файл не существует - используем это имя
|
|
486
|
+
return {
|
|
487
|
+
finalPath: candidate,
|
|
488
|
+
duplicate: false,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
package/package.json
CHANGED
|
@@ -1,28 +1,30 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "auto-image-converter",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"keywords": [
|
|
5
|
-
"image",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"png",
|
|
13
|
-
"jpg",
|
|
14
|
-
"jpeg",
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
"auto-convert-images
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "auto-image-converter",
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"image",
|
|
6
|
+
"sharp",
|
|
7
|
+
"watch",
|
|
8
|
+
"optimization",
|
|
9
|
+
"converter",
|
|
10
|
+
"webp",
|
|
11
|
+
"avif",
|
|
12
|
+
"png",
|
|
13
|
+
"jpg",
|
|
14
|
+
"jpeg",
|
|
15
|
+
"tiff",
|
|
16
|
+
"for frontend"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"bin": {
|
|
20
|
+
"auto-convert-images": "./bin/index.js",
|
|
21
|
+
"auto-convert-images-watch": "./bin/watcher.mjs",
|
|
22
|
+
"auto-convert-images-resize": "./bin/onetimeresizer.mjs"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"chokidar": "^4.0.3",
|
|
26
|
+
"commander": "^14.0.1",
|
|
27
|
+
"fast-glob": "^3.3.3",
|
|
28
|
+
"sharp": "^0.34.4"
|
|
29
|
+
}
|
|
28
30
|
}
|