import { defineConfig, loadEnv } from "vite" import react from "@vitejs/plugin-react" import wasm from "vite-plugin-wasm" import topLevelAwait from "vite-plugin-top-level-await" import { VitePWA } from "vite-plugin-pwa" export default defineConfig(({ mode }) => { // Load env file based on `mode` in the current working directory. // Set the third parameter to '' to load all env regardless of the `VITE_` prefix. const env = loadEnv(mode, process.cwd(), '') // Set the worker URL to localhost for local development process.env.VITE_TLDRAW_WORKER_URL = 'http://localhost:5172' return { envPrefix: ["VITE_"], plugins: [ react(), wasm(), topLevelAwait(), VitePWA({ registerType: 'autoUpdate', injectRegister: 'auto', workbox: { // Force the service worker to take control immediately skipWaiting: true, clientsClaim: true, // Cache all static assets globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2,wasm}'], // Increase the limit for large chunks (Board is ~8MB with tldraw, automerge, etc.) maximumFileSizeToCacheInBytes: 10 * 1024 * 1024, // 10MB // Runtime caching for dynamic requests runtimeCaching: [ { // Cache API responses with network-first strategy urlPattern: /^https?:\/\/.*\/api\/.*/i, handler: 'NetworkFirst', options: { cacheName: 'api-cache', expiration: { maxEntries: 100, maxAgeSeconds: 60 * 60 * 24, // 24 hours }, networkTimeoutSeconds: 10, }, }, { // Cache fonts urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i, handler: 'CacheFirst', options: { cacheName: 'google-fonts-cache', expiration: { maxEntries: 10, maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year }, }, }, { urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i, handler: 'CacheFirst', options: { cacheName: 'gstatic-fonts-cache', expiration: { maxEntries: 10, maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year }, }, }, ], }, manifest: { name: 'Jeff Emmett Canvas', short_name: 'Canvas', description: 'Collaborative canvas for research and creativity', theme_color: '#1a1a2e', background_color: '#1a1a2e', display: 'standalone', start_url: '/', icons: [ { src: '/pwa-192x192.svg', sizes: '192x192', type: 'image/svg+xml', }, { src: '/pwa-512x512.svg', sizes: '512x512', type: 'image/svg+xml', }, { src: '/pwa-512x512.svg', sizes: '512x512', type: 'image/svg+xml', purpose: 'maskable', }, ], }, devOptions: { // Enable SW in development for testing enabled: true, type: 'module', }, }), ], server: { host: "0.0.0.0", port: 5173, strictPort: true, // Force IPv4 to ensure compatibility with WSL2 and remote devices listen: "0.0.0.0", // Configure HMR to use the client's hostname for WebSocket connections // This allows HMR to work from any network (localhost, LAN, Tailscale) hmr: { // Use 'clientPort' to let client determine the correct host clientPort: 5173, }, // Proxy API requests to the worker server proxy: { '/api': { target: 'http://localhost:5172', changeOrigin: true, }, }, }, build: { sourcemap: false, // Disable sourcemaps in production to reduce bundle size rollupOptions: { output: { // Manual chunk splitting for large libraries to improve load times manualChunks(id) { // Core React libraries - load first if (id.includes('node_modules/react') || id.includes('node_modules/react-dom') || id.includes('node_modules/react-router')) { return 'react-vendor'; } // tldraw core - split from shapes if (id.includes('node_modules/tldraw') || id.includes('node_modules/@tldraw')) { return 'tldraw'; } // Automerge - CRDT sync library // Note: automerge-repo-react-hooks must NOT be in this chunk as it depends on React if (id.includes('node_modules/@automerge') && !id.includes('automerge-repo-react-hooks')) { return 'automerge'; } // AI SDKs (lazy load) if (id.includes('node_modules/@anthropic-ai') || id.includes('node_modules/openai') || id.includes('node_modules/ai/')) { return 'ai-sdks'; } // ML/transformers (VERY large, lazy loaded) if (id.includes('node_modules/@xenova')) { return 'ml-libs'; } // Markdown editors if (id.includes('node_modules/@uiw/react-md-editor') || id.includes('node_modules/cherry-markdown') || id.includes('node_modules/marked') || id.includes('node_modules/react-markdown')) { return 'markdown'; } // CodeMirror (used by markdown editors) if (id.includes('node_modules/@codemirror') || id.includes('node_modules/codemirror')) { return 'codemirror'; } // Daily video chat if (id.includes('node_modules/@daily-co')) { return 'daily-video'; } // html2canvas (screenshots) if (id.includes('node_modules/html2canvas')) { return 'html2canvas'; } // ONNX runtime (ML inference) if (id.includes('node_modules/onnxruntime')) { return 'onnx'; } // DOMPurify and sanitizers if (id.includes('node_modules/dompurify') || id.includes('node_modules/isomorphic-dompurify')) { return 'sanitizers'; } }, }, }, chunkSizeWarningLimit: 1000, // Warn on chunks larger than 1MB }, base: "/", publicDir: "src/public", resolve: { alias: { "@": "/src", }, }, define: { // Worker URL is now handled dynamically in Board.tsx based on window.location.hostname // This ensures remote devices connect to the correct worker IP __DAILY_API_KEY__: JSON.stringify(process.env.VITE_DAILY_API_KEY || env.VITE_DAILY_API_KEY) }, optimizeDeps: { include: [ '@xenova/transformers', '@xterm/xterm', '@xterm/addon-fit' ], exclude: [ // Exclude problematic modules from pre-bundling ] }, assetsInclude: [ // Include WebAssembly files '**/*.wasm', '**/*.onnx', '**/*.bin' ] } })