unbuild y tsdown: las opciones modernas y el stub mode
Dos empaquetadores encarnan la generación moderna. unbuild, del ecosistema UnJS, infiere las entradas desde el campo `exports` del `package.json` y ofrece un stub mode que, vía jiti, carga tu código fuente en vivo sin reconstruir durante el desarrollo de la propia librería. tsdown, construido sobre Rolldown y Oxc, es el empaquetador en Rust que el ecosistema de tsup señala como sucesor: rápido, con declaraciones apoyadas en isolatedDeclarations y compatible con el modelo de Rollup. Cuándo elegir cada uno según el bucle de trabajo.
Si tsup definió la generación anterior de empaquetadores de librerías, dos herramientas se reparten la moderna. unbuild viene del ecosistema UnJS y trae una idea que cambia el bucle de trabajo: un stub mode que te deja desarrollar la librería sin reconstruirla en cada guardado. tsdown viene del mundo de Rolldown y Oxc, y trae la promesa del motor en Rust aplicada por fin al empaquetado de librerías, no solo al de aplicaciones. Conocer ambas no es coleccionar herramientas: es entender las dos direcciones en que ha evolucionado el problema —la ergonomía del bucle de desarrollo y la velocidad bruta del motor— y saber cuál te resuelve el día a día.
- Configurar unbuild infiriendo las entradas desde el campo
exports. - Entender el stub mode y cómo jiti evita reconstruir durante el desarrollo.
- Situar tsdown sobre Rolldown y Oxc como el empaquetador moderno en Rust.
- Elegir entre uno y otro según el bucle de trabajo y el ecosistema.
unbuild y la inferencia desde exports
unbuild parte de una observación fina: la mayor parte de la configuración de un empaquetador ya está escrita en tu package.json. Los campos exports, main y module declaran qué expone tu librería y en qué formato; repetir esa información en un archivo de build es duplicarla y arriesgarse a que diverjan. Por eso unbuild puede inferir sus entradas y sus formatos directamente de esos campos, y funcionar casi sin configuración propia.
Cuando necesitas control, la configuración vive en un build.config.ts explícito. unbuild se apoya en Rollup para el bundling y en mkdist para el modo archivo a archivo, y expone ambos bajo una misma interfaz:
// build.config.ts
import { defineBuildConfig } from "unbuild";
export default defineBuildConfig({
entries: ["src/index"],
declaration: true,
rollup: { emitCJS: true },
});
entries: los puntos de entrada; si los omites, unbuild los deduce deexports.declaration: genera los.d.tsjunto al código.emitCJS: añade la salida CommonJS además del ESM por defecto.mkdist: un modo alternativo que transpila archivo a archivo sin empaquetar.
El stub mode: no reconstruir en desarrollo
La aportación más original de unbuild es el stub mode, y para apreciarla hay que ver el problema que resuelve. Cuando desarrollas una librería y la pruebas desde otro paquete —en un monorepo, por ejemplo—, cada cambio en el código de la librería exige reconstruir su dist para que el consumidor vea la versión nueva. Ese bucle de reconstruir y probar, repetido cientos de veces al día, es una fricción constante.
El stub mode lo elimina. Al ejecutar unbuild --stub, en vez de compilar tu código, unbuild escribe en dist un archivo diminuto —un stub— que reexporta tu código fuente cargándolo en vivo a través de jiti, un runtime que transpila TypeScript al vuelo. El consumidor importa dist como siempre, pero lo que recibe es tu src actual, sin build de por medio. Editas el fuente, guardas, y el cambio ya está: no hay reconstrucción porque no hay artefacto compilado que reconstruir.
El stub mode traslada el trabajo del momento de compilar al momento de ejecutar: en vez de transpilar toda la librería por adelantado cada vez que guardas, jiti transpila cada módulo la primera vez que se importa en tiempo de ejecución. Para el desarrollo de una librería que consumes localmente, es la diferencia entre un bucle con pausa y un bucle sin ella. Cuando llega el momento de publicar, ejecutas el unbuild real —sin --stub— y obtienes el dist compilado de verdad. El stub es para iterar; el build es para distribuir.
tsdown sobre Rolldown
tsdown es la otra dirección: no reinventa el bucle de desarrollo, sino que lleva el motor en Rust al empaquetado de librerías. Está construido sobre Rolldown —el bundler en Rust, sobre Oxc— y su propuesta es la coherencia final del toolchain de 2026: el mismo motor que Vite 8 usa para aplicaciones, ahora afinado para publicar paquetes. De ahí que el ecosistema de tsup lo señale como su sucesor natural: misma ergonomía de cero configuración, pero sin la costura de dos motores.
Su configuración se lee casi igual que la de tsup, lo cual es deliberado para suavizar la migración, pero con una diferencia decisiva bajo el capó: las declaraciones de tipos no viajan en una pasada lenta con un comprobador aparte, sino que se apoyan en isolatedDeclarations de TypeScript, lo que permite emitirlas sin la inferencia completa entre archivos y a una velocidad acorde con el resto del build.
// tsdown.config.ts
import { defineConfig } from "tsdown";
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm", "cjs"],
dts: true,
});
flowchart TD MOD[empaquetadores modernos] --> UN[unbuild del ecosistema UnJS] MOD --> TD[tsdown sobre Rolldown y Oxc] UN --> INF[infiere entradas desde exports] UN --> STUB[stub mode carga el fuente con jiti] TD --> RUST[motor en Rust unifica codigo y tipos] TD --> SUC[sucesor senalado de tsup]
El modo sin bundle: mkdist
unbuild no solo empaqueta con Rollup; trae también mkdist, un modo que transpila archivo a archivo sin fundir el grafo. Cada archivo de tu src se convierte en un archivo de dist, conservando la estructura, e incluso copia y procesa activos como bloques de estilo junto al código. Es la respuesta de unbuild al empaquetado transpile-only, y se declara como una entrada más dentro de la configuración.
// build.config.ts con una entrada mkdist sin bundle
import { defineBuildConfig } from "unbuild";
export default defineBuildConfig({
entries: [
{ builder: "mkdist", input: "src", outDir: "dist" },
],
declaration: true,
});
La diferencia con el modo Rollup de la misma herramienta es exactamente el eje empaquetar o no empaquetar que recorre todo este nivel. El builder de Rollup funde tu grafo en pocos archivos; el builder mkdist lo preserva módulo a módulo. Que unbuild ofrezca los dos bajo una sola configuración es su forma de decir que la elección no es de herramienta sino de forma de salida, y que una misma librería puede querer una cosa u otra según lo que exponga.
Esa flexibilidad es más útil de lo que parece: puedes empaquetar la entrada principal para dar un artefacto limpio y, a la vez, publicar sin bundle un conjunto de utilidades para que el consumidor las importe sueltas, todo desde el mismo build.config.ts.
builder: mkdist: transpila archivo a archivo, sin bundle, preservando el árbol.builder: rollup: funde el grafo en pocos artefactos, el modo empaquetado.- Activos junto al código: mkdist copia y procesa recursos que viven al lado de los módulos.
- Una config, dos formas: combinas entradas empaquetadas y sin bundle en el mismo build.
Y como unbuild lee el package.json, las entradas se anuncian solas: el mismo exports que consume el usuario es el que la herramienta usa para saber qué construir, sin duplicar nada.
{
"exports": {
".": "./dist/index.mjs",
"./utils": "./dist/utils.mjs"
}
}
La elección entre los dos builders se puede resumir en una línea por escenario:
- Entrada única y cerrada: el builder Rollup, que empaqueta el grafo.
- Muchas subrutas: el builder mkdist, que preserva el árbol de módulos.
- Activos junto al código: mkdist, que los copia y procesa al lado.
- Inferencia total: deja que
exportsdicte las entradas y no configures apenas nada.
Que unbuild exponga mkdist y Rollup bajo la misma interfaz enseña algo que trasciende a la herramienta: empaquetar y no empaquetar no son bandos rivales, son dos salidas legítimas que a veces conviven en un mismo paquete. La herramienta no te obliga a elegir un dogma; te pide que decidas, entrada por entrada, qué forma sirve mejor a quien la va a consumir. Reconocer eso te libera de la pregunta falsa de cuál modo es mejor y te devuelve la verdadera, que es cuál sirve a esta entrada concreta.
Inferencia desde exports
unbuild deduce entradas y formatos del package.json, evitando duplicar en un config lo que ya declaraste.
Stub mode
unbuild --stub escribe un stub que carga tu src en vivo con jiti. Desarrollas sin reconstruir la librería.
tsdown en Rust
Sobre Rolldown y Oxc. El motor de Vite 8 aplicado a librerías: rápido y sin la costura de dos motores.
dts por isolatedDeclarations
tsdown emite tipos sin la inferencia completa entre archivos, a una velocidad acorde con el resto del build.
Es tentador ordenar los empaquetadores en una única fila de peor a mejor, como si el más nuevo ganara siempre. unbuild y tsdown demuestran que hay al menos dos ejes distintos de progreso, y que avanzar en uno no es avanzar en el otro. El eje de tsdown es la velocidad del motor: llevar Rust y Oxc al empaquetado de librerías, fundir la generación de tipos con la del código, borrar la costura que tsup heredó de esbuild. Es un progreso vertical, de rendimiento, y es real. El eje de unbuild es otro por completo: la ergonomía del bucle de desarrollo. El stub mode no hace tu build más rápido —de hecho para publicar sigues compilando— sino que elimina la compilación del bucle de iteración, cambiando el trabajo de compilar por adelantado por transpilar bajo demanda. Son mejoras ortogonales: puedes querer el motor más rápido para tu CI y a la vez el stub para tu día a día, y en un monorepo real probablemente quieras las dos cosas en momentos distintos. Quien solo ve el eje de la velocidad elige tsdown y se pierde que su fricción diaria no era el tiempo de build sino el ciclo de reconstruir para probar. Quien solo ve la ergonomía se queda sin la velocidad que su pipeline sí nota. La madurez de ingeniería no es saber cuál es más rápido, sino saber qué eje te está costando tiempo hoy —el motor o el bucle— y elegir la herramienta que ataca ese eje. Casi nunca es la misma para todo.
- Configura unbuild sin
entriesy comprueba que infiere las entradas desde el campoexportsde tupackage.json. - Ejecuta
unbuild --stub, abre el archivo generado endisty localiza la referencia a jiti y a tu código fuente. - Edita el
srcde la librería sin reconstruir y verifica desde un consumidor que el cambio se ve al instante. - Empaqueta la misma librería con tsdown y compara el tiempo total, prestando atención a cuánto tarda la generación de
.d.ts. - Argumenta, para un monorepo real, en qué momento usarías el stub de unbuild y en qué momento el motor de tsdown.