UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

130 lines (129 loc) 4.62 kB
import fs from 'node:fs'; import path from 'node:path'; import { AppConfigSchema } from '../../shared/schemas/config.js'; /** * ConfigLoader - 配置加载器 * 直接读取 JSON 配置文件,避免 node-config 模块在 import 阶段就读取配置目录的时序问题 */ export class ConfigLoader { static instance = null; static config = null; /** * 沿目录树向上查找包含 package.json(name=autosnippet)的目录。 * ConfigLoader 是最早加载的模块之一,不能依赖 package-root.ts,因此内联实现。 */ static _findPackageRoot() { let dir = import.meta.dirname; for (let i = 0; i < 10; i++) { const candidate = path.join(dir, 'package.json'); if (fs.existsSync(candidate)) { try { const pkg = JSON.parse(fs.readFileSync(candidate, 'utf-8')); if (pkg.name === 'autosnippet') { return dir; } } catch { /* continue */ } } const parent = path.dirname(dir); if (parent === dir) { break; } dir = parent; } throw new Error('[ConfigLoader] Could not locate package root'); } static load(env = process.env.NODE_ENV || 'development') { if (!this.config) { // 使用包根自动发现,避免硬编码 ../../../.. 层级 const configDir = path.join(ConfigLoader._findPackageRoot(), 'config'); // 加载默认配置 const defaultPath = path.join(configDir, 'default.json'); let merged = {}; if (fs.existsSync(defaultPath)) { merged = JSON.parse(fs.readFileSync(defaultPath, 'utf8')); } // 加载环境专用配置(覆盖默认) const envPath = path.join(configDir, `${env}.json`); if (fs.existsSync(envPath)) { const envConfig = JSON.parse(fs.readFileSync(envPath, 'utf8')); merged = this._deepMerge(merged, envConfig); } // 加载 local 配置(开发者覆盖,不入版本控制) const localPath = path.join(configDir, 'local.json'); if (fs.existsSync(localPath)) { const localConfig = JSON.parse(fs.readFileSync(localPath, 'utf8')); merged = this._deepMerge(merged, localConfig); } merged.env = env; // Zod 运行时校验(非阻塞,仅警告) const result = AppConfigSchema.safeParse(merged); if (!result.success) { const issues = result.error.issues .map((i) => ` ${i.path.join('.')}: ${i.message}`) .join('\n'); process.stderr.write(`[ConfigLoader] ⚠️ Config validation warnings:\n${issues}\n`); } this.config = merged; } return this.config; } static _deepMerge(target, source) { const output = { ...target }; for (const key of Object.keys(source)) { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key]) && target[key] && typeof target[key] === 'object' && !Array.isArray(target[key])) { output[key] = this._deepMerge(target[key], source[key]); } else { output[key] = source[key]; } } return output; } static get(key) { if (!this.config) { this.load(); } const keys = key.split('.'); let value = this.config; for (const k of keys) { value = value?.[k]; if (value === undefined) { throw new Error(`Config key not found: ${key}`); } } return value; } static has(key) { try { this.get(key); return true; } catch { return false; } } static set(key, value) { if (!this.config) { this.load(); } const keys = key.split('.'); let obj = this.config; for (let i = 0; i < keys.length - 1; i++) { const k = keys[i]; if (!obj[k]) { obj[k] = {}; } obj = obj[k]; } obj[keys[keys.length - 1]] = value; } } export default ConfigLoader;