@puntoycoma/bulki 3.0.2 → 3.0.4
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 +49 -22
- package/dist/bin/bulki.js +144 -226
- package/dist/bin/bulki.js.map +1 -1
- package/dist/chunk-YUAKNSBH.js +851 -0
- package/dist/chunk-YUAKNSBH.js.map +1 -0
- package/dist/interactive-LGLWIHMM.js +566 -0
- package/dist/interactive-LGLWIHMM.js.map +1 -0
- package/package.json +12 -11
- package/dist/chunk-7TFBGWBB.js +0 -532
- package/dist/chunk-7TFBGWBB.js.map +0 -1
- package/dist/interactive-HBGX4FD6.js +0 -295
- package/dist/interactive-HBGX4FD6.js.map +0 -1
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/output/format-bytes.ts
|
|
4
|
+
function formatBytes(bytes) {
|
|
5
|
+
if (bytes <= 0) return "0 B";
|
|
6
|
+
const GB = 1024 * 1024 * 1024;
|
|
7
|
+
const MB = 1024 * 1024;
|
|
8
|
+
const KB = 1024;
|
|
9
|
+
if (bytes >= GB) return `${(bytes / GB).toFixed(2)} GB`;
|
|
10
|
+
if (bytes >= MB) return `${(bytes / MB).toFixed(2)} MB`;
|
|
11
|
+
if (bytes >= KB) return `${(bytes / KB).toFixed(2)} KB`;
|
|
12
|
+
return `${bytes} B`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/pipeline/filter.ts
|
|
16
|
+
import fs from "fs/promises";
|
|
17
|
+
import sharp from "sharp";
|
|
18
|
+
var UNSUPPORTED = "unsupported";
|
|
19
|
+
var PNG_MAGIC = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
20
|
+
var AVIF_BRANDS = /* @__PURE__ */ new Set(["avif", "avis"]);
|
|
21
|
+
var HEIF_BRANDS = /* @__PURE__ */ new Set(["heic", "heix", "hevc", "hevx", "heim", "heis", "mif1", "msf1"]);
|
|
22
|
+
async function detectFormat(filePath) {
|
|
23
|
+
const buffer = Buffer.alloc(12);
|
|
24
|
+
try {
|
|
25
|
+
const handle = await fs.open(filePath, "r");
|
|
26
|
+
try {
|
|
27
|
+
const { bytesRead } = await handle.read(buffer, 0, 12, 0);
|
|
28
|
+
if (bytesRead < 4) return null;
|
|
29
|
+
} finally {
|
|
30
|
+
await handle.close();
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255) return "jpeg";
|
|
36
|
+
if (buffer.subarray(0, 8).equals(PNG_MAGIC)) return "png";
|
|
37
|
+
if (buffer.toString("latin1", 0, 4) === "RIFF" && buffer.toString("latin1", 8, 12) === "WEBP")
|
|
38
|
+
return "webp";
|
|
39
|
+
if (buffer.toString("latin1", 0, 3) === "GIF") return "gif";
|
|
40
|
+
if (buffer.toString("latin1", 4, 8) === "ftyp") {
|
|
41
|
+
const brand = buffer.toString("latin1", 8, 12);
|
|
42
|
+
if (AVIF_BRANDS.has(brand)) return "avif";
|
|
43
|
+
if (HEIF_BRANDS.has(brand)) {
|
|
44
|
+
try {
|
|
45
|
+
const meta = await sharp(filePath).metadata();
|
|
46
|
+
if (meta.format === "heif" && meta.compression === "av1") return "avif";
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
return UNSUPPORTED;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/pipeline/optimize.ts
|
|
56
|
+
import fs3 from "fs/promises";
|
|
57
|
+
import path2 from "path";
|
|
58
|
+
|
|
59
|
+
// src/concurrency/pool.ts
|
|
60
|
+
async function processPool(items, processor, concurrency) {
|
|
61
|
+
const results = new Array(items.length);
|
|
62
|
+
const executing = /* @__PURE__ */ new Set();
|
|
63
|
+
let firstError;
|
|
64
|
+
let hasError = false;
|
|
65
|
+
for (let i = 0; i < items.length; i++) {
|
|
66
|
+
const index = i;
|
|
67
|
+
const p = processor(items[index]).then((result) => {
|
|
68
|
+
results[index] = result;
|
|
69
|
+
}).catch((err) => {
|
|
70
|
+
if (!hasError) {
|
|
71
|
+
hasError = true;
|
|
72
|
+
firstError = err;
|
|
73
|
+
}
|
|
74
|
+
}).finally(() => {
|
|
75
|
+
executing.delete(p);
|
|
76
|
+
});
|
|
77
|
+
executing.add(p);
|
|
78
|
+
if (executing.size >= concurrency) {
|
|
79
|
+
await Promise.race(executing);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
await Promise.all(executing);
|
|
83
|
+
if (hasError) {
|
|
84
|
+
throw firstError;
|
|
85
|
+
}
|
|
86
|
+
return results;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/pipeline/encode.ts
|
|
90
|
+
import { execFile } from "child_process";
|
|
91
|
+
import fs2 from "fs/promises";
|
|
92
|
+
import os from "os";
|
|
93
|
+
import path from "path";
|
|
94
|
+
import { promisify } from "util";
|
|
95
|
+
import sharp2 from "sharp";
|
|
96
|
+
sharp2.cache(false);
|
|
97
|
+
var execFileAsync = promisify(execFile);
|
|
98
|
+
var ANIMATED_OUTPUTS = /* @__PURE__ */ new Set(["gif", "webp"]);
|
|
99
|
+
var ANIMATED_INPUTS = /* @__PURE__ */ new Set(["gif", "webp"]);
|
|
100
|
+
function getFormatOptions(quality) {
|
|
101
|
+
return {
|
|
102
|
+
jpeg: { quality, mozjpeg: true },
|
|
103
|
+
// `quality` only has an effect together with `palette` (lossy quantisation, like TinyPNG)
|
|
104
|
+
png: { quality, palette: quality < 100, compressionLevel: 9, adaptiveFiltering: true },
|
|
105
|
+
webp: { quality },
|
|
106
|
+
avif: { quality },
|
|
107
|
+
gif: {}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function defaultOutputFormat(detectedFormat) {
|
|
111
|
+
return detectedFormat === UNSUPPORTED ? "jpeg" : detectedFormat;
|
|
112
|
+
}
|
|
113
|
+
function extensionFor(format) {
|
|
114
|
+
return format === "jpeg" ? ".jpg" : `.${format}`;
|
|
115
|
+
}
|
|
116
|
+
async function encodeImage(input, detectedFormat, settings, sourcePath) {
|
|
117
|
+
const outputFormat = settings.format ?? defaultOutputFormat(detectedFormat);
|
|
118
|
+
let source = input;
|
|
119
|
+
let sourceFormat = detectedFormat;
|
|
120
|
+
if (detectedFormat === UNSUPPORTED) {
|
|
121
|
+
source = await decodeHeic(input, sourcePath);
|
|
122
|
+
sourceFormat = "jpeg";
|
|
123
|
+
}
|
|
124
|
+
const animated = ANIMATED_INPUTS.has(sourceFormat) && ANIMATED_OUTPUTS.has(outputFormat);
|
|
125
|
+
let pipeline = sharp2(source, { animated, failOn: "error" });
|
|
126
|
+
if (!animated) pipeline = pipeline.autoOrient();
|
|
127
|
+
if (settings.resize !== void 0) {
|
|
128
|
+
pipeline = pipeline.resize({ width: settings.resize, withoutEnlargement: true });
|
|
129
|
+
}
|
|
130
|
+
if (outputFormat === "jpeg") pipeline = pipeline.flatten({ background: "#ffffff" });
|
|
131
|
+
const data = await pipeline.toFormat(outputFormat, getFormatOptions(settings.quality)[outputFormat]).toBuffer();
|
|
132
|
+
if (outputFormat === sourceFormat && detectedFormat !== UNSUPPORTED && data.length >= input.length) {
|
|
133
|
+
const fitsWidth = settings.resize === void 0 || ((await sharp2(input).metadata()).width ?? Number.POSITIVE_INFINITY) <= settings.resize;
|
|
134
|
+
if (fitsWidth) return { data: input, format: outputFormat, keptOriginal: true };
|
|
135
|
+
}
|
|
136
|
+
return { data, format: outputFormat, keptOriginal: false };
|
|
137
|
+
}
|
|
138
|
+
async function decodeHeic(input, sourcePath) {
|
|
139
|
+
if (process.platform !== "darwin") {
|
|
140
|
+
throw new Error(
|
|
141
|
+
"HEIC/HEIF photos are not supported on this system. Export them as JPEG first (or run bulki on macOS)."
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const tmpDir = await fs2.mkdtemp(path.join(os.tmpdir(), "bulki-heic-"));
|
|
145
|
+
try {
|
|
146
|
+
let inPath = sourcePath;
|
|
147
|
+
if (!inPath) {
|
|
148
|
+
inPath = path.join(tmpDir, "input.heic");
|
|
149
|
+
await fs2.writeFile(inPath, input);
|
|
150
|
+
}
|
|
151
|
+
const outPath = path.join(tmpDir, "converted.jpg");
|
|
152
|
+
await execFileAsync("sips", [
|
|
153
|
+
"-s",
|
|
154
|
+
"format",
|
|
155
|
+
"jpeg",
|
|
156
|
+
"-s",
|
|
157
|
+
"formatOptions",
|
|
158
|
+
"best",
|
|
159
|
+
inPath,
|
|
160
|
+
"--out",
|
|
161
|
+
outPath
|
|
162
|
+
]);
|
|
163
|
+
return await fs2.readFile(outPath);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
throw new Error(`Could not convert HEIC photo: ${err.message.split("\n")[0]}`);
|
|
166
|
+
} finally {
|
|
167
|
+
await fs2.rm(tmpDir, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/pipeline/optimize.ts
|
|
172
|
+
async function optimizeImage(job, settings) {
|
|
173
|
+
const start = performance.now();
|
|
174
|
+
const input = await fs3.readFile(job.input);
|
|
175
|
+
const result = await encodeImage(input, job.detectedFormat, settings, job.input);
|
|
176
|
+
await fs3.mkdir(path2.dirname(job.output), { recursive: true });
|
|
177
|
+
const tmp = `${job.output}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
178
|
+
try {
|
|
179
|
+
await fs3.writeFile(tmp, result.data);
|
|
180
|
+
await fs3.rename(tmp, job.output);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
await fs3.rm(tmp, { force: true });
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
inputPath: job.input,
|
|
187
|
+
outputPath: job.output,
|
|
188
|
+
originalSize: input.length,
|
|
189
|
+
newSize: result.data.length,
|
|
190
|
+
format: result.format,
|
|
191
|
+
duration: performance.now() - start,
|
|
192
|
+
keptOriginal: result.keptOriginal
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
async function runBatch(jobs, settings, concurrency, onFileDone, signal) {
|
|
196
|
+
const start = performance.now();
|
|
197
|
+
const errors = [];
|
|
198
|
+
const outcomes = await processPool(
|
|
199
|
+
jobs,
|
|
200
|
+
async (job) => {
|
|
201
|
+
if (signal?.aborted) return null;
|
|
202
|
+
try {
|
|
203
|
+
const result = await optimizeImage(job, settings);
|
|
204
|
+
onFileDone?.(job, result);
|
|
205
|
+
return result;
|
|
206
|
+
} catch (err) {
|
|
207
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
208
|
+
errors.push({ file: job.input, message: friendlyError(error) });
|
|
209
|
+
onFileDone?.(job, null, error);
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
concurrency
|
|
214
|
+
);
|
|
215
|
+
return {
|
|
216
|
+
results: outcomes.filter((r) => r !== null),
|
|
217
|
+
errors,
|
|
218
|
+
duration: performance.now() - start
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function friendlyError(err) {
|
|
222
|
+
const code = err.code;
|
|
223
|
+
if (code === "EACCES" || code === "EPERM") return "permission denied";
|
|
224
|
+
if (code === "ENOSPC") return "disk is full";
|
|
225
|
+
if (code === "ENOENT") return "file disappeared before it could be read";
|
|
226
|
+
const msg = err.message.split("\n")[0];
|
|
227
|
+
if (/VipsJpeg|premature end|corrupt|bad seek|unexpected end/i.test(msg))
|
|
228
|
+
return `damaged image (${msg})`;
|
|
229
|
+
return msg;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/pipeline/paths.ts
|
|
233
|
+
import fs4 from "fs";
|
|
234
|
+
import os2 from "os";
|
|
235
|
+
import path3 from "path";
|
|
236
|
+
import { fileURLToPath } from "url";
|
|
237
|
+
var isPosix = path3.sep === "/";
|
|
238
|
+
function cleanPathInput(raw) {
|
|
239
|
+
let value = raw.trim();
|
|
240
|
+
while (value.length >= 2 && (value.startsWith("'") && value.endsWith("'") || value.startsWith('"') && value.endsWith('"'))) {
|
|
241
|
+
value = value.slice(1, -1).trim();
|
|
242
|
+
}
|
|
243
|
+
if (value.startsWith("file://")) {
|
|
244
|
+
try {
|
|
245
|
+
value = fileURLToPath(value);
|
|
246
|
+
} catch {
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (isPosix) value = value.replace(/\\(.)/g, "$1");
|
|
250
|
+
return expandHome(value);
|
|
251
|
+
}
|
|
252
|
+
function expandHome(value) {
|
|
253
|
+
if (value === "~") return os2.homedir();
|
|
254
|
+
if (value.startsWith("~/") || value.startsWith("~\\"))
|
|
255
|
+
return path3.join(os2.homedir(), value.slice(2));
|
|
256
|
+
return value;
|
|
257
|
+
}
|
|
258
|
+
function splitShellWords(raw) {
|
|
259
|
+
const words = [];
|
|
260
|
+
let current = "";
|
|
261
|
+
let quote = null;
|
|
262
|
+
let hasWord = false;
|
|
263
|
+
for (let i = 0; i < raw.length; i++) {
|
|
264
|
+
const ch = raw[i];
|
|
265
|
+
if (quote) {
|
|
266
|
+
if (ch === quote) quote = null;
|
|
267
|
+
else if (ch === "\\" && quote === '"' && isPosix && i + 1 < raw.length) current += raw[++i];
|
|
268
|
+
else current += ch;
|
|
269
|
+
} else if (ch === "'" || ch === '"') {
|
|
270
|
+
quote = ch;
|
|
271
|
+
hasWord = true;
|
|
272
|
+
} else if (ch === "\\" && isPosix && i + 1 < raw.length) {
|
|
273
|
+
current += raw[++i];
|
|
274
|
+
hasWord = true;
|
|
275
|
+
} else if (/\s/.test(ch)) {
|
|
276
|
+
if (hasWord) words.push(current);
|
|
277
|
+
current = "";
|
|
278
|
+
hasWord = false;
|
|
279
|
+
} else {
|
|
280
|
+
current += ch;
|
|
281
|
+
hasWord = true;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (hasWord) words.push(current);
|
|
285
|
+
return words.map(expandHome);
|
|
286
|
+
}
|
|
287
|
+
function resolveUserPath(raw, cwd = process.cwd()) {
|
|
288
|
+
return path3.resolve(cwd, cleanPathInput(raw));
|
|
289
|
+
}
|
|
290
|
+
function parsePathInput(raw, cwd = process.cwd()) {
|
|
291
|
+
if (!raw || raw.trim() === "") return { ok: false, error: "Please enter a path" };
|
|
292
|
+
const single = resolveUserPath(raw, cwd);
|
|
293
|
+
if (fs4.existsSync(single)) return { ok: true, paths: [single] };
|
|
294
|
+
const words = splitShellWords(raw.trim());
|
|
295
|
+
if (words.length > 1) {
|
|
296
|
+
const resolved = words.map((w) => path3.resolve(cwd, w));
|
|
297
|
+
const missing = resolved.filter((p) => !fs4.existsSync(p));
|
|
298
|
+
if (missing.length === 0) return { ok: true, paths: resolved };
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
ok: false,
|
|
302
|
+
error: `Can't find "${single}". Check the spelling, or drag the folder into this window.`
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function isInside(child, parent) {
|
|
306
|
+
const rel = path3.relative(parent, child);
|
|
307
|
+
return rel === "" || !rel.startsWith("..") && !path3.isAbsolute(rel);
|
|
308
|
+
}
|
|
309
|
+
function samePath(a, b) {
|
|
310
|
+
const ra = path3.resolve(a);
|
|
311
|
+
const rb = path3.resolve(b);
|
|
312
|
+
return process.platform === "linux" ? ra === rb : ra.toLowerCase() === rb.toLowerCase();
|
|
313
|
+
}
|
|
314
|
+
function commonDir(dirs) {
|
|
315
|
+
if (dirs.length === 0) return process.cwd();
|
|
316
|
+
let common = path3.resolve(dirs[0]);
|
|
317
|
+
for (const d of dirs.slice(1)) {
|
|
318
|
+
while (!isInside(path3.resolve(d), common)) {
|
|
319
|
+
const parent = path3.dirname(common);
|
|
320
|
+
if (parent === common) return common;
|
|
321
|
+
common = parent;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return common;
|
|
325
|
+
}
|
|
326
|
+
function displayPath(absPath) {
|
|
327
|
+
const home = os2.homedir();
|
|
328
|
+
if (absPath === home) return "~";
|
|
329
|
+
if (absPath.startsWith(home + path3.sep)) return `~${absPath.slice(home.length)}`;
|
|
330
|
+
return absPath;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// src/validation/options.ts
|
|
334
|
+
import { availableParallelism } from "os";
|
|
335
|
+
import path4 from "path";
|
|
336
|
+
var DEFAULT_OUTPUT_FOLDER = "bulki-output";
|
|
337
|
+
var DEFAULTS = {
|
|
338
|
+
quality: 70,
|
|
339
|
+
output: `./${DEFAULT_OUTPUT_FOLDER}`
|
|
340
|
+
};
|
|
341
|
+
var OUTPUT_FORMATS = ["jpeg", "png", "webp", "gif", "avif"];
|
|
342
|
+
var VALID_FORMATS = ["jpeg", "jpg", "png", "webp", "gif", "avif"];
|
|
343
|
+
function defaultConcurrency() {
|
|
344
|
+
return Math.max(1, Math.min(availableParallelism(), 8));
|
|
345
|
+
}
|
|
346
|
+
function parseSize(input) {
|
|
347
|
+
const match = /^(\d+(?:\.\d+)?)\s*(b|kb|mb)$/i.exec(input.trim());
|
|
348
|
+
if (!match) {
|
|
349
|
+
throw new Error(`Invalid size format: "${input}". Expected format like 200kb, 1mb, 500b`);
|
|
350
|
+
}
|
|
351
|
+
const value = Number(match[1]);
|
|
352
|
+
const unit = match[2].toLowerCase();
|
|
353
|
+
if (unit === "b") return value;
|
|
354
|
+
if (unit === "kb") return value * 1024;
|
|
355
|
+
return value * 1024 * 1024;
|
|
356
|
+
}
|
|
357
|
+
function parseInteger(value) {
|
|
358
|
+
return /^\s*-?\d+\s*$/.test(value) ? Number(value) : Number.NaN;
|
|
359
|
+
}
|
|
360
|
+
function normalizeFormat(format) {
|
|
361
|
+
if (format === void 0 || format === "") return void 0;
|
|
362
|
+
const f = format.toLowerCase();
|
|
363
|
+
if (!VALID_FORMATS.includes(f)) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`Invalid format: "${format}" is not a supported format. Use one of: ${OUTPUT_FORMATS.join(", ")}`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
return f === "jpg" ? "jpeg" : f;
|
|
369
|
+
}
|
|
370
|
+
function validateEncodeSettings(raw) {
|
|
371
|
+
const quality = raw.quality ?? DEFAULTS.quality;
|
|
372
|
+
if (typeof quality !== "number" || !Number.isInteger(quality) || quality < 1 || quality > 100) {
|
|
373
|
+
throw new Error("Invalid quality: must be an integer between 1 and 100");
|
|
374
|
+
}
|
|
375
|
+
const format = normalizeFormat(raw.format);
|
|
376
|
+
const resize = raw.resize;
|
|
377
|
+
if (resize !== void 0) {
|
|
378
|
+
if (typeof resize !== "number" || !Number.isInteger(resize) || resize <= 0) {
|
|
379
|
+
throw new Error("Invalid resize: must be a positive integer (width in pixels)");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return { quality, format, resize };
|
|
383
|
+
}
|
|
384
|
+
function resolveOutputDir(raw, cwd = process.cwd()) {
|
|
385
|
+
const base = raw.destination !== void 0 ? path4.resolve(cwd, cleanPathInput(raw.destination)) : cwd;
|
|
386
|
+
if (raw.output !== void 0 && raw.output.trim() !== "") {
|
|
387
|
+
return path4.resolve(base, cleanPathInput(raw.output));
|
|
388
|
+
}
|
|
389
|
+
const folder = raw.outputFolder ?? DEFAULT_OUTPUT_FOLDER;
|
|
390
|
+
if (folder.includes("/") || folder.includes("\\") || folder.includes("..") || folder.trim() === "") {
|
|
391
|
+
throw new Error(
|
|
392
|
+
"Invalid outputFolder: must be a simple directory name. Use --output <path> to choose any folder."
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
return path4.resolve(base, folder);
|
|
396
|
+
}
|
|
397
|
+
function validateOptions(raw, cwd = process.cwd()) {
|
|
398
|
+
const settings = validateEncodeSettings(raw);
|
|
399
|
+
const concurrency = raw.concurrency ?? defaultConcurrency();
|
|
400
|
+
if (typeof concurrency !== "number" || !Number.isInteger(concurrency) || concurrency < 1 || concurrency > 32) {
|
|
401
|
+
throw new Error("Invalid concurrency: must be an integer between 1 and 32");
|
|
402
|
+
}
|
|
403
|
+
let maxSize;
|
|
404
|
+
if (raw.maxSize !== void 0) {
|
|
405
|
+
maxSize = parseSize(raw.maxSize);
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
...settings,
|
|
409
|
+
output: resolveOutputDir(raw, cwd),
|
|
410
|
+
concurrency,
|
|
411
|
+
exclude: raw.exclude ?? [],
|
|
412
|
+
dryRun: raw.dryRun ?? false,
|
|
413
|
+
verbose: raw.verbose ?? false,
|
|
414
|
+
json: raw.json ?? false,
|
|
415
|
+
maxSize
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/pipeline/plan.ts
|
|
420
|
+
import fs6 from "fs/promises";
|
|
421
|
+
import path6 from "path";
|
|
422
|
+
|
|
423
|
+
// src/pipeline/discover.ts
|
|
424
|
+
import fs5 from "fs/promises";
|
|
425
|
+
import path5 from "path";
|
|
426
|
+
async function* discoverFiles(dirPath, exclude = []) {
|
|
427
|
+
const entries = await fs5.readdir(dirPath, { withFileTypes: true });
|
|
428
|
+
for (const entry of entries) {
|
|
429
|
+
if (entry.name.startsWith(".")) continue;
|
|
430
|
+
if (entry.isSymbolicLink()) continue;
|
|
431
|
+
const fullPath = path5.resolve(dirPath, entry.name);
|
|
432
|
+
if (entry.isDirectory()) {
|
|
433
|
+
if (exclude.includes(entry.name) || entry.name === DEFAULT_OUTPUT_FOLDER) continue;
|
|
434
|
+
try {
|
|
435
|
+
yield* discoverFiles(fullPath, exclude);
|
|
436
|
+
} catch {
|
|
437
|
+
}
|
|
438
|
+
} else if (entry.isFile()) {
|
|
439
|
+
yield fullPath;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/pipeline/plan.ts
|
|
445
|
+
var PlanError = class extends Error {
|
|
446
|
+
};
|
|
447
|
+
async function scanInputs(inputs, exclude = []) {
|
|
448
|
+
if (inputs.length === 0) throw new PlanError("No input path given");
|
|
449
|
+
const candidates = [];
|
|
450
|
+
const dirs = [];
|
|
451
|
+
for (const input of inputs) {
|
|
452
|
+
const stat = await fs6.stat(input).catch(() => null);
|
|
453
|
+
if (!stat) throw new PlanError(`Path not found: ${input}`);
|
|
454
|
+
if (stat.isDirectory()) {
|
|
455
|
+
dirs.push(input);
|
|
456
|
+
for await (const file of discoverFiles(input, exclude)) candidates.push(file);
|
|
457
|
+
} else {
|
|
458
|
+
dirs.push(path6.dirname(input));
|
|
459
|
+
candidates.push(input);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const unique = [...new Set(candidates)];
|
|
463
|
+
const formats = await processPool(unique, detectFormat, 32);
|
|
464
|
+
const images = unique.map((file, i) => ({ file, detectedFormat: formats[i] })).filter((img) => img.detectedFormat !== null).sort((a, b) => a.file.localeCompare(b.file));
|
|
465
|
+
return { inputs, sourceRoot: commonDir(dirs), images };
|
|
466
|
+
}
|
|
467
|
+
function assignOutputs(scan, output, format) {
|
|
468
|
+
if (samePath(output, scan.sourceRoot) || scan.inputs.some((i) => samePath(i, output))) {
|
|
469
|
+
throw new PlanError(
|
|
470
|
+
"The output folder is the same as the source folder \u2014 that would overwrite your originals. Choose a different output folder."
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
const jobs = [];
|
|
474
|
+
const skipped = [];
|
|
475
|
+
const taken = /* @__PURE__ */ new Set();
|
|
476
|
+
for (const { file, detectedFormat } of scan.images) {
|
|
477
|
+
if (isInside(file, output)) continue;
|
|
478
|
+
const target = format ?? defaultOutputFormat(detectedFormat);
|
|
479
|
+
const parsed = path6.parse(path6.relative(scan.sourceRoot, file));
|
|
480
|
+
const keepExt = format === void 0 && detectedFormat !== UNSUPPORTED;
|
|
481
|
+
const ext = keepExt ? parsed.ext : extensionFor(target);
|
|
482
|
+
let out = path6.join(output, parsed.dir, `${parsed.name}${ext}`);
|
|
483
|
+
if (taken.has(out.toLowerCase())) {
|
|
484
|
+
out = path6.join(
|
|
485
|
+
output,
|
|
486
|
+
parsed.dir,
|
|
487
|
+
`${parsed.name}-${parsed.ext.slice(1).toLowerCase()}${ext}`
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
if (taken.has(out.toLowerCase())) {
|
|
491
|
+
skipped.push({ file, reason: "another image already produces the same output name" });
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
taken.add(out.toLowerCase());
|
|
495
|
+
jobs.push({ input: file, detectedFormat, output: out });
|
|
496
|
+
}
|
|
497
|
+
return { sourceRoot: scan.sourceRoot, output, jobs, skipped };
|
|
498
|
+
}
|
|
499
|
+
async function planJobs({ inputs, output, format, exclude = [] }) {
|
|
500
|
+
return assignOutputs(await scanInputs(inputs, exclude), output, format);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// src/commands/hook.ts
|
|
504
|
+
import { execFile as execFile2 } from "child_process";
|
|
505
|
+
import fs7 from "fs/promises";
|
|
506
|
+
import path7 from "path";
|
|
507
|
+
import { promisify as promisify2 } from "util";
|
|
508
|
+
import { Command } from "commander";
|
|
509
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
510
|
+
var HOOK_MARKER = "# bulki-pre-commit-hook";
|
|
511
|
+
var HOOK_END_MARKER = "# bulki-pre-commit-hook-end";
|
|
512
|
+
var HOOK_SCRIPT = `#!/bin/sh
|
|
513
|
+
${HOOK_MARKER}
|
|
514
|
+
# Auto-optimize staged images before commit
|
|
515
|
+
# Installed by: bulki hook install
|
|
516
|
+
# Remove with: bulki hook uninstall
|
|
517
|
+
|
|
518
|
+
STAGED=$(git diff --cached --name-only -z --diff-filter=ACM | tr '\\0' '\\n' | grep -iE '\\.(jpe?g|png|webp|gif|avif)$')
|
|
519
|
+
|
|
520
|
+
if [ -z "$STAGED" ]; then
|
|
521
|
+
exit 0
|
|
522
|
+
fi
|
|
523
|
+
|
|
524
|
+
echo "bulki: optimizing staged images..."
|
|
525
|
+
|
|
526
|
+
# Prefer a local/global install; npx on every image is slow
|
|
527
|
+
if command -v bulki >/dev/null 2>&1; then
|
|
528
|
+
BULKI="bulki"
|
|
529
|
+
elif [ -x ./node_modules/.bin/bulki ]; then
|
|
530
|
+
BULKI="./node_modules/.bin/bulki"
|
|
531
|
+
else
|
|
532
|
+
BULKI="npx --yes @puntoycoma/bulki"
|
|
533
|
+
fi
|
|
534
|
+
|
|
535
|
+
tmpdir=$(mktemp -d)
|
|
536
|
+
trap 'rm -rf "$tmpdir"' EXIT
|
|
537
|
+
|
|
538
|
+
echo "$STAGED" | while IFS= read -r img; do
|
|
539
|
+
if [ -f "$img" ]; then
|
|
540
|
+
ext="\${img##*.}"
|
|
541
|
+
case "$(echo "$ext" | tr '[:upper:]' '[:lower:]')" in
|
|
542
|
+
jpg|jpeg) fmt="jpeg" ;;
|
|
543
|
+
png) fmt="png" ;;
|
|
544
|
+
webp) fmt="webp" ;;
|
|
545
|
+
gif) fmt="gif" ;;
|
|
546
|
+
avif) fmt="avif" ;;
|
|
547
|
+
*) continue ;;
|
|
548
|
+
esac
|
|
549
|
+
tmpfile="$tmpdir/$(basename "$img")"
|
|
550
|
+
if $BULKI pipe -f "$fmt" -q 80 < "$img" > "$tmpfile" 2>/dev/null; then
|
|
551
|
+
if [ -s "$tmpfile" ]; then
|
|
552
|
+
mv "$tmpfile" "$img"
|
|
553
|
+
git add "$img"
|
|
554
|
+
fi
|
|
555
|
+
fi
|
|
556
|
+
fi
|
|
557
|
+
done
|
|
558
|
+
|
|
559
|
+
echo "bulki: done"
|
|
560
|
+
${HOOK_END_MARKER}
|
|
561
|
+
`;
|
|
562
|
+
var HookError = class extends Error {
|
|
563
|
+
};
|
|
564
|
+
async function installHook(cwd = process.cwd()) {
|
|
565
|
+
const hooksDir = await findHooksDir(cwd);
|
|
566
|
+
if (!hooksDir) {
|
|
567
|
+
throw new HookError("Not a git repository. Run this from inside a git project.");
|
|
568
|
+
}
|
|
569
|
+
const hookPath = path7.join(hooksDir, "pre-commit");
|
|
570
|
+
await fs7.mkdir(hooksDir, { recursive: true });
|
|
571
|
+
try {
|
|
572
|
+
const existing = await fs7.readFile(hookPath, "utf-8");
|
|
573
|
+
if (existing.includes(HOOK_MARKER)) {
|
|
574
|
+
console.log("bulki hook is already installed.");
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
await fs7.appendFile(hookPath, `
|
|
578
|
+
${HOOK_SCRIPT}`);
|
|
579
|
+
await fs7.chmod(hookPath, 493);
|
|
580
|
+
console.log("bulki hook appended to existing pre-commit hook.");
|
|
581
|
+
} catch {
|
|
582
|
+
await fs7.writeFile(hookPath, HOOK_SCRIPT, { mode: 493 });
|
|
583
|
+
console.log("bulki pre-commit hook installed.");
|
|
584
|
+
}
|
|
585
|
+
console.log("Staged images will be auto-optimized before each commit.");
|
|
586
|
+
}
|
|
587
|
+
async function uninstallHook(cwd = process.cwd()) {
|
|
588
|
+
const hooksDir = await findHooksDir(cwd);
|
|
589
|
+
if (!hooksDir) {
|
|
590
|
+
throw new HookError("Not a git repository.");
|
|
591
|
+
}
|
|
592
|
+
const hookPath = path7.join(hooksDir, "pre-commit");
|
|
593
|
+
try {
|
|
594
|
+
const content = await fs7.readFile(hookPath, "utf-8");
|
|
595
|
+
if (!content.includes(HOOK_MARKER)) {
|
|
596
|
+
console.log("No bulki hook found in pre-commit.");
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const lines = content.split("\n");
|
|
600
|
+
const markerIndex = lines.findIndex((l) => l.includes(HOOK_MARKER));
|
|
601
|
+
let endIndex = lines.findIndex((l) => l.includes(HOOK_END_MARKER));
|
|
602
|
+
if (endIndex === -1) endIndex = lines.length - 1;
|
|
603
|
+
const before = lines.slice(0, markerIndex).join("\n");
|
|
604
|
+
const after = lines.slice(endIndex + 1).join("\n");
|
|
605
|
+
const trimmed = `${before}
|
|
606
|
+
${after}`.trim();
|
|
607
|
+
if (trimmed === "" || trimmed === "#!/bin/sh") {
|
|
608
|
+
await fs7.unlink(hookPath);
|
|
609
|
+
console.log("bulki pre-commit hook removed.");
|
|
610
|
+
} else {
|
|
611
|
+
await fs7.writeFile(hookPath, `${trimmed}
|
|
612
|
+
`, { mode: 493 });
|
|
613
|
+
console.log("bulki section removed from pre-commit hook.");
|
|
614
|
+
}
|
|
615
|
+
} catch {
|
|
616
|
+
console.log("No pre-commit hook found.");
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
var hookCommand = new Command("hook").description(
|
|
620
|
+
"Manage git pre-commit hook for auto image optimization"
|
|
621
|
+
);
|
|
622
|
+
hookCommand.command("install").description("Install pre-commit hook to optimize images on commit").action(() => runOrExit(installHook));
|
|
623
|
+
hookCommand.command("uninstall").description("Remove the bulki pre-commit hook").action(() => runOrExit(uninstallHook));
|
|
624
|
+
async function runOrExit(fn) {
|
|
625
|
+
try {
|
|
626
|
+
await fn();
|
|
627
|
+
} catch (err) {
|
|
628
|
+
console.error(`error: ${err.message}`);
|
|
629
|
+
process.exit(1);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async function findHooksDir(startPath) {
|
|
633
|
+
try {
|
|
634
|
+
const { stdout } = await execFileAsync2("git", ["rev-parse", "--git-path", "hooks"], {
|
|
635
|
+
cwd: startPath
|
|
636
|
+
});
|
|
637
|
+
return path7.resolve(startPath, stdout.trim());
|
|
638
|
+
} catch {
|
|
639
|
+
}
|
|
640
|
+
let current = path7.resolve(startPath);
|
|
641
|
+
while (true) {
|
|
642
|
+
const gitPath = path7.join(current, ".git");
|
|
643
|
+
const stat = await fs7.stat(gitPath).catch(() => null);
|
|
644
|
+
if (stat?.isDirectory()) return path7.join(gitPath, "hooks");
|
|
645
|
+
const parent = path7.dirname(current);
|
|
646
|
+
if (parent === current) return null;
|
|
647
|
+
current = parent;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// src/commands/watch.ts
|
|
652
|
+
import fs8 from "fs";
|
|
653
|
+
import path9 from "path";
|
|
654
|
+
import { Command as Command2 } from "commander";
|
|
655
|
+
|
|
656
|
+
// src/output/results.ts
|
|
657
|
+
import path8 from "path";
|
|
658
|
+
function calcReduction(originalSize, newSize) {
|
|
659
|
+
return originalSize > 0 ? Math.round((1 - newSize / originalSize) * 100) : 0;
|
|
660
|
+
}
|
|
661
|
+
function formatFileResult(result) {
|
|
662
|
+
const filename = path8.basename(result.inputPath);
|
|
663
|
+
const original = formatBytes(result.originalSize);
|
|
664
|
+
const newSize = formatBytes(result.newSize);
|
|
665
|
+
const reduction = calcReduction(result.originalSize, result.newSize);
|
|
666
|
+
const pct = reduction < 0 ? `+${Math.abs(reduction)}%` : `${reduction}%`;
|
|
667
|
+
const note = result.keptOriginal ? " \u2014 already optimal, copied as-is" : "";
|
|
668
|
+
return `${filename} ${original} -> ${newSize} (${pct})${note}`;
|
|
669
|
+
}
|
|
670
|
+
function buildJsonOutput(results, errors, totalDuration, outputDir) {
|
|
671
|
+
const totalOriginal = results.reduce((sum, r) => sum + r.originalSize, 0);
|
|
672
|
+
const totalNew = results.reduce((sum, r) => sum + r.newSize, 0);
|
|
673
|
+
return {
|
|
674
|
+
...outputDir ? { output: outputDir } : {},
|
|
675
|
+
files: results.map((r) => ({
|
|
676
|
+
input: r.inputPath,
|
|
677
|
+
output: r.outputPath,
|
|
678
|
+
originalSize: r.originalSize,
|
|
679
|
+
newSize: r.newSize,
|
|
680
|
+
reduction: calcReduction(r.originalSize, r.newSize),
|
|
681
|
+
format: r.format,
|
|
682
|
+
duration: r.duration,
|
|
683
|
+
...r.keptOriginal ? { keptOriginal: true } : {}
|
|
684
|
+
})),
|
|
685
|
+
summary: {
|
|
686
|
+
totalFiles: results.length,
|
|
687
|
+
totalOriginalSize: totalOriginal,
|
|
688
|
+
totalNewSize: totalNew,
|
|
689
|
+
totalReduction: calcReduction(totalOriginal, totalNew),
|
|
690
|
+
duration: totalDuration,
|
|
691
|
+
errors: errors.length
|
|
692
|
+
},
|
|
693
|
+
errors
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
function formatSummary(results, errors, totalDuration) {
|
|
697
|
+
const totalOriginal = results.reduce((sum, r) => sum + r.originalSize, 0);
|
|
698
|
+
const totalNew = results.reduce((sum, r) => sum + r.newSize, 0);
|
|
699
|
+
const reduction = calcReduction(totalOriginal, totalNew);
|
|
700
|
+
const duration = `${(totalDuration / 1e3).toFixed(1)}s`;
|
|
701
|
+
const fileCount = results.length;
|
|
702
|
+
let summary = `${fileCount} ${fileCount === 1 ? "file" : "files"} | ${formatBytes(totalOriginal)} -> ${formatBytes(totalNew)} (${reduction}%) | ${duration}`;
|
|
703
|
+
if (errors.length > 0) {
|
|
704
|
+
summary += ` | ${errors.length} ${errors.length === 1 ? "error" : "errors"}`;
|
|
705
|
+
}
|
|
706
|
+
return summary;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// src/commands/watch.ts
|
|
710
|
+
var SETTLE_MS = 400;
|
|
711
|
+
async function startWatch(dir, opts) {
|
|
712
|
+
const root = path9.resolve(dir);
|
|
713
|
+
const stat = await fs8.promises.stat(root).catch(() => null);
|
|
714
|
+
if (!stat) throw new Error(`Path not found: ${root}`);
|
|
715
|
+
if (!stat.isDirectory()) throw new Error(`Watch path must be a folder: ${root}`);
|
|
716
|
+
if (samePath(root, opts.output)) {
|
|
717
|
+
throw new Error(
|
|
718
|
+
"The output folder is the same as the watched folder. Choose a different output folder."
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
const log = opts.log ?? console.log;
|
|
722
|
+
const logError = opts.logError ?? console.error;
|
|
723
|
+
const settings = {
|
|
724
|
+
quality: opts.quality,
|
|
725
|
+
format: opts.format,
|
|
726
|
+
resize: opts.resize
|
|
727
|
+
};
|
|
728
|
+
const timers = /* @__PURE__ */ new Map();
|
|
729
|
+
const queue = [];
|
|
730
|
+
const queued = /* @__PURE__ */ new Set();
|
|
731
|
+
let active = 0;
|
|
732
|
+
const maxActive = defaultConcurrency();
|
|
733
|
+
const pump = () => {
|
|
734
|
+
while (active < maxActive && queue.length > 0) {
|
|
735
|
+
const file = queue.shift();
|
|
736
|
+
queued.delete(file);
|
|
737
|
+
active++;
|
|
738
|
+
processFile(file).finally(() => {
|
|
739
|
+
active--;
|
|
740
|
+
pump();
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
const processFile = async (file) => {
|
|
745
|
+
const detectedFormat = await detectFormat(file);
|
|
746
|
+
if (!detectedFormat) return;
|
|
747
|
+
const rel = path9.relative(root, file);
|
|
748
|
+
const parsed = path9.parse(rel);
|
|
749
|
+
const ext = settings.format ? extensionFor(settings.format) : parsed.ext;
|
|
750
|
+
const target = settings.format ?? defaultOutputFormat(detectedFormat);
|
|
751
|
+
const output = path9.join(
|
|
752
|
+
opts.output,
|
|
753
|
+
parsed.dir,
|
|
754
|
+
`${parsed.name}${detectedFormat === "unsupported" && !settings.format ? extensionFor(target) : ext}`
|
|
755
|
+
);
|
|
756
|
+
try {
|
|
757
|
+
const result = await optimizeImage({ input: file, detectedFormat, output }, settings);
|
|
758
|
+
log(formatFileResult(result));
|
|
759
|
+
} catch (err) {
|
|
760
|
+
logError(`\u2717 ${rel}: ${friendlyError(err)}`);
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
const schedule = (file) => {
|
|
764
|
+
clearTimeout(timers.get(file));
|
|
765
|
+
timers.set(
|
|
766
|
+
file,
|
|
767
|
+
setTimeout(async () => {
|
|
768
|
+
timers.delete(file);
|
|
769
|
+
const s = await fs8.promises.stat(file).catch(() => null);
|
|
770
|
+
if (!s?.isFile() || queued.has(file)) return;
|
|
771
|
+
queue.push(file);
|
|
772
|
+
queued.add(file);
|
|
773
|
+
pump();
|
|
774
|
+
}, SETTLE_MS)
|
|
775
|
+
);
|
|
776
|
+
};
|
|
777
|
+
const watcher = fs8.watch(root, { recursive: true }, (_event, filename) => {
|
|
778
|
+
if (!filename) return;
|
|
779
|
+
const file = path9.resolve(root, filename.toString());
|
|
780
|
+
const rel = path9.relative(root, file);
|
|
781
|
+
if (isInside(file, opts.output)) return;
|
|
782
|
+
if (rel.split(path9.sep).some((part) => part.startsWith(".") || part === DEFAULT_OUTPUT_FOLDER))
|
|
783
|
+
return;
|
|
784
|
+
if (file.endsWith(".tmp")) return;
|
|
785
|
+
schedule(file);
|
|
786
|
+
});
|
|
787
|
+
watcher.on("error", (err) => logError(`Watcher error: ${err.message}`));
|
|
788
|
+
return {
|
|
789
|
+
output: opts.output,
|
|
790
|
+
close() {
|
|
791
|
+
watcher.close();
|
|
792
|
+
for (const t of timers.values()) clearTimeout(t);
|
|
793
|
+
timers.clear();
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
var watchCommand = new Command2("watch").description("Watch a folder and auto-optimize images as they appear").argument("<path>", "Folder to watch").option("-o, --output <dir>", "Folder where optimized images are written", DEFAULTS.output).option("-q, --quality <n>", "Output quality (1-100)", parseInteger, DEFAULTS.quality).option("-f, --format <fmt>", "Convert to jpeg, png, webp, gif or avif (default: keep format)").option("-r, --resize <width>", "Max width in px (never enlarges)", parseInteger).option("-d, --destination <dir>", "Deprecated: base folder; output goes to <dir>/bulki-output").option("--output-folder <name>", "Deprecated: subfolder name inside --destination").action(async (dirPath, opts, cmd) => {
|
|
798
|
+
try {
|
|
799
|
+
const settings = validateEncodeSettings(opts);
|
|
800
|
+
const outputFromCli = cmd.getOptionValueSource("output") === "cli";
|
|
801
|
+
const root = resolveUserPath(dirPath);
|
|
802
|
+
const output = resolveOutputDir({ ...opts, output: outputFromCli ? opts.output : void 0 });
|
|
803
|
+
const handle = await startWatch(root, { ...settings, output });
|
|
804
|
+
console.log(`Watching ${displayPath(root)} \u2192 ${displayPath(output)}`);
|
|
805
|
+
console.log("Press Ctrl+C to stop.\n");
|
|
806
|
+
process.once("SIGINT", () => {
|
|
807
|
+
handle.close();
|
|
808
|
+
console.log("\nWatch stopped.");
|
|
809
|
+
process.exit(0);
|
|
810
|
+
});
|
|
811
|
+
} catch (err) {
|
|
812
|
+
console.error(`error: ${err.message}`);
|
|
813
|
+
process.exit(1);
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
export {
|
|
818
|
+
formatBytes,
|
|
819
|
+
formatFileResult,
|
|
820
|
+
buildJsonOutput,
|
|
821
|
+
formatSummary,
|
|
822
|
+
UNSUPPORTED,
|
|
823
|
+
detectFormat,
|
|
824
|
+
encodeImage,
|
|
825
|
+
runBatch,
|
|
826
|
+
cleanPathInput,
|
|
827
|
+
resolveUserPath,
|
|
828
|
+
parsePathInput,
|
|
829
|
+
isInside,
|
|
830
|
+
samePath,
|
|
831
|
+
displayPath,
|
|
832
|
+
DEFAULT_OUTPUT_FOLDER,
|
|
833
|
+
DEFAULTS,
|
|
834
|
+
defaultConcurrency,
|
|
835
|
+
parseSize,
|
|
836
|
+
parseInteger,
|
|
837
|
+
validateEncodeSettings,
|
|
838
|
+
resolveOutputDir,
|
|
839
|
+
validateOptions,
|
|
840
|
+
PlanError,
|
|
841
|
+
scanInputs,
|
|
842
|
+
assignOutputs,
|
|
843
|
+
planJobs,
|
|
844
|
+
HookError,
|
|
845
|
+
installHook,
|
|
846
|
+
uninstallHook,
|
|
847
|
+
hookCommand,
|
|
848
|
+
startWatch,
|
|
849
|
+
watchCommand
|
|
850
|
+
};
|
|
851
|
+
//# sourceMappingURL=chunk-YUAKNSBH.js.map
|