wandres.dev
EMPAQUETAR UNA LIBRERÍA · ESM, CJS, tipos

Librería vs aplicación: empaquetar para desconocidos

Una aplicación tiene un consumidor y lo controlas tú: tu bundler, tu target de despliegue, tu versión de Node. Una librería tiene consumidores desconocidos —bundlers ajenos, runtimes dispares, sistemas de módulos mezclados— y no gobiernas ninguno. Esa diferencia invierte casi todas las decisiones de build: la app empaqueta todo y minifica sin piedad, la librería empaqueta casi nada y preserva la estructura para que otro la vuelva a empaquetar. Por qué publicar no es construir un artefacto final, sino diseñar un eslabón intermedio y una interfaz para extraños bajo restricciones que no eliges.

⏱ 15 min

Hasta aquí has empaquetado para ti: tu app, tu bundler, tu despliegue. Todas las piezas del otro lado eran conocidas y tuyas. Publicar una librería rompe esa comodidad de raíz. El código que subes al registro lo consumirá gente que no conoces, con un bundler que no elegiste, sobre una versión de Node que no fijaste, mezclando ESM y CommonJS de maneras que no puedes prever. No controlas ninguna de esas variables y, sin embargo, tu paquete tiene que funcionar en todas. Ese cambio de audiencia —de un consumidor conocido a infinitos desconocidos— invierte casi todas las decisiones de build que dabas por sentadas, y es la idea que gobierna todo este nivel.

🎯 Al terminar esta lección sabrás
  • Entender por qué el consumidor desconocido invierte las decisiones de build de una app.
  • Ver que una librería es un eslabón intermedio, no el artefacto final de la cadena.
  • Distinguir qué controla el autor de una app y qué deja de controlar el de una librería.
  • Adoptar la mentalidad de compatibilidad: funcionar en entornos que nunca probarás.

Un consumidor conocido frente a infinitos desconocidos

Cuando construyes una aplicación sabes exactamente quién ejecuta el resultado: el navegador de tus usuarios, tu servidor de Node, el runtime del edge que tú elegiste. Conoces el target, fijas la versión, decides el bundler y controlas el despliegue de punta a punta. El artefacto que emites es el último de la cadena: nadie lo vuelve a tocar, solo se carga y se ejecuta. Esa certeza es un lujo que impregna cada decisión —puedes minificar hasta lo ilegible, incrustar dependencias y apuntar a una sola sintaxis moderna— porque del otro lado hay un entorno concreto que tú mismo validaste.

Publicar una librería disuelve toda esa certeza. Tu paquete lo instalará gente que no conoces, en proyectos que aún no existen, con un abanico de entornos que no puedes enumerar: un bundler que puede ser Vite, webpack, esbuild o Rolldown; un Node que va del clásico con require al moderno con ESM nativo; un runtime que puede ser el navegador, Bun, Deno o un worker del edge. No eliges ninguna de esas variables y, aun así, tu código debe comportarse bien en todas. Diseñas para un consumidor abstracto del que solo sabes una cosa: que es distinto de ti.

Merece la pena inventariar con precisión lo que dejas de controlar en el instante en que publicas, porque cada punto de esta lista es una decisión que ya no es tuya:

  • El bundler: quien te instala te re-empaqueta con la herramienta que prefiera, con su tree shaking y sus reglas de resolución.
  • El sistema de módulos: puede importarte con ESM o exigirte con require, y a veces las dos cosas en el mismo árbol.
  • El runtime y su versión: navegadores viejos, Node LTS dispares, edge runtimes con APIs recortadas.
  • El resto del árbol de dependencias: tu paquete convive con versiones de terceros que tú nunca elegiste ni probaste.

La inversión: empaquetar todo frente a empaquetar casi nada

La consecuencia práctica es que casi todas las decisiones que tomabas para una app se invierten para una librería. Una app quiere un único artefacto autocontenido: incrusta sus dependencias, minifica sin piedad, aplasta el árbol de módulos en unos pocos chunks y apunta a un target concreto, porque su meta es cargar rápido en un entorno conocido. Una librería quiere lo contrario: dejar sus dependencias fuera, no minificar, preservar la estructura de módulos intacta y no asumir ningún target, porque su artefacto no es el final —es la materia prima de la build de otro.

// rollup.config.js — el perfil de una LIBRERIA, no de una app
export default {
  input: "src/index.ts",
  external: ["react", "react-dom"], // NO empaquetar: las resuelve el consumidor
  output: [
    { file: "dist/index.js", format: "es" }, // ESM: preserva estructura
  ],
  // sin minificar, sin inlinear deps, sin target agresivo
};

El motivo de fondo es que minificar o incrustar en una librería no ahorra nada y estorba mucho. No ahorra, porque el consumidor va a re-minificar y re-empaquetar de todas formas con su propia configuración. Y estorba, porque una salida aplastada y con dependencias incrustadas es más difícil de sacudir por tree shaking, duplica código que ya vive en el árbol del consumidor y borra los nombres que hacen depurable un stack trace. Lo que en una app es una virtud —comprimir al máximo— en una librería es una agresión contra quien te consume.

