Patrones de Vite 8+ — CULTIVA Dashboard

Guía de configuración y optimización aplicada al panel de campañas de marketing IA

Vite 8 · TypeScript · React 18
⚙️

vite.config.ts — CULTIVA Dashboard (completo)

// vite.config.ts — CULTIVA Dashboard
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react-swc'
import checker from 'vite-plugin-checker'
import tsconfigPaths from 'vite-tsconfig-paths'

export default defineConfig(({ command, mode }) => {
  // Solo vars VITE_ — sin secretos expuestos
  const env = loadEnv(mode, process.cwd(), ['VITE_'])

  return {
    plugins: [
      react(),             // SWC: HMR más rápido que Babel
      tsconfigPaths(),     // alias de tsconfig.json → sin duplicar
      checker({ typescript: true }), // type-check en build
    ],

    server: {
      host: true,           // 0.0.0.0 — accesible en Docker
      port: 3000,
      hmr: { clientPort: 3000 },
      warmup: {
        clientFiles: ['./src/main.tsx', './src/routes/**/*.tsx'],
      },
      proxy: {
        '/api': {
          target: env.VITE_API_URL || 'http://localhost:8080',
          changeOrigin: true,
          rewrite: (p) => p.replace(/^\/api/, ''),
        },
      },
      fs: { allow: ['..', '../../packages'] }, // monorepo
    },

    optimizeDeps: {
      include: ['lodash-es', 'date-fns'], // CJS → ESM
    },

    build: {
      sourcemap: false,     // no leak de fuente en producción
      rolldownOptions: {
        output: {
          manualChunks: {
            'react-vendor': ['react', 'react-dom'],
            'ui-vendor': ['@radix-ui/react-dialog'],
          },
        },
      },
    },
  }
})

6 Problemas resueltos en CULTIVA Dashboard

ANTES: Dev server inaccesible en Docker
Vite vinculaba solo a localhost, imposible llegar desde el contenedor o red local.
FIX: server.host: true + hmr.clientPort: 3000
Ahora escucha en 0.0.0.0. El HMR funciona detrás del proxy inverso de Docker.
ANTES: Secretos expuestos al cliente
Variables sin prefijo eran accesibles en el bundle con loadEnv(mode, root, '').
FIX: loadEnv(mode, cwd(), ['VITE_'])
Solo vars VITE_ son públicas. Tokens de API y DB permanecen server-side.
ANTES: Barrel files lenteando HMR
src/components/index.ts re-exportaba 80+ componentes — Vite cargaba todos en cada cambio.
FIX: Imports directos + warmup.clientFiles
Eliminado barrel. Pre-transform de rutas calientes. HMR redujo de 820ms → 95ms.
🔌

Plugins esenciales para el stack CULTIVA

@vitejs/plugin-react-swc
HMR + Fast Refresh via SWC. Más rápido que la variante Babel. Default para React 18.
🔎
vite-plugin-checker
Ejecuta tsc en worker thread. Rellena el gap de type-check que vite build ignora.
🗂️
vite-tsconfig-paths
Lee paths de tsconfig.json. Evita duplicar 30+ aliases en vite.config.
📊
rollup-plugin-visualizer
Genera treemap del bundle. Usar con enforce: 'post' para auditorías de tamaño.
📦
vite-plugin-dts
Emite .d.ts en modo librería. Necesario si publicas el ui-kit como package npm.
🛠️
vite-plugin-inspect
Debug del pipeline de transforms. Útil para perfilar plugins lentos en dev.
🔐

Variables de entorno — Modelo seguro

Variable Lado Visibilidad Estado
VITE_API_URL Cliente Bundle público OK
VITE_POSTHOG_KEY Cliente Bundle público OK
VITE_APP_VERSION Cliente Bundle público OK
DATABASE_URL Servidor Solo server-side Server
OPENAI_API_KEY Servidor Solo server-side Server
STRIPE_SECRET_KEY Servidor Solo server-side NUNCA VITE_
# .env.local (gitignoreado)
VITE_API_URL=http://localhost:8080
VITE_POSTHOG_KEY=phc_cultiva_dev_xxx

# .env.production
VITE_API_URL=https://api.cultivaia.com
VITE_APP_VERSION=2.4.1

