A 900-line README is where a fact goes to hide. Everything was in it and nothing
was findable: no search, no table of contents, and a crop anchor grid eleven
screens below the flag that needs it.
Added
- A documentation site at https://stamat.info/poops-images/, built with
Poops fromsite/srcand published by a
GitHub Actions workflow on every push tomain. Same content as the README,
one page per topic, with search and a table of contents.npm run docs
serves it locally on port 4041;npm run docs:buildwritessite/dist. - A Quick examples page on that site — a recipe per job, runnable as
written: a responsive srcset set, square thumbnails, an anchored hero crop, an
LQIP placeholder, a watermark, watch mode. It is the only page without a
README counterpart, because it composes documented facts rather than adding
any.
Security
sharpmoved from^0.33.0to^0.35.0, which closes four libvips
vulnerabilities inherited by every 0.33 and 0.34 release
(GHSA-f88m-g3jw-g9cj:
CVE-2026-33327, CVE-2026-33328, CVE-2026-35590, CVE-2026-35591). There is no
patched 0.33 line, so the upgrade is the only fix.
Changed
- The default
includeglob now namessvgandgifalongside the raster
formats, becauseincludegoverns every pipeline. If your config sets a
custominclude— say"**/*.jpg"— SVGs and GIFs are no longer processed
unless the glob covers them; add them to the pattern to keep the old
behavior. - Node ≥ 20.9 is now the minimum, because sharp 0.35 dropped Node 18. On
Node 18 the install now fails outright rather than half-working;enginesin
package.jsonsays so, so npm warns before anything is written. - AVIF output is not byte-for-byte what it was. sharp 0.35 retuned lossy AVIF
around SSIMULACRA2 quality metrics, so the samequalitynumber lands on a
different file size — usually smaller. Nothing in the config changed; if you
have a size budget pinned to a byte count, re-measure it.
Fixed
- An output never overwrites its own source. With
inandoutpointing at
the same directory — the default is both"."— a conversion-only variant,
a minified SVG or a copied GIF landed on the file it came from, and because
the cache kept the pre-write mtime, every run recompressed the previous
run's recompression. Such writes are now refused and counted as errors;
suffixed variants (photo-300w.jpg) are unaffected. - The tool no longer eats its own output. With
outinsidein— say
-i . -o dist— every run re-ingested the previous run's variants, producing
dist/dist/dist/a-100w-100w-100w.jpgby the third build. The output subtree
is now excluded from discovery, and anything the cache records as a written
variant is dropped from the source list, in build and watch mode both. - Pointing at one file means one file.
--in photo.jpgused to run the raster
pipeline on that file and then minify every SVG and copy every GIF found
beside it, recursively, because SVG and GIF discovery ignoredinclude. All
three pipelines now read the sameincludeglob. - Watch mode obeys
includeandexclude. It used to process any supported
extension anywhere underin— the exclude globs were handed to the watcher
in a form it never matched, and the include glob was not consulted at all. A
watch event now runs through the same discovery predicate as a build, so a
file is processed on change exactly when a build would have picked it up. - The custom-handler cache holds one entry per handler file instead of one per
edit, so a long watch session editing handlers no longer grows it without
bound. Edited handlers still reload on the next run. - Preprocessor
sizesare validated like top-levelsizes. A typo in a crop
anchor —"crop": ["middle", "top"]— used to surface as a sharp error per
image at process time; it is now one config error at startup. - The CLI exits
1when any file failed to process. It used to exit0no
matter how many errors were logged, so a CI build over corrupt sources came
up green. A run that merely contains failures still completes — the exit
code is how CI hears about them. verbose: falsemeans quiet. The SVGMinified:line and the format
normalization notices (Opaque PNG → JPEG:and friends) printed for every
file regardless of the setting; they are per-file progress and now obey it.- Editing an SVG under watch regenerates its preprocessor variants. Watch mode
used to only re-minify the SVG, leaving variants from"svg": true
preprocessors stale until the next full build; both modes now share one SVG
processing path. - SVG failures count. An unreadable SVG or an SVGO parse error was logged but
leftstats.errorsat zero, so a build full of broken SVGs still reported
itself clean to any caller reading the stats. They now land in the same
error count as raster failures, in build and watch mode both. - One truncated image no longer kills the whole build. A source whose header
reads fine but whose body fails to decode used to abortprocessAll()— the
remaining images went unprocessed and the cache unsaved. It is now counted in
stats.errorslike any other bad file, and a failed decode leaves the
previous run's outputs and cache entry for that file untouched instead of
sweeping them as stale. - The
poops.jsonexample under Poops Integration in the README was missing
a comma between two size objects, so copying it out produced invalid JSON. The
test that checks every README config example against the schema parses each
fence and skips what will not parse, which is exactly how a syntax error hid
from it; it now reports on the docs site's examples as well.
Full Changelog: v1.3.0...v1.4.0