import { execFileSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import react from '@vitejs/plugin-react'; import { defineConfig } from 'vitest/config'; interface PackageJson { version: string; } const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf-8')) as PackageJson; /** * `X-Kvitt-App-Version` (SPEC §14.3.1): the commit count of the checkout at build time (`git rev-list --count HEAD`, * as the app's release builds count). Without git or `.git` (the image's Node stage), `KVITT_VERSION_CODE` when set, * else 1. */ function versionCode(): number { const counted = gitCommitCount(); if (counted !== null) return counted; const fromEnv = process.env.KVITT_VERSION_CODE; return fromEnv !== undefined && /^[1-9]\d*$/.test(fromEnv) ? Number(fromEnv) : 1; } function gitCommitCount(): number | null { try { const count = execFileSync('git', ['rev-list', '--count', 'HEAD'], { cwd: fileURLToPath(new URL('.', import.meta.url)), encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], }).trim(); return /^[1-9]\d*$/.test(count) ? Number(count) : null; } catch { // No git binary, or no repository around web/. return null; } } /** API paths of contracts/openapi.yaml; `npm run dev` forwards them to a local kvitt-server (README). */ const API_PREFIXES = [ '/health', '/setup', '/invites', '/me', '/users', '/devices', '/accounts', '/categories', '/recurring-templates', '/sync', '/settlements', '/apk', '/analytics', '/cards', ]; const devApi = process.env.KVITT_DEV_API ?? 'http://localhost:8080'; export default defineConfig({ base: '/app/', plugins: [react()], define: { __KVITT_VERSION_NAME__: JSON.stringify(pkg.version), __KVITT_VERSION_CODE__: JSON.stringify(versionCode()), }, server: { // web/ itself, plus contracts/ for the one contract file the app imports at build time: W2's keyword table // (SPEC §15.7, D-055). Vite refuses `?raw` imports outside these paths, in Vitest as well as the dev server, and // matches the id with its query, so the entry must be the directory, not the file. fs: { allow: ['.', '../contracts'] }, proxy: Object.fromEntries(API_PREFIXES.map((prefix) => [prefix, { target: devApi, changeOrigin: true }])), }, build: { outDir: 'dist', sourcemap: false, rolldownOptions: { output: { // Libraries in their own chunks: they change less often than the app, so browsers keep them across deploys. codeSplitting: { groups: [ { name: 'react', test: /node_modules[\\/](react|react-dom|scheduler)[\\/]/ }, // Recharts and its dependencies, loaded only by the analytics pages (S-11, S-12, Insights), so the // shared vendor chunk stays small (it was 516 kB with them). { name: 'charts', test: /node_modules[\\/](recharts|victory-vendor|d3-[a-z-]+|internmap|@reduxjs|redux|redux-thunk|react-redux|reselect|immer|decimal\.js-light|es-toolkit|eventemitter3|tiny-invariant|clsx|use-sync-external-store)[\\/]/, }, { name: 'vendor', test: /node_modules[\\/]/ }, ], }, }, }, }, test: { environment: 'jsdom', setupFiles: ['src/test/setup.ts'], include: ['src/**/*.test.{ts,tsx}'], css: { modules: { classNameStrategy: 'non-scoped' } }, // No file-system module cache (node_modules/.vitest-cache): it once served a stale `import.meta.glob` of // catalog.ts (fixed in 83f5293), and the whole suite runs in seconds without it, so every run transforms afresh. fsModuleCache: false, }, });