- 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
483 lines
No EOL
21 KiB
TypeScript
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__')
|
|
}
|
|
}
|
|
}) |