Volver a proyectos
Personal2026

SMC2SFC2

Conversor de ROMs de SNES (.smc ↔ .sfc) que corre 100% en el navegador. Rescate de un proyecto que llevaba abandonado ~10 años y al que Heroku le bajó la persiana en algún momento de 2022. Lo reescribí en Astro + TypeScript, le metí drag & drop, batch, lector de header de SNES, deduplicación con SHA-256 y PWA. Live en producción.

  • Astro
  • TypeScript
  • PWA
  • Cloudflare Workers

Contexto

Yo era usuario del proyecto original (mwmccarthy/smc2sfc2, repo de hace ~10 años): una webapp ñoña y útil que convertía headers de ROMs de SNES entre .smc (con header de copier de 512 bytes) y .sfc (sin header). Lo usaba cada vez que quería pasar una ROM de un emulador a otro y los formatos no calzaban.

Un día quise entrar y la URL no respondía. Heroku había terminado el tier gratuito y el creador no había migrado el deploy a ningún lado. En ese momento no tenía las herramientas para revivirlo: era una app en Flask + React + Webpack con dependencias bien antiguas, y yo todavía no manejaba lo suficiente del stack como para forkear, modernizar y desplegar por mi cuenta.

Años después volví al problema con más kilometraje encima y con asistencia de IA en el medio. La decisión no fue “lo voy a portar tal cual”: el código original ya no compilaba con node moderno, el frontend estaba escrito antes de los hooks y el backend solo existía para servir un static. Aproveché para repensarlo: mismo propósito, stack mínimo, mejor diseño. Mantengo crédito al autor original en el repo y en la app.

Decisiones técnicas

Stack: Astro 6 + TypeScript + fflate para ZIPs en cliente. Deploy en Cloudflare Workers como sitio estático. Cero backend: solo se sirven estáticos.

100% en el navegador, sin backend. El original tenía Flask para hacer la conversión server-side; las ROMs subían al servidor y volvían convertidas. Acá la conversión ocurre íntegramente en el cliente usando ArrayBuffer y Blob. Razones: (1) ROMs son archivos personales del usuario, no tiene por qué pasar nada al servidor; (2) elimina toda una capa de infraestructura (workers, storage, límites de tamaño, costos por GB); (3) hace el proyecto trivialmente desplegable como sitio estático, sin un servidor que mantener: justo lo que mató al original cuando Heroku cerró el grifo.

Parser de header SNES propio. La conversión real es trivial (agregar o quitar 512 bytes), pero quería que la UI mostrara qué ROM estás convirtiendo: título interno, región, tipo (HiROM/LoROM), modo de video (NTSC/PAL), coprocessor (SA-1, SuperFX, DSP-1…), tamaño declarado, checksum. Eso fue meterme a leer la especificación del header de SNES y portarla a TypeScript. Detección de HiROM vs LoROM por validación de checksum complementario (checksum + complement === 0xFFFF), no por nombre de archivo. Side effect inesperado: aprendí bastante de arquitectura SNES como yapa.

Deduplicación con SHA-256. Si arrastras 30 ROMs y dos son la misma con distinta extensión, no tiene sentido procesarlas dos veces. Calculo el hash con crypto.subtle.digest("SHA-256", buffer) (Web Crypto API nativa, sin dependencias) y omito duplicados. Funciona también para detectar el caso “ya está en el formato destino”.

Batch + ZIP de salida. Drag & drop de múltiples archivos, procesamiento secuencial con progreso, y al final un único ZIP descargable armado con fflate. fflate va sobre JSZip porque pesa una fracción y es streaming-friendly; para batches grandes la diferencia se nota.

Headers de seguridad en Cloudflare. CSP estricta sin unsafe-inline ni unsafe-eval, HSTS, X-Frame-Options: DENY, COOP/CORP para aislamiento y Permissions-Policy cerrando cámara, micrófono y geolocalización, todo declarado en un archivo _headers. Sobreingeniería para una app que convierte ROMs, sí, pero también es práctica para mí: el mismo esquema me sirve de plantilla para los otros sitios estáticos que despliego.

PWA con instalación offline. Como toda la lógica ya vive en el cliente, hacerlo instalable y funcional sin red era casi gratis. Service worker cacheando el shell, manifest con íconos, y queda como app nativa en el dock si quieres.

Outcomes

  • Live en producción en smc2sfc2.etejeda.dev, reemplazando el deploy original caído.
  • Stack reducido a la mitad: Flask + React + Webpack + Heroku → Astro + TypeScript + Cloudflare Workers. Menos piezas que mantener, menos cosas que se pueden romper en 10 años.
  • Privacidad por construcción: las ROMs nunca salen del navegador. No hay servidor que pueda filtrar, perder o ser hackeado.
  • Crédito al autor original mantenido en el repo y en la app: la idea no es mía, el rescate sí.
  • Un proyecto de hace 10 años respira de nuevo: pequeño, pero el tipo de utility tool que cuando no existe te das cuenta de cuánto la usabas.