Ctrl K
ComunidadGuías…

web/vite.config.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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/<host>.pem` and
// `web/certs/<host>-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://<your-ip>: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 <host> --port <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'
        }
      }
    }
  }
})