bytecut 0.0.1 → 0.2.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.
Files changed (30) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +145 -0
  3. package/dist/{audit-HVW4EOLP.js → audit-LJOGSJS6.js} +12 -12
  4. package/dist/{check-YONIQTM7.js → check-MEBXJBNG.js} +11 -11
  5. package/dist/{chunk-YBEC5QGV.js → chunk-3PF6BBFK.js} +47 -20
  6. package/dist/{chunk-GMHOU4Y5.js → chunk-4FIB6VNC.js} +1 -1
  7. package/dist/{chunk-N536JFSF.js → chunk-5JEA233Z.js} +15 -9
  8. package/dist/{chunk-7IECTPJB.js → chunk-7DHE25UE.js} +1 -1
  9. package/dist/{chunk-XFJ3HW4X.js → chunk-CSFBJ3UH.js} +21 -10
  10. package/dist/{chunk-TWI2OZJZ.js → chunk-FY53B4NB.js} +10 -3
  11. package/dist/{chunk-CEZK3RQJ.js → chunk-HLBNRB3S.js} +6 -6
  12. package/dist/{chunk-LHTI3JMC.js → chunk-IZ4JEL3Q.js} +1 -1
  13. package/dist/{chunk-4XOB3C34.js → chunk-KN5QOB7R.js} +4 -4
  14. package/dist/{chunk-G53LNJ5O.js → chunk-LJWOPIM6.js} +1 -1
  15. package/dist/{chunk-J2XJO4JZ.js → chunk-MV6J66AP.js} +1 -1
  16. package/dist/{chunk-326CQQWX.js → chunk-PQRBLHCC.js} +1 -1
  17. package/dist/{chunk-WE7AEDVI.js → chunk-QOG2F4SF.js} +2 -2
  18. package/dist/{chunk-NAKV7HS5.js → chunk-RZ2V4PCS.js} +22 -10
  19. package/dist/{chunk-M6ZDSATP.js → chunk-S3FQYUTI.js} +1 -1
  20. package/dist/{chunk-5R7LR5BB.js → chunk-TCQFQ354.js} +4 -4
  21. package/dist/{chunk-U4KJMKI5.js → chunk-WZ6MMRNB.js} +12 -3
  22. package/dist/cli.js +23 -23
  23. package/dist/{compare-6PCE6533.js → compare-MPPGTUC5.js} +8 -8
  24. package/dist/{doctor-R226JU6P.js → doctor-PWCUEBD5.js} +5 -5
  25. package/dist/{explain-SGN5BOR7.js → explain-LKOB7FRT.js} +15 -13
  26. package/dist/{init-WHWUANOB.js → init-CWDWVJ55.js} +4 -4
  27. package/dist/{optimize-UXM6JEDA.js → optimize-SLLQGDDX.js} +12 -12
  28. package/dist/{snippet-2WVGMCNV.js → snippet-IIPVF6PS.js} +8 -8
  29. package/dist/{tui-J4Y44IL7.js → tui-ZAEXBY4L.js} +11 -11
  30. package/package.json +1 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 anupjon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # bytecut
