veza/apps/web/vite.config.ts
2025-12-17 08:07:35 -05:00

143 lines
No EOL
4.4 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,
].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: 3000,
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:",
"font-src 'self' data:",
"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__'"
);
} else {
basePolicy.push(
"script-src 'self' 'unsafe-inline' blob:",
"style-src 'self' 'unsafe-inline'"
);
}
return basePolicy.join('; ');
})()
}
},
build: {
outDir: 'dist',
sourcemap: isProduction,
target: 'esnext',
minify: 'esbuild',
rollupOptions: {
output: {
// Manual chunk splitting removed to avoid path resolution errors
// manualChunks: { ... },
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]';
},
},
},
chunkSizeWarningLimit: 1000,
},
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__')
}
}
})