env-plugin 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/bin/index.js +23 -0
  3. package/dist/Container.js +55 -0
  4. package/dist/Plugin.js +25 -0
  5. package/dist/PostProxyServer.js +85 -0
  6. package/dist/client/assets/element-plus-wndYbaZY.js.gz +0 -0
  7. package/dist/client/assets/index-Bl3B2M1W.js.gz +0 -0
  8. package/dist/client/assets/index-nrcddKJg.css.gz +0 -0
  9. package/dist/client/assets/vue-BGFZjnqd.js.gz +0 -0
  10. package/dist/client/favicon.ico +0 -0
  11. package/dist/client/index.html +16 -0
  12. package/dist/constants/responseCode.js +27 -0
  13. package/dist/controllers/DevServerController.js +102 -0
  14. package/dist/controllers/EnvController.js +177 -0
  15. package/dist/controllers/PasswordController.js +106 -0
  16. package/dist/controllers/RouteRuleController.js +106 -0
  17. package/dist/dto/BaseRes.js +1 -0
  18. package/dist/dto/EnvDTO.js +4 -0
  19. package/dist/index.js +108 -0
  20. package/dist/middleware/dto.middleware.js +38 -0
  21. package/dist/middleware/globalErrorHandler.js +42 -0
  22. package/dist/middleware/responseEnhancer.js +52 -0
  23. package/dist/models/DevServerModel.js +1 -0
  24. package/dist/models/EnvModel.js +14 -0
  25. package/dist/repositories/DevServerRepo.js +101 -0
  26. package/dist/repositories/EnvRepo.js +121 -0
  27. package/dist/repositories/PasswordRepo.js +142 -0
  28. package/dist/repositories/RouteRuleRepo.js +106 -0
  29. package/dist/repositories/database.js +95 -0
  30. package/dist/routes/index.js +78 -0
  31. package/dist/service/DevServerService.js +189 -0
  32. package/dist/service/EnvService.js +214 -0
  33. package/dist/service/PasswordService.js +100 -0
  34. package/dist/service/PreProxyServer.js +344 -0
  35. package/dist/service/ProxyAutoStarterService.js +62 -0
  36. package/dist/service/RouteRuleService.js +131 -0
  37. package/dist/types/index.js +5 -0
  38. package/dist/types/shared/BaseRes.js +36 -0
  39. package/dist/types/shared/DevServerItem.js +51 -0
  40. package/dist/types/shared/EnvItem.js +57 -0
  41. package/dist/types/shared/EnvmConfig.js +17 -0
  42. package/dist/types/shared/ListRes.js +1 -0
  43. package/dist/types/shared/Password.js +48 -0
  44. package/dist/types/shared/RouteRule.js +49 -0
  45. package/dist/utils/ResolveConfig.js +79 -0
  46. package/dist/utils/errors.js +8 -0
  47. package/dist/utils/logger.js +93 -0
  48. package/package.json +78 -0
  49. package/readme.md +214 -0
