bytecut 0.0.0-stage → 0.0.1
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/dist/audit-HVW4EOLP.js +97 -0
- package/dist/check-YONIQTM7.js +348 -0
- package/dist/chunk-326CQQWX.js +188 -0
- package/dist/chunk-3DBJKHOD.js +109 -0
- package/dist/chunk-4XOB3C34.js +186 -0
- package/dist/chunk-5R7LR5BB.js +282 -0
- package/dist/chunk-7IECTPJB.js +319 -0
- package/dist/chunk-CEZK3RQJ.js +2170 -0
- package/dist/chunk-DJ45NQZD.js +29 -0
- package/dist/chunk-G53LNJ5O.js +57 -0
- package/dist/chunk-GMHOU4Y5.js +59 -0
- package/dist/chunk-J2XJO4JZ.js +21 -0
- package/dist/chunk-LHTI3JMC.js +212 -0
- package/dist/chunk-LTWUPQDT.js +26 -0
- package/dist/chunk-M6ZDSATP.js +59 -0
- package/dist/chunk-N536JFSF.js +614 -0
- package/dist/chunk-NAKV7HS5.js +370 -0
- package/dist/chunk-TWI2OZJZ.js +1432 -0
- package/dist/chunk-U4KJMKI5.js +229 -0
- package/dist/chunk-WE7AEDVI.js +536 -0
- package/dist/chunk-XFJ3HW4X.js +248 -0
- package/dist/chunk-YBEC5QGV.js +463 -0
- package/dist/cli.js +138 -0
- package/dist/compare-6PCE6533.js +84 -0
- package/dist/doctor-R226JU6P.js +77 -0
- package/dist/explain-SGN5BOR7.js +265 -0
- package/dist/init-WHWUANOB.js +92 -0
- package/dist/optimize-UXM6JEDA.js +216 -0
- package/dist/snippet-2WVGMCNV.js +114 -0
- package/dist/tui-J4Y44IL7.js +1691 -0
- package/package.json +46 -3
- package/README.md +0 -3
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
svgoEngine
|
|
4
|
+
} from "./chunk-LTWUPQDT.js";
|
|
5
|
+
import {
|
|
6
|
+
audit
|
|
7
|
+
} from "./chunk-N536JFSF.js";
|
|
8
|
+
import {
|
|
9
|
+
formatAuditReport
|
|
10
|
+
} from "./chunk-CEZK3RQJ.js";
|
|
11
|
+
import "./chunk-3DBJKHOD.js";
|
|
12
|
+
import "./chunk-5R7LR5BB.js";
|
|
13
|
+
import {
|
|
14
|
+
sharpImageEngine
|
|
15
|
+
} from "./chunk-XFJ3HW4X.js";
|
|
16
|
+
import "./chunk-NAKV7HS5.js";
|
|
17
|
+
import {
|
|
18
|
+
configArg,
|
|
19
|
+
createVideoEngine,
|
|
20
|
+
discover,
|
|
21
|
+
ffmpegInstallText,
|
|
22
|
+
ffmpegStatus,
|
|
23
|
+
noAssetsMessage
|
|
24
|
+
} from "./chunk-4XOB3C34.js";
|
|
25
|
+
import "./chunk-M6ZDSATP.js";
|
|
26
|
+
import "./chunk-WE7AEDVI.js";
|
|
27
|
+
import "./chunk-YBEC5QGV.js";
|
|
28
|
+
import "./chunk-LHTI3JMC.js";
|
|
29
|
+
import {
|
|
30
|
+
guarded
|
|
31
|
+
} from "./chunk-J2XJO4JZ.js";
|
|
32
|
+
import "./chunk-TWI2OZJZ.js";
|
|
33
|
+
|
|
34
|
+
// src/cli/commands/audit.ts
|
|
35
|
+
import { defineCommand } from "citty";
|
|
36
|
+
var audit_default = defineCommand({
|
|
37
|
+
meta: {
|
|
38
|
+
name: "audit",
|
|
39
|
+
description: "Report what is too heavy in your image assets, with estimated savings"
|
|
40
|
+
},
|
|
41
|
+
args: {
|
|
42
|
+
...configArg,
|
|
43
|
+
json: { type: "boolean", description: "Print the report as JSON" },
|
|
44
|
+
all: { type: "boolean", description: "Show every finding instead of the first 10 per rule" },
|
|
45
|
+
"out-dir": {
|
|
46
|
+
type: "string",
|
|
47
|
+
description: "Output folder (default: from the detected framework)"
|
|
48
|
+
},
|
|
49
|
+
ffmpeg: {
|
|
50
|
+
type: "string",
|
|
51
|
+
description: "Path to the ffmpeg binary (else BYTECUT_FFMPEG, then PATH)"
|
|
52
|
+
},
|
|
53
|
+
ffprobe: { type: "string", description: "Path to the ffprobe binary" },
|
|
54
|
+
cache: {
|
|
55
|
+
type: "boolean",
|
|
56
|
+
default: true,
|
|
57
|
+
description: "Keep the analysis cache in <outDir>/.bytecut-cache (--no-cache to avoid writing)"
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
run: guarded(async ({ args }) => {
|
|
61
|
+
const found = await discover(args._.map(String), args["out-dir"], args.config);
|
|
62
|
+
const { config } = found;
|
|
63
|
+
if (found.scanInputs.length === 0) {
|
|
64
|
+
console.error(noAssetsMessage("audit", found.project));
|
|
65
|
+
process.exitCode = 2;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const report = await audit({
|
|
69
|
+
projectRoot: found.projectRoot,
|
|
70
|
+
engine: sharpImageEngine,
|
|
71
|
+
svgEngine: svgoEngine,
|
|
72
|
+
inputs: found.scanInputs,
|
|
73
|
+
outDir: found.outDir,
|
|
74
|
+
staticRoot: found.project.staticRoot,
|
|
75
|
+
ffmpeg: ffmpegStatus(config),
|
|
76
|
+
videoEngine: createVideoEngine({ ffmpeg: args.ffmpeg, ffprobe: args.ffprobe }, config),
|
|
77
|
+
policy: config.policy,
|
|
78
|
+
exclude: config.exclude,
|
|
79
|
+
ignore: config.auditIgnore,
|
|
80
|
+
settings: config.rules,
|
|
81
|
+
budgets: config.budgets,
|
|
82
|
+
needsFfmpeg: ffmpegInstallText(),
|
|
83
|
+
persistCache: args.cache !== false
|
|
84
|
+
});
|
|
85
|
+
if (args.json) {
|
|
86
|
+
process.stdout.write(
|
|
87
|
+
`${JSON.stringify({ version: 1, project: found.project, ...report }, null, 2)}
|
|
88
|
+
`
|
|
89
|
+
);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
console.log(formatAuditReport(report, { all: Boolean(args.all) }));
|
|
93
|
+
})
|
|
94
|
+
});
|
|
95
|
+
export {
|
|
96
|
+
audit_default as default
|
|
97
|
+
};
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
svgoEngine
|
|
4
|
+
} from "./chunk-LTWUPQDT.js";
|
|
5
|
+
import {
|
|
6
|
+
audit
|
|
7
|
+
} from "./chunk-N536JFSF.js";
|
|
8
|
+
import {
|
|
9
|
+
formatAuditReport
|
|
10
|
+
} from "./chunk-CEZK3RQJ.js";
|
|
11
|
+
import "./chunk-3DBJKHOD.js";
|
|
12
|
+
import "./chunk-5R7LR5BB.js";
|
|
13
|
+
import {
|
|
14
|
+
sharpImageEngine
|
|
15
|
+
} from "./chunk-XFJ3HW4X.js";
|
|
16
|
+
import "./chunk-NAKV7HS5.js";
|
|
17
|
+
import {
|
|
18
|
+
configArg,
|
|
19
|
+
createVideoEngine,
|
|
20
|
+
discover,
|
|
21
|
+
ffmpegInstallText,
|
|
22
|
+
ffmpegStatus,
|
|
23
|
+
noAssetsMessage
|
|
24
|
+
} from "./chunk-4XOB3C34.js";
|
|
25
|
+
import {
|
|
26
|
+
run
|
|
27
|
+
} from "./chunk-M6ZDSATP.js";
|
|
28
|
+
import {
|
|
29
|
+
toPosix
|
|
30
|
+
} from "./chunk-WE7AEDVI.js";
|
|
31
|
+
import "./chunk-YBEC5QGV.js";
|
|
32
|
+
import "./chunk-LHTI3JMC.js";
|
|
33
|
+
import {
|
|
34
|
+
BytecutError,
|
|
35
|
+
CONFIG_FILES,
|
|
36
|
+
formatBytes,
|
|
37
|
+
percentChange,
|
|
38
|
+
plural
|
|
39
|
+
} from "./chunk-TWI2OZJZ.js";
|
|
40
|
+
|
|
41
|
+
// src/cli/commands/check.ts
|
|
42
|
+
import { defineCommand } from "citty";
|
|
43
|
+
|
|
44
|
+
// src/core/commands/check.ts
|
|
45
|
+
function evaluate(report, options = {}) {
|
|
46
|
+
const failOn = options.failOn ?? "error";
|
|
47
|
+
const changed = options.changed ?? null;
|
|
48
|
+
const inScope = (f) => !changed || changed.all || f.source === void 0 || changed.files.has(f.source);
|
|
49
|
+
const findings = report.findings.filter(inScope);
|
|
50
|
+
const failing = findings.filter(
|
|
51
|
+
(f) => f.severity === "error" || failOn === "warn" && f.severity === "warn"
|
|
52
|
+
);
|
|
53
|
+
const count = (s) => findings.filter((f) => f.severity === s).length;
|
|
54
|
+
return {
|
|
55
|
+
ok: failing.length === 0,
|
|
56
|
+
failOn,
|
|
57
|
+
findings,
|
|
58
|
+
failing,
|
|
59
|
+
counts: { error: count("error"), warn: count("warn"), info: count("info") },
|
|
60
|
+
scope: changed ? {
|
|
61
|
+
base: changed.base,
|
|
62
|
+
changedFiles: changed.files.size,
|
|
63
|
+
everything: changed.all,
|
|
64
|
+
dropped: report.findings.length - findings.length
|
|
65
|
+
} : null
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/core/git/changed.ts
|
|
70
|
+
var fail = (message, cause) => new BytecutError("GIT_UNAVAILABLE", message, cause ? { cause } : void 0);
|
|
71
|
+
function nulList(stdout) {
|
|
72
|
+
return stdout.split("\0").filter((p) => p.length > 0);
|
|
73
|
+
}
|
|
74
|
+
async function changedFiles(options) {
|
|
75
|
+
const { projectRoot, base, run: run2 } = options;
|
|
76
|
+
const git = async (...args) => {
|
|
77
|
+
const { stdout } = await run2("git", ["-C", projectRoot, ...args]);
|
|
78
|
+
return stdout;
|
|
79
|
+
};
|
|
80
|
+
if (base.startsWith("-")) throw fail(`"${base}" is not a git ref.`);
|
|
81
|
+
try {
|
|
82
|
+
await git("rev-parse", "--is-inside-work-tree");
|
|
83
|
+
} catch (error) {
|
|
84
|
+
const missing = error.code === "ENOENT";
|
|
85
|
+
throw fail(
|
|
86
|
+
missing ? "--changed needs git, which was not found on PATH." : "--changed needs a git repository; this folder is not inside one.",
|
|
87
|
+
error
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
let mergeBase;
|
|
91
|
+
try {
|
|
92
|
+
await git("rev-parse", "--verify", "--quiet", `${base}^{commit}`);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
const shallow = (await git("rev-parse", "--is-shallow-repository").catch(() => "")).trim();
|
|
95
|
+
throw fail(
|
|
96
|
+
`The ref "${base}" is not in this repository.` + (shallow === "true" ? " This is a shallow clone: fetch more history first (for GitHub Actions, `fetch-depth: 0` on actions/checkout)." : " bytecut never fetches; fetch it yourself if it is a remote branch."),
|
|
97
|
+
error
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
mergeBase = (await git("merge-base", base, "HEAD")).trim();
|
|
102
|
+
} catch (error) {
|
|
103
|
+
throw fail(
|
|
104
|
+
`No common ancestor between "${base}" and HEAD. In a shallow clone, fetch more history (for GitHub Actions, \`fetch-depth: 0\`).`,
|
|
105
|
+
error
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const tracked = nulList(
|
|
109
|
+
await git(
|
|
110
|
+
"diff",
|
|
111
|
+
"--name-only",
|
|
112
|
+
"--relative",
|
|
113
|
+
"--diff-filter=ACMRT",
|
|
114
|
+
"-z",
|
|
115
|
+
mergeBase,
|
|
116
|
+
"--",
|
|
117
|
+
"."
|
|
118
|
+
)
|
|
119
|
+
);
|
|
120
|
+
const untracked = nulList(
|
|
121
|
+
await git("ls-files", "--others", "--exclude-standard", "-z", "--", ".")
|
|
122
|
+
);
|
|
123
|
+
const files = new Set([...tracked, ...untracked].map(toPosix));
|
|
124
|
+
const configNames = new Set(CONFIG_FILES);
|
|
125
|
+
const all = [...files].some((f) => configNames.has(f));
|
|
126
|
+
return { base, files, all };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/core/report/json.ts
|
|
130
|
+
function renderJsonReport(report, verdict) {
|
|
131
|
+
return `${JSON.stringify(
|
|
132
|
+
{
|
|
133
|
+
version: 1,
|
|
134
|
+
ok: verdict.ok,
|
|
135
|
+
failOn: verdict.failOn,
|
|
136
|
+
counts: verdict.counts,
|
|
137
|
+
changed: verdict.scope,
|
|
138
|
+
findings: verdict.findings,
|
|
139
|
+
failing: verdict.failing.length,
|
|
140
|
+
scanned: report.counts,
|
|
141
|
+
totals: report.totals,
|
|
142
|
+
unprobed: report.unprobed,
|
|
143
|
+
notes: report.notes
|
|
144
|
+
},
|
|
145
|
+
null,
|
|
146
|
+
2
|
|
147
|
+
)}
|
|
148
|
+
`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/core/report/markdown.ts
|
|
152
|
+
var MARKDOWN_LIMIT = 6e4;
|
|
153
|
+
var ROWS_PER_RULE = 20;
|
|
154
|
+
var SEVERITY_ORDER = { error: 0, warn: 1, info: 2 };
|
|
155
|
+
function cell(text) {
|
|
156
|
+
return text.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/[`*_<>[\]]/g, (c) => `\\${c}`).replace(/\r?\n/g, " ");
|
|
157
|
+
}
|
|
158
|
+
function size(f) {
|
|
159
|
+
if (f.current !== void 0 && f.estimated !== void 0) {
|
|
160
|
+
return `${formatBytes(f.current)} \u2192 ~${formatBytes(f.estimated)} (${percentChange(f.current, f.estimated)})`;
|
|
161
|
+
}
|
|
162
|
+
return f.current !== void 0 ? formatBytes(f.current) : "";
|
|
163
|
+
}
|
|
164
|
+
function renderMarkdownReport(report, verdict) {
|
|
165
|
+
const out = [];
|
|
166
|
+
const { counts, scope } = verdict;
|
|
167
|
+
out.push(`## bytecut check: ${verdict.ok ? "passed" : "failed"}`, "");
|
|
168
|
+
const summary = [
|
|
169
|
+
`${plural(counts.error, "error")}, ${plural(counts.warn, "warning")}, ${counts.info} info`
|
|
170
|
+
];
|
|
171
|
+
const c = report.counts;
|
|
172
|
+
summary.push(
|
|
173
|
+
`scanned ${plural(c.images + c.skipped, "image")}, ${plural(c.svgs, "SVG")}, ${plural(c.videos, "video")} (${formatBytes(report.totals.sourceBytes)})`
|
|
174
|
+
);
|
|
175
|
+
if (scope) {
|
|
176
|
+
summary.push(
|
|
177
|
+
scope.everything ? `a bytecut config changed since \`${cell(scope.base)}\`, so every file is in scope` : `${plural(scope.changedFiles, "file")} changed since \`${cell(scope.base)}\``
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
out.push(summary.join(" \xB7 "), "");
|
|
181
|
+
if (!verdict.ok) {
|
|
182
|
+
out.push(
|
|
183
|
+
`Failing on ${verdict.failOn === "warn" ? "warnings and errors" : "errors"}: ${plural(verdict.failing.length, "finding")}.`,
|
|
184
|
+
""
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
const groups = /* @__PURE__ */ new Map();
|
|
188
|
+
for (const f of verdict.findings) groups.set(f.ruleId, [...groups.get(f.ruleId) ?? [], f]);
|
|
189
|
+
const ordered = [...groups.entries()].sort(([ia, a], [ib, b]) => {
|
|
190
|
+
const d = (SEVERITY_ORDER[a[0].severity] ?? 9) - (SEVERITY_ORDER[b[0].severity] ?? 9);
|
|
191
|
+
return d || (ia < ib ? -1 : 1);
|
|
192
|
+
});
|
|
193
|
+
const tail = [];
|
|
194
|
+
if (report.unprobed.length > 0) {
|
|
195
|
+
tail.push(
|
|
196
|
+
`> ${plural(report.unprobed.length, "video")} not analysed because ffmpeg was not found.`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
for (const note of report.notes) tail.push(`> ${cell(note)}`);
|
|
200
|
+
let omitted = 0;
|
|
201
|
+
for (const [ruleId, findings] of ordered) {
|
|
202
|
+
const section = [
|
|
203
|
+
`### \`${ruleId}\` \xB7 ${findings[0].severity} \xB7 ${plural(findings.length, "file")}`,
|
|
204
|
+
"",
|
|
205
|
+
"| File | Size | Detail |",
|
|
206
|
+
"| --- | --- | --- |"
|
|
207
|
+
];
|
|
208
|
+
for (const f of findings.slice(0, ROWS_PER_RULE)) {
|
|
209
|
+
const where = f.path ? `${f.source ?? ""} \u2192 ${f.path}` : f.source ?? "(project)";
|
|
210
|
+
section.push(`| ${cell(where)} | ${cell(size(f))} | ${cell(f.message)} |`);
|
|
211
|
+
}
|
|
212
|
+
if (findings.length > ROWS_PER_RULE) {
|
|
213
|
+
section.push("", `\u2026 and ${findings.length - ROWS_PER_RULE} more.`);
|
|
214
|
+
}
|
|
215
|
+
section.push("");
|
|
216
|
+
const text = section.join("\n");
|
|
217
|
+
if (out.join("\n").length + text.length + tail.join("\n").length + 200 > MARKDOWN_LIMIT) {
|
|
218
|
+
omitted += findings.length;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
out.push(text);
|
|
222
|
+
}
|
|
223
|
+
if (omitted > 0) {
|
|
224
|
+
out.push(
|
|
225
|
+
`_${plural(omitted, "more finding")} not shown: this comment hit its size limit. Run \`bytecut check\` locally for the full list._`,
|
|
226
|
+
""
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
out.push(...tail);
|
|
230
|
+
return `${out.join("\n").trimEnd()}
|
|
231
|
+
`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/cli/commands/check.ts
|
|
235
|
+
var EXIT = { ok: 0, failed: 1, unusable: 2 };
|
|
236
|
+
var REPORTS = ["md", "json"];
|
|
237
|
+
var FAIL_ON = ["error", "warn"];
|
|
238
|
+
var check_default = defineCommand({
|
|
239
|
+
meta: {
|
|
240
|
+
name: "check",
|
|
241
|
+
description: "CI mode: audit the project and exit non-zero if any finding is an error"
|
|
242
|
+
},
|
|
243
|
+
args: {
|
|
244
|
+
...configArg,
|
|
245
|
+
// Declared boolean so a bare `--changed` never swallows the flag after it; the base ref
|
|
246
|
+
// is read from the raw arguments below.
|
|
247
|
+
changed: {
|
|
248
|
+
type: "boolean",
|
|
249
|
+
description: "Only report files changed since a git ref: --changed (HEAD, i.e. local changes), --changed origin/main or --changed=origin/main"
|
|
250
|
+
},
|
|
251
|
+
report: { type: "string", description: "Print a report on stdout: md or json" },
|
|
252
|
+
"fail-on": {
|
|
253
|
+
type: "string",
|
|
254
|
+
description: "Lowest severity that fails the check: error (default) or warn"
|
|
255
|
+
},
|
|
256
|
+
all: { type: "boolean", description: "Text report: every finding, not the first 10 per rule" },
|
|
257
|
+
"out-dir": {
|
|
258
|
+
type: "string",
|
|
259
|
+
description: "Output folder (default: from the detected framework)"
|
|
260
|
+
},
|
|
261
|
+
ffmpeg: { type: "string", description: "Path to the ffmpeg binary" },
|
|
262
|
+
ffprobe: { type: "string", description: "Path to the ffprobe binary" }
|
|
263
|
+
},
|
|
264
|
+
run: async ({ args, rawArgs }) => {
|
|
265
|
+
try {
|
|
266
|
+
process.exitCode = await check(args, rawArgs);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (!(error instanceof BytecutError)) throw error;
|
|
269
|
+
console.error(`bytecut: ${error.message} [${error.code}]`);
|
|
270
|
+
process.exitCode = EXIT.unusable;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
async function check(args, rawArgs) {
|
|
275
|
+
const report = args.report;
|
|
276
|
+
if (report !== void 0 && !REPORTS.includes(report)) {
|
|
277
|
+
throw new BytecutError("BAD_ARGUMENTS", `--report must be md or json, not "${report}".`);
|
|
278
|
+
}
|
|
279
|
+
const failOn = args["fail-on"] ?? "error";
|
|
280
|
+
if (!FAIL_ON.includes(failOn)) {
|
|
281
|
+
throw new BytecutError("BAD_ARGUMENTS", `--fail-on must be error or warn, not "${failOn}".`);
|
|
282
|
+
}
|
|
283
|
+
let changedBase = null;
|
|
284
|
+
const positional = args._.map(String);
|
|
285
|
+
const at = rawArgs.findIndex((a) => a === "--changed" || a.startsWith("--changed="));
|
|
286
|
+
if (at !== -1) {
|
|
287
|
+
const arg = rawArgs[at];
|
|
288
|
+
const next = rawArgs[at + 1];
|
|
289
|
+
if (arg.includes("=")) changedBase = arg.slice(arg.indexOf("=") + 1) || "HEAD";
|
|
290
|
+
else if (next !== void 0 && !next.startsWith("-")) {
|
|
291
|
+
changedBase = next;
|
|
292
|
+
const i = positional.indexOf(next);
|
|
293
|
+
if (i !== -1) positional.splice(i, 1);
|
|
294
|
+
} else changedBase = "HEAD";
|
|
295
|
+
}
|
|
296
|
+
const found = await discover(
|
|
297
|
+
positional,
|
|
298
|
+
args["out-dir"],
|
|
299
|
+
args.config
|
|
300
|
+
);
|
|
301
|
+
if (found.scanInputs.length === 0) {
|
|
302
|
+
console.error(noAssetsMessage("check", found.project));
|
|
303
|
+
return EXIT.unusable;
|
|
304
|
+
}
|
|
305
|
+
const { config } = found;
|
|
306
|
+
const changed = changedBase === null ? null : await changedFiles({ projectRoot: found.projectRoot, base: changedBase, run });
|
|
307
|
+
const result = await audit({
|
|
308
|
+
projectRoot: found.projectRoot,
|
|
309
|
+
engine: sharpImageEngine,
|
|
310
|
+
svgEngine: svgoEngine,
|
|
311
|
+
inputs: found.scanInputs,
|
|
312
|
+
outDir: found.outDir,
|
|
313
|
+
staticRoot: found.project.staticRoot,
|
|
314
|
+
ffmpeg: ffmpegStatus(config),
|
|
315
|
+
videoEngine: createVideoEngine(
|
|
316
|
+
{ ffmpeg: args.ffmpeg, ffprobe: args.ffprobe },
|
|
317
|
+
config
|
|
318
|
+
),
|
|
319
|
+
needsFfmpeg: ffmpegInstallText(),
|
|
320
|
+
persistCache: false,
|
|
321
|
+
policy: config.policy,
|
|
322
|
+
exclude: config.exclude,
|
|
323
|
+
ignore: config.auditIgnore,
|
|
324
|
+
settings: config.rules,
|
|
325
|
+
budgets: config.budgets
|
|
326
|
+
});
|
|
327
|
+
const verdict = evaluate(result, { failOn, changed });
|
|
328
|
+
if (report === "json") process.stdout.write(renderJsonReport(result, verdict));
|
|
329
|
+
else if (report === "md") process.stdout.write(renderMarkdownReport(result, verdict));
|
|
330
|
+
else {
|
|
331
|
+
const view = { ...result, findings: verdict.findings };
|
|
332
|
+
console.log(formatAuditReport(view, { all: Boolean(args.all) }));
|
|
333
|
+
if (verdict.scope) {
|
|
334
|
+
console.log(
|
|
335
|
+
verdict.scope.everything ? `A bytecut config changed since ${verdict.scope.base}: every file is in scope.` : `${plural(verdict.scope.changedFiles, "file")} changed since ${verdict.scope.base}; findings about other files are not shown (${verdict.scope.dropped} hidden).`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const summary = `${plural(verdict.counts.error, "error")}, ${plural(verdict.counts.warn, "warning")}`;
|
|
340
|
+
console.error(
|
|
341
|
+
verdict.ok ? `bytecut check: passed (${summary}).` : `bytecut check: failed (${plural(verdict.failing.length, "failing finding")}; ${summary}).`
|
|
342
|
+
);
|
|
343
|
+
return verdict.ok ? EXIT.ok : EXIT.failed;
|
|
344
|
+
}
|
|
345
|
+
export {
|
|
346
|
+
EXIT,
|
|
347
|
+
check_default as default
|
|
348
|
+
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ffmpegDirOf,
|
|
4
|
+
installHint,
|
|
5
|
+
pickAv1Encoder,
|
|
6
|
+
probeFfmpeg,
|
|
7
|
+
probeFfprobeVersion,
|
|
8
|
+
resolveBinary
|
|
9
|
+
} from "./chunk-YBEC5QGV.js";
|
|
10
|
+
|
|
11
|
+
// src/core/doctor/report.ts
|
|
12
|
+
var MIN_NODE_MAJOR = 22;
|
|
13
|
+
function message(error) {
|
|
14
|
+
return error instanceof Error ? error.message : String(error);
|
|
15
|
+
}
|
|
16
|
+
async function collectDoctorReport(deps, options = {}) {
|
|
17
|
+
const major = Number.parseInt(deps.nodeVersion.replace(/^v/, ""), 10);
|
|
18
|
+
const node = { version: deps.nodeVersion, supported: major >= MIN_NODE_MAJOR };
|
|
19
|
+
let sharp;
|
|
20
|
+
try {
|
|
21
|
+
sharp = { available: true, ...await deps.loadSharp() };
|
|
22
|
+
} catch (error) {
|
|
23
|
+
sharp = { available: false, error: message(error) };
|
|
24
|
+
}
|
|
25
|
+
const base = { env: deps.env, platform: deps.platform };
|
|
26
|
+
const ffmpegResolved = resolveBinary(
|
|
27
|
+
"ffmpeg",
|
|
28
|
+
{ ...base, flag: options.ffmpegFlag, config: options.ffmpegConfig },
|
|
29
|
+
deps
|
|
30
|
+
);
|
|
31
|
+
const ffprobeResolved = resolveBinary(
|
|
32
|
+
"ffprobe",
|
|
33
|
+
{
|
|
34
|
+
...base,
|
|
35
|
+
flag: options.ffprobeFlag,
|
|
36
|
+
config: options.ffprobeConfig,
|
|
37
|
+
ffmpegDir: ffmpegDirOf(ffmpegResolved)
|
|
38
|
+
},
|
|
39
|
+
deps
|
|
40
|
+
);
|
|
41
|
+
const ffmpeg = { resolved: ffmpegResolved };
|
|
42
|
+
if (ffmpegResolved.found) {
|
|
43
|
+
try {
|
|
44
|
+
ffmpeg.info = await probeFfmpeg(ffmpegResolved.path, deps.run);
|
|
45
|
+
ffmpeg.version = ffmpeg.info.version;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
ffmpeg.error = message(error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const ffprobe = { resolved: ffprobeResolved };
|
|
51
|
+
if (ffprobeResolved.found) {
|
|
52
|
+
try {
|
|
53
|
+
ffprobe.version = await probeFfprobeVersion(ffprobeResolved.path, deps.run);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
ffprobe.error = message(error);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const scorerResolved = resolveBinary(
|
|
59
|
+
"ssimulacra2",
|
|
60
|
+
{
|
|
61
|
+
env: deps.env,
|
|
62
|
+
platform: deps.platform,
|
|
63
|
+
flag: options.ssimulacra2Flag,
|
|
64
|
+
config: options.ssimulacra2Config
|
|
65
|
+
},
|
|
66
|
+
deps
|
|
67
|
+
);
|
|
68
|
+
const caps = ffmpeg.info?.capabilities;
|
|
69
|
+
const encoders = {
|
|
70
|
+
av1: caps ? pickAv1Encoder(caps) : null,
|
|
71
|
+
h264: caps?.libx264 ?? false,
|
|
72
|
+
vp9: caps?.libvpxVp9 ?? false,
|
|
73
|
+
vmaf: caps?.libvmaf ?? false
|
|
74
|
+
};
|
|
75
|
+
const degradations = [];
|
|
76
|
+
const hint = installHint(deps.platform, deps.osReleaseId);
|
|
77
|
+
if (!ffmpegResolved.found) {
|
|
78
|
+
degradations.push(
|
|
79
|
+
ffmpegResolved.attempted ? `ffmpeg not found at ${ffmpegResolved.attempted} (from ${sourceLabel(ffmpegResolved.source)}). Videos and GIFs will be marked "needs ffmpeg"; images are unaffected.` : `ffmpeg not found. Videos and GIFs will be marked "needs ffmpeg"; images are unaffected. Install it with: ${hint}`
|
|
80
|
+
);
|
|
81
|
+
} else if (ffmpeg.error) {
|
|
82
|
+
degradations.push(
|
|
83
|
+
`ffmpeg at ${ffmpegResolved.path} could not be run (${ffmpeg.error}). Videos and GIFs will be marked "needs ffmpeg".`
|
|
84
|
+
);
|
|
85
|
+
} else {
|
|
86
|
+
if (!ffprobeResolved.found || ffprobe.error) {
|
|
87
|
+
degradations.push(
|
|
88
|
+
`ffprobe is not usable. Videos cannot be probed, so they will be marked "needs ffmpeg". It normally ships alongside ffmpeg; install it with: ${hint}`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (!encoders.av1) {
|
|
92
|
+
degradations.push(
|
|
93
|
+
"No AV1 encoder (libsvtav1 / libaom-av1). AV1 outputs will be skipped; H.264 and poster are still produced."
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (!encoders.h264) {
|
|
97
|
+
degradations.push("libx264 is missing. H.264 MP4 fallbacks cannot be produced.");
|
|
98
|
+
}
|
|
99
|
+
if (!encoders.vmaf) {
|
|
100
|
+
degradations.push(
|
|
101
|
+
"libvmaf is missing. Fixed quality settings from the preset target will be used; the VMAF search is skipped."
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
node,
|
|
107
|
+
sharp,
|
|
108
|
+
ffmpeg,
|
|
109
|
+
ffprobe,
|
|
110
|
+
scorer: { kind: scorerResolved.found ? "native" : "wasm", resolved: scorerResolved },
|
|
111
|
+
encoders,
|
|
112
|
+
degradations,
|
|
113
|
+
ok: node.supported && sharp.available
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function sourceLabel(source) {
|
|
117
|
+
switch (source) {
|
|
118
|
+
case "flag":
|
|
119
|
+
return "flag";
|
|
120
|
+
case "env":
|
|
121
|
+
return "environment variable";
|
|
122
|
+
case "config":
|
|
123
|
+
return "config";
|
|
124
|
+
case "ffmpeg-dir":
|
|
125
|
+
return "ffmpeg's directory";
|
|
126
|
+
case "path":
|
|
127
|
+
return "PATH";
|
|
128
|
+
default:
|
|
129
|
+
return "not found";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function binaryLine(label, report) {
|
|
133
|
+
const { resolved } = report;
|
|
134
|
+
if (!resolved.found) {
|
|
135
|
+
return resolved.attempted ? `${label}: not found at ${resolved.attempted} (${sourceLabel(resolved.source)})` : `${label}: not found`;
|
|
136
|
+
}
|
|
137
|
+
const detail = report.error ? `failed to run: ${report.error}` : `version ${report.version}`;
|
|
138
|
+
return `${label}: ${resolved.path} (${sourceLabel(resolved.source)}), ${detail}`;
|
|
139
|
+
}
|
|
140
|
+
var yesNo = (value) => value ? "yes" : "no";
|
|
141
|
+
function formatDoctorReport(report) {
|
|
142
|
+
const lines = ["bytecut doctor", ""];
|
|
143
|
+
lines.push(
|
|
144
|
+
`Node: ${report.node.version}${report.node.supported ? "" : ` (needs >= ${MIN_NODE_MAJOR})`}`
|
|
145
|
+
);
|
|
146
|
+
lines.push(
|
|
147
|
+
report.sharp.available ? `sharp: ${report.sharp.version} (libvips ${report.sharp.libvips})` : `sharp: unavailable (${report.sharp.error})`
|
|
148
|
+
);
|
|
149
|
+
lines.push(binaryLine("ffmpeg ", report.ffmpeg));
|
|
150
|
+
lines.push(binaryLine("ffprobe", report.ffprobe));
|
|
151
|
+
{
|
|
152
|
+
const { kind, resolved } = report.scorer;
|
|
153
|
+
lines.push(
|
|
154
|
+
kind === "native" && resolved.found ? `scorer: native ssimulacra2 at ${resolved.path} (${sourceLabel(resolved.source)})` : resolved.found === false && resolved.attempted ? `scorer: built-in WebAssembly (no ssimulacra2 at ${resolved.attempted}, from ${sourceLabel(resolved.source)})` : "scorer: built-in WebAssembly SSIMULACRA2 (no native ssimulacra2 needed)"
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (report.ffmpeg.info) {
|
|
158
|
+
lines.push("", "Encoders:");
|
|
159
|
+
lines.push(` AV1: ${report.encoders.av1 ?? "none"}`);
|
|
160
|
+
lines.push(` H.264: ${yesNo(report.encoders.h264)} (libx264)`);
|
|
161
|
+
lines.push(` VP9: ${yesNo(report.encoders.vp9)} (libvpx-vp9)`);
|
|
162
|
+
lines.push(` libvmaf: ${yesNo(report.encoders.vmaf)}`);
|
|
163
|
+
}
|
|
164
|
+
if (report.project) {
|
|
165
|
+
const p = report.project;
|
|
166
|
+
lines.push("", `Project: ${p.framework} \u2192 ${p.outDir}`);
|
|
167
|
+
lines.push(`Config: ${p.configFile ?? "none (zero-config)"}`);
|
|
168
|
+
if (p.configError) lines.push(` problem: ${p.configError}`);
|
|
169
|
+
if (p.outDirIgnored) {
|
|
170
|
+
lines.push(
|
|
171
|
+
` note: ${p.outDir} is matched by a .gitignore. bytecut's outputs are meant to be committed, so deploys need no encoding step. Nothing was changed.`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
lines.push("");
|
|
176
|
+
if (report.degradations.length === 0) {
|
|
177
|
+
lines.push("Everything is available. No degradation.");
|
|
178
|
+
} else {
|
|
179
|
+
lines.push("Degraded:");
|
|
180
|
+
for (const item of report.degradations) lines.push(` - ${item}`);
|
|
181
|
+
}
|
|
182
|
+
return lines.join("\n");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export {
|
|
186
|
+
collectDoctorReport,
|
|
187
|
+
formatDoctorReport
|
|
188
|
+
};
|