web/vite.config.js
Rama main · réplica de lectura del repositorio original.
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'
}
}
}
}
})