/// 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, ].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, target: 'esnext', minify: 'esbuild', // PERF: Inline les petits assets (< 4KB) pour réduire les requêtes HTTP assetsInlineLimit: 4096, rollupOptions: { output: { // PERF: Manual chunk splitting pour optimiser le bundle size manualChunks: (id) => { // Vendor chunks - séparer les grandes dépendances if (id.includes('node_modules')) { // React core if (id.includes('react') || id.includes('react-dom')) { return 'vendor-react'; } // 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) => { const facadeModuleId = chunkInfo.facadeModuleId ? chunkInfo.facadeModuleId.split('/').pop()?.replace('.tsx', '').replace('.ts', '') : 'chunk'; return `js/${facadeModuleId}-[hash].js`; }, entryFileNames: 'js/[name]-[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, // PERF: Optimiser la taille des chunks cssCodeSplit: true, reportCompressedSize: true, }, optimizeDeps: { include: [ 'react', 'react-dom', 'zustand', 'date-fns', 'zod', 'dompurify', '@tanstack/react-virtual', ], exclude: ['@vite/client', '@vite/env'], }, css: { devSourcemap: !isProduction, }, define: { __CSP_NONCE__: JSON.stringify('__CSP_NONCE__') } } })