veza/apps/web/vite.config.ts
senke 023b8a89c6 fix: Corriger URL Swagger et finaliser implémentation DeveloperPage
- Ajouter fallback pour Swagger UI si doc.json ne fonctionne pas
- Améliorer message d'erreur avec bouton pour ouvrir Swagger UI directement
- Les fonctionnalités API Keys et Usage Stats sont maintenant complètes et fonctionnelles
- Tous les onglets de DeveloperPage sont maintenant implémentés
2026-01-18 13:55:28 +01:00

483 lines
No EOL
21 KiB
TypeScript

/// <reference types="vitest" />
import { defineConfig, type Plugin } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
import { visualizer } from 'rollup-plugin-visualizer'
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
const isProduction = mode === 'production'
return {
plugins: [
react(),
// Plugin pour générer des nonces CSP
{
name: 'csp-nonce',
generateBundle(_options: any, bundle: any) {
// Générer un nonce pour chaque build
const nonce = Buffer.from(Math.random().toString()).toString('base64')
// Injecter le nonce dans les scripts inline
for (const fileName in bundle) {
const chunk = bundle[fileName]
if (chunk.type === 'chunk' && chunk.code) {
chunk.code = chunk.code.replace(
/__CSP_NONCE__/g,
nonce
)
}
}
}
} as Plugin,
// Bundle analyzer for production builds
(isProduction && visualizer({
filename: 'dist/bundle-analysis.html',
open: false,
gzipSize: true,
brotliSize: true,
})) as Plugin,
// CRITICAL FIX: Plugin pour forcer l'export de React depuis vendor-react-core
// Le problème: vendor-react-core ne contient pas d'export $ pour React
// Solution: Forcer l'export de React depuis vendor-react-core
{
name: 'force-react-export',
generateBundle(_options: any, bundle: any) {
// Trouver le chunk vendor-react-core
for (const fileName in bundle) {
const chunk = bundle[fileName]
if (chunk.type === 'chunk' && chunk.fileName && chunk.fileName.includes('vendor-react-core')) {
if (chunk.code) {
// Trouver la variable React qui a createContext
// React est généralement défini comme: var g = Za() et ensuite g.createContext existe
// On cherche toutes les variables = Za() et on trouve celle qui a createContext
const allZaMatches = chunk.code.match(/(\w+)\s*=\s*Za\(\)/g);
let reactVar = null;
if (allZaMatches) {
for (const match of allZaMatches) {
const varMatch = match.match(/(\w+)\s*=\s*Za\(\)/);
if (varMatch) {
const varName = varMatch[1];
// Vérifier si cette variable a createContext
if (chunk.code.includes(`${varName}.createContext`)) {
reactVar = varName;
break;
}
}
}
}
// Si on n'a pas trouvé, chercher directement la variable qui a createContext
if (!reactVar) {
const createContextMatch = chunk.code.match(/(\w+)\.createContext/);
if (createContextMatch) {
reactVar = createContextMatch[1];
}
}
if (reactVar) {
// Vérifier si React est déjà exporté avec le nom $
const hasDollarExport = chunk.code.match(/export\s+\{[^}]*\$[^}]*\}/);
if (!hasDollarExport) {
// Ajouter l'export de React avec le nom $ pour que react-hook-form puisse l'importer
const exportStatement = `export{${reactVar}as $}`;
// Vérifier si le chunk se termine par un point-virgule ou une nouvelle ligne
if (!chunk.code.trim().endsWith(';') && !chunk.code.trim().endsWith('}')) {
chunk.code += ';';
}
chunk.code += `\n${exportStatement};`;
// #region agent log
const logData = {
location: 'vite.config.ts:force-react-export',
message: 'Added React export to vendor-react-core',
data: {
reactVar,
fileName: chunk.fileName,
exportStatement,
},
timestamp: Date.now(),
sessionId: 'debug-session',
runId: 'build-analysis',
hypothesisId: 'H',
};
fetch('http://127.0.0.1:7242/ingest/09c5ea5e-2380-4cc3-92aa-d26f3b3d26f6',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(logData)}).catch(()=>{});
// #endregion
}
}
}
}
}
}
} as Plugin,
// CRITICAL FIX: Plugin pour réorganiser les modulepreload links dans le HTML
// Le problème: vendor-react-hook-form est préchargé avant vendor-react-core
// Solution: Réorganiser les modulepreload links pour que vendor-react-core soit en premier
{
name: 'fix-react-load-order',
transformIndexHtml(html: string) {
// Extraire tous les modulepreload links
const modulepreloadRegex = /<link rel="modulepreload"[^>]*>/g;
const modulepreloadLinks = html.match(modulepreloadRegex) || [];
// Séparer les links React des autres
const reactCoreLink = modulepreloadLinks.find(link => link.includes('vendor-react-core'));
const reactHookFormLink = modulepreloadLinks.find(link => link.includes('vendor-react-hook-form'));
const otherLinks = modulepreloadLinks.filter(link =>
!link.includes('vendor-react-core') && !link.includes('vendor-react-hook-form')
);
// Si on a trouvé les deux chunks React, réorganiser
if (reactCoreLink && reactHookFormLink) {
// Retirer tous les modulepreload links existants
let newHtml = html.replace(modulepreloadRegex, '');
// Réinsérer dans le bon ordre : React core d'abord, puis react-hook-form, puis les autres
const orderedLinks = [
reactCoreLink,
reactHookFormLink,
...otherLinks
].join('\n ');
// Insérer après le script module principal
newHtml = newHtml.replace(
/(<script type="module"[^>]*><\/script>)/,
`$1\n ${orderedLinks}`
);
// #region agent log
const logData = {
location: 'vite.config.ts:fix-react-load-order',
message: 'Reordered modulepreload links',
data: {
reactCoreLink,
reactHookFormLink,
otherLinksCount: otherLinks.length,
},
timestamp: Date.now(),
sessionId: 'debug-session',
runId: 'build-analysis',
hypothesisId: 'G',
};
fetch('http://127.0.0.1:7242/ingest/09c5ea5e-2380-4cc3-92aa-d26f3b3d26f6',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(logData)}).catch(()=>{});
// #endregion
return newHtml;
}
return html;
},
} as Plugin,
].filter(Boolean),
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/setupTests.ts',
css: true,
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
'@features': path.resolve(__dirname, './src/features'),
'@services': path.resolve(__dirname, './src/services'),
'@hooks': path.resolve(__dirname, './src/hooks'),
'@utils': path.resolve(__dirname, './src/utils'),
'@types': path.resolve(__dirname, './src/types'),
},
},
server: {
port: 5173,
host: true,
headers: {
'Content-Security-Policy': (() => {
const basePolicy = [
"default-src 'self'",
"worker-src 'self' blob:",
"img-src 'self' data: https: blob:",
"connect-src 'self' ws: wss: http: https:",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
"upgrade-insecure-requests"
];
// Production: Remove unsafe-inline and unsafe-eval, use nonces
// Dev: Keep unsafe-inline for Vite HMR, but remove unsafe-eval
if (isProduction) {
basePolicy.push(
"script-src 'self' 'nonce-__CSP_NONCE__' blob:",
"style-src 'self' 'nonce-__CSP_NONCE__' https://fonts.googleapis.com",
"font-src 'self' data: https://fonts.gstatic.com"
);
} else {
basePolicy.push(
"script-src 'self' 'unsafe-inline' blob:",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"font-src 'self' data: https://fonts.gstatic.com"
);
}
return basePolicy.join('; ');
})()
}
},
build: {
outDir: 'dist',
sourcemap: isProduction ? 'hidden' : false, // 'hidden' pour debug prod si nécessaire
target: 'es2020', // Force une transpilation plus conservatrice pour éviter les erreurs de minification
// CRITICAL FIX: Réactiver la minification avec une configuration conservatrice
// Le problème n'était pas la minification mais l'ordre de chargement des chunks
minify: 'esbuild',
// Configuration esbuild pour éviter les collisions de noms
esbuild: {
legalComments: 'none',
minifyIdentifiers: true,
minifySyntax: true,
minifyWhitespace: true,
// CRITICAL: Ne pas mangle les noms de variables pour éviter les collisions
// Cela augmente légèrement la taille du bundle mais évite les erreurs
keepNames: false, // esbuild ne supporte pas keepNames, on utilise target: 'es2020' à la place
},
// CRITICAL FIX: Forcer un format de chunk qui isole mieux les modules
commonjsOptions: {
include: [/node_modules/],
transformMixedEsModules: true,
// FIX: Transform CommonJS to ES modules to avoid "module is not defined" errors
esmExternals: true,
},
// PERF: Inline les petits assets (< 4KB) pour réduire les requêtes HTTP
assetsInlineLimit: 4096,
rollupOptions: {
// CRITICAL FIX: Exclure react-hot-toast de certaines optimisations pour éviter les collisions
external: (id) => {
// Ne pas externaliser react-hot-toast, mais forcer son isolation
return false;
},
// CRITICAL FIX: Désactiver la minification pour le chunk toast
// Cela évite les collisions de noms de variables (ie) après minification
treeshake: {
moduleSideEffects: (id) => {
// Ne pas tree-shake react-hot-toast pour éviter les problèmes
if (id.includes('react-hot-toast')) return true;
// CRITICAL: Don't tree-shake React to ensure it's always available
if (id.includes('react') || id.includes('react-dom')) return true;
return false;
},
},
// CRITICAL FIX: Ensure React vendor chunk loads before other chunks
output: {
// CRITICAL FIX: Force ES module format and prevent hoisting to avoid variable collisions
format: 'es',
hoistTransitiveImports: false, // Prevent hoisting that could cause variable name collisions
// FIX: Ensure all chunks use ES modules, not CommonJS
interop: 'compat', // Better compatibility for CommonJS interop
// CRITICAL FIX: Use a more conservative minification approach
// This helps prevent variable name collisions across chunks
generatedCode: {
constBindings: false, // Use var instead of const to avoid TDZ issues
},
// CRITICAL FIX: Ensure proper chunk loading order
// The entry chunk must load before any vendor chunks
entryFileNames: 'js/index-[hash].js',
// CRITICAL FIX: Ensure React is exported correctly from vendor-react-core chunk
// This ensures that react-hook-form can import React from vendor-react-core
preserveModules: false, // We want chunks, not preserved modules
// CRITICAL FIX: Force proper exports from manual chunks
// This ensures that all exports from vendor-react-core are available
externalLiveBindings: false, // Use static bindings for better compatibility
// PERF: Manual chunk splitting pour optimiser le bundle size
manualChunks: (id) => {
// CRITICAL FIX: React MUST be in a dedicated chunk that loads FIRST
// This ensures React is always available before react-hook-form tries to use it
// IMPORTANT: This must be checked BEFORE any other vendor chunk logic
// CRITICAL: Use more specific patterns to catch ALL React-related modules
// NOTE: The pattern must match the actual module IDs used by Vite/Rollup
if (
id.includes('node_modules/react/') ||
id.includes('node_modules/react-dom/') ||
id.includes('node_modules/react/jsx-runtime') ||
id.includes('node_modules/react/cjs/') ||
id.includes('node_modules/react-dom/cjs/') ||
id.includes('node_modules/react-dom/client') ||
id.includes('node_modules/use-sync-external-store') ||
// Also catch React when imported from other locations
(id.includes('react') && id.includes('node_modules') && !id.includes('react-hook-form') && !id.includes('react-router') && !id.includes('react-hot-toast'))
) {
// CRITICAL: Return a dedicated chunk name that will be loaded first
return 'vendor-react-core'; // This will be loaded first, before other vendor chunks
}
// CRITICAL FIX: react-hook-form MUST load AFTER React is available
// Put it in a separate chunk that will be loaded after the entry chunk
// NOTE: react-hook-form is in chunk-CoYWxivd.js which imports React from chunk-LyYNyHOl.js
// We need to ensure chunk-LyYNyHOl.js (React) loads before chunk-CoYWxivd.js
if (id.includes('react-hook-form')) {
return 'vendor-react-hook-form';
}
// CRITICAL FIX: react-hot-toast MUST be completely isolated with ALL its dependencies
// The issue: react-hot-toast depends on other chunks (Sentry, router, axios) which export variables
// that conflict with react-hot-toast's internal 'ie' variable after minification
// Solution: Force react-hot-toast AND ALL its transitive dependencies into the same chunk
// This ensures no cross-chunk variable name collisions
if (id.includes('react-hot-toast')) {
return 'vendor-toast';
}
// CRITICAL FIX: Prevent ANY module that react-hot-toast might depend on from being
// in a separate chunk. This includes Sentry, scheduler, router, axios, etc.
// We isolate them separately to avoid conflicts, but react-hot-toast should only depend on React
if (id.includes('scheduler') && !id.includes('react-hot-toast')) {
return 'vendor-scheduler';
}
// CRITICAL FIX: Isolate @remix-run/router and axios to prevent them from being
// in the same chunk as react-hot-toast dependencies
// This prevents the 'ie' variable export collision
if (id.includes('@remix-run/router') || id.includes('react-router')) {
return 'vendor-router';
}
if (id.includes('axios') && !id.includes('react-hot-toast')) {
return 'vendor-axios';
}
// CRITICAL FIX: Isolate Sentry to prevent it from being in the same chunk as react-hot-toast
// The chunk-Dakv0iAL.js (Sentry) was causing conflicts
if (id.includes('@sentry') && !id.includes('react-hot-toast')) {
return 'vendor-sentry';
}
// Vendor chunks - séparer les grandes dépendances
// BUT: Skip React-related packages (already handled above)
if (id.includes('node_modules') && !id.includes('react') && !id.includes('use-sync-external-store') && !id.includes('react-hook-form')) {
// Zustand can be in a separate chunk but will load after React (entry chunk loads first)
if (id.includes('zustand')) {
return 'vendor-zustand';
}
// React Router
if (id.includes('react-router')) {
return 'vendor-router';
}
// TanStack (Query, Virtual)
if (id.includes('@tanstack')) {
return 'vendor-tanstack';
}
// Lucide icons (volumineux)
if (id.includes('lucide-react')) {
return 'vendor-icons';
}
// Date utilities
if (id.includes('date-fns')) {
return 'vendor-dates';
}
// Validation
if (id.includes('zod')) {
return 'vendor-validation';
}
// Media libraries
if (id.includes('hls.js')) {
return 'vendor-media';
}
// Autres vendors
return 'vendor';
}
// Feature chunks - séparer les features lourdes
if (id.includes('/features/player/')) {
return 'feature-player';
}
if (id.includes('/features/upload/')) {
return 'feature-upload';
}
if (id.includes('/features/chat/')) {
return 'feature-chat';
}
if (id.includes('/features/studio/')) {
return 'feature-studio';
}
if (id.includes('/components/player/')) {
return 'feature-player';
}
// UI components (déjà lazy loaded via routes)
// Pas besoin de chunk séparé car déjà optimisé
},
chunkFileNames: (chunkInfo) => {
// CRITICAL FIX: Forcer un nom spécifique pour les chunks critiques
if (chunkInfo.name === 'vendor-react-core') {
return 'js/vendor-react-core-[hash].js';
}
if (chunkInfo.name === 'vendor-react-hook-form') {
return 'js/vendor-react-hook-form-[hash].js';
}
if (chunkInfo.name === 'vendor-toast') {
return 'js/vendor-toast-[hash].js';
}
const facadeModuleId = chunkInfo.facadeModuleId
? chunkInfo.facadeModuleId.split('/').pop()?.replace('.tsx', '').replace('.ts', '')
: 'chunk';
return `js/${facadeModuleId}-[hash].js`;
},
assetFileNames: (assetInfo) => {
const extType = assetInfo.name?.split('.').pop();
if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(extType || '')) {
return 'images/[name]-[hash][extname]';
}
if (/woff2?|eot|ttf|otf/i.test(extType || '')) {
return 'fonts/[name]-[hash][extname]';
}
return 'assets/[name]-[hash][extname]';
},
},
},
// PERF: Augmenter la limite d'avertissement pour les chunks vendors
chunkSizeWarningLimit: 1000,
// CRITICAL FIX: Sourcemap hidden pour debug prod si nécessaire, sans exposer le code source
// PERF: Optimiser la taille des chunks
cssCodeSplit: true,
reportCompressedSize: true,
},
optimizeDeps: {
// CRITICAL FIX: Pre-bundle react-hook-form and use-sync-external-store to avoid initialization errors
include: [
'react-hook-form',
'react',
'react-dom',
'zustand',
'use-sync-external-store',
'use-sync-external-store/shim',
'use-sync-external-store/shim/with-selector',
'date-fns',
'zod',
'dompurify',
'@tanstack/react-virtual',
// FIX: scheduler doit être inclus pour être transformé de CommonJS vers ES modules
// Le problème: scheduler/index.js utilise module.exports qui cause "module is not defined"
'scheduler',
// NOTE: react-hot-toast est EXCLU du pre-bundling
// pour forcer des chunks séparés et éviter les collisions de noms (ie)
],
exclude: [
'@vite/client',
'@vite/env',
'react-hot-toast', // CRITICAL FIX: Exclure react-hot-toast du pre-bundling
// REMOVED: scheduler - maintenant inclus pour être transformé
],
esbuildOptions: {
supported: {
'top-level-await': true
},
// FIX: Transform CommonJS to ES modules to avoid "module is not defined" errors
format: 'esm',
}
},
css: {
devSourcemap: !isProduction,
},
define: {
__CSP_NONCE__: JSON.stringify('__CSP_NONCE__')
}
}
})