The card always displayed 1.0.0. VITE_APP_VERSION was never set: CI passes the release tag as a VERSION build arg, but the Dockerfile never declared it or forwarded it to the frontend build, so the hardcoded fallback always won. Declare the build arg and export it as VITE_APP_VERSION, and resolve the version in vite.config.ts for every other build: the env var, then the release tag via git describe, then package.json. The image version label was pinned at 1.0.0 for the same reason and now follows the arg too.
182 lines
4.9 KiB
TypeScript
182 lines
4.9 KiB
TypeScript
import { defineConfig } from 'vite';
|
|
import react from '@vitejs/plugin-react';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { execFileSync } from 'child_process';
|
|
import { visualizer } from 'rollup-plugin-visualizer';
|
|
|
|
/**
|
|
* Releases are cut as git tags, and package.json is never bumped along with
|
|
* them, so the tag is the only honest source. The Docker image has no .git,
|
|
* which is why CI passes the tag in as VITE_APP_VERSION instead.
|
|
*/
|
|
function resolveAppVersion(): string {
|
|
const fromEnv = process.env.VITE_APP_VERSION?.trim();
|
|
if (fromEnv) return fromEnv;
|
|
|
|
try {
|
|
const described = execFileSync('git', ['describe', '--tags', '--always', '--dirty'], {
|
|
cwd: __dirname,
|
|
encoding: 'utf-8',
|
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
}).trim();
|
|
if (described) return described.replace(/^v/, '');
|
|
} catch {
|
|
// Not a git checkout — fall through to package.json.
|
|
}
|
|
|
|
try {
|
|
const pkg: unknown = JSON.parse(
|
|
fs.readFileSync(path.resolve(__dirname, 'package.json'), 'utf-8')
|
|
);
|
|
if (pkg && typeof pkg === 'object' && 'version' in pkg && typeof pkg.version === 'string') {
|
|
return pkg.version;
|
|
}
|
|
} catch {
|
|
// Fall through to the unknown marker.
|
|
}
|
|
|
|
return 'unknown';
|
|
}
|
|
|
|
export default defineConfig({
|
|
define: {
|
|
'import.meta.env.VITE_APP_VERSION': JSON.stringify(resolveAppVersion()),
|
|
},
|
|
plugins: [
|
|
react(),
|
|
// Bundle analyzer - run with: npm run build:analyze && open stats.html
|
|
visualizer({
|
|
open: !!process.env.VITE_ANALYZE,
|
|
filename: 'stats.html',
|
|
gzipSize: true,
|
|
brotliSize: true,
|
|
}),
|
|
],
|
|
resolve: {
|
|
alias: {
|
|
'@': path.resolve(__dirname, './src'),
|
|
},
|
|
},
|
|
assetsInclude: ['**/*.wasm'],
|
|
esbuild: {
|
|
drop: ['debugger'],
|
|
pure: ['console.log', 'console.debug'],
|
|
},
|
|
build: {
|
|
// Target modern browsers for smaller bundles
|
|
target: 'es2020',
|
|
|
|
// Increase chunk size warning limit (default is 500kb)
|
|
chunkSizeWarningLimit: 600,
|
|
|
|
// Enable minification (esbuild is Vite's default — fast, no extra dependency)
|
|
minify: 'esbuild',
|
|
|
|
rollupOptions: {
|
|
output: {
|
|
// Chunk naming strategy
|
|
chunkFileNames: 'assets/[name]-[hash].js',
|
|
entryFileNames: 'assets/[name]-[hash].js',
|
|
assetFileNames: 'assets/[name]-[hash].[ext]',
|
|
|
|
manualChunks: (id) => {
|
|
// Core React libraries
|
|
if (
|
|
id.includes('node_modules/react') ||
|
|
id.includes('node_modules/react-dom') ||
|
|
id.includes('node_modules/react-router-dom')
|
|
) {
|
|
return 'react-vendor';
|
|
}
|
|
|
|
// Data fetching and state management
|
|
if (id.includes('@tanstack/react-query')) {
|
|
return 'react-query';
|
|
}
|
|
|
|
// State management (Zustand)
|
|
if (id.includes('zustand')) {
|
|
return 'state';
|
|
}
|
|
|
|
// Chart library (recharts is large - lazy loaded)
|
|
if (id.includes('recharts')) {
|
|
return 'charts';
|
|
}
|
|
|
|
// Icon library
|
|
if (id.includes('lucide-react')) {
|
|
return 'icons';
|
|
}
|
|
|
|
// Form libraries
|
|
if (
|
|
id.includes('react-hook-form') ||
|
|
id.includes('@hookform/resolvers') ||
|
|
id.includes('zod')
|
|
) {
|
|
return 'forms';
|
|
}
|
|
|
|
// UI component library (Radix UI)
|
|
if (id.includes('@radix-ui')) {
|
|
return 'ui-primitives';
|
|
}
|
|
|
|
// Utilities (date-fns, clsx, etc.)
|
|
if (
|
|
id.includes('date-fns') ||
|
|
id.includes('clsx') ||
|
|
id.includes('class-variance-authority')
|
|
) {
|
|
return 'utils';
|
|
}
|
|
|
|
// Remaining node_modules: let Rollup decide optimal chunking
|
|
// A catch-all 'vendor' bucket causes circular deps with react-vendor
|
|
// because packages like framer-motion/axios import React
|
|
},
|
|
},
|
|
},
|
|
|
|
// Source maps for production debugging (can be disabled for smaller builds)
|
|
sourcemap: false,
|
|
},
|
|
server: {
|
|
port: 5173,
|
|
headers: {
|
|
'Cross-Origin-Embedder-Policy': 'credentialless',
|
|
'Cross-Origin-Opener-Policy': 'same-origin',
|
|
},
|
|
proxy: {
|
|
'/api': {
|
|
target: 'http://localhost:3100',
|
|
changeOrigin: true,
|
|
},
|
|
'/proxy': {
|
|
target: 'http://localhost:3100',
|
|
changeOrigin: true,
|
|
},
|
|
// Without this the startup poll hits Vite's SPA fallback, which answers
|
|
// 200 with index.html and makes the "server is starting" screen reload
|
|
// itself every two seconds.
|
|
'/health': {
|
|
target: 'http://localhost:3100',
|
|
changeOrigin: true,
|
|
},
|
|
'/game-proxy': {
|
|
target: 'http://localhost:3100',
|
|
changeOrigin: true,
|
|
},
|
|
'/game-zip': {
|
|
target: 'http://localhost:3100',
|
|
changeOrigin: true,
|
|
},
|
|
},
|
|
},
|
|
optimizeDeps: {
|
|
exclude: ['*.wasm'],
|
|
},
|
|
});
|