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

@jamx-framework/storage

v1.0.0

Published

JAMX Framework — File storage with local and S3-compatible drivers

Readme

@jamx-framework/storage

Descripción

Sistema de almacenamiento de archivos para JAMX Framework. Proporciona una API unificada para guardar, recuperar, eliminar y gestionar archivos con múltiples backends: almacenamiento local (filesystem) y S3-compatible (AWS S3, MinIO, Cloudflare R2, etc.). Soporta metadatos, URLs públicas, listado de archivos y operaciones de copia/movimiento.

Cómo funciona

El módulo implementa un patrón de driver:

  1. Storage: Clase principal que delega operaciones al driver configurado
  2. Drivers: Implementaciones concretas (LocalDriver, S3Driver) que definen la persistencia
  3. StorageFile: Objeto que representa un archivo con metadatos (key, size, contentType, etc.)

Componentes principales

Storage (src/storage.ts)

Clase principal que proporciona la API unificada:

  • put(key, data, options?): Sube un archivo
  • get(key): Descarga un archivo
  • delete(key): Elimina un archivo
  • exists(key): Verifica existencia
  • stat(key): Obtiene metadatos
  • list(prefix?): Lista archivos
  • url(key): Obtiene URL pública (si aplica)
  • copy(source, dest): Copia archivo
  • move(source, dest): Mueve archivo

Drivers

LocalDriver (src/drivers/local.ts)

Almacena archivos en el filesystem local:

  • Config: root (directorio base), baseUrl (URL pública opcional)
  • Los archivos se guardan en ${root}/${key}
  • url(key) retorna ${baseUrl}/${key} si baseUrl está configurado

S3Driver (src/drivers/s3.ts)

Almacena en S3 o servicios compatibles:

  • Config: bucket, region, accessKey, secretKey, endpoint (opcional)
  • Usa AWS SDK v3 (o similar)
  • url(key) retorna URL pública de S3 (o signed URL si privado)

Tipos

StorageDriver

interface StorageDriver {
  put(key: string, data: Buffer, options?: PutOptions): Promise<void>;
  get(key: string): Promise<Buffer | null>;
  delete(key: string): Promise<boolean>;
  exists(key: string): Promise<boolean>;
  stat(key: string): Promise<StorageFile | null>;
  list(prefix?: string): Promise<StorageFile[]>;
  url(key: string): Promise<string>;
}

StorageFile

interface StorageFile {
  key: string;
  size: number;
  contentType: string;
  lastModified: Date;
  etag?: string;
}

PutOptions

interface PutOptions {
  contentType?: string;
  metadata?: Record<string, string>;
  acl?: 'private' | 'public-read' | 'public-read-write';
}

StorageConfig

interface StorageConfig {
  driver: 'local' | 's3';
  local?: LocalDriverConfig;
  s3?: S3Config;
}

Uso básico

Configuración local

import { Storage, LocalDriver } from '@jamx-framework/storage';

const storage = new Storage({
  driver: 'local',
  local: {
    root: './uploads',      // directorio donde guardar archivos
    baseUrl: 'http://localhost:3000/uploads', // URL pública (opcional)
  },
});

Configuración S3

import { Storage, S3Driver } from '@jamx-framework/storage';

const storage = new Storage({
  driver: 's3',
  s3: {
    bucket: 'my-bucket',
    region: 'us-east-1',
    accessKey: process.env.AWS_ACCESS_KEY_ID!,
    secretKey: process.env.AWS_SECRET_ACCESS_KEY!,
    // endpoint: 'https://nyc3.digitaloceanspaces.com', // opcional para S3-compatible
  },
});

Subir archivos

import { Storage } from '@jamx-framework/storage';

const storage = new Storage(config);

// Subir archivo
await storage.put('avatars/user123.png', buffer, {
  contentType: 'image/png',
});

// Con metadata
await storage.put('documents/report.pdf', buffer, {
  contentType: 'application/pdf',
  metadata: {
    uploadedBy: 'user123',
    description: 'Monthly report',
  },
});

Descargar archivos