2
+
3
+ Audit and optimize the images and videos in a web project, from the terminal.
4
+
5
+ ```sh
6
+ npx bytecut
7
+ ```
8
+
9
+ With no configuration, bytecut detects your framework (Next.js, Astro, Vite or a static
10
+ site), scans the asset folders, shows what is too heavy and how much you would save, and
11
+ optimizes on Enter. Your original files are never modified: outputs go to a separate folder
12
+ that mirrors your source tree, and that folder is meant to be committed.
13
+
14
+ ## What you get
15
+
16
+ By default each image becomes **one JPEG**, with the longest side capped at 1920 px (a
17
+ landscape image is 1920 wide, a portrait one is 1920 tall, nothing is ever upscaled). Metadata
18
+ and GPS are stripped, orientation is applied and the colour profile is kept.
19
+
20
+ ```
21
+ public/img/hero.jpg → public/_bytecut/img/hero-1920.jpg
22
+ public/_bytecut/bytecut-manifest.json
23
+ ```
24
+
25
+ - **Transparency is preserved.** A source that really uses alpha gets a lossless PNG instead of
26
+ a JPEG, and so does flat art such as icons and logos.
27
+ - **WebP and AVIF are opt-in** (see [Configuration](#configuration)).
28
+ - **A file is only written if it is smaller** than the source; otherwise it is reported as
29
+ "already optimal".
30
+ - **Videos and animated GIFs** become AV1 and H.264 MP4 plus a poster, using your own
31
+ `ffmpeg` (see [Video](#video)).
32
+
33
+ Outputs are flat: an image at the top of your static folder lands directly in `_bytecut/`, and
34
+ one in a subfolder keeps that subfolder (`public/img/hero.jpg` → `_bytecut/img/hero-1920.jpg`).
35
+ There is no extra folder per image.
36
+
37
+ Re-running is cheap: unchanged sources are skipped by content hash, and the manifest is
38
+ byte-identical when nothing changed, so it diffs cleanly in pull requests.
39
+
40
+ ## Commands
41
+
42
+ | Command | What it does |
43
+ | ----------------------------- | ------------------------------------------------------------------------- |
44
+ | `bytecut` | Interactive session on a TTY; prints the audit elsewhere |
45
+ | `bytecut audit` | Report what is too heavy, with estimated savings |
46
+ | `bytecut optimize [paths] -y` | Write optimized files. Without `-y` it prints the plan and writes nothing |
47
+ | `bytecut snippet <file>` | Print the `<picture>` / `<video>` markup (HTML or JSX) for a file |
48
+ | `bytecut compare <file>` | Serve a before/after page on `127.0.0.1` (random port, private path) |
49
+ | `bytecut explain <file>` | Show which rule chose a file's preset and what it resolves to |
50
+ | `bytecut init` | Write a minimal `bytecut.config.json` with only non-default choices |
51
+ | `bytecut check` | CI mode: exit 1 if a finding is an error, 2 if it could not run |
52
+ | `bytecut doctor` | Report Node, sharp, ffmpeg, the scorer, and what is degraded |
53
+
54
+ Useful flags: `--dry-run --json` prints the plan as JSON and `--plan <file>` executes a saved
55
+ one; `--out-dir`, `--preset`, `--force`, `--placeholder dominant-color|lqip|blurhash|none`.
56
+
57
+ In the interactive session: `space` toggles a file, `enter` reviews and runs, `p` changes the
58
+ preset, `o` the output folder, `d` details, `esc` cancels, `?` help. Type `/` for commands
59
+ (`/snippet @file`, `/compare @file`, `/save`) and `@` to mention a file.
60
+
61
+ ## Configuration
62
+
63
+ Everything is optional. Config is only needed to make a choice stick. Put it in
64
+ `bytecut.config.json` (or `.ts`, `.mjs`, `.toml`); unknown keys are errors.
65
+
66
+ ```json
67
+ {
68
+ "presets": {
69
+ "photo": { "formats": ["jpg", "webp", "avif"] },
70
+ "gallery": {
71
+ "extends": "photo",
72
+ "maxEdge": 1280,
73
+ "ssimulacra2": 80,
74
+ "maxSize": "150 KB",
75
+ "floor": 65
76
+ },
77
+ "loop": { "extends": "background-video", "vmaf": 92 }
78
+ },
79
+ "assets": { "public/gallery/**": "gallery", "public/loops/*.mp4": "loop" },
80
+ "budgets": { "maxFileSize": "250 KB", "maxTotalSize": "5 MB" }
81
+ }
82
+ ```
83
+
84
+ - **Presets**: built-in `photo`, `hero`, `icon`, `background-video`, `content-video` and
85
+ `gif-to-video`. Add your own with `extends`; the first matching `assets` glob wins.
86
+ - **Size**: `maxEdge` caps the longest side; `widths` lists widths instead (a responsive set).
87
+ - **Formats**: `jpg` (default), `png`, `webp`, `avif`. A `<picture>` lists AVIF, then WebP, and
88
+ the JPEG is the `<img>`.
89
+ - **Quality**: `target` (`low`, `balanced`, `high`, `lossless`), or `quality` per format.
90
+ - **Perceptual targets**: see below.
91
+
92
+ ## Quality targets
93
+
94
+ Instead of a fixed quality you can ask for a score. bytecut then finds the **smallest** output
95
+ that still meets it:
96
+
97
+ - `ssimulacra2: 80` (images and video) searches quality on a representative crop, then
98
+ confirms on the full-size output.
99
+ - `maxSize` with a required `floor` lowers quality, then steps the width down, to fit a size
100
+ cap. A constraint that cannot be met writes nothing and fails the run; it is never silently
101
+ exceeded.
102
+ - `vmaf: 92` (video) uses ffmpeg's `libvmaf`. Without it, the preset's fixed quality is used
103
+ and a warning says so.
104
+
105
+ SSIMULACRA2 runs as WebAssembly in worker threads, so no native binary is needed. A native
106
+ `ssimulacra2` is used if one is found (`--ssimulacra2`, `BYTECUT_SSIMULACRA2`,
107
+ `ssimulacra2Path`, or `PATH`), as a speed-up only.
108
+
109
+ ## Video
110
+
111
+ bytecut uses **your own** `ffmpeg` and `ffprobe` (`--ffmpeg`, `BYTECUT_FFMPEG`, `ffmpegPath`, or
112
+ `PATH`). It never downloads or bundles one. Without ffmpeg images work fully and videos are
113
+ listed as "needs ffmpeg" with the install command for your OS. Without an AV1 encoder you still
114
+ get the H.264 file and the poster. `bytecut doctor` shows exactly what is available.
115
+
116
+ ## CI
117
+
118
+ ```sh
119
+ npx bytecut check # exit 1 on an error finding
120
+ npx bytecut check --fail-on warn # stricter
121
+ npx bytecut check --changed origin/main # only files changed since a ref
122
+ npx bytecut check --report md # a Markdown report for a PR comment
123
+ ```
124
+
125
+ `project/over-budget` is an error by default; other rules are promoted or silenced under
126
+ `audit.rules`.
127
+
128
+ ## Guarantees
129
+
130
+ - Sources are read-only. bytecut only writes inside the output folder (and the config file,
131
+ and only when you ask it to), atomically, so a cancelled run never leaves a half-written
132
+ file.
133
+ - It only deletes files listed in its manifest, and never without confirmation or an explicit flag.
134
+ - No network access at runtime: no telemetry, no update checks, no downloads.
135
+ - Same input, same settings and same tool versions give the same bytes where the encoder allows.
136
+
137
+ ## Requirements
138
+
139
+ Node.js 22 or newer. Images use [sharp](https://sharp.pixelplumbing.com). Developed and tested
140
+ on Linux; macOS and Windows have not been verified yet.
141
+
142
+ ## Licence
143
+
144
+ MIT. The bundled SSIMULACRA2 build (`bytecut-ssimulacra2`) is a WebAssembly compilation of
145
+ [rust-av/ssimulacra2](https://github.com/rust-av/ssimulacra2) and is BSD-2-Clause.
@@ -4,16 +4,16 @@ import {
4
4
  } from "./chunk-LTWUPQDT.js";
5
5
  import {
6
6
  audit
7
- } from "./chunk-N536JFSF.js";
7
+ } from "./chunk-5JEA233Z.js";
8
8
  import {
9
9
  formatAuditReport
10
- } from "./chunk-CEZK3RQJ.js";
10
+ } from "./chunk-HLBNRB3S.js";
11
11
  import "./chunk-3DBJKHOD.js";
12
- import "./chunk-5R7LR5BB.js";
12
+ import "./chunk-TCQFQ354.js";
13
13
  import {
14
14
  sharpImageEngine
15
- } from "./chunk-XFJ3HW4X.js";
16
- import "./chunk-NAKV7HS5.js";
15
+ } from "./chunk-CSFBJ3UH.js";
16
+ import "./chunk-RZ2V4PCS.js";
17
17
  import {
18
18
  configArg,
19
19
  createVideoEngine,
@@ -21,15 +21,15 @@ import {
21
21
  ffmpegInstallText,
22
22
  ffmpegStatus,
23
23
  noAssetsMessage
24
- } from "./chunk-4XOB3C34.js";
25
- import "./chunk-M6ZDSATP.js";
26
- import "./chunk-WE7AEDVI.js";
27
- import "./chunk-YBEC5QGV.js";
28
- import "./chunk-LHTI3JMC.js";
24
+ } from "./chunk-KN5QOB7R.js";
25
+ import "./chunk-S3FQYUTI.js";
26
+ import "./chunk-QOG2F4SF.js";
27
+ import "./chunk-3PF6BBFK.js";
28
+ import "./chunk-IZ4JEL3Q.js";
29
29
  import {
30
30
  guarded
31
- } from "./chunk-J2XJO4JZ.js";
32
- import "./chunk-TWI2OZJZ.js";
31
+ } from "./chunk-MV6J66AP.js";
32
+ import "./chunk-FY53B4NB.js";
33
33
 
34
34
  // src/cli/commands/audit.ts
35
35
  import { defineCommand } from "citty";
@@ -4,16 +4,16 @@ import {
4
4
  } from "./chunk-LTWUPQDT.js";
5
5
  import {
6
6
  audit
7
- } from "./chunk-N536JFSF.js";
7
+ } from "./chunk-5JEA233Z.js";
8
8
  import {
9
9
  formatAuditReport
10
- } from "./chunk-CEZK3RQJ.js";
10
+ } from "./chunk-HLBNRB3S.js";
11
11
  import "./chunk-3DBJKHOD.js";
12
- import "./chunk-5R7LR5BB.js";
12
+ import "./chunk-TCQFQ354.js";
13
13
  import {
14
14
  sharpImageEngine
15
- } from "./chunk-XFJ3HW4X.js";
16
- import "./chunk-NAKV7HS5.js";
15
+ } from "./chunk-CSFBJ3UH.js";
16
+ import "./chunk-RZ2V4PCS.js";
17
17
  import {
18
18
  configArg,
19
19
  createVideoEngine,
@@ -21,22 +21,22 @@ import {
21
21
  ffmpegInstallText,
22
22
  ffmpegStatus,
23
23
  noAssetsMessage
24
- } from "./chunk-4XOB3C34.js";
24
+ } from "./chunk-KN5QOB7R.js";
25
25
  import {
26
26
  run
27
- } from "./chunk-M6ZDSATP.js";
27
+ } from "./chunk-S3FQYUTI.js";
28
28
  import {
29
29
  toPosix
30
- } from "./chunk-WE7AEDVI.js";
31
- import "./chunk-YBEC5QGV.js";
32
- import "./chunk-LHTI3JMC.js";
30
+ } from "./chunk-QOG2F4SF.js";
31
+ import "./chunk-3PF6BBFK.js";
32
+ import "./chunk-IZ4JEL3Q.js";
33
33
  import {
34
34
  BytecutError,
35
35
  CONFIG_FILES,
36
36
  formatBytes,
37
37
  percentChange,
38
38
  plural
39
- } from "./chunk-TWI2OZJZ.js";
39
+ } from "./chunk-FY53B4NB.js";
40
40
 
