El modelo de plugins de Vite
Vite no es un monolito: es un núcleo pequeño sobre un sistema de plugins que reutiliza la interfaz de Rollup y la extiende con hooks propios. Anatomía de un plugin como objeto con name y hooks, el eje enforce con sus tres posiciones pre, normal y post, el campo apply para acotar a serve o build, y qué cambia con Rolldown y los hook filters en Vite 8 (2026).
Vite no es un monolito con funciones cableadas por dentro; es un núcleo diminuto y un sistema de plugins que hace casi todo el trabajo real. Y ese sistema no lo inventó Vite: reutiliza la interfaz de plugins de Rollup y la amplía con hooks propios para el dev server, el HTML de entrada y el HMR. Un plugin de Vite es, literalmente, un objeto con un name y un puñado de funciones-hook. Entender esa arquitectura —qué hereda de Rollup, qué añade encima, y en qué orden corre todo— es la llave que abre el nivel entero.
- Ver un plugin como un objeto con
namey hooks, superconjunto del plugin de Rollup. - Distinguir los hooks heredados de Rollup de los que Vite añade por su cuenta.
- Dominar el eje
enforcecon sus tres posiciones: pre, normal y post. - Usar
applypara acotar un plugin a la fase deserveo a la debuild.
Un plugin es un objeto con hooks
La unidad mínima de extensión en Vite no es una clase ni un framework de plugins con ciclo de vida propio: es un objeto plano. Lo único obligatorio es name, una cadena que identifica al plugin en los mensajes de error y en las trazas. Todo lo demás son hooks, funciones que Vite invoca en momentos concretos del build y del servidor. Por convención se envuelve ese objeto en una función factoría que acepta opciones y devuelve el plugin, para poder parametrizarlo al configurarlo.
// mi-plugin.ts
import type { Plugin } from 'vite'
export function miPlugin(opciones = {}): Plugin {
return {
name: 'mi-plugin',
enforce: 'pre',
buildStart() {
this.warn('el build acaba de empezar')
},
transform(code, id) {
if (!id.endsWith('.especial')) return null
return { code: reescribir(code), map: null }
},
}
}
La clave conceptual: un plugin de Vite es un superconjunto de un plugin de Rollup. Todos los hooks de build de Rollup —options, buildStart, resolveId, load, transform, buildEnd— y los de generación de salida —renderStart, renderChunk, generateBundle, writeBundle— funcionan igual dentro de un plugin de Vite. Encima de esa base, Vite inyecta hooks que Rollup no tiene porque no le hacen falta a un bundler puro: config, configResolved, configureServer, transformIndexHtml y handleHotUpdate. Los primeros los estudiaremos en la lección 2; los segundos, en la 3.
Una factoría no está obligada a devolver un único objeto: puede devolver un array de plugins, y Vite lo aplana. Es el patrón de los plugins compuestos, como los de frameworks, que internamente son varios plugins con distintos enforce colaborando bajo un mismo paquete. Devolver false o null en una posición del array descarta ese plugin de forma condicional, sin romper el resto.
En Vite 8, el bundler por debajo ya no es Rollup sino Rolldown, escrito en Rust dentro del proyecto Oxc. Esto no rompe nada: Rolldown implementa deliberadamente la misma interfaz de plugins de Rollup, así que la práctica totalidad del ecosistema sigue funcionando sin cambios. Lo que sí aparece es un mecanismo nuevo, los hook filters: puedes declarar de forma estática qué módulos le interesan a un hook, y Rolldown se ahorra cruzar la frontera entre Rust y JavaScript para los módulos que no encajan. Es una optimización que solo tiene sentido cuando el núcleo es nativo.
El orden lo es todo: enforce
Con decenas de plugins activos, la pregunta crítica deja de ser qué hace cada uno y pasa a ser en qué orden corren. Vite resuelve esto con el campo enforce, que admite tres valores: 'pre', nada (normal) y 'post'. No es una prioridad numérica libre, sino tres cubos ordenados, y dentro de cada cubo los plugins respetan el orden del array de configuración.
flowchart TD A[alias] --> B[plugins con enforce pre] B --> C[plugins nucleo de Vite] C --> D[plugins normales sin enforce] D --> E[plugins de build de Vite] E --> F[plugins con enforce post] F --> G[plugins post build de Vite]
La tubería completa intercala tus plugins con los del propio Vite. Primero se resuelven los alias; luego corren tus plugins pre, antes que el núcleo de Vite, justo donde debe ir un plugin de framework que necesita transformar el fuente antes de que Vite lo toque. Después van los plugins del núcleo, luego tus plugins normales, luego los de build de Vite, y al final tus plugins post, cuya salida ya no verá ningún plugin normal. Los plugins post build de Vite —minificación, manifest, informe de tamaños— cierran la marcha.
enforce: pre
Corre antes del núcleo de Vite. El sitio de los plugins de framework y de cualquier transformación que deba ver el fuente sin tocar.
sin enforce (normal)
El comportamiento por defecto. Corre después del núcleo de Vite y antes de sus plugins de build. La posición sensata para la mayoría.
enforce: post
Corre después de las transformaciones de Vite. Para plugins que quieren la última palabra sobre el código ya procesado.
order por hook
Rollup permite afinar más: un hook puede ser { order, handler } para reordenar solo esa fase, sin mover el plugin entero de cubo.
El enforce de plugin es un instrumento grueso; a veces necesitas que solo un hook concreto se adelante o se atrase. Para eso, cualquier hook puede escribirse en su forma de objeto, con order y handler separados. Es más quirúrgico que mover el plugin entero:
export function miPlugin(): Plugin {
return {
name: 'mi-plugin',
transform: {
order: 'pre',
handler(code, id) {
return null
},
},
}
}
La tentación de marcar todo como pre para asegurarte de ir primero es una trampa. Si dos plugins compiten por ir antes, pre deja de ordenarlos y vuelves al orden del array, solo que ahora todo corre antes del núcleo de Vite y pierdes sus transformaciones. Usa enforce cuando tengas una razón concreta —transformar fuente crudo, o intervenir salida ya procesada— y déjalo sin marcar en cuanto no la tengas. La configuración de orden más robusta es la que apenas usa el eje.
apply y la doble naturaleza serve o build
Vite vive dos vidas: un dev server que sirve ESM sin empaquetar y un build de producción que sí empaqueta. Un plugin puede tener sentido en una y no en la otra. El campo apply acota su alcance: acepta 'serve', 'build' o una función que decide según la configuración y el entorno resueltos.
export function soloEnBuild(): Plugin {
return {
name: 'solo-en-build',
apply: 'build',
}
}
export function condicional(): Plugin {
return {
name: 'condicional',
apply(config, { command }) {
return command === 'build' && !config.build?.ssr
},
}
}
Un plugin sin apply corre en ambas fases, que es lo correcto para la mayoría: los hooks de Rollup como transform se ejecutan tanto en el dev server —de forma perezosa, módulo a módulo— como en el build. La razón para acotar suele ser el coste o la seguridad: una transformación cara que solo aporta en producción se marca apply: 'build'; un middleware de depuración, apply: 'serve'. En Vite 8, además, cada hook recibe el entorno activo por this.environment, de modo que un mismo plugin puede comportarse distinto en el entorno de cliente y en el de servidor sin duplicarse.
Un fallo recurrente es escribir un plugin probándolo solo con vite dev y descubrir en producción que su hook transform se comporta distinto, o que depende de estado que en vite build no existe. Recuerda que el mismo transform corre en las dos fases, sobre grafos de módulos construidos de forma diferente. Si tu plugin solo tiene sentido en una, decláralo con apply en vez de confiar en que nunca se ejecutará en la otra; lo segundo es una bomba de relojería.
La decisión de arquitectura más importante de Vite no fue ser rápido en desarrollo, sino no reinventar la interfaz de extensión. Al adoptar la de Rollup, Vite heredó de golpe un ecosistema entero de plugins ya escritos y, más profundo aún, un modelo mental que miles de personas ya conocían. Un plugin no es un objeto mágico: es un contrato de hooks bien definidos que se ejecutan en un orden bien definido. Esa doble apuesta —reutilizar el contrato de Rollup y añadirle solo lo que un bundler no cubre, el dev server y el HMR— es la que permitió que Vite 8, al cambiar su motor de Rollup a Rolldown en Rust, no rompiera el ecosistema: Rolldown implementa el mismo contrato, así que los plugins siguen encajando aunque por debajo cambie el lenguaje entero. Aquí está la lección duradera del nivel: cuando diseñas la extensibilidad como un conjunto pequeño de hooks ordenados en lugar de como una API monolítica, puedes reescribir el núcleo sin traicionar a quien construyó encima. El enforce y el apply no son detalles de configuración; son la forma en que ese contrato expresa el tiempo —cuándo corro— y el espacio —en qué fase corro— de cada extensión. Dominarlos es dejar de pelear contra el orden de los plugins y empezar a componerlos.
- Escribe una factoría que devuelva un
Pluginconnamey un hookbuildStartque registre un mensaje, y confirma que aparece al arrancar. - Añade dos plugins triviales, uno con
enforce: 'pre'y otro sin enforce, y observa en qué orden corren susbuildStart. - Convierte el
transformde uno de ellos a su forma de objeto{ order, handler }y razona en qué se diferencia de mover el plugin de cubo. - Marca un plugin con
apply: 'build'y comprueba que no corre bajovite devpero sí bajovite build. - Explica con tus palabras por qué el cambio de Rollup a Rolldown en Vite 8 no rompió los plugins existentes.