All notes

My Image Tool Refuses to Run on a Dirty Git Tree

A 26.96 MB PNG became 1.17 MB. The compression was the easy part. Making a tool that edits your code something you can trust was the hard part.

3 min readrusttypescripttooling

On a real Next.js project, Vysk turned a 26.96 MB PNG into a 1.17 MB file. That is about 96% smaller.

Compressing an image is not hard. Plenty of tools do it. What took the real thinking was a different question: how do you build a tool that is allowed to touch someone's project, and make it something they'd actually trust?

What Vysk does

Vysk is an npm package for making website images smaller. It scans your project, converts PNG and JPG files to WebP, and prints a report of what it saved.

The rules came before the features:

  • It never touches your originals. The optimised file goes beside the source.
  • It skips any output that isn't smaller. Sometimes a conversion makes a file bigger. Keeping it would make the "optimisation" a lie.
  • It remembers what it has done. A hash-based cache means unchanged images aren't processed again. On the same project, a second run over all 14 images finished instantly from the cache with identical totals.
  • --dry-run runs the entire pipeline and writes nothing, so you can see the plan before you commit to it.

Two things that broke

WebP has a size limit. The format can't encode an image larger than 16,383 pixels on either side. Huge images were simply being skipped. Now they fall back to AVIF, using the ravif crate. That pulled a feature forward from a later phase, and it added nasm as a build requirement, which is the sort of cost you only find after you've paid it.

My git check was too literal. Vysk refuses to make changes unless your git tree is clean, so you can always undo what it did. My first check looked for a .git folder in the project root. If your project sat inside a larger repository, it declared there was no git and refused to run.

I replaced it by asking git itself:

git status --porcelain

If that prints nothing, the tree is clean. Git finds the repository on its own, wherever it lives, and the answer comes from the one tool that knows. I wrote the first version assuming where the answer would be. Asking the real source was simpler and correct.

The command that edits your code

Optimising images is only half the job. The files are useless if your code still points at hero.png while the optimised file is hero.webp.

So there is a rewrite command that finds image references in your code and updates them. This is the dangerous one, because it modifies source files, so it is built as a sequence of refusals:

  1. It refuses to run on a dirty git tree.
  2. It shows the complete plan of what it will change.
  3. It writes nothing until you explicitly say yes.
  4. It backs up the originals first.

It has 25 passing tests, and most of them check that it does nothing when it shouldn't.

Why Rust, and why not just use sharp

sharp exists, and it is excellent. I'm not going to pretend I picked Rust because it was the only way. Part of the reason is that I wanted this to be a real Rust project, and an image tool is a good fit: decoding and encoding is exactly the kind of work a native core is for.

The architecture splits the job along a clean line:

CLI → Node + TypeScript → napi-rs → Rust core → WebP / AVIF

Node understands the project: scanning, config, caching, reporting. Rust does the decoding, compression and encoding. The two are joined with napi-rs, so a user installs a single package and never needs a Rust toolchain.

Where it stands

Vysk isn't on npm yet. I haven't tested it on Windows, and I'd like people to install it and tell me what breaks.

That is the honest state of it: it works on the projects I've tried, and it needs more machines than mine.

Next noteWhy I Split Billing Into Four Services