Los hooks propios de Vite
Lo que Vite añade sobre Rollup: config y configResolved para leer y moldear la configuración, configureServer para inyectar middlewares en el dev server, transformIndexHtml para intervenir el punto de entrada HTML, y handleHotUpdate junto al nuevo hotUpdate por entorno de la Environment API en Vite 8. Los hooks que convierten un bundler en una herramienta de desarrollo.
Rollup es un bundler: sabe de módulos, chunks y salida. No sabe nada de un servidor de desarrollo, de un index.html ni de recarga en caliente, porque no es su trabajo. Todo eso lo aporta Vite, y lo hace con cinco hooks propios que no existen en Rollup: config y configResolved para la configuración, configureServer para el dev server, transformIndexHtml para el HTML de entrada y handleHotUpdate para el HMR. Son los hooks que convierten un empaquetador en una herramienta de desarrollo completa, y por eso son exclusivos de Vite.
- Leer y moldear la configuración con
configyconfigResolved. - Inyectar middlewares en el dev server con
configureServer. - Transformar el punto de entrada con
transformIndexHtmly sus descriptores de etiqueta. - Personalizar el HMR con
handleHotUpdatey situar el nuevohotUpdatepor entorno.
config y configResolved: leer y moldear
El primer par de hooks rodea el momento en que Vite resuelve su configuración. config(config, env) corre antes de la resolución: recibe la configuración del usuario y un entorno con command y mode, y puede devolver un objeto parcial que Vite fusionará en profundidad con el resto, o mutar el objeto recibido. Es el sitio para fijar valores por defecto o registrar dependencias que deben pre-empaquetarse.
config(config, { command }) {
return {
optimizeDeps: {
include: ['libreria-pesada'],
},
define: {
__ES_BUILD__: JSON.stringify(command === 'build'),
},
}
}
Su compañero, configResolved(resolved), corre después: recibe la configuración final, ya fusionada y normalizada, y es de solo lectura. Su uso canónico es guardar esa configuración en una variable del plugin para consultarla desde otros hooks —el root, el command, el base, la lista de plugins activos—. Devolver algo aquí no tiene efecto; el momento de influir ya pasó.
let configResuelta
export function miPlugin(): Plugin {
return {
name: 'mi-plugin',
config(config, env) {
return { define: { __MODO__: JSON.stringify(env.mode) } }
},
configResolved(resolved) {
configResuelta = resolved
},
transform(code, id) {
if (configResuelta.command === 'build') {
// camino solo de producción
}
return null
},
}
}
La asimetría entre ambos hooks no es arbitraria: config es el único punto donde todavía puedes cambiar la configuración, porque corre antes de que Vite la congele; configResolved es el punto donde ya está congelada y solo puedes leerla. Confundirlos —intentar mutar en configResolved, o leer valores finales en config que aún no existen— es un error frecuente. La regla mnemotécnica: en config propones, en configResolved te enteras de lo que se decidió.
configureServer: middlewares en el dev server
configureServer(server) te entrega el ViteDevServer, el objeto que gobierna el servidor de desarrollo. Su uso más común es añadir middleware de estilo Connect a la pila que atiende cada petición, para servir un endpoint propio, un mock de API o una página de depuración. El servidor expone además server.ws para hablar por WebSocket con el cliente, server.watcher para observar el sistema de archivos, y server.moduleGraph para inspeccionar el grafo en vivo.
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url === '/__estado') {
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ ok: true }))
return
}
next()
})
}
Hay un matiz de orden importante. Si añades el middleware directamente, se instala antes que los middlewares internos de Vite. Si en cambio devuelves una función, Vite la ejecuta después de instalar los suyos, lo que te deja actuar como último eslabón —útil para un catch-all que solo debe responder cuando nada más lo hizo—. Es el mismo patrón pre/post del enforce, aplicado aquí a la pila de middlewares.
configureServer(server) {
return () => {
server.middlewares.use((req, res, next) => {
// corre despues de los middlewares internos de Vite
next()
})
}
}
transformIndexHtml: el punto de entrada HTML
En Vite, el index.html no es un archivo estático servido tal cual: es un módulo de primera clase, y transformIndexHtml(html, ctx) es el hook que lo interviene. Puede devolver el HTML como cadena ya modificada, o —lo idiomático— un array de descriptores de etiqueta que Vite inserta en la posición que indiques. Esto evita manipular el HTML con expresiones regulares y deja que Vite se encargue de colocarlo bien.
transformIndexHtml(html, ctx) {
return [
{
tag: 'script',
attrs: { type: 'application/json', id: 'estado-inicial' },
children: JSON.stringify({ version: '8' }),
injectTo: 'head',
},
{
tag: 'link',
attrs: { rel: 'preconnect', href: 'https://cdn.ejemplo' },
injectTo: 'head-prepend',
},
]
}
El campo injectTo admite 'head', 'body', 'head-prepend' y 'body-prepend', cubriendo las cuatro posiciones útiles. El hook tiene también su forma de objeto { order, handler }, donde order puede ser 'pre' o 'post', por si necesitas inyectar antes o después de otros plugins que también tocan el HTML. El contexto ctx te da el server en desarrollo, y en build el bundle y el chunk, para que puedas inyectar rutas de assets ya hasheadas.
handleHotUpdate y el nuevo hotUpdate
El último hook propio gobierna el HMR. Cuando cambia un archivo, handleHotUpdate(ctx) recibe un contexto con ctx.file —la ruta que cambió—, ctx.modules —los módulos del grafo afectados—, ctx.read —una función para leer el contenido nuevo— y ctx.server. Puedes devolver una lista filtrada de módulos para acotar qué se actualiza, devolver un array vacío para tomar el control manual, o enviar un evento propio por el WebSocket y que el cliente reaccione a medida.
handleHotUpdate({ file, server, modules }) {
if (file.endsWith('.datos.json')) {
server.ws.send({ type: 'custom', event: 'datos-actualizados' })
return []
}
return modules
}
flowchart TD A[config propone configuracion] --> B[configResolved observa la final] B --> C[configureServer monta middlewares] C --> D[transformIndexHtml inyecta en el HTML] D --> E[cambia un archivo] E --> F[handleHotUpdate o hotUpdate decide el HMR] F --> E
En Vite 8, la Environment API reordena este último hook. El clásico handleHotUpdate sigue funcionando, pero está pensado para un solo grafo de módulos, y hoy Vite maneja varios entornos —cliente, servidor, y los que definas— cada uno con su grafo. Su sustituto es hotUpdate, que se ejecuta una vez por entorno y expone this.environment para saber en cuál estás, además de un ctx.type que distingue creación, actualización y borrado del archivo. Para plugins nuevos que operan en escenarios con SSR o múltiples entornos, hotUpdate es la forma correcta; handleHotUpdate queda para el caso de un único entorno y por compatibilidad.
Con la Environment API, un mismo archivo puede pertenecer al grafo del cliente y al del servidor a la vez, con módulos distintos en cada uno. Si escribes lógica de HMR con handleHotUpdate asumiendo un grafo único, en un proyecto con SSR puedes invalidar el módulo equivocado o disparar la recarga en el entorno que no era. Migrar a hotUpdate y ramificar por this.environment.name no es cosmético: es lo que mantiene coherente la recarga cuando hay más de un grafo en juego.
Hay una lección de diseño escondida en el hecho de que estos hooks no existan en Rollup. Rollup se detiene en el límite de su responsabilidad: convertir un grafo de módulos en artefactos. No servir peticiones, no parsear HTML, no recargar el navegador. Vite no metió esas capacidades dentro de Rollup ni las cableó en su núcleo; las expuso como cinco hooks que cualquier plugin puede usar, respetando el mismo contrato de objeto con funciones que ya conocías. El resultado es que la frontera entre lo que es empaquetar y lo que es desarrollar queda dibujada con precisión: config y configResolved gestionan el estado, configureServer abre el servidor, transformIndexHtml reclama el HTML como territorio de plugins, y handleHotUpdate —hoy hotUpdate por entorno— pone la recarga bajo tu control. Cuando Vite 8 introdujo la Environment API y multiplicó los grafos, no tuvo que rediseñar el modelo entero: bastó con añadir un hook por entorno junto al clásico, porque la arquitectura de hooks ya era lo bastante fina para absorber el cambio. Esa es la señal de una buena API de extensión: crece añadiendo hooks, no rompiendo los que ya usabas. Dominar estos cinco es dejar de ver Vite como una caja que sirve tu app y empezar a verlo como una plataforma sobre la que puedes construir la tuya.
- Escribe un
configque activeoptimizeDeps.includepara una dependencia y confirma en el arranque que se pre-empaqueta. - Guarda la configuración en
configResolvedy úsala en untransformpara ramificar entreserveybuild. - Añade un middleware en
configureServerque responda a una ruta propia, y luego conviértelo en post devolviendo una función. - Inyecta con
transformIndexHtmluna etiquetascriptcon estado inicial en elheadusando descriptores, sin tocar el HTML a mano. - Escribe un
handleHotUpdateque emita un evento propio porserver.wsy razona qué ganarías migrándolo ahotUpdatepor entorno.