feat: 新增web配置中心
This commit is contained in:
parent
cab53e7947
commit
8d20792fc3
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
node_modules/
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
14
README.md
14
README.md
@ -95,7 +95,21 @@ npm run typecheck
|
|||||||
| 方法 | 路径 | 说明 |
|
| 方法 | 路径 | 说明 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `GET` | `http://localhost:<proxyPort>/__config` | 查看当前路由与配置 |
|
| `GET` | `http://localhost:<proxyPort>/__config` | 查看当前路由与配置 |
|
||||||
|
| `POST` | `http://localhost:<proxyPort>/__config` | 保存 `config.json`(包含 `routes` 与 `config`) |
|
||||||
| `POST` | `http://localhost:<proxyPort>/__reload-config` | 手动重新加载 `config.json` |
|
| `POST` | `http://localhost:<proxyPort>/__reload-config` | 手动重新加载 `config.json` |
|
||||||
|
| `POST` | `http://localhost:<proxyPort>/__routes` | 动态新增单个路由与 mock 文件 |
|
||||||
|
| `GET` | `http://localhost:<proxyPort>/__admin` | 配置管理页面(Element UI) |
|
||||||
|
|
||||||
|
#### `POST /__routes` 请求示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"route": "/api/new/mock",
|
||||||
|
"filePath": "mock/new-api.json",
|
||||||
|
"fileContent": "{\"code\":0,\"message\":\"ok\"}",
|
||||||
|
"overwrite": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### TypeScript 与编译说明
|
### TypeScript 与编译说明
|
||||||
|
|
||||||
|
|||||||
293
admin.html
Normal file
293
admin.html
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>API Proxy Mock 配置管理</title>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 20px auto;
|
||||||
|
padding: 0 16px 24px;
|
||||||
|
}
|
||||||
|
.section-card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.small-text {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app" class="container">
|
||||||
|
<el-card class="section-card">
|
||||||
|
<div slot="header"><strong>基础配置</strong></div>
|
||||||
|
<el-form :model="form.config" label-width="180px">
|
||||||
|
<el-form-item label="Mock 开关">
|
||||||
|
<el-switch v-model="form.config.mockEnabled"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="自动重载配置文件">
|
||||||
|
<el-switch v-model="form.config.reloadOnChange"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="默认响应 Content-Type">
|
||||||
|
<el-input v-model="form.config.defaultContentType"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="本地代理端口">
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.config.proxyPort"
|
||||||
|
:min="1"
|
||||||
|
:max="65535"
|
||||||
|
></el-input-number>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="目标主机">
|
||||||
|
<el-input v-model="form.config.targetHost"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="目标端口">
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.config.targetPort"
|
||||||
|
:min="1"
|
||||||
|
:max="65535"
|
||||||
|
></el-input-number>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="目标 HTTPS">
|
||||||
|
<el-switch v-model="form.config.targetHttps"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="section-card">
|
||||||
|
<div slot="header" style="display:flex;justify-content:space-between;align-items:center;">
|
||||||
|
<strong>路由配置(routes)</strong>
|
||||||
|
<el-button size="mini" type="primary" @click="addRouteRow">新增一行</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="form.routes" border>
|
||||||
|
<el-table-column label="请求路径" min-width="260">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-input v-model="scope.row.route" placeholder="/api/example"></el-input>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="Mock 文件路径" min-width="300">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-input
|
||||||
|
v-model="scope.row.filePath"
|
||||||
|
placeholder="mock/example.json"
|
||||||
|
></el-input>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button size="mini" type="danger" @click="removeRoute(scope.$index)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div class="small-text" style="margin-top:8px;">
|
||||||
|
说明:如果路由以 # 开头,会作为注释保留,不参与 mock 命中。
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="section-card">
|
||||||
|
<div slot="header"><strong>动态新增接口配置文件</strong></div>
|
||||||
|
<el-form :model="newRoute" label-width="180px">
|
||||||
|
<el-form-item label="内容模板">
|
||||||
|
<el-select
|
||||||
|
v-model="newRoute.template"
|
||||||
|
placeholder="请选择模板"
|
||||||
|
@change="applyTemplate"
|
||||||
|
style="width: 100%;"
|
||||||
|
>
|
||||||
|
<el-option label="自定义内容" value="custom"></el-option>
|
||||||
|
<el-option label="基础失败模板" value="basicError"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="请求路径">
|
||||||
|
<el-input v-model="newRoute.route" placeholder="/api/new/mock"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Mock 文件路径">
|
||||||
|
<el-input
|
||||||
|
v-model="newRoute.filePath"
|
||||||
|
placeholder="mock/new-api.json"
|
||||||
|
:disabled="newRoute.template === 'basicError'"
|
||||||
|
></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="文件内容">
|
||||||
|
<el-input
|
||||||
|
type="textarea"
|
||||||
|
:rows="8"
|
||||||
|
v-model="newRoute.fileContent"
|
||||||
|
placeholder='{"code":0,"message":"ok"}'
|
||||||
|
:disabled="newRoute.template === 'basicError'"
|
||||||
|
></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="覆盖已存在文件">
|
||||||
|
<el-switch
|
||||||
|
v-model="newRoute.overwrite"
|
||||||
|
:disabled="newRoute.template === 'basicError'"
|
||||||
|
></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<el-button type="success" @click="createRouteAndFile">创建路由+文件</el-button>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card>
|
||||||
|
<div class="actions">
|
||||||
|
<el-button type="primary" @click="saveConfig">保存配置到 config.json</el-button>
|
||||||
|
<el-button @click="reloadConfig">重载服务内存配置</el-button>
|
||||||
|
<el-button @click="loadConfig">刷新页面数据</el-button>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
|
||||||
|
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
|
||||||
|
<script>
|
||||||
|
new Vue({
|
||||||
|
el: "#app",
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
form: {
|
||||||
|
config: {
|
||||||
|
mockEnabled: true,
|
||||||
|
cacheConfig: true,
|
||||||
|
reloadOnChange: true,
|
||||||
|
defaultContentType: "application/json",
|
||||||
|
proxyPort: 8877,
|
||||||
|
targetHost: "",
|
||||||
|
targetPort: 443,
|
||||||
|
targetHttps: true,
|
||||||
|
},
|
||||||
|
routes: [],
|
||||||
|
},
|
||||||
|
newRoute: {
|
||||||
|
template: "custom",
|
||||||
|
route: "",
|
||||||
|
filePath: "mock/",
|
||||||
|
fileContent: "",
|
||||||
|
overwrite: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created: function () {
|
||||||
|
this.loadConfig();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toRouteArray: function (routesObj) {
|
||||||
|
return Object.keys(routesObj || {}).map(function (route) {
|
||||||
|
return { route: route, filePath: routesObj[route] };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
toRouteObject: function (routeArray) {
|
||||||
|
var obj = {};
|
||||||
|
(routeArray || []).forEach(function (item) {
|
||||||
|
var route = (item.route || "").trim();
|
||||||
|
var filePath = (item.filePath || "").trim();
|
||||||
|
if (route && filePath) {
|
||||||
|
obj[route] = filePath;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
addRouteRow: function () {
|
||||||
|
this.form.routes.push({ route: "", filePath: "" });
|
||||||
|
},
|
||||||
|
removeRoute: function (index) {
|
||||||
|
this.form.routes.splice(index, 1);
|
||||||
|
},
|
||||||
|
loadConfig: async function () {
|
||||||
|
try {
|
||||||
|
var resp = await fetch("/__config");
|
||||||
|
var data = await resp.json();
|
||||||
|
this.form.config = Object.assign({}, this.form.config, data.config || {});
|
||||||
|
this.form.routes = this.toRouteArray(data.routes || {});
|
||||||
|
} catch (err) {
|
||||||
|
this.$message.error("加载配置失败: " + err.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
saveConfig: async function () {
|
||||||
|
var payload = {
|
||||||
|
config: this.form.config,
|
||||||
|
routes: this.toRouteObject(this.form.routes),
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
var resp = await fetch("/__config", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
var data = await resp.json();
|
||||||
|
if (!resp.ok || data.success === false) {
|
||||||
|
throw new Error(data.error || "保存失败");
|
||||||
|
}
|
||||||
|
this.$message.success("配置已保存");
|
||||||
|
} catch (err) {
|
||||||
|
this.$message.error("保存失败: " + err.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reloadConfig: async function () {
|
||||||
|
try {
|
||||||
|
var resp = await fetch("/__reload-config", { method: "POST" });
|
||||||
|
var data = await resp.json();
|
||||||
|
if (!resp.ok || data.success === false) {
|
||||||
|
throw new Error(data.error || "重载失败");
|
||||||
|
}
|
||||||
|
this.$message.success("配置已重载");
|
||||||
|
this.loadConfig();
|
||||||
|
} catch (err) {
|
||||||
|
this.$message.error("重载失败: " + err.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createRouteAndFile: async function () {
|
||||||
|
try {
|
||||||
|
var resp = await fetch("/__routes", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(
|
||||||
|
Object.assign({}, this.newRoute, {
|
||||||
|
useExistingFile: this.newRoute.template === "basicError",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
var data = await resp.json();
|
||||||
|
if (!resp.ok || data.success === false) {
|
||||||
|
throw new Error(data.error || "创建失败");
|
||||||
|
}
|
||||||
|
this.$message.success("接口配置文件创建成功");
|
||||||
|
this.newRoute.template = "custom";
|
||||||
|
this.newRoute.route = "";
|
||||||
|
this.newRoute.filePath = "mock/";
|
||||||
|
this.newRoute.fileContent = "";
|
||||||
|
this.newRoute.overwrite = false;
|
||||||
|
this.loadConfig();
|
||||||
|
} catch (err) {
|
||||||
|
this.$message.error("创建失败: " + err.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
applyTemplate: function (template) {
|
||||||
|
if (template === "basicError") {
|
||||||
|
this.newRoute.filePath = "mock/basic-error.json";
|
||||||
|
this.newRoute.fileContent =
|
||||||
|
'{"code": -1, "success": false, "msg":"失败"}';
|
||||||
|
this.newRoute.overwrite = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.newRoute.filePath = "mock/";
|
||||||
|
this.newRoute.fileContent = "";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"routes": {
|
"routes": {
|
||||||
"/api2/admin/banner/list": "mock/banner.txt"
|
"/api2/admin/banner/list": "mock/banner.txt",
|
||||||
|
"/api2/home/ad/list": "mock/basic-error.json"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"mockEnabled": true,
|
"mockEnabled": true,
|
||||||
|
|||||||
171
index.api.ts
171
index.api.ts
@ -33,6 +33,7 @@ interface ConfigFile {
|
|||||||
|
|
||||||
// 存储当前的路由配置
|
// 存储当前的路由配置
|
||||||
let MOCK_ROUTES: RouteConfig = {};
|
let MOCK_ROUTES: RouteConfig = {};
|
||||||
|
let RAW_ROUTES: RouteConfig = {};
|
||||||
let CONFIG: AppConfig = {
|
let CONFIG: AppConfig = {
|
||||||
cacheConfig: true,
|
cacheConfig: true,
|
||||||
reloadOnChange: true,
|
reloadOnChange: true,
|
||||||
@ -110,13 +111,15 @@ function loadConfig() {
|
|||||||
const configData: ConfigFile = JSON.parse(
|
const configData: ConfigFile = JSON.parse(
|
||||||
fs.readFileSync(CONFIG_FILE, "utf-8"),
|
fs.readFileSync(CONFIG_FILE, "utf-8"),
|
||||||
);
|
);
|
||||||
MOCK_ROUTES = filterActiveRoutes(configData.routes || {});
|
RAW_ROUTES = configData.routes || {};
|
||||||
|
MOCK_ROUTES = filterActiveRoutes(RAW_ROUTES);
|
||||||
CONFIG = configData.config || CONFIG;
|
CONFIG = configData.config || CONFIG;
|
||||||
} else {
|
} else {
|
||||||
const configData: ConfigFile = JSON.parse(
|
const configData: ConfigFile = JSON.parse(
|
||||||
fs.readFileSync(CONFIG_FILE, "utf-8"),
|
fs.readFileSync(CONFIG_FILE, "utf-8"),
|
||||||
);
|
);
|
||||||
MOCK_ROUTES = filterActiveRoutes(configData.routes || {});
|
RAW_ROUTES = configData.routes || {};
|
||||||
|
MOCK_ROUTES = filterActiveRoutes(RAW_ROUTES);
|
||||||
CONFIG = configData.config || CONFIG;
|
CONFIG = configData.config || CONFIG;
|
||||||
console.log(
|
console.log(
|
||||||
`[CONFIG] 配置文件已加载,共 ${Object.keys(MOCK_ROUTES).length} 个路由${CONFIG.mockEnabled !== false ? "" : "(mock 已关闭,全部走代理)"}`,
|
`[CONFIG] 配置文件已加载,共 ${Object.keys(MOCK_ROUTES).length} 个路由${CONFIG.mockEnabled !== false ? "" : "(mock 已关闭,全部走代理)"}`,
|
||||||
@ -131,6 +134,7 @@ function loadConfig() {
|
|||||||
MOCK_ROUTES = {
|
MOCK_ROUTES = {
|
||||||
"/api2/user/list": "mock/user_list.txt",
|
"/api2/user/list": "mock/user_list.txt",
|
||||||
};
|
};
|
||||||
|
RAW_ROUTES = { ...MOCK_ROUTES };
|
||||||
CONFIG = {
|
CONFIG = {
|
||||||
cacheConfig: true,
|
cacheConfig: true,
|
||||||
reloadOnChange: true,
|
reloadOnChange: true,
|
||||||
@ -142,6 +146,31 @@ function loadConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildCurrentConfigFile(): ConfigFile {
|
||||||
|
return {
|
||||||
|
routes: { ...RAW_ROUTES },
|
||||||
|
config: { ...CONFIG },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveConfigFile(nextConfig: ConfigFile): void {
|
||||||
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(nextConfig, null, 2), "utf-8");
|
||||||
|
RAW_ROUTES = nextConfig.routes || {};
|
||||||
|
MOCK_ROUTES = filterActiveRoutes(RAW_ROUTES);
|
||||||
|
CONFIG = nextConfig.config || CONFIG;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readBody(req: http.IncomingMessage): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
req.on("data", (chunk: Buffer) => {
|
||||||
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||||
|
});
|
||||||
|
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
||||||
|
req.on("error", reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 验证mock文件是否存在
|
// 验证mock文件是否存在
|
||||||
function validateMockFiles() {
|
function validateMockFiles() {
|
||||||
console.log(`[CONFIG] 验证mock文件...`);
|
console.log(`[CONFIG] 验证mock文件...`);
|
||||||
@ -255,27 +284,58 @@ const proxyServer = http.createServer((clientReq, clientRes) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (requestPath === "/__config") {
|
if (requestPath === "/__config") {
|
||||||
if (clientReq.method !== "GET") {
|
if (clientReq.method !== "GET" && clientReq.method !== "POST") {
|
||||||
clientRes.writeHead(405, {
|
clientRes.writeHead(405, {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Allow: "GET",
|
Allow: "GET, POST",
|
||||||
});
|
});
|
||||||
clientRes.end(
|
clientRes.end(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
success: false,
|
success: false,
|
||||||
error: "Method Not Allowed",
|
error: "Method Not Allowed",
|
||||||
allow: ["GET"],
|
allow: ["GET", "POST"],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (clientReq.method === "POST") {
|
||||||
|
readBody(clientReq)
|
||||||
|
.then((bodyText) => {
|
||||||
|
const body = bodyText ? JSON.parse(bodyText) : {};
|
||||||
|
const nextConfig: ConfigFile = {
|
||||||
|
routes: body.routes || {},
|
||||||
|
config: body.config || CONFIG,
|
||||||
|
};
|
||||||
|
saveConfigFile(nextConfig);
|
||||||
|
validateMockFiles();
|
||||||
|
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: "Configuration saved successfully",
|
||||||
|
totalRoutes: Object.keys(MOCK_ROUTES).length,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
clientRes.writeHead(400, { "Content-Type": "application/json" });
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||||
clientRes.end(
|
clientRes.end(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
routes: MOCK_ROUTES,
|
routes: RAW_ROUTES,
|
||||||
config: CONFIG,
|
config: CONFIG,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
totalRoutes: Object.keys(MOCK_ROUTES).length,
|
totalRoutes: Object.keys(MOCK_ROUTES).length,
|
||||||
@ -296,6 +356,105 @@ const proxyServer = http.createServer((clientReq, clientRes) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (requestPath === "/__routes") {
|
||||||
|
if (clientReq.method !== "POST") {
|
||||||
|
clientRes.writeHead(405, {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Allow: "POST",
|
||||||
|
});
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: "Method Not Allowed",
|
||||||
|
allow: ["POST"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
readBody(clientReq)
|
||||||
|
.then((bodyText) => {
|
||||||
|
const body = bodyText ? JSON.parse(bodyText) : {};
|
||||||
|
const route = String(body.route || "").trim();
|
||||||
|
const filePath = String(body.filePath || "").trim();
|
||||||
|
const fileContent = String(body.fileContent || "");
|
||||||
|
const overwrite = body.overwrite === true;
|
||||||
|
const template = String(body.template || "").trim();
|
||||||
|
const useExistingFile =
|
||||||
|
body.useExistingFile === true || template === "basicError";
|
||||||
|
|
||||||
|
if (!route.startsWith("/")) {
|
||||||
|
throw new Error("route must start with '/'");
|
||||||
|
}
|
||||||
|
if (!filePath) {
|
||||||
|
throw new Error("filePath is required");
|
||||||
|
}
|
||||||
|
if (path.isAbsolute(filePath)) {
|
||||||
|
throw new Error("filePath must be a relative path");
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullPath = path.join(__dirname, filePath);
|
||||||
|
if (!fullPath.startsWith(__dirname)) {
|
||||||
|
throw new Error("filePath is invalid");
|
||||||
|
}
|
||||||
|
if (useExistingFile) {
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
throw new Error("mock file does not exist");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!overwrite && fs.existsSync(fullPath)) {
|
||||||
|
throw new Error(
|
||||||
|
"mock file already exists, set overwrite=true to replace",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||||
|
fs.writeFileSync(fullPath, fileContent, "utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextConfig = buildCurrentConfigFile();
|
||||||
|
nextConfig.routes[route] = filePath;
|
||||||
|
saveConfigFile(nextConfig);
|
||||||
|
|
||||||
|
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: "Route and mock file created successfully",
|
||||||
|
route,
|
||||||
|
filePath,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
clientRes.writeHead(400, { "Content-Type": "application/json" });
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestPath === "/__admin") {
|
||||||
|
if (clientReq.method !== "GET") {
|
||||||
|
clientRes.writeHead(405, { "Content-Type": "text/plain; charset=utf-8" });
|
||||||
|
clientRes.end("Method Not Allowed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const adminHtmlPath = path.join(__dirname, "admin.html");
|
||||||
|
if (!fs.existsSync(adminHtmlPath)) {
|
||||||
|
clientRes.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
||||||
|
clientRes.end("admin.html not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const html = fs.readFileSync(adminHtmlPath, "utf-8");
|
||||||
|
clientRes.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
||||||
|
clientRes.end(html);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否为需要mock的路由
|
// 检查是否为需要mock的路由
|
||||||
if (isMockRoute(requestPath)) {
|
if (isMockRoute(requestPath)) {
|
||||||
const mockFile = getMockFilePath(requestPath);
|
const mockFile = getMockFilePath(requestPath);
|
||||||
|
|||||||
1
mock/basic-error.json
Normal file
1
mock/basic-error.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"code": -1, "success": false, "msg": "失败"}
|
||||||
168
pnpm-lock.yaml
generated
Normal file
168
pnpm-lock.yaml
generated
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
importers:
|
||||||
|
|
||||||
|
.:
|
||||||
|
devDependencies:
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^25.5.0
|
||||||
|
version: 25.6.0
|
||||||
|
ts-node:
|
||||||
|
specifier: ^10.9.2
|
||||||
|
version: 10.9.2(@types/node@25.6.0)(typescript@5.9.3)
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.7.3
|
||||||
|
version: 5.9.3
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
'@cspotcode/source-map-support@0.8.1':
|
||||||
|
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
'@jridgewell/resolve-uri@3.1.2':
|
||||||
|
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
|
'@jridgewell/sourcemap-codec@1.5.5':
|
||||||
|
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||||
|
|
||||||
|
'@jridgewell/trace-mapping@0.3.9':
|
||||||
|
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||||
|
|
||||||
|
'@tsconfig/node10@1.0.12':
|
||||||
|
resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==}
|
||||||
|
|
||||||
|
'@tsconfig/node12@1.0.11':
|
||||||
|
resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==}
|
||||||
|
|
||||||
|
'@tsconfig/node14@1.0.3':
|
||||||
|
resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==}
|
||||||
|
|
||||||
|
'@tsconfig/node16@1.0.4':
|
||||||
|
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
|
||||||
|
|
||||||
|
'@types/node@25.6.0':
|
||||||
|
resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==}
|
||||||
|
|
||||||
|
acorn-walk@8.3.5:
|
||||||
|
resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
|
||||||
|
acorn@8.16.0:
|
||||||
|
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
arg@4.1.3:
|
||||||
|
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
|
||||||
|
|
||||||
|
create-require@1.1.1:
|
||||||
|
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
|
||||||
|
|
||||||
|
diff@4.0.4:
|
||||||
|
resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==}
|
||||||
|
engines: {node: '>=0.3.1'}
|
||||||
|
|
||||||
|
make-error@1.3.6:
|
||||||
|
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
|
||||||
|
|
||||||
|
ts-node@10.9.2:
|
||||||
|
resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@swc/core': '>=1.2.50'
|
||||||
|
'@swc/wasm': '>=1.2.50'
|
||||||
|
'@types/node': '*'
|
||||||
|
typescript: '>=2.7'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@swc/core':
|
||||||
|
optional: true
|
||||||
|
'@swc/wasm':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
typescript@5.9.3:
|
||||||
|
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||||
|
engines: {node: '>=14.17'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
undici-types@7.19.2:
|
||||||
|
resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==}
|
||||||
|
|
||||||
|
v8-compile-cache-lib@3.0.1:
|
||||||
|
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
|
||||||
|
|
||||||
|
yn@3.1.1:
|
||||||
|
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
snapshots:
|
||||||
|
|
||||||
|
'@cspotcode/source-map-support@0.8.1':
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/trace-mapping': 0.3.9
|
||||||
|
|
||||||
|
'@jridgewell/resolve-uri@3.1.2': {}
|
||||||
|
|
||||||
|
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||||
|
|
||||||
|
'@jridgewell/trace-mapping@0.3.9':
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
'@tsconfig/node10@1.0.12': {}
|
||||||
|
|
||||||
|
'@tsconfig/node12@1.0.11': {}
|
||||||
|
|
||||||
|
'@tsconfig/node14@1.0.3': {}
|
||||||
|
|
||||||
|
'@tsconfig/node16@1.0.4': {}
|
||||||
|
|
||||||
|
'@types/node@25.6.0':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 7.19.2
|
||||||
|
|
||||||
|
acorn-walk@8.3.5:
|
||||||
|
dependencies:
|
||||||
|
acorn: 8.16.0
|
||||||
|
|
||||||
|
acorn@8.16.0: {}
|
||||||
|
|
||||||
|
arg@4.1.3: {}
|
||||||
|
|
||||||
|
create-require@1.1.1: {}
|
||||||
|
|
||||||
|
diff@4.0.4: {}
|
||||||
|
|
||||||
|
make-error@1.3.6: {}
|
||||||
|
|
||||||
|
ts-node@10.9.2(@types/node@25.6.0)(typescript@5.9.3):
|
||||||
|
dependencies:
|
||||||
|
'@cspotcode/source-map-support': 0.8.1
|
||||||
|
'@tsconfig/node10': 1.0.12
|
||||||
|
'@tsconfig/node12': 1.0.11
|
||||||
|
'@tsconfig/node14': 1.0.3
|
||||||
|
'@tsconfig/node16': 1.0.4
|
||||||
|
'@types/node': 25.6.0
|
||||||
|
acorn: 8.16.0
|
||||||
|
acorn-walk: 8.3.5
|
||||||
|
arg: 4.1.3
|
||||||
|
create-require: 1.1.1
|
||||||
|
diff: 4.0.4
|
||||||
|
make-error: 1.3.6
|
||||||
|
typescript: 5.9.3
|
||||||
|
v8-compile-cache-lib: 3.0.1
|
||||||
|
yn: 3.1.1
|
||||||
|
|
||||||
|
typescript@5.9.3: {}
|
||||||
|
|
||||||
|
undici-types@7.19.2: {}
|
||||||
|
|
||||||
|
v8-compile-cache-lib@3.0.1: {}
|
||||||
|
|
||||||
|
yn@3.1.1: {}
|
||||||
Loading…
x
Reference in New Issue
Block a user