Output formats: esm, cjs, iife y umd
Un mismo grafo de módulos puede emitirse en varias formas, y cada una habla el idioma de un consumidor distinto. El eje `output.format` de Rollup: `esm` para bundlers y ESM nativo, `cjs` para Node y toolchains antiguos, `iife` para un script suelto en el navegador, `umd` para funcionar en cualquier entorno. Cuándo emitir cada uno, por qué `output.name` y `output.globals` son obligatorios en los formatos de navegador, y cómo publicar un paquete dual sin caer en sus trampas.
El tree shaking y el scope hoisting deciden qué código entra en el bundle; los output formats deciden en qué idioma sale. Rollup construye una vez el grafo y luego lo emite en la forma que pidas: un módulo de ES para bundlers, un módulo de CommonJS para Node, una función autoejecutable para un <script> suelto, o un envoltorio universal que se adapta a lo que encuentre. Elegir bien el formato no es un detalle de configuración: es responder con precisión a la pregunta de quién va a consumir tu código y bajo qué sistema de módulos.
- Dominar el eje
output.formaty sus cuatro valores centrales. - Distinguir los formatos de módulo —
esm,cjs— de los de navegador —iife,umd—. - Saber por qué
output.nameyoutput.globalsson obligatorios eniifeyumd. - Decidir qué formato emitir según el consumidor, y cómo publicar un paquete dual.
El eje output.format
Toda la elección se concentra en un campo: output.format. Sus valores canónicos son es —con los alias esm y module—, cjs —alias commonjs—, iife, umd, y los heredados amd para RequireJS y system para SystemJS. El grafo es el mismo en todos; lo que cambia es el envoltorio con que Rollup lo entrega y el sistema de carga que ese envoltorio asume del otro lado.
Como output admite un array, un solo build puede emitir varios formatos a la vez. Es el patrón habitual de una librería que quiere servir a consumidores dispares sin reconstruir el grafo tres veces:
// rollup.config.js
export default {
input: 'src/index.js',
external: ['react'],
output: [
{ file: 'dist/index.mjs', format: 'es' },
{ file: 'dist/index.cjs', format: 'cjs' },
{
file: 'dist/index.umd.js',
format: 'umd',
name: 'MiLib',
globals: { react: 'React' },
},
],
};
esm y cjs: para sistemas de módulos
El formato esm es el destino por defecto de una librería moderna. Emite import y export tal cual, de modo que el consumidor —otro bundler, o Node con ESM nativo— pueda a su vez analizarlo y hacer tree shaking de lo que no use. Es el único formato que preserva la estructura estática del grafo aguas abajo: al reexportar ESM, no cierras ninguna puerta.
El formato cjs traduce ese mismo grafo a require y module.exports. Su razón de ser es la compatibilidad: Node en su modo clásico, herramientas de test antiguas, y cualquier consumidor que aún no cargue ESM. El coste es que CommonJS no es analizable estáticamente, así que quien importe tu cjs recibe el paquete entero y pierde la capacidad de podarlo. Emites cjs por alcance, no por calidad de salida.
La decisión de qué formato emitir se reduce a quién consume el código y bajo qué sistema de módulos:
esm: para otro bundler o Node con ESM nativo. El destino por defecto de una librería moderna.cjs: para Node clásico o toolchains conrequire. Se añade por compatibilidad, no por calidad.iife: para un<script>suelto que expone un global. El drop-in de navegador sin dependencias.umd: para una librería que debe correr en cualquier entorno sin saber cómo la van a cargar.
es, esm y module designan exactamente el mismo formato; Rollup los acepta todos por compatibilidad con configuraciones antiguas. Lo mismo con cjs y commonjs. No hay diferencia técnica entre escribir uno u otro: elige el que tu equipo lea mejor y sé coherente. Lo que sí importa es no confundir el formato de salida con la extensión del archivo o con el campo type del package.json: son tres decisiones separadas que deben coincidir para que Node interprete bien lo que emites.
iife y umd: para el navegador directo
Los dos formatos restantes existen para un mundo sin bundler: cargar un archivo con <script> y usarlo de inmediato. El formato iife envuelve todo en una función que se ejecuta al vuelo —una immediately-invoked function expression— y expone un único global con el nombre que le des en output.name. Es el drop-in clásico: un <script src> y tu librería aparece como window.MiLib, sin ningún sistema de módulos de por medio.
El formato umd —universal module definition— va un paso más allá: al arrancar, detecta el entorno y se adapta. Si hay un cargador AMD, se registra como AMD; si hay CommonJS, se exporta como module.exports; si no hay ninguno, cae en el global como iife. Es el formato universal de las librerías que debían funcionar en cualquier página sin saber de antemano cómo se iban a cargar. Por eso durante años los CDN servían umd por defecto.
Ambos formatos comparten dos exigencias que esm y cjs no tienen. La primera es output.name: como exponen un global, necesitan saber cómo llamarlo. La segunda es output.globals: si tu código depende de algo externo —React, por ejemplo—, en el navegador ese externo también vive como global, y Rollup necesita el mapa que traduce el especificador react al nombre React de window.
// output para un umd que asume React ya cargado en la pagina
{
format: 'umd',
name: 'MiLib',
globals: { react: 'React' },
}
Quedan dos formatos heredados que rara vez elegirás hoy, pero conviene reconocer. amd es el formato de RequireJS, pensado para la carga asíncrona en navegadores anteriores a los módulos nativos; system es el de SystemJS, un cargador dinámico que soportaba ESM y su semántica de live bindings antes de que los navegadores lo hicieran de serie. Ambos pertenecen a una era previa a <script type="module">: emitirlos solo tiene sentido si mantienes un entorno que aún los exige.
flowchart TD Q[para quien empaquetas] --> L[libreria publicada en npm] Q --> C[script suelto en el navegador] Q --> A[aplicacion con bundler] L --> ES[formato es y opcional cjs] C --> II[formato iife o umd con name] A --> CH[formato es troceado en chunks]
esm
Para bundlers y ESM nativo de Node. Preserva la estructura estática: el consumidor puede seguir haciendo tree shaking.
cjs
Para Node clásico y toolchains con require. Amplía el alcance a costa de perder el análisis estático aguas abajo.
iife
Para un <script> directo. Autoejecutable, expone un único global vía output.name. Sin sistema de módulos.
umd
Universal: se adapta a AMD, CommonJS o global en runtime. El formato de las librerías que deben correr en todas partes.
Publicar a la vez esm y cjs —un paquete dual— resuelve el alcance, pero abre un riesgo conocido como dual package hazard: si una parte del árbol de dependencias carga tu versión esm y otra la cjs, coexisten dos copias del mismo módulo con estado separado, y comprobaciones como instanceof fallan porque las clases no son idénticas. Se mitiga cableando bien el campo exports del package.json con condiciones import y require que apunten al mismo comportamiento, y manteniendo el estado con carga lateral fuera del módulo dual. Emitir dos formatos es fácil; garantizar que nadie cargue los dos a la vez es el trabajo de verdad.
El campo exports es quien conecta cada condición de carga con el archivo del formato correcto, y es la pieza que hace usable un paquete dual:
{
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
}
}
La tentación del principiante es tratar output.format como una casilla que rellenar y copiar del ejemplo de turno. La verdad es que cada formato codifica una hipótesis sobre quién está del otro lado y qué sabe hacer. esm dice: te empaqueta otra herramienta, así que te doy estructura para que la aproveches. cjs dice: te carga un Node que no habla ESM, así que renuncio a la pureza por llegar a ti. iife dice: no hay ninguna herramienta, solo un navegador y un <script>, así que me convierto en un global y me ejecuto solo. umd dice: no sé quién eres, así que pregunto en runtime y me adapto. Elegir el formato es, literalmente, elegir a tu audiencia. Y por eso una librería seria no elige uno: emite varios en una sola pasada, porque su público es heterogéneo y su trabajo es no dejar a nadie fuera. Cuando entiendes esto dejas de copiar configuraciones y empiezas a diseñar la superficie de distribución de tu código: qué formatos, con qué exports, para qué consumidores, con qué garantías. Esa es la diferencia entre publicar un paquete y publicar un paquete que la gente pueda usar sin sorpresas. El grafo lo construye el bundler; el contrato con el mundo lo firmas tú en el bloque output.
- Configura un
outputcomo array que emitaes,cjs,iifeyumda partir del mismoinput, y observa las cuatro salidas. - Abre el
iifey localiza dónde se expone el global que definiste conoutput.name. - Marca una dependencia como
external, dale su entrada enoutput.globals, y comprueba cómo elumdla referencia en vez de incluirla. - Cablea un
package.jsonconexportsque apunte alesmbajo la condiciónimporty alcjsbajorequire, y razona qué evita eso. - Explica en dos frases por qué el
esmdeja hacer tree shaking al consumidor y elcjsno.