@dnbhq/sharp-lint-staged
v1.0.2
Published
sharp-powered image optimisation CLI designed for lint-staged usage with sensible defaults
Downloads
323
Maintainers
Readme
Sharp-lint-staged
sharp-powered image optimisation CLI designed for lint-staged usage with sensible defaults
This is a drop-in successor to @davidsneighbour/imagemin-lint-staged. It keeps the same workflow — optimise staged images on commit — but replaces the unmaintained imagemin toolchain (which ships a long tail of security advisories on its native binary dependencies) with sharp for raster images and the actively maintained svgo for SVG.
Raster formats (GIF, PNG, JPEG) are handled by sharp's modern libvips-based encoders. SVG is handled by svgo directly, with no imagemin wrapper in between.
Why switch from imagemin?
imageminand its plugins are effectively unmaintained and pull in native binaries (gifsicle,jpegtran,optipng) that generate repeatednpm auditadvisories.sharpis a single, well-maintained, prebuilt native dependency covering GIF, PNG and JPEG (plus WebP, AVIF and TIFF if you want them later).svgoreplacesimagemin-svgowith no loss of functionality.
Installation
npm i --save-dev @dnbhq/sharp-lint-stagedRequires Node.js 22 or newer.
Usage
Use in conjunction with lint-staged. In your package.json:
"lint-staged": {
"*.{png,jpeg,jpg,gif,svg}": ["sharp-lint-staged"]
}On commit, every staged image matching the glob is optimised in place. lint-staged automatically re-stages the modified files. The optimised buffer is only written back when it is strictly smaller than the original, so already-optimised images are left untouched and repeated runs converge to a stable file.
You can also call the API directly:
import { optimizeFile } from '@dnbhq/sharp-lint-staged';
const wasRewritten = await optimizeFile('assets/logo.png');optimizeFile resolves to true when the file was rewritten with a smaller result, and false when it was left unchanged (already optimal, or an unsupported format).
Configuration
The package uses cosmiconfig with the module name sharp-lint-staged. You can configure the encoders from a project-level configuration file instead of changing the package source.
Configuration is searched from the current working directory. For normal lint-staged usage this is usually the repository root, because lint-staged runs from there.
Supported configuration locations include:
package.json, using thesharp-lint-stagedproperty.sharp-lint-stagedrc.sharp-lint-stagedrc.{json,yaml,yml,js,ts,mjs,cjs}.config/sharp-lint-stagedrc.config/sharp-lint-stagedrc.{json,yaml,yml,js,ts,mjs,cjs}sharp-lint-staged.config.{js,ts,mjs,cjs}
cosmiconfig's asynchronous API is used, so ESM configuration files are supported. In ESM projects, prefer sharp-lint-staged.config.js or sharp-lint-staged.config.mjs with export default.
Default configuration
The built-in defaults favour the strongest encoder effort available in sharp while keeping JPEG re-encoding visually close to the source. SVG optimisation mirrors the lossless, accessibility-friendly preset from the predecessor package.
export default {
jpeg: {
quality: 80,
mozjpeg: true,
progressive: true,
},
png: {
compressionLevel: 9,
effort: 10,
palette: false,
progressive: false,
},
gif: {
effort: 10,
},
svg: {
multipass: true,
plugins: [
{
name: 'preset-default',
params: {
// svgo v4 keeps `viewBox` by default; only the remaining
// safety overrides are needed.
overrides: {
cleanupIds: false,
removeDesc: false,
},
},
},
],
},
resize: null,
};Example using package.json
{
"sharp-lint-staged": {
"jpeg": { "quality": 82, "mozjpeg": true },
"png": { "compressionLevel": 9, "effort": 10 },
"gif": { "effort": 10 }
}
}Example using .sharp-lint-stagedrc.json
{
"png": { "compressionLevel": 9, "effort": 10, "palette": true, "quality": 90 },
"svg": {
"multipass": true,
"plugins": [{ "name": "preset-default" }]
}
}Example using sharp-lint-staged.config.mjs
export default {
// Resize is OFF by default. Enable it to cap the maximum width:
resize: { width: 2000, withoutEnlargement: true },
jpeg: { quality: 80, mozjpeg: true, progressive: true },
};Options
All options are optional. Each top-level key configures one encoder.
| Option | Engine | Passed to | Default |
| -------- | ------ | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| jpeg | sharp | sharp.jpeg() | { quality: 80, mozjpeg: true, progressive: true } |
| png | sharp | sharp.png() | { compressionLevel: 9, effort: 10, palette: false, progressive: false } |
| gif | sharp | sharp.gif() | { effort: 10 } |
| svg | svgo | svgo.optimize() | preset-default with cleanupIds/removeDesc disabled (viewBox kept) |
| resize | sharp | sharp.resize() (only when non-null) | null (no resizing) |
The object for each key is passed directly to the matching engine method.
Default option details
jpegusesmozjpeg: truefor the best compression sharp offers, plusprogressive: truefor better perceived load time.quality: 80is a widely-used near-transparent default. JPEG is re-encoded rather than losslessly rewritten — see the trade-offs below.pnguses the maximumcompressionLevel(9) andeffort(10).palette: falsekeeps output lossless; setpalette: true(optionally withquality/colours) to enable lossy palette quantisation for much smaller files.gifuses the maximumeffort(10). Animated GIFs are preserved (all frames are read and written back).svguses svgo'spreset-default. svgo v4 keepsviewBoxby default (so scaling is preserved);cleanupIdsis disabled to avoid breaking inlined/scripted/externally-referenced SVGs;removeDescis disabled to preserve accessibility metadata.resizeisnull(disabled) to preserve image dimensions by default. Provide a sharp resize options object to cap dimensions; usingwithoutEnlargement: trueis recommended so smaller images are not upscaled.
Behaviour
- If no configuration file is found, all defaults above are used.
- If a key is missing, that format uses its built-in default.
- If a key is present, its value replaces the built-in default for that format. The configuration is not deep-merged.
- The format is detected from the file content (and the extension for SVG), so the file extension does not have to match the real format.
- Unsupported formats are skipped silently, which makes a broad lint-staged glob safe.
- A file is only rewritten when the optimised result is strictly smaller than the original.
Trade-offs and known differences
Because sharp re-encodes raster images rather than performing the byte-level lossless transforms that jpegtran/optipng/gifsicle did, there are intentional behavioural differences from imagemin-lint-staged. These are tracked in ToDo.md:
- JPEG is lossy. sharp has no lossless JPEG optimiser equivalent to
jpegtran. The "strictly smaller" write guard keeps repeated commits from snowballing quality loss, but the first optimisation of a JPEG is a re-encode. - PNG/GIF are lossless by default and effectively idempotent thanks to the write guard.
- SVG is handled by svgo, matching the previous behaviour.
Migrating from imagemin-lint-staged
Replace the dependency:
npm rm @davidsneighbour/imagemin-lint-staged npm i -D @dnbhq/sharp-lint-stagedUpdate the lint-staged command from
imagemin-lint-stagedtosharp-lint-staged.Rename any config file/key from
imagemin-lint-stagedtosharp-lint-staged. The config shape changed:gifsicle/jpegtran/optipngbecomegif/jpeg/png(sharp options), whilesvgobecomessvg(svgo options).
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
Development
npm install # install dependencies
npm run build # compile TypeScript from src/ to dist/
npm test # run the vitest suite against real fixtures