// Descargar como Buffer
const data = await storage.get('avatars/user123.png');
if (data) {
  // usar data (Buffer)
  console.log('File size:', data.length);
}

// Verificar existencia
const exists = await storage.exists('avatars/user123.png');
if (exists) {
  // ...
}

Obtener metadatos

const file = await storage.stat('avatars/user123.png');
if (file) {
  console.log('Key:', file.key);
  console.log('Size:', file.size);
  console.log('Content-Type:', file.contentType);
  console.log('Last Modified:', file.lastModified);
  console.log('ETag:', file.etag);
}

Listar archivos

// Listar todos
const files = await storage.list();
for (const file of files) {
  console.log(file.key, file.size);
}

// Listar con prefijo
const userFiles = await storage.list('avatars/user123/');
// Retorna archivos que empiezan con 'avatars/user123/'

URLs públicas

// Local driver
const url = await storage.url('avatars/user123.png');
// → 'http://localhost:3000/uploads/avatars/user123.png'

// S3 driver (depende de configuración de bucket)
const url = await storage.url('avatars/user123.png');
// → 'https://my-bucket.s3.amazonaws.com/avatars/user123.png'

Eliminar archivos

const deleted = await storage.delete('avatars/user123.png');
if (deleted) {
  console.log('File deleted');
}

Copiar y mover

// Copiar
await storage.copy('avatars/old.png', 'avatars/new.png');

// Mover (copy + delete)
await storage.move('avatars/temp.png', 'avatars/permanent.png');

API Reference

Storage

Constructor

new Storage(config: StorageConfig)

Crea una instancia de storage con el driver especificado.

put()

async put(key: string, data: Buffer, options?: PutOptions): Promise<void>

Sube un archivo.

  • key: Ruta del archivo en el storage (ej: 'users/123/avatar.png')
  • data: Contenido del archivo como Buffer
  • options.contentType: MIME type (ej: 'image/png')
  • options.metadata: Metadatos personalizados (S3 only)
  • options.acl: Permisos de acceso (S3 only)

Ejemplo:

await storage.put('docs/report.pdf', pdfBuffer, {
  contentType: 'application/pdf',
  metadata: { author: 'john' },
});

get()

async get(key: string): Promise<Buffer | null>

Descarga un archivo. Retorna null si no existe.

Ejemplo:

const data = await storage.get('image.jpg');
if (data) {
  // usar data
}

delete()

async delete(key: string): Promise<boolean>

Elimina un archivo. Retorna true si existía y fue eliminado, false si no existía.

exists()

async exists(key: string): Promise<boolean>

Verifica si un archivo existe.

stat()

async stat(key: string): Promise<StorageFile | null>

Obtiene metadatos del archivo.

StorageFile:

interface StorageFile {
  key: string;
  size: number;          // bytes
  contentType: string;   // MIME type
  lastModified: Date;    // fecha de modificación
  etag?: string;         // ETag (S3)
}

list()

async list(prefix?: string): Promise<StorageFile[]>

Lista archivos. Si prefix se especifica, solo lista archivos que empiezan con ese prefijo.

Ejemplo:

// Todos los archivos
const all = await storage.list();

// Solo avatares
const avatars = await storage.list('avatars/');

url()

async url(key: string): Promise<string>

Obtiene la URL pública del archivo. Depende del driver:

  • Local: ${baseUrl}/${key} (requiere baseUrl en config)
  • S3: URL pública de S3 (bucket debe ser público o usar signed URLs)

copy()

async copy(source: string, dest: string): Promise<void>

Copia un archivo dentro del mismo storage.

move()

async move(source: string, dest: string): Promise<void>

Mueve un archivo (copia + elimina origen).

LocalDriver

Configuración

interface LocalDriverConfig {
  root: string;           // directorio base (absoluto o relativo)
  baseUrl?: string;       // URL base para acceder a archivos (opcional)
}

Ejemplo:

{
  driver: 'local',
  local: {
    root: '/var/uploads',
    baseUrl: 'https://cdn.example.com/uploads',
  },
}

