npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@chrono-os/image-editor-backend

v0.2.0

Published

Upload + Sharp optimizer + Fastify plugin para o editor de imagens com crop visual

Readme

@chrono-os/image-editor-backend

npm License

Pipeline Sharp (WebP qualidade 82, resize max 1920px, max input 15MB) + plugin Fastify pra upload/list/delete em ${prefix}. Pareado com @chrono-os/image-editor-react — o adapter do frontend bate nas rotas que esse plugin registra.

Install

yarn add @chrono-os/image-editor-backend
yarn add fastify @fastify/multipart sharp

Peer dependencies: fastify@^4 || ^5 sharp@^0.33.

@fastify/multipart é dependência do consumer (não peer formal pra não amarrar versão major), mas obrigatório pra uploadPlugin funcionar.

Exemplo: Fastify completo

// app.ts
import Fastify from 'fastify'
import multipart from '@fastify/multipart'
import { uploadPlugin } from '@chrono-os/image-editor-backend'

const app = Fastify({ logger: true })

// 1) Multipart parser PRIMEIRO (registrado no escopo root).
await app.register(multipart, {
  limits: { fileSize: 15 * 1024 * 1024, files: 1 },
})

// 2) Depois o uploadPlugin com prefix.
await app.register(uploadPlugin, {
  prefix: '/admin/uploads',
  uploadsDir: process.env.UPLOADS_DIR ?? '/var/uploads',
  publicUrl: process.env.UPLOADS_PUBLIC_URL,  // ex 'https://cdn.example.com/uploads'
})

await app.listen({ port: 3000, host: '0.0.0.0' })

Rotas registradas (relativas a prefix):

  • GET /admin/uploads{ items: UploadListItem[] }
  • POST /admin/uploads201 UploadedImage (multipart com field file)
  • DELETE /admin/uploads/:filename{ ok: true } ou 404

Gotcha: @fastify/multipart DEVE ser registrado ANTES do uploadPlugin. Sem isso, request.file() não existe e o plugin responde 500 MULTIPART_NOT_REGISTERED.

Exemplo: com authHook

Integração típica com cookie+JWT do consumer:

import { uploadPlugin } from '@chrono-os/image-editor-backend'

await app.register(uploadPlugin, {
  prefix: '/admin/uploads',
  uploadsDir: env.UPLOADS_DIR,
  publicUrl: env.UPLOADS_PUBLIC_URL,
  authHook: async (request, reply) => {
    // request.adminUser foi setado num onRequest hook anterior
    // (decorando request com jose.verifyJwt do cookie).
    if (!request.adminUser) {
      return false   // plugin responde 401 automaticamente
    }
    return true
  },
})

O hook recebe (request, reply) e retorna boolean (ou Promise<boolean>). Retornar false faz o plugin enviar 401 { error: 'UNAUTHORIZED' } automaticamente — não precisa chamar reply.send() manualmente.

Exemplo: createImageOptimizer standalone

Pra quem quer só o pipeline Sharp (sem Fastify — ex CLI, NestJS, serverless lambda):

import { createImageOptimizer } from '@chrono-os/image-editor-backend'
import { readFile } from 'node:fs/promises'

const optimizer = createImageOptimizer({
  uploadsDir: '/var/uploads',
  publicUrl: 'https://cdn.example.com/uploads',
  maxInputBytes: 20 * 1024 * 1024,   // override pra 20MB
  maxOutputWidth: 2400,
  webpQuality: 88,
})

await optimizer.ensureUploadsDir()

const buffer = await readFile('/tmp/source.jpg')
const saved = await optimizer.saveAndOptimizeImage({
  data: buffer,
  mimeType: 'image/jpeg',
  originalFilename: 'foto-naírio.jpg',  // será saneado pra 'foto-nairio.webp'
})

console.log(saved)
// { filename: 'foto-nairio.webp', url: 'https://cdn.example.com/uploads/foto-nairio.webp',
//   width: 1600, height: 2133, sizeBytes: 184_293, mimeType: 'image/webp' }

const items = await optimizer.listUploads()
await optimizer.deleteUpload('foto-nairio.webp')

Config (ImageOptimizerConfig)

| Campo | Tipo | Default | Descrição | |---|---|---|---| | uploadsDir | string | — (obrigatório) | Path absoluto onde os WebPs são gravados. | | publicUrl | string | — | URL base servindo uploadsDir. Quando ausente, retorna /uploads/<filename> relativo. | | maxInputBytes | number | 15 * 1024 * 1024 | Tamanho máximo do upload em bytes. | | maxOutputWidth | number | 1920 | Largura máxima do WebP gerado (px). Aspect preservado via Sharp fit:'inside'. | | webpQuality | number | 82 | Qualidade WebP (0-100). | | allowedMime | Set<string> | {'image/jpeg','image/png','image/webp','image/avif','image/gif'} | MIME types aceitos na entrada. |

Plugin options (UploadPluginOptions)

Estende ImageOptimizerConfig com:

| Campo | Tipo | Default | Descrição | |---|---|---|---| | authHook | (req, reply) => boolean \| Promise<boolean> | — | Autorização por rota. false → 401 automático. | | maxFiles | number | 1 | Limite de arquivos por request multipart. |

Pra montar as rotas em path custom, use prefix do próprio Fastify register:

await app.register(uploadPlugin, { prefix: '/api/uploads', uploadsDir, publicUrl })

Comportamento de filename

saveAndOptimizeImage resolve o nome final assim:

  1. Pega o originalFilename (ou usa fallback random lp-<ts36>-<hex>.webp se ausente).
  2. Sanitiza: lowercase, NFD strip acentos, substitui chars não-alfanuméricos por -, colapsa - repetidos, trim de hífens, slice 60 chars.
  3. Força extensão .webp (Sharp sempre transcodifica).
  4. Se já existe arquivo com mesmo nome em uploadsDir, tenta -2, -3, ..., -100. Acima de 100 colisões cai no fallback random.

Exemplos:

  • "Foto do Naírio.JPG""foto-do-nairio.webp" (ou "foto-do-nairio-2.webp" se houver colisão)
  • "_____ .png" (stem vazio após sanitização) → fallback random lp-XXX.webp
  • nome >60 chars → truncado pra 60

License

MIT