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
package/dist/cli.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
collectDoctorReport,
|
|
4
|
+
formatDoctorReport
|
|
5
|
+
} from "./chunk-326CQQWX.js";
|
|
6
|
+
import {
|
|
7
|
+
svgoEngine
|
|
8
|
+
} from "./chunk-LTWUPQDT.js";
|
|
9
|
+
import {
|
|
10
|
+
audit
|
|
11
|
+
} from "./chunk-N536JFSF.js";
|
|
12
|
+
import {
|
|
13
|
+
package_default
|
|
14
|
+
} from "./chunk-G53LNJ5O.js";
|
|
15
|
+
import {
|
|
16
|
+
formatAuditReport
|
|
17
|
+
} from "./chunk-CEZK3RQJ.js";
|
|
18
|
+
import "./chunk-3DBJKHOD.js";
|
|
19
|
+
import "./chunk-5R7LR5BB.js";
|
|
20
|
+
import {
|
|
21
|
+
sharpImageEngine
|
|
22
|
+
} from "./chunk-XFJ3HW4X.js";
|
|
23
|
+
import "./chunk-NAKV7HS5.js";
|
|
24
|
+
import {
|
|
25
|
+
createVideoEngine,
|
|
26
|
+
discover,
|
|
27
|
+
ffmpegInstallText,
|
|
28
|
+
ffmpegStatus,
|
|
29
|
+
makeScorerFactory,
|
|
30
|
+
noAssetsMessage
|
|
31
|
+
} from "./chunk-4XOB3C34.js";
|
|
32
|
+
import {
|
|
33
|
+
doctorDeps
|
|
34
|
+
} from "./chunk-M6ZDSATP.js";
|
|
35
|
+
import "./chunk-WE7AEDVI.js";
|
|
36
|
+
import "./chunk-YBEC5QGV.js";
|
|
37
|
+
import "./chunk-LHTI3JMC.js";
|
|
38
|
+
import {
|
|
39
|
+
guarded
|
|
40
|
+
} from "./chunk-J2XJO4JZ.js";
|
|
41
|
+
import "./chunk-TWI2OZJZ.js";
|
|
42
|
+
|
|
43
|
+
// src/cli/index.ts
|
|
44
|
+
import { defineCommand, runMain } from "citty";
|
|
45
|
+
|
|
46
|
+
// src/cli/interactive.ts
|
|
47
|
+
function isInteractive(env = process.env, stdin = process.stdin, stdout = process.stdout) {
|
|
48
|
+
const ci = env.CI;
|
|
49
|
+
const inCi = ci !== void 0 && ci !== "" && ci !== "0" && ci.toLowerCase() !== "false";
|
|
50
|
+
return Boolean(stdin.isTTY && stdout.isTTY) && !inCi;
|
|
51
|
+
}
|
|
52
|
+
async function runDefault() {
|
|
53
|
+
const found = await discover([]);
|
|
54
|
+
if (found.scanInputs.length === 0) {
|
|
55
|
+
console.error(noAssetsMessage("", found.project));
|
|
56
|
+
process.exitCode = 2;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (isInteractive()) {
|
|
60
|
+
const { runTui } = await import("./tui-J4Y44IL7.js");
|
|
61
|
+
await runTui({
|
|
62
|
+
projectRoot: found.projectRoot,
|
|
63
|
+
project: found.project,
|
|
64
|
+
config: found.config,
|
|
65
|
+
inputs: found.scanInputs,
|
|
66
|
+
outDir: found.outDir,
|
|
67
|
+
engine: sharpImageEngine,
|
|
68
|
+
svgEngine: svgoEngine,
|
|
69
|
+
toolVersion: package_default.version,
|
|
70
|
+
ffmpeg: ffmpegStatus(found.config),
|
|
71
|
+
videoEngine: createVideoEngine({}, found.config),
|
|
72
|
+
makeScorer: makeScorerFactory(void 0, found.config),
|
|
73
|
+
needsFfmpeg: ffmpegInstallText(),
|
|
74
|
+
loadDoctor: async () => formatDoctorReport(await collectDoctorReport(doctorDeps()))
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const report = await audit({
|
|
79
|
+
projectRoot: found.projectRoot,
|
|
80
|
+
engine: sharpImageEngine,
|
|
81
|
+
svgEngine: svgoEngine,
|
|
82
|
+
inputs: found.scanInputs,
|
|
83
|
+
outDir: found.outDir,
|
|
84
|
+
staticRoot: found.project.staticRoot,
|
|
85
|
+
ffmpeg: ffmpegStatus(found.config),
|
|
86
|
+
videoEngine: createVideoEngine({}, found.config),
|
|
87
|
+
needsFfmpeg: ffmpegInstallText(),
|
|
88
|
+
policy: found.config.policy,
|
|
89
|
+
exclude: found.config.exclude,
|
|
90
|
+
ignore: found.config.auditIgnore,
|
|
91
|
+
settings: found.config.rules,
|
|
92
|
+
budgets: found.config.budgets,
|
|
93
|
+
// A bare `npx bytecut` in CI should not create folders in the repo.
|
|
94
|
+
persistCache: false
|
|
95
|
+
});
|
|
96
|
+
console.log(formatAuditReport(report));
|
|
97
|
+
console.log(
|
|
98
|
+
"\nNot an interactive terminal. Run `bytecut optimize -y` to write the optimized files."
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/cli/index.ts
|
|
103
|
+
var SUBCOMMANDS = [
|
|
104
|
+
"audit",
|
|
105
|
+
"optimize",
|
|
106
|
+
"snippet",
|
|
107
|
+
"compare",
|
|
108
|
+
"explain",
|
|
109
|
+
"init",
|
|
110
|
+
"check",
|
|
111
|
+
"doctor"
|
|
112
|
+
];
|
|
113
|
+
var main = defineCommand({
|
|
114
|
+
meta: {
|
|
115
|
+
name: "bytecut",
|
|
116
|
+
version: package_default.version,
|
|
117
|
+
description: "Audit and optimize images and videos in web projects"
|
|
118
|
+
},
|
|
119
|
+
// Plain `bytecut` (no subcommand): interactive UI on a TTY, audit report otherwise.
|
|
120
|
+
// citty also invokes the parent's `run` after a subcommand finishes, so bail out then, or
|
|
121
|
+
// every subcommand would be followed by a second, unrelated audit of the project.
|
|
122
|
+
run: guarded(async ({ rawArgs }) => {
|
|
123
|
+
const first = rawArgs.find((arg) => !arg.startsWith("-"));
|
|
124
|
+
if (first !== void 0 && SUBCOMMANDS.includes(first)) return;
|
|
125
|
+
await runDefault();
|
|
126
|
+
}),
|
|
127
|
+
subCommands: {
|
|
128
|
+
audit: () => import("./audit-HVW4EOLP.js").then((m) => m.default),
|
|
129
|
+
optimize: () => import("./optimize-UXM6JEDA.js").then((m) => m.default),
|
|
130
|
+
snippet: () => import("./snippet-2WVGMCNV.js").then((m) => m.default),
|
|
131
|
+
compare: () => import("./compare-6PCE6533.js").then((m) => m.default),
|
|
132
|
+
explain: () => import("./explain-SGN5BOR7.js").then((m) => m.default),
|
|
133
|
+
init: () => import("./init-WHWUANOB.js").then((m) => m.default),
|
|
134
|
+
check: () => import("./check-YONIQTM7.js").then((m) => m.default),
|
|
135
|
+
doctor: () => import("./doctor-R226JU6P.js").then((m) => m.default)
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
await runMain(main);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
startCompareServer
|
|
4
|
+
} from "./chunk-7IECTPJB.js";
|
|
5
|
+
import {
|
|
6
|
+
MANIFEST_FILE,
|
|
7
|
+
parseManifest
|
|
8
|
+
} from "./chunk-5R7LR5BB.js";
|
|
9
|
+
import {
|
|
10
|
+
configArg,
|
|
11
|
+
discover
|
|
12
|
+
} from "./chunk-4XOB3C34.js";
|
|
13
|
+
import "./chunk-M6ZDSATP.js";
|
|
14
|
+
import {
|
|
15
|
+
toPosix
|
|
16
|
+
} from "./chunk-WE7AEDVI.js";
|
|
17
|
+
import "./chunk-YBEC5QGV.js";
|
|
18
|
+
import {
|
|
19
|
+
guarded
|
|
20
|
+
} from "./chunk-J2XJO4JZ.js";
|
|
21
|
+
import {
|
|
22
|
+
BytecutError
|
|
23
|
+
} from "./chunk-TWI2OZJZ.js";
|
|
24
|
+
|
|
25
|
+
// src/cli/commands/compare.ts
|
|
26
|
+
import { readFile } from "fs/promises";
|
|
27
|
+
import { relative, resolve } from "path";
|
|
28
|
+
import { defineCommand } from "citty";
|
|
29
|
+
var compare_default = defineCommand({
|
|
30
|
+
meta: {
|
|
31
|
+
name: "compare",
|
|
32
|
+
description: "Serve a before/after page for one optimized file on localhost (127.0.0.1, random port)"
|
|
33
|
+
},
|
|
34
|
+
args: {
|
|
35
|
+
...configArg,
|
|
36
|
+
port: { type: "string", description: "Port to listen on (default: a free one)" },
|
|
37
|
+
"out-dir": { type: "string", description: "Output folder (default: from the framework)" }
|
|
38
|
+
},
|
|
39
|
+
run: guarded(async ({ args }) => {
|
|
40
|
+
const inputs = args._.map(String);
|
|
41
|
+
if (inputs.length !== 1) {
|
|
42
|
+
throw new BytecutError(
|
|
43
|
+
"BAD_ARGUMENTS",
|
|
44
|
+
"Name one optimized file, e.g. `bytecut compare public/img/hero.jpg`."
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
let port = 0;
|
|
48
|
+
if (args.port !== void 0) {
|
|
49
|
+
port = Number(args.port);
|
|
50
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
51
|
+
throw new BytecutError("BAD_ARGUMENTS", `--port must be 0-65535, not "${args.port}".`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const found = await discover([], args["out-dir"], args.config);
|
|
55
|
+
let text;
|
|
56
|
+
try {
|
|
57
|
+
text = await readFile(resolve(found.projectRoot, found.outDir, MANIFEST_FILE), "utf8");
|
|
58
|
+
} catch {
|
|
59
|
+
throw new BytecutError(
|
|
60
|
+
"INPUT_NOT_FOUND",
|
|
61
|
+
`No manifest at ${found.outDir}/${MANIFEST_FILE}. Run \`bytecut optimize -y\` first.`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const source = toPosix(relative(found.projectRoot, resolve(found.projectRoot, inputs[0])));
|
|
65
|
+
const server = await startCompareServer({
|
|
66
|
+
projectRoot: found.projectRoot,
|
|
67
|
+
outDir: found.outDir,
|
|
68
|
+
manifest: parseManifest(text),
|
|
69
|
+
source,
|
|
70
|
+
port
|
|
71
|
+
});
|
|
72
|
+
console.log(`Comparing ${source}`);
|
|
73
|
+
console.log(` ${server.url}`);
|
|
74
|
+
console.log("Local only (127.0.0.1). Press Ctrl-C to stop.");
|
|
75
|
+
await new Promise((done) => {
|
|
76
|
+
process.once("SIGINT", () => done());
|
|
77
|
+
process.once("SIGTERM", () => done());
|
|
78
|
+
});
|
|
79
|
+
await server.close();
|
|
80
|
+
})
|
|
81
|
+
});
|
|
82
|
+
export {
|
|
83
|
+
compare_default as default
|
|
84
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
collectDoctorReport,
|
|
4
|
+
formatDoctorReport
|
|
5
|
+
} from "./chunk-326CQQWX.js";
|
|
6
|
+
import {
|
|
7
|
+
doctorDeps
|
|
8
|
+
} from "./chunk-M6ZDSATP.js";
|
|
9
|
+
import {
|
|
10
|
+
resolveConfig
|
|
11
|
+
} from "./chunk-YBEC5QGV.js";
|
|
12
|
+
import {
|
|
13
|
+
isGitIgnored
|
|
14
|
+
} from "./chunk-DJ45NQZD.js";
|
|
15
|
+
import {
|
|
16
|
+
guarded
|
|
17
|
+
} from "./chunk-J2XJO4JZ.js";
|
|
18
|
+
import {
|
|
19
|
+
BytecutError,
|
|
20
|
+
detectProject,
|
|
21
|
+
loadConfig
|
|
22
|
+
} from "./chunk-TWI2OZJZ.js";
|
|
23
|
+
|
|
24
|
+
// src/cli/commands/doctor.ts
|
|
25
|
+
import { defineCommand } from "citty";
|
|
26
|
+
import { resolve } from "path";
|
|
27
|
+
var doctor_default = defineCommand({
|
|
28
|
+
meta: {
|
|
29
|
+
name: "doctor",
|
|
30
|
+
description: "Check Node, sharp and ffmpeg, and report what is degraded"
|
|
31
|
+
},
|
|
32
|
+
args: {
|
|
33
|
+
config: {
|
|
34
|
+
type: "string",
|
|
35
|
+
description: "Path to a config file (default: bytecut.config.* in the project)"
|
|
36
|
+
},
|
|
37
|
+
ffmpeg: { type: "string", description: "Path to the ffmpeg binary" },
|
|
38
|
+
ffprobe: { type: "string", description: "Path to the ffprobe binary" },
|
|
39
|
+
ssimulacra2: {
|
|
40
|
+
type: "string",
|
|
41
|
+
description: "Path to a native ssimulacra2 (optional; a WebAssembly scorer is built in)"
|
|
42
|
+
},
|
|
43
|
+
json: { type: "boolean", description: "Print the report as JSON" }
|
|
44
|
+
},
|
|
45
|
+
run: guarded(async ({ args }) => {
|
|
46
|
+
const projectRoot = process.cwd();
|
|
47
|
+
let configError;
|
|
48
|
+
let resolved = await resolveConfig(projectRoot, null);
|
|
49
|
+
try {
|
|
50
|
+
resolved = await resolveConfig(projectRoot, await loadConfig(projectRoot, args.config));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (!(error instanceof BytecutError)) throw error;
|
|
53
|
+
configError = error.message;
|
|
54
|
+
resolved = { ...resolved, project: await detectProject(projectRoot) };
|
|
55
|
+
}
|
|
56
|
+
const report = await collectDoctorReport(doctorDeps(), {
|
|
57
|
+
ffmpegFlag: args.ffmpeg,
|
|
58
|
+
ffprobeFlag: args.ffprobe,
|
|
59
|
+
ffmpegConfig: resolved.ffmpegPath,
|
|
60
|
+
ffprobeConfig: resolved.ffprobePath,
|
|
61
|
+
ssimulacra2Flag: args.ssimulacra2,
|
|
62
|
+
ssimulacra2Config: resolved.ssimulacra2Path
|
|
63
|
+
});
|
|
64
|
+
report.project = {
|
|
65
|
+
framework: resolved.project.framework,
|
|
66
|
+
configFile: resolved.loaded?.name ?? null,
|
|
67
|
+
...configError ? { configError } : {},
|
|
68
|
+
outDir: resolved.project.outDir,
|
|
69
|
+
outDirIgnored: await isGitIgnored(projectRoot, resolve(projectRoot, resolved.project.outDir))
|
|
70
|
+
};
|
|
71
|
+
console.log(args.json ? JSON.stringify(report, null, 2) : formatDoctorReport(report));
|
|
72
|
+
if (!report.ok) process.exitCode = 1;
|
|
73
|
+
})
|
|
74
|
+
});
|
|
75
|
+
export {
|
|
76
|
+
doctor_default as default
|
|
77
|
+
};
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
sharpImageEngine
|
|
4
|
+
} from "./chunk-XFJ3HW4X.js";
|
|
5
|
+
import {
|
|
6
|
+
classify,
|
|
7
|
+
classifyVideo,
|
|
8
|
+
mirroredDir,
|
|
9
|
+
plannedVideoOutputs,
|
|
10
|
+
targetWidths
|
|
11
|
+
} from "./chunk-NAKV7HS5.js";
|
|
12
|
+
import {
|
|
13
|
+
configArg,
|
|
14
|
+
createVideoEngine,
|
|
15
|
+
discover
|
|
16
|
+
} from "./chunk-4XOB3C34.js";
|
|
17
|
+
import "./chunk-M6ZDSATP.js";
|
|
18
|
+
import {
|
|
19
|
+
toPosix
|
|
20
|
+
} from "./chunk-WE7AEDVI.js";
|
|
21
|
+
import {
|
|
22
|
+
getVideoPreset
|
|
23
|
+
} from "./chunk-YBEC5QGV.js";
|
|
24
|
+
import {
|
|
25
|
+
guarded
|
|
26
|
+
} from "./chunk-J2XJO4JZ.js";
|
|
27
|
+
import {
|
|
28
|
+
BytecutError,
|
|
29
|
+
formatBytes,
|
|
30
|
+
matchesGlob
|
|
31
|
+
} from "./chunk-TWI2OZJZ.js";
|
|
32
|
+
|
|
33
|
+
// src/cli/commands/explain.ts
|
|
34
|
+
import { stat } from "fs/promises";
|
|
35
|
+
import { resolve as resolve2 } from "path";
|
|
36
|
+
import { defineCommand } from "citty";
|
|
37
|
+
|
|
38
|
+
// src/core/config/explain.ts
|
|
39
|
+
import { readFile } from "fs/promises";
|
|
40
|
+
import { extname, posix, relative, resolve } from "path";
|
|
41
|
+
var RASTER = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".webp", ".avif", ".gif"]);
|
|
42
|
+
var VIDEO = /* @__PURE__ */ new Set([".mp4", ".webm", ".mov", ".m4v", ".mkv"]);
|
|
43
|
+
async function explainFile(options) {
|
|
44
|
+
const { config, projectRoot } = options;
|
|
45
|
+
const absolute = resolve(projectRoot, options.file);
|
|
46
|
+
const source = toPosix(relative(projectRoot, absolute));
|
|
47
|
+
const ext = extname(source).toLowerCase();
|
|
48
|
+
const { catalog } = config;
|
|
49
|
+
const base = {
|
|
50
|
+
source,
|
|
51
|
+
outDir: config.project.outDir,
|
|
52
|
+
publicPath: config.project.publicPath,
|
|
53
|
+
excluded: false,
|
|
54
|
+
auditIgnored: config.auditIgnore.some((p) => matchesGlob(p, source))
|
|
55
|
+
};
|
|
56
|
+
const none = (kind, reason) => ({
|
|
57
|
+
...base,
|
|
58
|
+
kind,
|
|
59
|
+
decision: { by: "unavailable", reason },
|
|
60
|
+
preset: null,
|
|
61
|
+
chain: [],
|
|
62
|
+
changedBy: [],
|
|
63
|
+
resolved: null,
|
|
64
|
+
outputs: []
|
|
65
|
+
});
|
|
66
|
+
if (ext === ".svg") return none("svg", "SVG output is not implemented yet");
|
|
67
|
+
const isVideo = VIDEO.has(ext);
|
|
68
|
+
if (!isVideo && !RASTER.has(ext))
|
|
69
|
+
return none("unsupported", `${ext || "this"} is not a supported type`);
|
|
70
|
+
const matched = config.policy.assetFor(source);
|
|
71
|
+
const detail = (preset2) => ({
|
|
72
|
+
chain: catalog.origin[preset2]?.chain ?? [preset2],
|
|
73
|
+
changedBy: catalog.origin[preset2]?.overrides ?? []
|
|
74
|
+
});
|
|
75
|
+
const pick = (kind, classifier) => {
|
|
76
|
+
const known = (name) => kind === "image" ? catalog.image[name] : catalog.video[name];
|
|
77
|
+
if (options.presetFlag) {
|
|
78
|
+
if (!known(options.presetFlag)) {
|
|
79
|
+
return {
|
|
80
|
+
preset: null,
|
|
81
|
+
decision: {
|
|
82
|
+
by: "unavailable",
|
|
83
|
+
reason: `--preset ${options.presetFlag} is not a ${kind} preset`
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
preset: options.presetFlag,
|
|
89
|
+
decision: { by: "flag", reason: `forced with --preset ${options.presetFlag}` }
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (matched && known(matched.preset)) {
|
|
93
|
+
return {
|
|
94
|
+
preset: matched.preset,
|
|
95
|
+
decision: {
|
|
96
|
+
by: "assets",
|
|
97
|
+
pattern: matched.pattern,
|
|
98
|
+
reason: `matched assets "${matched.pattern}"`
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const verdict = classifier();
|
|
103
|
+
if (!verdict) {
|
|
104
|
+
return {
|
|
105
|
+
preset: null,
|
|
106
|
+
decision: {
|
|
107
|
+
by: "unavailable",
|
|
108
|
+
reason: "classifying a video needs ffprobe, which was not found"
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
preset: verdict.preset,
|
|
114
|
+
decision: { by: "classifier", reason: verdict.reason, confidence: verdict.confidence }
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
if (!isVideo) {
|
|
118
|
+
const data = await readFile(absolute);
|
|
119
|
+
const analysis = await options.engine.analyze(data);
|
|
120
|
+
if (!(analysis.animated && analysis.format === "gif")) {
|
|
121
|
+
const { preset: preset2, decision: decision2 } = pick("image", () => classify(source, analysis));
|
|
122
|
+
const resolved2 = preset2 ? catalog.image[preset2] : null;
|
|
123
|
+
const dir = mirroredDir(source, config.project.staticRoot);
|
|
124
|
+
const name = posix.basename(source, posix.extname(source));
|
|
125
|
+
const outputs = resolved2 ? targetWidths(resolved2.widths, analysis.width).flatMap(
|
|
126
|
+
(w) => resolved2.formats.map((f) => posix.join(dir, name, `${name}-${w}.${f}`))
|
|
127
|
+
) : [];
|
|
128
|
+
return {
|
|
129
|
+
...base,
|
|
130
|
+
kind: "image",
|
|
131
|
+
decision: decision2,
|
|
132
|
+
preset: preset2,
|
|
133
|
+
...preset2 ? detail(preset2) : { chain: [], changedBy: [] },
|
|
134
|
+
resolved: resolved2,
|
|
135
|
+
outputs
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const probe = options.videoEngine ? await options.videoEngine.probe(absolute).catch(() => null) : null;
|
|
140
|
+
const isGif = ext === ".gif";
|
|
141
|
+
const { preset, decision } = pick("video", () => {
|
|
142
|
+
if (isGif) return { preset: "gif-to-video", reason: "animated GIF", confidence: "high" };
|
|
143
|
+
return probe ? classifyVideo(source, probe) : null;
|
|
144
|
+
});
|
|
145
|
+
const resolved = preset ? getVideoPreset(preset, catalog) ?? null : null;
|
|
146
|
+
return {
|
|
147
|
+
...base,
|
|
148
|
+
kind: "video",
|
|
149
|
+
decision,
|
|
150
|
+
preset,
|
|
151
|
+
...preset ? detail(preset) : { chain: [], changedBy: [] },
|
|
152
|
+
resolved,
|
|
153
|
+
outputs: resolved && probe ? plannedVideoOutputs(source, config.project.staticRoot, resolved, probe).map((o) => o.path) : []
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function formatExplanation(e) {
|
|
157
|
+
const lines = [e.source];
|
|
158
|
+
lines.push(` type: ${e.kind}`);
|
|
159
|
+
if (e.preset === null) {
|
|
160
|
+
lines.push(` preset: none (${e.decision.reason})`);
|
|
161
|
+
} else {
|
|
162
|
+
const how = e.decision.by === "assets" ? `assets rule "${e.decision.pattern}"` : e.decision.by === "flag" ? "the --preset flag" : e.decision.by === "classifier" ? `classifier, ${e.decision.confidence} confidence: ${e.decision.reason}` : e.decision.reason;
|
|
163
|
+
lines.push(` preset: ${e.preset} (from ${how})`);
|
|
164
|
+
if (e.chain.length > 1 || e.changedBy.length > 0) {
|
|
165
|
+
lines.push(` extends: ${e.chain.join(" \u2192 ")}`);
|
|
166
|
+
if (e.changedBy.length > 0) lines.push(` changed: ${e.changedBy.join(", ")}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const r = e.resolved;
|
|
170
|
+
if (r && "widths" in r) {
|
|
171
|
+
lines.push(` widths: ${r.widths.join(", ")} (never above the source)`);
|
|
172
|
+
lines.push(` formats: ${r.formats.join(", ")}`);
|
|
173
|
+
for (const f of r.formats) {
|
|
174
|
+
const lossless = r.lossless ? ", lossless" : "";
|
|
175
|
+
lines.push(
|
|
176
|
+
` ${f}: quality ${r.encoders[f].quality}, effort ${r.encoders[f].effort}${lossless}`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
lines.push(` priority: ${r.priority ? "yes (fetchpriority=high, never lazy)" : "no"}`);
|
|
180
|
+
if (r.search) {
|
|
181
|
+
const parts = [`ssimulacra2 >= ${r.search.target}`];
|
|
182
|
+
if (r.search.maxBytes !== void 0) {
|
|
183
|
+
parts.push(
|
|
184
|
+
`at most ${formatBytes(r.search.maxBytes)} per file, quality may fall to a score of ${r.search.floor}`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
lines.push(
|
|
188
|
+
` search: ${parts.join("; ")} (quality is found per image; lossless outputs are exact)`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
} else if (r) {
|
|
192
|
+
lines.push(` codecs: ${r.codecs.join(", ")}`);
|
|
193
|
+
lines.push(` audio: ${r.audio === "strip" ? "removed" : "kept"}`);
|
|
194
|
+
lines.push(
|
|
195
|
+
` cap: ${r.maxWidth ? `${r.maxWidth}px wide` : "no width cap"}, ${r.maxFps ? `${r.maxFps} fps` : "no fps cap"}`
|
|
196
|
+
);
|
|
197
|
+
lines.push(` crf: av1 ${r.crf.av1}, h264 ${r.crf.h264}`);
|
|
198
|
+
if (r.search) {
|
|
199
|
+
lines.push(
|
|
200
|
+
` search: ${r.search.metric} >= ${r.search.target} on the worst of 3 sampled clips (crf above is the starting point)`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (e.outputs.length > 0) {
|
|
205
|
+
lines.push(` outputs: ${e.outDir}/`);
|
|
206
|
+
for (const o of e.outputs) lines.push(` ${o}`);
|
|
207
|
+
}
|
|
208
|
+
if (e.auditIgnored)
|
|
209
|
+
lines.push(" note: matched audit.ignore, so audit raises no findings for it");
|
|
210
|
+
return lines.join("\n");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/cli/commands/explain.ts
|
|
214
|
+
var explain_default = defineCommand({
|
|
215
|
+
meta: {
|
|
216
|
+
name: "explain",
|
|
217
|
+
description: "Show which rule chose a file's preset and what that preset resolves to"
|
|
218
|
+
},
|
|
219
|
+
args: {
|
|
220
|
+
...configArg,
|
|
221
|
+
preset: { type: "string", description: "Show the result of forcing this preset" },
|
|
222
|
+
json: { type: "boolean", description: "Print the explanation as JSON" },
|
|
223
|
+
ffmpeg: { type: "string", description: "Path to the ffmpeg binary" },
|
|
224
|
+
ffprobe: { type: "string", description: "Path to the ffprobe binary" }
|
|
225
|
+
},
|
|
226
|
+
run: guarded(async ({ args }) => {
|
|
227
|
+
const files = args._.map(String);
|
|
228
|
+
if (files.length === 0) {
|
|
229
|
+
throw new BytecutError(
|
|
230
|
+
"BAD_ARGUMENTS",
|
|
231
|
+
"Name a file, e.g. `bytecut explain public/img/hero.jpg`."
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const found = await discover([], void 0, args.config);
|
|
235
|
+
const engine = createVideoEngine({ ffmpeg: args.ffmpeg, ffprobe: args.ffprobe }, found.config);
|
|
236
|
+
const results = [];
|
|
237
|
+
for (const file of files) {
|
|
238
|
+
if (!(await stat(resolve2(found.projectRoot, file)).catch(() => null))?.isFile()) {
|
|
239
|
+
throw new BytecutError("INPUT_NOT_FOUND", `No such file: ${file}`);
|
|
240
|
+
}
|
|
241
|
+
results.push(
|
|
242
|
+
await explainFile({
|
|
243
|
+
projectRoot: found.projectRoot,
|
|
244
|
+
file,
|
|
245
|
+
config: found.config,
|
|
246
|
+
engine: sharpImageEngine,
|
|
247
|
+
videoEngine: engine,
|
|
248
|
+
presetFlag: args.preset
|
|
249
|
+
})
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
if (args.json) {
|
|
253
|
+
process.stdout.write(`${JSON.stringify({ version: 1, files: results }, null, 2)}
|
|
254
|
+
`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const where = found.config.loaded ? found.config.loaded.name : "no config (zero-config)";
|
|
258
|
+
console.log(`Config: ${where}
|
|
259
|
+
`);
|
|
260
|
+
console.log(results.map(formatExplanation).join("\n\n"));
|
|
261
|
+
})
|
|
262
|
+
});
|
|
263
|
+
export {
|
|
264
|
+
explain_default as default
|
|
265
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
isGitIgnored
|
|
4
|
+
} from "./chunk-DJ45NQZD.js";
|
|
5
|
+
import {
|
|
6
|
+
applyChoices,
|
|
7
|
+
isEmpty,
|
|
8
|
+
renderConfig
|
|
9
|
+
} from "./chunk-GMHOU4Y5.js";
|
|
10
|
+
import {
|
|
11
|
+
writeConfigFile
|
|
12
|
+
} from "./chunk-LHTI3JMC.js";
|
|
13
|
+
import {
|
|
14
|
+
guarded
|
|
15
|
+
} from "./chunk-J2XJO4JZ.js";
|
|
16
|
+
import {
|
|
17
|
+
BytecutError,
|
|
18
|
+
FRAMEWORKS,
|
|
19
|
+
PLACEHOLDERS,
|
|
20
|
+
detectProject,
|
|
21
|
+
findConfigFiles
|
|
22
|
+
} from "./chunk-TWI2OZJZ.js";
|
|
23
|
+
|
|
24
|
+
// src/cli/commands/init.ts
|
|
25
|
+
import { relative } from "path";
|
|
26
|
+
import { defineCommand } from "citty";
|
|
27
|
+
import { resolve } from "path";
|
|
28
|
+
var FILE = "bytecut.config.json";
|
|
29
|
+
var init_default = defineCommand({
|
|
30
|
+
meta: {
|
|
31
|
+
name: "init",
|
|
32
|
+
description: "Write a minimal bytecut.config.json containing only non-default choices"
|
|
33
|
+
},
|
|
34
|
+
args: {
|
|
35
|
+
"out-dir": { type: "string", description: "Output folder to record" },
|
|
36
|
+
"public-path": { type: "string", description: "URL prefix to record" },
|
|
37
|
+
framework: { type: "string", description: `Framework to record (${FRAMEWORKS.join(", ")})` },
|
|
38
|
+
placeholder: {
|
|
39
|
+
type: "string",
|
|
40
|
+
description: `Placeholder to record (${PLACEHOLDERS.join(", ")})`
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
run: guarded(async ({ args }) => {
|
|
44
|
+
const projectRoot = process.cwd();
|
|
45
|
+
const existing = await findConfigFiles(projectRoot);
|
|
46
|
+
if (existing.length > 0) {
|
|
47
|
+
throw new BytecutError(
|
|
48
|
+
"CONFIG_EXISTS",
|
|
49
|
+
`${existing[0]} already exists; edit it instead. init never overwrites a config.`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const framework = args.framework;
|
|
53
|
+
if (framework !== void 0 && !FRAMEWORKS.includes(framework)) {
|
|
54
|
+
throw new BytecutError(
|
|
55
|
+
"BAD_ARGUMENTS",
|
|
56
|
+
`--framework must be one of ${FRAMEWORKS.join(", ")}.`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
const placeholder = args.placeholder;
|
|
60
|
+
if (placeholder !== void 0 && !PLACEHOLDERS.includes(placeholder)) {
|
|
61
|
+
throw new BytecutError(
|
|
62
|
+
"BAD_ARGUMENTS",
|
|
63
|
+
`--placeholder must be one of ${PLACEHOLDERS.join(", ")}.`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
const detected = await detectProject(projectRoot);
|
|
67
|
+
const effective = await detectProject(projectRoot, { framework });
|
|
68
|
+
const choices = {
|
|
69
|
+
...args["out-dir"] && args["out-dir"] !== effective.outDir ? { outDir: args["out-dir"] } : {},
|
|
70
|
+
...args["public-path"] && args["public-path"] !== effective.publicPath ? { publicPath: args["public-path"] } : {},
|
|
71
|
+
...placeholder && placeholder !== "dominant-color" ? { placeholder } : {}
|
|
72
|
+
};
|
|
73
|
+
const data = applyChoices(
|
|
74
|
+
framework && framework !== detected.framework ? { framework } : {},
|
|
75
|
+
choices
|
|
76
|
+
);
|
|
77
|
+
const path = await writeConfigFile(projectRoot, FILE, renderConfig("json", data));
|
|
78
|
+
console.log(
|
|
79
|
+
isEmpty(choices) && !data.framework ? `Wrote ${relative(projectRoot, path)} with no overrides: everything is at its default.` : `Wrote ${relative(projectRoot, path)}:
|
|
80
|
+
${renderConfig("json", data)}`
|
|
81
|
+
);
|
|
82
|
+
const outDir = args["out-dir"] ?? effective.outDir;
|
|
83
|
+
if (await isGitIgnored(projectRoot, resolve(projectRoot, outDir))) {
|
|
84
|
+
console.warn(
|
|
85
|
+
`Note: ${outDir} is matched by a .gitignore. bytecut's outputs are meant to be committed so deploys need no encoding step. Nothing was changed.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
});
|
|
90
|
+
export {
|
|
91
|
+
init_default as default
|
|
92
|
+
};
|