import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import tailwindcss from '@tailwindcss/vite'; import { spawn } from 'child_process'; import { readFileSync, statSync } from 'fs'; import { join, resolve } from 'path'; import { fileURLToPath } from 'url'; import { getSyncRun, getSyncStatus, startSyncRun } from './sync/sync-runner.mjs'; import { readLedgerLoopRuns } from './sync/loop-ledger-to-runs.mjs'; import { querySessionHistory } from './sync/session-history.mjs'; import { publicDemoAssets } from './sync/build-public-assets.mjs'; import { readSnapshot } from './sync/snapshot-generation.mjs'; import { loadEnv } from './sync/load-env.mjs'; const __dirname = fileURLToPath(new URL('.', import.meta.url)); loadEnv(__dirname); // Local-only dev plugin: exposes the same observable background sync contract // as the localhost helper. function meowSyncPlugin() { return { name: 'meow-sync', configureServer(server) { // Local-only guard. These endpoints spawn local processes, so they must // never be reachable from another site. Reject non-localhost Host headers // (DNS-rebinding) and any cross-origin request. Without this, a page the // developer visits during `npm run dev` could POST /api/sync. function blockNonLocal(req, res) { const address = server.httpServer?.address(); const activePort = typeof address === 'object' && address ? address.port : Number(server.config.server.port || 5173); const isLoopback = (hostname) => ['localhost', '127.0.0.1', '::1'] .includes(hostname.replace(/^\[|\]$/g, '').toLowerCase()); const portOf = (url) => Number(url.port || (url.protocol === 'https:' ? 443 : 80)); let okHost = false; try { const host = new URL(`http://${req.headers.host || ''}`); okHost = isLoopback(host.hostname) && portOf(host) === activePort; } catch { /* Reject malformed Host headers. */ } let okOrigin = true; if (req.headers.origin) { try { const origin = new URL(req.headers.origin); const protocol = server.config.server.https ? 'https:' : 'http:'; okOrigin = origin.origin === req.headers.origin && origin.protocol === protocol && isLoopback(origin.hostname) && portOf(origin) === activePort; } catch { okOrigin = false; } } if (!okHost || !okOrigin) { res.statusCode = 403; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ ok: false, error: 'forbidden (local-only endpoint)' })); return true; } return false; } server.middlewares.use('/api/sync/status', (req, res) => { if (req.method !== 'GET') { res.statusCode = 405; res.end(); return; } if (blockNonLocal(req, res)) return; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(getSyncStatus({ repoRoot: server.config.root }))); }); server.middlewares.use('/api/sync/runs', (req, res) => { if (req.method !== 'GET') { res.statusCode = 405; res.end(); return; } if (blockNonLocal(req, res)) return; const runId = new URL(req.url || '/', 'http://localhost').pathname.split('/').filter(Boolean)[0]; const run = getSyncRun(runId); res.statusCode = run ? 200 : 404; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(run || { ok: false, error: 'Sync run not found' })); }); server.middlewares.use('/api/data/snapshot.json', (req, res) => { if (req.method !== 'GET') { res.statusCode = 405; res.end(); return; } if (blockNonLocal(req, res)) return; res.setHeader('Content-Type', 'application/json'); res.setHeader('Cache-Control', 'no-store'); try { const snapshot = readSnapshot(process.env.MEOW_DATA_DIR || join(server.config.root, 'public', 'data')); res.statusCode = snapshot ? 200 : 404; res.end(JSON.stringify(snapshot || { error: 'No generation published yet.' })); } catch { res.statusCode = 503; res.end(JSON.stringify({ error: 'Local snapshot validation failed.' })); } }); server.middlewares.use('/api/session-history/sessions', (req, res) => { if (req.method !== 'GET') { res.statusCode = 405; res.end(); return; } if (blockNonLocal(req, res)) return; try { const url = new URL(req.url || '/', 'http://localhost'); const result = querySessionHistory({ limit: url.searchParams.get('limit'), cursor: url.searchParams.get('cursor'), from: url.searchParams.get('from'), to: url.searchParams.get('to'), project: url.searchParams.get('project'), source: url.searchParams.get('source'), model: url.searchParams.get('model'), expectedVersion: url.searchParams.get('expectedVersion'), snapshotBytes: url.searchParams.get('snapshotBytes'), }); res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(result)); } catch (err) { res.statusCode = err?.code === 'stale_archive_snapshot' ? 409 : 500; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) })); } }); // Dev-mode mirror of the local API's Loop-Ops endpoints (sync/local-api.mjs). // Spec is served statically; runs/gates need a local fallback when their // gitignored JSON files do not exist. for (const [route, file, fallback] of [ ['/api/loop-ops/runs', 'runs.json', () => readLedgerLoopRuns()], ['/api/loop-ops/gates', 'gates.json', () => []], ]) { server.middlewares.use(route, (req, res) => { if (req.method !== 'GET') { res.statusCode = 405; res.end(); return; } if (blockNonLocal(req, res)) return; let payload; try { payload = JSON.parse(readFileSync(join(server.config.root, 'public', 'data', 'loop-ops', file), 'utf8')); } catch { payload = fallback(); } res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(payload)); }); } server.middlewares.use('/api/loop-ops/status', (req, res) => { if (req.method !== 'GET') { res.statusCode = 405; res.end(); return; } if (blockNonLocal(req, res)) return; const dir = join(server.config.root, 'public', 'data', 'loop-ops'); const files = {}; for (const name of ['spec.json', 'gates.json', 'runs.json']) { try { const st = statSync(join(dir, name)); files[name] = { mtime: st.mtimeMs, size: st.size }; } catch { files[name] = null; } } res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ ok: files['spec.json'] !== null, files, productionWritesEnabled: false })); }); server.middlewares.use('/api/loop-ops/sync', (req, res) => { if (req.method !== 'POST') { res.statusCode = 405; res.end(); return; } if (blockNonLocal(req, res)) return; // No env option — the child inherits the dev server's environment. const child = spawn('node', [join(server.config.root, 'sync', 'loop-ops-import.mjs')], { cwd: server.config.root, }); let stdout = ''; let stderr = ''; child.stdout.on('data', (c) => { stdout += c.toString(); }); child.stderr.on('data', (c) => { stderr += c.toString(); }); const timeout = setTimeout(() => child.kill(), 90_000); child.on('close', (code) => { clearTimeout(timeout); let mtime = null; try { mtime = statSync(join(server.config.root, 'public', 'data', 'loop-ops', 'spec.json')).mtimeMs; } catch { /* not written yet */ } res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ ok: code === 0, code, stdout: stdout.slice(-2000), stderr: stderr.slice(-500), mtime })); }); child.on('error', (err) => { clearTimeout(timeout); res.statusCode = 500; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ ok: false, error: err.message })); }); }); // Dev-mode mirror of sync/local-api.mjs for the hidden Seats page. server.middlewares.use('/api/superadmin-usage/status', (req, res) => { if (req.method !== 'GET') { res.statusCode = 405; res.end(); return; } if (blockNonLocal(req, res)) return; try { const filePath = join(server.config.root, 'public', 'data', 'superadmin-usage.json'); const stat = statSync(filePath); res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ ok: true, mtime: stat.mtimeMs, size: stat.size, productionWritesEnabled: false, })); } catch { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ ok: false, productionWritesEnabled: false })); } }); server.middlewares.use('/api/superadmin-usage/sync', (req, res) => { if (req.method !== 'POST') { res.statusCode = 405; res.end(); return; } if (blockNonLocal(req, res)) return; const child = spawn('node', [join(server.config.root, 'sync', 'superadmin-usage.mjs')], { cwd: server.config.root, env: process.env, }); let stdout = ''; let stderr = ''; child.stdout.on('data', (c) => { stdout += c.toString(); }); child.stderr.on('data', (c) => { stderr += c.toString(); }); const timeout = setTimeout(() => child.kill(), 120_000); child.on('close', (code) => { clearTimeout(timeout); let mtime = null; try { mtime = statSync(join(server.config.root, 'public', 'data', 'superadmin-usage.json')).mtimeMs; } catch { /* not written yet */ } res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ ok: code === 0, code, stdout: stdout.slice(-2000), stderr: stderr.slice(-500), mtime })); }); child.on('error', (err) => { clearTimeout(timeout); res.statusCode = 500; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ ok: false, error: err.message })); }); }); server.middlewares.use('/api/sync', (req, res) => { if (req.method !== 'POST') { res.statusCode = 405; res.end(); return; } if (blockNonLocal(req, res)) return; const started = startSyncRun({ repoRoot: server.config.root, node: process.execPath, trigger: 'dashboard-dev', env: process.env, }); res.statusCode = started.accepted ? 202 : 409; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ ok: started.accepted, accepted: started.accepted, busy: started.busy, run_id: started.run_id, status: started.snapshot, })); }); }, }; } export default defineConfig({ resolve: { alias: { '@': resolve(__dirname, 'src'), }, // dedupe: force a single copy of three / react / react-dom across the // dep tree. Without this, @react-three/postprocessing transitively pulls // stats-gl which ships its own three, causing "Multiple instances of // THREE" + "Invalid hook call" runtime errors that black-screen the // canvas. With dedupe, all three.js + react references resolve to the // root install, fixing bloom + future R3F-postprocessing usage. dedupe: ['three', 'react', 'react-dom'], }, plugins: [react(), tailwindcss(), meowSyncPlugin(), publicDemoAssets(join(__dirname, 'public'))], build: { copyPublicDir: false, // Split only stable vendor groups identified by a local generateBundle // module-size pass. includeDependenciesRecursively is off so react / // react-dom stay in the app graph (pulling them into a vendor group // would create circular chunk init). Route surfaces are split in App.jsx. rolldownOptions: { output: { codeSplitting: { includeDependenciesRecursively: false, groups: [ { name: 'vendor-three', test: /[\\/]node_modules[\\/]three[\\/]/, priority: 40 }, { name: 'vendor-r3f', test: /[\\/]node_modules[\\/](?:@react-three|three-stdlib)[\\/]/, priority: 30 }, { name: 'vendor-ag-grid', test: /[\\/]node_modules[\\/]ag-grid-/, priority: 30 }, // recharts stays in the app graph. Splitting it into vendor-recharts // left createSelector undefined in the production chunk (Today and // Ledger threw "s is not a function" inside the legend selector). { name: 'vendor-xyflow', test: /[\\/]node_modules[\\/]@xyflow[\\/]/, priority: 30 }, { name: 'vendor-motion', test: /[\\/]node_modules[\\/](?:framer-motion|motion-dom|motion-utils)[\\/]/, priority: 30 }, ], }, }, }, }, });