wandres.dev
HERRAMIENTAS DE BUILD · Vite lib, tsup, unbuild

tsup sobre esbuild: configuración mínima y su reinado

tsup convirtió empaquetar una librería de TypeScript en una sola línea de comando. Construido sobre esbuild para transpilar y empaquetar a velocidad de Go, y apoyado en Rollup para el bundling de las declaraciones, ofrece múltiples formatos, tree shaking y minificación con cero configuración. Por qué se volvió el estándar de facto de las librerías TS entre 2021 y 2025, dónde está su costura —esbuild no comprueba tipos, así que los `.d.ts` viajan en una pasada aparte y más lenta— y por qué en 2026 su propio ecosistema señala a tsdown como sucesor.

⏱ 15 min

Durante buena parte de la última década, empaquetar una librería de TypeScript era un ejercicio de configuración: un rollup.config.js con media docena de plugins, o un webpack que nadie quería tocar. tsup llegó con una tesis distinta —que el caso común no debería necesitar configuración alguna— y la cumplió apoyándose en esbuild para ir rápido. Una sola línea, tsup src/index.ts, y ya tienes un bundle. Esa promesa de cero configuración, servida a la velocidad de Go, explica por qué tsup se volvió el pavimento por defecto sobre el que se publicaron miles de paquetes, y también por qué su límite está exactamente donde esbuild deja de alcanzar.

🎯 Al terminar esta lección sabrás
  • Empaquetar una librería con la configuración mínima de tsup y entender qué asume.
  • Emitir varios formatos en una sola pasada y añadir declaraciones de tipos.
  • Situar la arquitectura de tsup: esbuild para el código, Rollup para los .d.ts.
  • Comprender su popularidad, su costura y por qué tsdown se perfila como su relevo.

Configuración mínima

La tesis de tsup es que empaquetar una librería sencilla no debería costar más que nombrar el punto de entrada. Todo lo demás —el formato, el target, el tree shaking— trae un valor por defecto sensato que puedes no tocar nunca. En su forma más pura, tsup es un comando:

# Un bundle a partir de un unico punto de entrada
tsup src/index.ts

# Varios formatos y declaraciones, todavia sin archivo de config
tsup src/index.ts --format esm,cjs --dts

Cuando el proyecto crece, la misma configuración se muda a un tsup.config.ts tipado, sin cambiar de modelo mental. Cada bandera del comando tiene su campo equivalente, y defineConfig te da el autocompletado:

// tsup.config.ts
import { defineConfig } from "tsup";

export default defineConfig({
  entry: ["src/index.ts"],
  format: ["esm", "cjs"],
  dts: true,
  clean: true,
  sourcemap: true,
  treeshake: true,
});
  • entry: uno o varios puntos de entrada; cada uno produce su propio artefacto.
  • format: el array de formatos; esm y cjs cubren a casi todos los consumidores.
  • dts: activa la generación de declaraciones en una pasada separada.
  • clean: vacía el directorio de salida antes de cada build para no dejar restos.

Múltiples formatos en una pasada

El punto donde tsup ahorra más trabajo es el paquete dual. Pedir --format esm,cjs produce, del mismo grafo, un .mjs para los consumidores de ESM y un .cjs para el Node clásico, con las extensiones correctas y sin que orquestes dos builds. Añade --dts y obtienes además un .d.ts que sirve a ambos. Con tres banderas cubres la superficie de distribución que en Rollup exigía un output como array y un plugin de declaraciones cableado a mano.

Esa comodidad descansa en una arquitectura de dos motores que conviene ver con claridad. El código —transpilar TypeScript, resolver el grafo, aplicar tree shaking, minificar— lo hace esbuild, y de ahí viene la velocidad. Pero esbuild no comprueba tipos: no sabe inferir el tipo de una expresión ni, por tanto, emitir un .d.ts correcto. Así que tsup delega esa tarea en una pasada aparte, apoyada en Rollup y rollup-plugin-dts, que lee tus tipos y los empaqueta en un único archivo de declaraciones.

