import { createServer as createViteServer, createLogger, type ViteDevServer } from 'vite'; import type http from 'http'; import fs from 'fs'; import path from 'path'; import { PKG_DIR, WORKSPACE_DIR } from '../shared/paths.js'; import { log } from '../shared/logger.js'; import { appendFrontendLog } from './frontend-log.js'; let dashboardVite: ViteDevServer | null = null; const stripAnsi = (s: string) => String(s).replace(/\x1b\[[0-9;]*m/g, ''); /** A Vite logger that mirrors error/warn to stdout (preserving the human-facing boot/HMR logs) AND * captures them into the server-side frontend ring, so GET /__bloby/control/logs/frontend surfaces * COMPILE/transform errors even when the browser never ran a line of JS (hard compile failure). */ function makeCaptureLogger() { const logger = createLogger('info'); const origError = logger.error.bind(logger); const origWarn = logger.warn.bind(logger); const origWarnOnce = logger.warnOnce.bind(logger); logger.error = (msg, opts) => { try { appendFrontendLog('vite-error', stripAnsi(msg), opts?.error?.stack ? stripAnsi(opts.error.stack) : undefined); } catch {} origError(msg, opts); }; logger.warn = (msg, opts) => { try { appendFrontendLog('vite-warn', stripAnsi(msg)); } catch {} origWarn(msg, opts); }; logger.warnOnce = (msg, opts) => { try { appendFrontendLog('vite-warn', stripAnsi(msg)); } catch {} origWarnOnce(msg, opts); }; return logger; } export async function startViteDevServers(supervisorPort: number, hmrServer: http.Server): Promise<{ dashboard: number }> { const ports = { dashboard: supervisorPort + 2, }; // vite.config.ts keys the dep-prebundle cache dir by package version (.vite-v) so // each release re-optimizes exactly once. Sweep the previous releases' dirs (best-effort, // async — recharts-sized prebundles are ~10 MB each and would otherwise accumulate forever). try { let current = 'dev'; try { current = JSON.parse(fs.readFileSync(path.join(PKG_DIR, 'package.json'), 'utf-8')).version || 'dev'; } catch {} const nmDir = path.join(WORKSPACE_DIR, 'client', 'node_modules'); for (const name of fs.readdirSync(nmDir)) { if (name.startsWith('.vite-v') && name !== '.vite-v' + current) { fs.rm(path.join(nmDir, name), { recursive: true, force: true }, () => {}); } } } catch { /* no node_modules yet / first boot — nothing to sweep */ } try { dashboardVite = await createViteServer({ configFile: path.join(PKG_DIR, 'vite.config.ts'), server: { port: ports.dashboard, host: '127.0.0.1', strictPort: true, allowedHosts: true, // HMR WebSocket is attached directly to the supervisor's HTTP server. // No clientPort — the browser connects on the same origin the page is served from, // so it works both locally (localhost:3000) and through the relay (riven.bloby.bot:443). hmr: { server: hmrServer }, }, // customLogger captures compile/transform errors into the frontend ring (and still prints // them); it supersedes logLevel, which Vite ignores when a customLogger is provided. customLogger: makeCaptureLogger(), }); await dashboardVite.listen(); } catch (err) { console.error('[vite-dev] ✗ Dashboard Vite failed:', err); throw err; } log.ok(`Vite HMR active — dashboard :${ports.dashboard}`); // Warm up: one fetch of the entry HTML (html transform isn't covered by server.warmup), // then wait for the warmup graph (vite.config.ts now lists the whole src tree) to finish // transforming. __VITE_WARM__ (consumed by the CLI spinner) used to print after only // main.tsx — "warm" now actually means warm. Timeout guard so a huge graph or a wedged // transform can never hang the boot signal. const warm = dashboardVite; Promise.resolve() .then(() => fetch(`http://127.0.0.1:${ports.dashboard}/`).then((r) => r.text()).catch(() => {})) .then(() => Promise.race([ warm ? warm.waitForRequestsIdle() : Promise.resolve(), new Promise((resolve) => setTimeout(resolve, 20_000)), ])) .finally(() => console.log('__VITE_WARM__')); return ports; } /** Tell connected browsers to full-reload via Vite's HMR WebSocket */ export function reloadDashboard(): void { if (!dashboardVite) return; dashboardVite.hot.send({ type: 'full-reload', path: '*' }); } export async function stopViteDevServers(): Promise { if (dashboardVite) { await dashboardVite.close(); dashboardVite = null; } }