# Acceso en componentes
const url = import.meta.env.VITE_API_URL
const isDev = import.meta.env.DEV
📦

Bundle splitting

build: {
  rolldownOptions: {
    output: {
      manualChunks: {
        // vendores React aislados
        'react-vendor': [
          'react',
          'react-dom',
          'react/jsx-runtime',
        ],
        // UI separado de lógica
        'ui-vendor': [
          '@radix-ui/react-dialog',
          '@radix-ui/react-popover',
          'lucide-react',
        ],
        // utils pesados
        'utils-vendor': [
          'lodash-es',
          'date-fns',
          'zod',
        ],
      },
    },
  },
}
react-vendor.js 148 kB
ui-vendor.js 89 kB
index.js (app) 42 kB
🗂️

Monorepo + SSR externals

// Acceso monorepo a packages/
server: {
  fs: {
    allow: [
      '.',       // project root
      '..',      // workspace root
      '../../packages',
    ],
  },
},

// SSR: arreglar paquetes ESM-only
ssr: {
  noExternal: [
    '@cultivaia/ui-kit', // ESM-only
    'some-esm-dep',
  ],
  external: [
    'node-native-pkg', // keep as require()
  ],
  target: 'node',
},

// Pre-bundle deps CJS problemáticos
optimizeDeps: {
  include: [
    'lodash-es',
    'date-fns',
    'cjs-package',
  ],
}
🔥

HMR manual — state store

// src/stores/campaigns.ts
// Store de campañas con HMR preservado
let state = {
  campaigns: [] as Campaign[],
  activeFilter: 'all',
}

if (import.meta.hot) {
  // Recuperar estado previo al hot-reload
  const saved = import.meta.hot.data.state
  if (saved) state = saved

  // Guardar estado ANTES de reemplazar módulo
  import.meta.hot.dispose((data) => {
    data.state = state  // mutar, no reasignar
  })

  import.meta.hot.accept()
}

// Optimización: evitar barrel
// BAD:  import { useCampaigns } from '@/stores'
// GOOD: import { useCampaigns } from '@/stores/campaigns'

Anti-patrones — Lo que NO hacer

EVITAR
envPrefix: '' — expone TODAS las variables de entorno (incluyendo secretos) al bundle del cliente.
EVITAR
loadEnv(mode, root, '') — igual de peligroso. Usar siempre el tercer argumento con prefijos explícitos.
EVITAR
require('lib') en código fuente — Vite es ESM-first. Usar import. require() solo funciona en Node server-side.
EVITAR
Un chunk por package en manualChunks — crea cientos de ficheros pequeños, el browser corre el límite de conexiones HTTP.
EVITAR
vite preview como servidor de producción — es un smoke test local. Desplegar dist/ en NGINX, Cloudflare Pages o Vercel.
EVITAR
Asumir que vite build type-chequea — solo transpila. Errores de tipos llegan silenciosamente a producción sin vite-plugin-checker.
EVITAR
Reasignar import.meta.hot.data = {...} — hay que mutar propiedades. La reasignación rompe la persistencia de estado entre hot-reloads.

Quick Reference — Cuándo usar cada patrón

defineConfig Siempre — aporta inferencia de tipos
loadEnv(mode, root, ['VITE_']) Necesitas env vars en el config
vite-plugin-checker Cualquier app TypeScript (imprescindible)
vite-tsconfig-paths Ya tienes paths en tsconfig.json
optimizeDeps.include Deps CJS que causan errores de interop
server.proxy API backend separado en desarrollo
server.host: true Docker, contenedores, acceso remoto
server.warmup.clientFiles Pre-transformar rutas principales
build.lib + external Publicar packages npm
manualChunks (objeto) Separar vendor bundles de app code
vite --profile Dev server lento, perfilar plugins
vite build && vite preview Smoke-test del bundle local (no prod)
Opción config Default Descripción
root '.' Directorio raíz del proyecto
base '/' Ruta pública para assets desplegados
build.minify 'oxc' Minificador: oxc (def), terser, false
build.sourcemap false true | 'inline' | 'hidden' | false
envPrefix 'VITE_' Prefijo para vars públicas al cliente