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.
- package/LICENSE +21 -0
- package/bin/index.js +23 -0
- package/dist/Container.js +55 -0
- package/dist/Plugin.js +25 -0
- package/dist/PostProxyServer.js +85 -0
- package/dist/client/assets/element-plus-wndYbaZY.js.gz +0 -0
- package/dist/client/assets/index-Bl3B2M1W.js.gz +0 -0
- package/dist/client/assets/index-nrcddKJg.css.gz +0 -0
- package/dist/client/assets/vue-BGFZjnqd.js.gz +0 -0
- package/dist/client/favicon.ico +0 -0
- package/dist/client/index.html +16 -0
- package/dist/constants/responseCode.js +27 -0
- package/dist/controllers/DevServerController.js +102 -0
- package/dist/controllers/EnvController.js +177 -0
- package/dist/controllers/PasswordController.js +106 -0
- package/dist/controllers/RouteRuleController.js +106 -0
- package/dist/dto/BaseRes.js +1 -0
- package/dist/dto/EnvDTO.js +4 -0
- package/dist/index.js +108 -0
- package/dist/middleware/dto.middleware.js +38 -0
- package/dist/middleware/globalErrorHandler.js +42 -0
- package/dist/middleware/responseEnhancer.js +52 -0
- package/dist/models/DevServerModel.js +1 -0
- package/dist/models/EnvModel.js +14 -0
- package/dist/repositories/DevServerRepo.js +101 -0
- package/dist/repositories/EnvRepo.js +121 -0
- package/dist/repositories/PasswordRepo.js +142 -0
- package/dist/repositories/RouteRuleRepo.js +106 -0
- package/dist/repositories/database.js +95 -0
- package/dist/routes/index.js +78 -0
- package/dist/service/DevServerService.js +189 -0
- package/dist/service/EnvService.js +214 -0
- package/dist/service/PasswordService.js +100 -0
- package/dist/service/PreProxyServer.js +344 -0
- package/dist/service/ProxyAutoStarterService.js +62 -0
- package/dist/service/RouteRuleService.js +131 -0
- package/dist/types/index.js +5 -0
- package/dist/types/shared/BaseRes.js +36 -0
- package/dist/types/shared/DevServerItem.js +51 -0
- package/dist/types/shared/EnvItem.js +57 -0
- package/dist/types/shared/EnvmConfig.js +17 -0
- package/dist/types/shared/ListRes.js +1 -0
- package/dist/types/shared/Password.js +48 -0
- package/dist/types/shared/RouteRule.js +49 -0
- package/dist/utils/ResolveConfig.js +79 -0
- package/dist/utils/errors.js +8 -0
- package/dist/utils/logger.js +93 -0
- package/package.json +78 -0
- package/readme.md +214 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from "uuid";
|
|
2
|
+
import PreProxyServer from "./PreProxyServer.js";
|
|
3
|
+
import { AppError } from "../utils/errors.js";
|
|
4
|
+
import { envLogger } from "../utils/logger.js";
|
|
5
|
+
/**
|
|
6
|
+
* 环境服务类
|
|
7
|
+
* 负责处理与环境相关的核心业务逻辑,包括环境的增删改查、服务器启停等操作
|
|
8
|
+
* 依赖环境仓库(EnvRepo)和开发服务器仓库(DevServerRepo)实现数据持久化和关联操作
|
|
9
|
+
*/
|
|
10
|
+
class EnvService {
|
|
11
|
+
/**
|
|
12
|
+
* 构造函数 - 通过依赖注入初始化仓库实例
|
|
13
|
+
* @param envRepo - 环境仓库实例,用于环境数据的持久化操作
|
|
14
|
+
* @param devServerRepo - 开发服务器仓库实例,用于关联开发服务器的操作
|
|
15
|
+
* @param routeRuleRepo - 路由规则仓库实例,用于获取路由规则数量
|
|
16
|
+
*/
|
|
17
|
+
constructor(envRepo, devServerRepo, routeRuleRepo) {
|
|
18
|
+
this.envRepo = envRepo;
|
|
19
|
+
this.devServerRepo = devServerRepo;
|
|
20
|
+
this.routeRuleRepo = routeRuleRepo;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 添加新环境
|
|
24
|
+
* 1. 校验输入参数合法性 2. 检查环境是否已存在 3. 生成唯一ID 4. 保存新环境
|
|
25
|
+
* @param envItem - 待添加的环境信息(不含ID)
|
|
26
|
+
* @returns {void}
|
|
27
|
+
* @throws {AppError} 当输入参数不合法时抛出
|
|
28
|
+
* @throws {AppError} 当环境已存在(通过apiBaseUrl判断)时抛出
|
|
29
|
+
*/
|
|
30
|
+
handleAddEnv(envItem) {
|
|
31
|
+
// 参数校验
|
|
32
|
+
envLogger.debug({ envItem }, "准备添加环境");
|
|
33
|
+
// 检查环境是否已存在(通过apiBaseUrl唯一标识)
|
|
34
|
+
const existingEnv = this.envRepo.findOneByApiBaseUrl(envItem);
|
|
35
|
+
if (existingEnv) {
|
|
36
|
+
throw new AppError(`添加失败,环境【${existingEnv.apiBaseUrl}】已存在`);
|
|
37
|
+
}
|
|
38
|
+
// 生成唯一ID并组装完整环境信息
|
|
39
|
+
const newEnvItem = {
|
|
40
|
+
...envItem,
|
|
41
|
+
id: uuidv4(),
|
|
42
|
+
status: "stopped", // 默认初始状态为停止
|
|
43
|
+
};
|
|
44
|
+
// 保存环境
|
|
45
|
+
this.envRepo.addEnv(newEnvItem);
|
|
46
|
+
envLogger.info({ newEnvItem }, "环境添加成功");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 删除指定环境
|
|
50
|
+
* 1. 校验输入参数 2. 检查环境是否存在 3. 执行删除操作
|
|
51
|
+
* @param envItem - 包含待删除环境ID的对象
|
|
52
|
+
* @returns {void}
|
|
53
|
+
* @throws {AppError} 当输入参数不合法时抛出
|
|
54
|
+
* @throws {AppError} 当环境不存在时抛出
|
|
55
|
+
*/
|
|
56
|
+
async handleDeleteEnv(envItem) {
|
|
57
|
+
// 参数校验
|
|
58
|
+
const { id } = envItem;
|
|
59
|
+
envLogger.info({ envItem }, "准备删除环境");
|
|
60
|
+
// 检查环境是否存在
|
|
61
|
+
const existingEnv = this.envRepo.findOneById(id);
|
|
62
|
+
if (!existingEnv) {
|
|
63
|
+
throw new AppError(`删除失败,环境【${id}】不存在`);
|
|
64
|
+
}
|
|
65
|
+
if (existingEnv.status === "running") {
|
|
66
|
+
await this.handleStopServer(existingEnv);
|
|
67
|
+
}
|
|
68
|
+
// 执行删除
|
|
69
|
+
this.envRepo.deleteEnv(envItem);
|
|
70
|
+
envLogger.info({ envItem }, "环境删除成功");
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 更新环境信息
|
|
74
|
+
* 1. 校验输入参数 2. 检查环境是否存在 3. 执行更新操作
|
|
75
|
+
* @param envItem - 包含待更新环境ID及字段的对象
|
|
76
|
+
* @returns {void}
|
|
77
|
+
* @throws {AppError} 当输入参数不合法时抛出
|
|
78
|
+
* @throws {AppError} 当环境不存在时抛出
|
|
79
|
+
*/
|
|
80
|
+
handleUpdateEnv(envItem) {
|
|
81
|
+
// 参数校验
|
|
82
|
+
const { id } = envItem;
|
|
83
|
+
envLogger.info({ envItem }, "准备更新环境");
|
|
84
|
+
// 检查环境是否存在
|
|
85
|
+
const existingEnv = this.envRepo.findOneById(id);
|
|
86
|
+
if (!existingEnv) {
|
|
87
|
+
throw new AppError(`更新失败,环境【${id}】不存在`);
|
|
88
|
+
}
|
|
89
|
+
// 执行更新
|
|
90
|
+
this.envRepo.update(envItem);
|
|
91
|
+
envLogger.info({ envItem }, "环境更新成功");
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 获取所有环境列表
|
|
95
|
+
* @returns {Array<EnvModel & {routeRuleCount?: number}>} 环境信息数组
|
|
96
|
+
*/
|
|
97
|
+
handleGetList() {
|
|
98
|
+
const list = this.envRepo.getAll();
|
|
99
|
+
// 如果有 routeRuleRepo,则统计每个环境的路由规则数量
|
|
100
|
+
if (this.routeRuleRepo) {
|
|
101
|
+
const result = list.map((env) => ({
|
|
102
|
+
...env,
|
|
103
|
+
routeRuleCount: this.routeRuleRepo?.countByEnvId(env.id) ?? 0,
|
|
104
|
+
}));
|
|
105
|
+
envLogger.info({ count: list.length }, `查询到${list.length}个环境`);
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
envLogger.info({ count: list.length }, `查询到${list.length}个环境`);
|
|
109
|
+
return list;
|
|
110
|
+
}
|
|
111
|
+
findOneById(id) {
|
|
112
|
+
return this.envRepo.findOneById(id);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 启动指定环境的代理服务器
|
|
116
|
+
* 1. 校验参数 2. 检查环境存在性 3. 先停止同端口服务 4. 启动新服务 5. 更新状态
|
|
117
|
+
* @param env - 包含待启动环境ID的对象
|
|
118
|
+
* @returns {Promise<void>}
|
|
119
|
+
* @throws {AppError} 当输入参数不合法时抛出
|
|
120
|
+
* @throws {AppError} 当环境不存在时抛出
|
|
121
|
+
* @throws {Error} 当服务器启动失败时抛出
|
|
122
|
+
*/
|
|
123
|
+
async handleStartServer(env) {
|
|
124
|
+
const { id } = env;
|
|
125
|
+
envLogger.info({ env }, "准备启动环境服务");
|
|
126
|
+
// 检查环境是否存在
|
|
127
|
+
const envItem = this.envRepo.findOneById(id);
|
|
128
|
+
if (!envItem) {
|
|
129
|
+
throw new AppError(`启动失败,环境【${id}】不存在`);
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
// 先查与当前服务同端口的环境,如果启动,则关闭掉
|
|
133
|
+
await this.stopServerAtSamePort(envItem);
|
|
134
|
+
// 启动新的代理服务器
|
|
135
|
+
await PreProxyServer.create(id, this.envRepo, this.devServerRepo, this.routeRuleRepo);
|
|
136
|
+
// 更新环境状态为运行中
|
|
137
|
+
await this.updateEnvStatus({
|
|
138
|
+
...env,
|
|
139
|
+
status: "running",
|
|
140
|
+
});
|
|
141
|
+
envLogger.info({ envItem }, "环境服务启动成功");
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
envLogger.error(error, "环境服务启动失败");
|
|
145
|
+
throw new AppError(`启动服务失败:${error.message}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 关闭指定端口的服务
|
|
150
|
+
* @param port
|
|
151
|
+
*/
|
|
152
|
+
async stopServerAtSamePort(envItem) {
|
|
153
|
+
// 先查与当前服务同端口的环境,如果启动,则关闭掉
|
|
154
|
+
const samePortAndRunningEnv = this.envRepo.findOne({
|
|
155
|
+
$and: [
|
|
156
|
+
{ id: { $ne: envItem.id } }, // $ne 表示 "不等于"
|
|
157
|
+
{ port: envItem.port },
|
|
158
|
+
{ status: "running" }, // 直接匹配等于的值
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
if (samePortAndRunningEnv) {
|
|
162
|
+
envLogger.info({
|
|
163
|
+
name: samePortAndRunningEnv.name,
|
|
164
|
+
}, `关闭 ${samePortAndRunningEnv.port} 端口服务`);
|
|
165
|
+
await this.handleStopServer(samePortAndRunningEnv);
|
|
166
|
+
}
|
|
167
|
+
envLogger.info({ port: envItem.port }, `端口${envItem.port}没有服务启动`);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* 停止指定环境的代理服务器
|
|
171
|
+
* 1. 校验参数 2. 检查环境存在性 3. 停止服务 4. 更新状态
|
|
172
|
+
* @param env - 包含待停止环境ID的对象
|
|
173
|
+
* @returns {Promise<void>}
|
|
174
|
+
* @throws {AppError} 当输入参数不合法时抛出
|
|
175
|
+
* @throws {AppError} 当环境不存在时抛出
|
|
176
|
+
*/
|
|
177
|
+
async handleStopServer(env) {
|
|
178
|
+
const { id } = env;
|
|
179
|
+
envLogger.info({ id }, "准备停止环境服务");
|
|
180
|
+
// 检查环境是否存在
|
|
181
|
+
const envItem = this.envRepo.findOneById(id);
|
|
182
|
+
if (!envItem) {
|
|
183
|
+
throw new AppError(`停止失败,环境【${id}】不存在`);
|
|
184
|
+
}
|
|
185
|
+
// 停止对应端口的服务
|
|
186
|
+
await PreProxyServer.stopServer(id);
|
|
187
|
+
envLogger.info({ envItem }, `环境【${envItem.name}】的服务已停止`);
|
|
188
|
+
// 更新环境的运行状态
|
|
189
|
+
await this.updateEnvStatus({
|
|
190
|
+
id,
|
|
191
|
+
status: "stopped",
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* 私有方法:更新环境运行状态
|
|
196
|
+
* @param env - 包含环境ID的对象
|
|
197
|
+
* @param status - 目标状态(running/stopped)
|
|
198
|
+
* @returns {Promise<EnvModel | undefined>} 更新后的环境信息
|
|
199
|
+
*/
|
|
200
|
+
async updateEnvStatus(env) {
|
|
201
|
+
// 查找最新的环境信息(避免使用旧引用)
|
|
202
|
+
const envItem = this.envRepo.findOneById(env.id);
|
|
203
|
+
if (!envItem) {
|
|
204
|
+
envLogger.warn(`更新状态失败,环境【${env.id}】不存在`);
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
// 执行状态更新
|
|
208
|
+
const updatedEnv = { ...envItem, ...env };
|
|
209
|
+
this.envRepo.update(updatedEnv);
|
|
210
|
+
envLogger.info({ env, updatedEnv }, `环境【${env.id}】状态已更新为${updatedEnv.status}`);
|
|
211
|
+
return updatedEnv;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
export { EnvService };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from "uuid";
|
|
2
|
+
/**
|
|
3
|
+
* 密码服务类
|
|
4
|
+
* 负责处理与密码相关的核心业务逻辑,包括密码的增删改查等操作
|
|
5
|
+
* 依赖密码仓库(PasswordRepo)实现数据持久化
|
|
6
|
+
*/
|
|
7
|
+
class PasswordService {
|
|
8
|
+
/**
|
|
9
|
+
* 构造函数 - 通过依赖注入初始化仓库实例
|
|
10
|
+
* @param passwordRepo - 密码仓库实例,用于密码数据的持久化操作
|
|
11
|
+
*/
|
|
12
|
+
constructor(passwordRepo) {
|
|
13
|
+
this.passwordRepo = passwordRepo;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 获取指定环境的所有密码
|
|
17
|
+
* @param envId - 环境ID
|
|
18
|
+
* @returns 该环境的所有密码数组
|
|
19
|
+
*/
|
|
20
|
+
handleGetByEnvId(envId) {
|
|
21
|
+
return this.passwordRepo.getByEnvId(envId);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 添加新密码
|
|
25
|
+
* 1. 校验输入参数合法性 2. 检查是否已存在相同名称 3. 如果设为默认密码则先清除其他默认密码 4. 生成唯一ID 5. 保存
|
|
26
|
+
* @param passwordItem - 待添加的密码信息(不含ID)
|
|
27
|
+
* @returns 新创建的密码
|
|
28
|
+
* @throws {Error} 当输入参数不合法或已存在相同名称时抛出
|
|
29
|
+
*/
|
|
30
|
+
handleAdd(passwordItem) {
|
|
31
|
+
const { envId, name, isDefault } = passwordItem;
|
|
32
|
+
// 检查是否已存在相同名称的密码(同一环境下)
|
|
33
|
+
if (this.passwordRepo.existsByEnvIdAndName(envId, name)) {
|
|
34
|
+
throw new Error(`添加失败,该环境下已存在名称为【${name}】的密码`);
|
|
35
|
+
}
|
|
36
|
+
// 如果设为默认密码,先清除其他默认密码
|
|
37
|
+
if (isDefault) {
|
|
38
|
+
this.passwordRepo.clearDefaultByEnvId(envId);
|
|
39
|
+
}
|
|
40
|
+
// 生成唯一ID并组装完整密码信息
|
|
41
|
+
const now = new Date().toISOString();
|
|
42
|
+
const newPassword = {
|
|
43
|
+
...passwordItem,
|
|
44
|
+
id: uuidv4(),
|
|
45
|
+
createdAt: now,
|
|
46
|
+
updatedAt: now,
|
|
47
|
+
};
|
|
48
|
+
// 保存密码
|
|
49
|
+
this.passwordRepo.create(newPassword);
|
|
50
|
+
return newPassword;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 更新密码
|
|
54
|
+
* 1. 校验输入参数 2. 检查密码是否存在 3. 如果设为默认密码则先清除其他默认密码 4. 执行更新
|
|
55
|
+
* @param passwordItem - 包含待更新密码ID及字段的对象
|
|
56
|
+
* @returns 更新后的密码
|
|
57
|
+
* @throws {Error} 当输入参数不合法或密码不存在时抛出
|
|
58
|
+
*/
|
|
59
|
+
handleUpdate(passwordItem) {
|
|
60
|
+
const { id, isDefault } = passwordItem;
|
|
61
|
+
// 检查密码是否存在
|
|
62
|
+
const existingPassword = this.passwordRepo.findOneById(id);
|
|
63
|
+
if (!existingPassword) {
|
|
64
|
+
throw new Error(`更新失败,密码【${id}】不存在`);
|
|
65
|
+
}
|
|
66
|
+
// 如果更新了名称,检查是否与其他密码冲突
|
|
67
|
+
if (passwordItem.name && passwordItem.name !== existingPassword.name) {
|
|
68
|
+
if (this.passwordRepo.existsByEnvIdAndName(existingPassword.envId, passwordItem.name)) {
|
|
69
|
+
throw new Error(`更新失败,该环境下已存在名称为【${passwordItem.name}】的密码`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// 如果设为默认密码(从false改为true),先清除其他默认密码
|
|
73
|
+
if (isDefault && !existingPassword.isDefault) {
|
|
74
|
+
this.passwordRepo.clearDefaultByEnvId(existingPassword.envId, id);
|
|
75
|
+
}
|
|
76
|
+
// 添加更新时间
|
|
77
|
+
passwordItem.updatedAt = new Date().toISOString();
|
|
78
|
+
// 执行更新
|
|
79
|
+
this.passwordRepo.update(passwordItem);
|
|
80
|
+
// 返回更新后的密码
|
|
81
|
+
return this.passwordRepo.findOneById(id);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 删除指定密码
|
|
85
|
+
* 1. 校验输入参数 2. 检查密码是否存在 3. 执行删除操作
|
|
86
|
+
* @param passwordItem - 包含待删除密码ID的对象
|
|
87
|
+
* @throws {Error} 当输入参数不合法或密码不存在时抛出
|
|
88
|
+
*/
|
|
89
|
+
handleDelete(passwordItem) {
|
|
90
|
+
const { id } = passwordItem;
|
|
91
|
+
// 检查密码是否存在
|
|
92
|
+
const existingPassword = this.passwordRepo.findOneById(id);
|
|
93
|
+
if (!existingPassword) {
|
|
94
|
+
throw new Error(`删除失败,密码【${id}】不存在`);
|
|
95
|
+
}
|
|
96
|
+
// 执行删除
|
|
97
|
+
this.passwordRepo.delete(passwordItem);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export { PasswordService };
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { createProxyMiddleware } from "http-proxy-middleware";
|
|
3
|
+
import setCookieParser from "set-cookie-parser";
|
|
4
|
+
import * as libCookie from "cookie";
|
|
5
|
+
import { minimatch } from "minimatch";
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import { getConfig } from "../utils/ResolveConfig.js";
|
|
9
|
+
import { devServerLogger } from "../utils/logger.js";
|
|
10
|
+
class PreProxyServer {
|
|
11
|
+
/**
|
|
12
|
+
* 判断该端口是否已经有服务存在
|
|
13
|
+
* @param port
|
|
14
|
+
* @returns
|
|
15
|
+
*/
|
|
16
|
+
static getAppInsByPort(port) {
|
|
17
|
+
return this.appMap[port];
|
|
18
|
+
}
|
|
19
|
+
static async create(envId, envRepo, devServerRepo, routeRuleRepo) {
|
|
20
|
+
if (this.appMap[envId]) {
|
|
21
|
+
devServerLogger.info(`环境 ${envId} 已经启动`);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const preProxyServer = new PreProxyServer(envId, envRepo, devServerRepo, routeRuleRepo);
|
|
25
|
+
await preProxyServer.startServer();
|
|
26
|
+
PreProxyServer.appMap[envId] = preProxyServer;
|
|
27
|
+
return preProxyServer;
|
|
28
|
+
}
|
|
29
|
+
constructor(envId, envRepo, devServerRepo, routeRuleRepo, app = express()) {
|
|
30
|
+
this.envId = envId;
|
|
31
|
+
this.envRepo = envRepo;
|
|
32
|
+
this.devServerRepo = devServerRepo;
|
|
33
|
+
this.routeRuleRepo = routeRuleRepo;
|
|
34
|
+
this.app = app;
|
|
35
|
+
app.use([`${getConfig().apiPrefix}/inject/getcurrentenv`], (req, res) => {
|
|
36
|
+
res.json({ envId: this.envId });
|
|
37
|
+
res.end();
|
|
38
|
+
});
|
|
39
|
+
// 代理 /envm 相关的请求到无需经过 Dev Server ProxyServer(PostProxyServer)
|
|
40
|
+
app.use(["/envm-inject", getConfig().apiPrefix], this.createInjectProxyMiddleware());
|
|
41
|
+
// 主代理中间件
|
|
42
|
+
app.use(this.createPreProxyMiddleware());
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 创建注入脚本的代理中间件
|
|
46
|
+
* 代理所有 /envm-inject 开头的请求到 ProxyServer
|
|
47
|
+
*/
|
|
48
|
+
createInjectProxyMiddleware() {
|
|
49
|
+
return createProxyMiddleware({
|
|
50
|
+
ws: true,
|
|
51
|
+
pathRewrite: (path, req) => {
|
|
52
|
+
return req.originalUrl;
|
|
53
|
+
},
|
|
54
|
+
router: () => {
|
|
55
|
+
const config = getConfig();
|
|
56
|
+
// 代理到 ProxyServer(后置代理服务器)
|
|
57
|
+
return `http://localhost:${config.port}`;
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
*
|
|
63
|
+
* @returns 获取绑定的环境信息
|
|
64
|
+
*/
|
|
65
|
+
getEnvItem() {
|
|
66
|
+
const envItem = this.envRepo.findOneById(this.envId);
|
|
67
|
+
return envItem;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 获取绑定的服务器详情
|
|
71
|
+
*/
|
|
72
|
+
getDevServer() { }
|
|
73
|
+
/**
|
|
74
|
+
* 当前 代理的 cookie 后缀
|
|
75
|
+
*/
|
|
76
|
+
get cookieSuffix() {
|
|
77
|
+
const envItem = this.getEnvItem();
|
|
78
|
+
return `-${envItem?.port}-${PreProxyServer.configCookieSuffix}`;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 配置的 cookie 后缀
|
|
82
|
+
*/
|
|
83
|
+
static get configCookieSuffix() {
|
|
84
|
+
return `${getConfig().cookieSuffix}`;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 根据请求路径匹配路由规则,检查是否需要注入 Script
|
|
88
|
+
* @param requestPath 请求路径
|
|
89
|
+
* @returns 是否需要注入 Script
|
|
90
|
+
*/
|
|
91
|
+
shouldInjectScript(requestPath) {
|
|
92
|
+
const routeRules = this.routeRuleRepo.getByEnvId(this.envId);
|
|
93
|
+
const enabledRules = routeRules.filter((rule) => rule.enabled !== false);
|
|
94
|
+
for (const rule of enabledRules) {
|
|
95
|
+
if (minimatch(requestPath, rule.pathPrefix, { dot: true })) {
|
|
96
|
+
return rule.injectScript === true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 根据请求路径匹配路由规则
|
|
103
|
+
* @param requestPath 请求路径
|
|
104
|
+
* @returns 匹配到的目标环境地址,如果没有匹配则返回 null
|
|
105
|
+
*/
|
|
106
|
+
matchRouteRule(requestPath) {
|
|
107
|
+
const routeRules = this.routeRuleRepo.getByEnvId(this.envId);
|
|
108
|
+
// 过滤出已启用的规则
|
|
109
|
+
const enabledRules = routeRules.filter((rule) => rule.enabled !== false);
|
|
110
|
+
// 按 pathPrefix 长度降序排序,确保最长前缀匹配优先
|
|
111
|
+
const sortedRules = [...enabledRules].sort((a, b) => b.pathPrefix.length - a.pathPrefix.length);
|
|
112
|
+
for (const rule of sortedRules) {
|
|
113
|
+
// 使用 minimatch 支持 glob 模式匹配
|
|
114
|
+
if (minimatch(requestPath, rule.pathPrefix, { dot: true })) {
|
|
115
|
+
// 获取目标环境信息
|
|
116
|
+
if (rule.targetEnvId) {
|
|
117
|
+
const targetEnv = this.envRepo.findOneById(rule.targetEnvId);
|
|
118
|
+
if (targetEnv?.apiBaseUrl) {
|
|
119
|
+
devServerLogger.info(`路由规则匹配成功: ${requestPath} -> ${rule.pathPrefix}, 转发到: ${targetEnv.apiBaseUrl}`);
|
|
120
|
+
return targetEnv.apiBaseUrl;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 生成代理中间件
|
|
129
|
+
* @returns
|
|
130
|
+
*/
|
|
131
|
+
createPreProxyMiddleware() {
|
|
132
|
+
// 前置转发:将请求转发到 开发服务器
|
|
133
|
+
return createProxyMiddleware({
|
|
134
|
+
ws: true,
|
|
135
|
+
changeOrigin: true,
|
|
136
|
+
selfHandleResponse: true,
|
|
137
|
+
router: (req) => {
|
|
138
|
+
const requestPath = req.path || "/";
|
|
139
|
+
// 1. 检查是否配置了路由规则
|
|
140
|
+
const customTarget = this.matchRouteRule(requestPath);
|
|
141
|
+
if (customTarget) {
|
|
142
|
+
return customTarget;
|
|
143
|
+
}
|
|
144
|
+
// 2. 默认转发到 devServer
|
|
145
|
+
const envItem = this.getEnvItem();
|
|
146
|
+
const devServerConfig = this.devServerRepo.findOneById({
|
|
147
|
+
id: envItem?.devServerId ?? "",
|
|
148
|
+
});
|
|
149
|
+
return `${devServerConfig?.devServerUrl}`;
|
|
150
|
+
},
|
|
151
|
+
on: {
|
|
152
|
+
proxyReq: (proxyReq, req) => {
|
|
153
|
+
const requestPath = req.path || "/";
|
|
154
|
+
// 优先使用路由规则的目标地址,否则使用环境默认的 apiBaseUrl
|
|
155
|
+
const customTarget = this.matchRouteRule(requestPath);
|
|
156
|
+
const target = customTarget || `${this.getEnvItem()?.apiBaseUrl}`;
|
|
157
|
+
proxyReq.setHeader("x-api-server", `${target}`);
|
|
158
|
+
this._rewrieCookieOnProxyReq(proxyReq, req);
|
|
159
|
+
},
|
|
160
|
+
proxyRes: (proxyRes, req, res) => {
|
|
161
|
+
this._rewriteSetCookieOnProxyRes(proxyRes);
|
|
162
|
+
this._rewriteLoginRedirect(proxyRes, req, res);
|
|
163
|
+
},
|
|
164
|
+
proxyReqWs: (proxyReq, req) => {
|
|
165
|
+
const requestPath = req.path || "/";
|
|
166
|
+
const customTarget = this.matchRouteRule(requestPath);
|
|
167
|
+
const target = customTarget || `${this.getEnvItem()?.apiBaseUrl}`;
|
|
168
|
+
proxyReq.setHeader("x-api-server", `${target}`);
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* 代理的时候如果收到了 setcookie 请求
|
|
175
|
+
* 给 每一个 setcookie 追加保存一个 代理cookie
|
|
176
|
+
* @param proxyRes
|
|
177
|
+
*/
|
|
178
|
+
_rewriteSetCookieOnProxyRes(proxyRes) {
|
|
179
|
+
const envItem = this.getEnvItem();
|
|
180
|
+
const setCookie = proxyRes.headers["set-cookie"];
|
|
181
|
+
if (envItem && setCookie) {
|
|
182
|
+
const setCookies = setCookieParser.parse(setCookie);
|
|
183
|
+
const proxyCookie = setCookies.map((item) => {
|
|
184
|
+
const cookie = {
|
|
185
|
+
...item,
|
|
186
|
+
name: `${item.name}${this.cookieSuffix}`,
|
|
187
|
+
};
|
|
188
|
+
return libCookie.serialize(cookie.name, cookie.value, cookie);
|
|
189
|
+
});
|
|
190
|
+
setCookie.push(...proxyCookie);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* 代理 转发的时候,将对应端口的cookie 重写回去
|
|
195
|
+
*/
|
|
196
|
+
_rewrieCookieOnProxyReq(proxyReq, req) {
|
|
197
|
+
const envItem = this.getEnvItem();
|
|
198
|
+
if (envItem && req.headers.cookie) {
|
|
199
|
+
const cookie = libCookie.parse(req.headers.cookie || "");
|
|
200
|
+
const newCookies = [];
|
|
201
|
+
Object.keys(cookie).forEach((item) => {
|
|
202
|
+
if (item.endsWith(PreProxyServer.configCookieSuffix)) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
let cookieName = `${item}${this.cookieSuffix}`;
|
|
206
|
+
if (!cookie[cookieName]) {
|
|
207
|
+
cookieName = item;
|
|
208
|
+
}
|
|
209
|
+
newCookies.push(libCookie.serialize(item, cookie[cookieName] || ""));
|
|
210
|
+
});
|
|
211
|
+
proxyReq.setHeader("cookie", newCookies.join(";"));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
_rewriteLoginRedirect(proxyRes, req, res) {
|
|
215
|
+
const contentType = proxyRes.headers["content-type"] || "";
|
|
216
|
+
const requestPath = req.path || "/";
|
|
217
|
+
// 转发状态码 + 响应头
|
|
218
|
+
res.statusCode = proxyRes.statusCode || 200;
|
|
219
|
+
for (const key in proxyRes.headers) {
|
|
220
|
+
if (!res.headersSent && proxyRes.headers[key]) {
|
|
221
|
+
res.setHeader(key, proxyRes.headers[key]);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const chunks = [];
|
|
225
|
+
proxyRes.on("data", (chunk) => chunks.push(chunk));
|
|
226
|
+
proxyRes.on("end", () => {
|
|
227
|
+
try {
|
|
228
|
+
const body = Buffer.concat(chunks);
|
|
229
|
+
// 只对 HTML && 路由规则开启注入的路径
|
|
230
|
+
if (contentType.includes("text/html") &&
|
|
231
|
+
this.shouldInjectScript(requestPath)) {
|
|
232
|
+
const config = getConfig();
|
|
233
|
+
const scriptDir = config.injectScriptDir;
|
|
234
|
+
if (!scriptDir) {
|
|
235
|
+
devServerLogger.warn("未配置注入脚本目录 (injectScriptDir)");
|
|
236
|
+
res.end(body);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
let html = body.toString("utf8");
|
|
240
|
+
// 读取文件夹下所有 js 文件(排除 # 开头的文件)
|
|
241
|
+
const scriptTags = this.generateImportScripts(scriptDir);
|
|
242
|
+
html = html.replace(/<\/body>\s*<\/html>/gi, `${scriptTags}</body></html>`);
|
|
243
|
+
const newBody = Buffer.from(html, "utf8");
|
|
244
|
+
res.setHeader("Content-Length", newBody.length);
|
|
245
|
+
res.end(newBody);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
// 其他内容原样返回
|
|
249
|
+
res.end(body);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
console.error("注入处理异常", err);
|
|
254
|
+
res.end(Buffer.concat(chunks));
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* 生成导入脚本的 HTML
|
|
260
|
+
* @param scriptDir 脚本文件夹路径
|
|
261
|
+
* @returns 脚本标签 HTML
|
|
262
|
+
*/
|
|
263
|
+
generateImportScripts(scriptDir) {
|
|
264
|
+
const fullPath = path.resolve(scriptDir);
|
|
265
|
+
if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isDirectory()) {
|
|
266
|
+
devServerLogger.warn(`注入脚本目录不存在: ${fullPath}`);
|
|
267
|
+
return "";
|
|
268
|
+
}
|
|
269
|
+
const files = fs.readdirSync(fullPath);
|
|
270
|
+
const jsFiles = files
|
|
271
|
+
.filter((file) => file.endsWith(".js") && !file.startsWith("#"))
|
|
272
|
+
.sort();
|
|
273
|
+
if (jsFiles.length === 0) {
|
|
274
|
+
devServerLogger.warn(`注入脚本目录没有 js 文件: ${fullPath}`);
|
|
275
|
+
return "";
|
|
276
|
+
}
|
|
277
|
+
const importStatements = jsFiles
|
|
278
|
+
.map((file) => `<script type="module" src="/envm-inject/${file}"></script>`)
|
|
279
|
+
.join("\n");
|
|
280
|
+
return importStatements;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* 启动服务
|
|
284
|
+
* @param envItem
|
|
285
|
+
* @returns
|
|
286
|
+
*/
|
|
287
|
+
async startServer() {
|
|
288
|
+
const { port } = this.getEnvItem();
|
|
289
|
+
if (PreProxyServer.appMap[this.envId]) {
|
|
290
|
+
devServerLogger.info(`环境 ${this.envId} 已经启动`);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
this.server = await new Promise((resolve, reject) => {
|
|
294
|
+
const server = this.app.listen(port, () => {
|
|
295
|
+
devServerLogger.info(`Server is running on http://localhost:${port}`);
|
|
296
|
+
resolve(server);
|
|
297
|
+
});
|
|
298
|
+
server.on("error", (err) => {
|
|
299
|
+
console.error(`端口 ${port} 启动失败:`, err.message);
|
|
300
|
+
reject(err);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
this.sockets = new Set();
|
|
304
|
+
this.server.on("connection", (socket) => {
|
|
305
|
+
this.sockets.add(socket);
|
|
306
|
+
socket.setTimeout(300000);
|
|
307
|
+
socket.on("timeout", () => {
|
|
308
|
+
socket.destroy();
|
|
309
|
+
this.sockets.delete(socket);
|
|
310
|
+
});
|
|
311
|
+
socket.on("close", () => {
|
|
312
|
+
this.sockets.delete(socket);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
static stopServer(id) {
|
|
317
|
+
return new Promise((resolve) => {
|
|
318
|
+
if (!this.getAppInsByPort(id)) {
|
|
319
|
+
devServerLogger.info(`端口 【${id}】 未启动,无需停止!`);
|
|
320
|
+
resolve(1);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
for (const socket of this.appMap[id].sockets) {
|
|
324
|
+
socket.destroy();
|
|
325
|
+
}
|
|
326
|
+
this.appMap[id].server.close((err) => {
|
|
327
|
+
if (err) {
|
|
328
|
+
console.error("服务器关闭失败:", err);
|
|
329
|
+
resolve(0);
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
delete this.appMap[id];
|
|
333
|
+
devServerLogger.info({ id }, `Server on port ${id} 已关闭`);
|
|
334
|
+
resolve(1);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* 保存启动的环境实例
|
|
342
|
+
*/
|
|
343
|
+
PreProxyServer.appMap = {};
|
|
344
|
+
export default PreProxyServer;
|