Comportamiento:

  • Los archivos se guardan en ${root}/${key}
  • root se crea automáticamente si no existe
  • url(key) retorna ${baseUrl}/${key} si baseUrl está configurado
  • Si baseUrl no está configurado, url() lanza error

S3Driver

Configuración

interface S3Config {
  bucket: string;         // nombre del bucket
  region: string;         // región de AWS
  accessKey: string;      // AWS_ACCESS_KEY_ID
  secretKey: string;      // AWS_SECRET_ACCESS_KEY
  endpoint?: string;      // endpoint personalizado (para S3-compatible)
  forcePathStyle?: boolean; // usar path-style URLs (para MinIO, etc.)
}

Ejemplo AWS S3:

{
  driver: 's3',
  s3: {
    bucket: 'my-app-uploads',
    region: 'us-east-1',
    accessKey: process.env.AWS_ACCESS_KEY_ID!,
    secretKey: process.env.AWS_SECRET_ACCESS_KEY!,
  },
}

Ejemplo MinIO:

{
  driver: 's3',
  s3: {
    bucket: 'uploads',
    region: 'us-east-1',
    accessKey: 'minioadmin',
    secretKey: 'minioadmin',
    endpoint: 'http://localhost:9000',
    forcePathStyle: true,
  },
}

Ejemplo Cloudflare R2:

{
  driver: 's3',
  s3: {
    bucket: 'my-bucket',
    region: 'auto',
    accessKey: process.env.CF_ACCESS_KEY_ID!,
    secretKey: process.env.CF_SECRET_ACCESS_KEY!,
    endpoint: 'https://<account-id>.r2.cloudflarestorage.com',
  },
}

Comportamiento:

  • Usa AWS SDK S3 client
  • put() sube con PutObjectCommand
  • get() descarga con GetObjectCommand
  • url(key) retorna URL pública si el bucket es público, o signed URL si es privado
  • Soporta metadata personalizada

Ejemplos completos

Upload de avatar de usuario

import { Storage } from '@jamx-framework/storage';

const storage = new Storage({
  driver: 's3',
  s3: {
    bucket: 'my-app-avatars',
    region: 'us-east-1',
    accessKey: process.env.AWS_ACCESS_KEY_ID!,
    secretKey: process.env.AWS_SECRET_ACCESS_KEY!,
  },
});

async function uploadAvatar(userId: string, file: Express.Multer.File) {
  const key = `avatars/${userId}/${Date.now()}-${file.originalname}`;

  await storage.put(key, file.buffer, {
    contentType: file.mimetype,
    metadata: {
      userId,
      originalName: file.originalname,
    },
  });

  const url = await storage.url(key);
  return url;
}

Servir archivos estáticos

import { Storage, LocalDriver } from '@jamx-framework/storage';
import { serve } from '@jamx-framework/server';

const storage = new Storage({
  driver: 'local',
  local: {
    root: './public/uploads',
    baseUrl: '/uploads',
  },
});

// En el servidor
server.use('/uploads', async (req, res) => {
  const key = req.path.slice('/uploads'.length); // remover prefijo
  const file = await storage.get(key);

  if (!file) {
    res.notFound();
    return;
  }

  const meta = await storage.stat(key);
  res.header('Content-Type', meta?.contentType ?? 'application/octet-stream');
  res.send(file);
});

Limpieza de archivos antiguos

import { Storage } from '@jamx-framework/storage';

const storage = new Storage(config);

async function cleanupOldFiles(days: number) {
  const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
  const files = await storage.list();

  for (const file of files) {
    if (file.lastModified.getTime() < cutoff) {
      await storage.delete(file.key);
      console.log(`Deleted ${file.key}`);
    }
  }
}

// Ejecutar diariamente
setInterval(() => cleanupOldFiles(30), 24 * 60 * 60 * 1000);

Migración de local a S3

import { Storage } from '@jamx-framework/storage';

const local = new Storage({
  driver: 'local',
  local: { root: './old-uploads' },
});

const s3 = new Storage({
  driver: 's3',
  s3: { /* config */ },
});

async function migrate() {
  const files = await local.list();

  for (const file of files) {
    const data = await local.get(file.key);
    if (data) {
      await s3.put(file.key, data, {
        contentType: file.contentType,
      });
      console.log(`Migrated ${file.key}`);
    }
  }
}

