bytecut 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ BytecutError
4
+ } from "./chunk-FY53B4NB.js";
5
+
6
+ // src/core/plan/schema.ts
7
+ import { z } from "zod";
8
+ var safeRelativePath = z.string().min(1).refine(
9
+ (p) => !p.startsWith("/") && !/^[A-Za-z]:/.test(p) && !p.includes("\\") && p.split("/").every((s) => s !== "" && s !== "." && s !== ".."),
10
+ "must be a relative POSIX path without '..' segments"
11
+ );
12
+ var outputSchema = z.object({
13
+ path: safeRelativePath,
14
+ format: z.enum(["jpg", "png", "webp", "avif"]),
15
+ width: z.number().int().positive(),
16
+ lossless: z.boolean(),
17
+ quality: z.number().int().min(1).max(100),
18
+ effort: z.number().int().min(0).max(10),
19
+ search: z.object({
20
+ metric: z.literal("ssimulacra2"),
21
+ target: z.number().min(1).max(100),
22
+ floor: z.number().min(1).max(100).optional(),
23
+ maxBytes: z.number().int().positive().optional()
24
+ }).strict().optional()
25
+ }).strict();
26
+ var itemSchema = z.object({
27
+ source: safeRelativePath,
28
+ sourceBytes: z.number().int().nonnegative(),
29
+ sourceSha256: z.string().regex(/^[0-9a-f]{64}$/),
30
+ sourceWidth: z.number().int().nonnegative(),
31
+ sourceHeight: z.number().int().nonnegative(),
32
+ preset: z.string().min(1),
33
+ classification: z.object({
34
+ reason: z.string(),
35
+ confidence: z.enum(["high", "medium", "low"]).optional()
36
+ }).strict().optional(),
37
+ outputs: z.array(outputSchema),
38
+ skip: z.object({ reason: z.string() }).strict().optional()
39
+ }).strict();
40
+ var videoOutputSchema = z.discriminatedUnion("role", [
41
+ z.object({
42
+ role: z.literal("video"),
43
+ path: safeRelativePath,
44
+ codec: z.enum(["av1", "h264"]),
45
+ width: z.number().int().positive(),
46
+ crf: z.number().int().min(0).max(63),
47
+ speed: z.string().min(1),
48
+ maxWidth: z.number().int().positive().nullable(),
49
+ maxFps: z.number().positive().nullable(),
50
+ audio: z.enum(["strip", "keep"]),
51
+ audioBitrate: z.string().regex(/^\d+k$/),
52
+ search: z.object({ metric: z.enum(["ssimulacra2", "vmaf"]), target: z.number().min(1).max(100) }).strict().optional()
53
+ }).strict(),
54
+ z.object({
55
+ role: z.literal("poster"),
56
+ path: safeRelativePath,
57
+ format: z.literal("avif"),
58
+ width: z.number().int().positive(),
59
+ quality: z.number().int().min(1).max(100),
60
+ effort: z.number().int().min(0).max(9),
61
+ at: z.number().min(0).max(1)
62
+ }).strict()
63
+ ]);
64
+ var videoItemSchema = z.object({
65
+ source: safeRelativePath,
66
+ sourceBytes: z.number().int().nonnegative(),
67
+ sourceSha256: z.string().regex(/^[0-9a-f]{64}$/),
68
+ sourceWidth: z.number().int().nonnegative(),
69
+ sourceHeight: z.number().int().nonnegative(),
70
+ durationSec: z.number().nonnegative(),
71
+ fps: z.number().nonnegative(),
72
+ hasAudio: z.boolean(),
73
+ videoCodec: z.string(),
74
+ preset: z.string().min(1),
75
+ classification: z.object({
76
+ reason: z.string(),
77
+ confidence: z.enum(["high", "medium", "low"]).optional()
78
+ }).strict().optional(),
79
+ outputs: z.array(videoOutputSchema),
80
+ skip: z.object({ reason: z.string() }).strict().optional()
81
+ }).strict();
82
+ var planSchema = z.object({
83
+ version: z.literal(1),
84
+ // May legitimately contain `..` (an output folder beside the project); SafeFs validates it.
85
+ outDir: z.string().min(1).refine((p) => !p.startsWith("/") && !/^[A-Za-z]:/.test(p) && !p.includes("\\")),
86
+ publicPath: z.string().min(1),
87
+ items: z.array(itemSchema),
88
+ videos: z.array(videoItemSchema).optional()
89
+ }).strict().superRefine((plan, ctx) => {
90
+ const seen = /* @__PURE__ */ new Set();
91
+ const outputs = [
92
+ ...plan.items.flatMap((i) => i.outputs),
93
+ ...(plan.videos ?? []).flatMap((v) => v.outputs)
94
+ ];
95
+ for (const out of outputs) {
96
+ if (seen.has(out.path)) {
97
+ ctx.addIssue({ code: "custom", message: `duplicate output path ${out.path}` });
98
+ }
99
+ seen.add(out.path);
100
+ }
101
+ });
102
+ function formatZodIssues(error) {
103
+ return error.issues.slice(0, 5).map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
104
+ }
105
+ function parsePlan(text, origin = "plan") {
106
+ let json;
107
+ try {
108
+ json = JSON.parse(text);
109
+ } catch {
110
+ throw new BytecutError("PLAN_INVALID", `${origin} is not valid JSON.`);
111
+ }
112
+ const result = planSchema.safeParse(json);
113
+ if (!result.success) {
114
+ throw new BytecutError(
115
+ "PLAN_INVALID",
116
+ `${origin} is not a valid bytecut plan: ${formatZodIssues(result.error)}`
117
+ );
118
+ }
119
+ return result.data;
120
+ }
121
+ function serializePlan(plan) {
122
+ return `${JSON.stringify(plan, null, 2)}
123
+ `;
124
+ }
125
+
126
+ // src/core/placeholders/types.ts
127
+ var PLACEHOLDER_KINDS = ["dominant-color", "lqip", "blurhash"];
128
+ function isPlaceholderKind(value) {
129
+ return PLACEHOLDER_KINDS.includes(value);
130
+ }
131
+
132
+ // src/core/manifest/types.ts
133
+ var MANIFEST_FILE = "bytecut-manifest.json";
134
+ var MANIFEST_VERSION = 3;
135
+ var MANIFEST_MIN_VERSION = 2;
136
+
137
+ // src/core/manifest/io.ts
138
+ import { z as z2 } from "zod";
139
+ var outputSchema2 = z2.object({
140
+ path: z2.string().min(1),
141
+ url: z2.string(),
142
+ format: z2.enum(["jpg", "png", "webp", "avif"]),
143
+ width: z2.number().int().nonnegative(),
144
+ height: z2.number().int().nonnegative(),
145
+ bytes: z2.number().int().nonnegative(),
146
+ sha256: z2.string().regex(/^[0-9a-f]{64}$/).optional(),
147
+ lossless: z2.boolean(),
148
+ quality: z2.number().int(),
149
+ effort: z2.number().int(),
150
+ encoder: z2.string(),
151
+ status: z2.enum(["written", "already-optimal", "stale"]),
152
+ search: z2.object({
153
+ metric: z2.literal("ssimulacra2"),
154
+ target: z2.number(),
155
+ floor: z2.number().optional(),
156
+ maxBytes: z2.number().int().optional(),
157
+ plannedPath: z2.string().optional()
158
+ }).strict().optional(),
159
+ score: z2.number().optional(),
160
+ scorer: z2.enum(["wasm", "native"]).optional()
161
+ }).strict();
162
+ var placeholderSchema = z2.object({ kind: z2.enum(PLACEHOLDER_KINDS), value: z2.string() }).strict();
163
+ var videoOutputSchema2 = z2.object({
164
+ path: z2.string().min(1),
165
+ url: z2.string(),
166
+ role: z2.enum(["video", "poster"]),
167
+ format: z2.enum(["mp4", "avif"]),
168
+ codec: z2.enum(["av1", "h264"]).optional(),
169
+ width: z2.number().int().nonnegative(),
170
+ height: z2.number().int().nonnegative(),
171
+ fps: z2.number().nonnegative().optional(),
172
+ bytes: z2.number().int().nonnegative(),
173
+ sha256: z2.string().regex(/^[0-9a-f]{64}$/).optional(),
174
+ crf: z2.number().int().optional(),
175
+ speed: z2.string().optional(),
176
+ maxWidth: z2.number().int().nullable().optional(),
177
+ maxFps: z2.number().nullable().optional(),
178
+ audio: z2.enum(["strip", "keep"]).optional(),
179
+ quality: z2.number().int().optional(),
180
+ effort: z2.number().int().optional(),
181
+ at: z2.number().optional(),
182
+ codecs: z2.string().optional(),
183
+ encoder: z2.string(),
184
+ status: z2.enum(["written", "already-optimal", "stale"]),
185
+ search: z2.object({ metric: z2.enum(["ssimulacra2", "vmaf"]), target: z2.number() }).strict().optional(),
186
+ score: z2.number().optional(),
187
+ scorer: z2.enum(["wasm", "native", "libvmaf"]).optional()
188
+ }).strict();
189
+ var manifestSchema = z2.object({
190
+ version: z2.union([z2.literal(2), z2.literal(3)]),
191
+ tools: z2.record(z2.string(), z2.string()),
192
+ publicPath: z2.string(),
193
+ sources: z2.record(
194
+ z2.string(),
195
+ z2.object({
196
+ sha256: z2.string().regex(/^[0-9a-f]{64}$/),
197
+ bytes: z2.number().int().nonnegative(),
198
+ width: z2.number().int().nonnegative(),
199
+ height: z2.number().int().nonnegative(),
200
+ preset: z2.string(),
201
+ priority: z2.boolean().optional(),
202
+ placeholder: placeholderSchema.optional(),
203
+ outputs: z2.array(outputSchema2)
204
+ }).strict()
205
+ ),
206
+ // Absent in version 2.
207
+ videos: z2.record(
208
+ z2.string(),
209
+ z2.object({
210
+ sha256: z2.string().regex(/^[0-9a-f]{64}$/),
211
+ bytes: z2.number().int().nonnegative(),
212
+ width: z2.number().int().nonnegative(),
213
+ height: z2.number().int().nonnegative(),
214
+ durationSec: z2.number().nonnegative(),
215
+ preset: z2.string(),
216
+ priority: z2.boolean().optional(),
217
+ placeholder: placeholderSchema.optional(),
218
+ outputs: z2.array(videoOutputSchema2)
219
+ }).strict()
220
+ ).optional()
221
+ }).strict();
222
+ function parseManifest(text) {
223
+ let json;
224
+ try {
225
+ json = JSON.parse(text);
226
+ } catch {
227
+ throw new BytecutError(
228
+ "MANIFEST_INVALID",
229
+ "bytecut-manifest.json is not valid JSON. Fix or delete it (outputs are regenerated from sources)."
230
+ );
231
+ }
232
+ const version = json?.version;
233
+ if (typeof version === "number" && version < MANIFEST_MIN_VERSION) {
234
+ throw new BytecutError(
235
+ "MANIFEST_OUTDATED",
236
+ `bytecut-manifest.json is version ${version}, written by a pre-release bytecut with an older output layout. Delete the output folder and re-run to regenerate it.`
237
+ );
238
+ }
239
+ if (typeof version === "number" && version > MANIFEST_VERSION) {
240
+ throw new BytecutError(
241
+ "MANIFEST_UNSUPPORTED",
242
+ `bytecut-manifest.json is version ${version}, but this bytecut understands up to ${MANIFEST_VERSION}. Upgrade bytecut.`
243
+ );
244
+ }
245
+ const result = manifestSchema.safeParse(json);
246
+ if (!result.success) {
247
+ throw new BytecutError(
248
+ "MANIFEST_INVALID",
249
+ `bytecut-manifest.json is not a valid manifest: ${formatZodIssues(result.error)}`
250
+ );
251
+ }
252
+ return {
253
+ ...result.data,
254
+ version: MANIFEST_VERSION,
255
+ videos: result.data.videos ?? {}
256
+ };
257
+ }
258
+ function serializeManifest(manifest) {
259
+ const sorted = {
260
+ version: manifest.version,
261
+ tools: Object.fromEntries(Object.entries(manifest.tools).sort(([a], [b]) => a < b ? -1 : 1)),
262
+ publicPath: manifest.publicPath,
263
+ sources: Object.fromEntries(
264
+ Object.entries(manifest.sources).sort(([a], [b]) => a < b ? -1 : 1)
265
+ ),
266
+ videos: Object.fromEntries(
267
+ Object.entries(manifest.videos).sort(([a], [b]) => a < b ? -1 : 1)
268
+ )
269
+ };
270
+ return `${JSON.stringify(sorted, null, 2)}
271
+ `;
272
+ }
273
+
274
+ export {
275
+ parsePlan,
276
+ serializePlan,
277
+ isPlaceholderKind,
278
+ MANIFEST_FILE,
279
+ MANIFEST_VERSION,
280
+ parseManifest,
281
+ serializeManifest
282
+ };
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ fallbackAv1CodecString
4
+ } from "./chunk-3DBJKHOD.js";
5
+
6
+ // src/core/snippets/render.ts
7
+ var JSX_NAMES = {
8
+ class: "className",
9
+ srcset: "srcSet",
10
+ fetchpriority: "fetchPriority",
11
+ playsinline: "playsInline",
12
+ autoplay: "autoPlay",
13
+ crossorigin: "crossOrigin",
14
+ decoding: "decoding",
15
+ tabindex: "tabIndex"
16
+ };
17
+ var kebab = (s) => s.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
18
+ var camel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
19
+ function escapeHtml(value, quote) {
20
+ let out = value.replace(/&/g, "&amp;").replace(/</g, "&lt;");
21
+ out = quote === '"' ? out.replace(/"/g, "&quot;") : out.replace(/'/g, "&#39;");
22
+ return out;
23
+ }
24
+ function htmlValue(value) {
25
+ if (value.includes('"') && !value.includes("'")) return `'${escapeHtml(value, "'")}'`;
26
+ return `"${escapeHtml(value, '"')}"`;
27
+ }
28
+ function jsxValue(value) {
29
+ const plain = /^[\x20-\x7e]*$/.test(value) && !/[\\{}&<>]/.test(value);
30
+ if (plain && !value.includes('"')) return `"${value}"`;
31
+ if (plain && !value.includes("'")) return `'${value}'`;
32
+ return `{${JSON.stringify(value)}}`;
33
+ }
34
+ function renderAttr(attr, syntax) {
35
+ if (syntax === "html") {
36
+ if (attr.value === true) return attr.name;
37
+ if (typeof attr.value === "string") return `${attr.name}=${htmlValue(attr.value)}`;
38
+ const css = Object.entries(attr.value.style).map(([k, v]) => `${kebab(k)}:${v}`).join(";");
39
+ return `style=${htmlValue(css)}`;
40
+ }
41
+ const name = JSX_NAMES[attr.name] ?? attr.name;
42
+ if (attr.value === true) return name;
43
+ if (typeof attr.value === "string") return `${name}=${jsxValue(attr.value)}`;
44
+ const body = Object.entries(attr.value.style).map(([k, v]) => `${camel(k)}: ${JSON.stringify(v)}`).join(", ");
45
+ return `style={{ ${body} }}`;
46
+ }
47
+ var VOID = /* @__PURE__ */ new Set(["source", "img"]);
48
+ function render(el, syntax, indent = "") {
49
+ const attrs = el.attrs.map((a) => renderAttr(a, syntax));
50
+ const open = [el.tag, ...attrs].join(" ");
51
+ if (VOID.has(el.tag)) return `${indent}<${open}${syntax === "jsx" ? " /" : ""}>`;
52
+ const children = el.children ?? [];
53
+ if (children.length === 0) return `${indent}<${open}></${el.tag}>`;
54
+ const inner = children.map((c) => render(c, syntax, `${indent} `)).join("\n");
55
+ return `${indent}<${open}>
56
+ ${inner}
57
+ ${indent}</${el.tag}>`;
58
+ }
59
+
60
+ // src/core/snippets/build.ts
61
+ function encodeUrl(url) {
62
+ return url.split("/").map((segment) => segment === "" ? segment : encodeURIComponent(decodeSafe(segment))).join("/");
63
+ }
64
+ function decodeSafe(segment) {
65
+ try {
66
+ return decodeURIComponent(segment);
67
+ } catch {
68
+ return segment;
69
+ }
70
+ }
71
+ var MIME = { avif: "image/avif", webp: "image/webp" };
72
+ var byWidth = (a, b) => a.width - b.width;
73
+ function placeholderAttrs(p) {
74
+ if (!p) return [];
75
+ switch (p.kind) {
76
+ case "dominant-color":
77
+ return [{ name: "style", value: { style: { backgroundColor: p.value } } }];
78
+ case "lqip":
79
+ return [
80
+ {
81
+ name: "style",
82
+ value: {
83
+ style: {
84
+ backgroundImage: `url(${p.value})`,
85
+ backgroundSize: "cover"
86
+ }
87
+ }
88
+ }
89
+ ];
90
+ case "blurhash":
91
+ return [{ name: "data-blurhash", value: p.value }];
92
+ }
93
+ }
94
+ function imageElement(entry, target, options) {
95
+ const hints = [];
96
+ const written = entry.outputs.filter((o) => o.status === "written");
97
+ const ofFormat = (f) => written.filter((o) => o.format === f).sort(byWidth);
98
+ const sources = [];
99
+ for (const format of ["avif", "webp"]) {
100
+ const outs = ofFormat(format);
101
+ if (outs.length === 0) continue;
102
+ sources.push({
103
+ tag: "source",
104
+ attrs: [
105
+ { name: "type", value: MIME[format] },
106
+ {
107
+ name: "srcset",
108
+ value: outs.map((o) => `${encodeUrl(o.url)} ${o.width}w`).join(", ")
109
+ },
110
+ { name: "sizes", value: options.sizes ?? "100vw" }
111
+ ]
112
+ });
113
+ }
114
+ const compatible = ofFormat("jpg").length > 0 ? ofFormat("jpg") : ofFormat("png");
115
+ const fallbackSet = compatible.length > 0 ? compatible : ofFormat("webp");
116
+ const fallback = fallbackSet[fallbackSet.length - 1];
117
+ if (!fallback) {
118
+ hints.push(
119
+ sources.length === 0 ? "No optimized outputs were written (they were not smaller than the source), so this uses the original file." : "No JPEG, PNG or WebP output exists, so the <img> falls back to the original file."
120
+ );
121
+ }
122
+ const priority = options.priority ?? entry.priority === true;
123
+ const lazy = options.lazy ?? !priority;
124
+ const src = fallback ? encodeUrl(fallback.url) : encodeUrl(target.originalUrl);
125
+ const width = fallback?.width ?? entry.width;
126
+ const height = fallback?.height ?? entry.height;
127
+ const img = [
128
+ { name: "src", value: src },
129
+ // Several JPEG/PNG widths and no <picture> to carry them: the <img> carries the set.
130
+ ...compatible.length > 1 ? [
131
+ {
132
+ name: "srcset",
133
+ value: compatible.map((o) => `${encodeUrl(o.url)} ${o.width}w`).join(", ")
134
+ },
135
+ { name: "sizes", value: options.sizes ?? "100vw" }
136
+ ] : [],
137
+ { name: "width", value: String(width) },
138
+ { name: "height", value: String(height) },
139
+ { name: "alt", value: options.alt ?? "" }
140
+ ];
141
+ if (options.className) img.push({ name: "class", value: options.className });
142
+ if (priority) img.push({ name: "fetchpriority", value: "high" });
143
+ if (lazy) img.push({ name: "loading", value: "lazy" });
144
+ img.push({ name: "decoding", value: "async" });
145
+ img.push(...placeholderAttrs(entry.placeholder));
146
+ if (options.alt === void 0) {
147
+ hints.push(
148
+ 'alt text is empty (marks the image decorative). Set it with --alt "\u2026" if it carries meaning.'
149
+ );
150
+ }
151
+ const imgEl = { tag: "img", attrs: img };
152
+ const el = sources.length === 0 ? imgEl : { tag: "picture", attrs: [], children: [...sources, imgEl] };
153
+ return { el, hints };
154
+ }
155
+ var LOOPING = /* @__PURE__ */ new Set(["background-video", "gif-to-video"]);
156
+ function videoType(out) {
157
+ if (out.codecs) return `video/mp4; codecs="${out.codecs}"`;
158
+ if (out.codec === "av1")
159
+ return `video/mp4; codecs="${fallbackAv1CodecString(out.width, out.height)}"`;
160
+ return "video/mp4";
161
+ }
162
+ function videoElement(entry, _target, options) {
163
+ const hints = [];
164
+ const videos = entry.outputs.filter((o) => o.role === "video" && o.status === "written");
165
+ const order = { av1: 0, h264: 1 };
166
+ videos.sort((a, b) => (order[a.codec ?? "h264"] ?? 9) - (order[b.codec ?? "h264"] ?? 9));
167
+ const poster = entry.outputs.find((o) => o.role === "poster" && o.status === "written");
168
+ if (videos.length === 0) {
169
+ hints.push("No video outputs were written, so there is nothing to embed.");
170
+ }
171
+ const main = videos[0];
172
+ const looping = LOOPING.has(entry.preset);
173
+ const controls = options.controls ?? !looping;
174
+ const attrs = [];
175
+ if (main) {
176
+ attrs.push({ name: "width", value: String(main.width) });
177
+ attrs.push({ name: "height", value: String(main.height) });
178
+ }
179
+ if (options.className) attrs.push({ name: "class", value: options.className });
180
+ if (poster) attrs.push({ name: "poster", value: encodeUrl(poster.url) });
181
+ if (looping) {
182
+ attrs.push(
183
+ { name: "autoplay", value: true },
184
+ { name: "muted", value: true },
185
+ { name: "loop", value: true },
186
+ { name: "playsinline", value: true }
187
+ );
188
+ } else {
189
+ attrs.push({ name: "playsinline", value: true });
190
+ }
191
+ if (controls) attrs.push({ name: "controls", value: true });
192
+ attrs.push({ name: "preload", value: looping ? "auto" : "metadata" });
193
+ attrs.push(...placeholderAttrs(entry.placeholder));
194
+ const children = videos.map((o) => ({
195
+ tag: "source",
196
+ attrs: [
197
+ { name: "src", value: encodeUrl(o.url) },
198
+ { name: "type", value: videoType(o) }
199
+ ]
200
+ }));
201
+ return { el: { tag: "video", attrs, children }, hints };
202
+ }
203
+ function lookup(manifest, source) {
204
+ const image = manifest.sources[source];
205
+ if (image) return { kind: "image", entry: image };
206
+ const video = manifest.videos[source];
207
+ if (video) return { kind: "video", entry: video };
208
+ return null;
209
+ }
210
+ function buildSnippet(manifest, target, options) {
211
+ const found = lookup(manifest, target.source);
212
+ if (!found) return null;
213
+ const { el, hints } = found.kind === "image" ? imageElement(found.entry, target, options) : videoElement(found.entry, target, options);
214
+ return { kind: found.kind, source: target.source, code: render(el, options.format), hints };
215
+ }
216
+
217
+ // src/core/snippets/format.ts
218
+ function originalUrl(source, staticRoot) {
219
+ const rel = staticRoot === "." || staticRoot === "" ? source : source.startsWith(`${staticRoot}/`) ? source.slice(staticRoot.length + 1) : source;
220
+ return `/${rel}`;
221
+ }
222
+ function snippetFormatFor(project) {
223
+ switch (project.framework) {
224
+ case "next":
225
+ return "jsx";
226
+ case "vite":
227
+ return project.react ? "jsx" : "html";
228
+ case "astro":
229
+ case "static":
230
+ return "html";
231
+ }
232
+ }
233
+
234
+ export {
235
+ buildSnippet,
236
+ originalUrl,
237
+ snippetFormatFor
238
+ };