base.js 4.95 KB
/**
 * 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<string, unknown>} 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<string, unknown>} */ (out)[key]
    if (isPlainObject(b) && isPlainObject(o)) {
      /** @type {Record<string, unknown>} */ (out)[key] = deepMerge(b, o)
    } else {
      /** @type {Record<string, unknown>} */ (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<string, string | undefined>, command?: string }} [options]
 * @returns {Record<string, unknown>}
 */
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'
}