41
41
  // src/cli/commands/check.ts
42
42
  import { defineCommand } from "citty";
@@ -4,14 +4,35 @@ import {
4
4
  detectProject,
5
5
  hint,
6
6
  matchesGlob
7
- } from "./chunk-TWI2OZJZ.js";
7
+ } from "./chunk-FY53B4NB.js";
8
+
9
+ // src/core/presets/photo.ts
10
+ var OTHER_ENCODERS = {
11
+ // PNG has no quality; `effort` is libpng's/oxipng-style effort (1–10).
12
+ png: { quality: 100, effort: 7 }
13
+ };
14
+ var PHOTO_PRESET = {
15
+ name: "photo",
16
+ widths: [],
17
+ maxEdge: 1920,
18
+ formats: ["jpg"],
19
+ encoders: {
20
+ ...OTHER_ENCODERS,
21
+ jpg: { quality: 80, effort: 0 },
22
+ avif: { quality: 60, effort: 4 },
23
+ webp: { quality: 78, effort: 4 }
24
+ }
25
+ };
8
26
 
9
27
  // src/core/presets/hero.ts
10
28
  var HERO_PRESET = {
11
29
  name: "hero",
12
- widths: [640, 1280, 1920, 2560],
13
- formats: ["avif", "webp"],
30
+ widths: [],
31
+ maxEdge: 1920,
32
+ formats: ["jpg"],
14
33
  encoders: {
34
+ ...OTHER_ENCODERS,
35
+ jpg: { quality: 85, effort: 0 },
15
36
  avif: { quality: 65, effort: 4 },
16
37
  webp: { quality: 82, effort: 4 }
17
38
  },
@@ -22,25 +43,16 @@ var HERO_PRESET = {
22
43
  var ICON_PRESET = {
23
44
  name: "icon",
24
45
  widths: [64, 128, 256, 512],
25
- formats: ["avif", "webp"],
46
+ formats: ["png"],
26
47
  encoders: {
48
+ ...OTHER_ENCODERS,
49
+ jpg: { quality: 90, effort: 0 },
27
50
  avif: { quality: 80, effort: 4 },
28
51
  webp: { quality: 90, effort: 4 }
29
52
  },
30
53
  lossless: true
31
54
  };
32
55
 
33
- // src/core/presets/photo.ts
34
- var PHOTO_PRESET = {
35
- name: "photo",
36
- widths: [640, 1280, 1920],
37
- formats: ["avif", "webp"],
38
- encoders: {
39
- avif: { quality: 60, effort: 4 },
40
- webp: { quality: 78, effort: 4 }
41
- }
42
- };
43
-
44
56
  // src/core/presets/index.ts
45
57
  var IMAGE_PRESETS = {
46
58
  photo: PHOTO_PRESET,
@@ -122,13 +134,14 @@ var BUILTIN_POLICY = {
122
134
 
123
135
  // src/core/config/presets.ts
124
136
  var IMAGE_TARGETS = {
125
- low: { avif: 45, webp: 65 },
126
- balanced: { avif: 60, webp: 78 },
127
- high: { avif: 75, webp: 88 }
137
+ low: { jpg: 65, avif: 45, webp: 65 },
138
+ balanced: { jpg: 80, avif: 60, webp: 78 },
139
+ high: { jpg: 90, avif: 75, webp: 88 }
128
140
  };
129
141
  var VIDEO_TARGET_CRF_OFFSET = { low: 4, balanced: 0, high: -4 };
130
142
  var IMAGE_ONLY = [
131
143
  "widths",
144
+ "maxEdge",
132
145
  "formats",
133
146
  "quality",
134
147
  "effort",
@@ -156,15 +169,29 @@ function applyImage(name, base, def) {
156
169
  name,
157
170
  widths: [...base.widths],
158
171
  formats: [...base.formats],
159
- encoders: { avif: { ...base.encoders.avif }, webp: { ...base.encoders.webp } }
172
+ encoders: {
173
+ jpg: { ...base.encoders.jpg },
174
+ png: { ...base.encoders.png },
175
+ avif: { ...base.encoders.avif },
176
+ webp: { ...base.encoders.webp }
177
+ }
160
178
  };
161
179
  if (def.target === "lossless") next.lossless = true;
162
180
  else if (def.target) {
181
+ next.encoders.jpg.quality = IMAGE_TARGETS[def.target].jpg;
163
182
  next.encoders.avif.quality = IMAGE_TARGETS[def.target].avif;
164
183
  next.encoders.webp.quality = IMAGE_TARGETS[def.target].webp;
165
184
  }
166
- if (def.widths) next.widths = [...new Set(def.widths)].sort((a, b) => a - b);
185
+ if (def.widths) {
186
+ next.widths = [...new Set(def.widths)].sort((a, b) => a - b);
187
+ delete next.maxEdge;
188
+ }
189
+ if (def.maxEdge !== void 0) {
190
+ next.maxEdge = def.maxEdge;
191
+ next.widths = [];
192
+ }
167
193
  if (def.formats) next.formats = [...new Set(def.formats)];
194
+ if (def.quality?.jpg !== void 0) next.encoders.jpg.quality = def.quality.jpg;
168
195
  if (def.quality?.avif !== void 0) next.encoders.avif.quality = def.quality.avif;
169
196
  if (def.quality?.webp !== void 0) next.encoders.webp.quality = def.quality.webp;
170
197
  if (def.effort?.avif !== void 0) next.encoders.avif.effort = def.effort.avif;
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  escapeGlob
4
- } from "./chunk-TWI2OZJZ.js";
4
+ } from "./chunk-FY53B4NB.js";
5
5
 
6
6
  // src/core/config/choices.ts
7
7
  import { stringify as stringifyToml } from "smol-toml";
@@ -6,38 +6,38 @@ import {
6
6
  sortFindings,
7
7
  walkFiles,
8
8
  withVideos
9
- } from "./chunk-CEZK3RQJ.js";
9
+ } from "./chunk-HLBNRB3S.js";
10
10
  import {
11
11
  MANIFEST_FILE,
12
12
  parseManifest
13
- } from "./chunk-5R7LR5BB.js";
13
+ } from "./chunk-TCQFQ354.js";
14
14
  import {
15
15
  ANIMATED_GIF_REASON,
16
16
  buildPlan,
17
17
  classify,
18
18
  classifyVideo,
19
19
  sha256
20
- } from "./chunk-NAKV7HS5.js";
20
+ } from "./chunk-RZ2V4PCS.js";
21
21
  import {
22
22
  isFastStart,
23
23
  toPosix
24
- } from "./chunk-WE7AEDVI.js";
24
+ } from "./chunk-QOG2F4SF.js";
25
25
  import {
26
26
  BUILTIN_POLICY,
27
27
  getImagePreset,
28
28
  pickAv1Encoder,
29
29
  rootPreset
30
- } from "./chunk-YBEC5QGV.js";
30
+ } from "./chunk-3PF6BBFK.js";
31
31
  import {
32
32
  SafeFs
33
- } from "./chunk-LHTI3JMC.js";
33
+ } from "./chunk-IZ4JEL3Q.js";
34
34
  import {
35
35
  ALL_RULES,
36
36
  checkCache,
37
37
  checkVideoCache,
38
38
  matchesGlob,
39
39
  savingOf
40
- } from "./chunk-TWI2OZJZ.js";
40
+ } from "./chunk-FY53B4NB.js";
41
41
 
