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

Elegir la herramienta y decidir: empaquetar o publicar sin bundle

Dos decisiones cierran el nivel. La primera es cuál de las herramientas —Vite en lib mode, tsup, unbuild, tsdown— encaja con tu librería. La segunda, más profunda, es si empaquetar la librería en uno o pocos archivos o publicarla sin bundle, archivo a archivo, en modo transpile-only. Empaquetar oculta la estructura y afina el artefacto; no empaquetar preserva el árbol de módulos, habilita los deep imports y deja que el consumidor haga tree shaking. Una matriz de decisión por escenario y la recomendación fundamentada para 2026.

⏱ 18 min

Llegas al final del nivel con cuatro herramientas en la mano y una duda legítima: ¿cuál uso? Pero antes de esa pregunta hay otra más honda que casi nadie plantea y que condiciona la respuesta entera: ¿debo empaquetar mi librería, o publicarla sin bundle, archivo a archivo? La elección de la herramienta es táctica y reversible; la de empaquetar o no es arquitectónica y le dice a tus consumidores cómo pueden importar tu código, hasta qué punto pueden podarlo y cómo lo van a depurar. Este capítulo cierra el nivel poniendo esas dos decisiones en su orden correcto: primero la naturaleza del artefacto, después la herramienta que lo produce.

🎯 Al terminar esta lección sabrás
  • Distinguir empaquetar la librería de publicarla sin bundle en modo transpile-only.
  • Sopesar las consecuencias de cada opción para el consumidor: deep imports y tree shaking.
  • Aplicar una matriz de decisión que cruza el tipo de librería con la herramienta.
  • Formular una recomendación fundamentada para el ecosistema de 2026.

El eje empaquetar o no empaquetar

La primera decisión no es qué herramienta, sino qué forma quieres que tenga tu salida. Empaquetar significa recorrer el grafo y fundir tus muchos módulos en uno o pocos archivos, aplicando tree shaking y ocultando la estructura interna. Publicar sin bundle —el modo transpile-only, también llamado unbundled— significa transpilar cada archivo fuente a su archivo de salida, uno a uno, preservando el árbol de módulos tal cual lo escribiste.

Cada opción es un contrato distinto con quien te consume. El bundle entrega un artefacto cerrado y afinado: menos archivos, sin rastro de tu organización interna, con el tree shaking ya aplicado. El unbundled entrega tu estructura abierta: el consumidor puede importar un submódulo concreto —un deep import como mi-lib/utils— sin arrastrar el resto, su bundler ve módulos pequeños y granulares sobre los que hace su propio tree shaking con precisión, y los sourcemaps apuntan a archivos que se corresponden con los tuyos, lo que facilita depurar.

  • Empaquetar: uno o pocos archivos, estructura oculta, tree shaking hecho por ti, ideal para un distribuible cerrado.
  • Transpile-only: un archivo por módulo, estructura preservada, deep imports habilitados, tree shaking delegado al consumidor.
  • preserveModules: la opción de Rollup y Rolldown que emite sin fundir, conservando el grafo.
  • mkdist y tsc: transpilan archivo a archivo por naturaleza, sin construir un bundle.

La matriz de decisión

Con el eje claro, la elección de herramienta casi se deduce del tipo de librería. Una utilidad pequeña y autocontenida se beneficia de empaquetar: un tsdown o un tsup producen un artefacto limpio en segundos. Una librería grande con muchos puntos de entrada —un design system, una colección de utilidades donde el consumidor solo quiere una— pide transpile-only, para que cada deep import cueste solo lo que importa. Una librería de componentes con CSS reclama Vite en lib mode por su procesado de assets. Y un paquete del estilo UnJS, que quiere iterar sin reconstruir, encuentra en unbuild y su stub mode su bucle natural.

// tsdown sin bundle: preserva el arbol de modulos
import { defineConfig } from "tsdown";

export default defineConfig({
  entry: ["src/**/*.ts"],
  format: ["esm"],
  dts: true,
  unbundle: true,
});
📝
No empaquetar no es no construir

Conviene no confundir transpile-only con no tener build. Sigues transpilando TypeScript a JavaScript, sigues emitiendo .d.ts, sigues resolviendo qué formatos publicas y cómo enruta exports. Lo único que no haces es fundir los módulos en un bundle: cada archivo cruza el compilador y sale por el otro lado como un archivo. Por eso el nivel anterior sobre módulos y exports no era un preámbulo, era el cimiento: una librería unbundled es, literalmente, un árbol de módulos con un package.json bien enrutado, y todo depende de que ese enrutado sea correcto.

