import { fileURLToPath, URL } from 'node:url' import type { IncomingMessage } from 'node:http' import { defineConfig, loadEnv, type Connect, type PluginOption } from 'vite' import vue from '@vitejs/plugin-vue' // 自写 mock 调度器(DOM-free,dev/preview 两端复用)。严格按 contracts/api-schemas 返回数据。 import { dispatch, MOCK_PREFIXES } from './src/mock/index.ts' /** * 读取请求体(POST/PUT 等),解析为 JSON。 * connect 中间件层面手动收 chunk,避免依赖 body-parser(零额外依赖)。 */ function readBody(req: IncomingMessage): Promise { return new Promise((resolve) => { const chunks: Buffer[] = [] req.on('data', (c: Buffer) => chunks.push(c)) req.on('end', () => { if (chunks.length === 0) { resolve(undefined) return } const raw = Buffer.concat(chunks).toString('utf-8') try { resolve(raw ? JSON.parse(raw) : undefined) } catch { // 非 JSON 体(如 sendBeacon 偶发)直接透传原文,handler 自行兜底 resolve(raw) } }) req.on('error', () => resolve(undefined)) }) } /** 判断 url 是否命中 mock 前缀(/app-api、/admin-api) */ function isMockUrl(url: string | undefined): boolean { if (!url) return false return MOCK_PREFIXES.some((p) => url === p || url.startsWith(p + '/') || url.startsWith(p + '?')) } /** * connect 中间件:拦截 API 请求 → 调 dispatch → 写 JSON 包络。 * 未命中 mock 路由则调用 next() 放行(不误吞静态资源/HMR 等)。 */ const mockMiddleware: Connect.NextHandleFunction = (req, res, next) => { const url = req.url if (!isMockUrl(url)) { next() return } void (async () => { try { const body = await readBody(req) const result = await dispatch(req.method || 'GET', url as string, body) if (result === null) { // 命中前缀但无对应路由:返回 404 包络,便于排查“契约里没有的端点”被误调 res.statusCode = 404 res.setHeader('Content-Type', 'application/json; charset=utf-8') res.end(JSON.stringify({ code: 404, data: null, msg: `mock 未匹配: ${req.method} ${url}` })) return } res.statusCode = 200 res.setHeader('Content-Type', 'application/json; charset=utf-8') res.end(JSON.stringify(result)) } catch (err) { // 兜底:mock 自身异常也返回包络,不让请求悬挂 res.statusCode = 200 res.setHeader('Content-Type', 'application/json; charset=utf-8') res.end(JSON.stringify({ code: 1, data: null, msg: (err as Error)?.message || 'mock error' })) } })() } /** * 自写 mock vite 插件。 * 关键:同时 hook configureServer(dev) 与 configurePreviewServer(preview), * 两端都把 mockMiddleware 前置注入 connect 栈 → `vite` 与 `vite preview` 都能命中 mock(验收要求)。 */ function mockPlugin(): PluginOption { return { name: 'wanxiang-local-mock', // 开发服务器(vite dev) configureServer(server: { middlewares: Connect.Server }) { server.middlewares.use(mockMiddleware) }, // 预览服务器(vite preview)——验收闭环在 preview 跑,必须生效 configurePreviewServer(server: { middlewares: Connect.Server }) { server.middlewares.use(mockMiddleware) }, } } // https://vite.dev/config/ export default defineConfig(({ mode }) => { // 读 env(第三参 '' = 不限前缀,确保拿到 VITE_API_BASE)。 // VITE_API_BASE 非空 = 联调真连后端(如 staging),此时关本地 mock;空 = 同源 mock(§2.3)。 const env = loadEnv(mode, process.cwd(), '') const apiBase = env.VITE_API_BASE ?? '' const mockEnabled = !apiBase // 启动打印:一眼确认"真连后端"还是"mock" console.log(`[wanxiang] mode=${mode} VITE_API_BASE=${apiBase || '(空=同源/mock)'} mock=${mockEnabled ? 'ON' : 'OFF(真连后端)'}`) return { // mockEnabled=false 时不注册 mock 插件,请求直达 VITE_API_BASE plugins: [vue(), ...(mockEnabled ? [mockPlugin()] : [])], resolve: { alias: { // '@' → src(供 B 工位用 @/ 绝对导入;A1 自身用相对导入,避免依赖 tsconfig paths) '@': fileURLToPath(new URL('./src', import.meta.url)), }, }, } })