El reparto de trabajo entre los dos motores es nítido, y explica exactamente dónde se va el tiempo del build cuando el proyecto crece:

  • esbuild: transpila, resuelve el grafo, hace tree shaking y minifica el código.
  • Rollup: empaqueta las declaraciones que esbuild no sabe generar.
  • La velocidad: nace en esbuild y domina el tiempo del código.
  • El cuello de botella: es la pasada de tipos, de otro orden de magnitud.
  • La consecuencia: el mismo build sin --dts compila casi al instante.
⚠️
La pasada de dts es el cuello de botella

El detalle que sorprende a quien cronometra su build por primera vez es que activar dts: true puede multiplicar el tiempo total, a veces hasta dominarlo. La razón es estructural: mientras esbuild empaqueta tu JavaScript en decenas de milisegundos, la generación de tipos tiene que arrancar el comprobador de TypeScript, que es de otro orden de magnitud más lento. No es un fallo de tsup, es la frontera de esbuild: transpila sin entender los tipos, así que alguien más lento tiene que entenderlos por él. Si tu build de librería se siente lento, casi siempre es el dts, no el bundle.

Por qué reinó y quién lo releva

tsup se volvió el estándar de facto por una combinación difícil de batir en su momento: cero configuración para el caso común, velocidad de esbuild para el código, y salida dual con tipos en un solo comando. Entre 2021 y 2025, si abrías un paquete de TypeScript publicado en npm, lo más probable es que detrás hubiera un tsup. Su ergonomía marcó el listón de lo que la gente esperaba de un empaquetador de librerías.

Pero la misma costura que lo define —esbuild rápido para el código, un motor más lento para los tipos— es la que abre la puerta a su relevo. En 2026, el empuje de Rolldown y Oxc permite un empaquetador que unifica ambas mitades bajo un motor en Rust, y el propio ecosistema de tsup señala a tsdown como el camino natural de migración. tsup no desaparece ni deja de funcionar; sencillamente deja de ser la frontera del rendimiento, igual que Rollup dejó de serlo cuando llegó Rolldown.

flowchart LR
SRC[tu codigo TypeScript] --> ES[esbuild transpila y empaqueta]
SRC --> DTS[pasada aparte para los tipos]
ES --> JS[salida esm y cjs rapida]
DTS --> RD[Rollup empaqueta las declaraciones]
RD --> OUT[un archivo de tipos]
JS --> BUN[bundle listo]

Las banderas que de verdad usarás

Más allá del caso mínimo, un puñado de opciones cubren casi todo lo que una librería necesita afinar, y conviene conocerlas porque son las que separan un artefacto correcto de uno descuidado. El target fija la sintaxis de salida —qué versión de JavaScript emites—; minify decide si comprimes; sourcemap adjunta los mapas para depurar; splitting permite trocear el ESM en fragmentos compartidos; y treeshake poda lo que no se use antes de emitir.

// tsup.config.ts con las opciones habituales de una libreria
import { defineConfig } from "tsup";

export default defineConfig({
  entry: ["src/index.ts"],
  format: ["esm", "cjs"],
  target: "es2022",
  dts: true,
  splitting: true,
  treeshake: true,
  sourcemap: true,
  minify: false,
  clean: true,
});

Dos matices importan para una librería. El primero: rara vez querrás minify: true en un paquete que otros van a re-empaquetar, porque su bundler ya minificará la aplicación final y una librería minificada solo dificulta leer los sourcemaps. El segundo: splitting solo aplica al formato esm; el cjs no soporta la carga dinámica de fragmentos que el troceado necesita. Y para el bucle de desarrollo, --watch reconstruye al guardar, con un onSuccess opcional que ejecuta un comando tras cada build correcto.