42
42
  // src/core/commands/audit.ts
43
43
  import { stat as stat2 } from "fs/promises";
@@ -66,8 +66,14 @@ async function estimateItem(item, readData, deps) {
66
66
  const outs = item.outputs.filter((o) => o.format === format).sort((a, b) => a.width - b.width);
67
67
  const largest = outs[outs.length - 1];
68
68
  const within = outs.filter((o) => o.width <= TRIAL_MAX_WIDTH);
69
- const trial = within.length > 0 ? within[within.length - 1] : outs[0];
70
- const key = estimateKey(item.sourceSha256, item.preset, trial, trial.width, encoders[format]);
69
+ const trial = within.length > 0 ? within[within.length - 1] : { ...outs[0], width: TRIAL_MAX_WIDTH };
70
+ const key = estimateKey(
71
+ item.sourceSha256,
72
+ item.preset,
73
+ trial,
74
+ trial.width,
75
+ encoders[format] ?? format
76
+ );
71
77
  let bytes = deps.cache.getEstimate(key)?.bytes;
72
78
  if (bytes === void 0) {
73
79
  const encoded = await deps.engine.encode(await readData(), trial);
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  BytecutError
4
- } from "./chunk-TWI2OZJZ.js";
4
+ } from "./chunk-FY53B4NB.js";
5
5
 
6
6
  // src/core/compare/server.ts
7
7
  import { randomBytes } from "crypto";
@@ -131,6 +131,8 @@ async function sampleColors(input) {
131
131
  function encoderNames() {
132
132
  const v = sharp.versions;
133
133
  return {
134
+ jpg: `mozjpeg ${v.mozjpeg ?? "unknown"}`,
135
+ png: `libpng ${v.png ?? "unknown"}`,
134
136
  avif: `libaom-av1 ${v.aom ?? "unknown"}`,
135
137
  webp: `libwebp ${v.webp ?? "unknown"}`
136
138
  };
@@ -155,6 +157,18 @@ async function makePlaceholder(input, kind) {
155
157
  }
156
158
  }
157
159
  }
160
+ function encodeAs(image, r) {
161
+ switch (r.format) {
162
+ case "jpg":
163
+ return image.jpeg({ quality: r.quality, mozjpeg: true });
164
+ case "png":
165
+ return image.png({ compressionLevel: 9, effort: Math.max(1, Math.min(10, r.effort)) });
166
+ case "avif":
167
+ return image.avif({ quality: r.quality, effort: r.effort, lossless: r.lossless });
168
+ case "webp":
169
+ return image.webp({ quality: r.quality, effort: r.effort, lossless: r.lossless });
170
+ }
171
+ }
158
172
  var sharpImageEngine = {
159
173
  encoders: encoderNames,
160
174
  placeholder: makePlaceholder,
@@ -211,7 +225,12 @@ var sharpImageEngine = {
211
225
  const raw = sharp(image.data, {
212
226
  raw: { width: image.width, height: image.height, channels: image.channels }
213
227
  });
214
- const encoded = request.format === "avif" ? raw.avif({ quality: request.quality, effort: request.effort }) : raw.webp({ quality: request.quality, effort: request.effort });
228
+ const encoded = encodeAs(raw, {
229
+ format: request.format,
230
+ quality: request.quality,
231
+ effort: request.effort,
232
+ lossless: false
233
+ });
215
234
  const { data, info } = await encoded.toBuffer({ resolveWithObject: true });
216
235
  return {
217
236
  data,
@@ -224,15 +243,7 @@ var sharpImageEngine = {
224
243
  const meta = await sharp(input).metadata();
225
244
  let image = sharp(input).rotate().resize({ width: request.width, withoutEnlargement: true });
226
245
  image = meta.space === "cmyk" ? image.toColourspace("srgb") : image.keepIccProfile();
227
- image = request.format === "avif" ? image.avif({
228
- quality: request.quality,
229
- effort: request.effort,
230
- lossless: request.lossless
231
- }) : image.webp({
232
- quality: request.quality,
233
- effort: request.effort,
234
- lossless: request.lossless
235
- });
246
+ image = encodeAs(image, request);
236
247
  const { data, info } = await image.toBuffer({ resolveWithObject: true });
237
248
  return {
238
249
  data,
@@ -307,7 +307,7 @@ var noModernFormat = {
307
307
  ruleId: ID8,
308
308
  severity: "warn",
309
309
  source: image.source,
310
- message: `${format.toUpperCase()} source has no up-to-date AVIF/WebP outputs`,
310
+ message: `${format.toUpperCase()} source has no up-to-date optimized outputs`,
311
311
  current: image.bytes,
312
312
  estimated: image.estimate.best.bytes,
313
313
  savingsBytes: saving
@@ -1006,7 +1006,7 @@ var FRAMEWORKS = ["next", "astro", "vite", "static"];
1006
1006
  var rejected = (what, why = M9) => z.unknown().optional().superRefine((value, ctx) => {
1007
1007
  if (value !== void 0) ctx.addIssue({ code: "custom", message: `${what} ${why}` });
1008
1008
  });
1009
- var qualityByFormat = z.object({ avif: quality.optional(), webp: quality.optional() }).strict();
1009
+ var qualityByFormat = z.object({ jpg: quality.optional(), avif: quality.optional(), webp: quality.optional() }).strict();
1010
1010
  var effortByFormat = z.object({
1011
1011
  avif: z.number().int().min(0).max(9).optional(),
1012
1012
  webp: z.number().int().min(0).max(6).optional()
@@ -1020,7 +1020,8 @@ var presetSchema = z.object({
1020
1020
  target: z.enum(TARGETS).optional(),
1021
1021
  // images
1022
1022
  widths: z.array(z.number().int().min(16).max(16384)).min(1).optional(),
1023
- formats: z.array(z.enum(["avif", "webp"])).min(1).optional(),
1023
+ maxEdge: z.number().int().min(16).max(16384).optional(),
1024
+ formats: z.array(z.enum(["jpg", "png", "webp", "avif"])).min(1).optional(),
1024
1025
  quality: qualityByFormat.optional(),
1025
1026
  effort: effortByFormat.optional(),
1026
1027
  lossless: z.boolean().optional(),
@@ -1040,6 +1041,12 @@ var presetSchema = z.object({
1040
1041
  raw: rejected("`raw`", "is not supported; use `target`, `quality` or `crf`")
1041
1042
  }).strict().superRefine((preset, ctx) => {
1042
1043
  const say = (path, message) => ctx.addIssue({ code: "custom", path: [path], message });
1044
+ if (preset.maxEdge !== void 0 && preset.widths !== void 0) {
1045
+ say(
1046
+ "maxEdge",
1047
+ "cannot be combined with `widths`: one caps the longest side, the other lists widths"
1048
+ );
1049
+ }
1043
1050
  if (preset.vmaf !== void 0 && preset.ssimulacra2 !== void 0) {
1044
1051
  say("vmaf", "cannot be combined with `ssimulacra2`: a preset searches for one metric");
1045
1052
  }
@@ -7,7 +7,7 @@ import {
7
7
  MANIFEST_VERSION,
8
8
  parseManifest,
9
9
  serializeManifest
10
- } from "./chunk-5R7LR5BB.js";
10
+ } from "./chunk-TCQFQ354.js";
11
11
  import {
12
12
  ANIMATED_GIF_REASON,
13
13
  DEFAULT_PUBLIC_PATH,
@@ -16,20 +16,20 @@ import {
16
16
  classify,
17
17
  sha256,
18
18
  sha256File
19
- } from "./chunk-NAKV7HS5.js";
19
+ } from "./chunk-RZ2V4PCS.js";
20
20
  import {
21
21
  AbortedError,
22
22
  outputWidth
23
- } from "./chunk-WE7AEDVI.js";
23
+ } from "./chunk-QOG2F4SF.js";
24
24
  import {
25
25
  BUILTIN_POLICY,
26
26
  IMAGE_PRESETS,
27
27
  getImagePreset,
28
28
  pickAv1Encoder
29
- } from "./chunk-YBEC5QGV.js";
29
+ } from "./chunk-3PF6BBFK.js";
30
30
  import {
31
31
  SafeFs
32
- } from "./chunk-LHTI3JMC.js";
32
+ } from "./chunk-IZ4JEL3Q.js";
33
33
  import {
34
34
  BytecutError,
35
35
  PROJECT_RULES,
@@ -39,7 +39,7 @@ import {
39
39
  percentChange,
40
40
  plural,
41
41
  recordedByPlannedPath
42
- } from "./chunk-TWI2OZJZ.js";
42
+ } from "./chunk-FY53B4NB.js";
43
43
 
44
44
  // src/core/commands/optimize.ts
45
45
  import { stat as stat3 } from "fs/promises";
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  BytecutError,
4
4
  CONFIG_FILES
5
- } from "./chunk-TWI2OZJZ.js";
5
+ } from "./chunk-FY53B4NB.js";
6
6
 
7
7
  // src/core/fs-safe.ts
8
8
  import { randomBytes } from "crypto";
@@ -3,19 +3,19 @@ import {
3
3
  isFile,
4
4
  readOsReleaseId,
5
5
  run
6
- } from "./chunk-M6ZDSATP.js";
6
+ } from "./chunk-S3FQYUTI.js";
7
7
  import {
8
8
  createFfmpegVideoEngine
9
- } from "./chunk-WE7AEDVI.js";
9
+ } from "./chunk-QOG2F4SF.js";
10
10
  import {
11
11
  installHint,
12
12
  resolveBinary,
13
13
  resolveConfig
14
- } from "./chunk-YBEC5QGV.js";
14
+ } from "./chunk-3PF6BBFK.js";
15
15
  import {
16
16
  BytecutError,
17
17
  loadConfig
18
- } from "./chunk-TWI2OZJZ.js";
18
+ } from "./chunk-FY53B4NB.js";
19
19
 
20
20
  // src/cli/shared.ts
21
21
  import { existsSync } from "fs";
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "bytecut",
6
- version: "0.0.1",
6
+ version: "0.2.0",
7
7
  description: "Audit and optimize images and videos in web projects.",
8
8
  type: "module",
9
9
  license: "MIT",
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  BytecutError
4
- } from "./chunk-TWI2OZJZ.js";
4
+ } from "./chunk-FY53B4NB.js";
5
5
 
