IPX — image optimizer (library + ipx CLI) powered by sharp and svgo. This branch is v4 (alpha); v3 lives on its own branch.
pnpm install # pnpm only (workspace: root + examples/)
pnpm test # lint + tsc --noEmit + vitest run --coverage (what CI runs)
pnpm vitest <file> # single test file / watch mode
pnpm test:types # tsc --noEmit
pnpm lint # eslint + prettier -c
pnpm lint:fix # automd (regenerates README blocks) + eslint --fix + prettier -w
pnpm build # obuild -> dist/
pnpm ipx serve --dir ./test/assets # run the CLI straight from src (Node runs TS)
pnpm gen:operations # regenerate assets/operations/ + the Modifiers table in README.md| Path | What |
|---|---|
src/index.ts |
Public API surface — everything exported must be intentional |
src/ipx.ts |
createIPX(): id/alias resolution, storage pick, SVG branch, sharp pipeline |
src/server.ts |
parseIPXURL, h3 handler, caching/ETag/304, auto format, security headers |
src/handlers/handlers.ts |
One Handler per modifier (width, blur, …) + aliases at the bottom |
src/handlers/utils.ts |
Arg mappers (VNumber, VEnum, VColor, …), clamping, asModifierError |
src/storage/* |
node-fs, http, unstorage backends implementing IPXStorage |
src/svg.ts |
SVGO sanitizer plugin (XSS hardening) |
test/ |
vitest, mirrors src/; fixtures in test/assets* |
scripts/operations.ts |
Modifier docs data: one entry per modifier, drives the README table + samples |
scripts/gen-operations.ts |
Renders assets/operations/* with the real pipeline, then runs automd |
automd.config.ts |
ipx-operations generator for the README table (generated, do not hand-edit) |
- ESM + TypeScript, relative imports carry the
.tsextension (./utils.ts).verbatimModuleSyntaxis on, so type-only imports needimport type. - Errors are
HTTPErrorfrom h3 with a machine-readablestatusTextcode (IPX_FORBIDDEN_HOST,IPX_INVALID_MODIFIER_ARG, …). Anything derived from user input must surface as a4xx, never an unhandled500. - Runtime deps are only
sharpandsrvx.h3,svgo,image-meta,etag,ufo,@fastify/accept-negotiatorare devDependencies that obuild bundles intodist/. Add new imports as devDependencies unless a runtime dep is truly intended. - No static
node:imports in library code — usegetBuiltinModule()/requireModule()fromsrc/utils.tsso the bundle stays loadable on non-Node runtimes. - Options follow the pattern
userOption ?? getEnv("IPX_*") ?? default. - Prettier defaults; comments explain why (especially the security rationale), not what.
Both the resource id and every modifier are attacker-controlled. Existing protections you must not regress:
ipxHttpStorage: domain allowlist,http(s)only, redirects followed manually one hop at a time and re-validated (SSRF).ipxFSStorage: resolved path must stay inside the configured dir (traversal).maxOutputDimension(default 8192) clampswidth/height/resize/extendso a tiny source cannot force a multi-GB allocation.- SVG is always sanitized (
src/svg.ts), even with optimization disabled. - Server sends
content-security-policy: default-src 'none'andx-content-type-options: nosniff. safeString()inserver.tsescapes parser output — customparseURLresults are never trusted.
- Export a
Handlerfromsrc/handlers/handlers.ts, using the validatingV*arg mappers so bad input is a400before it reaches sharp. Clamp anything that drives allocation size. - Add the key to
IPXModifiersinsrc/ipx.ts— thesatisfies Record<HandlerName, …>check fails otherwise. - Tests in
test/handlers/handlers.test.ts(unit, mocked pipe) and the real-sharp matrix intest/index.test.ts— mocking hides sharp's own validation. - Add an entry to
OPERATIONSinscripts/operations.tsand runpnpm gen:operations— the Modifiers table inREADME.mdis generated from it. The script fails on a modifier that is not listed there, so this step is not optional.
README.md is partly automd-generated (badges, examples/*.ts code blocks, the Modifiers table); edit the source of a block, then pnpm lint:fix.