import tailwindcss from '@tailwindcss/vite'; import react from '@vitejs/plugin-react'; import fs from 'fs/promises'; import { existsSync } from 'fs'; import path from 'path'; import { defineConfig, Plugin } from 'vite'; import { deleteDocumentByFileName, getDocumentByFileName, getDocumentById, getVocabCards, initDatabase, listDocuments, migrateSavesToDatabase, saveDocument, } from './server/db.js'; const SAVES_DIR = path.resolve(__dirname, 'saves'); const ensureSavesDir = async () => { await fs.mkdir(SAVES_DIR, { recursive: true }); }; const sanitizeSaveName = (name: string) => { const cleaned = name .replace(/\.json$/i, '') .replace(/[<>:"/\\|?*\x00-\x1F]/g, '_') .trim(); return `${cleaned || `save-${Date.now()}`}.json`; }; const readJsonBody = async (req: any) => { const chunks: Buffer[] = []; for await (const chunk of req) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } const raw = Buffer.concat(chunks).toString('utf-8'); return raw ? JSON.parse(raw) : {}; }; const sendJson = (res: any, statusCode: number, payload: unknown) => { res.statusCode = statusCode; res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.end(JSON.stringify(payload)); }; const localSavesPlugin = (): Plugin => ({ name: 'local-saves-api', configureServer(server) { const ready = (async () => { await initDatabase(); await ensureSavesDir(); await migrateSavesToDatabase(SAVES_DIR); })(); server.middlewares.use(async (req, res, next) => { if (!req.url?.startsWith('/api/saves') && !req.url?.startsWith('/api/documents') && !req.url?.startsWith('/api/vocab')) { next(); return; } try { await ready; await ensureSavesDir(); const url = new URL(req.url, 'http://localhost'); const fileNameFromPath = decodeURIComponent(url.pathname.replace(/^\/api\/saves\/?/, '')); if (req.method === 'GET' && url.pathname === '/api/saves') { const files = await listDocuments(); sendJson(res, 200, { files }); return; } if (req.method === 'GET' && url.pathname.startsWith('/api/saves/') && fileNameFromPath) { const fileName = sanitizeSaveName(fileNameFromPath); const document = await getDocumentByFileName(fileName); if (document) { sendJson(res, 200, document); return; } const filePath = path.join(SAVES_DIR, fileName); if (!existsSync(filePath)) { sendJson(res, 404, { error: 'Save file not found' }); return; } const content = await fs.readFile(filePath, 'utf-8'); sendJson(res, 200, JSON.parse(content)); return; } if (req.method === 'POST' && url.pathname === '/api/saves') { const body = await readJsonBody(req); const fileName = sanitizeSaveName(String(body.name || 'current')); const document = body.document; if (!document || typeof document !== 'object') { sendJson(res, 400, { error: 'Missing document payload' }); return; } const filePath = path.join(SAVES_DIR, fileName); await fs.writeFile(filePath, JSON.stringify(document, null, 2), 'utf-8'); const file = await saveDocument({ name: fileName, document }); sendJson(res, 200, { file }); return; } if (req.method === 'DELETE' && url.pathname.startsWith('/api/saves/') && fileNameFromPath) { const fileName = sanitizeSaveName(fileNameFromPath); const filePath = path.join(SAVES_DIR, fileName); if (!existsSync(filePath)) { sendJson(res, 404, { error: 'Save file not found' }); return; } await fs.unlink(filePath); await deleteDocumentByFileName(fileName); sendJson(res, 200, { ok: true }); return; } if (req.method === 'GET' && url.pathname === '/api/documents') { const files = await listDocuments(); sendJson(res, 200, { files }); return; } if (req.method === 'GET' && url.pathname.startsWith('/api/documents/')) { const id = decodeURIComponent(url.pathname.replace(/^\/api\/documents\/?/, '')); const document = await getDocumentById(id); if (!document) { sendJson(res, 404, { error: 'Document not found' }); return; } sendJson(res, 200, document); return; } if (req.method === 'POST' && url.pathname === '/api/documents') { const body = await readJsonBody(req); const document = body.document; if (!document || typeof document !== 'object') { sendJson(res, 400, { error: 'Missing document payload' }); return; } const file = await saveDocument({ name: String(body.name || document.title || 'current'), document }); await fs.writeFile(path.join(SAVES_DIR, file.name), JSON.stringify(document, null, 2), 'utf-8'); sendJson(res, 200, { file }); return; } if (req.method === 'GET' && url.pathname === '/api/vocab') { const cards = await getVocabCards(); sendJson(res, 200, { cards }); return; } if (req.method === 'GET' && url.pathname.startsWith('/api/vocab/')) { const term = decodeURIComponent(url.pathname.replace(/^\/api\/vocab\/?/, '')); const cards = await getVocabCards(); const card = cards.find((item: any) => item.normalizedText === term || item.text === term); if (!card) { sendJson(res, 404, { error: 'Vocab card not found' }); return; } sendJson(res, 200, { card }); return; } sendJson(res, 405, { error: 'Unsupported saves API request' }); } catch (error) { console.error(error); sendJson(res, 500, { error: 'Local saves API failed' }); } }); }, }); export default defineConfig(() => { return { base: './', plugins: [react(), tailwindcss(), localSavesPlugin()], resolve: { alias: { '@': path.resolve(__dirname, '.'), }, }, server: { // HMR is disabled in AI Studio via DISABLE_HMR env var. // Do not modify—file watching is disabled to prevent flickering during agent edits. hmr: process.env.DISABLE_HMR !== 'true', // Disable file watching when DISABLE_HMR is true to save CPU during agent edits. watch: process.env.DISABLE_HMR === 'true' ? null : {}, }, }; });