Every app that accepts an image eventually grows the same three hundred lines: a resize loop, a format matrix, a naming scheme, an S3 client, a retry, and a nagging suspicion that none of it validates the input properly. Lens is that stack written once and tested, with the parts you are likely to disagree with behind interfaces.
What it is not
It is Node-first. There is no client-side processing, no video, no watermarking and no batch-processing UI. It also does not try to be a CDN or an image server it produces files and puts them where you tell it, and something else serves them.
The packages
Separate installs on purpose. A script that only compresses takes the core alone; everything else is opt-in.
@lens-image/coreThe engine0 dependenciesValidation, planning, encoding, key templating, retries, caching, and the upload handler. No React, no SDKs.
@lens-image/adapter-s3StorageCore + AWS SDK as a peerS3, and anything that speaks its API: R2, MinIO, Spaces, B2.
@lens-image/adapter-localStorageDepends on core onlyThe filesystem, with atomic writes and path-traversal refusal.
@lens-image/adapter-cloudinaryStorageDepends on core onlySigned uploads over plain HTTP. The official SDK is not required.
@lens-image/reactThe UI layerReact is a peerHeadless upload hooks with real progress. No components, no CSS.
About that “zero dependencies” claim
Worth being precise, because this is the sort of claim that deserves it.
@lens-image/core has zero entries in dependencies. Installing it adds one package to your lockfile and downloads no native binaries. That is enforced by a check in CI, not just promised in a README.
Actual pixel work needs sharp, which is a native module. Lens treats the codec as a pluggable engine and declares sharp as an optional peer dependency, imported dynamically the first time you encode something.
interface ImageEngine {
readonly name: string;
supports(format: ImageFormat): boolean | Promise<boolean>;
probe(input: Uint8Array): Promise<ImageMetadata>;
transform(input: Uint8Array, op: TransformOp): Promise<EncodedImage>;
}So, concretely:
- You install sharp yourself. Its native binary and platform matrix live in your lockfile, visible, rather than arriving as a transitive surprise.
- You can skip it. Validating, content-hashing and storing originals works with the built-in passthrough engine and no codec at all.
- You can replace it. Three methods. Point them at squoosh, a WASM codec, or an ImageMagick shell-out.
- If you forget it, you get one clear error naming the install command, not mysteriously unprocessed images.