Build-time image pipeline for Next.js and web apps.
npm · Contributing · Security · Changelog
Generate optimized derivatives (webp, avif) and blurDataURL placeholders with hash-based caching.
- One command for image conversion + manifest generation
- Blur placeholder generation for
next/image(blurDataURL) - Hash-based cache for fast reruns
- Bounded parallel processing with
--concurrency - Deterministic CI guard with
--check - Structured machine output with
--json
Runtime requirement: Node.js >= 20.
Install globally:
npm install -g @imageforge/cliRun without global install:
npx @imageforge/cli ./public/imagesimageforge ./public/imagesBy default this writes:
- Derivatives next to source files (for example
hero.jpg -> hero.webp) - Cache file at
./public/images/.imageforge-cache.json - Manifest at
./imageforge.json
Generate both formats:
imageforge ./public/images --formats webp,avifWrite outputs to a dedicated directory:
imageforge ./public/images --out-dir ./public/generatedGenerate responsive width variants:
imageforge ./public/images --formats webp,avif --widths 320,640,960,1280--widths values are requested targets. ImageForge generates effective widths that do not exceed
the source image dimensions (no upscaling).
imageforge <directory> [options]| Option | Description |
|---|---|
-o, --output <path> |
Manifest output path (default: imageforge.json) |
-f, --formats <formats> |
Output formats, comma-separated (default: webp) |
-q, --quality <number> |
Output quality 1..100 (default: 80) |
--blur / --no-blur |
Enable/disable blur placeholder generation |
--blur-size <number> |
Blur dimensions 1..256 (default: 4) |
--widths <list> |
Requested width targets as comma-separated integers (source-bounded, max 16 unique) |
--cache / --no-cache |
Enable/disable cache reads/writes |
--force-overwrite / --no-force-overwrite |
Allow/disallow overwriting existing outputs |
--check / --no-check |
Check mode for CI (exit 1 if processing is needed) |
--dry-run / --no-dry-run |
Preview processing without writing outputs, manifest, or cache |
--include <pattern> |
Include input-relative glob pattern (repeatable or comma-separated) |
--exclude <pattern> |
Exclude input-relative glob pattern (repeatable or comma-separated) |
--out-dir <path> |
Output directory for generated derivatives |
--concurrency <number> |
Parallel processing (1..64, default: min(8, availableParallelism)) |
--json / --no-json |
Emit machine-readable JSON report to stdout |
--verbose / --no-verbose |
Show additional diagnostics |
--quiet / --no-quiet |
Suppress per-file non-error logs |
--config <path> |
Explicit JSON config path |
-V, --version |
Print version |
-h, --help |
Print help |
- Normal runs exit with code
1if any file fails processing. --checkexits1when at least one file needs processing, otherwise0.- Symlinks are skipped during discovery.
- Output collision checks are case-insensitive.
- Existing outputs are protected unless explicitly overwritten with
--force-overwrite. - With
--check, ImageForge prints an exact copy-pastable rerun command. --dry-runpreviews which images would be processed but performs no writes.--checkand--dry-runcannot be used together.- Responsive width sets are opt-in via
--widths(default behavior is unchanged). - Requested widths are targets; generated effective widths may be smaller for source-bounded runs.
- Width lists are capped at 16 unique values to bound compute and output fan-out.
- Full behavior contract:
docs/product/responsive-widths-contract.md.
ImageForge enforces a maximum of 16 unique requested widths per run/config. This guard keeps responsive generation predictable and reduces accidental or hostile CPU/IO amplification from oversized width lists.
Scaffold a starter config:
imageforge initOverwrite an existing scaffold:
imageforge init --forceConfig resolution order:
- Internal defaults
- Config file (
--config <path>, otherwiseimageforge.config.json, otherwisepackage.json#imageforge) - CLI flags
Unknown config keys fail fast.
Example imageforge.config.json:
{
"output": "imageforge.json",
"formats": ["webp", "avif"],
"quality": 80,
"blur": true,
"blurSize": 4,
"widths": [320, 640, 960, 1280],
"cache": true,
"dryRun": false,
"include": ["**/*.jpg", "**/*.png"],
"exclude": ["**/legacy/**"],
"outDir": "public/generated",
"concurrency": 4
}Use --json to emit a structured report:
imageforge ./public/images --jsonThe report includes:
- Effective options
- Per-image status (
processed,cached,failed,needs-processing) - Effective generated widths in
images[*].variants[*].widthwhen--widthsis used - Summary counters and size totals
- Rerun command hint for
--checkfailures
Manifest shape (imageforge.json):
{
"version": "1.0",
"generated": "2026-02-08T00:00:00.000Z",
"images": {
"hero.jpg": {
"width": 1920,
"height": 1280,
"aspectRatio": 1.5,
"blurDataURL": "data:image/png;base64,...",
"originalSize": 345678,
"outputs": {
"webp": { "path": "hero.w1280.webp", "size": 50210 },
"avif": { "path": "hero.w1280.avif", "size": 31100 }
},
"variants": {
"webp": [
{ "width": 320, "height": 213, "path": "hero.w320.webp", "size": 9012 },
{ "width": 640, "height": 427, "path": "hero.w640.webp", "size": 17654 },
{ "width": 960, "height": 640, "path": "hero.w960.webp", "size": 33210 },
{ "width": 1280, "height": 853, "path": "hero.w1280.webp", "size": 50210 }
],
"avif": [
{ "width": 320, "height": 213, "path": "hero.w320.avif", "size": 6010 },
{ "width": 640, "height": 427, "path": "hero.w640.avif", "size": 12203 },
{ "width": 960, "height": 640, "path": "hero.w960.avif", "size": 21998 },
{ "width": 1280, "height": 853, "path": "hero.w1280.avif", "size": 31100 }
]
},
"hash": "abc123..."
}
}
}Notes:
- Manifest keys and output paths are input-directory-relative POSIX paths.
- When using
--out-dir, output paths remain relative to the input directory. - If
--out-diris outside the input tree, manifest paths may include../segments. - When
--widthsis used,outputs.<format>points to the largest generated variant. variants[*].widthstores effective generated widths (requested values filtered by source size).
import manifest from "./imageforge.json";
type Manifest = typeof manifest;
export function getImageData(src: string) {
return (manifest as Manifest).images[src];
}Then use:
- Original source path for
src getImageData(src)?.blurDataURLforplaceholder="blur"
Optional srcset helper for responsive variants:
export function getSrcSet(src: string, format: "webp" | "avif") {
const variants = (manifest as Manifest).images[src]?.variants?.[format];
return variants?.map((variant) => `${variant.path} ${variant.width}w`).join(", ");
}ImageForge supports both ESM (import) and CJS (require) consumers.
Root exports processor helpers and manifest types.
Runner functions are exposed on a stable subpath API: @imageforge/cli/runner.
ESM:
import * as imageforge from "@imageforge/cli";
import * as processor from "@imageforge/cli/processor";
import { getDefaultConcurrency, runImageforge } from "@imageforge/cli/runner";CJS:
const imageforge = require("@imageforge/cli");
const processor = require("@imageforge/cli/processor");
const { getDefaultConcurrency, runImageforge } = require("@imageforge/cli/runner");Useful root exports include processImage, convertImage, generateBlurDataURL, and manifest
types. The runner API is intentionally subpath-only and semver-stable.
Current supported source extensions:
jpg,jpeg,png,gif,tiff,tif
Notes:
webpandavifsource files are currently excluded as inputs.- GIF handling is static-only (first frame).
- Source-input expansion roadmap:
docs/product/source-input-roadmap.md.
Use check mode in CI to fail when assets are out of date:
imageforge ./public/images --checkCI-native benchmark tooling and contracts live in docs/benchmark/.
- Standard and thresholds:
docs/benchmark/STANDARD.md - Data contracts:
docs/benchmark/INTERFACES.md - Operational runbook:
docs/benchmark/RUNBOOK.md - Dataset policy:
docs/benchmark/DATASET_POLICY.md
Core commands:
pnpm run bench:dataset:download -- --dataset-version 1.0.0 --tier tier30 --out-dir /tmp/imageforge-bench-dataset
pnpm run bench:run -- --cli-path ./dist/cli.js --tier-manifest /tmp/imageforge-bench-dataset/extracted/tier30/tier-manifest.json --workspace /tmp/imageforge-bench-run --run-count 4 --profiles P1,P2,P3
pnpm run bench:compare -- --base-summary /tmp/base-summary.json --head-summary /tmp/head-summary.json --out-json /tmp/compare.json --out-md /tmp/compare.md
pnpm run bench:report -- --head-summary /tmp/head-summary.json --base-summary /tmp/base-summary.json --compare /tmp/compare.json --out /tmp/report.mdpnpm install
pnpm build
pnpm run typecheck
pnpm run lint
pnpm run format:check
pnpm test
pnpm run test:mutation:pilot
pnpm run checkQuality checks run in CI on Node 20, 22, and 24.
Mutation testing runs as an advisory pilot in CI (non-blocking), uploads mutation artifacts,
and reports score trend deltas against .github/mutation-baseline.json.
- Semantic PR titles are enforced in CI; commit-message lint is currently informational unless branch-protection policy is changed.
- Releases and
CHANGELOG.mdupdates are automated via Release Please. - Tags follow annotated SemVer with
vprefix (for examplev0.1.3). - npm publish workflow uses GitHub OIDC trusted publishing.
Run the local pre-release gate before publishing:
pnpm run release:verifySee CONTRIBUTING.md.
See SECURITY.md.
See CODE_OF_CONDUCT.md.