Recomendación para 2026

Con el ecosistema de 2026 sobre la mesa, la recomendación se puede afinar sin caer en el dogma. Para una librería de TypeScript nueva y sin assets, tsdown sobre Rolldown es el punto de partida por defecto: hereda el motor en Rust del resto del toolchain, genera tipos con isolatedDeclarations sin la pasada lenta, y el propio ecosistema de tsup lo señala como relevo. Para una librería de componentes con CSS, Vite en lib mode sigue siendo la respuesta por su procesado de assets. Para el flujo UnJS, unbuild y su stub. Y tsup, aunque cede la frontera del rendimiento, sigue siendo una elección perfectamente válida para lo que ya funciona.

Sobre el eje del bundle, la regla práctica es: empaqueta las librerías pequeñas y autocontenidas, y publica sin bundle las grandes con muchas entradas. En caso de duda, el unbundled preservando módulos es la opción más respetuosa con el consumidor, porque le deja a él las decisiones de poda en vez de tomarlas por él.

Cuando publicas sin bundle, el exports suele apoyarse en comodines para no listar cada archivo a mano, dejando que un solo patrón cubra todas las subrutas del árbol:

{
  "exports": {
    ".": "./dist/index.mjs",
    "./*": "./dist/*.mjs"
  }
}

Ese comodín es lo que convierte un árbol de módulos en un conjunto de deep imports usables sin escribir una entrada por archivo. Con él, cada módulo que emitas queda disponible como subruta de forma automática, y el consumidor importa exactamente lo que necesita.

  • Utilidad pequeña: empaqueta; un artefacto cerrado basta y sobra.
  • Design system: sin bundle; el consumidor importa solo los componentes que use.
  • Componentes con CSS: Vite en lib mode por su procesado de assets.
  • Iteración local intensa: unbuild con stub para no reconstruir en cada cambio.
  • Preset o plugin compartido: empaqueta; una entrada única simplifica el consumo.
  • Muchos consumidores dispares: publica dual y sin bundle, la superficie más flexible.
💡
En la duda, respeta al consumidor

Cuando ninguna señal es decisiva, inclínate por no empaquetar preservando módulos. Es la opción que menos decide por el consumidor: le entrega tu árbol abierto y le deja podar, importar en profundidad y depurar con sourcemaps fieles. Empaquetar es cómodo de publicar pero cierra puertas; el unbundled cuesta un poco más de enrutado y las deja todas abiertas. Ante la incertidumbre, elige la salida que no le quita opciones a quien todavía no conoces.

flowchart TD
Q[que tipo de libreria publicas] --> P[utilidad pequena autocontenida]
Q --> G[libreria grande con muchas entradas]
Q --> C[componentes con CSS]
Q --> U[flujo UnJS con iteracion local]
P --> B[empaquetar con tsdown o tsup]
G --> T[transpile only preservando modulos]
C --> V[Vite en lib mode]
U --> N[unbuild con stub mode]

Preservar módulos: cómo se ve sin bundle

Publicar sin bundle tiene un nombre técnico en Rollup y Rolldown: preserveModules. En vez de fundir el grafo, el bundler transpila cada módulo a su propio archivo de salida y conserva la estructura de carpetas, de modo que el dist es un espejo de tu src pero en JavaScript ya emitido. Es el mecanismo que hace posible el deep import, porque para que el consumidor pueda pedir mi-lib/utils esa ruta tiene que existir como archivo real en el paquete.

// rolldown o rollup preservando el arbol de modulos
export default {
  input: "src/index.ts",
  output: {
    dir: "dist",
    format: "es",
    preserveModules: true,
    preserveModulesRoot: "src",
  },
};

La consecuencia más importante es para el tree shaking del consumidor. Cuando tu librería llega como un solo bundle, el bundler de la aplicación tiene que analizar ese archivo grande para decidir qué sobra; cuando llega como módulos granulares, la poda es casi trivial, porque cada import trae solo su archivo y sus dependencias directas. Por eso los design systems y las librerías de utilidades grandes casi siempre se publican sin bundle: maximizan lo que el consumidor puede dejar fuera.

  • preserveModules: emite un archivo por módulo en vez de fundir el grafo.
  • preserveModulesRoot: fija la raíz para que el dist no arrastre la ruta src.
  • Deep imports: cada módulo es un archivo, así que las subrutas existen de verdad.
  • Tree shaking fino: el consumidor poda por archivo, sin analizar un bundle monolítico.

