wandres.dev
CONFIGURAR VITE · plugins y resolve

vite.config.ts y defineConfig: la forma de la configuración

La anatomía de vite.config.ts: por qué defineConfig solo aporta tipos, la config condicional por command y mode, la config asíncrona y la forma que toma la configuración de Vite 8 en 2026.

⏱ 17 min

Toda la potencia de Vite entra por un único punto: un vite.config.ts que exporta, por defecto, una configuración. Pero ese export no tiene por qué ser un objeto estático: puede ser una función del entorno, una promesa, o una función asíncrona que consulta el disco o la red antes de decidir. Entender que la configuración de Vite es un programa —evaluado una vez, en Node, antes de que arranque nada— es el salto que separa copiar recetas de diseñar builds. Esta lección abre el nivel con la forma misma de la config: su firma, sus variantes y el papel real, y modesto, de defineConfig.

🎯 Al terminar esta lección sabrás
  • Reconocer la anatomía de vite.config.ts y el papel de defineConfig.
  • Escribir config condicional en función de command y mode.
  • Usar la forma asíncrona para resolver configuración antes del arranque.
  • Situar la Environment API como la forma de la config en 2026.

defineConfig no configura: da tipos

El malentendido más común es creer que defineConfig hace algo en tiempo de ejecución. No hace nada: es la identidad. Recibe su argumento y lo devuelve tal cual. Su único valor es de tipos: envuelve la config para que TypeScript —y el editor— conozcan la forma exacta de cada clave, autocompleten opciones y marquen en rojo un nombre mal escrito antes de que Vite arranque. Podrías exportar el objeto pelado y Vite lo aceptaría igual; defineConfig solo te compra el autocompletado y la verificación estática de una superficie de API enorme.

// vite.config.ts
import { defineConfig } from "vite"

// Forma 1: objeto estatico. defineConfig solo aporta el tipo.
export default defineConfig({
  plugins: [],
  build: { sourcemap: true },
})

La firma admite cuatro formas, y ahí está su verdadera riqueza: un objeto de configuración; una función que recibe el entorno y devuelve un objeto; una promesa de objeto; o una función asíncrona. Vite normaliza las cuatro. Si el export es invocable, lo llama con el objeto ConfigEnv; si el resultado es una promesa, hace await. El resultado siempre acaba siendo el mismo objeto resuelto que Vite fusiona con sus defaults y con la config que aportan los plugins.

El orden de evaluación importa porque explica qué puedes hacer en cada forma. La config se evalúa una sola vez, en Node, antes de levantar el servidor o el build; no tienes aún ni módulos de la app cargados ni un window. Lo que sí tienes es el entorno del proceso y el ConfigEnv que Vite entrega.

flowchart TD
A[vite lee vite.config.ts] --> B[evalua el export default]
B --> C[si es funcion la llama con command y mode]
B --> D[si es objeto lo usa directo]
C --> E[si devuelve promesa hace await]
E --> F[config resuelta]
D --> F
F --> G[merge con defaults y config de plugins]
style F fill:#89b4fa,color:#11111b

Config condicional: una función de command y mode

La forma más útil en la práctica es la función. Vite la invoca con un objeto que describe la invocación: command distingue serve —arrancas el dev server— de build —empaquetas para producción—; mode es la etiqueta de modo, development o production por defecto, o la que pases por bandera; y dos banderas opcionales, isSsrBuild e isPreview, afinan el caso de SSR y el del servidor de vista previa.

import { defineConfig } from "vite"

export default defineConfig(({ command, mode, isSsrBuild }) => {
  const isProd = mode === "production"
  return {
    // command vale "serve" en dev y "build" al empaquetar
    define: {
      __DEV__: JSON.stringify(command === "serve"),
    },
    build: {
      sourcemap: !isProd,
      minify: isProd ? "esbuild" : false,
    },
  }
})

Con eso ramificas la config sin duplicar archivos. El patrón canónico es derivar un booleano de producción una vez y usarlo para decidir sourcemaps, minificación y define. La regla de oro: no leas process.env.NODE_ENV para esto. El mode de Vite es la fuente de verdad, y forzarlo con la bandera de modo te da modos a medida —un staging que compila como producción pero apunta a otra API— sin tocar una línea de la app.

💡
command y mode son ejes ortogonales

No confundas los dos. command responde a qué estás haciendo —servir o construir— y solo tiene dos valores. mode responde a para qué entorno —desarrollo, producción, staging— y es libre. Puedes construir en modo development para depurar el bundle, o servir en modo staging para probar contra otra API. Tratarlos como una sola variable es la causa de la mitad de las configs enredadas que existen.

Config asíncrona y la forma de 2026

Cuando la config depende de algo que solo se conoce al arrancar —flags remotos, un manifiesto generado, un secreto leído de un gestor— la función asíncrona es la herramienta. Devuelves una promesa, o marcas la función async y usas await, y Vite espera a que se resuelva antes de seguir. Es el gancho correcto para inyectar en define un valor calculado en vez de una constante escrita a mano.