6
6
  // src/cli/errors.ts
7
7
  function guarded(run) {
@@ -6,7 +6,7 @@ import {
6
6
  probeFfmpeg,
7
7
  probeFfprobeVersion,
8
8
  resolveBinary
9
- } from "./chunk-YBEC5QGV.js";
9
+ } from "./chunk-3PF6BBFK.js";
10
10
 
11
11
  // src/core/doctor/report.ts
12
12
  var MIN_NODE_MAJOR = 22;
@@ -3,10 +3,10 @@ import {
3
3
  ffmpegDirOf,
4
4
  probeFfmpeg,
5
5
  resolveBinary
6
- } from "./chunk-YBEC5QGV.js";
6
+ } from "./chunk-3PF6BBFK.js";
7
7
  import {
8
8
  BytecutError
9
- } from "./chunk-TWI2OZJZ.js";
9
+ } from "./chunk-FY53B4NB.js";
10
10
 
11
11
  // src/core/posix.ts
12
12
  import { sep } from "path";
@@ -2,14 +2,14 @@
2
2
  import {
3
3
  outputWidth,
4
4
  toPosix
5
- } from "./chunk-WE7AEDVI.js";
5
+ } from "./chunk-QOG2F4SF.js";
6
6
  import {
7
7
  BUILTIN_POLICY,
8
8
  getVideoPreset
9
- } from "./chunk-YBEC5QGV.js";
9
+ } from "./chunk-3PF6BBFK.js";
10
10
  import {
11
11
  BytecutError
12
- } from "./chunk-TWI2OZJZ.js";
12
+ } from "./chunk-FY53B4NB.js";
13
13
 
14
14
  // src/core/classify/classify.ts
15
15
  var FLAT_COLOR_LIMIT = 64;
@@ -82,6 +82,18 @@ function targetWidths(presetWidths, sourceWidth) {
82
82
  if (sourceWidth < largest && !widths.includes(sourceWidth)) widths.push(sourceWidth);
83
83
  return [...new Set(widths)].sort((a, b) => a - b);
84
84
  }
