323 lines
13 KiB
TypeScript
323 lines
13 KiB
TypeScript
import { defineConfig } from "vite";
|
|
import react from "@vitejs/plugin-react-swc";
|
|
import path from "path";
|
|
import { manifestPlugin } from "./vite-plugin-manifest";
|
|
import { fixHtmlReferencesPlugin } from "./vite-plugin-fix-html-references";
|
|
import { stripPreloadWarningPlugin } from "./vite-plugin-strip-preload-warning";
|
|
/// <reference types="vitest" />
|
|
|
|
// Detectar si estamos en GitHub Pages
|
|
// GitHub Pages usa el formato: https://username.github.io/repository-name/
|
|
const isGitHubPages = process.env.GITHUB_PAGES === 'true';
|
|
const repositoryName = process.env.GITHUB_REPOSITORY_NAME || 'guia-tes-digital';
|
|
const base = isGitHubPages ? `/${repositoryName}/` : '/';
|
|
|
|
// https://vitejs.dev/config/
|
|
export default defineConfig({
|
|
// Base path para GitHub Pages (necesario para rutas SPA)
|
|
base: base,
|
|
server: {
|
|
host: "::",
|
|
port: 8096,
|
|
fs: {
|
|
// Permitir acceso a archivos fuera del proyecto si es necesario
|
|
strict: true,
|
|
},
|
|
// SPA fallback: todas las rutas no encontradas redirigen a index.html
|
|
// Esto permite que React Router maneje el enrutamiento del lado del cliente
|
|
middlewareMode: false,
|
|
},
|
|
preview: {
|
|
port: 4173,
|
|
// Configurar preview para SPA routing
|
|
// Esto asegura que el servidor de preview también maneje rutas correctamente
|
|
},
|
|
plugins: [
|
|
react(),
|
|
manifestPlugin(),
|
|
fixHtmlReferencesPlugin(), // Corregir referencias en index.html después del build
|
|
stripPreloadWarningPlugin(), // En dev: quitar preload que provocan advertencia "not used"
|
|
],
|
|
resolve: {
|
|
alias: {
|
|
"@": path.resolve(__dirname, "./src"),
|
|
"@shared/schemas": path.resolve(__dirname, "./backend/src/shared/schemas/index.ts"),
|
|
// CRÍTICO: Forzar alias de React para asegurar una sola instancia
|
|
// Esto previene múltiples instancias de React en el bundle
|
|
"react": path.resolve(__dirname, "./node_modules/react"),
|
|
"react-dom": path.resolve(__dirname, "./node_modules/react-dom"),
|
|
"react/jsx-runtime": path.resolve(__dirname, "./node_modules/react/jsx-runtime.js"),
|
|
},
|
|
// CRÍTICO: Forzar deduplicación de React para evitar errores useLayoutEffect
|
|
// Esto asegura que solo hay una instancia de React en el bundle
|
|
dedupe: ["react", "react-dom", "react/jsx-runtime"],
|
|
// Asegurar que React se resuelve correctamente
|
|
conditions: ["import", "module", "browser", "default"],
|
|
},
|
|
// Configuración para procesar archivos .md e imágenes
|
|
assetsInclude: ["**/*.md", "**/*.png", "**/*.jpg", "**/*.jpeg", "**/*.svg", "**/*.gif"],
|
|
|
|
// Configuración de build para incluir archivos .md e imágenes
|
|
build: {
|
|
// Evitar preload de módulos que generan advertencia "preloaded but not used"
|
|
modulePreload: false,
|
|
// Ajustar límite de warning para chunks grandes (vendor-react es grande pero necesario)
|
|
chunkSizeWarningLimit: 1000, // 1MB - vendor-react puede ser grande pero es necesario
|
|
// Minificar y optimizar
|
|
minify: 'esbuild',
|
|
// Generar source maps para producción (útil para debugging)
|
|
sourcemap: false, // Desactivado para reducir tamaño, activar si necesitas debugging
|
|
// Optimizar assets
|
|
assetsInlineLimit: 4096, // Inline assets < 4KB como base64
|
|
rollupOptions: {
|
|
// Code splitting: dividir el bundle en chunks más pequeños
|
|
output: {
|
|
// CRÍTICO: Asegurar orden de carga correcto usando prefijos numéricos
|
|
// El navegador carga los scripts en orden alfabético, así que usamos números
|
|
entryFileNames: 'assets/[name]-[hash].js',
|
|
chunkFileNames: (chunkInfo) => {
|
|
// CRÍTICO: Numerar chunks para garantizar orden de carga alfabético
|
|
// 0 = vendor-react (debe cargarse PRIMERO - orden alfabético)
|
|
// 1 = vendor-utils (carga DESPUÉS de vendor-react)
|
|
// 2 = vendor-markdown (carga después)
|
|
if (chunkInfo.name === 'vendor-react') {
|
|
return 'assets/0-vendor-react-[hash].js';
|
|
}
|
|
if (chunkInfo.name === 'vendor-utils') {
|
|
return 'assets/1-vendor-utils-[hash].js';
|
|
}
|
|
if (chunkInfo.name === 'vendor-markdown') {
|
|
return 'assets/2-vendor-markdown-[hash].js';
|
|
}
|
|
return 'assets/[name]-[hash].js';
|
|
},
|
|
manualChunks: (id) => {
|
|
// Separar node_modules en chunks por librería
|
|
if (id.includes('node_modules')) {
|
|
// SOLUCIÓN DRÁSTICA: Poner TODO lo relacionado con React en un solo chunk
|
|
// Esto garantiza que React esté disponible antes de cualquier otro código
|
|
// IMPORTANTE: Añadir TODAS las dependencias que usan React aquí
|
|
if (
|
|
id.includes('react') ||
|
|
id.includes('react-dom') ||
|
|
id.includes('scheduler') ||
|
|
id.includes('react-router') ||
|
|
id.includes('@radix-ui') ||
|
|
id.includes('@tanstack') ||
|
|
id.includes('lucide-react') ||
|
|
id.includes('recharts') ||
|
|
id.includes('react-hook-form') ||
|
|
id.includes('@hookform') ||
|
|
id.includes('react-day-picker') ||
|
|
id.includes('embla-carousel-react') ||
|
|
id.includes('next-themes') ||
|
|
id.includes('sonner') ||
|
|
id.includes('react-resizable-panels') ||
|
|
id.includes('input-otp') ||
|
|
id.includes('cmdk') ||
|
|
id.includes('vaul') ||
|
|
id.includes('react-markdown') ||
|
|
id.includes('@floating-ui') ||
|
|
id.includes('@remix-run/router') ||
|
|
id.includes('use-callback-ref') ||
|
|
id.includes('use-sidecar') ||
|
|
id.includes('aria-hidden')
|
|
) {
|
|
return 'vendor-react';
|
|
}
|
|
// CRÍTICO: hast-util-to-jsx-runtime USA React - debe estar en vendor-react
|
|
if (id.includes('hast-util-to-jsx-runtime')) {
|
|
return 'vendor-react';
|
|
}
|
|
// Markdown y procesamiento de texto (NO usa React directamente)
|
|
if (
|
|
id.includes('remark') ||
|
|
id.includes('rehype') ||
|
|
id.includes('unified') ||
|
|
id.includes('micromark') ||
|
|
id.includes('mdast') ||
|
|
id.includes('lowlight') // Syntax highlighting para markdown
|
|
) {
|
|
return 'vendor-markdown';
|
|
}
|
|
// Utilidades que NO usan React
|
|
if (
|
|
id.includes('zod') ||
|
|
id.includes('date-fns') ||
|
|
id.includes('clsx') ||
|
|
id.includes('tailwind-merge') ||
|
|
id.includes('class-variance-authority') ||
|
|
id.includes('dompurify') ||
|
|
id.includes('highlight.js') ||
|
|
id.includes('hast-util') ||
|
|
id.includes('unist-util') ||
|
|
id.includes('vfile') ||
|
|
id.includes('parse5') ||
|
|
id.includes('entities') ||
|
|
id.includes('property-information') ||
|
|
id.includes('style-to-js') ||
|
|
id.includes('style-to-object') ||
|
|
id.includes('trough') ||
|
|
id.includes('bail') ||
|
|
id.includes('extend') ||
|
|
id.includes('is-plain-obj') ||
|
|
id.includes('zwitch') ||
|
|
id.includes('web-namespaces') ||
|
|
id.includes('html-void-elements') ||
|
|
id.includes('html-url-attributes') ||
|
|
id.includes('comma-separated-tokens') ||
|
|
id.includes('space-separated-tokens') ||
|
|
id.includes('estree-util') ||
|
|
id.includes('decode-named-character-reference') ||
|
|
id.includes('ccount') ||
|
|
id.includes('markdown-table') ||
|
|
id.includes('format') ||
|
|
id.includes('hastscript') ||
|
|
id.includes('vfile-location') ||
|
|
id.includes('vfile-message') ||
|
|
id.includes('unist-util-stringify-position') ||
|
|
id.includes('unist-util-is') ||
|
|
id.includes('unist-util-find-after') ||
|
|
id.includes('unist-util-visit-parents') ||
|
|
id.includes('trim-lines') ||
|
|
id.includes('longest-streak') ||
|
|
id.includes('hast-util-parse-selector') ||
|
|
id.includes('detect-node-es') ||
|
|
id.includes('get-nonce') ||
|
|
id.includes('devlop') ||
|
|
id.includes('fault') ||
|
|
id.includes('hast-util-from-parse5') ||
|
|
id.includes('hast-util-to-parse5') ||
|
|
id.includes('inline-style-parser') ||
|
|
id.includes('tslib')
|
|
) {
|
|
return 'vendor-utils';
|
|
}
|
|
// CRÍTICO: @ungap/structured-clone puede ser usado por librerías React
|
|
// Moverlo a vendor-react para asegurar que React está disponible
|
|
if (id.includes('@ungap/structured-clone')) {
|
|
return 'vendor-react';
|
|
}
|
|
// CRÍTICO: Si llegamos aquí, algo se nos escapó
|
|
// Por seguridad, mover TODO a vendor-react si podría usar React
|
|
// Esto previene que cualquier código desconocido use React antes de tiempo
|
|
// En producción, esto NO debería ocurrir - todos los módulos deberían estar clasificados
|
|
if (process.env.NODE_ENV === 'production') {
|
|
console.error('[Vite] ERROR: Unclassified dependency in production:', id);
|
|
// En producción, si no está clasificado, moverlo a vendor-react por seguridad
|
|
// Es mejor tener código extra en vendor-react que tener errores useLayoutEffect
|
|
return 'vendor-react';
|
|
} else {
|
|
console.warn('[Vite] Unclassified dependency:', id);
|
|
// En desarrollo, mover a vendor-utils para debugging
|
|
return 'vendor-utils';
|
|
}
|
|
}
|
|
|
|
// Separar páginas en chunks individuales
|
|
if (id.includes('/src/pages/')) {
|
|
const pageName = id.split('/src/pages/')[1]?.split('.')[0];
|
|
if (pageName) {
|
|
// ManualViewer es muy grande, mantenerlo separado
|
|
if (pageName === 'ManualViewer') {
|
|
return 'page-manual-viewer';
|
|
}
|
|
return `page-${pageName.toLowerCase()}`;
|
|
}
|
|
}
|
|
|
|
// Separar componentes grandes
|
|
if (id.includes('/src/components/')) {
|
|
// MarkdownViewer es grande (usa react-markdown)
|
|
if (id.includes('MarkdownViewer')) {
|
|
return 'component-markdown';
|
|
}
|
|
}
|
|
},
|
|
assetFileNames: (assetInfo) => {
|
|
const name = assetInfo.name || '';
|
|
|
|
// Mantener estructura de carpetas para archivos .md en public/
|
|
if (name.endsWith('.md')) {
|
|
if (name.includes('manual')) {
|
|
return 'manual/[name][extname]';
|
|
}
|
|
return 'assets/[name]-[hash][extname]';
|
|
}
|
|
|
|
// Mantener estructura de carpetas para imágenes en public/assets/
|
|
if (name.match(/\.(png|jpg|jpeg|svg|gif)$/i)) {
|
|
// Si está en public/assets/infografias/, mantener estructura
|
|
if (assetInfo.source && typeof assetInfo.source === 'string') {
|
|
// Mantener estructura de carpetas para assets
|
|
if (assetInfo.source.includes('assets/infografias')) {
|
|
// Extraer ruta relativa desde public/
|
|
const relativePath = assetInfo.source.split('public/')[1] || name;
|
|
return relativePath;
|
|
}
|
|
}
|
|
return 'assets/[name]-[hash][extname]';
|
|
}
|
|
|
|
return 'assets/[name]-[hash][extname]';
|
|
},
|
|
},
|
|
},
|
|
// Copiar todo el directorio public/ a dist/ (incluye imágenes)
|
|
copyPublicDir: true,
|
|
},
|
|
// Configuración para PWA
|
|
// El service worker se copia automáticamente desde public/ con copyPublicDir
|
|
|
|
// Configuración para importar archivos .md como texto usando ?raw
|
|
// Ejemplo de uso:
|
|
// import content from './file.md?raw'
|
|
// Esto importará el contenido del archivo como string
|
|
// Configuración de Vitest
|
|
test: {
|
|
globals: true,
|
|
environment: 'jsdom',
|
|
setupFiles: ['./src/test/setup.ts'],
|
|
include: ['src/**/*.test.{ts,tsx}'],
|
|
css: true,
|
|
coverage: {
|
|
provider: 'v8',
|
|
reporter: ['text', 'json', 'html'],
|
|
include: ['src/**/*.{ts,tsx}'],
|
|
exclude: [
|
|
'node_modules/',
|
|
'src/test/',
|
|
'**/*.d.ts',
|
|
'**/*.config.*',
|
|
'**/dist/',
|
|
'**/coverage/',
|
|
],
|
|
// Objetivo 80% (TICKET-019); descomentar cuando se alcance
|
|
// thresholds: { lines: 80, functions: 80, branches: 80, statements: 80 },
|
|
},
|
|
},
|
|
optimizeDeps: {
|
|
// CRÍTICO: Incluir TODAS las dependencias que usan React
|
|
// Esto asegura que React está disponible cuando se necesitan
|
|
include: [
|
|
"react",
|
|
"react-dom",
|
|
"react/jsx-runtime",
|
|
"react-markdown",
|
|
"hast-util-to-jsx-runtime",
|
|
"use-sidecar",
|
|
"use-callback-ref",
|
|
"@radix-ui/react-use-callback-ref",
|
|
"react-router-dom",
|
|
"@tanstack/react-query",
|
|
],
|
|
exclude: [],
|
|
// Forzar pre-bundling de React para evitar problemas de resolución
|
|
esbuildOptions: {
|
|
target: "es2020",
|
|
// Asegurar que React se resuelve correctamente
|
|
jsx: "automatic",
|
|
},
|
|
},
|
|
});
|