import { jsxLocPlugin } from "@builder.io/vite-plugin-jsx-loc"; import tailwindcss from "@tailwindcss/vite"; import react from "@vitejs/plugin-react"; import fs from "node:fs"; import path from "node:path"; import { defineConfig, type Plugin, type ViteDevServer } from "vite"; import { vitePluginManusRuntime } from "vite-plugin-manus-runtime"; // ============================================================================= // Manus Debug Collector - Vite Plugin // Writes browser logs directly to files, trimmed when exceeding size limit // ============================================================================= const PROJECT_ROOT = import.meta.dirname; const LOG_DIR = path.join(PROJECT_ROOT, ".manus-logs"); const MAX_LOG_SIZE_BYTES = 1 * 1024 * 1024; // 1MB per log file const TRIM_TARGET_BYTES = Math.floor(MAX_LOG_SIZE_BYTES * 0.6); // Trim to 60% to avoid constant re-trimming type LogSource = "browserConsole" | "networkRequests" | "sessionReplay"; function ensureLogDir() { if (!fs.existsSync(LOG_DIR)) { fs.mkdirSync(LOG_DIR, { recursive: true }); } } function trimLogFile(logPath: string, maxSize: number) { try { if (!fs.existsSync(logPath) || fs.statSync(logPath).size <= maxSize) { return; } const lines = fs.readFileSync(logPath, "utf-8").split("\n"); const keptLines: string[] = []; let keptBytes = 0; // Keep newest lines (from end) that fit within 60% of maxSize const targetSize = TRIM_TARGET_BYTES; for (let i = lines.length - 1; i >= 0; i--) { const lineBytes = Buffer.byteLength(`${lines[i]}\n`, "utf-8"); if (keptBytes + lineBytes > targetSize) break; keptLines.unshift(lines[i]); keptBytes += lineBytes; } fs.writeFileSync(logPath, keptLines.join("\n"), "utf-8"); } catch { /* ignore trim errors */ } } function writeToLogFile(source: LogSource, entries: unknown[]) { if (entries.length === 0) return; ensureLogDir(); const logPath = path.join(LOG_DIR, `${source}.log`); // Format entries with timestamps const lines = entries.map((entry) => { const ts = new Date().toISOString(); return `[${ts}] ${JSON.stringify(entry)}`; }); // Append to log file fs.appendFileSync(logPath, `${lines.join("\n")}\n`, "utf-8"); // Trim if exceeds max size trimLogFile(logPath, MAX_LOG_SIZE_BYTES); } /** * Vite plugin to collect browser debug logs * - POST /__manus__/logs: Browser sends logs, written directly to files * - Files: browserConsole.log, networkRequests.log, sessionReplay.log * - Auto-trimmed when exceeding 1MB (keeps newest entries) */ function vitePluginManusDebugCollector(): Plugin { return { name: "manus-debug-collector", transformIndexHtml(html) { if (process.env.NODE_ENV === "production") { return html; } return { html, tags: [ { tag: "script", attrs: { src: "/__manus__/debug-collector.js", defer: true, }, injectTo: "head", }, ], }; }, configureServer(server: ViteDevServer) { // POST /__manus__/logs: Browser sends logs (written directly to files) server.middlewares.use("/__manus__/logs", (req, res, next) => { if (req.method !== "POST") { return next(); } const handlePayload = (payload: any) => { // Write logs directly to files if (payload.consoleLogs?.length > 0) { writeToLogFile("browserConsole", payload.consoleLogs); } if (payload.networkRequests?.length > 0) { writeToLogFile("networkRequests", payload.networkRequests); } if (payload.sessionEvents?.length > 0) { writeToLogFile("sessionReplay", payload.sessionEvents); } res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ success: true })); }; const reqBody = (req as { body?: unknown }).body; if (reqBody && typeof reqBody === "object") { try { handlePayload(reqBody); } catch (e) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ success: false, error: String(e) })); } return; } let body = ""; req.on("data", (chunk) => { body += chunk.toString(); }); req.on("end", () => { try { const payload = JSON.parse(body); handlePayload(payload); } catch (e) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ success: false, error: String(e) })); } }); }); }, }; } const plugins = [react(), tailwindcss(), jsxLocPlugin(), vitePluginManusRuntime(), vitePluginManusDebugCollector()]; export default defineConfig({ plugins, base: process.env.NODE_ENV === 'production' ? '/x8/v2/public/' : '/', resolve: { alias: { "@": path.resolve(import.meta.dirname, "client", "src"), "@shared": path.resolve(import.meta.dirname, "shared"), "@assets": path.resolve(import.meta.dirname, "attached_assets"), }, }, envDir: path.resolve(import.meta.dirname), root: path.resolve(import.meta.dirname, "client"), publicDir: path.resolve(import.meta.dirname, "client/public"), // 性能优化 build: { target: 'esnext', cssTarget: 'esnext', outDir: path.resolve(import.meta.dirname, "dist/public"), emptyOutDir: true, minify: 'terser', terserOptions: { compress: { drop_console: true, drop_debugger: true, }, }, rollupOptions: { output: { manualChunks(id) { if (!id.includes("node_modules")) return; // Keep core runtime dependencies stable for better long-term caching. if (id.includes("node_modules/react/") || id.includes("node_modules/react-dom/")) { return "react-vendor"; } if (id.includes("node_modules/wouter/")) { return "router-vendor"; } // Split large libraries into their own chunks to avoid a single huge entry bundle. if (id.includes("node_modules/echarts/")) return "echarts-vendor"; if (id.includes("node_modules/recharts/")) return "recharts-vendor"; if (id.includes("node_modules/framer-motion/")) return "motion-vendor"; if (id.includes("node_modules/@radix-ui/")) return "radix-vendor"; // Group frequently used small UI helpers. if ( id.includes("node_modules/lucide-react/") || id.includes("node_modules/class-variance-authority/") || id.includes("node_modules/clsx/") || id.includes("node_modules/tailwind-merge/") ) { return "ui-vendor"; } // Fall back to per-package vendor chunks. const packageMatch = id.match( /node_modules[\\/](?:\.pnpm[\\/][^\\/]+[\\/]node_modules[\\/])?(@?[^\\/]+(?:[\\/][^\\/]+)?)/, ); const packagePath = packageMatch?.[1]; if (!packagePath) return "vendor"; const packageName = packagePath.startsWith("@") ? packagePath.split(/[\\/]/).slice(0, 2).join("_") : packagePath.split(/[\\/]/)[0]; return `vendor-${packageName}`; }, // Prevent Rollup from merging manual chunks back into larger bundles. onlyExplicitManualChunks: true, }, }, chunkSizeWarningLimit: 500, cssCodeSplit: true, }, server: { port: 3000, strictPort: false, // Will find next available port if 3000 is busy host: true, // Browser requests with Origin are rejected (403) by the cloud API; proxy in dev avoids CORS/WAF. proxy: { "/luxsin-audio-api": { target: "https://api.luxsin.com.cn", changeOrigin: true, rewrite: (p) => p.replace(/^\/luxsin-audio-api/, "/audio"), }, }, allowedHosts: [ ".manuspre.computer", ".manus.computer", ".manus-asia.computer", ".manuscomputer.ai", ".manusvm.computer", "localhost", "127.0.0.1", ], fs: { strict: true, deny: ["**/.*"], }, }, });