85
+ function outputWidths(preset, sourceWidth, sourceHeight) {
86
+ if (preset.maxEdge === void 0) return targetWidths(preset.widths, sourceWidth);
87
+ const longest = Math.max(sourceWidth, sourceHeight);
88
+ if (longest <= preset.maxEdge) return [sourceWidth];
89
+ return [Math.max(1, Math.floor(sourceWidth * preset.maxEdge / longest))];
90
+ }
91
+ function formatsFor(preset, analysis, confidence) {
92
+ const ambiguousPng = analysis.format === "png" && confidence === "low";
93
+ const needsPng = analysis.alphaUsed || preset.lossless === true || ambiguousPng;
94
+ const out = preset.formats.map((f) => f === "jpg" && needsPng ? "png" : f);
95
+ return [...new Set(out)];
96
+ }
85
97
  function mirroredDir(source, staticRoot) {
86
98
  const dir = posix.dirname(source);
87
99
  if (staticRoot !== "." && staticRoot !== "") {
@@ -146,10 +158,10 @@ async function buildPlan(options) {
146
158
  const dir = mirroredDir(source, staticRoot);
147
159
  const name = posix.basename(source, posix.extname(source));
148
160
  const outputs = [];
149
- for (const width of targetWidths(preset.widths, analysis.width)) {
150
- for (const format of preset.formats) {
151
- const lossless = preset.lossless ?? (analysis.format === "png" && format === "webp");
152
- const path = posix.join(dir, name, `${name}-${width}.${format}`);
161
+ for (const width of outputWidths(preset, analysis.width, analysis.height)) {
162
+ for (const format of formatsFor(preset, analysis, choice.confidence)) {
163
+ const lossless = format === "png" || (preset.lossless ?? (analysis.format === "png" && format === "webp"));
164
+ const path = posix.join(dir, `${name}-${width}.${format}`);
153
165
  const owner = claimed.get(path);
154
166
  if (owner !== void 0) {
155
167
  throw new BytecutError(
@@ -248,7 +260,7 @@ function plannedVideoOutputs(source, staticRoot, preset, probe) {
248
260
  for (const codec of preset.codecs) {
249
261
  outputs.push({
250
262
  role: "video",
251
- path: posix2.join(dir, name, `${name}-${width}.${codec}.mp4`),
263
+ path: posix2.join(dir, `${name}-${width}.${codec}.mp4`),
252
264
  codec,
253
265
  width,
254
266
  crf: preset.crf[codec],
@@ -262,7 +274,7 @@ function plannedVideoOutputs(source, staticRoot, preset, probe) {
262
274
  }
263
275
  outputs.push({
264
276
  role: "poster",
265
- path: posix2.join(dir, name, `${name}-poster.avif`),
277
+ path: posix2.join(dir, `${name}-poster.avif`),
266
278
  format: "avif",
267
279
  width,
268
280
  quality: preset.poster.quality,
@@ -359,7 +371,7 @@ export {
359
371
  classify,
360
372
  sha256,
361
373
  sha256File,
362
- targetWidths,
374
+ outputWidths,
363
375
  mirroredDir,
364
376
  DEFAULT_PUBLIC_PATH,
365
377
  ANIMATED_GIF_REASON,
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  BytecutError
4
- } from "./chunk-TWI2OZJZ.js";
4
+ } from "./chunk-FY53B4NB.js";
5
5
 
6
6
  // src/cli/system.ts
7
7
  import { execFile } from "child_process";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  BytecutError
4
- } from "./chunk-TWI2OZJZ.js";
4
+ } from "./chunk-FY53B4NB.js";
5
5
 
6
6
  // src/core/plan/schema.ts
7
7
  import { z } from "zod";
@@ -11,11 +11,11 @@ var safeRelativePath = z.string().min(1).refine(
11
11
  );
12
12
  var outputSchema = z.object({
13
13
  path: safeRelativePath,
14
- format: z.enum(["avif", "webp"]),
14
+ format: z.enum(["jpg", "png", "webp", "avif"]),
15
15
  width: z.number().int().positive(),
16
16
  lossless: z.boolean(),
17
17
  quality: z.number().int().min(1).max(100),
18
- effort: z.number().int().min(0).max(9),
18
+ effort: z.number().int().min(0).max(10),
19
19
  search: z.object({
20
20
  metric: z.literal("ssimulacra2"),
21
21
  target: z.number().min(1).max(100),
@@ -139,7 +139,7 @@ import { z as z2 } from "zod";
139
139
  var outputSchema2 = z2.object({
140
140
  path: z2.string().min(1),
141
141
  url: z2.string(),
142
- format: z2.enum(["avif", "webp"]),
142
+ format: z2.enum(["jpg", "png", "webp", "avif"]),
143
143
  width: z2.number().int().nonnegative(),
144
144
  height: z2.number().int().nonnegative(),
145
145
  bytes: z2.number().int().nonnegative(),
@@ -111,11 +111,12 @@ function imageElement(entry, target, options) {
111
111
  ]
112
112
  });
113
113
  }
114
- const webp = ofFormat("webp");
115
- const fallback = webp[webp.length - 1];
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];
116
117
  if (!fallback) {
117
118
  hints.push(
118
- sources.length === 0 ? "No optimized outputs were written (they were not smaller than the source), so this uses the original file." : "No WebP output exists, so the <img> falls back to the original file."
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."
119
120
  );
120
121
  }
121
122
  const priority = options.priority ?? entry.priority === true;
@@ -125,6 +126,14 @@ function imageElement(entry, target, options) {
125
126
  const height = fallback?.height ?? entry.height;
126
127
  const img = [
127
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
+ ] : [],
128
137
  { name: "width", value: String(width) },
129
138
  { name: "height", value: String(height) },
130
139
  { name: "alt", value: options.alt ?? "" }
package/dist/cli.js CHANGED
@@ -2,25 +2,25 @@
2
2
  import {
3
3
  collectDoctorReport,
4
4
  formatDoctorReport
5
- } from "./chunk-326CQQWX.js";
5
+ } from "./chunk-PQRBLHCC.js";
6
6
  import {
7
7
  svgoEngine
8
8
  } from "./chunk-LTWUPQDT.js";
9
9
  import {
10
10
  audit
11
- } from "./chunk-N536JFSF.js";
11
+ } from "./chunk-5JEA233Z.js";
12
12
  import {
13
13
  package_default
14
- } from "./chunk-G53LNJ5O.js";
14
+ } from "./chunk-LJWOPIM6.js";
15
15
  import {
16
16
  formatAuditReport
17
- } from "./chunk-CEZK3RQJ.js";
17
+ } from "./chunk-HLBNRB3S.js";
18
18
  import "./chunk-3DBJKHOD.js";
19
- import "./chunk-5R7LR5BB.js";
19
+ import "./chunk-TCQFQ354.js";
20
20
  import {
21
21
  sharpImageEngine
22
- } from "./chunk-XFJ3HW4X.js";
23
- import "./chunk-NAKV7HS5.js";
22
+ } from "./chunk-CSFBJ3UH.js";
23
+ import "./chunk-RZ2V4PCS.js";
24
24
  import {
25
25
  createVideoEngine,
26
26
  discover,
@@ -28,17 +28,17 @@ import {
28
28
  ffmpegStatus,
29
29
  makeScorerFactory,
30
30
  noAssetsMessage
31
- } from "./chunk-4XOB3C34.js";
31
+ } from "./chunk-KN5QOB7R.js";
32
32
  import {
33
33
  doctorDeps
34
- } from "./chunk-M6ZDSATP.js";
35
- import "./chunk-WE7AEDVI.js";
36
- import "./chunk-YBEC5QGV.js";
37
- import "./chunk-LHTI3JMC.js";
34
+ } from "./chunk-S3FQYUTI.js";
35
+ import "./chunk-QOG2F4SF.js";
36
+ import "./chunk-3PF6BBFK.js";
37
+ import "./chunk-IZ4JEL3Q.js";
38
38
  import {
39
39
  guarded
40
- } from "./chunk-J2XJO4JZ.js";
41
- import "./chunk-TWI2OZJZ.js";
40
+ } from "./chunk-MV6J66AP.js";
41
+ import "./chunk-FY53B4NB.js";
42
42
 
43
43
  // src/cli/index.ts
44
44
  import { defineCommand, runMain } from "citty";
@@ -57,7 +57,7 @@ async function runDefault() {
57
57
  return;
58
58
  }
59
59
  if (isInteractive()) {
60
- const { runTui } = await import("./tui-J4Y44IL7.js");
60
+ const { runTui } = await import("./tui-ZAEXBY4L.js");
61
61
  await runTui({
62
62
  projectRoot: found.projectRoot,
63
63
  project: found.project,
@@ -125,14 +125,14 @@ var main = defineCommand({
125
125
  await runDefault();
126
126
  }),
127
127
  subCommands: {
128
- audit: () => import("./audit-HVW4EOLP.js").then((m) => m.default),
129
- optimize: () => import("./optimize-UXM6JEDA.js").then((m) => m.default),
130
- snippet: () => import("./snippet-2WVGMCNV.js").then((m) => m.default),
131
- compare: () => import("./compare-6PCE6533.js").then((m) => m.default),
132
- explain: () => import("./explain-SGN5BOR7.js").then((m) => m.default),
133
- init: () => import("./init-WHWUANOB.js").then((m) => m.default),
134
- check: () => import("./check-YONIQTM7.js").then((m) => m.default),
135
- doctor: () => import("./doctor-R226JU6P.js").then((m) => m.default)
128
+ audit: () => import("./audit-LJOGSJS6.js").then((m) => m.default),
129
+ optimize: () => import("./optimize-SLLQGDDX.js").then((m) => m.default),
130
+ snippet: () => import("./snippet-IIPVF6PS.js").then((m) => m.default),
131
+ compare: () => import("./compare-MPPGTUC5.js").then((m) => m.default),
132
+ explain: () => import("./explain-LKOB7FRT.js").then((m) => m.default),
133
+ init: () => import("./init-CWDWVJ55.js").then((m) => m.default),
134
+ check: () => import("./check-MEBXJBNG.js").then((m) => m.default),
135
+ doctor: () => import("./doctor-PWCUEBD5.js").then((m) => m.default)
136
136
  }
137
137
  });
138
138
  await runMain(main);
@@ -1,26 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  startCompareServer
4
- } from "./chunk-7IECTPJB.js";
4
+ } from "./chunk-7DHE25UE.js";
5
5
  import {
6
6
  MANIFEST_FILE,
7
7
  parseManifest
8
- } from "./chunk-5R7LR5BB.js";
8
+ } from "./chunk-TCQFQ354.js";
9
9
  import {
10
10
  configArg,
11
11
  discover
12
- } from "./chunk-4XOB3C34.js";
13
- import "./chunk-M6ZDSATP.js";
12
+ } from "./chunk-KN5QOB7R.js";
13
+ import "./chunk-S3FQYUTI.js";
14
14
  import {
15
15
  toPosix
16
- } from "./chunk-WE7AEDVI.js";
17
- import "./chunk-YBEC5QGV.js";
16
+ } from "./chunk-QOG2F4SF.js";
17
+ import "./chunk-3PF6BBFK.js";
18
18
  import {
19
19
  guarded
20
- } from "./chunk-J2XJO4JZ.js";
20
+ } from "./chunk-MV6J66AP.js";
21
21
  import {
22
22
  BytecutError
23
- } from "./chunk-TWI2OZJZ.js";
23
+ } from "./chunk-FY53B4NB.js";
24
24
 
25
25
  // src/cli/commands/compare.ts
26
26
  import { readFile } from "fs/promises";
@@ -2,24 +2,24 @@
2
2
  import {
3
3
  collectDoctorReport,
4
4
  formatDoctorReport
5
- } from "./chunk-326CQQWX.js";
5
+ } from "./chunk-PQRBLHCC.js";
6
6
  import {
7
7
  doctorDeps
8
- } from "./chunk-M6ZDSATP.js";
8
+ } from "./chunk-S3FQYUTI.js";
9
9
  import {
10
10
  resolveConfig
11
- } from "./chunk-YBEC5QGV.js";
11
+ } from "./chunk-3PF6BBFK.js";
12
12
  import {
13
13
  isGitIgnored
14
14
  } from "./chunk-DJ45NQZD.js";