@@ -0,0 +1,62 @@
1
+ import { AppError } from "../utils/errors.js";
2
+ import { logger } from "../utils/logger.js";
3
+ /**
4
+ * 代理自动启动器
5
+ * 负责在应用启动时检查并启动数据库中状态为运行的代理
6
+ */
7
+ export class ProxyAutoStarter {
8
+ constructor(envRepo, proxyService, routeRuleRepo) {
9
+ this.envRepo = envRepo;
10
+ this.proxyService = proxyService;
11
+ this.routeRuleRepo = routeRuleRepo;
12
+ this.start();
13
+ }
14
+ /**
15
+ * 应用启动时执行的代理检查和启动逻辑
16
+ */
17
+ async start() {
18
+ try {
19
+ logger.info("开始检查需要自动启动的代理...");
20
+ // 查询数据库中状态为"运行中"的代理
21
+ const runningProxies = this.envRepo.findAllByStatus("running");
22
+ if (runningProxies.length === 0) {
23
+ logger.info("没有需要自动启动的代理");
24
+ return;
25
+ }
26
+ logger.info(`发现${runningProxies.length}个状态为运行的代理,准备启动...`);
27
+ // 逐个启动代理,记录成功和失败的数量
28
+ let successCount = 0;
29
+ let failCount = 0;
30
+ // 使用for循环而非forEach以支持异步等待
31
+ for (const proxy of runningProxies) {
32
+ try {
33
+ logger.info(`正在启动代理 [${proxy.name}] (ID: ${proxy.id})`);
34
+ // 检查代理是否已经在运行(避免重复启动)
35
+ // await this.proxyService.handleStartServer({
36
+ // id: proxy.id,
37
+ // });
38
+ // if (isAlreadyRunning) {
39
+ // console.warn(`代理 [${proxy.name}] 已在运行,跳过启动`);
40
+ // successCount++;
41
+ // continue;
42
+ // }
43
+ // 启动代理
44
+ await this.proxyService.handleStartServer(proxy);
45
+ logger.info(`代理 [${proxy.name}] 启动成功`);
46
+ successCount++;
47
+ }
48
+ catch (error) {
49
+ logger.error(error, `代理 [${proxy.name}] (ID: ${proxy.id}) 启动失败:`);
50
+ failCount++;
51
+ }
52
+ }
53
+ // 输出启动结果统计
54
+ logger.info(`代理自动启动完成 - 成功: ${successCount}, 失败: ${failCount}, 总计: ${runningProxies.length}`);
55
+ }
56
+ catch (error) {
57
+ logger.error(error, "代理自动启动流程失败:");
58
+ // 根据实际需求决定是否抛出错误终止应用,或仅记录错误
59
+ throw new AppError("代理自动启动流程执行失败");
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,131 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+ /**
3
+ * 路由规则服务类
4
+ * 负责处理与路由规则相关的核心业务逻辑,包括路由规则的增删改查等操作
5
+ * 依赖路由规则仓库(RouteRuleRepo)和环境仓库(EnvRepo)实现数据持久化和关联操作
6
+ */
7
+ class RouteRuleService {
8
+ /**
9
+ * 构造函数 - 通过依赖注入初始化仓库实例
10
+ * @param routeRuleRepo - 路由规则仓库实例,用于路由规则数据的持久化操作
11
+ * @param envRepo - 环境仓库实例,用于获取环境信息
12
+ */
13
+ constructor(routeRuleRepo, envRepo) {
14
+ this.routeRuleRepo = routeRuleRepo;
15
+ this.envRepo = envRepo;
16
+ }
17
+ /**
18
+ * 获取指定环境的所有路由规则
19
+ * @param envId - 环境ID
20
+ * @returns 该环境的所有路由规则数组(实时关联环境名称)
21
+ */
22
+ handleGetByEnvId(envId) {
23
+ const rules = this.routeRuleRepo.getByEnvId(envId);
24
+ // 实时关联环境名称
25
+ return rules.map((rule) => {
26
+ const targetEnv = rule.targetEnvId
27
+ ? this.envRepo.findOneById(rule.targetEnvId)
28
+ : null;
29
+ return {
30
+ ...rule,
31
+ targetEnvName: targetEnv
32
+ ? `${targetEnv.name}${targetEnv.apiBaseUrl}` || targetEnv.apiBaseUrl
33
+ : "",
34
+ };
35
+ });
36
+ }
37
+ /**
38
+ * 添加新路由规则
39
+ * 1. 校验输入参数合法性 2. 检查目标环境是否存在 3. 生成唯一ID 4. 保存
40
+ * @param routeRuleItem - 待添加的路由规则信息(不含ID)
41
+ * @returns 新创建的路由规则
42
+ * @throws {Error} 当输入参数不合法或目标环境不存在时抛出
43
+ */
44
+ handleAdd(routeRuleItem) {
45
+ const { envId, pathPrefix, targetEnvId, injectScript } = routeRuleItem;
46
+ // 如果目标环境有值,检查目标环境是否存在
47
+ if (targetEnvId) {
48
+ const targetEnv = this.envRepo.findOneById(targetEnvId);
49
+ if (!targetEnv) {
50
+ throw new Error(`添加失败,目标环境【${targetEnvId}】不存在`);
51
+ }
52
+ }
53
+ else if (!injectScript) {
54
+ // 未开启注入脚本且目标环境为空时报错
55
+ throw new Error("添加失败,目标环境不能为空");
56
+ }
57
+ // 检查是否已存在相同路径前缀的规则(同一环境下)
58
+ if (this.routeRuleRepo.existsByEnvIdAndPathPrefix(envId, pathPrefix)) {
59
+ throw new Error(`添加失败,该环境下已存在路径前缀【${pathPrefix}】的规则`);
60
+ }
61
+ // 生成唯一ID并组装完整路由规则信息
62
+ const now = new Date().toISOString();
63
+ const newRouteRule = {
64
+ ...routeRuleItem,
65
+ id: uuidv4(),
66
+ createdAt: now,
67
+ updatedAt: now,
68
+ };
69
+ // 保存路由规则
70
+ this.routeRuleRepo.create(newRouteRule);
71
+ return newRouteRule;
72
+ }
73
+ /**
74
+ * 更新路由规则
75
+ * 1. 校验输入参数 2. 检查规则是否存在 3. 检查目标环境是否存在 4. 执行更新
76
+ * @param routeRuleItem - 包含待更新路由规则ID及字段的对象
77
+ * @returns 更新后的路由规则
78
+ * @throws {Error} 当输入参数不合法或规则/目标环境不存在时抛出
79
+ */
80
+ handleUpdate(routeRuleItem) {
81
+ const { id, targetEnvId } = routeRuleItem;
82
+ // 检查路由规则是否存在
83
+ const existingRule = this.routeRuleRepo.findOneById(id);
84
+ if (!existingRule) {
85
+ throw new Error(`更新失败,路由规则【${id}】不存在`);
86
+ }
87
+ // 判断是否需要清空目标环境
88
+ const shouldClearTarget = targetEnvId === "" || targetEnvId === undefined;
89
+ if (shouldClearTarget) {
90
+ // 清空目标环境
91
+ routeRuleItem.targetEnvId = "";
92
+ }
93
+ else if (targetEnvId !== existingRule.targetEnvId) {
94
+ // 更新了目标环境,检查目标环境是否存在
95
+ const targetEnv = this.envRepo.findOneById(targetEnvId);
96
+ if (!targetEnv) {
97
+ throw new Error(`更新失败,目标环境【${targetEnvId}】不存在`);
98
+ }
99
+ }
100
+ // 如果更新了路径前缀,检查是否与其他规则冲突
101
+ if (routeRuleItem.pathPrefix &&
102
+ routeRuleItem.pathPrefix !== existingRule.pathPrefix) {
103
+ if (this.routeRuleRepo.existsByEnvIdAndPathPrefix(existingRule.envId, routeRuleItem.pathPrefix)) {
104
+ throw new Error(`更新失败,该环境下已存在路径前缀【${routeRuleItem.pathPrefix}】的规则`);
105
+ }
106
+ }
107
+ // 添加更新时间
108
+ routeRuleItem.updatedAt = new Date().toISOString();
109
+ // 执行更新
110
+ this.routeRuleRepo.update(routeRuleItem);
111
+ // 返回更新后的规则
112
+ return this.routeRuleRepo.findOneById(id);
113
+ }
114
+ /**
115
+ * 删除指定路由规则
116
+ * 1. 校验输入参数 2. 检查规则是否存在 3. 执行删除操作
117
+ * @param routeRuleItem - 包含待删除路由规则ID的对象
118
+ * @throws {Error} 当输入参数不合法或规则不存在时抛出
119
+ */
120
+ handleDelete(routeRuleItem) {
121
+ const { id } = routeRuleItem;
122
+ // 检查路由规则是否存在
123
+ const existingRule = this.routeRuleRepo.findOneById(id);
124
+ if (!existingRule) {
125
+ throw new Error(`删除失败,路由规则【${id}】不存在`);
126
+ }
127
+ // 执行删除
128
+ this.routeRuleRepo.delete(routeRuleItem);
129
+ }
130
+ }
131
+ export { RouteRuleService };
@@ -0,0 +1,5 @@
1
+ export * from "./shared/EnvItem.js";
2
+ export * from "./shared/DevServerItem.js";
3
+ export * from "./shared/EnvmConfig.js";
4
+ export * from "./shared/RouteRule.js";
5
+ export * from "./shared/Password.js";
@@ -0,0 +1,36 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * 基础响应结构的Zod校验规则
4
+ * 用于统一API返回格式,同时支持前后端类型共享
5
+ */
6
+ export const BaseResponseSchema = z.object({
7
+ /**
8
+ * 状态码
9
+ * - 200表示成功
10
+ * - 非200表示异常
11
+ */
12
+ code: z.number(),
13
+ /**
14
+ * 响应消息
15
+ * - 成功时为"success"或相关提示
16
+ * - 失败时为错误描述
17
+ */
18
+ message: z.string(),
19
+ /**
20
+ * 响应数据
21
+ * - 可选字段,根据接口需求动态变化
22
+ * - 类型由泛型参数指定
23
+ */
24
+ data: z.unknown().optional(),
25
+ });
26
+ /**
27
+ * 创建带具体数据类型的响应Schema
28
+ * 用于后端校验特定接口的响应格式
29
+ * @param dataSchema 具体数据字段的Zod校验规则
30
+ * @returns 包含具体数据类型的完整响应Schema
31
+ */
32
+ export const createResponseSchema = (dataSchema) => {
33
+ return BaseResponseSchema.extend({
34
+ data: dataSchema.optional(),
35
+ });
36
+ };
@@ -0,0 +1,51 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * 开发服务器主键Schema
4
+ * 用于标识开发服务器的唯一ID
5
+ */
6
+ export const DevServerPrimarySchema = z.object({
7
+ id: z.string().describe("主键"),
8
+ });
9
+ /**
10
+ * 开发服务器基础信息Schema
11
+ * 包含开发服务器的基本配置信息
12
+ */
13
+ export const BaseDevServerSchema = z.object({
14
+ /**
15
+ * 开发服务器的名称
16
+ */
17
+ name: z.string().describe("开发服务器名称"),
18
+ /**
19
+ * 开发服务器的描述
20
+ */
21
+ description: z.string().optional().default("").describe("开发服务器描述"),
22
+ /**
23
+ * 开发服务器的IP地址或URL
24
+ */
25
+ devServerUrl: z.string().describe("开发服务器地址"),
26
+ });
27
+ /**
28
+ * 完整开发服务器模型Schema
29
+ * 合并了主键和基础信息,用于创建完整的开发服务器模型
30
+ */
31
+ export const DevServerModelSchema = DevServerPrimarySchema.extend(BaseDevServerSchema.shape);
32
+ /**
33
+ * 新增开发服务器Schema
34
+ * 用于创建新开发服务器时的参数验证,不需要提供id(由系统生成)
35
+ */
36
+ export const DevServerCreateSchema = BaseDevServerSchema;
37
+ /**
38
+ * 删除 开发服务器Schema
39
+ * 用于删除开发服务器时的参数验证,只需要提供开发服务器id
40
+ */
41
+ export const DevServerDeleteSchema = DevServerPrimarySchema;
42
+ /**
43
+ * 修改开发服务器Schema
44
+ * 用于更新开发服务器信息时的参数验证,id为必填项,其他字段可选
45
+ */
46
+ export const DevServerUpdateSchema = DevServerPrimarySchema.extend(BaseDevServerSchema.partial().shape);
47
+ /**
48
+ * 查询开发服务器Schema
49
+ * 用于查询开发服务器信息时的参数验证,只需要提供开发服务器id
50
+ */
51
+ export const DevServerQuerySchema = DevServerPrimarySchema;
@@ -0,0 +1,57 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * 环境主键Schema
4
+ * 用于标识环境的唯一ID
5
+ */
6
+ export const EnvPrimarySchema = z.object({
7
+ id: z.string().describe("主键"),
8
+ });
9
+ /**
10
+ * 环境基础信息Schema
11
+ * 包含环境的基本配置信息
12
+ */
13
+ export const EnvItemBaseSchema = z.object({
14
+ apiBaseUrl: z.string().describe("API服务器地址"),
15
+ port: z
16
+ .number()
17
+ .int("端口号必须是整数")
18
+ .min(1, "端口号不能小于1")
19
+ .max(65535, "端口号不能大于65535")
20
+ .describe("环境绑定端口(必填)"),
21
+ name: z.string().describe("环境名称").optional(),
22
+ description: z.string().optional().describe("环境描述"),
23
+ devServerId: z
24
+ .string()
25
+ .min(1, "开发服务器ID不能为空")
26
+ .optional()
27
+ .describe("环境绑定的开发服务器ID"),
28
+ homePage: z.string().optional().describe("环境首页"),
29
+ status: z.enum(["running", "stopped"]).optional(),
30
+ });
31
+ /**
32
+ * 完整环境模型Schema
33
+ * 合并了主键和基础信息,用于创建完整的环境模型
34
+ */
35
+ export const EnvModelSchema = EnvPrimarySchema.extend(EnvItemBaseSchema.shape);
36
+ /**
37
+ * 新增环境Schema
38
+ * 用于创建新环境时的参数验证,不需要提供id(由系统生成)
39
+ */
40
+ export const EnvCreateSchema = EnvItemBaseSchema.omit({
41
+ status: true, // 新增时状态由系统自动设置
42
+ });
43
+ /**
44
+ * 删除环境Schema
45
+ * 用于删除环境时的参数验证,只需要提供环境id
46
+ */
47
+ export const EnvDeleteSchema = EnvPrimarySchema;
48
+ /**
49
+ * 修改环境Schema
50
+ * 用于更新环境信息时的参数验证,id为必填项,其他字段可选
51
+ */
52
+ export const EnvUpdateSchema = EnvPrimarySchema.extend(EnvItemBaseSchema.partial().shape);
53
+ /**
54
+ * 查询环境Schema
55
+ * 用于查询环境信息时的参数验证,只需要提供环境id
56
+ */
57
+ export const EnvQuerySchema = EnvPrimarySchema;
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+ // 2. 定义环境变量模式
3
+ export const EnvmConfigSchema = z.object({
4
+ port: z.coerce
5
+ .number()
6
+ .min(3000, { message: "端口必须大于等于3000" })
7
+ .max(65535, { message: "端口必须小于等于65535" })
8
+ .default(3099), // 自动转换类型
9
+ apiPrefix: z.string().default("/dev-manage-api"),
10
+ cookieSuffix: z.string().default("envm"), // 新增 cookie 后缀配置
11
+ logLevel: z.string().default("info"),
12
+ injectScriptDir: z
13
+ .string()
14
+ .default(".envminject")
15
+ .optional()
16
+ .describe("注入脚本的文件夹路径"),
17
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * 密码主键Schema
4
+ */
5
+ export const PasswordPrimarySchema = z.object({
6
+ id: z.string().describe("主键"),
7
+ });
8
+ /**
9
+ * 密码基础信息Schema
10
+ */
11
+ export const PasswordBaseSchema = z.object({
12
+ envId: z.string().describe("关联的环境ID"),
13
+ name: z
14
+ .string()
15
+ .min(1, "名称不能为空")
16
+ .describe("密码名称(如:数据库、API密钥等)"),
17
+ username: z
18
+ .string()
19
+ .min(1, "用户名不能为空")
20
+ .describe("用户名"),
21
+ password: z
22
+ .string()
23
+ .min(1, "密码不能为空")
24
+ .describe("密码"),
25
+ description: z.string().optional().describe("描述"),
26
+ isDefault: z.boolean().optional().default(false).describe("是否为默认密码"),
27
+ createdAt: z.string().optional(),
28
+ updatedAt: z.string().optional(),
29
+ });
30
+ /**
31
+ * 完整密码模型Schema
32
+ */
33
+ export const PasswordModelSchema = PasswordPrimarySchema.extend(PasswordBaseSchema.shape);
34
+ /**
35
+ * 新增密码Schema
36
+ */
37
+ export const PasswordCreateSchema = PasswordBaseSchema.omit({
38
+ createdAt: true,
39
+ updatedAt: true,
40
+ });
41
+ /**
42
+ * 删除密码Schema
43
+ */
44
+ export const PasswordDeleteSchema = PasswordPrimarySchema;
45
+ /**
46
+ * 修改密码Schema
47
+ */
48
+ export const PasswordUpdateSchema = PasswordPrimarySchema.extend(PasswordBaseSchema.partial().shape);
@@ -0,0 +1,49 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * 路由规则主键Schema
4
+ */
5
+ export const RouteRulePrimarySchema = z.object({
6
+ id: z.string().describe("主键"),
7
+ });
8
+ /**
9
+ * 路由规则基础信息Schema
10
+ */
11
+ export const RouteRuleBaseSchema = z.object({
12
+ envId: z.string().describe("关联的环境ID"),
13
+ pathPrefix: z
14
+ .string()
15
+ .min(1, "路径前缀不能为空")
16
+ .describe("请求路径前缀,如 /api/user"),
17
+ targetEnvId: z
18
+ .string()
19
+ .optional()
20
+ .describe("目标环境ID(转发到此环境)"),
21
+ description: z.string().optional().describe("描述"),
22
+ enabled: z.boolean().optional().default(true).describe("是否启用"),
23
+ injectScript: z
24
+ .boolean()
25
+ .optional()
26
+ .default(false)
27
+ .describe("是否注入Script脚本"),
28
+ createdAt: z.string().optional(),
29
+ updatedAt: z.string().optional(),
30
+ });
31
+ /**
32
+ * 完整路由规则模型Schema
33
+ */
34
+ export const RouteRuleModelSchema = RouteRulePrimarySchema.extend(RouteRuleBaseSchema.shape);
35
+ /**
36
+ * 新增路由规则Schema
37
+ */
38
+ export const RouteRuleCreateSchema = RouteRuleBaseSchema.omit({
39
+ createdAt: true,
40
+ updatedAt: true,
41
+ });
42
+ /**
43
+ * 删除路由规则Schema
44
+ */
45
+ export const RouteRuleDeleteSchema = RouteRulePrimarySchema;
46
+ /**
47
+ * 修改路由规则Schema
48
+ */
49
+ export const RouteRuleUpdateSchema = RouteRulePrimarySchema.extend(RouteRuleBaseSchema.partial().shape);
@@ -0,0 +1,79 @@
1
+ // config.ts
2
+ import { z } from "zod";
3
+ import dotenv from "dotenv";
4
+ import { EnvmConfigSchema } from "../types/index.js";
5
+ import path from "path";
6
+ import fs from "fs";
7
+ // 缓存已解析的配置(避免重复加载,实现“单例”效果)
8
+ let cachedConfig = null;
9
+ /**
10
+ * 从 package.json 读取 envm 配置(支持自定义路径)
11
+ */
12
+ const getPackageJsonConfig = () => {
13
+ const defaultPath = path.resolve(process.cwd(), "package.json");
14
+ const targetPath = defaultPath;
15
+ try {
16
+ const content = fs.readFileSync(targetPath, "utf8");
17
+ const pkg = JSON.parse(content);
18
+ return pkg.envm || {};
19
+ }
20
+ catch {
21
+ console.warn(`⚠️ 无法读取 package.json(路径:${targetPath})中的 envm 配置,将忽略这部分配置:`);
22
+ return {};
23
+ }
24
+ };
25
+ /**
26
+ * 加载并解析配置(核心函数)
27
+ * @param options 动态配置参数(优先级最高)
28
+ * @returns 验证后的配置对象
29
+ */
30
+ export const loadConfig = (overrideConfig) => {
31
+ // 1. 单例保护:已加载过则直接返回缓存,避免重复IO和解析
32
+ if (cachedConfig)
33
+ return cachedConfig;
34
+ try {
35
+ // 2. 加载 .env 文件(支持自定义路径)
36
+ const dotenvResult = dotenv.config();
37
+ if (dotenvResult.error) {
38
+ console.warn("⚠️ 未找到 .env 文件,忽略...");
39
+ }
40
+ const { envm_port: port, envm_apiPrefix: apiPrefix, envm_cookieSuffix: cookieSuffix, envm_logLevel: logLevel, envm_injectScriptDir: injectScriptDir, } = process.env;
41
+ // 3. 读取 package.json 配置(支持自定义路径)
42
+ const packageJsonConfig = getPackageJsonConfig();
43
+ // 4. 合并配置:环境变量优先级 > package.json 配置
44
+ // 3. 合并配置:优先级从低到高
45
+ const mergedConfig = {
46
+ ...packageJsonConfig, // 最低优先级
47
+ port, // 中间优先级
48
+ apiPrefix,
49
+ cookieSuffix,
50
+ logLevel,
51
+ injectScriptDir,
52
+ ...overrideConfig, // 最高优先级(外部传入的配置)
53
+ };
54
+ // 5. Zod 验证
55
+ const parsedConfig = EnvmConfigSchema.parse(mergedConfig);
56
+ // 6. 缓存配置并返回
57
+ cachedConfig = parsedConfig;
58
+ console.log("✅ 配置加载与验证成功", cachedConfig);
59
+ return parsedConfig;
60
+ }
61
+ catch (error) {
62
+ // 7. 精细化错误处理
63
+ if (error instanceof z.ZodError) {
64
+ throw new Error(`配置字段错误:${error.issues
65
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
66
+ .join("; ")}`);
67
+ }
68
+ throw new Error(`配置加载异常:${error.message}`);
69
+ }
70
+ };
71
+ /**
72
+ * 获取已加载的配置(需在 loadConfig() 后调用)
73
+ * @returns 缓存的配置对象
74
+ */
75
+ export const getConfig = () => {
76
+ if (!cachedConfig)
77
+ throw new Error("配置未加载,请先调用 loadConfig()");
78
+ return cachedConfig;
79
+ };
@@ -0,0 +1,8 @@
1
+ // src/utils/errors.ts
2
+ export class AppError extends Error {
3
+ constructor(message, code = 500, status = 500) {
4
+ super(message);
5
+ this.code = code;
6
+ this.status = status;
7
+ }
8
+ }
@@ -0,0 +1,93 @@
1
+ // src/utils/logger.ts
2
+ import pino from "pino";
3
+ import { execSync } from "child_process";
4
+ import os from "os";
5
+ /**
6
+ * 自动将终端编码切换为 UTF-8(跨平台)
7
+ */
8
+ function setTerminalEncodingToUtf8() {
9
+ const platform = os.platform();
10
+ try {
11
+ if (platform === "win32") {
12
+ // Windows 系统:切换代码页为 UTF-8(65001)
13
+ // execSync 执行 cmd 命令,stdio: 'ignore' 避免命令输出干扰日志
14
+ execSync("chcp 65001", { stdio: "ignore" });
15
+ console.log("Windows 终端已切换为 UTF-8 编码(代码页 65001)");
16
+ }
17
+ else if (platform === "linux" || platform === "darwin") {
18
+ // Linux/macOS 系统:设置 LC_ALL 为 UTF-8(临时生效)
19
+ // 注意:类 Unix 系统编码通常由 locale 控制,这里仅临时覆盖
20
+ process.env.LC_ALL = "en_US.UTF-8";
21
+ console.log("类 Unix 终端已设置为 UTF-8 编码");
22
+ }
23
+ }
24
+ catch (error) {
25
+ console.warn("切换终端编码失败,可能导致中文乱码:", error);
26
+ }
27
+ }
28
+ // 在日志初始化前调用,确保编码已切换
29
+ setTerminalEncodingToUtf8();
30
+ let rootLogger = null;
31
+ /**
32
+ * 创建根日志实例(Node.js 环境专用)
33
+ * 优先级:customOptions > 默认配置
34
+ */
35
+ export const createRootLogger = (logLevel) => {
36
+ if (rootLogger)
37
+ return rootLogger;
38
+ const level = logLevel || "info";
39
+ // 默认配置:区分开发/生产环境
40
+ const defaultConfig = {
41
+ level, // 从配置读取日志级别
42
+ base: {
43
+ service: "envm-service", // 服务名(多服务日志区分)
44
+ // env: options.config.env || "development", // 环境标识(dev/prod/test)
45
+ pid: process.pid, // Node.js 进程 ID
46
+ },
47
+ // 开发环境:格式化输出(pino-pretty);生产环境:JSON 格式(便于日志收集工具解析)
48
+ transport: {
49
+ target: "pino-pretty",
50
+ options: {
51
+ colorize: true,
52
+ encoding: "utf8",
53
+ translateTime: "yyyy-mm-dd HH:MM:ss",
54
+ hideObject: level === "info",
55
+ },
56
+ },
57
+ // transport: options.config.env === "development"
58
+ // ? { target: "pino-pretty", options: { colorize: true, translateTime: "yyyy-MM-dd HH:mm:ss" } }
59
+ // : undefined,
60
+ timestamp: pino.stdTimeFunctions.isoTime, // ISO 时间格式
61
+ redact: ["req.headers.authorization", "res.data.token"], // 敏感字段脱敏(生产环境必备)
62
+ };
63
+ // 合并默认配置与自定义配置
64
+ rootLogger = pino({ ...defaultConfig });
65
+ return rootLogger;
66
+ };
67
+ /**
68
+ * 初始化模块级日志(单例,全局复用)
69
+ * 按业务模块分类,避免日志混乱
70
+ */
71
+ export let logger; // 环境管理模块日志
72
+ export let envLogger; // 环境管理模块日志
73
+ export let devServerLogger; // 开发服务器模块日志
74
+ export let proxyLogger; // 代理模块日志
75
+ /**
76
+ * Node.js 日志初始化入口(需在 config 加载后调用)
77
+ */
78
+ export const initLoggers = (logLevel) => {
79
+ const rootLog = createRootLogger(logLevel);
80
+ // 为每个模块创建子日志(附加 module 标识,便于筛选)
81
+ logger = rootLog.child({ module: "envm" });
82
+ envLogger = rootLog.child({ module: "env" });
83
+ devServerLogger = rootLog.child({ module: "devServer" });
84
+ proxyLogger = rootLog.child({ module: "proxy" });
85
+ };
86
+ /**
87
+ * 获取根日志实例(确保已初始化)
88
+ */
89
+ export const getRootLogger = () => {
90
+ if (!rootLogger)
91
+ throw new Error("日志未初始化,请先调用 initNodeLoggers()");
92
+ return rootLogger;
93
+ };