Generar signed URLs (S3 privado)

import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

// Si necesitas signed URLs, extiende S3Driver:
class SecureS3Driver extends S3Driver {
  async signedUrl(key: string, expiresIn: number = 3600): Promise<string> {
    const command = new GetObjectCommand({
      Bucket: this.config.bucket,
      Key: key,
    });

    return await getSignedUrl(this.client, command, { expiresIn });
  }
}

// Uso
const storage = new Storage({ driver: 's3', s3: { /* ... */ } });
const url = await (storage.driver as SecureS3Driver).signedUrl('private/file.pdf');

Consideraciones de rendimiento

LocalDriver

  • I/O bloqueante: Node.js es single-threaded; muchos accesos concurrentes pueden bloquear event loop
  • Escalabilidad: No escala en clúster (cada instancia tiene su propio filesystem)
  • Backup: Responsabilidad del usuario (backup del directorio root)
  • CDN: Para servir archivos eficientemente, usar baseUrl apuntando a CDN

S3Driver

  • Latencia: Cada operación es una llamada de red (100ms+ típicamente)
  • Throughput: S3 soporta miles de requests/segundo por bucket
  • Costos: Cada PUT/GET tiene costo; considerar CloudFront para caching
  • Consistencia: S3 tiene read-after-write consistency para PUTs nuevos

Concurrencia

  • El storage no limita concurrencia
  • Para uploads masivos, considera limitar con semáforos
  • Los drivers son thread-safe (no comparten estado mutable)

Caching

  • El storage no incluye cache interno
  • Para archivos frecuentemente accedidos, usar CDN o cache en memoria:
import { Cache } from '@jamx-framework/cache';

const cache = new Cache({ driver: 'redis' });

async function getWithCache(key: string) {
  const cached = await cache.get(key);
  if (cached) return Buffer.from(cached);

  const data = await storage.get(key);
  if (data) {
    await cache.set(key, data, 3600); // cache 1 hora
  }
  return data;
}

Testing

Tests con LocalDriver