15
15
  import {
16
16
  guarded
17
- } from "./chunk-J2XJO4JZ.js";
17
+ } from "./chunk-MV6J66AP.js";
18
18
  import {
19
19
  BytecutError,
20
20
  detectProject,
21
21
  loadConfig
22
- } from "./chunk-TWI2OZJZ.js";
22
+ } from "./chunk-FY53B4NB.js";
23
23
 
24
24
  // src/cli/commands/doctor.ts
25
25
  import { defineCommand } from "citty";
@@ -1,34 +1,34 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  sharpImageEngine
4
- } from "./chunk-XFJ3HW4X.js";
4
+ } from "./chunk-CSFBJ3UH.js";
5
5
  import {
6
6
  classify,
7
7
  classifyVideo,
8
8
  mirroredDir,
9
- plannedVideoOutputs,
10
- targetWidths
11
- } from "./chunk-NAKV7HS5.js";
9
+ outputWidths,
10
+ plannedVideoOutputs
11
+ } from "./chunk-RZ2V4PCS.js";
12
12
  import {
13
13
  configArg,
14
14
  createVideoEngine,
15
15
  discover
16
- } from "./chunk-4XOB3C34.js";
17
- import "./chunk-M6ZDSATP.js";
16
+ } from "./chunk-KN5QOB7R.js";
17
+ import "./chunk-S3FQYUTI.js";
18
18
  import {
19
19
  toPosix
20
- } from "./chunk-WE7AEDVI.js";
20
+ } from "./chunk-QOG2F4SF.js";
21
21
  import {
22
22
  getVideoPreset
23
- } from "./chunk-YBEC5QGV.js";
23
+ } from "./chunk-3PF6BBFK.js";
24
24
  import {
25
25
  guarded
26
- } from "./chunk-J2XJO4JZ.js";
26
+ } from "./chunk-MV6J66AP.js";
27
27
  import {
28
28
  BytecutError,
29
29
  formatBytes,
30
30
  matchesGlob
31
- } from "./chunk-TWI2OZJZ.js";
31
+ } from "./chunk-FY53B4NB.js";
32
32
 
33
33
  // src/cli/commands/explain.ts
34
34
  import { stat } from "fs/promises";
@@ -122,8 +122,8 @@ async function explainFile(options) {
122
122
  const resolved2 = preset2 ? catalog.image[preset2] : null;
123
123
  const dir = mirroredDir(source, config.project.staticRoot);
124
124
  const name = posix.basename(source, posix.extname(source));
125
- const outputs = resolved2 ? targetWidths(resolved2.widths, analysis.width).flatMap(
126
- (w) => resolved2.formats.map((f) => posix.join(dir, name, `${name}-${w}.${f}`))
125
+ const outputs = resolved2 ? outputWidths(resolved2, analysis.width, analysis.height).flatMap(
126
+ (w) => resolved2.formats.map((f) => posix.join(dir, `${name}-${w}.${f}`))
127
127
  ) : [];
128
128
  return {
129
129
  ...base,
@@ -168,7 +168,9 @@ function formatExplanation(e) {
168
168
  }
169
169
  const r = e.resolved;
170
170
  if (r && "widths" in r) {
171
- lines.push(` widths: ${r.widths.join(", ")} (never above the source)`);
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
+ );
172
174
  lines.push(` formats: ${r.formats.join(", ")}`);
173
175
  for (const f of r.formats) {
174
176
  const lossless = r.lossless ? ", lossless" : "";
@@ -6,20 +6,20 @@ import {
6
6
  applyChoices,
7
7
  isEmpty,
8
8
  renderConfig
9
- } from "./chunk-GMHOU4Y5.js";
9
+ } from "./chunk-4FIB6VNC.js";
10
10
  import {
11
11
  writeConfigFile
12
- } from "./chunk-LHTI3JMC.js";
12
+ } from "./chunk-IZ4JEL3Q.js";
13
13
  import {
14
14
  guarded
15
- } from "./chunk-J2XJO4JZ.js";
15
+ } from "./chunk-MV6J66AP.js";
16
16
  import {
17
17
  BytecutError,
18
18
  FRAMEWORKS,
19
19
  PLACEHOLDERS,
20
20
  detectProject,
21
21
  findConfigFiles
22
- } from "./chunk-TWI2OZJZ.js";
22
+ } from "./chunk-FY53B4NB.js";
23
23
 
