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.
- package/LICENSE +21 -0
- package/README.md +140 -2
- package/dist/audit-PEKVNXKA.js +97 -0
- package/dist/check-BYNAWCDE.js +348 -0
- package/dist/chunk-3DBJKHOD.js +109 -0
- package/dist/chunk-3PF6BBFK.js +490 -0
- package/dist/chunk-4FIB6VNC.js +59 -0
- package/dist/chunk-4PWCQ4NF.js +620 -0
- package/dist/chunk-7DHE25UE.js +319 -0
- package/dist/chunk-AYYKUSRP.js +57 -0
- package/dist/chunk-CSFBJ3UH.js +259 -0
- package/dist/chunk-DJ45NQZD.js +29 -0
- package/dist/chunk-FY53B4NB.js +1439 -0
- package/dist/chunk-IZ4JEL3Q.js +212 -0
- package/dist/chunk-KN5QOB7R.js +186 -0
- package/dist/chunk-LTWUPQDT.js +26 -0
- package/dist/chunk-MV6J66AP.js +21 -0
- package/dist/chunk-P5PFI3AO.js +2170 -0
- package/dist/chunk-PQRBLHCC.js +188 -0
- package/dist/chunk-QOG2F4SF.js +536 -0
- package/dist/chunk-S3FQYUTI.js +59 -0
- package/dist/chunk-TCQFQ354.js +282 -0
- package/dist/chunk-WZ6MMRNB.js +238 -0
- package/dist/chunk-XUBQWBWH.js +382 -0
- package/dist/cli.js +138 -0
- package/dist/compare-MPPGTUC5.js +84 -0
- package/dist/doctor-PWCUEBD5.js +77 -0
- package/dist/explain-TS6XF5TX.js +267 -0
- package/dist/init-CWDWVJ55.js +92 -0
- package/dist/optimize-47YALM3D.js +216 -0
- package/dist/snippet-IIPVF6PS.js +114 -0
- package/dist/tui-CDF23I73.js +1691 -0
- package/package.json +46 -3
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
sharpImageEngine
|
|
4
|
+
} from "./chunk-CSFBJ3UH.js";
|
|
5
|
+
import {
|
|
6
|
+
classify,
|
|
7
|
+
classifyVideo,
|
|
8
|
+
mirroredDir,
|
|
9
|
+
outputWidths,
|
|
10
|
+
plannedVideoOutputs
|
|
11
|
+
} from "./chunk-XUBQWBWH.js";
|
|
12
|
+
import {
|
|
13
|
+
configArg,
|
|
14
|
+
createVideoEngine,
|
|
15
|
+
discover
|
|
16
|
+
} from "./chunk-KN5QOB7R.js";
|
|
17
|
+
import "./chunk-S3FQYUTI.js";
|
|
18
|
+
import {
|
|
19
|
+
toPosix
|
|
20
|
+
} from "./chunk-QOG2F4SF.js";
|
|
21
|
+
import {
|
|
22
|
+
getVideoPreset
|
|
23
|
+
} from "./chunk-3PF6BBFK.js";
|
|
24
|
+
import {
|
|
25
|
+
guarded
|
|
26
|
+
} from "./chunk-MV6J66AP.js";
|
|
27
|
+
import {
|
|
28
|
+
BytecutError,
|
|
29
|
+
formatBytes,
|
|
30
|
+
matchesGlob
|
|
31
|
+
} from "./chunk-FY53B4NB.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 ? outputWidths(resolved2, analysis.width, analysis.height).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(
|
|
172
|
+
r.maxEdge !== void 0 ? ` size: longest side up to ${r.maxEdge}px (width if landscape, height if portrait; never above the source)` : ` widths: ${r.widths.join(", ")} (never above the source)`
|
|
173
|
+
);
|
|
174
|
+
lines.push(` formats: ${r.formats.join(", ")}`);
|
|
175
|
+
for (const f of r.formats) {
|
|
176
|
+
const lossless = r.lossless ? ", lossless" : "";
|
|
177
|
+
lines.push(
|
|
178
|
+
` ${f}: quality ${r.encoders[f].quality}, effort ${r.encoders[f].effort}${lossless}`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
lines.push(` priority: ${r.priority ? "yes (fetchpriority=high, never lazy)" : "no"}`);
|
|
182
|
+
if (r.search) {
|
|
183
|
+
const parts = [`ssimulacra2 >= ${r.search.target}`];
|
|
184
|
+
if (r.search.maxBytes !== void 0) {
|
|
185
|
+
parts.push(
|
|
186
|
+
`at most ${formatBytes(r.search.maxBytes)} per file, quality may fall to a score of ${r.search.floor}`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
lines.push(
|
|
190
|
+
` search: ${parts.join("; ")} (quality is found per image; lossless outputs are exact)`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
} else if (r) {
|
|
194
|
+
lines.push(` codecs: ${r.codecs.join(", ")}`);
|
|
195
|
+
lines.push(` audio: ${r.audio === "strip" ? "removed" : "kept"}`);
|
|
196
|
+
lines.push(
|
|
197
|
+
` cap: ${r.maxWidth ? `${r.maxWidth}px wide` : "no width cap"}, ${r.maxFps ? `${r.maxFps} fps` : "no fps cap"}`
|
|
198
|
+
);
|
|
199
|
+
lines.push(` crf: av1 ${r.crf.av1}, h264 ${r.crf.h264}`);
|
|
200
|
+
if (r.search) {
|
|
201
|
+
lines.push(
|
|
202
|
+
` search: ${r.search.metric} >= ${r.search.target} on the worst of 3 sampled clips (crf above is the starting point)`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (e.outputs.length > 0) {
|
|
207
|
+
lines.push(` outputs: ${e.outDir}/`);
|
|
208
|
+
for (const o of e.outputs) lines.push(` ${o}`);
|
|
209
|
+
}
|
|
210
|
+
if (e.auditIgnored)
|
|
211
|
+
lines.push(" note: matched audit.ignore, so audit raises no findings for it");
|
|
212
|
+
return lines.join("\n");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/cli/commands/explain.ts
|
|
216
|
+
var explain_default = defineCommand({
|
|
217
|
+
meta: {
|
|
218
|
+
name: "explain",
|
|
219
|
+
description: "Show which rule chose a file's preset and what that preset resolves to"
|
|
220
|
+
},
|
|
221
|
+
args: {
|
|
222
|
+
...configArg,
|
|
223
|
+
preset: { type: "string", description: "Show the result of forcing this preset" },
|
|
224
|
+
json: { type: "boolean", description: "Print the explanation as JSON" },
|
|
225
|
+
ffmpeg: { type: "string", description: "Path to the ffmpeg binary" },
|
|
226
|
+
ffprobe: { type: "string", description: "Path to the ffprobe binary" }
|
|
227
|
+
},
|
|
228
|
+
run: guarded(async ({ args }) => {
|
|
229
|
+
const files = args._.map(String);
|
|
230
|
+
if (files.length === 0) {
|
|
231
|
+
throw new BytecutError(
|
|
232
|
+
"BAD_ARGUMENTS",
|
|
233
|
+
"Name a file, e.g. `bytecut explain public/img/hero.jpg`."
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
const found = await discover([], void 0, args.config);
|
|
237
|
+
const engine = createVideoEngine({ ffmpeg: args.ffmpeg, ffprobe: args.ffprobe }, found.config);
|
|
238
|
+
const results = [];
|
|
239
|
+
for (const file of files) {
|
|
240
|
+
if (!(await stat(resolve2(found.projectRoot, file)).catch(() => null))?.isFile()) {
|
|
241
|
+
throw new BytecutError("INPUT_NOT_FOUND", `No such file: ${file}`);
|
|
242
|
+
}
|
|
243
|
+
results.push(
|
|
244
|
+
await explainFile({
|
|
245
|
+
projectRoot: found.projectRoot,
|
|
246
|
+
file,
|
|
247
|
+
config: found.config,
|
|
248
|
+
engine: sharpImageEngine,
|
|
249
|
+
videoEngine: engine,
|
|
250
|
+
presetFlag: args.preset
|
|
251
|
+
})
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
if (args.json) {
|
|
255
|
+
process.stdout.write(`${JSON.stringify({ version: 1, files: results }, null, 2)}
|
|
256
|
+
`);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const where = found.config.loaded ? found.config.loaded.name : "no config (zero-config)";
|
|
260
|
+
console.log(`Config: ${where}
|
|
261
|
+
`);
|
|
262
|
+
console.log(results.map(formatExplanation).join("\n\n"));
|
|
263
|
+
})
|
|
264
|
+
});
|
|
265
|
+
export {
|
|
266
|
+
explain_default as default
|
|
267
|
+
};
|
|
@@ -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-4FIB6VNC.js";
|
|
10
|
+
import {
|
|
11
|
+
writeConfigFile
|
|
12
|
+
} from "./chunk-IZ4JEL3Q.js";
|
|
13
|
+
import {
|
|
14
|
+
guarded
|
|
15
|
+
} from "./chunk-MV6J66AP.js";
|
|
16
|
+
import {
|
|
17
|
+
BytecutError,
|
|
18
|
+
FRAMEWORKS,
|
|
19
|
+
PLACEHOLDERS,
|
|
20
|
+
detectProject,
|
|
21
|
+
findConfigFiles
|
|
22
|
+
} from "./chunk-FY53B4NB.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
|
+
};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
package_default
|
|
4
|
+
} from "./chunk-AYYKUSRP.js";
|
|
5
|
+
import {
|
|
6
|
+
formatFindings,
|
|
7
|
+
formatPlan,
|
|
8
|
+
formatSummary,
|
|
9
|
+
optimize
|
|
10
|
+
} from "./chunk-P5PFI3AO.js";
|
|
11
|
+
import "./chunk-3DBJKHOD.js";
|
|
12
|
+
import {
|
|
13
|
+
isPlaceholderKind,
|
|
14
|
+
parsePlan,
|
|
15
|
+
serializePlan
|
|
16
|
+
} from "./chunk-TCQFQ354.js";
|
|
17
|
+
import {
|
|
18
|
+
sharpImageEngine
|
|
19
|
+
} from "./chunk-CSFBJ3UH.js";
|
|
20
|
+
import "./chunk-XUBQWBWH.js";
|
|
21
|
+
import {
|
|
22
|
+
configArg,
|
|
23
|
+
createVideoEngine,
|
|
24
|
+
ffmpegInstallText,
|
|
25
|
+
makeScorerFactory
|
|
26
|
+
} from "./chunk-KN5QOB7R.js";
|
|
27
|
+
import "./chunk-S3FQYUTI.js";
|
|
28
|
+
import "./chunk-QOG2F4SF.js";
|
|
29
|
+
import {
|
|
30
|
+
resolveConfig
|
|
31
|
+
} from "./chunk-3PF6BBFK.js";
|
|
32
|
+
import "./chunk-IZ4JEL3Q.js";
|
|
33
|
+
import {
|
|
34
|
+
guarded
|
|
35
|
+
} from "./chunk-MV6J66AP.js";
|
|
36
|
+
import {
|
|
37
|
+
BytecutError,
|
|
38
|
+
loadConfig
|
|
39
|
+
} from "./chunk-FY53B4NB.js";
|
|
40
|
+
|
|
41
|
+
// src/cli/commands/optimize.ts
|
|
42
|
+
import { existsSync } from "fs";
|
|
43
|
+
import { readFile } from "fs/promises";
|
|
44
|
+
import { availableParallelism } from "os";
|
|
45
|
+
import { resolve } from "path";
|
|
46
|
+
import { defineCommand } from "citty";
|
|
47
|
+
var FRAMEWORK_NAMES = {
|
|
48
|
+
next: "Next.js",
|
|
49
|
+
astro: "Astro",
|
|
50
|
+
vite: "Vite",
|
|
51
|
+
static: "a static site"
|
|
52
|
+
};
|
|
53
|
+
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
54
|
+
var optimize_default = defineCommand({
|
|
55
|
+
meta: {
|
|
56
|
+
name: "optimize",
|
|
57
|
+
description: "Optimize images into the output folder (sources are never modified)"
|
|
58
|
+
},
|
|
59
|
+
args: {
|
|
60
|
+
...configArg,
|
|
61
|
+
yes: {
|
|
62
|
+
type: "boolean",
|
|
63
|
+
alias: "y",
|
|
64
|
+
description: "Write outputs; without it only the plan is printed"
|
|
65
|
+
},
|
|
66
|
+
"dry-run": { type: "boolean", description: "Print the plan and write nothing" },
|
|
67
|
+
json: { type: "boolean", description: "With --dry-run: print the plan as JSON on stdout" },
|
|
68
|
+
plan: { type: "string", description: "Execute a plan saved with --dry-run --json" },
|
|
69
|
+
force: {
|
|
70
|
+
type: "boolean",
|
|
71
|
+
description: "Re-encode unchanged files and overwrite outputs bytecut does not own"
|
|
72
|
+
},
|
|
73
|
+
"out-dir": {
|
|
74
|
+
type: "string",
|
|
75
|
+
description: "Output folder (default: from the detected framework)"
|
|
76
|
+
},
|
|
77
|
+
"public-path": { type: "string", description: "URL prefix for outputs (default: /_bytecut)" },
|
|
78
|
+
preset: {
|
|
79
|
+
type: "string",
|
|
80
|
+
description: "Force a preset (images: photo, hero, icon; video: background-video, content-video, gif-to-video)"
|
|
81
|
+
},
|
|
82
|
+
placeholder: {
|
|
83
|
+
type: "string",
|
|
84
|
+
description: "Placeholder to record in the manifest: dominant-color (default), lqip, blurhash or none"
|
|
85
|
+
},
|
|
86
|
+
ffmpeg: {
|
|
87
|
+
type: "string",
|
|
88
|
+
description: "Path to the ffmpeg binary (otherwise BYTECUT_FFMPEG, then PATH)"
|
|
89
|
+
},
|
|
90
|
+
ffprobe: { type: "string", description: "Path to the ffprobe binary" },
|
|
91
|
+
ssimulacra2: {
|
|
92
|
+
type: "string",
|
|
93
|
+
description: "Path to a native ssimulacra2 for quality searches (otherwise BYTECUT_SSIMULACRA2, then PATH, then the built-in scorer)"
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
run: guarded(async ({ args }) => {
|
|
97
|
+
const inputs = args._.map(String);
|
|
98
|
+
const planFile = args.plan;
|
|
99
|
+
const dryRun = Boolean(args["dry-run"]) || !args.yes;
|
|
100
|
+
if (planFile && (inputs.length > 0 || args["out-dir"] || args["public-path"])) {
|
|
101
|
+
throw new BytecutError(
|
|
102
|
+
"BAD_ARGUMENTS",
|
|
103
|
+
"--plan already fixes the sources, output folder and public path; don't combine it with paths, --out-dir or --public-path."
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (planFile && args.preset) {
|
|
107
|
+
throw new BytecutError("BAD_ARGUMENTS", "--plan already fixes each file's preset.");
|
|
108
|
+
}
|
|
109
|
+
if (args.json && !dryRun) {
|
|
110
|
+
throw new BytecutError(
|
|
111
|
+
"BAD_ARGUMENTS",
|
|
112
|
+
"--json is only supported with --dry-run for now (stdout would mix two formats)."
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
let savedPlan;
|
|
116
|
+
if (planFile) {
|
|
117
|
+
const text = await readFile(resolve(planFile), "utf8").catch(() => {
|
|
118
|
+
throw new BytecutError("INPUT_NOT_FOUND", `Cannot read plan file: ${planFile}`);
|
|
119
|
+
});
|
|
120
|
+
savedPlan = parsePlan(text, planFile);
|
|
121
|
+
}
|
|
122
|
+
let placeholder;
|
|
123
|
+
if (args.placeholder !== void 0) {
|
|
124
|
+
const wanted = String(args.placeholder);
|
|
125
|
+
if (wanted !== "none" && !isPlaceholderKind(wanted)) {
|
|
126
|
+
throw new BytecutError(
|
|
127
|
+
"BAD_ARGUMENTS",
|
|
128
|
+
`Unknown placeholder "${wanted}". Use dominant-color, lqip, blurhash or none.`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
placeholder = wanted;
|
|
132
|
+
}
|
|
133
|
+
const controller = new AbortController();
|
|
134
|
+
const onSigint = () => controller.abort();
|
|
135
|
+
if (!dryRun) process.once("SIGINT", onSigint);
|
|
136
|
+
const projectRoot = process.cwd();
|
|
137
|
+
const config = await resolveConfig(projectRoot, await loadConfig(projectRoot, args.config));
|
|
138
|
+
const project = config.project;
|
|
139
|
+
placeholder ??= config.placeholder;
|
|
140
|
+
let scanInputs = inputs;
|
|
141
|
+
if (!savedPlan && inputs.length === 0) {
|
|
142
|
+
scanInputs = project.include.filter((p) => existsSync(resolve(projectRoot, p)));
|
|
143
|
+
if (scanInputs.length === 0) {
|
|
144
|
+
console.error(
|
|
145
|
+
`bytecut optimize: no asset folder found (looked for ${project.include.join(", ")}). Pass files or folders.`
|
|
146
|
+
);
|
|
147
|
+
process.exitCode = 2;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
console.error(
|
|
151
|
+
`Detected ${FRAMEWORK_NAMES[project.framework]}: scanning ${scanInputs.join(", ")} \u2192 ${args["out-dir"] ?? project.outDir}`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
const outcome = await optimize({
|
|
155
|
+
projectRoot,
|
|
156
|
+
engine: sharpImageEngine,
|
|
157
|
+
toolVersion: package_default.version,
|
|
158
|
+
...savedPlan ? { savedPlan } : { inputs: scanInputs },
|
|
159
|
+
outDir: savedPlan?.outDir ?? args["out-dir"] ?? project.outDir,
|
|
160
|
+
staticRoot: project.staticRoot,
|
|
161
|
+
publicPath: args["public-path"] ?? project.publicPath,
|
|
162
|
+
policy: config.policy,
|
|
163
|
+
exclude: config.exclude,
|
|
164
|
+
makeScorer: makeScorerFactory(args.ssimulacra2, config),
|
|
165
|
+
...args.preset ? { presetOverride: args.preset } : {},
|
|
166
|
+
...placeholder ? { placeholder } : {},
|
|
167
|
+
execute: !dryRun,
|
|
168
|
+
force: Boolean(args.force),
|
|
169
|
+
concurrency: config.concurrency ?? Math.min(4, availableParallelism()),
|
|
170
|
+
videoEngine: createVideoEngine({ ffmpeg: args.ffmpeg, ffprobe: args.ffprobe }, config),
|
|
171
|
+
needsFfmpeg: ffmpegInstallText(),
|
|
172
|
+
signal: controller.signal,
|
|
173
|
+
onItemDone: (item, done, total) => console.error(`[${done}/${total}] ${item.source}`),
|
|
174
|
+
onVideoItemDone: (item, done, total) => console.error(`[video ${done}/${total}] ${item.source}`)
|
|
175
|
+
});
|
|
176
|
+
process.off("SIGINT", onSigint);
|
|
177
|
+
for (const file of outcome.unsupported) {
|
|
178
|
+
console.warn(`Skipping ${file}: not a supported image (JPEG, PNG, WebP, AVIF).`);
|
|
179
|
+
}
|
|
180
|
+
if (outcome.svgs.length > 0) {
|
|
181
|
+
console.warn(
|
|
182
|
+
`Skipping ${plural(outcome.svgs.length, "SVG file")}: SVG output is not implemented yet (see \`bytecut audit\`).`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
if (outcome.plan.items.length === 0 && (outcome.plan.videos ?? []).length === 0) {
|
|
186
|
+
console.log("No images or videos found.");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const findings = formatFindings(outcome.findings);
|
|
190
|
+
for (const note of outcome.notes) console.error(`Note: ${note}`);
|
|
191
|
+
if (dryRun) {
|
|
192
|
+
if (args.json) {
|
|
193
|
+
process.stdout.write(serializePlan(outcome.plan));
|
|
194
|
+
if (findings) console.error(findings);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
console.log(formatPlan(outcome.plan, outcome.cache));
|
|
198
|
+
if (findings) console.log(`
|
|
199
|
+
${findings}`);
|
|
200
|
+
console.log("\nDry run: nothing written. Re-run with -y to write these outputs.");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const result = outcome.result;
|
|
204
|
+
console.log(formatSummary(result));
|
|
205
|
+
for (const warning of result.warnings) console.warn(`warning: ${warning}`);
|
|
206
|
+
if (findings) console.log(`
|
|
207
|
+
${findings}`);
|
|
208
|
+
if (result.cancelled) process.exitCode = 130;
|
|
209
|
+
else if (result.items.some((i) => i.status === "failed") || (result.videos ?? []).some((v) => v.status === "failed")) {
|
|
210
|
+
process.exitCode = 1;
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
});
|
|
214
|
+
export {
|
|
215
|
+
optimize_default as default
|
|
216
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
buildSnippet,
|
|
4
|
+
originalUrl,
|
|
5
|
+
snippetFormatFor
|
|
6
|
+
} from "./chunk-WZ6MMRNB.js";
|
|
7
|
+
import "./chunk-3DBJKHOD.js";
|
|
8
|
+
import {
|
|
9
|
+
MANIFEST_FILE,
|
|
10
|
+
parseManifest
|
|
11
|
+
} from "./chunk-TCQFQ354.js";
|
|
12
|
+
import {
|
|
13
|
+
configArg,
|
|
14
|
+
discover
|
|
15
|
+
} from "./chunk-KN5QOB7R.js";
|
|
16
|
+
import "./chunk-S3FQYUTI.js";
|
|
17
|
+
import {
|
|
18
|
+
toPosix
|
|
19
|
+
} from "./chunk-QOG2F4SF.js";
|
|
20
|
+
import "./chunk-3PF6BBFK.js";
|
|
21
|
+
import {
|
|
22
|
+
guarded
|
|
23
|
+
} from "./chunk-MV6J66AP.js";
|
|
24
|
+
import {
|
|
25
|
+
BytecutError
|
|
26
|
+
} from "./chunk-FY53B4NB.js";
|
|
27
|
+
|
|
28
|
+
// src/cli/commands/snippet.ts
|
|
29
|
+
import { readFile } from "fs/promises";
|
|
30
|
+
import { relative, resolve } from "path";
|
|
31
|
+
import { defineCommand } from "citty";
|
|
32
|
+
var snippet_default = defineCommand({
|
|
33
|
+
meta: {
|
|
34
|
+
name: "snippet",
|
|
35
|
+
description: "Print the <picture> / <video> markup for files that have been optimized"
|
|
36
|
+
},
|
|
37
|
+
args: {
|
|
38
|
+
...configArg,
|
|
39
|
+
format: { type: "string", description: "html or jsx (default: from the detected framework)" },
|
|
40
|
+
alt: { type: "string", description: "Alt text for images (default: empty, i.e. decorative)" },
|
|
41
|
+
sizes: { type: "string", description: 'The sizes attribute (default: "100vw")' },
|
|
42
|
+
class: { type: "string", description: "A class (className in JSX) to add to the element" },
|
|
43
|
+
priority: {
|
|
44
|
+
type: "boolean",
|
|
45
|
+
description: 'Mark as the main image: fetchpriority="high", no lazy loading'
|
|
46
|
+
},
|
|
47
|
+
lazy: { type: "boolean", description: "Lazy-load even a priority image" },
|
|
48
|
+
"out-dir": { type: "string", description: "Output folder (default: from the framework)" },
|
|
49
|
+
json: { type: "boolean", description: "Print structured data instead of markup" }
|
|
50
|
+
},
|
|
51
|
+
run: guarded(async ({ args }) => {
|
|
52
|
+
const inputs = args._.map(String);
|
|
53
|
+
if (inputs.length === 0) {
|
|
54
|
+
throw new BytecutError(
|
|
55
|
+
"BAD_ARGUMENTS",
|
|
56
|
+
"Name at least one optimized file, e.g. `bytecut snippet public/img/hero.jpg`."
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
const found = await discover([], args["out-dir"], args.config);
|
|
60
|
+
const format = args.format ?? snippetFormatFor(found.project);
|
|
61
|
+
if (format !== "html" && format !== "jsx") {
|
|
62
|
+
throw new BytecutError("BAD_ARGUMENTS", `--format must be html or jsx, not "${format}".`);
|
|
63
|
+
}
|
|
64
|
+
let manifestText;
|
|
65
|
+
try {
|
|
66
|
+
manifestText = await readFile(
|
|
67
|
+
resolve(found.projectRoot, found.outDir, MANIFEST_FILE),
|
|
68
|
+
"utf8"
|
|
69
|
+
);
|
|
70
|
+
} catch {
|
|
71
|
+
throw new BytecutError(
|
|
72
|
+
"INPUT_NOT_FOUND",
|
|
73
|
+
`No manifest at ${found.outDir}/${MANIFEST_FILE}. Run \`bytecut optimize -y\` first.`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const manifest = parseManifest(manifestText);
|
|
77
|
+
const results = [];
|
|
78
|
+
for (const input of inputs) {
|
|
79
|
+
const source = toPosix(relative(found.projectRoot, resolve(found.projectRoot, input)));
|
|
80
|
+
const snippet = buildSnippet(
|
|
81
|
+
manifest,
|
|
82
|
+
{ source, originalUrl: originalUrl(source, found.project.staticRoot) },
|
|
83
|
+
{
|
|
84
|
+
format,
|
|
85
|
+
...args.alt !== void 0 ? { alt: String(args.alt) } : {},
|
|
86
|
+
...args.sizes ? { sizes: String(args.sizes) } : {},
|
|
87
|
+
...args.class ? { className: String(args.class) } : {},
|
|
88
|
+
...args.priority ? { priority: true } : {},
|
|
89
|
+
...args.lazy ? { lazy: true } : {}
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
if (!snippet) {
|
|
93
|
+
throw new BytecutError(
|
|
94
|
+
"INPUT_NOT_FOUND",
|
|
95
|
+
`${source} has not been optimized yet. Run \`bytecut optimize -y ${input}\` first.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
results.push(snippet);
|
|
99
|
+
}
|
|
100
|
+
if (args.json) {
|
|
101
|
+
process.stdout.write(
|
|
102
|
+
`${JSON.stringify({ version: 1, format, snippets: results }, null, 2)}
|
|
103
|
+
`
|
|
104
|
+
);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
console.log(results.map((s) => s.code).join("\n\n"));
|
|
108
|
+
for (const s of results)
|
|
109
|
+
for (const hint of s.hints) console.error(`Note (${s.source}): ${hint}`);
|
|
110
|
+
})
|
|
111
|
+
});
|
|
112
|
+
export {
|
|
113
|
+
snippet_default as default
|
|
114
|
+
};
|