Compatibilidad con Rollup: misma API, mismos plugins
Rolldown reimplementa deliberadamente la API de Rollup —las mismas opciones, el mismo objeto de plugin y los mismos hooks— para que años de ecosistema sobrevivan al cambio de motor sin reescribirse. Este es el análisis fino de qué se mantiene idéntico, qué diverge por necesidad de rendimiento y qué añade Rolldown de nuevo: transformaciones nativas, filtros de hooks y plugins internos en Rust.
La compatibilidad con Rollup no es un detalle de marketing de Rolldown: es la condición que hace viable todo el proyecto. Un bundler sin plugins es inútil, y reconstruir desde cero el ecosistema de plugins de Rollup —resolución de paths, inyección de variables, manejo de CSS, integración con frameworks— habría tardado años. En lugar de eso, Rolldown reimplementa la interfaz de Rollup con fidelidad quirúrgica, de modo que la mayoría de los plugins existentes funcionan sin tocar una línea. Pero “compatible” no significa “idéntico”: hay costuras donde el rendimiento obliga a divergir y hay superpoderes nuevos que Rollup nunca tuvo. Este nivel diseca esa frontera.
- Ver qué partes de la API de Rollup conserva Rolldown de forma idéntica.
- Entender el modelo de plugins y el ciclo de hooks que ambos comparten.
- Reconocer dónde diverge Rolldown y por qué la razón casi siempre es el rendimiento.
- Conocer lo que Rolldown añade: transforms nativos, filtros de hooks y plugins en Rust.
La API que se conserva
Rolldown expone la misma forma que Rollup en casi todo lo que un usuario o un autor de plugin toca. La función principal rolldown() acepta un objeto de opciones de entrada con input, plugins, external y resolve, y el bundle resultante ofrece write y generate con opciones de salida donde eliges format, dir, sourcemap o el patrón de nombres de los chunks. Los formatos de salida son los de siempre: esm, cjs, iife y umd. Si sabías configurar Rollup, sabes configurar Rolldown sin abrir la documentación.
El inventario de lo que se conserva idéntico es largo y deliberado:
- Las opciones de entrada:
input,plugins,external,resolveytreeshake. - Las opciones de salida:
dir,format,sourcemapy el patrón de nombres de los chunks. - Los formatos de salida:
esm,cjs,iifeyumd, los mismos cuatro de siempre. - El objeto de contexto:
this.resolve,this.emitFile,this.getModuleInfoythis.addWatchFile.
// El mismo vite.config o rollup.config, con un cambio de import
import { defineConfig } from 'rolldown'
export default defineConfig({
input: 'src/index.ts',
external: ['react', 'react-dom'],
output: { dir: 'dist', format: 'esm', sourcemap: true },
})
Esa fidelidad es intencional y tiene un nombre: la API de plugins de Rollup se había convertido en el estándar de facto del empaquetado. La propia API de plugins de Vite es un superconjunto de la de Rollup. Reimplementarla en vez de inventar otra nueva es lo que permite que un plugin escrito en 2019 se cargue en el motor de 2026 sin saber que el suelo bajo sus pies cambió de JavaScript a Rust.
Conviene ser preciso sobre qué significa “compatible”. No es que Rolldown ejecute el código de Rollup; es que implementa el mismo contrato. Cuando tu plugin exporta un objeto con name, resolveId y transform, Rolldown lo entiende y lo llama en los mismos momentos y con los mismos argumentos que Rollup, aunque quien orqueste esas llamadas sea ahora un núcleo en Rust. La compatibilidad es de interfaz, no de implementación: mismo contrato, motor distinto.
El modelo de plugins y los hooks
Un plugin de Rollup —y por tanto de Rolldown— es un objeto con un name y una serie de funciones-hook que el bundler llama en momentos precisos de su ciclo. Los hooks se agrupan en dos fases bien delimitadas:
- Fase de build:
buildStartabre el proceso,resolveIddecide a qué archivo apunta cada especificador,loadentrega el contenido de un módulo,transformlo reescribe ymoduleParsedavisa cuando un módulo ya está en el grafo. - Fase de output:
renderChunkretoca cada chunk,generateBundleve el conjunto completo antes de escribirlo ywriteBundlecorre cuando los archivos ya están en disco.
Esa separación no es cosmética: los hooks de build ven módulos individuales mientras se construye el grafo, y los de output ven el resultado ya troceado. Un plugin bien escrito respeta esa frontera y sabe en qué fase vive cada una de sus responsabilidades.
// Un plugin identico en Rollup y en Rolldown
export default function miPlugin() {
return {
name: 'mi-plugin',
resolveId(source) {
if (source === 'virtual:config') return source
return null
},
load(id) {
if (id === 'virtual:config') return 'export const modo = "prod"'
return null
},
}
}
El objeto de contexto —lo que aparece como this dentro de un hook— también se conserva: this.resolve para delegar la resolución, this.emitFile para emitir un asset o un chunk, this.addWatchFile para observar dependencias, this.getModuleInfo para inspeccionar el grafo. Un módulo virtual —ese patrón de resolveId más load que sirve contenido que no existe en disco— funciona en Rolldown exactamente igual que en Rollup, porque el contrato del ciclo de vida es el mismo.
resolveId y load
Deciden a qué apunta un import y qué contenido tiene. La puerta de entrada de cada módulo al grafo, idéntica en ambos motores.
transform
Reescribe el código de cada módulo. El hook más llamado y, por eso, el que más cuida Rolldown en rendimiento.
renderChunk y generateBundle
Retocan la salida ya troceada. Ven los chunks finales antes de escribirlos, con la misma semántica que en Rollup.
Qué diverge y por qué
La compatibilidad es alta, pero no total, y las divergencias tienen casi siempre la misma raíz: el motor está en Rust y los plugins de terceros están en JavaScript. Cada vez que Rolldown llama a un hook escrito en JavaScript, cruza la frontera entre Rust y el runtime de JavaScript, y ese cruce tiene un coste. En un hook que se ejecuta una vez, como buildStart, el coste es irrelevante. En transform, que se llama por cada módulo del grafo —a veces decenas de miles—, ese ida y vuelta puede dominar el tiempo de build y borrar la ventaja de tener el núcleo en Rust.
De esa tensión nacen las tres divergencias que debes conocer. La primera son los filtros de hooks: Rolldown deja declarar, junto a un hook como transform, un filter con los patrones de módulos que le interesan. Así el núcleo en Rust decide sin cruzar la frontera si un módulo concreto justifica llamar a tu plugin, y salta los que no. Es una divergencia nacida del rendimiento, tan útil que el propio Rollup la adoptó después. La segunda son los plugins internos en Rust: utilidades muy usadas —alias, replace, la resolución estilo Node— se reimplementan de forma nativa dentro de Rolldown, de modo que su trabajo nunca sale del núcleo. La tercera son las transformaciones integradas: como Rolldown lleva Oxc dentro, transpila TypeScript y JSX sin necesidad de Babel ni de un plugin externo, y trae su propio minificador.
// Divergencia por rendimiento: un hook con filtro declarativo
export default function soloVue() {
return {
name: 'solo-vue',
transform: {
filter: { id: /\.vue$/ },
handler(code, id) {
// El nucleo en Rust ya descarto lo que no es .vue
return transformarVue(code)
},
},
}
}
Más allá de esas divergencias por rendimiento, Rolldown suma capacidades que Rollup nunca ofreció de serie y que absorben trabajo que antes exigía plugins externos:
- Transpilación integrada de TypeScript y JSX vía Oxc, sin
@rollup/plugin-babelni un paso previo con esbuild. - Minificador propio dentro del motor, en lugar de encadenar un plugin de minificación aparte.
- Soporte de HMR pensado para el uso dentro de Vite, algo ajeno al Rollup clásico de build.
- Troceado avanzado con control fino de cómo se agrupan los módulos en chunks.
Hay además opciones y casos límite de Rollup que Rolldown todavía no replica al cien por cien, y algún plugin muy acoplado a detalles internos de Rollup puede necesitar un ajuste. Pero la regla práctica se sostiene: la inmensa mayoría de los plugins funciona sin cambios, las divergencias viven en los bordes, y casi todas existen para ganar velocidad o para ofrecer de serie lo que antes exigía un plugin extra.
La forma sana de encarar la migración de un plugin, entonces, es esta: asume que funcionará sin cambios, verifica que así sea, y si algo falla busca primero en los tres focos conocidos —un hook muy acoplado a los internals de Rollup, una opción aún no replicada, o un choque con un plugin nativo— antes de sospechar de un problema profundo. La probabilidad de que un plugin corriente necesite trabajo real es baja, y esa baja probabilidad es precisamente la métrica de éxito de la compatibilidad.
flowchart LR plugin[Plugin en JavaScript] --> frontera[Frontera Rust y JS] frontera --> nucleo[Nucleo de Rolldown en Rust] filter[Filtro declarativo] --> nucleo nativo[Plugins nativos alias replace resolve] --> nucleo oxc[Transforms de Oxc TS y JSX] --> nucleo style nucleo fill:#94e2d5,color:#11111b style frontera fill:#f9e2af,color:#11111b style filter fill:#a6e3a1,color:#11111b
Rolldown demuestra con una precisión casi de laboratorio la tesis central del toolchain moderno: en software, la interfaz que la gente usa vale más que la implementación que hay debajo. La API de plugins de Rollup no era técnicamente perfecta, pero era la que todo el ecosistema conocía, y esa familiaridad la convirtió en un activo tan valioso que reescribir el motor entero mereció la pena solo si se conservaba. Piénsalo desde la economía del cambio: había miles de plugins, decenas de miles de proyectos y una generación de desarrolladores que habían internalizado resolveId, load, transform y generateBundle. Tirar todo eso para imponer una API nueva y teóricamente mejor habría sido un suicidio de adopción, por muy rápido que fuera el motor. Rolldown eligió el camino difícil pero correcto: reproducir la interfaz con fidelidad quirúrgica y absorber en el propio núcleo el coste de la compatibilidad. Y donde tuvo que divergir —los filtros de hooks, los plugins nativos, los transforms integrados— no lo hizo por capricho, sino empujado por la física del problema: cruzar la frontera entre Rust y JavaScript en un hook llamado decenas de miles de veces era el único punto donde la fidelidad literal habría matado el rendimiento que justificaba el proyecto. Fíjate en la elegancia de la solución: en vez de romper la interfaz, la extendió de forma retrocompatible con un filter opcional que los plugins viejos ignoran y los nuevos aprovechan, tan acertado que Rollup lo copió. Esa es la maestría que debes perseguir cuando diseñes cualquier sistema con usuarios: trata su forma de trabajar como un contrato que no puedes romper unilateralmente, mueve la complejidad hacia dentro de tu implementación en vez de exportarla a ellos, y cuando debas cambiar la interfaz, hazlo de forma aditiva para que lo viejo siga vivo. Quien entiende que la compatibilidad con Rollup no fue una concesión sino la estrategia entera, entiende por qué Rolldown ganó el ecosistema sin librar la guerra de reescribir sus plugins.
- Coge un plugin de Rollup que uses y cárgalo en una config de Rolldown; confirma que funciona sin cambios.
- Escribe un plugin con
resolveIdyloadque sirva un módulo virtual y verifica que el ciclo de vida es idéntico al de Rollup. - Añade un
filterde tipoida un hooktransformy razona qué llamadas al plugin ahorra el núcleo en Rust. - Localiza en la documentación de Rolldown un plugin que reimplemente de forma nativa lo que antes hacía un plugin de Rollup en JavaScript.
- Explica por qué el coste de cruzar la frontera Rust-JavaScript pesa en
transformpero no enbuildStart.