import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import vuetify from 'vite-plugin-vuetify' import { VitePWA } from 'vite-plugin-pwa' import { fileURLToPath, URL } from 'node:url' import fs from 'fs' /** * Custom plugin to optimize Material Design Icons font loading. * Removes ttf, eot, and woff formats from @font-face, keeping only woff2. * This reduces the bundle size by ~2.9MB. * * woff2 is supported by all modern browsers (Chrome 36+, Firefox 39+, * Safari 12+, Edge 14+, iOS Safari 10+, Android 5+). */ function mdiWoff2Only() { return { name: 'mdi-woff2-only', enforce: 'pre', transform(code, id) { // Only process the MDI CSS file if (id.includes('@mdi/font') && id.endsWith('.css')) { // Replace the @font-face src with woff2 only const modified = code.replace( /src:\s*url\([^)]+\.eot[^;]*;[\s\S]*?src:\s*url\([^)]+\.eot[^)]*\)[^,]*,\s*url\(([^)]+\.woff2[^)]*)\)[^,]*,\s*url\([^)]+\.woff[^)]*\)[^,]*,\s*url\([^)]+\.ttf[^)]*\)[^;]*;/g, 'src: url($1) format("woff2");' ) return { code: modified, map: null } } return null } } } // https://vitejs.dev/config/ // Optional local HTTPS support: if `web/certs/.pem` and // `web/certs/-key.pem` exist the Vite config will use them for // an HTTPS dev/preview server. To opt-in, generate certs using mkcert // and place them in `web/certs` (the `scripts/start-frontend.sh` helper // can generate these for you). const CERT_DIR = fileURLToPath(new URL('./certs', import.meta.url)) // Read configuration from environment variables (set by start-frontend.sh) const DEFAULT_HOST = process.env.VITE_DEV_HOST || 'localhost' const DEFAULT_PORT = process.env.VITE_DEV_PORT || '3000' const USE_HTTPS = (process.env.VITE_USE_HTTPS || 'false') === 'true' const DEPLOYMENT_URL = process.env.VITE_DEPLOYMENT_URL || `http://localhost:${DEFAULT_PORT}` // Ensure DEPLOYMENT_URL has protocol const FULL_DEPLOYMENT_URL = DEPLOYMENT_URL.match(/^https?:\/\//) ? DEPLOYMENT_URL : (USE_HTTPS ? `https://${DEPLOYMENT_URL}` : `http://${DEPLOYMENT_URL}`) const KEY_PATH = `${CERT_DIR}/${DEFAULT_HOST}-key.pem` const CERT_PATH = `${CERT_DIR}/${DEFAULT_HOST}.pem` let httpsOptions = undefined if (USE_HTTPS && fs.existsSync(KEY_PATH) && fs.existsSync(CERT_PATH)) { httpsOptions = { key: fs.readFileSync(KEY_PATH), cert: fs.readFileSync(CERT_PATH) } } // https://vitejs.dev/config/ export default defineConfig({ optimizeDeps: { include: [ 'vuetify', 'workbox-window' ], exclude: [] }, plugins: [ mdiWoff2Only(), // Must be first to transform MDI CSS before other processing vue(), vuetify({ autoImport: true }), VitePWA({ // Use 'prompt' for user-controlled updates (works with UpdatePrompt.vue) // User decides when to apply updates instead of auto-reload registerType: 'prompt', includeAssets: [ 'favicon.ico', 'robots.txt', 'apple-touch-icon.png', 'apple-touch-icon-120x120.png', 'apple-touch-icon-152x152.png', 'apple-touch-icon-167x167.png', 'apple-touch-icon-180x180.png', 'apple-touch-icon-1024x1024.png', 'logo.svg', 'logo.png' ], manifest: { name: 'ActaLog - CrossFit Workout Tracker', short_name: 'ActaLog', description: 'Track your CrossFit workouts, log weights and reps, and monitor your fitness progress', theme_color: '#00bcd4', background_color: '#ffffff', display: 'standalone', orientation: 'portrait', // NOTE: set `scope` and `start_url` to the canonical origin // for your deployment. These are now read from VITE_DEPLOYMENT_URL // environment variable (set by scripts/start-frontend.sh) scope: `${FULL_DEPLOYMENT_URL}/`, start_url: `${FULL_DEPLOYMENT_URL}/`, icons: [ { src: '/icons/icon-72x72.png', sizes: '72x72', type: 'image/png', purpose: 'any' }, { src: '/icons/icon-96x96.png', sizes: '96x96', type: 'image/png', purpose: 'any' }, { src: '/icons/icon-128x128.png', sizes: '128x128', type: 'image/png', purpose: 'any' }, { src: '/icons/icon-144x144.png', sizes: '144x144', type: 'image/png', purpose: 'any' }, { src: '/icons/icon-152x152.png', sizes: '152x152', type: 'image/png', purpose: 'any' }, { src: '/icons/icon-192x192.png', sizes: '192x192', type: 'image/png', purpose: 'any' }, { src: '/icons/icon-384x384.png', sizes: '384x384', type: 'image/png', purpose: 'any' }, { src: '/icons/icon-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'any' }, { src: '/icons/icon-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }, { src: '/icons/icon-1024x1024.png', sizes: '1024x1024', type: 'image/png', purpose: 'any' } ] }, workbox: { globDirectory: 'dist', // Precache app shell and assets, but NOT /fonts/ directory (lazy-loaded) // MDI icons in /assets/ are included, accessibility fonts in /fonts/ are loaded on demand globPatterns: [ '**/*.{js,css,html,ico,png,svg}', 'assets/*.woff2' // Only MDI icon font, not /fonts/ directory ], // Exclude accessibility fonts from precache - they're lazy-loaded globIgnores: ['fonts/**/*'], navigateFallback: 'index.html', navigateFallbackDenylist: [/^\/api/], cleanupOutdatedCaches: true, // Note: skipWaiting and clientsClaim removed for prompt mode // User controls when to apply updates via UpdatePrompt.vue runtimeCaching: [ { urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i, handler: 'CacheFirst', options: { cacheName: 'google-fonts-cache', expiration: { maxEntries: 10, maxAgeSeconds: 60 * 60 * 24 * 365 // 1 year }, cacheableResponse: { statuses: [0, 200] } } }, { urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i, handler: 'CacheFirst', options: { cacheName: 'gstatic-fonts-cache', expiration: { maxEntries: 10, maxAgeSeconds: 60 * 60 * 24 * 365 // 1 year }, cacheableResponse: { statuses: [0, 200] } } }, { urlPattern: /^https:\/\/cdn\.jsdelivr\.net\/.*/i, handler: 'CacheFirst', options: { cacheName: 'cdn-cache', expiration: { maxEntries: 10, maxAgeSeconds: 60 * 60 * 24 * 365 // 1 year }, cacheableResponse: { statuses: [0, 200] } } }, // Cache API GET requests for offline access { urlPattern: /\/api\/(workouts|movements|wods|templates)/, handler: 'NetworkFirst', options: { cacheName: 'api-cache', expiration: { maxEntries: 100, maxAgeSeconds: 60 * 60 * 24 // 24 hours for offline access }, cacheableResponse: { statuses: [0, 200] }, networkTimeoutSeconds: 5 } }, // Cache user-specific data with shorter expiration { urlPattern: /\/api\/user/, handler: 'NetworkFirst', options: { cacheName: 'user-api-cache', expiration: { maxEntries: 20, maxAgeSeconds: 60 * 30 // 30 minutes }, cacheableResponse: { statuses: [0, 200] }, networkTimeoutSeconds: 5 } }, // Cache local fonts with long expiration { urlPattern: /\/fonts\/.*/i, handler: 'CacheFirst', options: { cacheName: 'local-fonts-cache', expiration: { maxEntries: 30, maxAgeSeconds: 60 * 60 * 24 * 365 // 1 year }, cacheableResponse: { statuses: [0, 200] } } }, // Cache uploaded images with StaleWhileRevalidate { urlPattern: /\/uploads\/.*/i, handler: 'StaleWhileRevalidate', options: { cacheName: 'uploads-cache', expiration: { maxEntries: 50, maxAgeSeconds: 60 * 60 * 24 * 7 // 7 days }, cacheableResponse: { statuses: [0, 200] } } } ] }, devOptions: { enabled: true, type: 'module', navigateFallback: 'index.html', suppressWarnings: true } }) ], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }, // SERVER CONFIGURATION // -------------------- // The `server` block controls Vite's dev server behavior. Typical workflows: // - Local development (no hosts file): bind to 0.0.0.0 or localhost and use // `http://localhost:3000`. // - Local development with a mapped hostname (recommended for PWA/cookie // testing): map a name (example: subdomain.example.com) to your loopback // interface in your OS hosts file, then set `host` to that name so HMR and // Service Worker expectations match the origin. // - Named server (staging/production): do NOT use the dev server. Build and // deploy the `dist/` output to your web host, ensuring the `build.base` and // PWA manifest values match the deployed origin. // // To run the dev server without editing your hosts file, start Vite with // `--host 0.0.0.0` (or export HOST=0.0.0.0). Example: // cd web && npm run dev -- --host 0.0.0.0 // This will bind Vite to all interfaces and allow testing via // `http://localhost:3000` or `http://:3000`. server: { // Host and port are read from environment variables (set by start-frontend.sh) // or can be overridden via CLI: npm run dev -- --host --port host: DEFAULT_HOST, port: parseInt(DEFAULT_PORT), https: httpsOptions ? httpsOptions : false, hmr: { // HMR client will try to connect to this host; set to the same host // you use to access the dev server. CLI args override this. host: DEFAULT_HOST }, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, }, '/uploads': { target: 'http://localhost:8080', changeOrigin: true, } } }, build: { // Serve built assets from the deployment URL (read from environment) base: `${FULL_DEPLOYMENT_URL}/`, outDir: 'dist', // Disable sourcemaps in production for smaller bundles sourcemap: false, rollupOptions: { output: { // Manual chunk splitting for better caching. // Function form (not the object form): vite 8's rolldown bundler only // accepts a function here, and it works under rollup (vite 7) too. manualChunks(id) { if (!id.includes('node_modules')) return if (/[\\/]node_modules[\\/](vue|vue-router|pinia)[\\/]/.test(id)) return 'vue-vendor' if (/[\\/]node_modules[\\/]vuetify[\\/]/.test(id)) return 'vuetify' if (/[\\/]node_modules[\\/](chart\.js|vue-chartjs)[\\/]/.test(id)) return 'chart' } } } } })