⚠️
No minifiques tu librería

El impulso de publicar un dist minúsculo y minificado es un error de categoría: confundes tu artefacto con el artefacto final. Quien te instala aplica su propia minificación al bundle completo, así que tu minificado no reduce el tamaño que llega al usuario —solo entorpece el tree shaking, rompe los sourcemaps y ensucia los mensajes de error con nombres de una letra. Publica código legible y con estructura; la compresión es trabajo del consumidor, no tuyo. Lo único que debes emitir minúsculo es la superficie, no los bytes.

flowchart TD
A[tu codigo fuente] --> B{que publicas}
B -->|una app| C[consumidor conocido: tu despliegue]
B -->|una libreria| D[consumidores desconocidos]
C --> E[empaqueta todo, minifica, un target]
D --> F[empaqueta casi nada, preserva estructura]
F --> G[otro bundler la re-empaqueta]
style E fill:#89b4fa,color:#11111b
style F fill:#a6e3a1,color:#11111b
style G fill:#cba6f7,color:#11111b

La librería es un eslabón, no el final de la cadena

La imagen que hay que fijar es la de la cadena. Una app está al final: su build produce el artefacto que el navegador ejecuta, y ahí termina el viaje. Una librería está en medio: su build produce una entrada para la build de otro, que a su vez puede ser otra librería o la app final. Esa posición intermedia lo explica casi todo. Preservas ESM porque el eslabón siguiente necesita estructura estática para sacudir lo que no use. Dejas las dependencias fuera porque el eslabón siguiente ya las tiene y no quieres duplicarlas. Publicas tipos porque el eslabón siguiente compila con TypeScript y espera que tu contrato venga descrito.

De esa misma posición nace un deber que una app no tiene: la compatibilidad hacia adelante y hacia atrás con entornos que jamás probarás. No puedes ejecutar tu paquete en las mil combinaciones de bundler, runtime y versión que existen ahí fuera, así que la corrección deja de comprobarse por ejecución y pasa a garantizarse por diseño: eliges formatos estándar, declaras tus fronteras en el package.json y te apoyas en convenios que todo el ecosistema respeta. Publicar bien es, en gran medida, sustituir la prueba empírica que tenías en una app por contratos que otros leerán y harán cumplir en tu ausencia.

Y como es un eslabón que otros encadenan, cada cambio que publicas viaja aguas abajo sin que puedas retirarlo. Una versión rota de tu app la arreglas y redespliegas; una versión rota de tu librería ya está instalada en cientos de proyectos que la fijaron en su lockfile. Por eso el versionado semántico deja de ser burocracia y se vuelve el idioma con el que le prometes al desconocido qué puede romperse y qué no. Publicar es difícil de deshacer, y esa irreversibilidad es la que obliga a tratar cada decisión de empaquetado como parte de un contrato público.

📝
El semver es el contrato, no un número de adorno

En una app la versión es casi decorativa: nadie fuera de tu equipo la lee. En una librería es el núcleo del acuerdo con el desconocido. Un cambio mayor dice que algo público cambió y hay que actuar; uno menor añade sin romper; un parche corrige sin tocar la superficie. Todo el nivel —qué formatos ofreces, qué dejas en exports, qué tipos publicas— existe para poder responder con honestidad a una sola pregunta: cuando alguien actualice tu paquete, ¿se romperá su build? Diseñar la superficie con cuidado es lo que te permite prometer que no.

Los errores de tratar una librería como una app

El modo más rápido de interiorizar la inversión es catalogar los errores concretos que nacen de arrastrar la mentalidad de app al publicar. Cada uno es un hábito correcto para un artefacto final y dañino para un eslabón intermedio, y todos comparten la misma raíz: hornear en el paquete la suposición de un entorno que no controlas.

// ANTIPATRON: configuracion de APP aplicada a una libreria
export default {
  input: "src/index.ts",
  output: { file: "dist/index.js", format: "iife" }, // formato de app
  plugins: [minify()],           // el consumidor ya minificara
  // sin external: incrusta React y rompe su singleton
};

Ese perfil, impecable para desplegar una app, sabotea una librería en cada línea. El iife cierra la estructura estática que el consumidor necesitaba para podar; el minify destruye sourcemaps y stack traces sin ahorrar un byte al usuario final; y la ausencia de external incrusta dependencias que debían venir del árbol de quien instala. Los fallos más frecuentes forman un catálogo reconocible:

  • Minificar el dist: el consumidor re-minifica; tú solo rompes sourcemaps y ensucias los stack traces.
  • Incrustar dependencias: duplicas código que ya vive en el árbol del consumidor y rompes sus singletons.
  • Publicar el lockfile: un package-lock o un pnpm-lock no viajan al registro; el consumidor resuelve sus versiones, no las tuyas.
  • Fijar versiones exactas: clavar una dependencia a una versión concreta hace tu paquete imposible de deduplicar en árboles ajenos.
  • Asumir un target: transpilar a una sintaxis vieja “por si acaso” infla tu salida para navegadores que tu consumidor quizá ni soporta.

