step③ game-studio 联调(编译验证待 mini-desktop): - request.ts 注入 tenant-id:1;user.ts token 取 VITE_STUDIO_TOKEN(=test1) - vite.config.ts: VITE_API_BASE 非空则关 mock + 启动打印 baseURL/mock 状态 - runtimeApi.ts: 取包改委托 src/api/runtime.ts(带 base+token+tenant,修 Codex C3 裸 axios 同源) - inject.ts: manifestUrl resolve 到 VITE_API_BASE + 带鉴权头(修 Codex C4) - telemetry: 信封补 eventId(randomUUID)+ traceId 兜底非空;sendBeacon→fetch keepalive 带头(sendBeacon 不能带 Authorization/tenant) - Play.vue onTelemetry 白名单原样转发 like/share/play_end(修 Codex H8 吞事件) - 新增 game-studio/.env.staging + vite-env.d.ts;.gitignore 豁免 .env.staging(防 gitignore-swallows-frontend-source 同类坑) - 注:Feed.vue 互动经 buildEnvelope traceId 兜底覆盖,免改 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
111 lines
4.3 KiB
TypeScript
111 lines
4.3 KiB
TypeScript
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<unknown> {
|
||
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)),
|
||
},
|
||
},
|
||
}
|
||
})
|