bytecut 0.0.0-stage → 0.1.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.
@@ -0,0 +1,620 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ AnalysisCache,
4
+ collectAssets,
5
+ engineFingerprint,
6
+ sortFindings,
7
+ walkFiles,
8
+ withVideos
9
+ } from "./chunk-P5PFI3AO.js";
10
+ import {
11
+ MANIFEST_FILE,
12
+ parseManifest
13
+ } from "./chunk-TCQFQ354.js";
14
+ import {
15
+ ANIMATED_GIF_REASON,
16
+ buildPlan,
17
+ classify,
18
+ classifyVideo,
19
+ sha256
20
+ } from "./chunk-XUBQWBWH.js";
21
+ import {
22
+ isFastStart,
23
+ toPosix
24
+ } from "./chunk-QOG2F4SF.js";
25
+ import {
26
+ BUILTIN_POLICY,
27
+ getImagePreset,
28
+ pickAv1Encoder,
29
+ rootPreset
30
+ } from "./chunk-3PF6BBFK.js";
31
+ import {
32
+ SafeFs
33
+ } from "./chunk-IZ4JEL3Q.js";
34
+ import {
35
+ ALL_RULES,
36
+ checkCache,
37
+ checkVideoCache,
38
+ matchesGlob,
39
+ savingOf
40
+ } from "./chunk-FY53B4NB.js";
41
+
42
+ // src/core/commands/audit.ts
43
+ import { stat as stat2 } from "fs/promises";
44
+ import { resolve } from "path";
45
+
46
+ // src/core/audit/estimate.ts
47
+ var TRIAL_MAX_WIDTH = 640;
48
+ var SIZE_EXPONENT = 1.6;
49
+ function estimateKey(sha2562, preset, out, width, encoder) {
50
+ return [
51
+ sha2562,
52
+ preset,
53
+ out.format,
54
+ width,
55
+ out.lossless ? "L" : `q${out.quality}`,
56
+ out.effort,
57
+ encoder
58
+ ].join("|");
59
+ }
60
+ async function estimateItem(item, readData, deps) {
61
+ if (item.skip || item.outputs.length === 0) return null;
62
+ const encoders = deps.engine.encoders();
63
+ const perFormat = [];
64
+ const formats = [...new Set(item.outputs.map((o) => o.format))];
65
+ for (const format of formats) {
66
+ const outs = item.outputs.filter((o) => o.format === format).sort((a, b) => a.width - b.width);
67
+ const largest = outs[outs.length - 1];
68
+ const within = outs.filter((o) => o.width <= TRIAL_MAX_WIDTH);
69
+ const trial = within.length > 0 ? within[within.length - 1] : { ...outs[0], width: TRIAL_MAX_WIDTH };
70
+ const key = estimateKey(
71
+ item.sourceSha256,
72
+ item.preset,
73
+ trial,
74
+ trial.width,
75
+ encoders[format] ?? format
76
+ );
77
+ let bytes = deps.cache.getEstimate(key)?.bytes;
78
+ if (bytes === void 0) {
79
+ const encoded = await deps.engine.encode(await readData(), trial);
80
+ bytes = encoded.data.byteLength;
81
+ deps.cache.setEstimate(key, { bytes, basis: "exact" });
82
+ }
83
+ const exact = trial.width === largest.width;
84
+ perFormat.push({
85
+ format,
86
+ width: largest.width,
87
+ bytes: exact ? bytes : Math.round(bytes * (largest.width / trial.width) ** SIZE_EXPONENT),
88
+ basis: exact ? "exact" : "extrapolated"
89
+ });
90
+ }
91
+ const best = perFormat.reduce((a, b) => b.bytes < a.bytes ? b : a);
92
+ return { best, perFormat };
93
+ }
94
+
95
+ // src/core/scan/references.ts
96
+ import { readFile, stat } from "fs/promises";
97
+ import { basename, extname } from "path";
98
+ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
99
+ ".html",
100
+ ".htm",
101
+ ".css",
102
+ ".scss",
103
+ ".sass",
104
+ ".less",
105
+ ".js",
106
+ ".jsx",
107
+ ".mjs",
108
+ ".cjs",
109
+ ".ts",
110
+ ".tsx",
111
+ ".mts",
112
+ ".vue",
113
+ ".svelte",
114
+ ".astro",
115
+ ".md",
116
+ ".mdx",
117
+ ".json",
118
+ ".yaml",
119
+ ".yml",
120
+ ".xml",
121
+ ".php",
122
+ ".twig",
123
+ ".liquid",
124
+ ".njk",
125
+ ".ejs",
126
+ ".hbs",
127
+ ".erb",
128
+ ".txt"
129
+ ]);
130
+ var SKIPPED_NAMES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb"]);
131
+ var MAX_FILE_BYTES = 1024 * 1024;
132
+ var MAX_TOTAL_BYTES = 64 * 1024 * 1024;
133
+ var IMAGE_REFERENCE = /[\w@%+~.\-/\\]+\.(?:jpe?g|png|webp|avif|gif|svg)\b/gi;
134
+ async function scanReferences(options) {
135
+ const names = /* @__PURE__ */ new Set();
136
+ let filesScanned = 0;
137
+ let total = 0;
138
+ let truncated = false;
139
+ await walkFiles(options.cwd, options, async (path) => {
140
+ if (truncated) return;
141
+ if (!TEXT_EXTENSIONS.has(extname(path).toLowerCase()) || SKIPPED_NAMES.has(basename(path))) {
142
+ return;
143
+ }
144
+ const info = await stat(path).catch(() => null);
145
+ if (!info || info.size > MAX_FILE_BYTES) return;
146
+ if (total + info.size > MAX_TOTAL_BYTES) {
147
+ truncated = true;
148
+ return;
149
+ }
150
+ total += info.size;
151
+ let text;
152
+ try {
153
+ text = await readFile(path, "utf8");
154
+ } catch {
155
+ return;
156
+ }
157
+ filesScanned++;
158
+ for (const match of text.matchAll(IMAGE_REFERENCE)) {
159
+ let name = match[0].split(/[\\/]/).pop().toLowerCase();
160
+ try {
161
+ name = decodeURIComponent(name);
162
+ } catch {
163
+ }
164
+ names.add(name);
165
+ }
166
+ });
167
+ return { names, filesScanned, truncated };
168
+ }
169
+
170
+ // src/core/commands/audit.ts
171
+ import { relative } from "path";
172
+
173
+ // src/core/audit/estimate-video.ts
174
+ var EXACT_MAX_SECONDS = 4;
175
+ var SAMPLE_SECONDS = 1.5;
176
+ var SAMPLE_POINTS = [0.2, 0.5, 0.8];
177
+ function encoderFor(codec, caps) {
178
+ return codec === "h264" ? caps.libx264 ? "libx264" : null : pickAv1Encoder(caps);
179
+ }
180
+ var probeOf = (item) => ({
181
+ durationSec: item.durationSec,
182
+ width: item.sourceWidth,
183
+ height: item.sourceHeight,
184
+ fps: item.fps,
185
+ videoCodec: item.videoCodec,
186
+ pixelFormat: null,
187
+ bitrate: null,
188
+ hasAudio: item.hasAudio,
189
+ audioCodec: null,
190
+ container: "unknown"
191
+ });
192
+ function sampleStarts(durationSec) {
193
+ if (durationSec <= EXACT_MAX_SECONDS) return [{ startSec: 0, durationSec }];
194
+ return SAMPLE_POINTS.map((p) => ({
195
+ startSec: Math.max(
196
+ 0,
197
+ Math.min(durationSec - SAMPLE_SECONDS, durationSec * p - SAMPLE_SECONDS / 2)
198
+ ),
199
+ durationSec: SAMPLE_SECONDS
200
+ }));
201
+ }
202
+ function audioBytes(out, item) {
203
+ if (out.audio !== "keep" || !item.hasAudio) return 0;
204
+ const kbps = Number(out.audioBitrate.replace(/k$/, ""));
205
+ return Math.round(kbps * 1e3 * item.durationSec / 8);
206
+ }
207
+ async function estimateVideo(item, deps, sourcePath) {
208
+ if (item.skip) return null;
209
+ const caps = await deps.engine.capabilities();
210
+ if (!caps) return null;
211
+ const probe = probeOf(item);
212
+ const starts = sampleStarts(item.durationSec);
213
+ const exact = starts.length === 1 && starts[0].durationSec >= item.durationSec;
214
+ const perCodec = [];
215
+ for (const out of item.outputs) {
216
+ if (out.role !== "video") continue;
217
+ const encoder = encoderFor(out.codec, caps);
218
+ if (!encoder) continue;
219
+ const key = [
220
+ "video",
221
+ item.sourceSha256,
222
+ out.codec,
223
+ encoder,
224
+ out.crf,
225
+ out.speed,
226
+ out.maxWidth ?? "-",
227
+ out.maxFps ?? "-",
228
+ deps.ffmpegVersion,
229
+ exact ? "exact" : "sampled"
230
+ ].join("|");
231
+ let streamBytes = deps.cache.getEstimate(key)?.bytes;
232
+ if (streamBytes === void 0) {
233
+ let total = 0;
234
+ let seconds = 0;
235
+ for (const s of starts) {
236
+ total += await deps.engine.sampleBytes(
237
+ {
238
+ source: sourcePath,
239
+ encoder,
240
+ crf: out.crf,
241
+ speed: out.speed,
242
+ maxWidth: out.maxWidth,
243
+ maxFps: out.maxFps,
244
+ startSec: s.startSec,
245
+ durationSec: s.durationSec,
246
+ probe
247
+ },
248
+ deps.signal
249
+ );
250
+ seconds += s.durationSec;
251
+ }
252
+ streamBytes = Math.round(seconds > 0 ? total / seconds * item.durationSec : total);
253
+ deps.cache.setEstimate(key, { bytes: streamBytes, basis: exact ? "exact" : "extrapolated" });
254
+ }
255
+ perCodec.push({
256
+ codec: out.codec,
257
+ bytes: streamBytes + audioBytes(out, item),
258
+ basis: exact ? "exact" : "extrapolated"
259
+ });
260
+ }
261
+ if (perCodec.length === 0) return null;
262
+ const best = perCodec.reduce((a, b) => b.bytes < a.bytes ? b : a);
263
+ return { best, perCodec };
264
+ }
265
+
266
+ // src/core/commands/audit.ts
267
+ import { basename as basename2, dirname, extname as extname2 } from "path";
268
+ async function audit(options) {
269
+ const { projectRoot, engine, svgEngine } = options;
270
+ const outDir = resolve(projectRoot, options.outDir);
271
+ const notes = [];
272
+ const found = await collectAssets(options.inputs, {
273
+ cwd: projectRoot,
274
+ outDir,
275
+ ...options.exclude ? { exclude: options.exclude } : {}
276
+ });
277
+ const policy = options.policy ?? BUILTIN_POLICY;
278
+ const fs = await SafeFs.create({
279
+ projectRoot,
280
+ outDir,
281
+ sourceFiles: [...found.files, ...found.svgs, ...found.videos],
282
+ sourceDirs: found.sourceDirs
283
+ });
284
+ const videoVersions = options.videoEngine ? await options.videoEngine.versions() : {};
285
+ const cache = await AnalysisCache.load(
286
+ fs,
287
+ `${engineFingerprint(engine, videoVersions)};${svgEngine.version()}`
288
+ );
289
+ const manifestData = await fs.readOutput(MANIFEST_FILE);
290
+ const manifest = manifestData ? parseManifest(manifestData.toString("utf8")) : null;
291
+ const total = found.files.length + found.videos.length;
292
+ let analyzed = 0;
293
+ options.onProgress?.({ phase: "scan", done: 0, total });
294
+ const analyze = async (data, digest) => {
295
+ let result = cache.getAnalysis(digest);
296
+ if (!result) {
297
+ result = await engine.analyze(data);
298
+ cache.setAnalysis(digest, result);
299
+ }
300
+ options.onProgress?.({ phase: "analyze", done: ++analyzed, total });
301
+ return result;
302
+ };
303
+ const imagePlan = await buildPlan({
304
+ files: found.files,
305
+ projectRoot,
306
+ outDir,
307
+ staticRoot: options.staticRoot,
308
+ choosePreset: (source, analysis) => {
309
+ const forced = options.presetOverrides?.[source];
310
+ if (forced) {
311
+ return {
312
+ preset: getImagePreset(forced, policy.catalog),
313
+ reason: `set to ${forced} for this file`
314
+ };
315
+ }
316
+ const matched = policy.assetFor(source);
317
+ const assetPreset = matched ? policy.catalog.image[matched.preset] : void 0;
318
+ if (matched && assetPreset) {
319
+ return {
320
+ preset: assetPreset,
321
+ reason: `set to ${matched.preset} by assets "${matched.pattern}"`,
322
+ confidence: "high"
323
+ };
324
+ }
325
+ const verdict = classify(source, analysis);
326
+ return {
327
+ preset: getImagePreset(verdict.preset, policy.catalog),
328
+ reason: verdict.reason,
329
+ confidence: verdict.confidence
330
+ };
331
+ },
332
+ engine,
333
+ analyze,
334
+ readSource: (path) => fs.readSource(path)
335
+ });
336
+ const needsFfmpeg = options.needsFfmpeg ?? "install ffmpeg";
337
+ const plan = await withVideos(imagePlan, {
338
+ projectRoot,
339
+ staticRoot: options.staticRoot,
340
+ videoFiles: found.videos,
341
+ videoEngine: options.videoEngine ?? null,
342
+ needsFfmpeg,
343
+ presetOverrides: options.presetOverrides,
344
+ policy,
345
+ cache
346
+ });
347
+ const outputHash = async (path) => {
348
+ const data = await fs.readOutput(path);
349
+ return data ? sha256(data) : null;
350
+ };
351
+ const absoluteOf = (source) => resolve(projectRoot, ...source.split("/"));
352
+ const images = [];
353
+ const skipped = [];
354
+ let estimated = 0;
355
+ for (const item of plan.items) {
356
+ options.onProgress?.({ phase: "estimate", done: estimated++, total });
357
+ if (item.skip) {
358
+ skipped.push({
359
+ source: item.source,
360
+ bytes: item.sourceBytes,
361
+ reason: item.skip.reason,
362
+ animatedGif: item.skip.reason.startsWith(ANIMATED_GIF_REASON)
363
+ });
364
+ continue;
365
+ }
366
+ const data = absoluteOf(item.source);
367
+ const estimate = await estimateItem(item, () => fs.readSource(data), { engine, cache });
368
+ images.push({
369
+ source: item.source,
370
+ bytes: item.sourceBytes,
371
+ sha256: item.sourceSha256,
372
+ // The analysis is cached by hash, so this is a lookup, not a second decode.
373
+ analysis: cache.getAnalysis(item.sourceSha256),
374
+ classification: {
375
+ // Rules reason about the built-in a custom preset descends from.
376
+ preset: rootPreset(policy.catalog, item.preset),
377
+ confidence: item.classification?.confidence ?? "low",
378
+ reason: item.classification?.reason ?? ""
379
+ },
380
+ item,
381
+ cache: await checkCache(item, manifest?.sources[item.source], outputHash),
382
+ estimate
383
+ });
384
+ }
385
+ const svgs = [];
386
+ for (const path of found.svgs) {
387
+ const buffer = await fs.readSource(path);
388
+ const digest = sha256(buffer);
389
+ let analysis = cache.getSvg(digest);
390
+ if (!analysis) {
391
+ analysis = svgEngine.analyze(buffer.toString("utf8"));
392
+ cache.setSvg(digest, analysis);
393
+ }
394
+ svgs.push({
395
+ source: toPosix(relative(projectRoot, path)),
396
+ bytes: buffer.byteLength,
397
+ sha256: digest,
398
+ ...analysis
399
+ });
400
+ }
401
+ svgs.sort((a, b) => a.source < b.source ? -1 : 1);
402
+ const videoFacts = [];
403
+ const unprobed = [];
404
+ const siblingImages = /* @__PURE__ */ new Set([
405
+ ...images.map((i) => i.source),
406
+ ...skipped.map((i) => i.source),
407
+ ...plan.items.map((i) => i.source)
408
+ ]);
409
+ const missingCodecs = /* @__PURE__ */ new Set();
410
+ const caps = options.videoEngine ? await options.videoEngine.capabilities() : null;
411
+ if (caps) {
412
+ if (!caps.libsvtav1 && !caps.libaomAv1) missingCodecs.add("av1");
413
+ if (!caps.libx264) missingCodecs.add("h264");
414
+ }
415
+ let probed = 0;
416
+ for (const item of plan.videos ?? []) {
417
+ options.onProgress?.({ phase: "estimate", done: probed++, total });
418
+ if (item.skip) {
419
+ if (item.skip.reason.startsWith("needs ffmpeg")) {
420
+ unprobed.push({ source: item.source, bytes: item.sourceBytes });
421
+ } else {
422
+ skipped.push({
423
+ source: item.source,
424
+ bytes: item.sourceBytes,
425
+ reason: item.skip.reason,
426
+ animatedGif: false
427
+ });
428
+ }
429
+ continue;
430
+ }
431
+ const probe = cache.getVideoProbe(item.sourceSha256);
432
+ if (!probe) continue;
433
+ const absolute = absoluteOf(item.source);
434
+ const cacheState = await checkVideoCache(
435
+ item,
436
+ manifest?.videos[item.source],
437
+ outputHash,
438
+ missingCodecs
439
+ );
440
+ const isGif = extname2(item.source).toLowerCase() === ".gif";
441
+ const covered = cacheState.kind === "hit";
442
+ const estimate = covered || !options.videoEngine ? null : await estimateVideo(
443
+ item,
444
+ {
445
+ engine: options.videoEngine,
446
+ cache,
447
+ ffmpegVersion: videoVersions.ffmpeg ?? "unknown"
448
+ },
449
+ absolute
450
+ );
451
+ const stem = `${dirname(item.source) === "." ? "" : `${dirname(item.source)}/`}${basename2(item.source, extname2(item.source))}`;
452
+ const hasPoster = ["jpg", "jpeg", "png", "webp", "avif"].some(
453
+ (ext) => siblingImages.has(`${stem}.${ext}`) || siblingImages.has(`${stem}-poster.${ext}`) || siblingImages.has(`${stem}_poster.${ext}`)
454
+ );
455
+ videoFacts.push({
456
+ source: item.source,
457
+ bytes: item.sourceBytes,
458
+ sha256: item.sourceSha256,
459
+ probe,
460
+ faststart: isGif ? null : await isFastStart(absolute).catch(() => null),
461
+ classification: isGif ? { preset: "gif-to-video", confidence: "high", reason: "animated GIF" } : {
462
+ // What the plan chose (a per-file or `assets` choice included), as its built-in.
463
+ preset: rootPreset(policy.catalog, item.preset),
464
+ confidence: item.classification?.confidence ?? classifyVideo(item.source, probe).confidence,
465
+ reason: item.classification?.reason ?? classifyVideo(item.source, probe).reason
466
+ },
467
+ item,
468
+ cache: cacheState,
469
+ estimate,
470
+ hasPoster,
471
+ isGif
472
+ });
473
+ }
474
+ let references = null;
475
+ if (options.scanReferences !== false) {
476
+ const scan = await scanReferences({ cwd: projectRoot, outDir });
477
+ references = scan.names;
478
+ if (scan.truncated) {
479
+ notes.push("Reference scan hit its size limit; project/unreferenced may over-report.");
480
+ }
481
+ }
482
+ const settings = options.settings ?? {};
483
+ const settingFor = (id) => {
484
+ const raw = settings[id];
485
+ return Array.isArray(raw) ? { severity: raw[0], options: raw[1] } : { severity: raw, options: {} };
486
+ };
487
+ const videoEncoders = {};
488
+ if (caps && videoVersions.ffmpeg) {
489
+ const av1 = caps.libsvtav1 ? "libsvtav1" : caps.libaomAv1 ? "libaom-av1" : null;
490
+ if (av1) videoEncoders.av1 = `${av1} (ffmpeg ${videoVersions.ffmpeg})`;
491
+ if (caps.libx264) videoEncoders.h264 = `libx264 (ffmpeg ${videoVersions.ffmpeg})`;
492
+ }
493
+ const ctx = {
494
+ plan,
495
+ manifest,
496
+ videoEncoders,
497
+ unavailableCodecs: missingCodecs,
498
+ encoders: engine.encoders(),
499
+ outputHash,
500
+ sourceExists: async (source) => (await stat2(absoluteOf(source)).catch(() => null))?.isFile() ?? false,
501
+ images,
502
+ skipped,
503
+ svgs,
504
+ videos: videoFacts,
505
+ unprobed: unprobed.map((u) => ({ ...u, reason: `needs ffmpeg: ${needsFfmpeg}` })),
506
+ references,
507
+ budgets: options.budgets ?? {},
508
+ ffmpeg: options.ffmpeg,
509
+ options: (id) => settingFor(id).options
510
+ };
511
+ const ignorePatterns = options.ignore ?? [];
512
+ const isIgnored = (source) => ignorePatterns.some((p) => matchesGlob(p, source));
513
+ const ignoreSources = (all) => ignorePatterns.length === 0 ? all : all.filter((f) => !f.source || !isIgnored(f.source));
514
+ const findings = [];
515
+ for (const rule of ALL_RULES) {
516
+ const { severity } = settingFor(rule.id);
517
+ if (severity === "off") continue;
518
+ for (const finding of await rule.check(ctx)) {
519
+ findings.push(severity ? { ...finding, severity } : finding);
520
+ }
521
+ }
522
+ if (unprobed.length > 0) {
523
+ notes.push(
524
+ `${unprobed.length} video file${unprobed.length === 1 ? "" : "s"} need ffmpeg to be analysed: ${needsFfmpeg}`
525
+ );
526
+ }
527
+ if (caps && !caps.libsvtav1 && !caps.libaomAv1 && videoFacts.length > 0) {
528
+ notes.push("No AV1 encoder installed: video estimates use H.264 only.");
529
+ }
530
+ if (options.persistCache !== false) await cache.save(fs);
531
+ const potentialImages = images.reduce(
532
+ (sum, image) => sum + (image.cache.kind === "hit" ? 0 : savingOf(image) ?? 0),
533
+ 0
534
+ );
535
+ const potentialVideos = videoFacts.reduce(
536
+ (sum, v) => sum + (v.cache.kind === "hit" ? 0 : Math.max(0, v.bytes - (v.estimate?.best.bytes ?? v.bytes))),
537
+ 0
538
+ );
539
+ const potential = potentialImages + potentialVideos;
540
+ const files = [
541
+ ...images.map((image) => ({
542
+ source: image.source,
543
+ bytes: image.bytes,
544
+ width: image.analysis.width,
545
+ height: image.analysis.height,
546
+ format: image.analysis.format,
547
+ preset: image.item.preset,
548
+ reason: image.classification.reason,
549
+ covered: image.cache.kind === "hit",
550
+ estimatedBytes: image.estimate?.best.bytes ?? null,
551
+ savingsBytes: savingOf(image) ?? null,
552
+ plannedOutputs: image.item.outputs.length
553
+ })),
554
+ ...videoFacts.map((v) => ({
555
+ source: v.source,
556
+ bytes: v.bytes,
557
+ width: v.probe.width,
558
+ height: v.probe.height,
559
+ format: v.isGif ? "gif" : v.probe.videoCodec,
560
+ kind: "video",
561
+ durationSec: v.probe.durationSec,
562
+ preset: v.item.preset,
563
+ reason: v.classification.reason,
564
+ covered: v.cache.kind === "hit",
565
+ estimatedBytes: v.estimate?.best.bytes ?? null,
566
+ savingsBytes: v.estimate ? Math.max(0, v.bytes - v.estimate.best.bytes) : null,
567
+ plannedOutputs: v.item.outputs.length
568
+ })),
569
+ ...unprobed.map((u) => ({
570
+ source: u.source,
571
+ bytes: u.bytes,
572
+ width: 0,
573
+ height: 0,
574
+ format: "-",
575
+ kind: "video",
576
+ preset: "-",
577
+ reason: "",
578
+ covered: false,
579
+ estimatedBytes: null,
580
+ savingsBytes: null,
581
+ plannedOutputs: 0,
582
+ skipped: `needs ffmpeg: ${needsFfmpeg}`
583
+ })),
584
+ ...plan.items.filter((i) => i.skip).map((i) => ({
585
+ source: i.source,
586
+ bytes: i.sourceBytes,
587
+ width: i.sourceWidth,
588
+ height: i.sourceHeight,
589
+ format: "-",
590
+ preset: "-",
591
+ reason: "",
592
+ covered: false,
593
+ estimatedBytes: null,
594
+ savingsBytes: null,
595
+ plannedOutputs: 0,
596
+ skipped: i.skip.reason
597
+ }))
598
+ ].sort((a, b) => a.source < b.source ? -1 : a.source > b.source ? 1 : 0);
599
+ return {
600
+ findings: sortFindings(ignoreSources(findings)),
601
+ unprobed,
602
+ files: files.filter((f) => !isIgnored(f.source)),
603
+ counts: {
604
+ images: images.length,
605
+ svgs: svgs.length,
606
+ videos: videoFacts.length + unprobed.length,
607
+ skipped: skipped.length
608
+ },
609
+ totals: {
610
+ sourceBytes: images.reduce((s, i) => s + i.bytes, 0) + videoFacts.reduce((s, v) => s + v.bytes, 0) + unprobed.reduce((s, u) => s + u.bytes, 0) + skipped.reduce((s, i) => s + i.bytes, 0),
611
+ potentialSavingsBytes: potential,
612
+ optimizedImages: images.filter((i) => i.cache.kind === "hit").length + videoFacts.filter((v) => v.cache.kind === "hit").length
613
+ },
614
+ notes
615
+ };
616
+ }
617
+
618
+ export {
619
+ audit
620
+ };