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,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
package_default
|
|
4
|
+
} from "./chunk-G53LNJ5O.js";
|
|
5
|
+
import {
|
|
6
|
+
formatFindings,
|
|
7
|
+
formatPlan,
|
|
8
|
+
formatSummary,
|
|
9
|
+
optimize
|
|
10
|
+
} from "./chunk-CEZK3RQJ.js";
|
|
11
|
+
import "./chunk-3DBJKHOD.js";
|
|
12
|
+
import {
|
|
13
|
+
isPlaceholderKind,
|
|
14
|
+
parsePlan,
|
|
15
|
+
serializePlan
|
|
16
|
+
} from "./chunk-5R7LR5BB.js";
|
|
17
|
+
import {
|
|
18
|
+
sharpImageEngine
|
|
19
|
+
} from "./chunk-XFJ3HW4X.js";
|
|
20
|
+
import "./chunk-NAKV7HS5.js";
|
|
21
|
+
import {
|
|
22
|
+
configArg,
|
|
23
|
+
createVideoEngine,
|
|
24
|
+
ffmpegInstallText,
|
|
25
|
+
makeScorerFactory
|
|
26
|
+
} from "./chunk-4XOB3C34.js";
|
|
27
|
+
import "./chunk-M6ZDSATP.js";
|
|
28
|
+
import "./chunk-WE7AEDVI.js";
|
|
29
|
+
import {
|
|
30
|
+
resolveConfig
|
|
31
|
+
} from "./chunk-YBEC5QGV.js";
|
|
32
|
+
import "./chunk-LHTI3JMC.js";
|
|
33
|
+
import {
|
|
34
|
+
guarded
|
|
35
|
+
} from "./chunk-J2XJO4JZ.js";
|
|
36
|
+
import {
|
|
37
|
+
BytecutError,
|
|
38
|
+
loadConfig
|
|
39
|
+
} from "./chunk-TWI2OZJZ.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-U4KJMKI5.js";
|
|
7
|
+
import "./chunk-3DBJKHOD.js";
|
|
8
|
+
import {
|
|
9
|
+
MANIFEST_FILE,
|
|
10
|
+
parseManifest
|
|
11
|
+
} from "./chunk-5R7LR5BB.js";
|
|
12
|
+
import {
|
|
13
|
+
configArg,
|
|
14
|
+
discover
|
|
15
|
+
} from "./chunk-4XOB3C34.js";
|
|
16
|
+
import "./chunk-M6ZDSATP.js";
|
|
17
|
+
import {
|
|
18
|
+
toPosix
|
|
19
|
+
} from "./chunk-WE7AEDVI.js";
|
|
20
|
+
import "./chunk-YBEC5QGV.js";
|
|
21
|
+
import {
|
|
22
|
+
guarded
|
|
23
|
+
} from "./chunk-J2XJO4JZ.js";
|
|
24
|
+
import {
|
|
25
|
+
BytecutError
|
|
26
|
+
} from "./chunk-TWI2OZJZ.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
|
+
};
|