qxun-api-generator 1.0.0
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/bin/api-generator.js +23 -0
- package/lib/index.js +204 -0
- package/lib/schema.json +46 -0
- package/package.json +22 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { program } = require("commander");
|
|
4
|
+
const { version } = require("../package.json");
|
|
5
|
+
|
|
6
|
+
program
|
|
7
|
+
.command("generate", { isDefault: true })
|
|
8
|
+
.version(version)
|
|
9
|
+
.description("Generate End-to-end type-safe APIs")
|
|
10
|
+
.option("-i, --input <input>", "api.json 所在路径,默认是项目根目录")
|
|
11
|
+
.option("--auth [auth]", "Authorization header 值(访问受保护的 swagger 地址时使用)")
|
|
12
|
+
.action(function (inputSchema) {
|
|
13
|
+
const options = program.opts();
|
|
14
|
+
const finalSchema = { ...inputSchema, ...options };
|
|
15
|
+
require("../lib")
|
|
16
|
+
.run(finalSchema)
|
|
17
|
+
.catch((err) => {
|
|
18
|
+
console.error("错误:", err);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
program.parse(process.argv);
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.run = exports.writeFileSafely = void 0;
|
|
4
|
+
const ajv_1 = require("ajv");
|
|
5
|
+
const axios_1 = require("axios");
|
|
6
|
+
const cp = require("child_process");
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const mkdirp_1 = require("mkdirp");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
|
|
11
|
+
const DEFAULT_DIR = "src/api";
|
|
12
|
+
|
|
13
|
+
function checkResponseType(res) {
|
|
14
|
+
switch (res.headers["content-type"]) {
|
|
15
|
+
case "application/x-yaml":
|
|
16
|
+
return "YAML";
|
|
17
|
+
default:
|
|
18
|
+
return "JSON";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 安全写文件:自动创建所有中间目录
|
|
24
|
+
*/
|
|
25
|
+
async function writeFileSafely(filePath, data) {
|
|
26
|
+
await (0, mkdirp_1.mkdirp)(path.dirname(filePath));
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
fs.writeFile(filePath, data, (err) => {
|
|
29
|
+
if (err) reject(err);
|
|
30
|
+
else resolve();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
exports.writeFileSafely = writeFileSafely;
|
|
35
|
+
|
|
36
|
+
function deleteDirectory(dirPath) {
|
|
37
|
+
if (!fs.existsSync(dirPath)) return;
|
|
38
|
+
const files = fs.readdirSync(dirPath);
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
const cur = `${dirPath}/${file}`;
|
|
41
|
+
if (fs.lstatSync(cur).isDirectory()) {
|
|
42
|
+
deleteDirectory(cur);
|
|
43
|
+
} else {
|
|
44
|
+
fs.unlinkSync(cur);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
fs.rmdirSync(dirPath);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function replaceInFile(filePath, matcher, replacement) {
|
|
51
|
+
if (!fs.existsSync(filePath)) return;
|
|
52
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
53
|
+
fs.writeFileSync(filePath, content.replaceAll(matcher, replacement), "utf8");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 获取 Swagger 文件:
|
|
58
|
+
* - 本地路径 → 直接使用
|
|
59
|
+
* - 远端 URL → 下载保存到 _swaggers/ 目录后返回本地路径
|
|
60
|
+
*/
|
|
61
|
+
async function fetchSwagger(config, auth) {
|
|
62
|
+
const isLocal = !config.path.startsWith("http");
|
|
63
|
+
if (isLocal) return config;
|
|
64
|
+
|
|
65
|
+
const headers = {};
|
|
66
|
+
if (auth) headers["Authorization"] = auth;
|
|
67
|
+
|
|
68
|
+
const res = await axios_1.default.request({
|
|
69
|
+
method: "GET",
|
|
70
|
+
url: config.path,
|
|
71
|
+
headers,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const type = checkResponseType(res);
|
|
75
|
+
const ext = type === "YAML" ? ".yaml" : ".json";
|
|
76
|
+
const localFilePath = path.join(
|
|
77
|
+
process.cwd(),
|
|
78
|
+
config.outputDir || DEFAULT_DIR,
|
|
79
|
+
"_swaggers",
|
|
80
|
+
config.service + ext
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const data = type === "JSON" ? JSON.stringify(res.data, null, 2) : res.data;
|
|
84
|
+
await writeFileSafely(localFilePath, data);
|
|
85
|
+
|
|
86
|
+
return { ...config, path: localFilePath };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function generateApi(config) {
|
|
90
|
+
const destDir = path.join(process.cwd(), config.outputDir || DEFAULT_DIR, config.service);
|
|
91
|
+
console.log("输出目录:", destDir);
|
|
92
|
+
await (0, mkdirp_1.mkdirp)(destDir);
|
|
93
|
+
|
|
94
|
+
// 调用 openapi-generator CLI 生成 typescript-axios 代码
|
|
95
|
+
try {
|
|
96
|
+
cp.execSync(
|
|
97
|
+
`openapi-generator generate -g typescript-axios -i ${config.path} -o ${destDir}` +
|
|
98
|
+
` --additional-properties=useSingleRequestParameter=true,ensureUniqueParams=true,stringEnums=true`,
|
|
99
|
+
{ stdio: "inherit" }
|
|
100
|
+
);
|
|
101
|
+
} catch (e) {
|
|
102
|
+
console.error(e);
|
|
103
|
+
console.error('\n如果未安装 openapi-generator:请运行 `brew install openapi-generator`\n');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const apiFile = `${destDir}/api.ts`;
|
|
108
|
+
|
|
109
|
+
// 替换 axios import → 项目自定义 http 客户端
|
|
110
|
+
replaceInFile(
|
|
111
|
+
apiFile,
|
|
112
|
+
`import globalAxios from 'axios';`,
|
|
113
|
+
config.httpPath ?? `import { http as globalAxios } from '../../utils/http'`
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// 修正 AxiosPromise 类型(openapi-generator 6.x)
|
|
117
|
+
replaceInFile(
|
|
118
|
+
apiFile,
|
|
119
|
+
`import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';`,
|
|
120
|
+
`import type { AxiosInstance, AxiosRequestConfig } from 'axios';\n` +
|
|
121
|
+
` type AxiosPromise<T> = Promise<T extends {data?: any } ? T['data'] : void>;`
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// 修正 AxiosPromise 类型(openapi-generator 7.x)
|
|
125
|
+
replaceInFile(
|
|
126
|
+
apiFile,
|
|
127
|
+
`import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';`,
|
|
128
|
+
`import type { AxiosInstance, AxiosRequestConfig } from 'axios';\n` +
|
|
129
|
+
` type AxiosPromise<T> = Promise<T extends {data?: any } ? T['data'] : void>;`
|
|
130
|
+
);
|
|
131
|
+
replaceInFile(apiFile, `RawAxiosRequestConfig`, `AxiosRequestConfig`);
|
|
132
|
+
|
|
133
|
+
// 修正 createRequestFunction 签名,兼容自定义 http 客户端
|
|
134
|
+
replaceInFile(
|
|
135
|
+
`${destDir}/common.ts`,
|
|
136
|
+
"export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) {",
|
|
137
|
+
"export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: any, BASE_PATH: string, configuration?: Configuration) {"
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// 设置 BASE_PATH 为单一固定地址
|
|
141
|
+
replaceInFile(
|
|
142
|
+
`${destDir}/base.ts`,
|
|
143
|
+
`export const BASE_PATH = "http://localhost".replace(/\\/+$/, "");`,
|
|
144
|
+
`export const BASE_PATH = "${config.baseUrl ?? ''}";`
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// 清理无用文件
|
|
148
|
+
deleteDirectory(`${destDir}/.openapi-generator`);
|
|
149
|
+
fs.unlink(`${destDir}/.gitignore`, () => void 0);
|
|
150
|
+
fs.unlink(`${destDir}/.npmignore`, () => void 0);
|
|
151
|
+
fs.unlink(`${destDir}/.openapi-generator-ignore`, () => void 0);
|
|
152
|
+
fs.unlink(`${destDir}/git_push.sh`, () => void 0);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function run(inputSchema) {
|
|
156
|
+
const configPath = inputSchema.input ?? path.resolve(process.cwd(), "./api.json");
|
|
157
|
+
|
|
158
|
+
let config;
|
|
159
|
+
try {
|
|
160
|
+
config = require(configPath);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error("ERROR: 无法读取 api.json,请确认文件存在且格式正确");
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 校验配置格式
|
|
167
|
+
const ajv = new ajv_1.default();
|
|
168
|
+
const schema = fs.readFileSync(path.resolve(__dirname, "./schema.json"), { encoding: "utf-8" });
|
|
169
|
+
const validate = ajv.compile(JSON.parse(schema));
|
|
170
|
+
if (!validate(config)) {
|
|
171
|
+
console.error("ERROR: api.json 配置不合法", validate.errors);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const auth = inputSchema.auth;
|
|
176
|
+
|
|
177
|
+
console.log("\n生成中...");
|
|
178
|
+
|
|
179
|
+
const p = config.apis.map(async (api) => {
|
|
180
|
+
const resolved = await fetchSwagger(api, auth);
|
|
181
|
+
return generateApi(resolved);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const results = (await Promise.allSettled(p)).map((item, index) => ({
|
|
185
|
+
name: config.apis[index].service,
|
|
186
|
+
failed: item.status === "rejected",
|
|
187
|
+
reason: item.status === "rejected" ? item.reason : null,
|
|
188
|
+
}));
|
|
189
|
+
|
|
190
|
+
const success = results.filter((r) => !r.failed);
|
|
191
|
+
const failed = results.filter((r) => r.failed);
|
|
192
|
+
|
|
193
|
+
process.stdout.write("\x1b[1A\x1b[K");
|
|
194
|
+
console.log("\n执行完毕,共 " + results.length + " 个服务");
|
|
195
|
+
|
|
196
|
+
if (success.length) {
|
|
197
|
+
console.log("\n✅ 成功生成:" + success.map((r) => r.name).join(", ") + "\n");
|
|
198
|
+
}
|
|
199
|
+
if (failed.length) {
|
|
200
|
+
console.log("\x1b[31m%s\x1b[0m", "❌ 生成失败:" + failed.map((r) => r.name).join(", ") + "\n");
|
|
201
|
+
failed.forEach((r) => console.error(r.reason));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
exports.run = run;
|
package/lib/schema.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"additionalProperties": false,
|
|
4
|
+
"properties": {
|
|
5
|
+
"$schema": {
|
|
6
|
+
"type": "string"
|
|
7
|
+
},
|
|
8
|
+
"apis": {
|
|
9
|
+
"description": "API 定义数组",
|
|
10
|
+
"items": {
|
|
11
|
+
"$ref": "#/definitions/API"
|
|
12
|
+
},
|
|
13
|
+
"type": "array"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"definitions": {
|
|
17
|
+
"API": {
|
|
18
|
+
"additionalProperties": false,
|
|
19
|
+
"properties": {
|
|
20
|
+
"service": {
|
|
21
|
+
"description": "后端服务名称",
|
|
22
|
+
"type": "string"
|
|
23
|
+
},
|
|
24
|
+
"outputDir": {
|
|
25
|
+
"description": "输出目录",
|
|
26
|
+
"type": "string"
|
|
27
|
+
},
|
|
28
|
+
"path": {
|
|
29
|
+
"description": "swagger 路径,可以是本地路径也可以是远端地址",
|
|
30
|
+
"type": "string"
|
|
31
|
+
},
|
|
32
|
+
"httpPath": {
|
|
33
|
+
"description": "全局 http 文件的路径",
|
|
34
|
+
"type": "string"
|
|
35
|
+
},
|
|
36
|
+
"baseUrl": {
|
|
37
|
+
"description": "服务的 baseURL",
|
|
38
|
+
"type": "string"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"required": ["service", "outputDir", "path", "httpPath"],
|
|
42
|
+
"type": "object"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"type": "object"
|
|
46
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "qxun-api-generator",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "从 Swagger / OpenAPI 生成端到端类型安全的 TypeScript API Client",
|
|
5
|
+
"main": "./lib/index.js",
|
|
6
|
+
"typings": "./lib/index.d.ts",
|
|
7
|
+
"bin": "./bin/api-generator.js",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"lib",
|
|
13
|
+
"bin"
|
|
14
|
+
],
|
|
15
|
+
"license": "ISC",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"ajv": "^8.12.0",
|
|
18
|
+
"axios": "^1.4.0",
|
|
19
|
+
"commander": "^12.1.0",
|
|
20
|
+
"mkdirp": "^3.0.1"
|
|
21
|
+
}
|
|
22
|
+
}
|