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,212 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ BytecutError,
4
+ CONFIG_FILES
5
+ } from "./chunk-FY53B4NB.js";
6
+
7
+ // src/core/fs-safe.ts
8
+ import { randomBytes } from "crypto";
9
+ import { lstat, mkdir, readFile, realpath, rename, unlink, writeFile } from "fs/promises";
10
+ import { basename, dirname, isAbsolute, join, relative, sep } from "path";
11
+ function isInside(parent, child) {
12
+ const rel = relative(parent, child);
13
+ if (rel === "") return true;
14
+ return rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
15
+ }
16
+ async function realpathOfExisting(path) {
17
+ const rest = [];
18
+ let current = path;
19
+ for (; ; ) {
20
+ try {
21
+ return join(await realpath(current), ...rest.reverse());
22
+ } catch (error) {
23
+ if (error.code !== "ENOENT") throw error;
24
+ const parent = dirname(current);
25
+ if (parent === current) throw error;
26
+ rest.push(basename(current));
27
+ current = parent;
28
+ }
29
+ }
30
+ }
31
+ var SafeFs = class _SafeFs {
32
+ constructor(outDir) {
33
+ this.outDir = outDir;
34
+ }
35
+ outDir;
36
+ /** Validates `outDir` against §6.5 before anything can be written. */
37
+ static async create(options) {
38
+ const realOut = await realpathOfExisting(options.outDir);
39
+ const realRoot = await realpathOfExisting(options.projectRoot);
40
+ if (realOut === realRoot) {
41
+ throw new BytecutError(
42
+ "UNSAFE_OUT_DIR",
43
+ `Refusing to use the project root as the output folder (${options.outDir}).`
44
+ );
45
+ }
46
+ if (isInside(realOut, realRoot)) {
47
+ throw new BytecutError(
48
+ "UNSAFE_OUT_DIR",
49
+ `The output folder ${options.outDir} contains the project root.`
50
+ );
51
+ }
52
+ for (const dir of options.sourceDirs) {
53
+ const realDir = await realpathOfExisting(dir);
54
+ if (realOut === realDir) {
55
+ throw new BytecutError(
56
+ "UNSAFE_OUT_DIR",
57
+ `The output folder ${options.outDir} is itself a source folder.`
58
+ );
59
+ }
60
+ if (isInside(realOut, realDir)) {
61
+ throw new BytecutError(
62
+ "UNSAFE_OUT_DIR",
63
+ `The output folder ${options.outDir} contains the source folder ${dir}.`
64
+ );
65
+ }
66
+ }
67
+ for (const file of options.sourceFiles) {
68
+ if (isInside(realOut, await realpathOfExisting(file))) {
69
+ throw new BytecutError(
70
+ "UNSAFE_OUT_DIR",
71
+ `The output folder ${options.outDir} contains the source file ${file}.`
72
+ );
73
+ }
74
+ }
75
+ return new _SafeFs(realOut);
76
+ }
77
+ readSource(absolutePath) {
78
+ return readFile(absolutePath);
79
+ }
80
+ /** Validates a path relative to `outDir` and returns where it lives; touches nothing. */
81
+ locate(relativePosixPath) {
82
+ const segments = relativePosixPath.split("/");
83
+ if (isAbsolute(relativePosixPath) || relativePosixPath.includes("\\") || segments.some((s) => s === "" || s === "." || s === "..")) {
84
+ throw new BytecutError("UNSAFE_PATH", `Unsafe output path: ${relativePosixPath}`);
85
+ }
86
+ const target = join(this.outDir, ...segments);
87
+ return { target, dir: dirname(target) };
88
+ }
89
+ /**
90
+ * Reads a file we previously wrote (or one that sits where an output would go), or null if
91
+ * it doesn't exist. Same path checks as writes, and never reads through a symlink.
92
+ */
93
+ async readOutput(relativePosixPath) {
94
+ const { target, dir } = this.locate(relativePosixPath);
95
+ await this.assertInside(await realpathOfExisting(dir), relativePosixPath);
96
+ try {
97
+ if ((await lstat(target)).isSymbolicLink()) {
98
+ throw new BytecutError("UNSAFE_PATH", `Refusing to read through symlink: ${target}`);
99
+ }
100
+ return await readFile(target);
101
+ } catch (error) {
102
+ if (error.code === "ENOENT") return null;
103
+ throw error;
104
+ }
105
+ }
106
+ /** `relativePosixPath` is relative to `outDir`. */
107
+ async writeOutput(relativePosixPath, data) {
108
+ const { target, dir } = this.locate(relativePosixPath);
109
+ await this.assertInside(await realpathOfExisting(dir), relativePosixPath);
110
+ await mkdir(dir, { recursive: true });
111
+ await this.assertInside(await realpath(dir), relativePosixPath);
112
+ try {
113
+ if ((await lstat(target)).isSymbolicLink()) {
114
+ throw new BytecutError("UNSAFE_PATH", `Refusing to write through symlink: ${target}`);
115
+ }
116
+ } catch (error) {
117
+ if (error.code !== "ENOENT") throw error;
118
+ }
119
+ const temp = join(dir, `.${basename(target)}.${randomBytes(6).toString("hex")}.tmp`);
120
+ try {
121
+ await writeFile(temp, data, { flag: "wx" });
122
+ await rename(temp, target);
123
+ } catch (error) {
124
+ await unlink(temp).catch(() => void 0);
125
+ throw error;
126
+ }
127
+ }
128
+ /**
129
+ * A path inside `outDir` for a child process (ffmpeg) to write to. The process writes it
130
+ * itself, so the write boundary is enforced here: the location is validated, never
131
+ * pre-existing, and only `commitTemp` can turn it into a real output (§6.4).
132
+ */
133
+ async reserveTemp(relativePosixPath) {
134
+ const { target, dir } = this.locate(relativePosixPath);
135
+ await this.assertInside(await realpathOfExisting(dir), relativePosixPath);
136
+ await mkdir(dir, { recursive: true });
137
+ await this.assertInside(await realpath(dir), relativePosixPath);
138
+ return join(dir, `.${basename(target)}.${randomBytes(6).toString("hex")}.tmp`);
139
+ }
140
+ /** Atomically publishes a file made through `reserveTemp` as `relativePosixPath`. */
141
+ async commitTemp(tempPath, relativePosixPath) {
142
+ const { target, dir } = this.locate(relativePosixPath);
143
+ if (dirname(tempPath) !== dir || !basename(tempPath).endsWith(".tmp")) {
144
+ throw new BytecutError("UNSAFE_PATH", `Not a reserved temp file for ${relativePosixPath}`);
145
+ }
146
+ try {
147
+ if ((await lstat(target)).isSymbolicLink()) {
148
+ throw new BytecutError("UNSAFE_PATH", `Refusing to write through symlink: ${target}`);
149
+ }
150
+ } catch (error) {
151
+ if (error.code !== "ENOENT") throw error;
152
+ }
153
+ await rename(tempPath, target);
154
+ }
155
+ /**
156
+ * Writes `data` to a reserved temp path and returns it, for handing a file to a child
157
+ * process (a native scorer). It is never an output: `discardTemp` removes it.
158
+ */
159
+ async writeTemp(relativePosixPath, data) {
160
+ const temp = await this.reserveTemp(relativePosixPath);
161
+ try {
162
+ await writeFile(temp, data, { flag: "wx" });
163
+ } catch (error) {
164
+ await unlink(temp).catch(() => void 0);
165
+ throw error;
166
+ }
167
+ return temp;
168
+ }
169
+ /** Removes a reserved temp file (after a failure or cancel). Never throws. */
170
+ async discardTemp(tempPath) {
171
+ if (!isInside(this.outDir, tempPath) || !basename(tempPath).endsWith(".tmp")) return;
172
+ await unlink(tempPath).catch(() => void 0);
173
+ }
174
+ /** Absolute path of an existing output, for reading it with a streaming API. */
175
+ async outputPath(relativePosixPath) {
176
+ const { target, dir } = this.locate(relativePosixPath);
177
+ await this.assertInside(await realpathOfExisting(dir), relativePosixPath);
178
+ return target;
179
+ }
180
+ async assertInside(resolved, requested) {
181
+ if (!isInside(this.outDir, resolved)) {
182
+ throw new BytecutError("UNSAFE_PATH", `Output path escapes the output folder: ${requested}`);
183
+ }
184
+ }
185
+ };
186
+ async function writeConfigFile(projectRoot, fileName, content) {
187
+ if (!CONFIG_FILES.includes(fileName)) {
188
+ throw new BytecutError("UNSAFE_PATH", `Not a bytecut config file name: ${fileName}`);
189
+ }
190
+ const target = join(await realpath(projectRoot), fileName);
191
+ try {
192
+ if ((await lstat(target)).isSymbolicLink()) {
193
+ throw new BytecutError("UNSAFE_PATH", `Refusing to write through symlink: ${fileName}`);
194
+ }
195
+ } catch (error) {
196
+ if (error.code !== "ENOENT") throw error;
197
+ }
198
+ const temp = join(dirname(target), `.${fileName}.${randomBytes(6).toString("hex")}.tmp`);
199
+ try {
200
+ await writeFile(temp, content, { flag: "wx" });
201
+ await rename(temp, target);
202
+ } catch (error) {
203
+ await unlink(temp).catch(() => void 0);
204
+ throw error;
205
+ }
206
+ return target;
207
+ }
208
+
209
+ export {
210
+ SafeFs,
211
+ writeConfigFile
212
+ };
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ isFile,
4
+ readOsReleaseId,
5
+ run
6
+ } from "./chunk-S3FQYUTI.js";
7
+ import {
8
+ createFfmpegVideoEngine
9
+ } from "./chunk-QOG2F4SF.js";
10
+ import {
11
+ installHint,
12
+ resolveBinary,
13
+ resolveConfig
14
+ } from "./chunk-3PF6BBFK.js";
15
+ import {
16
+ BytecutError,
17
+ loadConfig
18
+ } from "./chunk-FY53B4NB.js";
19
+
20
+ // src/cli/shared.ts
21
+ import { existsSync } from "fs";
22
+ import { resolve } from "path";
23
+
24
+ // src/core/quality/native.ts
25
+ var counter = 0;
26
+ function createNativeScorer(options) {
27
+ return {
28
+ kind: "native",
29
+ label: `ssimulacra2 (${options.binary})`,
30
+ async score(reference, distorted) {
31
+ const id = `${process.pid}-${counter++}`;
32
+ const paths = [];
33
+ try {
34
+ paths.push(
35
+ await options.fs.writeTemp(
36
+ `.bytecut-cache/tmp/${id}-a.png`,
37
+ await options.toPng(reference)
38
+ ),
39
+ await options.fs.writeTemp(
40
+ `.bytecut-cache/tmp/${id}-b.png`,
41
+ await options.toPng(distorted)
42
+ )
43
+ );
44
+ const { stdout } = await options.run(options.binary, paths);
45
+ const value = Number.parseFloat(stdout.trim().split(/\s+/).pop() ?? "");
46
+ if (!Number.isFinite(value)) {
47
+ throw new BytecutError(
48
+ "SCORER_UNAVAILABLE",
49
+ `${options.binary} printed something that is not a score: ${stdout.trim().slice(0, 80)}`
50
+ );
51
+ }
52
+ return value;
53
+ } finally {
54
+ await Promise.all(paths.map((p) => options.fs.discardTemp(p)));
55
+ }
56
+ },
57
+ async close() {
58
+ }
59
+ };
60
+ }
61
+
62
+ // src/core/quality/wasm.ts
63
+ async function createWasmScorer(options = {}) {
64
+ let lib;
65
+ try {
66
+ lib = await import("bytecut-ssimulacra2");
67
+ } catch (error) {
68
+ throw new BytecutError(
69
+ "SCORER_UNAVAILABLE",
70
+ `The built-in SSIMULACRA2 scorer could not be loaded: ${error instanceof Error ? error.message : String(error)}`,
71
+ { cause: error }
72
+ );
73
+ }
74
+ const pool = lib.createPool({ size: options.size ?? 2 });
75
+ return {
76
+ kind: "wasm",
77
+ label: "bytecut-ssimulacra2",
78
+ score: (reference, distorted) => pool.score(reference, distorted),
79
+ close: () => pool.close()
80
+ };
81
+ }
82
+
83
+ // src/core/quality/select.ts
84
+ function resolveScorerBinary(options) {
85
+ return resolveBinary(
86
+ "ssimulacra2",
87
+ { flag: options.flag, config: options.config, env: options.env, platform: options.platform },
88
+ options.deps
89
+ );
90
+ }
91
+ async function selectScorer(options) {
92
+ const resolved = resolveScorerBinary(options);
93
+ const notes = [];
94
+ if (resolved.found) {
95
+ return {
96
+ scorer: createNativeScorer({
97
+ binary: resolved.path,
98
+ fs: options.fs,
99
+ toPng: options.toPng,
100
+ run: options.run
101
+ }),
102
+ notes
103
+ };
104
+ }
105
+ if (resolved.attempted) {
106
+ notes.push(
107
+ `ssimulacra2 not found at ${resolved.attempted} (from ${resolved.source}); using the built-in WebAssembly scorer.`
108
+ );
109
+ }
110
+ return {
111
+ scorer: await createWasmScorer(options.size !== void 0 ? { size: options.size } : {}),
112
+ notes
113
+ };
114
+ }
115
+
116
+ // src/cli/shared.ts
117
+ import { availableParallelism } from "os";
118
+ var configArg = {
119
+ config: {
120
+ type: "string",
121
+ description: "Path to a config file (default: bytecut.config.{ts,js,json,toml} in the project)"
122
+ }
123
+ };
124
+ async function discover(inputs, outDirArg, configPath) {
125
+ const projectRoot = process.cwd();
126
+ const config = await resolveConfig(projectRoot, await loadConfig(projectRoot, configPath));
127
+ const { project } = config;
128
+ const scanInputs = inputs.length > 0 ? inputs : project.include.filter((p) => existsSync(resolve(projectRoot, p)));
129
+ return { projectRoot, project, scanInputs, outDir: outDirArg ?? project.outDir, config };
130
+ }
131
+ function ffmpegStatus(config) {
132
+ const resolved = resolveBinary(
133
+ "ffmpeg",
134
+ { env: process.env, platform: process.platform, config: config?.ffmpegPath },
135
+ { isFile }
136
+ );
137
+ return { found: resolved.found, installHint: installHint(process.platform, readOsReleaseId()) };
138
+ }
139
+ function noAssetsMessage(command, project) {
140
+ return `bytecut${command ? ` ${command}` : ""}: no asset folder found (looked for ${project.include.join(", ")}). Pass files or folders.`;
141
+ }
142
+ function createVideoEngine(flags = {}, config = {}) {
143
+ return createFfmpegVideoEngine({
144
+ ffmpegFlag: flags.ffmpeg,
145
+ ffprobeFlag: flags.ffprobe,
146
+ ffmpegConfig: config.ffmpegPath,
147
+ ffprobeConfig: config.ffprobePath,
148
+ env: process.env,
149
+ platform: process.platform,
150
+ deps: { isFile },
151
+ run
152
+ });
153
+ }
154
+ function makeScorerFactory(flag, config = {}) {
155
+ return async (fs) => {
156
+ const sharp = (await import("sharp")).default;
157
+ return selectScorer({
158
+ flag,
159
+ config: config.ssimulacra2Path,
160
+ env: process.env,
161
+ platform: process.platform,
162
+ deps: { isFile },
163
+ fs,
164
+ run,
165
+ size: Math.max(1, Math.min(4, availableParallelism() - 1)),
166
+ toPng: async (image) => new Uint8Array(
167
+ await sharp(image.data, {
168
+ raw: { width: image.width, height: image.height, channels: 3 }
169
+ }).png().toBuffer()
170
+ )
171
+ });
172
+ };
173
+ }
174
+ function ffmpegInstallText() {
175
+ return `install ffmpeg with: ${installHint(process.platform, readOsReleaseId())}`;
176
+ }
177
+
178
+ export {
179
+ configArg,
180
+ discover,
181
+ ffmpegStatus,
182
+ noAssetsMessage,
183
+ createVideoEngine,
184
+ makeScorerFactory,
185
+ ffmpegInstallText
186
+ };
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/core/engines/svg.ts
4
+ import { VERSION, optimize } from "svgo";
5
+ var EMBEDDED_RASTER = /data:image\/(?:png|jpe?g|webp|gif|avif)[^"')\s]*?;base64,([A-Za-z0-9+/=\s]+)/gi;
6
+ var svgoEngine = {
7
+ version: () => `svgo ${VERSION}`,
8
+ analyze(text) {
9
+ let embeddedRasterBytes = 0;
10
+ let embeddedRasterCount = 0;
11
+ for (const match of text.matchAll(EMBEDDED_RASTER)) {
12
+ embeddedRasterCount++;
13
+ embeddedRasterBytes += match[1].replace(/\s/g, "").length;
14
+ }
15
+ let minifiedBytes = null;
16
+ try {
17
+ minifiedBytes = Buffer.byteLength(optimize(text, { multipass: true }).data);
18
+ } catch {
19
+ }
20
+ return { minifiedBytes, embeddedRasterBytes, embeddedRasterCount };
21
+ }
22
+ };
23
+
24
+ export {
25
+ svgoEngine
26
+ };
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ BytecutError
4
+ } from "./chunk-FY53B4NB.js";
5
+
6
+ // src/cli/errors.ts
7
+ function guarded(run) {
8
+ return async (context) => {
9
+ try {
10
+ await run(context);
11
+ } catch (error) {
12
+ if (!(error instanceof BytecutError)) throw error;
13
+ console.error(`bytecut: ${error.message} [${error.code}]`);
14
+ process.exitCode = 1;
15
+ }
16
+ };
17
+ }
18
+
19
+ export {
20
+ guarded
21
+ };