El coste es simétrico y hay que nombrarlo. Sin bundle publicas muchos más archivos, el exports se vuelve más grande —o usas un patrón con comodines para no listarlos uno a uno—, y pierdes las optimizaciones que solo son posibles cuando el bundler ve todo el grafo junto, como inlinear una función privada que se usa una sola vez. No hay opción gratis: el bundle optimiza a cambio de cerrar; el unbundled abre a cambio de no optimizar entre archivos.

⚠️
Sin bundle, el enrutado de exports es todo

Una librería sin bundle es un árbol de archivos, y lo único que la hace usable es un exports correcto en el package.json. Si una subruta no está enrutada, el consumidor no puede importarla aunque el archivo exista en el dist. Por eso al publicar unbundled el trabajo se desplaza del empaquetado al enrutado: cada entrada, cada condición import y require, cada types tiene que apuntar al archivo correcto. El bundle te ahorra ese cableado fundiendo todo en una entrada; el unbundled te lo devuelve entero, y ahí es donde se rompen la mayoría de las publicaciones descuidadas.

📦

Empaquetar

Uno o pocos archivos, estructura oculta, tree shaking hecho por ti. Ideal para una utilidad pequeña y cerrada.

🌳

Transpile-only

Un archivo por módulo, árbol preservado, deep imports y tree shaking del consumidor. Para librerías grandes.

🧭

La herramienta se deduce

Utilidad pura a tsdown; componentes con CSS a Vite; iteración local a unbuild. El tipo de librería casi la elige.

🗓️

Defecto 2026

tsdown sobre Rolldown como punto de partida; tsup sigue válido; Vite para assets; unbuild para el bucle UnJS.

Elegir la herramienta es fácil; elegir la forma del artefacto es la decisión de ingeniería

Si algo debe quedarte de este nivel, es el orden de las dos preguntas, porque casi todo el mundo las hace al revés. La pregunta pequeña —tsup o tsdown, Vite o unbuild— es la que primero apetece, pero es táctica: si eliges mal, migras en una tarde, porque todas producen aproximadamente el mismo tipo de salida y comparten el modelo de Rollup. La pregunta grande —empaquetar o no empaquetar— casi nadie la formula, y sin embargo es la que define el contrato con cada persona que instale tu paquete durante años. Empaquetar dice: aquí tienes un artefacto que he cerrado y optimizado por ti, tómalo como un todo. No empaquetar dice: aquí tienes mi árbol de módulos abierto, importa exactamente lo que necesites y poda el resto tú, que conoces tu aplicación mejor que yo. Ninguna es superior en abstracto; son promesas distintas a consumidores distintos. Y fíjate en que esta decisión es la síntesis de todo el track: solo puedes elegirla bien si entiendes los módulos ESM que hacen posible el deep import, el campo exports que enruta cada subruta, el tree shaking que premia al unbundled con módulos granulares, los output formats que decidirás emitir y los .d.ts que tendrás que enrutar en paralelo. La herramienta es el andamio; la forma del artefacto es la arquitectura. Un ingeniero júnior pregunta cuál es la mejor herramienta de build; uno sénior pregunta qué forma debe tener su librería para servir mejor a quien la use, y solo entonces elige la herramienta que produce esa forma con el menor esfuerzo. Aprende a hacer las dos preguntas, y en ese orden: primero el contrato, después el instrumento.

⚔️ Decide la forma antes que la herramienta
  1. Toma una librería tuya y clasifícala: ¿es pequeña y autocontenida, o grande con muchas entradas? Justifica el eje de bundle que le corresponde.
  2. Empaquétala primero como un bundle y luego en modo transpile-only preservando módulos, y compara el número de archivos y su estructura.
  3. Desde un consumidor, intenta un deep import como mi-lib/submodulo con cada salida y observa cuál lo permite y cuál no.
  4. Cruza tu tipo de librería con la matriz y elige la herramienta —tsdown, tsup, Vite o unbuild— que produce esa forma con menos fricción.
  5. Escribe en tres frases el contrato que tu decisión ofrece al consumidor: qué puede importar, qué puede podar y cómo va a depurar.