/** * Vite 预设的「默认值」与共享工具 —— base config + 深合并工具。 * * `index.js` 持有工厂与合并逻辑,本文件持有默认数据(Vue 插件、workspace 解析、build 约定、 * SPA base)与无副作用的 `deepMerge`。 * * 本文件读 `process.env`(构建期判 dev/prod、开关调试插件)——这是 Vite 配置的合理需求,不是散读 * env,故 `packages/config/vite/base.js` 已列入连接规则白名单(tools/eslint-rules/config.js)。 * * visual-feedback 是开发期反馈组件,不再由本预设硬注入(否则 config 就得依赖它)。需要它的 app * 在自己的 `vite.config.js` 里显式 `plugins.push(visualFeedbackPlugin())`,自选、自担依赖。 */ import vue from '@vitejs/plugin-vue' import { codeInspectorPlugin } from 'code-inspector-plugin' /** * 是否为可深合并的纯对象(非数组、非 null、非类实例如 plugin)。 * @param {unknown} v * @returns {boolean} */ function isPlainObject(v) { if (v === null || typeof v !== 'object' || Array.isArray(v)) return false const proto = Object.getPrototypeOf(v) return proto === Object.prototype || proto === null } /** * 递归深合并:同名嵌套对象逐层合并(保留未被覆盖的默认子字段),数组与标量整体替换。 * 这是 §5 review 关注点 2「override 是深合并不是浅覆盖」的实现:app 传 `server.proxy` * 只覆盖 `server.proxy`,不把整个 `server` 段(含默认 port 等)抹掉。 * @template T * @param {T} base * @param {Record} override * @returns {T} */ export function deepMerge(base, override) { if (!isPlainObject(override)) return /** @type {T} */ (override ?? base) const out = isPlainObject(base) ? { ...base } : {} for (const key of Object.keys(override)) { const o = override[key] const b = /** @type {Record} */ (out)[key] if (isPlainObject(b) && isPlainObject(o)) { /** @type {Record} */ (out)[key] = deepMerge(b, o) } else { /** @type {Record} */ (out)[key] = o } } return /** @type {T} */ (out) } /** 标识值:base 默认携带的 Vue 插件实例(供测试断言「默认 config 含 Vue 插件」)。 */ export const VUE_PLUGIN_NAME = 'vite:vue' /** * `@drama/*` / `@app/*` workspace 别名解析约定。 * monorepo 下包名经 workspace 解析即可,这里显式声明 `conditions` 让 Vite 优先走源码 `exports`, * 保证 dev 直接跑源码、无编译步骤(架构基线 §3)。 * */ export const WORKSPACE_RESOLVE = { // 让 @drama/* / @app/* 走包 exports 的 import 条件,命中各包 src/index.js。 conditions: ['import', 'module', 'browser', 'default'], dedupe: ['vue'], } /** * 产出 base Vite config(不含 overrides)。每次调用返回新对象 + 新插件实例,避免跨 app 共享可变状态。 * @param {{ env?: Record, command?: string }} [options] * @returns {Record} */ export function buildBaseConfig(options = {}) { const env = options.env ?? process.env const command = options.command ?? commandFromProcessArgv(process.argv) const isDev = command !== 'build' && env.VITEST !== 'true' const enableInspector = isVisualFeedbackInspectorEnabled(env, command) return { plugins: [ vue(), enableInspector && codeInspectorPlugin({ bundler: 'vite', showSwitch: false, hideConsole: true, autoToggle: false, behavior: { locate: false, copy: false, target: '', }, server: isDev ? 'open' : 'close', dev: enableInspector, skipSnippets: ['htmlScript', 'console'], }), ].filter(Boolean), resolve: { ...WORKSPACE_RESOLVE }, base: '/', build: { outDir: 'dist', emptyOutDir: true, sourcemap: true, target: 'es2022', }, server: { port: 5173, strictPort: false, allowedHosts: allowedHostsFromEnv(env), }, } } /** * 反代进来的 Host 白名单。 * * 部署成 `frontend_mode: proxy` 的环境里,nginx 用 `proxy_set_header Host $host` 把公网域名 * 原样透传给 dev server,而 Vite 只放行 localhost 和 IP,别的域名一律 403——页面打不开, * 日志里也只有一行「This host is not allowed」。 * * 域名是环境差异,所以只从各环境自己的 .env 读(`VITE_ALLOWED_HOSTS=a.example,b.example`), * 仓库里不硬编码任何域名。不配就是空数组,行为和以前完全一样。 */ function allowedHostsFromEnv(env) { const raw = env.VITE_ALLOWED_HOSTS if (typeof raw !== 'string') return [] return raw.split(',').map(host => host.trim()).filter(host => host !== '') } function isVisualFeedbackInspectorEnabled(env, _command) { if (env.VITEST === 'true') return false return env.VISUAL_FEEDBACK === 'true' } function commandFromProcessArgv(argv) { return argv.includes('build') ? 'build' : 'serve' }