Merece la pena interiorizar que estos valores por defecto no son neutrales: son la postura de tsup sobre qué es una librería sana. Publicar sin minificar, con sourcemaps y con tree shaking activo es lo que un consumidor espera, y tsup te lo da sin pedírselo.

  • target: la versión de JavaScript de salida; deja que el consumidor baje más si lo necesita.
  • minify: normalmente false en librerías; minifica quien construye la app final.
  • splitting: trocea el esm en fragmentos compartidos; no aplica al cjs.
  • sourcemap: adjunta los mapas para que el consumidor pueda depurar tu código.
  • --watch: reconstruye al guardar, el modo para iterar sobre la propia librería.
  • onSuccess: ejecuta un comando tras cada build correcto, útil para encadenar validaciones.
ℹ️
La librería no es la app: no minifiques por defecto

Un error común al migrar de configurar apps a publicar librerías es arrastrar hábitos de aplicación, y el más frecuente es minificar. En una app tiene sentido: el resultado va directo al navegador. En una librería casi nunca: el consumidor la re-empaqueta junto a su código, su bundler aplica su propia minificación, y lo único que consigues minificando antes es entregar un dist ilegible que estorba al depurar y complica los sourcemaps. Publica legible; deja la compresión final a quien ensambla la aplicación.

🚀

Cero configuración

tsup src/index.ts ya produce un bundle. El caso común no pide un archivo de config; los defaults son sensatos.

esbuild debajo

Transpila y empaqueta a velocidad de Go. La razón de que el build del código se sienta instantáneo.

🧬

dts por Rollup

Como esbuild no comprueba tipos, las declaraciones viajan en una pasada aparte con rollup-plugin-dts, más lenta.

🔁

tsdown como relevo

En 2026, el sucesor sobre Rolldown unifica código y tipos en un motor en Rust. tsup sigue vivo, pero cede la frontera.

La cero configuración es una postura sobre dónde poner la complejidad, no su ausencia

El regalo de tsup nunca fue no tener configuración: fue moverla del usuario al autor de la herramienta. Los defaults sensatos que te dejan escribir tsup src/index.ts y marcharte no son la ausencia de decisiones, son decisiones que alguien tomó por ti y escondió con buen criterio. Esta es una de las lecciones más transferibles de todo el toolchain: la ergonomía de una herramienta se mide por cuántas decisiones correctas puede tomar en tu lugar sin quitarte el control cuando lo necesitas. tsup acertó en qué esconder —el target, el formato base, el tree shaking— y en qué exponer —los formatos, las entradas, el dts—, y esa curación es la mitad de su valor. Pero fíjate en la otra mitad de la historia, porque es la que se repite en cada nivel de este track: la comodidad de tsup se construyó sobre esbuild, y por tanto heredó su frontera exacta. esbuild no comprueba tipos, así que tsup no podía generar .d.ts a su misma velocidad, y esa grieta —invisible en el caso pequeño, dolorosa en el grande— es la que un motor nuevo viene a cerrar. La moraleja no es que tsup esté superado; es que toda herramienta hereda las virtudes y los límites del motor sobre el que se apoya, y que saber leer esa herencia —qué te regala la base y qué techo te impone— es lo que te permite elegir con criterio en vez de por moda. tsup te enseñó lo que puede ser fácil; tsdown te enseñará hasta dónde puede llegar cuando la base ya no frena.

⚔️ Mide la costura de esbuild y los tipos
  1. Empaqueta una librería con tsup src/index.ts --format esm,cjs y examina las extensiones y el contenido de los dos artefactos.
  2. Cronometra el build sin --dts y luego con --dts, y anota cuánto suma la pasada de declaraciones sobre el total.
  3. Traslada la configuración a un tsup.config.ts con defineConfig y reproduce el mismo resultado desde el archivo.
  4. Explica con tus palabras por qué esbuild puede transpilar tu código pero no emitir sus .d.ts por sí solo.
  5. Lee la recomendación de migración a tsdown y enumera qué mitad del build esperas que se acelere más al unificar el motor.