Qué formatos ofrecer: ESM-first, CJS con criterio y el ocaso de UMD
Un mismo grafo puede emitirse en varios idiomas de módulos, pero en 2026 la respuesta por defecto para una librería es una: ESM primero. Es el único formato que preserva la estructura estática que el consumidor necesita para hacer tree shaking, y el que todo bundler moderno entiende de forma nativa. CommonJS sigue teniendo sentido, pero por alcance y ya no por defecto: solo si tu público real incluye un Node clásico con require. Y UMD, que durante una década fue el formato universal del navegador, casi murió cuando los import maps y los CDN de ESM le quitaron su única razón de ser. Cómo decidir qué formatos publicar sin cargar peso muerto.
El grafo de tu librería es uno solo, pero puedes emitirlo en varios idiomas de módulos, y cada idioma habla con un consumidor distinto. La pregunta del autor no es técnica sino estratégica: ¿a qué público sirvo y con cuánto peso muerto? En 2026 la respuesta por defecto se ha simplificado hasta casi un solo formato. ESM primero, porque es el único que preserva la estructura estática que el consumidor necesita para podar, y el que cualquier bundler y cualquier runtime moderno entiende de serie. CommonJS sobrevive, pero como concesión medida al Node clásico, no como obligación. Y UMD, el que durante una década fue el formato universal del navegador, casi ha desaparecido porque el ecosistema le quitó su razón de existir.
- Justificar por qué ESM es el formato por defecto de una librería en 2026.
- Decidir cuándo añadir CommonJS por alcance y cuándo es peso muerto.
- Entender por qué UMD casi murió y qué lo sustituyó en el navegador.
- Diseñar la matriz de formatos según el público real, no según la costumbre.
ESM primero: el formato que preserva el contrato
La razón por la que ESM es el destino por defecto no es la moda, sino una propiedad estructural: import y export son declaraciones estáticas, analizables sin ejecutar el código. Cuando publicas ESM, el bundler del consumidor puede leer tu grafo, saber qué exports usa de verdad y podar el resto. Preservas aguas abajo la única cosa que hace ligero a un paquete: la capacidad de que quien te instala se lleve solo lo que toca. Ningún otro formato ofrece esto, porque ningún otro conserva la estructura estática intacta.
ESM es además el terreno común de 2026. Node lo carga de forma nativa, todo bundler moderno lo prefiere, y los runtimes nuevos —Bun, Deno, los workers del edge— nacieron hablándolo. Publicar ESM no es apostar por un nicho: es dirigirte al centro del ecosistema y dejar que las capas de compatibilidad se ocupen de los bordes. Un paquete solo-ESM en 2026 es una decisión perfectamente razonable, y cada vez más la que toman las librerías nuevas que no arrastran historia.
{
"name": "mi-lib",
"type": "module",
"exports": {
".": "./dist/index.js"
}
}
Con type: module declaras que tus .js son ESM, y con un único exports sirves a todo consumidor moderno. Esta es la forma mínima de 2026: sin ramas, sin dobles builds, sin superficie que auditar. Menos formatos significan menos maneras de equivocarse, y la simplicidad de un paquete solo-ESM es en sí misma una característica —menos peso, menos configuración y menos trampas que la coexistencia de dos mundos.
CommonJS: por alcance, ya no por defecto
CommonJS no ha muerto, pero cambió de estatus: pasó de ser el formato por defecto a ser una concesión que se justifica. Lo emites cuando tu público real incluye consumidores que aún cargan con require —un Node clásico, una herramienta de test antigua, un toolchain corporativo que no migró—. Si sabes que ese público existe y te importa, añadir una salida CJS amplía tu alcance. Si no lo sabes, probablemente estés cargando peso muerto por costumbre.
El coste de ofrecer CJS no es solo emitir un segundo archivo. Un paquete dual —ESM y CJS a la vez— abre el riesgo del dual package hazard: si una parte del árbol carga tu versión ESM y otra la CJS, coexisten dos copias del módulo con estado separado, y las comprobaciones de identidad como instanceof fallan. Se mitiga cableando bien el campo exports con condiciones import y require que apunten a un comportamiento coherente, pero la mitigación es trabajo real y una fuente conocida de bugs sutiles.
{
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}
La decisión, por tanto, se reduce a una pregunta honesta sobre tu audiencia:
- Solo ESM: el defecto de 2026. Librería nueva, consumidores modernos, cero ambigüedad de carga.
- ESM + CJS dual: solo si tu público real incluye Node clásico o toolchains con
requireque te importan. - Solo CJS: casi nunca justificable hoy; cierra el tree shaking y te deja fuera del terreno común.
Un consumidor de CommonJS puede cargar tu ESM con un import() dinámico, pero no con un require síncrono sin ayuda del runtime; un consumidor de ESM importa tu CJS con relativa comodidad porque Node le fabrica un default export. Esa asimetría es la que hace que la condición import deba resolver siempre a tu ESM y require a tu CJS, nunca cruzados. Cablear mal el exports es lo que produce el clásico paquete que “funciona en mi máquina” y revienta en el árbol de otro, donde las dos versiones se encuentran.
Por qué UMD casi murió
Durante una década, UMD fue el formato universal. Su gracia era detectar el entorno en runtime: si había un cargador AMD se registraba como AMD, si había CommonJS se exportaba como module.exports, y si no había nada caía en un global sobre window. Esa adaptabilidad resolvía un problema muy concreto de su época: querías servir una librería desde un CDN con una etiqueta script suelta, sin bundler, y que funcionara en cualquier página sin saber cómo la iban a cargar. UMD era la respuesta a un navegador que no tenía módulos nativos.
Ese problema desapareció. Los navegadores modernos entienden script type=module de forma nativa, así que un CDN puede servir ESM directamente y el navegador lo importa sin envoltorio universal. Los import maps permiten mapear especificadores desnudos a URLs sin necesidad de globals. Y los CDN orientados a ESM —esm.sh, Skypack en su día, jsDelivr con su capa de módulos— entregan cualquier paquete de npm como ESM listo para importar. Cuando el navegador aprendió a hablar ESM, la única razón de ser de UMD se evaporó.
Lo que queda de UMD es residual y conviene reconocerlo sin nostalgia. Sobrevive en la carga por script de scripts heredados que aún esperan un global, en algún widget embebible de terceros, y en documentación antigua que nadie actualizó. Para una librería nueva en 2026, emitir UMD es casi siempre cargar un formato que nadie de tu público real necesita. Si de verdad debes soportar la etiqueta script directa, el formato iife con un global explícito cubre ese caso con menos ceremonia que UMD.
flowchart TD
Q[que publico en 2026] --> E[ESM por defecto]
E --> P[preserva estructura: el consumidor poda]
Q --> C{mi publico usa require}
C -->|si, de verdad| D[anado CJS dual con exports]
C -->|no| S[solo ESM, sin peso muerto]
Q --> U{necesito script suelto}
U -->|casi nunca| M[UMD murio: usa import maps o esm CDN]
U -->|caso puntual| I[iife con un global]
style E fill:#a6e3a1,color:#11111b
style S fill:#a6e3a1,color:#11111b
style M fill:#f38ba8,color:#11111bEn 2026 no configuras Rollup a pelo para una librería: usas un empaquetador de librerías —tsdown, unbuild o similares sobre Rolldown— que, a partir de un package.json y unos entry points, emite los formatos correctos, genera los tipos y cablea exports por ti. Tu trabajo no es escribir la config de salida, sino decidir la matriz de formatos: qué público sirves y con cuánto peso. La herramienta traduce esa decisión en archivos; tú tomas la decisión estratégica que ninguna herramienta puede tomar por ti.
Un formato, varios entornos: las conditions de runtime
Elegir ESM resuelve el idioma de módulos, pero no el entorno. Una misma librería puede necesitar código distinto según dónde corra: una implementación que use APIs del navegador y otra que use las de Node, aunque ambas se publiquen como ESM. Esa segunda dimensión no se decide con el formato, sino con las conditions de runtime dentro de exports —browser, node, worker, default— que el bundler del consumidor evalúa para servir la variante correcta.
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"browser": "./dist/index.browser.js",
"node": "./dist/index.node.js",
"default": "./dist/index.js"
}
}
}
Conviene no confundir los dos ejes. El formato —ESM o CJS— responde a qué sistema de módulos usa el consumidor; la condition de entorno —browser, node, worker— responde a en qué runtime se ejecuta. Un paquete puede ser solo-ESM y aun así ofrecer tres variantes de entorno, porque son preguntas ortogonales. Mezclarlas es una fuente clásica de confusión: creer que “dar soporte al navegador” exige UMD, cuando basta con una condition browser sobre el mismo ESM.
La regla de oro de las conditions es que default va siempre la última, como red que atrapa cualquier entorno no contemplado. El resolvedor recorre las ramas en orden y se queda con la primera que coincide, así que las condiciones específicas van arriba y la genérica al fondo. Es la misma disciplina de orden que gobierna la condition types, y por la misma razón: quien resuelve se detiene en el primer acierto.
Antes de multiplicar variantes, comprueba que de verdad las necesitas. La inmensa mayoría de librerías corren idénticas en navegador, Node y edge porque solo usan JavaScript estándar, y para ellas una única salida ESM con default basta y sobra. Las conditions de entorno son para el caso real —una librería que toca el DOM en el navegador y el sistema de archivos en Node— no para curarse en salud. Cada variante que añades es otra superficie que mantener y otra combinación que verificar.
ESM primero
El defecto de 2026. Preserva la estructura estática, permite tree shaking aguas abajo y lo entiende todo el ecosistema.
CJS por alcance
Solo si tu público real carga con require. Amplía cobertura a costa del dual package hazard y más superficie.
UMD casi muerto
Su razón —un navegador sin módulos— desapareció. Los import maps y los CDN de ESM lo jubilaron.
iife si acaso
Para el caso puntual de un script suelto con global, iife es más simple y honesto que UMD.
El error dual más común no es ofrecer dos formatos, sino que la extensión, el campo type y el exports no concuerden: un archivo .js servido bajo la condition require cuando el paquete es type: module, o un .mjs mapeado bajo require. El runtime intenta cargarlo con el sistema equivocado y falla con un error de sintaxis o de módulo que desconcierta a quien te instala. La regla que elimina todo el género: usa extensiones explícitas —.mjs para ESM, .cjs para CommonJS— y deja que la extensión, no el type, decida sin ambigüedad el sistema de cada archivo de tu dist.
El error de categoría más común al publicar es tratar los formatos como un menú del que se marca todo por si acaso: ESM, CJS, UMD, iife, los cuatro, para no dejar a nadie fuera. Esa generosidad es en realidad pereza estratégica, porque cada formato que emites es una promesa que tendrás que mantener, una superficie más que auditar y, con frecuencia, peso muerto que nadie de tu público real consume. La disciplina de 2026 es la contraria: parte de ESM como única respuesta y obliga a justificar cada formato adicional con un consumidor concreto que de verdad lo necesita. ESM no es una opción entre varias; es el terreno común al que se dirige el centro del ecosistema, el único que preserva la estructura estática de la que depende todo el tree shaking aguas abajo, y el que cualquier bundler y cualquier runtime nuevo entiende sin ayuda. CommonJS deja de ser el defecto y pasa a ser una concesión que pagas —con el dual package hazard incluido— solo cuando tu audiencia real incluye un require que te importa. Y UMD es la lección histórica más nítida del nivel: un formato puede ser imprescindible durante una década y volverse residual en cuanto desaparece el problema que resolvía. UMD existía porque el navegador no tenía módulos; el día que los tuvo, con script type=module, import maps y CDN de ESM, UMD perdió su único cometido y se convirtió en peso muerto que muchos siguen emitiendo por inercia. Interioriza el patrón y trasciende a los formatos concretos: no publicas para cubrir todas las casillas imaginables, sino para servir con precisión a un público que puedes nombrar. La generosidad indiscriminada no te hace más compatible; te hace más pesado, más frágil y más difícil de mantener. Un paquete que ofrece exactamente los formatos que su público usa —casi siempre ESM y basta— es más ligero, más simple y más honesto que uno que reparte cuatro idiomas para no tener que pensar a quién le habla.
- Publica en seco una librería solo-ESM con
type: moduley un únicoexports, e impórtala desde un proyecto Vite para confirmar que se poda bien. - Añade una salida CJS con condiciones
importyrequireenexports, y razona qué consumidor concreto justifica ese segundo formato. - Provoca a propósito un dual package hazard cargando las dos versiones en el mismo árbol y observa cómo falla un
instanceof. - Sirve tu paquete ESM desde un CDN de módulos e impórtalo en una página con
script type=modulesin bundler, para comprobar que UMD ya no hace falta. - Audita una dependencia tuya: mira qué formatos emite y decide cuáles de ellos consume de verdad tu público y cuáles son peso muerto.