import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import tailwindcss from '@tailwindcss/vite'; import { VitePWA } from 'vite-plugin-pwa'; import { fileURLToPath, URL } from 'node:url'; // Strict CSP injected at build time only (dev needs inline scripts for react-refresh). // All external football APIs are called from the server, so the browser only ever // talks to its own origin + WebSocket — connect-src can stay locked to 'self'. const PROD_CSP = "default-src 'self'; img-src 'self' data: blob:; style-src 'self' 'unsafe-inline'; " + "script-src 'self'; connect-src 'self' ws: wss:; " + "font-src 'self' data:; worker-src 'self' blob:; " + "manifest-src 'self'; object-src 'none'; base-uri 'self'"; function cspPlugin() { return { name: 'inject-csp', transformIndexHtml(html: string) { const tag = ``; return html.replace('', tag); }, apply: 'build' as const, }; } export default defineConfig({ plugins: [ react(), tailwindcss(), cspPlugin(), VitePWA({ registerType: 'autoUpdate', includeAssets: ['favicon.svg'], manifest: { name: 'Cup26 — World Cup 2026 Dashboard', short_name: 'Cup26', description: 'Live World Cup 2026 scores, model-driven odds, and data-story visualizations.', theme_color: '#0a1410', background_color: '#0a1410', display: 'standalone', icons: [ { src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' }, { src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' }, { src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }, ], }, workbox: { globPatterns: ['**/*.{js,css,html,svg,png,woff2,json}'], // server-only refit training data (~1MB) — keep it off every client globIgnores: ['**/dcTrain.json', '**/push-sw.js'], // goal/kickoff Web Push handlers ride along with the generated SW importScripts: ['push-sw.js'], maximumFileSizeToCacheInBytes: 6 * 1024 * 1024, skipWaiting: true, clientsClaim: true, cleanupOutdatedCaches: true, // Live data is fetched fresh from /api; never serve it from the precache. navigateFallbackDenylist: [/^\/api/, /^\/ws/], }, }), ], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)), }, }, server: { // Dev: proxy API + WS to the Fastify server so the browser only talks to :5173. proxy: { '/api': { target: 'http://localhost:8787', changeOrigin: true }, '/ws': { target: 'ws://localhost:8787', ws: true }, }, }, });