El patrón común es la proyección: das por hecho un entorno —el tuyo— y lo incrustas en el artefacto. Una librería no puede permitírselo, porque su entorno es el conjunto de todos los consumidores posibles. La cura no es memorizar la lista, sino invertir la pregunta antes de cada decisión: no “¿qué necesita mi máquina?”, sino “¿qué asume del consumidor esta elección, y tengo derecho a asumirlo?”.

💡
Pruébate como te probará un desconocido

La mejor defensa contra los errores de identidad es dejar de mirar tu paquete desde dentro y consumirlo desde fuera. Empaqueta en seco, instala el tarball resultante en un proyecto limpio que no comparta tu node_modules, e impórtalo como lo haría un extraño. Ahí afloran de golpe la dependencia que olvidaste declarar, el tipo que no resuelve, el archivo que no viajó. Es leer tu propia API con los ojos de quien no escribió el código: incómodo y revelador a partes iguales.

🎯

App: un target

Consumidor conocido, entorno fijo, artefacto final. Minifica, incrusta y aplasta: nadie lo vuelve a tocar.

🌍

Librería: mil targets

Consumidor desconocido, entorno impredecible, eslabón intermedio. Preserva estructura y deja fuera lo ajeno.

🔗

Eslabón, no final

Tu salida es la entrada de otra build. Emites materia prima re-empaquetable, no un bundle listo para ejecutar.

📜

Contrato público

Sin poder probar cada entorno, la corrección se firma en el package.json y se versiona con semver honesto.

Hay una última pieza que una app no tiene y una librería no puede descuidar: la documentación es parte del paquete. Un desconocido que instala tu librería no puede preguntarte cómo se usa —lee tu README, tus tipos y tus ejemplos, y con eso decide si tu paquete le sirve o busca otro—. En una app el código es solo para máquinas; en una librería, buena parte de la superficie que publicas es para humanos que jamás hablarán contigo, y esa superficie legible pesa tanto en la adopción como la corrección técnica.

📝
El README es la primera línea de tu API

En el registro, tu README es lo primero que ve un consumidor potencial, antes que una sola línea de tu código. Un ejemplo mínimo de instalación e importación, la lista de entry points y un par de casos de uso reales valen más para la adopción que cualquier optimización de bytes. Documentar no es cortesía añadida al final: es parte de la interfaz que publicas. Una librería excelente sin un README que la explique es, para el desconocido que la evalúa en treinta segundos, indistinguible de una que no funciona.

Publicar es diseñar una interfaz para extraños que no puedes probar

La lección que abre este nivel y lo gobierna entero es un cambio de identidad, no una lista de flags. Mientras construyes apps eres el último eslabón: controlas el entorno, validas por ejecución y optimizas para un target que conoces. En el momento en que publicas una librería te conviertes en un eslabón intermedio de una cadena cuyos demás eslabones no ves —y esa sola inversión reordena todo lo que creías saber sobre empaquetar. Ya no optimizas para tu máquina, sino para la ausencia de máquina: para el conjunto abstracto de todos los consumidores posibles, cada uno con su bundler, su runtime, su versión de Node y su forma de mezclar módulos. Por eso dejas de minificar —el consumidor ya lo hará—, dejas de incrustar dependencias —el consumidor ya las tiene—, y dejas de asumir un target —el consumidor ya elige el suyo—. Lo único que sí produces es lo contrario de un artefacto cerrado: una materia prima abierta, estructurada, tipada y descrita, pensada para que un bundler que no conoces la vuelva a procesar sin sorpresas. Y como no puedes ejecutar tu código en los mil entornos que lo recibirán, la corrección migra de la prueba al contrato: los formatos que ofreces, los externals que declaras, los tipos que publicas y el manifiesto que firmas son promesas que otros leerán y harán cumplir en tu ausencia. Interioriza esto y las cuatro lecciones que siguen dejan de ser reglas sueltas y se revelan como caras de una misma decisión: cómo ser un buen eslabón para desconocidos. Quien empaqueta pensando en su propia máquina publica paquetes que rompen en las ajenas; quien empaqueta pensando en el extraño publica paquetes que sobreviven a bundlers que aún no existen.

⚔️ Cambia de identidad: de app a librería
  1. Toma un proyecto tuyo de app y anota tres decisiones de su build —minificación, target, dependencias incrustadas— que serían un error en una librería, y explica por qué.
  2. Enumera cinco entornos distintos en los que podría acabar una librería tuya y razona qué asume cada uno sobre el formato de módulos.
  3. Configura un build de librería con external para sus dependencias y format: es, y compara la salida con la de un build de app equivalente.
  4. Publica en seco el paquete y verifica que la salida es legible, sin minificar y con la estructura de módulos preservada.
  5. Escribe en una frase la promesa de compatibilidad que tu paquete le hace a un consumidor desconocido, y decide qué cambio la rompería.