Files
controller-v2/vite.config.ts
T

264 lines
8.1 KiB
TypeScript
Raw Normal View History

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) }));
}
});
});
},
};
}
/** 生产构建时剔除 public 中的 __manus__(仅开发调试使用) */
function vitePluginExcludeManusFromDist(): Plugin {
2026-06-02 19:55:09 +08:00
const distDir = path.resolve(import.meta.dirname, "dist/modern");
return {
name: "exclude-manus-from-dist",
closeBundle() {
if (process.env.NODE_ENV !== "production") return;
const manusDir = path.join(distDir, "__manus__");
if (fs.existsSync(manusDir)) {
fs.rmSync(manusDir, { recursive: true, force: true });
}
},
};
}
2026-06-02 19:55:09 +08:00
/** 构建后将统一入口 i.html 复制到 dist/ 根目录 */
function vitePluginCopyRouterEntry(): Plugin {
const src = path.resolve(import.meta.dirname, "client", "i.html");
const dest = path.resolve(import.meta.dirname, "dist", "i.html");
return {
name: "copy-router-entry",
closeBundle() {
if (process.env.NODE_ENV !== "production") return;
const distRoot = path.resolve(import.meta.dirname, "dist");
if (!fs.existsSync(distRoot)) fs.mkdirSync(distRoot, { recursive: true });
fs.copyFileSync(src, dest);
},
};
}
const plugins = [
react(),
tailwindcss(),
jsxLocPlugin(),
vitePluginManusRuntime(),
vitePluginManusDebugCollector(),
vitePluginExcludeManusFromDist(),
2026-06-02 19:55:09 +08:00
vitePluginCopyRouterEntry(),
];
export default defineConfig({
plugins,
2026-06-02 19:55:09 +08:00
base: process.env.NODE_ENV === "production" ? "/x8/v2/modern/" : "/",
resolve: {
alias: {
"@": path.resolve(import.meta.dirname, "client", "src"),
"@shared": path.resolve(import.meta.dirname, "shared"),
"@assets": path.resolve(import.meta.dirname, "attached_assets"),
},
dedupe: ["react", "react-dom"],
},
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',
2026-06-02 19:55:09 +08:00
outDir: path.resolve(import.meta.dirname, "dist/modern"),
emptyOutDir: true,
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
rollupOptions: {
input: {
2026-06-02 19:55:09 +08:00
i: path.resolve(import.meta.dirname, "client", "i.modern.html"),
},
output: {
manualChunks(id) {
if (!id.includes("node_modules")) return;
// Only split echarts (lazy EQ page). Do not manually chunk react, radix, lucide,
// or ai-markdown — forced splits hoist shared helpers and break React.forwardRef.
if (id.includes("/echarts/")) return "echarts-vendor";
},
},
},
// streamdown pulls full @shikijs/langs (~7 MB) + themes + mermaid; lazy-loaded with AIPage.
chunkSizeWarningLimit: 15000,
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: ["**/.*"],
},
},
});