import { loadEnv, type Plugin } from 'vite'; import type { SessionPersona } from '../../project/persona.js'; import { NoopAnalyticsRecorder, SqliteAnalyticsRecorder } from './analytics.js'; import { loadConfig } from './config.js'; import { resolveSessionDefaults } from './defaults.js'; import { selectProvider } from './providers/index.js'; import type { ToolRegistry } from './tools.js'; import { handleRealtimeUpgrade } from './ws-handler.js'; export interface RealtimeDevWsPluginOptions { /** * Raw, env-independent pieces (project persona + tool registry) — taken as * parameters rather than imported here so this module (server/core) never * has to import project code directly. See vite.config.ts, the * composition root that supplies these, and docs/TOOLS.md. * * Deliberately NOT pre-resolved against env vars by the caller: resolving * them happens inside configureServer below, gated by the VITEST check — * see the comment on that check for why. */ persona: SessionPersona; toolRegistry: ToolRegistry; } /** * Wires the same upgrade handling used in production (see repo-root * server.js) into Vite's dev server, so `npm run dev` supports the realtime * WS endpoint without a production build. * * Loads .env itself: this hook runs at dev-server startup, outside * SvelteKit's own request/module graph, so `$env/...` isn't available here — * see the note in ./tokens.ts for why this whole module tree reads * `process.env` directly instead. */ export function realtimeDevWsPlugin(options: RealtimeDevWsPluginOptions): Plugin { return { name: 'webavatar-realtime-ws-dev', configureServer(server) { // vitest's browser-mode project boots its own Vite dev server for // component tests; this plugin has nothing to do there. Critically, // this guard is also what keeps loadConfig()'s fail-fast validation // (below) from running during `npm run test:unit` — that suite's // `test.projects` entries both `extends: './vite.config.ts'`, so this // plugin's config gets evaluated there too, and unit tests need no env // vars set at all. Don't move config resolution above this line. if (process.env.VITEST) return; const loaded = loadEnv('development', process.cwd(), ''); for (const [key, value] of Object.entries(loaded)) { if (process.env[key] === undefined) process.env[key] = value; } const config = loadConfig(); const provider = selectProvider(config); const recorder = config.analytics.enabled ? new SqliteAnalyticsRecorder(config.analytics.dbPath, config.analytics) : new NoopAnalyticsRecorder(); const defaults = { ...resolveSessionDefaults(options.persona, config.session), toolRegistry: options.toolRegistry }; server.httpServer?.on('upgrade', (req, socket, head) => { handleRealtimeUpgrade(req, socket, head, { provider, defaults, recorder, clientToolTimeoutMs: config.session.clientToolTimeoutMs, // This plugin only ever runs in dev (see the VITEST guard above), // so this is a literal, not an env lookup — debug-log forwarding // is frictionless here. See ../../../../server.js for the prod side. isDev: true, // Unlike isDev, this reads from config (not a literal) — a // developer may still want DEBUG_LOG_MESSAGES=false locally, e.g. // to rehearse prod-like log hygiene. See docs/LOGGING.md. logMessages: config.debug.messages }); }); } }; }