import { defineConfig } from "vite"

export default defineConfig(async ({ mode }) => {
  // await de cualquier cosa: leer del disco, un fetch de flags, etc.
  const flags = await import("./config/flags.js").then((m) => m.load(mode))
  return {
    define: {
      __FLAGS__: JSON.stringify(flags),
    },
  }
})

La novedad estructural que se consolida en Vite 8 es la Environment API. Un build moderno rara vez tiene un solo destino: existe el entorno de cliente —navegador— y el de servidor —SSR—, y cada vez más un tercero —edge o workers—. La clave environments deja declarar cada uno con su propio resolve, sus conditions y su salida, en lugar de exprimir todo por las opciones planas heredadas. La config plana sigue funcionando y se mapea al entorno de cliente; la forma explícita, por entornos, es la de 2026.

export default defineConfig({
  environments: {
    client: {
      resolve: { conditions: ["browser"] },
    },
    ssr: {
      resolve: { conditions: ["node", "module"] },
    },
  },
})

Componer configs: mergeConfig y una base compartida

En un monorepo, repetir la misma config en cada app es la misma deriva que combatiste con los catalogs, ahora en la capa de build: la respuesta es una configuración base compartida que cada app extiende. Vite trae mergeConfig para fusionar dos configuraciones con las reglas correctas —concatena arrays como plugins, funde objetos en profundidad— en vez del reemplazo ingenuo que haría un spread manual.

// vite.config.base.ts
import { defineConfig } from "vite"

export const base = defineConfig({
  build: { sourcemap: true },
  resolve: { dedupe: ["react", "react-dom"] },
})

// apps/web/vite.config.ts
import { defineConfig, mergeConfig } from "vite"
import { base } from "../../vite.config.base"

export default mergeConfig(
  base,
  defineConfig({
    server: { port: 5180 },
  }),
)

Ese mergeConfig no es un Object.assign: entiende la semántica de cada clave. Fundir plugins con reemplazo perdería los del base; fundirlos con concatenación los preserva. Por eso usar la utilidad en vez de un spread es la diferencia entre una base que compone y una que pisa en silencio lo que creías heredar.

Y hay un tercer contribuyente a la config final que conviene no olvidar: los plugins. Un plugin puede aportar su propia configuración mediante el hook config, que Vite funde con la tuya bajo las mismas reglas. La config que Vite acaba ejecutando no es solo la que escribiste, sino la fusión de tu base, tu config de app y lo que cada plugin inyecta, resuelta en un orden definido.

📝
Inspecciona la config resuelta, no la que crees tener

Cuando el build hace algo que tu vite.config.ts no parece pedir, no lo deduzcas: míralo. Entre la config base, la de app y la de cada plugin, lo que Vite ejecuta puede diferir de lo que leíste en un solo archivo. Arranca con la bandera de depuración para volcar la configuración resuelta y confirma qué plugins, qué resolve y qué define ganaron de verdad. Instrumentar la config real que corre, en vez de la que imaginas, es la misma disciplina que gobierna todo el nivel.

La configuración de Vite es un programa, no un archivo de datos

El error de fondo del principiante es leer vite.config.ts como un .json con azúcar: una tabla de valores que Vite consume. No lo es. Es código que Vite ejecuta en Node, una sola vez, antes de que exista tu aplicación, y cuyo trabajo es devolver la configuración como resultado de una función del entorno. Esa diferencia de categoría lo cambia todo. Si la config es una función pura de command y mode, tu build es reproducible y auditable: dado el mismo entorno, siempre sale la misma configuración, y puedes razonar sobre ella como razonas sobre cualquier función. Si en cambio salpicas efectos secundarios, lees el reloj o mutas variables globales, conviertes tu build en algo no determinista, y los bugs de build no determinista son de los más caros que existen porque no se reproducen a voluntad. La forma asíncrona y la Environment API no son adornos: son el reconocimiento de que un build serio tiene varios destinos y depende de datos que solo se saben al arrancar, y de que el lugar correcto para resolver esa complejidad es una función bien tipada, no un objeto plano copiado de un blog. defineConfig no configura nada porque no le toca: su humildad es la pista de que el trabajo de verdad —decidir, en función del entorno, qué build quieres— es tuyo, y es de diseño.

⚔️ Convierte tu config en una función
  1. Toma un vite.config.ts con un objeto plano y envuélvelo en defineConfig para ver el autocompletado de cada clave.
  2. Reescríbelo como función que recibe command y mode, y deriva un booleano isProd que gobierne build.sourcemap y build.minify.
  3. Arranca con un modo a medida por bandera —por ejemplo staging— y comprueba con un console.log del mode que la rama correcta se activa.
  4. Añade una función async que lea un archivo de flags del disco e inyéctalo en define.
  5. Declara dos entornos, client y ssr, con conditions distintas, y explica en una frase por qué la config plana ya no basta.