24
24
  // src/cli/commands/init.ts
25
25
  import { relative } from "path";
@@ -1,42 +1,42 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  package_default
4
- } from "./chunk-G53LNJ5O.js";
4
+ } from "./chunk-LJWOPIM6.js";
5
5
  import {
6
6
  formatFindings,
7
7
  formatPlan,
8
8
  formatSummary,
9
9
  optimize
10
- } from "./chunk-CEZK3RQJ.js";
10
+ } from "./chunk-HLBNRB3S.js";
11
11
  import "./chunk-3DBJKHOD.js";
12
12
  import {
13
13
  isPlaceholderKind,
14
14
  parsePlan,
15
15
  serializePlan
16
- } from "./chunk-5R7LR5BB.js";
16
+ } from "./chunk-TCQFQ354.js";
17
17
  import {
18
18
  sharpImageEngine
19
- } from "./chunk-XFJ3HW4X.js";
20
- import "./chunk-NAKV7HS5.js";
19
+ } from "./chunk-CSFBJ3UH.js";
20
+ import "./chunk-RZ2V4PCS.js";
21
21
  import {
22
22
  configArg,
23
23
  createVideoEngine,
24
24
  ffmpegInstallText,
25
25
  makeScorerFactory
26
- } from "./chunk-4XOB3C34.js";
27
- import "./chunk-M6ZDSATP.js";
28
- import "./chunk-WE7AEDVI.js";
26
+ } from "./chunk-KN5QOB7R.js";
27
+ import "./chunk-S3FQYUTI.js";
28
+ import "./chunk-QOG2F4SF.js";
29
29
  import {
30
30
  resolveConfig
31
- } from "./chunk-YBEC5QGV.js";
32
- import "./chunk-LHTI3JMC.js";
31
+ } from "./chunk-3PF6BBFK.js";
32
+ import "./chunk-IZ4JEL3Q.js";
33
33
  import {
34
34
  guarded
35
- } from "./chunk-J2XJO4JZ.js";
35
+ } from "./chunk-MV6J66AP.js";
36
36
  import {
37
37
  BytecutError,
38
38
  loadConfig
39
- } from "./chunk-TWI2OZJZ.js";
39
+ } from "./chunk-FY53B4NB.js";
40
40
 
41
41
  // src/cli/commands/optimize.ts
42
42
  import { existsSync } from "fs";
@@ -3,27 +3,27 @@ import {
3
3
  buildSnippet,
4
4
  originalUrl,
5
5
  snippetFormatFor
6
- } from "./chunk-U4KJMKI5.js";
6
+ } from "./chunk-WZ6MMRNB.js";
7
7
  import "./chunk-3DBJKHOD.js";
8
8
  import {
9
9
  MANIFEST_FILE,
10
10
  parseManifest
11
- } from "./chunk-5R7LR5BB.js";
11
+ } from "./chunk-TCQFQ354.js";
12
12
  import {
13
13
  configArg,
14
14
  discover
15
- } from "./chunk-4XOB3C34.js";
16
- import "./chunk-M6ZDSATP.js";
15
+ } from "./chunk-KN5QOB7R.js";
16
+ import "./chunk-S3FQYUTI.js";
17
17
  import {
18
18
  toPosix
19
- } from "./chunk-WE7AEDVI.js";
20
- import "./chunk-YBEC5QGV.js";
19
+ } from "./chunk-QOG2F4SF.js";
20
+ import "./chunk-3PF6BBFK.js";
21
21
  import {
22
22
  guarded
23
- } from "./chunk-J2XJO4JZ.js";
23
+ } from "./chunk-MV6J66AP.js";
24
24
  import {
25
25
  BytecutError
26
- } from "./chunk-TWI2OZJZ.js";
26
+ } from "./chunk-FY53B4NB.js";
27
27
 
28
28
  // src/cli/commands/snippet.ts
29
29
  import { readFile } from "fs/promises";
@@ -1,43 +1,43 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  audit
4
- } from "./chunk-N536JFSF.js";
4
+ } from "./chunk-5JEA233Z.js";
5
5
  import {
6
6
  formatSummary,
7
7
  optimize
8
- } from "./chunk-CEZK3RQJ.js";
8
+ } from "./chunk-HLBNRB3S.js";
9
9
  import {
10
10
  buildSnippet,
11
11
  originalUrl,
12
12
  snippetFormatFor
13
- } from "./chunk-U4KJMKI5.js";
13
+ } from "./chunk-WZ6MMRNB.js";
14
14
  import "./chunk-3DBJKHOD.js";
15
15
  import {
16
16
  startCompareServer
17
- } from "./chunk-7IECTPJB.js";
17
+ } from "./chunk-7DHE25UE.js";
18
18
  import {
19
19
  MANIFEST_FILE,
20
20
  parseManifest
21
- } from "./chunk-5R7LR5BB.js";
22
- import "./chunk-NAKV7HS5.js";
21
+ } from "./chunk-TCQFQ354.js";
22
+ import "./chunk-RZ2V4PCS.js";
23
23
  import {
24
24
  toPosix
25
- } from "./chunk-WE7AEDVI.js";
25
+ } from "./chunk-QOG2F4SF.js";
26
26
  import {
27
27
  resolveConfig,
28
28
  resolveConfigData
29
- } from "./chunk-YBEC5QGV.js";
29
+ } from "./chunk-3PF6BBFK.js";
30
30
  import {
31
31
  applyChoices,
32
32
  canRewrite,
33
33
  isEmpty,
34
34
  renderChoicesSnippet,
35
35
  renderConfig
36
- } from "./chunk-GMHOU4Y5.js";
36
+ } from "./chunk-4FIB6VNC.js";
37
37
  import {
38
38
  SafeFs,
39
39
  writeConfigFile
40
- } from "./chunk-LHTI3JMC.js";
40
+ } from "./chunk-IZ4JEL3Q.js";
41
41
  import {
42
42
  ALL_RULES,
43
43
  BytecutError,
@@ -46,7 +46,7 @@ import {
46
46
  parseConfig,
47
47
  percentChange,
48
48
  plural
49
- } from "./chunk-TWI2OZJZ.js";
49
+ } from "./chunk-FY53B4NB.js";
50
50
 
51
51
  // src/tui/index.tsx
52
52
  import { render } from "ink";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bytecut",
3
- "version": "0.0.1",
3
+ "version": "0.2.0",
4
4
  "description": "Audit and optimize images and videos in web projects.",
5
5
  "type": "module",
6
6
  "license": "MIT",