import { Storage } from '@jamx-framework/storage';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { rmSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';

describe('LocalStorage', () => {
  const testDir = './test-uploads';

  beforeEach(() => {
    mkdirSync(testDir, { recursive: true });
  });

  afterEach(() => {
    rmSync(testDir, { recursive: true, force: true });
  });

  it('should upload and download files', async () => {
    const storage = new Storage({
      driver: 'local',
      local: { root: testDir, baseUrl: 'http://localhost/uploads' },
    });

    await storage.put('test.txt', Buffer.from('Hello World'));
    const data = await storage.get('test.txt');
    expect(data?.toString()).toBe('Hello World');
  });

  it('should list files', async () => {
    const storage = new Storage({
      driver: 'local',
      local: { root: testDir },
    });

    await storage.put('a.txt', Buffer.from('A'));
    await storage.put('b.txt', Buffer.from('B'));

    const files = await storage.list();
    expect(files).toHaveLength(2);
  });
});

Tests con S3Driver (mock)

import { Storage } from '@jamx-framework/storage';
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { mockClient } from 'aws-sdk-client-mock';

const s3Mock = mockClient(S3Client);

describe('S3Storage', () => {
  beforeEach(() => {
    s3Mock.reset();
  });

  it('should upload file', async () => {
    s3Mock.on(PutObjectCommand).resolves({});

    const storage = new Storage({
      driver: 's3',
      s3: {
        bucket: 'test-bucket',
        region: 'us-east-1',
        accessKey: 'test',
        secretKey: 'test',
      },
    });

    await storage.put('key.txt', Buffer.from('content'));

    expect(s3Mock.calls()).toHaveLength(1);
  });
});

Seguridad

Validación de keys

  • Los drivers deben sanitizar key para evitar path traversal
  • LocalDriver previene ../ en paths
  • S3Driver usa el key directamente (S3 ya previene problemas)

Permisos

  • LocalDriver: Permisos del filesystem del OS
  • S3Driver: Permisos de IAM (accessKey/secretKey)
  • Configurar acl apropiadamente (private por defecto)

URLs firmadas (S3)

Para buckets privados, generar signed URLs:

// No implementado por defecto, pero se puede extender
const url = await storage.driver.url(key); // puede fallar si bucket es privado

Encriptación

  • El storage no encripta datos en tránsito (usar HTTPS para S3)
  • Para encriptación en reposo, usar S3 SSE o encriptar antes de put()

Limitaciones

LocalDriver

  • No soporta directorios virtuales (todos los keys son "archivos")
  • No hay límite de tamaño de archivo (depende del filesystem)
  • No soporta metadatos extensivos (solo contentType en PutOptions)

S3Driver

  • Depende de AWS SDK (pesado, ~1MB)
  • No soporta todos los features de S3 (multipart upload, versioning, etc.)
  • url() puede no funcionar para buckets privados

General

  • No soporta streaming (todo es Buffer en memoria)
  • No hay operaciones de bulk (delete masivo, copy masivo)
  • No hay eventos (watcher de cambios)
  • No soporta ranges (partial get)

Buenas prácticas

1. Usar keys estructuradas

// ✅ Bien: estructura jerárquica
const key = `users/${userId}/avatars/${Date.now()}.png`;

// ❌ No: keys planos
const key = 'avatar123.png';

2. Validar contenido

import { validate } from '@jamx-framework/validator';

const schema = z.object({
  contentType: z.string().regex(/^image\//),
  size: z.number().max(5 * 1024 * 1024), // max 5MB
});

async function safeUpload(key: string, data: Buffer) {
  // Validar antes de subir
  const fileInfo = {
    contentType: detectMimeType(data),
    size: data.length,
  };

  const result = schema.parse(fileInfo);
  await storage.put(key, data, { contentType: result.contentType });
}

3. Manejar errores

try {
  await storage.put(key, data);
} catch (err) {
  if (err.code === 'ENOENT') {
    // directorio no existe (LocalDriver)
    await fs.mkdirSync(path.dirname(localPath), { recursive: true });
    await storage.put(key, data);
  } else if (err.code === 'NoSuchBucket') {
    // bucket no existe (S3Driver)
    await createBucket();
    await storage.put(key, data);
  } else {
    throw err;
  }
}

4. Limpiar archivos huérfanos

// Después de eliminar un usuario, eliminar sus archivos
async function deleteUserFiles(userId: string) {
  const prefix = `users/${userId}/`;
  const files = await storage.list(prefix);

  for (const file of files) {
    await storage.delete(file.key);
  }
}

5. Usar TTL para archivos temporales

import { Scheduler } from '@jamx-framework/scheduler';

// Limpiar archivos temporales cada 24h
scheduler.add({
  name: 'cleanup-temp',
  cron: '0 3 * * *', // 3 AM diario
  handler: async () => {
    const files = await storage.list('temp/');
    const cutoff = Date.now() - 24 * 60 * 60 * 1000;

    for (const file of files) {
      if (file.lastModified.getTime() < cutoff) {
        await storage.delete(file.key);
      }
    }
  },
});

Integración con otros paquetes

Con @jamx-framework/server

import { JamxServer, staticFiles } from '@jamx-framework/server';
import { Storage } from '@jamx-framework/storage';

const storage = new Storage(config);

server.use('/uploads', async (req, res) => {
  const key = req.path.slice('/uploads'.length);
  const file = await storage.get(key);

  if (!file) {
    res.notFound();
    return;
  }

  const meta = await storage.stat(key);
  res.header('Content-Type', meta?.contentType ?? 'application/octet-stream');
  res.send(file);
});

Con @jamx-framework/auth

import { authMiddleware } from '@jamx-framework/auth';
import { Storage } from '@jamx-framework/storage';

const storage = new Storage(config);

server.use(authMiddleware());

server.use('/profile/avatar', async (req, res) => {
  const user = req.locals.user;
  const key = `avatars/${user.id}.png`;

  const file = await storage.get(key);
  if (!file) {
    res.notFound();
    return;
  }

  res.header('Content-Type', 'image/png');
  res.send(file);
});

Con @jamx-framework/validator

import { validate } from '@jamx-framework/validator';
import { Storage } from '@jamx-framework/storage';

const uploadSchema = z.object({
  key: z.string().regex(/^[a-zA-Z0-9_\-/\.]+$/),
  contentType: z.string(),
  size: z.number().max(10 * 1024 * 1024), // 10MB max
});

async function upload(req: JamxRequest, res: JamxResponse) {
  const result = validate(req.body, uploadSchema);
  if (!result.valid) {
    res.json({ errors: result.errors }, 400);
    return;
  }

  const { key, contentType, size } = result.value;
  const buffer = req.body as Buffer; // asumiendo bodyParser con raw

  if (buffer.length !== size) {
    res.json({ error: 'Size mismatch' }, 400);
    return;
  }

  await storage.put(key, buffer, { contentType });
  res.json({ url: await storage.url(key) });
}

Preguntas frecuentes

¿Cómo manejar uploads grandes?

Para archivos > 10MB, usar streaming o multipart upload (S3):

// S3 multipart upload (no implementado por defecto)
// Considerar usar AWS SDK directamente para casos complejos

¿Cómo servir archivos privados?

Para S3 privado, generar signed URLs:

// Extender S3Driver
async signedUrl(key: string, expiresIn: number = 3600): Promise<string> {
  const command = new GetObjectCommand({
    Bucket: this.config.bucket,
    Key: key,
  });
  return await getSignedUrl(this.client, command, { expiresIn });
}

¿Cómo cambiar de driver?

La API es la misma para todos los drivers:

// Cambiar de local a S3
const storage = new Storage({
  driver: 's3',
  s3: { /* config */ },
});

// El código de la aplicación no cambia
await storage.put('file.txt', buffer);

¿Qué pasa si el directorio local no existe?

LocalDriver crea el directorio automáticamente en el primer put().

¿Cómo manejar conflictos de nombres?

El storage no previene overwrites. Si subes un archivo con la misma key, se sobrescribe.

// Evitar overwrite
if (await storage.exists(key)) {
  throw new Error('File already exists');
}
await storage.put(key, data);

¿Cómo obtener tamaño total de storage?

async function getTotalSize(): Promise<number> {
  let total = 0;
  const files = await storage.list();
  for (const file of files) {
    total += file.size;
  }
  return total;
}

Referencia rápida

Crear storage

const storage = new Storage({
  driver: 'local', // o 's3'
  local: { root: './uploads' },
  // o
  s3: { bucket: '...', region: '...', accessKey: '...', secretKey: '...' },
});

Operaciones CRUD

await storage.put(key, buffer, options);
const data = await storage.get(key);
await storage.delete(key);
const exists = await storage.exists(key);
const file = await storage.stat(key);

Listar

const files = await storage.list(prefix);

Utilidades

const url = await storage.url(key);
await storage.copy(source, dest);
await storage.move(source, dest);

Archivos importantes

  • src/storage.ts - Clase Storage principal
  • src/drivers/local.ts - Driver de filesystem local
  • src/drivers/s3.ts - Driver de S3
  • src/drivers/types.ts - Tipos compartidos
  • tests/unit/storage.test.ts - Tests de Storage
  • tests/unit/drivers/local.test.ts - Tests de LocalDriver

Dependencias

  • @types/node - Tipos de Node.js
  • vitest - Testing
  • rimraf - Limpieza
  • (S3Driver) @aws-sdk/client-s3 - AWS SDK (no listada en package.json, debe instalarse)

Scripts del paquete

  • pnpm build - Compila TypeScript
  • pnpm dev - Watch mode
  • pnpm test - Tests unitarios
  • pnpm test:watch - Tests en watch
  • pnpm type-check - Verificar tipos
  • pnpm clean - Limpiar build

Notas sobre S3Driver

El S3Driver requiere @aws-sdk/client-s3 como dependencia adicional. Asegúrate de instalarla:

pnpm add -w @aws-sdk/client-s3

O si el paquete no la incluye, agregarla a devDependencies del paquete.