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.
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/core/scan/gitignore.ts
4
+ import { readFile } from "fs/promises";
5
+ import { join, relative, sep } from "path";
6
+ import ignore from "ignore";
7
+ async function isGitIgnored(projectRoot, absolute) {
8
+ const rel = relative(projectRoot, absolute);
9
+ if (rel === "" || rel.startsWith("..")) return false;
10
+ const parts = rel.split(sep);
11
+ let dir = projectRoot;
12
+ const scopes = [];
13
+ for (let i = 0; i < parts.length; i++) {
14
+ const text = await readFile(join(dir, ".gitignore"), "utf8").catch(() => null);
15
+ if (text !== null) scopes.push({ base: dir, matcher: ignore().add(text) });
16
+ dir = join(dir, parts[i]);
17
+ for (let s = scopes.length - 1; s >= 0; s--) {
18
+ const scope = scopes[s];
19
+ const verdict = scope.matcher.test(`${relative(scope.base, dir).split(sep).join("/")}/`);
20
+ if (verdict.ignored) return true;
21
+ if (verdict.unignored) break;
22
+ }
23
+ }
24
+ return false;
25
+ }
26
+
27
+ export {
28
+ isGitIgnored
29
+ };
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+
3
+ // package.json
4
+ var package_default = {
5
+ name: "bytecut",
6
+ version: "0.0.1",
7
+ description: "Audit and optimize images and videos in web projects.",
8
+ type: "module",
9
+ license: "MIT",
10
+ bin: {
11
+ bytecut: "./dist/cli.js"
12
+ },
13
+ files: [
14
+ "dist"
15
+ ],
16
+ engines: {
17
+ node: ">=22"
18
+ },
19
+ scripts: {
20
+ dev: "tsx src/cli/index.ts",
21
+ build: "tsup",
22
+ test: "vitest run",
23
+ "test:watch": "vitest",
24
+ lint: "eslint . && prettier --check .",
25
+ typecheck: "tsc --noEmit",
26
+ fixtures: "tsx scripts/make-fixtures.ts"
27
+ },
28
+ dependencies: {
29
+ "bytecut-ssimulacra2": "workspace:*",
30
+ citty: "^0.2.2",
31
+ ignore: "^7.0.12",
32
+ ink: "^8.0.0",
33
+ react: "^19.3.0",
34
+ sharp: "^0.35.5",
35
+ "smol-toml": "^1.9.1",
36
+ svgo: "^4.1.0",
37
+ zod: "^4.6.5"
38
+ },
39
+ devDependencies: {
40
+ "@eslint/js": "^10.0.1",
41
+ "@types/node": "^26.6.5",
42
+ "@types/react": "^19.3.0",
43
+ eslint: "^10.12.0",
44
+ "ink-testing-library": "^4.0.0",
45
+ prettier: "^3.9.9",
46
+ tsup: "^8.5.1",
47
+ tsx: "^4.23.15",
48
+ typescript: "^5.9.3",
49
+ "typescript-eslint": "^8.71.1",
50
+ vitest: "^5.0.3"
51
+ },
52
+ packageManager: "pnpm@10.18.1"
53
+ };
54
+
55
+ export {
56
+ package_default
57
+ };
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ escapeGlob
4
+ } from "./chunk-TWI2OZJZ.js";
5
+
6
+ // src/core/config/choices.ts
7
+ import { stringify as stringifyToml } from "smol-toml";
8
+ function isEmpty(choices) {
9
+ return choices.outDir === void 0 && choices.publicPath === void 0 && choices.placeholder === void 0 && Object.keys(choices.presets ?? {}).length === 0;
10
+ }
11
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
12
+ function applyChoices(existing, choices) {
13
+ const next = isObject(existing) ? { ...existing } : {};
14
+ if (choices.outDir !== void 0) next.outDir = choices.outDir;
15
+ if (choices.publicPath !== void 0) next.publicPath = choices.publicPath;
16
+ if (choices.placeholder !== void 0) {
17
+ next.manifest = {
18
+ ...isObject(next.manifest) ? next.manifest : {},
19
+ placeholder: choices.placeholder
20
+ };
21
+ }
22
+ const presets = choices.presets ?? {};
23
+ if (Object.keys(presets).length > 0) {
24
+ const mine = Object.fromEntries(
25
+ Object.entries(presets).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([source, preset]) => [escapeGlob(source), preset])
26
+ );
27
+ const others = Object.entries(isObject(next.assets) ? next.assets : {}).filter(
28
+ ([pattern]) => !(pattern in mine)
29
+ );
30
+ next.assets = { ...mine, ...Object.fromEntries(others) };
31
+ }
32
+ return next;
33
+ }
34
+ function canRewrite(format) {
35
+ return format === "json";
36
+ }
37
+ function renderConfig(format, data) {
38
+ if (format === "json") return `${JSON.stringify(data, null, 2)}
39
+ `;
40
+ if (format === "toml") return `${stringifyToml(data)}
41
+ `;
42
+ return `export default ${JSON.stringify(data, null, 2)};
43
+ `;
44
+ }
45
+ function renderChoicesSnippet(format, choices) {
46
+ const fragment = applyChoices({}, choices);
47
+ return renderConfig(
48
+ format === "toml" ? "toml" : format === "json" ? "json" : "ts",
49
+ fragment
50
+ ).trimEnd();
51
+ }
52
+
53
+ export {
54
+ isEmpty,
55
+ applyChoices,
56
+ canRewrite,
57
+ renderConfig,
58
+ renderChoicesSnippet
59
+ };
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ BytecutError
4
+ } from "./chunk-TWI2OZJZ.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
+ };
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ BytecutError,
4
+ CONFIG_FILES
5
+ } from "./chunk-TWI2OZJZ.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,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,59 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ BytecutError
4
+ } from "./chunk-TWI2OZJZ.js";
5
+
6
+ // src/cli/system.ts
7
+ import { execFile } from "child_process";
8
+ import { readFileSync, statSync } from "fs";
9
+ import { promisify } from "util";
10
+ function isFile(path) {
11
+ try {
12
+ return statSync(path).isFile();
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+ function readOsReleaseId() {
18
+ if (process.platform !== "linux") return void 0;
19
+ try {
20
+ const text = readFileSync("/etc/os-release", "utf8");
21
+ return /^ID=("?)([^"\n]+)\1$/m.exec(text)?.[2];
22
+ } catch {
23
+ return void 0;
24
+ }
25
+ }
26
+ var execFileAsync = promisify(execFile);
27
+ var run = async (file, args) => {
28
+ const { stdout } = await execFileAsync(file, args, { maxBuffer: 16 * 1024 * 1024 });
29
+ return { stdout };
30
+ };
31
+ function doctorDeps() {
32
+ return {
33
+ nodeVersion: process.version,
34
+ platform: process.platform,
35
+ env: process.env,
36
+ osReleaseId: readOsReleaseId(),
37
+ isFile,
38
+ run,
39
+ async loadSharp() {
40
+ try {
41
+ const sharp = (await import("sharp")).default;
42
+ return { version: sharp.versions.sharp ?? "unknown", libvips: sharp.versions.vips };
43
+ } catch (error) {
44
+ throw new BytecutError(
45
+ "SHARP_UNAVAILABLE",
46
+ error instanceof Error ? error.message.split("\n")[0] : String(error),
47
+ { cause: error }
48
+ );
49
+ }
50
+ }
51
+ };
52
+ }
53
+
54
+ export {
55
+ isFile,
56
+ readOsReleaseId,
57
+ run,
58
+ doctorDeps
59
+ };