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.
Features
- 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
Install
Runtime requirement: Node.js >= 20.
Install globally:
npm install -g @imageforge/cli
Run without global install:
npx @imageforge/cli ./public/images --dry-run
Quick Start
imageforge ./public/images --dry-run
Review the preview, then apply the same command without --dry-run:
imageforge ./public/images
By 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,avif
Write outputs to a dedicated directory:
imageforge ./public/images --out-dir ./public/generated
Generate 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).
CLI Usage
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 outputs + cache + manifest for CI (exit 1 when stale) |
--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 |
Runtime Behavior
- Normal runs exit with code
1if any file fails processing. --checkexits1when a source needs processing or the cache/manifest is missing, invalid, or stale; otherwise it exits0.- 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 a generation command matching the effective options. The CLI detects npm, pnpm, Yarn, or Bun invocations. It uses the exact project-installed version when present; otherwise the hint names the exact scoped package and version, never the unrelated unscopedimageforgepackage. TreatrerunCommandas a shell-dependent hint and keep the canonical invocation in a package script. If cache provenance is missing or malformed, inspect existing derivatives before removing conflicts or adding--force-overwriteintentionally. --dry-runpreviews which images would be processed but performs no output, manifest, cache, directory, or lock 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.
Responsive Guardrail
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.
Configuration
Scaffold a starter config:
imageforge init
Overwrite an existing scaffold:
imageforge init --force
Config 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
}
JSON Output
Use --json to emit a structured report:
imageforge ./public/images --json
The 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
- Non-fatal warnings, including cache-owned derivatives made obsolete by a formats, widths, or output-contract change
- Effective-option generation hint for
--checkfailures, with protected recovery when cache provenance is unavailable
Manifest
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).
Next.js Integration Example
import manifest from "./imageforge.json";
import type { ImageForgeEntry } from "@imageforge/cli";
const images = manifest.images as Record<string, ImageForgeEntry>;
export function getImageData(src: string): ImageForgeEntry {
const image = images[src];
if (!image) throw new Error(`ImageForge manifest entry not found: ${src}`);
return image;
}
function joinPublicPath(base: string, outputPath: string): string {
return `${base.replace(/\/?$/, "/")}${outputPath}`;
}
export function getImageUrl(src: string, format: "webp" | "avif", publicBase = "/images/"): string {
const output = getImageData(src).outputs[format];
if (!output) throw new Error(`ImageForge ${format} output not found: ${src}`);
return joinPublicPath(publicBase, output.path);
}
Then serve a generated derivative. When ./public/images is the input directory, manifest output
paths are relative to /images/:
import Image from "next/image";
const hero = getImageData("hero.jpg");
<Image
src={getImageUrl("hero.jpg", "webp")}
width={hero.width}
height={hero.height}
alt="Product screenshot"
placeholder="blur"
blurDataURL={hero.blurDataURL}
unoptimized
/>;
unoptimized is intentional: it serves ImageForge's pre-generated file instead of sending it
through the Next.js runtime image optimizer again. For art direction or multiple formats, render a
native <picture> using manifest variants and an accurate sizes attribute.
Optional srcset helper for responsive variants:
export function getSrcSet(src: string, format: "webp" | "avif", publicBase = "/images/") {
const variants = getImageData(src).variants?.[format];
return variants
?.map((variant) => `${joinPublicPath(publicBase, variant.path)} ${variant.width}w`)
.join(", ");
}
Programmatic API
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.
Source Input Scope
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.
CI Mode
Install ImageForge as a pinned project dependency so CI uses the version recorded in your lockfile:
pnpm add --save-dev --save-exact @imageforge/cli
Add repeatable scripts:
{
"scripts": {
"images:build": "imageforge ./public/images --formats webp,avif --widths 320,640,960,1280",
"images:check": "imageforge ./public/images --formats webp,avif --widths 320,640,960,1280 --check"
}
}
Commit the manifest, cache, and generated derivatives, then verify them after a frozen install:
pnpm install --frozen-lockfile
pnpm run images:check
--check is read-only and fails if an input needs processing or the checked-in cache/manifest is
missing, invalid, or stale. Its failure output includes a build command with the effective options.
Review shell quoting before running it, especially for include/exclude patterns containing spaces or
shell metacharacters.
Cache v2 verifies derivative/blur-metadata SHA-256 digests and generator identity; run one unfiltered
regeneration after a v1/legacy cache or ImageForge, Sharp, or libvips change so every entry gets
current integrity metadata.
See the focused guides for CI and generated assets, manifest semantics, and Next.js delivery, plus installation and generated-state troubleshooting. These absolute links remain useful from the npm package page. The focused guides are intentionally not bundled in the package tarball.
Benchmarking
CI-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.md
Development
pnpm install
pnpm build
pnpm run typecheck
pnpm run lint
pnpm run format:check
pnpm test
pnpm run test:mutation:pilot
pnpm run check
Quality 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.
Release Workflow
- 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:verify
Contributing
See CONTRIBUTING.md.
Security
See SECURITY.md.
Code of Conduct
See CODE_OF_CONDUCT.md.