swagger2api-v3 1.1.9 → 1.1.10
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/README.md +2 -0
- package/dist/.swagger2api.schema.json +4 -0
- package/dist/cli/index.js +20 -19
- package/dist/config/validator.js +2 -0
- package/dist/core/generator.d.ts +18 -0
- package/dist/core/generator.js +59 -13
- package/dist/core/parser.d.ts +6 -0
- package/dist/core/parser.js +44 -17
- package/dist/index.js +22 -19
- package/dist/types/index.d.ts +4 -0
- package/dist/utils/file.js +4 -3
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/logger.d.ts +115 -0
- package/dist/utils/logger.js +261 -0
- package/dist/utils/type.d.ts +12 -0
- package/dist/utils/type.js +25 -6
- package/package.json +19 -17
package/README.md
CHANGED
|
@@ -44,6 +44,7 @@ The tool generates a `.swagger.config.json` configuration file:
|
|
|
44
44
|
"generator": "typescript",
|
|
45
45
|
"requestStyle": "generic",
|
|
46
46
|
"groupByTags": true,
|
|
47
|
+
"multiTagStrategy": "first",
|
|
47
48
|
"overwrite": true,
|
|
48
49
|
"prefix": "",
|
|
49
50
|
"lint": "prettier --write",
|
|
@@ -79,6 +80,7 @@ npx swagger2api-v3 generate
|
|
|
79
80
|
| `output` | string | `'./src/api'` | Output directory for generated code |
|
|
80
81
|
| `generator` | string | `'typescript'` | Code generator type. Supports `'typescript'` and `'javascript'`. `'javascript'` outputs `.js` files and skips type file generation |
|
|
81
82
|
| `groupByTags` | boolean | `true` | Whether to group files by tags |
|
|
83
|
+
| `multiTagStrategy` | 'first' \| 'all' | `'first'` | Grouping strategy for operations with multiple tags. `first` uses only the first tag, `all` combines all tags into one group name |
|
|
82
84
|
| `overwrite` | boolean | `true` | Whether to overwrite existing files |
|
|
83
85
|
| `prefix` | string | `''` | Common prefix for API paths |
|
|
84
86
|
| `importTemplate` | string | - | Import statement template for request function |
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
"type": "boolean",
|
|
25
25
|
"description": "是否按 tags 分组生成文件"
|
|
26
26
|
},
|
|
27
|
+
"multiTagStrategy": {
|
|
28
|
+
"enum": ["first", "all"],
|
|
29
|
+
"description": "多 tag 接口分组策略。first 只使用第一个 tag,all 将所有 tags 合成一个分组"
|
|
30
|
+
},
|
|
27
31
|
"overwrite": {
|
|
28
32
|
"type": "boolean",
|
|
29
33
|
"description": "生成前是否覆盖输出目录"
|
package/dist/cli/index.js
CHANGED
|
@@ -38,6 +38,7 @@ const commander_1 = require("commander");
|
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const fs = __importStar(require("fs"));
|
|
40
40
|
const index_1 = require("../index");
|
|
41
|
+
const utils_1 = require("../utils");
|
|
41
42
|
const program = new commander_1.Command();
|
|
42
43
|
// 版本信息
|
|
43
44
|
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf-8'));
|
|
@@ -73,7 +74,7 @@ program
|
|
|
73
74
|
prettify: true
|
|
74
75
|
}
|
|
75
76
|
};
|
|
76
|
-
const { generate } = await
|
|
77
|
+
const { generate } = await import('../index.js');
|
|
77
78
|
await generate(config);
|
|
78
79
|
}
|
|
79
80
|
else {
|
|
@@ -82,7 +83,7 @@ program
|
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
catch (error) {
|
|
85
|
-
|
|
86
|
+
utils_1.logger.error('生成失败', error);
|
|
86
87
|
process.exit(1);
|
|
87
88
|
}
|
|
88
89
|
});
|
|
@@ -98,11 +99,11 @@ program
|
|
|
98
99
|
const config = createInitConfig(configPath, options.force);
|
|
99
100
|
const successMessage = getInitSuccessMessage(existsBeforeInit, options.force);
|
|
100
101
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
utils_1.logger.success(`${successMessage} ${configPath}`);
|
|
103
|
+
utils_1.logger.info('请根据需要修改配置文件,然后运行 swagger2api-v3 generate');
|
|
103
104
|
}
|
|
104
105
|
catch (error) {
|
|
105
|
-
|
|
106
|
+
utils_1.logger.error('创建配置文件失败', error);
|
|
106
107
|
process.exit(1);
|
|
107
108
|
}
|
|
108
109
|
});
|
|
@@ -115,29 +116,28 @@ program
|
|
|
115
116
|
const configPath = path.resolve(process.cwd(), options.config);
|
|
116
117
|
try {
|
|
117
118
|
if (!fs.existsSync(configPath)) {
|
|
118
|
-
|
|
119
|
+
utils_1.logger.error(`找不到配置文件: ${configPath}`);
|
|
119
120
|
process.exit(1);
|
|
120
121
|
}
|
|
121
122
|
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
122
123
|
const config = JSON.parse(configContent);
|
|
123
|
-
const { validateSwaggerConfig } = await
|
|
124
|
+
const { validateSwaggerConfig } = await import('../config/validator.js');
|
|
124
125
|
const errors = validateSwaggerConfig(config);
|
|
125
126
|
if (errors.length === 0) {
|
|
126
|
-
|
|
127
|
+
utils_1.logger.success('配置文件验证通过');
|
|
127
128
|
}
|
|
128
129
|
else {
|
|
129
|
-
|
|
130
|
-
errors.forEach((error) => console.error(` - ${error}`));
|
|
130
|
+
utils_1.logger.errorList('配置验证失败:', errors);
|
|
131
131
|
process.exit(1);
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
catch (error) {
|
|
135
135
|
if (error instanceof SyntaxError) {
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
utils_1.logger.error(`配置文件 JSON 格式错误: ${configPath}`);
|
|
137
|
+
utils_1.logger.error(`错误详情: ${error.message}`);
|
|
138
138
|
}
|
|
139
139
|
else {
|
|
140
|
-
|
|
140
|
+
utils_1.logger.error('配置文件验证失败', error);
|
|
141
141
|
}
|
|
142
142
|
process.exit(1);
|
|
143
143
|
}
|
|
@@ -175,6 +175,7 @@ function createDefaultConfig() {
|
|
|
175
175
|
generator: 'typescript',
|
|
176
176
|
requestStyle: 'generic',
|
|
177
177
|
groupByTags: true,
|
|
178
|
+
multiTagStrategy: 'first',
|
|
178
179
|
overwrite: true,
|
|
179
180
|
prefix: '',
|
|
180
181
|
lint: 'prettier --write',
|
|
@@ -202,12 +203,12 @@ function createDefaultConfig() {
|
|
|
202
203
|
*/
|
|
203
204
|
function getInitSuccessMessage(existsBeforeInit, force) {
|
|
204
205
|
if (existsBeforeInit && force) {
|
|
205
|
-
return '
|
|
206
|
+
return '配置文件已覆盖:';
|
|
206
207
|
}
|
|
207
208
|
if (existsBeforeInit) {
|
|
208
|
-
return '
|
|
209
|
+
return '配置文件已补全:';
|
|
209
210
|
}
|
|
210
|
-
return '
|
|
211
|
+
return '配置文件已创建:';
|
|
211
212
|
}
|
|
212
213
|
/**
|
|
213
214
|
* 读取已存在的配置文件
|
|
@@ -221,11 +222,11 @@ function readExistingConfig(configPath) {
|
|
|
221
222
|
}
|
|
222
223
|
catch (error) {
|
|
223
224
|
if (error instanceof SyntaxError) {
|
|
224
|
-
|
|
225
|
-
|
|
225
|
+
utils_1.logger.error(`配置文件 JSON 格式错误: ${configPath}`);
|
|
226
|
+
utils_1.logger.error(`错误详情: ${error.message}`);
|
|
226
227
|
}
|
|
227
228
|
else {
|
|
228
|
-
|
|
229
|
+
utils_1.logger.error('读取配置文件失败', error);
|
|
229
230
|
}
|
|
230
231
|
process.exit(1);
|
|
231
232
|
throw error;
|
package/dist/config/validator.js
CHANGED
|
@@ -7,6 +7,7 @@ const CONFIG_KEYS = [
|
|
|
7
7
|
'output',
|
|
8
8
|
'generator',
|
|
9
9
|
'groupByTags',
|
|
10
|
+
'multiTagStrategy',
|
|
10
11
|
'overwrite',
|
|
11
12
|
'prefix',
|
|
12
13
|
'requestStyle',
|
|
@@ -46,6 +47,7 @@ function validateSwaggerConfig(config) {
|
|
|
46
47
|
validateRequiredString(config.output, 'output', errors);
|
|
47
48
|
validateEnum(config.generator, 'generator', ['typescript', 'javascript'], errors);
|
|
48
49
|
validateBoolean(config.groupByTags, 'groupByTags', errors);
|
|
50
|
+
validateOptionalEnum(config.multiTagStrategy, 'multiTagStrategy', ['first', 'all'], errors);
|
|
49
51
|
validateOptionalBoolean(config.overwrite, 'overwrite', errors);
|
|
50
52
|
validateOptionalString(config.prefix, 'prefix', errors);
|
|
51
53
|
validateOptionalString(config.importTemplate, 'importTemplate', errors);
|
package/dist/core/generator.d.ts
CHANGED
|
@@ -61,6 +61,12 @@ export declare class CodeGenerator {
|
|
|
61
61
|
* @returns 类型字符串
|
|
62
62
|
*/
|
|
63
63
|
private getTypeFromSchema;
|
|
64
|
+
/**
|
|
65
|
+
* 获取当前配置下可安全输出的类型
|
|
66
|
+
* @param type 类型字符串
|
|
67
|
+
* @returns 安全类型字符串
|
|
68
|
+
*/
|
|
69
|
+
private getSafeGeneratedType;
|
|
64
70
|
/**
|
|
65
71
|
* 收集API数组中实际使用的类型
|
|
66
72
|
* @param apis API接口数组
|
|
@@ -93,11 +99,23 @@ export declare class CodeGenerator {
|
|
|
93
99
|
* @returns 请求配置代码
|
|
94
100
|
*/
|
|
95
101
|
private generateRequestConfig;
|
|
102
|
+
/**
|
|
103
|
+
* 生成 params 对象字段访问表达式
|
|
104
|
+
* @param name 参数名
|
|
105
|
+
* @returns 字段访问表达式
|
|
106
|
+
*/
|
|
107
|
+
private getParamAccessExpression;
|
|
96
108
|
/**
|
|
97
109
|
* 生成入口文件
|
|
98
110
|
* @param groupedApis 按标签分组的API
|
|
99
111
|
*/
|
|
100
112
|
private generateIndexFile;
|
|
113
|
+
/**
|
|
114
|
+
* 写入生成文件,overwrite=false 时保留已存在文件
|
|
115
|
+
* @param filePath 文件路径
|
|
116
|
+
* @param content 文件内容
|
|
117
|
+
*/
|
|
118
|
+
private writeGeneratedFile;
|
|
101
119
|
/**
|
|
102
120
|
* 获取标签对应的文件名
|
|
103
121
|
* @param tag 标签名
|
package/dist/core/generator.js
CHANGED
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.CodeGenerator = void 0;
|
|
37
37
|
const path = __importStar(require("path"));
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
38
39
|
const child_process_1 = require("child_process");
|
|
39
40
|
const util_1 = require("util");
|
|
40
41
|
const utils_1 = require("../utils");
|
|
@@ -90,7 +91,7 @@ class CodeGenerator {
|
|
|
90
91
|
async generateTypesFile(types) {
|
|
91
92
|
const content = this.generateTypesContent(types);
|
|
92
93
|
const filePath = path.join(this.config.output, 'types.ts');
|
|
93
|
-
|
|
94
|
+
this.writeGeneratedFile(filePath, content);
|
|
94
95
|
}
|
|
95
96
|
/**
|
|
96
97
|
* 生成类型文件内容
|
|
@@ -136,7 +137,7 @@ class CodeGenerator {
|
|
|
136
137
|
const ext = this.config.generator === 'javascript' ? 'js' : 'ts';
|
|
137
138
|
const filePath = path.join(tagFolderPath, `index.${ext}`);
|
|
138
139
|
const content = this.generateApiFileContent(apis, types, tag);
|
|
139
|
-
|
|
140
|
+
this.writeGeneratedFile(filePath, content);
|
|
140
141
|
}
|
|
141
142
|
}
|
|
142
143
|
/**
|
|
@@ -148,7 +149,7 @@ class CodeGenerator {
|
|
|
148
149
|
const ext = this.config.generator === 'javascript' ? 'js' : 'ts';
|
|
149
150
|
const filePath = path.join(this.config.output, `api.${ext}`);
|
|
150
151
|
const content = this.generateApiFileContent(apis, types);
|
|
151
|
-
|
|
152
|
+
this.writeGeneratedFile(filePath, content);
|
|
152
153
|
}
|
|
153
154
|
/**
|
|
154
155
|
* 生成API文件内容
|
|
@@ -200,7 +201,7 @@ class CodeGenerator {
|
|
|
200
201
|
type: p.type,
|
|
201
202
|
description: p.description
|
|
202
203
|
}));
|
|
203
|
-
const comment = (0, utils_1.generateApiComment)({ summary: api.description, deprecated:
|
|
204
|
+
const comment = (0, utils_1.generateApiComment)({ summary: api.description, deprecated: api.deprecated }, swaggerParams);
|
|
204
205
|
parts.push(comment);
|
|
205
206
|
}
|
|
206
207
|
// 生成函数签名
|
|
@@ -213,7 +214,9 @@ class CodeGenerator {
|
|
|
213
214
|
}));
|
|
214
215
|
// 生成直接参数形式
|
|
215
216
|
const functionParams = this.generateDirectParameters(swaggerParameters, this.config.generator === 'javascript');
|
|
216
|
-
const responseType =
|
|
217
|
+
const responseType = this.config.options?.generateModels === false
|
|
218
|
+
? 'any'
|
|
219
|
+
: api.responseType || 'any';
|
|
217
220
|
const functionName = (0, utils_1.toCamelCase)(api.name);
|
|
218
221
|
parts.push(`export const ${functionName} = (${functionParams}) => {`);
|
|
219
222
|
// 生成请求配置
|
|
@@ -254,7 +257,8 @@ class CodeGenerator {
|
|
|
254
257
|
const paramType = allParams
|
|
255
258
|
.map((p) => {
|
|
256
259
|
const optional = p.required ? '' : '?';
|
|
257
|
-
|
|
260
|
+
const type = this.getSafeGeneratedType(p.type);
|
|
261
|
+
return `${(0, utils_1.formatTsPropertyName)(p.name)}${optional}: ${type}`;
|
|
258
262
|
})
|
|
259
263
|
.join(', ');
|
|
260
264
|
// 检查是否所有参数都是可选的
|
|
@@ -273,7 +277,7 @@ class CodeGenerator {
|
|
|
273
277
|
const bodyType = bodyParam.schema
|
|
274
278
|
? this.getTypeFromSchema(bodyParam.schema)
|
|
275
279
|
: bodyParam.type;
|
|
276
|
-
params.push(`data: ${bodyType}`);
|
|
280
|
+
params.push(`data: ${this.getSafeGeneratedType(bodyType)}`);
|
|
277
281
|
}
|
|
278
282
|
}
|
|
279
283
|
// 添加可选的config参数
|
|
@@ -286,8 +290,23 @@ class CodeGenerator {
|
|
|
286
290
|
* @returns 类型字符串
|
|
287
291
|
*/
|
|
288
292
|
getTypeFromSchema(schema) {
|
|
293
|
+
if (this.config.options?.generateModels === false) {
|
|
294
|
+
return 'any';
|
|
295
|
+
}
|
|
289
296
|
return (0, utils_1.swaggerTypeToTsType)(schema);
|
|
290
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* 获取当前配置下可安全输出的类型
|
|
300
|
+
* @param type 类型字符串
|
|
301
|
+
* @returns 安全类型字符串
|
|
302
|
+
*/
|
|
303
|
+
getSafeGeneratedType(type) {
|
|
304
|
+
if (!type)
|
|
305
|
+
return 'any';
|
|
306
|
+
if (this.config.options?.generateModels !== false)
|
|
307
|
+
return type;
|
|
308
|
+
return this.isPrimitiveType(type) ? type : 'any';
|
|
309
|
+
}
|
|
291
310
|
/**
|
|
292
311
|
* 收集API数组中实际使用的类型
|
|
293
312
|
* @param apis API接口数组
|
|
@@ -409,8 +428,14 @@ class CodeGenerator {
|
|
|
409
428
|
}
|
|
410
429
|
// 查询参数和路径参数都从params对象中获取
|
|
411
430
|
if (queryParams.length > 0) {
|
|
412
|
-
|
|
413
|
-
|
|
431
|
+
if (pathParams.length > 0) {
|
|
432
|
+
const queryEntries = queryParams.map((param) => `${JSON.stringify(param.name)}: ${this.getParamAccessExpression(param.name)}`);
|
|
433
|
+
config.push(`params: { ${queryEntries.join(', ')} }`);
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
// 直接传递params对象,让axios自动过滤undefined值
|
|
437
|
+
config.push('params');
|
|
438
|
+
}
|
|
414
439
|
}
|
|
415
440
|
// 请求体数据
|
|
416
441
|
const bodyParams = api.parameters.filter((p) => p.in === 'body');
|
|
@@ -425,6 +450,16 @@ class CodeGenerator {
|
|
|
425
450
|
config.push('...config');
|
|
426
451
|
return `{\n ${config.join(',\n ')}\n }`;
|
|
427
452
|
}
|
|
453
|
+
/**
|
|
454
|
+
* 生成 params 对象字段访问表达式
|
|
455
|
+
* @param name 参数名
|
|
456
|
+
* @returns 字段访问表达式
|
|
457
|
+
*/
|
|
458
|
+
getParamAccessExpression(name) {
|
|
459
|
+
return (0, utils_1.isValidIdentifier)(name)
|
|
460
|
+
? `params.${name}`
|
|
461
|
+
: `params[${JSON.stringify(name)}]`;
|
|
462
|
+
}
|
|
428
463
|
/**
|
|
429
464
|
* 生成入口文件
|
|
430
465
|
* @param groupedApis 按标签分组的API
|
|
@@ -452,6 +487,17 @@ class CodeGenerator {
|
|
|
452
487
|
const content = [this.generateHeader(), '', ...exports, ''].join('\n');
|
|
453
488
|
const ext = this.config.generator === 'javascript' ? 'js' : 'ts';
|
|
454
489
|
const filePath = path.join(this.config.output, `index.${ext}`);
|
|
490
|
+
this.writeGeneratedFile(filePath, content);
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* 写入生成文件,overwrite=false 时保留已存在文件
|
|
494
|
+
* @param filePath 文件路径
|
|
495
|
+
* @param content 文件内容
|
|
496
|
+
*/
|
|
497
|
+
writeGeneratedFile(filePath, content) {
|
|
498
|
+
if (this.config.overwrite === false && fs.existsSync(filePath)) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
455
501
|
(0, utils_1.writeFile)(filePath, content);
|
|
456
502
|
}
|
|
457
503
|
/**
|
|
@@ -492,15 +538,15 @@ class CodeGenerator {
|
|
|
492
538
|
if (!this.config.lint)
|
|
493
539
|
return;
|
|
494
540
|
try {
|
|
495
|
-
|
|
541
|
+
utils_1.logger.info(`执行 Lint 命令: ${this.config.lint} ${this.config.output}`);
|
|
496
542
|
const result = await execPromise(`${this.config.lint} ${this.config.output}`);
|
|
497
543
|
if (result.stdout) {
|
|
498
|
-
|
|
544
|
+
utils_1.logger.info(result.stdout.trim());
|
|
499
545
|
}
|
|
500
|
-
|
|
546
|
+
utils_1.logger.success('Lint 执行完成');
|
|
501
547
|
}
|
|
502
548
|
catch (error) {
|
|
503
|
-
|
|
549
|
+
utils_1.logger.warn(`Lint 命令执行失败: ${error.message}`);
|
|
504
550
|
}
|
|
505
551
|
}
|
|
506
552
|
}
|
package/dist/core/parser.d.ts
CHANGED
|
@@ -51,6 +51,12 @@ export declare class SwaggerParser {
|
|
|
51
51
|
* @returns 引用解析后的对象
|
|
52
52
|
*/
|
|
53
53
|
private resolveReference;
|
|
54
|
+
/**
|
|
55
|
+
* 合并同名同位置参数,后出现的 operation 级参数覆盖 path 级参数
|
|
56
|
+
* @param parameters 参数数组
|
|
57
|
+
* @returns 合并后的参数数组
|
|
58
|
+
*/
|
|
59
|
+
private mergeParameters;
|
|
54
60
|
/**
|
|
55
61
|
* 按标签分组API
|
|
56
62
|
* @param apis API接口数组
|
package/dist/core/parser.js
CHANGED
|
@@ -11,6 +11,7 @@ const HTTP_METHODS = [
|
|
|
11
11
|
'head',
|
|
12
12
|
'options'
|
|
13
13
|
];
|
|
14
|
+
const MAX_REF_RESOLVE_DEPTH = 20;
|
|
14
15
|
/**
|
|
15
16
|
* Swagger文档解析器
|
|
16
17
|
*/
|
|
@@ -54,10 +55,7 @@ class SwaggerParser {
|
|
|
54
55
|
*/
|
|
55
56
|
parseOperation(method, path, operation, globalParameters) {
|
|
56
57
|
// 合并全局参数和操作参数
|
|
57
|
-
const allParameters = [
|
|
58
|
-
...(globalParameters || []),
|
|
59
|
-
...(operation.parameters || [])
|
|
60
|
-
].map((parameter) => this.resolveReference(parameter));
|
|
58
|
+
const allParameters = this.mergeParameters([...(globalParameters || []), ...(operation.parameters || [])].map((parameter) => this.resolveReference(parameter)));
|
|
61
59
|
// 处理 OpenAPI 3.0 requestBody
|
|
62
60
|
if (operation.requestBody) {
|
|
63
61
|
const requestBody = this.resolveReference(operation.requestBody);
|
|
@@ -98,7 +96,7 @@ class SwaggerParser {
|
|
|
98
96
|
// 获取请求体类型
|
|
99
97
|
const bodyParam = allParameters.find((p) => p.in === 'body');
|
|
100
98
|
const requestBodyType = bodyParam?.schema
|
|
101
|
-
? (0, utils_1.swaggerTypeToTsType)(bodyParam.schema)
|
|
99
|
+
? (0, utils_1.swaggerTypeToTsType)(bodyParam.schema, this.getAllSchemas())
|
|
102
100
|
: undefined;
|
|
103
101
|
// 解析参数信息
|
|
104
102
|
const parameters = allParameters.map((param) => {
|
|
@@ -118,6 +116,7 @@ class SwaggerParser {
|
|
|
118
116
|
path,
|
|
119
117
|
description: operation.summary || operation.description,
|
|
120
118
|
tags: operation.tags || [],
|
|
119
|
+
deprecated: operation.deprecated,
|
|
121
120
|
parameters,
|
|
122
121
|
responseType,
|
|
123
122
|
requestBodyType
|
|
@@ -130,12 +129,12 @@ class SwaggerParser {
|
|
|
130
129
|
parseTypes() {
|
|
131
130
|
const types = [];
|
|
132
131
|
// Debug log
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
utils_1.logger.debug('解析类型定义...');
|
|
133
|
+
utils_1.logger.debugKV('components', !!this.document.components);
|
|
135
134
|
if (this.document.components) {
|
|
136
|
-
|
|
135
|
+
utils_1.logger.debugKV('schemas', !!this.document.components.schemas);
|
|
137
136
|
if (this.document.components.schemas) {
|
|
138
|
-
|
|
137
|
+
utils_1.logger.debugKV('schema 列表', Object.keys(this.document.components.schemas).join(', '));
|
|
139
138
|
}
|
|
140
139
|
}
|
|
141
140
|
// 解析 OpenAPI 3.x components.schemas
|
|
@@ -170,7 +169,7 @@ class SwaggerParser {
|
|
|
170
169
|
const comment = value.description
|
|
171
170
|
? ` /** ${value.description} */`
|
|
172
171
|
: '';
|
|
173
|
-
return `${comment}\n ${key}${optional}: ${type};`;
|
|
172
|
+
return `${comment}\n ${(0, utils_1.formatTsPropertyName)(key)}${optional}: ${type};`;
|
|
174
173
|
})
|
|
175
174
|
.join('\n');
|
|
176
175
|
definition = `export interface ${typeName} {\n${properties}\n}`;
|
|
@@ -186,7 +185,7 @@ class SwaggerParser {
|
|
|
186
185
|
const enumValues = schema.enum
|
|
187
186
|
.map((value, index) => {
|
|
188
187
|
const key = this.getEnumMemberName(schema, value, index);
|
|
189
|
-
return ` ${key} =
|
|
188
|
+
return ` ${key} = ${JSON.stringify(String(value))}`;
|
|
190
189
|
})
|
|
191
190
|
.join(',\n');
|
|
192
191
|
definition = `export enum ${typeName} {\n${enumValues}\n}`;
|
|
@@ -225,19 +224,45 @@ class SwaggerParser {
|
|
|
225
224
|
* @param value 可能带有 $ref 的对象
|
|
226
225
|
* @returns 引用解析后的对象
|
|
227
226
|
*/
|
|
228
|
-
resolveReference(value) {
|
|
227
|
+
resolveReference(value, seenRefs = new Set(), depth = 0) {
|
|
229
228
|
if (!value || !value.$ref) {
|
|
230
229
|
return value;
|
|
231
230
|
}
|
|
232
|
-
const
|
|
231
|
+
const ref = value.$ref;
|
|
232
|
+
if (!ref.startsWith('#/')) {
|
|
233
|
+
throw new Error(`暂不支持外部 $ref 引用: ${ref}`);
|
|
234
|
+
}
|
|
235
|
+
if (depth >= MAX_REF_RESOLVE_DEPTH) {
|
|
236
|
+
throw new Error(`$ref 解析超过最大深度: ${ref}`);
|
|
237
|
+
}
|
|
238
|
+
if (seenRefs.has(ref)) {
|
|
239
|
+
throw new Error(`检测到循环 $ref 引用: ${ref}`);
|
|
240
|
+
}
|
|
241
|
+
seenRefs.add(ref);
|
|
242
|
+
const refPath = ref
|
|
243
|
+
.replace(/^#\//, '')
|
|
244
|
+
.split('/')
|
|
245
|
+
.map((segment) => decodeURIComponent(segment).replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
233
246
|
let current = this.document;
|
|
234
247
|
for (const segment of refPath) {
|
|
235
248
|
current = current?.[segment];
|
|
236
|
-
if (
|
|
237
|
-
|
|
249
|
+
if (current === undefined) {
|
|
250
|
+
throw new Error(`无法解析 $ref 引用: ${ref}`);
|
|
238
251
|
}
|
|
239
252
|
}
|
|
240
|
-
return current;
|
|
253
|
+
return this.resolveReference(current, seenRefs, depth + 1);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* 合并同名同位置参数,后出现的 operation 级参数覆盖 path 级参数
|
|
257
|
+
* @param parameters 参数数组
|
|
258
|
+
* @returns 合并后的参数数组
|
|
259
|
+
*/
|
|
260
|
+
mergeParameters(parameters) {
|
|
261
|
+
const merged = new Map();
|
|
262
|
+
for (const parameter of parameters) {
|
|
263
|
+
merged.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
264
|
+
}
|
|
265
|
+
return Array.from(merged.values());
|
|
241
266
|
}
|
|
242
267
|
/**
|
|
243
268
|
* 按标签分组API
|
|
@@ -248,7 +273,9 @@ class SwaggerParser {
|
|
|
248
273
|
const groupedApis = new Map();
|
|
249
274
|
for (const api of apis) {
|
|
250
275
|
const tags = api.tags.length > 0 ? api.tags : ['default'];
|
|
251
|
-
|
|
276
|
+
const strategy = this.config.multiTagStrategy ?? 'first';
|
|
277
|
+
const groupTags = strategy === 'all' ? [tags.join('-')] : [tags[0]];
|
|
278
|
+
for (const tag of groupTags) {
|
|
252
279
|
if (!groupedApis.has(tag)) {
|
|
253
280
|
groupedApis.set(tag, []);
|
|
254
281
|
}
|
package/dist/index.js
CHANGED
|
@@ -62,32 +62,36 @@ class Swagger2API {
|
|
|
62
62
|
*/
|
|
63
63
|
async generate() {
|
|
64
64
|
try {
|
|
65
|
-
|
|
65
|
+
utils_1.logger.banner('🚀 swagger2api-v3', '开始生成 API 接口文件');
|
|
66
|
+
utils_1.logger.setTotalSteps(4);
|
|
66
67
|
// 1. 加载Swagger文档
|
|
67
|
-
|
|
68
|
+
utils_1.logger.stepTitle('📖', '加载 Swagger 文档');
|
|
68
69
|
const document = await (0, utils_1.loadSwaggerDocument)(this.config.input);
|
|
69
|
-
|
|
70
|
+
utils_1.logger.success(`文档加载成功: ${document.info.title} v${document.info.version}`);
|
|
70
71
|
// 2. 解析文档
|
|
71
|
-
|
|
72
|
+
utils_1.logger.stepTitle('🔍', '解析 API 接口');
|
|
72
73
|
const parser = new parser_1.SwaggerParser(document, this.config);
|
|
73
74
|
const apis = this.filterApis(parser.parseApis());
|
|
74
75
|
const types = parser.parseTypes();
|
|
75
76
|
const groupedApis = parser.groupApisByTags(apis);
|
|
76
|
-
|
|
77
|
+
utils_1.logger.success(`解析完成: ${apis.length} 个接口, ${types.length} 个类型`);
|
|
77
78
|
if (this.config.groupByTags) {
|
|
78
|
-
|
|
79
|
+
utils_1.logger.info(`按标签分组: ${groupedApis.size} 个分组`);
|
|
79
80
|
for (const [tag, tagApis] of groupedApis) {
|
|
80
|
-
|
|
81
|
+
utils_1.logger.listItem(tag, `${tagApis.length} 个接口`);
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
// 3. 生成代码
|
|
84
|
-
|
|
85
|
+
utils_1.logger.stepTitle('⚡', '生成代码文件');
|
|
85
86
|
const generator = new generator_1.CodeGenerator(this.config);
|
|
86
87
|
await generator.generateAll(apis, types, groupedApis);
|
|
87
|
-
|
|
88
|
+
// 4. 完成
|
|
89
|
+
utils_1.logger.stepTitle('📦', '生成完成');
|
|
90
|
+
utils_1.logger.path('输出目录', this.config.output);
|
|
91
|
+
utils_1.logger.done('API 接口文件生成完成');
|
|
88
92
|
}
|
|
89
93
|
catch (error) {
|
|
90
|
-
|
|
94
|
+
utils_1.logger.error('生成失败', error);
|
|
91
95
|
throw error;
|
|
92
96
|
}
|
|
93
97
|
}
|
|
@@ -97,8 +101,7 @@ class Swagger2API {
|
|
|
97
101
|
validateConfig() {
|
|
98
102
|
const errors = (0, validator_1.validateSwaggerConfig)(this.config);
|
|
99
103
|
if (errors.length > 0) {
|
|
100
|
-
|
|
101
|
-
errors.forEach((error) => console.error(` - ${error}`));
|
|
104
|
+
utils_1.logger.errorList('配置验证失败:', errors);
|
|
102
105
|
return false;
|
|
103
106
|
}
|
|
104
107
|
return true;
|
|
@@ -134,9 +137,9 @@ async function generateFromConfig(configPath) {
|
|
|
134
137
|
let config;
|
|
135
138
|
// 读取并解析 JSON 配置文件
|
|
136
139
|
if (!fs.existsSync(fullPath)) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
+
utils_1.logger.error(`找不到配置文件: ${fullPath}`);
|
|
141
|
+
utils_1.logger.error('请确保配置文件存在并且路径正确');
|
|
142
|
+
utils_1.logger.error('提示: 运行 swagger2api-v3 init 来创建配置文件');
|
|
140
143
|
process.exit(1);
|
|
141
144
|
}
|
|
142
145
|
const configContent = fs.readFileSync(fullPath, 'utf-8');
|
|
@@ -149,12 +152,12 @@ async function generateFromConfig(configPath) {
|
|
|
149
152
|
}
|
|
150
153
|
catch (error) {
|
|
151
154
|
if (error instanceof SyntaxError) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
+
utils_1.logger.error(`配置文件 JSON 格式错误: ${fullPath}`);
|
|
156
|
+
utils_1.logger.error('请检查 JSON 语法是否正确');
|
|
157
|
+
utils_1.logger.error(`错误详情: ${error.message}`);
|
|
155
158
|
}
|
|
156
159
|
else {
|
|
157
|
-
|
|
160
|
+
utils_1.logger.error('加载配置文件失败', error);
|
|
158
161
|
}
|
|
159
162
|
process.exit(1);
|
|
160
163
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface SwaggerConfig {
|
|
|
15
15
|
generator: 'typescript' | 'javascript';
|
|
16
16
|
/** 按 tags 分组生成文件 */
|
|
17
17
|
groupByTags: boolean;
|
|
18
|
+
/** 多 tag 接口分组策略,first 只使用第一个 tag,all 将所有 tags 合成一个分组 */
|
|
19
|
+
multiTagStrategy?: 'first' | 'all';
|
|
18
20
|
/** 是否覆盖更新,默认为true。为true时会先删除输出目录下的所有文件 */
|
|
19
21
|
overwrite?: boolean;
|
|
20
22
|
/** 接口路径公共前缀,默认为空字符串 */
|
|
@@ -373,6 +375,8 @@ export interface ApiInfo {
|
|
|
373
375
|
description?: string;
|
|
374
376
|
/** 标签 */
|
|
375
377
|
tags: string[];
|
|
378
|
+
/** 是否废弃 */
|
|
379
|
+
deprecated?: boolean;
|
|
376
380
|
/** 参数 */
|
|
377
381
|
parameters: ParameterInfo[];
|
|
378
382
|
/** 响应类型 */
|
package/dist/utils/file.js
CHANGED
|
@@ -43,6 +43,7 @@ exports.writeFile = writeFile;
|
|
|
43
43
|
const fs = __importStar(require("fs"));
|
|
44
44
|
const path = __importStar(require("path"));
|
|
45
45
|
const axios_1 = __importDefault(require("axios"));
|
|
46
|
+
const logger_1 = require("./logger");
|
|
46
47
|
/**
|
|
47
48
|
* 确保目录存在,如果不存在则创建
|
|
48
49
|
* @param dirPath 目录路径
|
|
@@ -70,12 +71,12 @@ async function loadSwaggerDocument(input) {
|
|
|
70
71
|
try {
|
|
71
72
|
if (input.startsWith('http://') || input.startsWith('https://')) {
|
|
72
73
|
const { data } = await axios_1.default.get(input);
|
|
73
|
-
|
|
74
|
+
logger_1.logger.debug(`从 URL 加载: ${input}`);
|
|
74
75
|
if (data.components?.schemas) {
|
|
75
|
-
|
|
76
|
+
logger_1.logger.debugKV('schemas 数量', Object.keys(data.components.schemas).length);
|
|
76
77
|
}
|
|
77
78
|
else {
|
|
78
|
-
|
|
79
|
+
logger_1.logger.debug('加载的数据中未发现 schemas');
|
|
79
80
|
}
|
|
80
81
|
return data;
|
|
81
82
|
}
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 轻量级终端日志工具(零依赖,基于 ANSI 转义码)
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* 日志工具类
|
|
6
|
+
*
|
|
7
|
+
* 提供统一的、带颜色的终端输出格式。
|
|
8
|
+
* error 方法不使用颜色码,确保 console.error 的参数为纯文本,
|
|
9
|
+
* 兼容测试中对 console.error 的 spy 断言。
|
|
10
|
+
*/
|
|
11
|
+
declare class Logger {
|
|
12
|
+
private step;
|
|
13
|
+
private totalSteps;
|
|
14
|
+
/**
|
|
15
|
+
* 写入标准输出
|
|
16
|
+
* @param message 输出内容
|
|
17
|
+
*/
|
|
18
|
+
private log;
|
|
19
|
+
/**
|
|
20
|
+
* 写入错误输出
|
|
21
|
+
* @param message 输出内容
|
|
22
|
+
*/
|
|
23
|
+
private fail;
|
|
24
|
+
/**
|
|
25
|
+
* 格式化未知错误
|
|
26
|
+
* @param error 错误对象
|
|
27
|
+
* @returns 错误文案
|
|
28
|
+
*/
|
|
29
|
+
private formatError;
|
|
30
|
+
/**
|
|
31
|
+
* 输出带图标的状态行
|
|
32
|
+
* @param symbol 状态图标
|
|
33
|
+
* @param message 消息内容
|
|
34
|
+
*/
|
|
35
|
+
private status;
|
|
36
|
+
/**
|
|
37
|
+
* 设置总步骤数
|
|
38
|
+
* @param n 步骤总数
|
|
39
|
+
*/
|
|
40
|
+
setTotalSteps(n: number): void;
|
|
41
|
+
/**
|
|
42
|
+
* 输出步骤标题
|
|
43
|
+
* @param emoji 步骤图标
|
|
44
|
+
* @param title 步骤标题
|
|
45
|
+
*/
|
|
46
|
+
stepTitle(emoji: string, title: string): void;
|
|
47
|
+
/**
|
|
48
|
+
* 输出成功信息
|
|
49
|
+
* @param message 消息内容
|
|
50
|
+
*/
|
|
51
|
+
success(message: string): void;
|
|
52
|
+
/**
|
|
53
|
+
* 输出信息
|
|
54
|
+
* @param message 消息内容
|
|
55
|
+
*/
|
|
56
|
+
info(message: string): void;
|
|
57
|
+
/**
|
|
58
|
+
* 输出文件/路径信息
|
|
59
|
+
* @param label 标签
|
|
60
|
+
* @param path 路径
|
|
61
|
+
*/
|
|
62
|
+
path(label: string, path: string): void;
|
|
63
|
+
/**
|
|
64
|
+
* 输出警告信息
|
|
65
|
+
* @param message 消息内容
|
|
66
|
+
*/
|
|
67
|
+
warn(message: string): void;
|
|
68
|
+
/**
|
|
69
|
+
* 输出错误信息(不使用颜色码,兼容测试断言)
|
|
70
|
+
* @param message 消息内容
|
|
71
|
+
* @param error 错误对象
|
|
72
|
+
*/
|
|
73
|
+
error(message: string, error?: unknown): void;
|
|
74
|
+
/**
|
|
75
|
+
* 输出错误列表
|
|
76
|
+
* @param title 错误标题
|
|
77
|
+
* @param items 错误项列表
|
|
78
|
+
*/
|
|
79
|
+
errorList(title: string, items: string[]): void;
|
|
80
|
+
/**
|
|
81
|
+
* 输出调试信息(灰色暗淡)
|
|
82
|
+
* @param message 消息内容
|
|
83
|
+
*/
|
|
84
|
+
debug(message: string): void;
|
|
85
|
+
/**
|
|
86
|
+
* 输出调试键值对
|
|
87
|
+
* @param key 键名
|
|
88
|
+
* @param value 键值
|
|
89
|
+
*/
|
|
90
|
+
debugKV(key: string, value: string | number | boolean): void;
|
|
91
|
+
/**
|
|
92
|
+
* 输出列表项
|
|
93
|
+
* @param label 列表项标签
|
|
94
|
+
* @param detail 列表项详情
|
|
95
|
+
*/
|
|
96
|
+
listItem(label: string, detail: string): void;
|
|
97
|
+
/**
|
|
98
|
+
* 输出分隔线
|
|
99
|
+
*/
|
|
100
|
+
divider(): void;
|
|
101
|
+
/**
|
|
102
|
+
* 输出横幅标题
|
|
103
|
+
* @param title 标题
|
|
104
|
+
* @param subtitle 副标题(可选)
|
|
105
|
+
*/
|
|
106
|
+
banner(title: string, subtitle?: string): void;
|
|
107
|
+
/**
|
|
108
|
+
* 输出完成横幅
|
|
109
|
+
* @param message 完成消息
|
|
110
|
+
*/
|
|
111
|
+
done(message: string): void;
|
|
112
|
+
}
|
|
113
|
+
/** 日志单例 */
|
|
114
|
+
export declare const logger: Logger;
|
|
115
|
+
export {};
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 轻量级终端日志工具(零依赖,基于 ANSI 转义码)
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.logger = void 0;
|
|
7
|
+
/** 终端是否支持颜色输出 */
|
|
8
|
+
const supportsColor = process.stdout.isTTY &&
|
|
9
|
+
process.env.NODE_ENV !== 'test' &&
|
|
10
|
+
process.env.NO_COLOR === undefined;
|
|
11
|
+
/** 日志左侧缩进 */
|
|
12
|
+
const INDENT = ' ';
|
|
13
|
+
/** 列表标签默认宽度 */
|
|
14
|
+
const LIST_LABEL_WIDTH = 22;
|
|
15
|
+
/** 日志图标 */
|
|
16
|
+
const icon = {
|
|
17
|
+
success: '✅',
|
|
18
|
+
info: 'ℹ',
|
|
19
|
+
warn: '⚠',
|
|
20
|
+
error: '❌',
|
|
21
|
+
debug: '·',
|
|
22
|
+
path: '📂',
|
|
23
|
+
bullet: '•'
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* 包裹 ANSI 颜色码
|
|
27
|
+
* @param code ANSI 转义码
|
|
28
|
+
* @param text 文本内容
|
|
29
|
+
* @returns 带颜色的字符串(不支持颜色时返回原文)
|
|
30
|
+
*/
|
|
31
|
+
function wrap(code, text) {
|
|
32
|
+
return supportsColor ? `\x1b[${code}m${text}\x1b[0m` : text;
|
|
33
|
+
}
|
|
34
|
+
/** 颜色与样式快捷方法 */
|
|
35
|
+
const style = {
|
|
36
|
+
/**
|
|
37
|
+
* 加粗文本
|
|
38
|
+
* @param text 文本内容
|
|
39
|
+
* @returns 格式化后的文本
|
|
40
|
+
*/
|
|
41
|
+
bold(text) {
|
|
42
|
+
return wrap('1', text);
|
|
43
|
+
},
|
|
44
|
+
/**
|
|
45
|
+
* 弱化文本
|
|
46
|
+
* @param text 文本内容
|
|
47
|
+
* @returns 格式化后的文本
|
|
48
|
+
*/
|
|
49
|
+
dim(text) {
|
|
50
|
+
return wrap('2', text);
|
|
51
|
+
},
|
|
52
|
+
/**
|
|
53
|
+
* 绿色文本
|
|
54
|
+
* @param text 文本内容
|
|
55
|
+
* @returns 格式化后的文本
|
|
56
|
+
*/
|
|
57
|
+
green(text) {
|
|
58
|
+
return wrap('32', text);
|
|
59
|
+
},
|
|
60
|
+
/**
|
|
61
|
+
* 黄色文本
|
|
62
|
+
* @param text 文本内容
|
|
63
|
+
* @returns 格式化后的文本
|
|
64
|
+
*/
|
|
65
|
+
yellow(text) {
|
|
66
|
+
return wrap('33', text);
|
|
67
|
+
},
|
|
68
|
+
/**
|
|
69
|
+
* 紫色文本
|
|
70
|
+
* @param text 文本内容
|
|
71
|
+
* @returns 格式化后的文本
|
|
72
|
+
*/
|
|
73
|
+
magenta(text) {
|
|
74
|
+
return wrap('35', text);
|
|
75
|
+
},
|
|
76
|
+
/**
|
|
77
|
+
* 青色文本
|
|
78
|
+
* @param text 文本内容
|
|
79
|
+
* @returns 格式化后的文本
|
|
80
|
+
*/
|
|
81
|
+
cyan(text) {
|
|
82
|
+
return wrap('36', text);
|
|
83
|
+
},
|
|
84
|
+
/**
|
|
85
|
+
* 灰色文本
|
|
86
|
+
* @param text 文本内容
|
|
87
|
+
* @returns 格式化后的文本
|
|
88
|
+
*/
|
|
89
|
+
gray(text) {
|
|
90
|
+
return wrap('90', text);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* 日志工具类
|
|
95
|
+
*
|
|
96
|
+
* 提供统一的、带颜色的终端输出格式。
|
|
97
|
+
* error 方法不使用颜色码,确保 console.error 的参数为纯文本,
|
|
98
|
+
* 兼容测试中对 console.error 的 spy 断言。
|
|
99
|
+
*/
|
|
100
|
+
class Logger {
|
|
101
|
+
constructor() {
|
|
102
|
+
this.step = 0;
|
|
103
|
+
this.totalSteps = 0;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 写入标准输出
|
|
107
|
+
* @param message 输出内容
|
|
108
|
+
*/
|
|
109
|
+
log(message = '') {
|
|
110
|
+
console.log(message);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 写入错误输出
|
|
114
|
+
* @param message 输出内容
|
|
115
|
+
*/
|
|
116
|
+
fail(message) {
|
|
117
|
+
console.error(message);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 格式化未知错误
|
|
121
|
+
* @param error 错误对象
|
|
122
|
+
* @returns 错误文案
|
|
123
|
+
*/
|
|
124
|
+
formatError(error) {
|
|
125
|
+
if (error instanceof Error) {
|
|
126
|
+
return error.message ? `${error.name}: ${error.message}` : error.name;
|
|
127
|
+
}
|
|
128
|
+
return String(error);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 输出带图标的状态行
|
|
132
|
+
* @param symbol 状态图标
|
|
133
|
+
* @param message 消息内容
|
|
134
|
+
*/
|
|
135
|
+
status(symbol, message) {
|
|
136
|
+
this.log(`${INDENT}${symbol} ${message}`);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* 设置总步骤数
|
|
140
|
+
* @param n 步骤总数
|
|
141
|
+
*/
|
|
142
|
+
setTotalSteps(n) {
|
|
143
|
+
this.totalSteps = n;
|
|
144
|
+
this.step = 0;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* 输出步骤标题
|
|
148
|
+
* @param emoji 步骤图标
|
|
149
|
+
* @param title 步骤标题
|
|
150
|
+
*/
|
|
151
|
+
stepTitle(emoji, title) {
|
|
152
|
+
this.step++;
|
|
153
|
+
const progress = this.totalSteps > 0
|
|
154
|
+
? style.dim(`[${this.step}/${this.totalSteps}] `)
|
|
155
|
+
: '';
|
|
156
|
+
this.log();
|
|
157
|
+
this.log(`${INDENT}${progress}${emoji} ${style.bold(title)}`);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* 输出成功信息
|
|
161
|
+
* @param message 消息内容
|
|
162
|
+
*/
|
|
163
|
+
success(message) {
|
|
164
|
+
this.status(style.green(icon.success), message);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 输出信息
|
|
168
|
+
* @param message 消息内容
|
|
169
|
+
*/
|
|
170
|
+
info(message) {
|
|
171
|
+
this.status(style.cyan(icon.info), message);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* 输出文件/路径信息
|
|
175
|
+
* @param label 标签
|
|
176
|
+
* @param path 路径
|
|
177
|
+
*/
|
|
178
|
+
path(label, path) {
|
|
179
|
+
this.status(icon.path, `${style.dim(label)}: ${style.bold(path)}`);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* 输出警告信息
|
|
183
|
+
* @param message 消息内容
|
|
184
|
+
*/
|
|
185
|
+
warn(message) {
|
|
186
|
+
this.status(style.yellow(icon.warn), message);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* 输出错误信息(不使用颜色码,兼容测试断言)
|
|
190
|
+
* @param message 消息内容
|
|
191
|
+
* @param error 错误对象
|
|
192
|
+
*/
|
|
193
|
+
error(message, error) {
|
|
194
|
+
const detail = error === undefined ? message : `${message}: ${this.formatError(error)}`;
|
|
195
|
+
this.fail(`${icon.error} ${detail}`);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* 输出错误列表
|
|
199
|
+
* @param title 错误标题
|
|
200
|
+
* @param items 错误项列表
|
|
201
|
+
*/
|
|
202
|
+
errorList(title, items) {
|
|
203
|
+
this.fail(`${icon.error} ${title}`);
|
|
204
|
+
items.forEach((item) => this.fail(`${INDENT} - ${item}`));
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* 输出调试信息(灰色暗淡)
|
|
208
|
+
* @param message 消息内容
|
|
209
|
+
*/
|
|
210
|
+
debug(message) {
|
|
211
|
+
this.status(style.gray(icon.debug), style.gray(message));
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* 输出调试键值对
|
|
215
|
+
* @param key 键名
|
|
216
|
+
* @param value 键值
|
|
217
|
+
*/
|
|
218
|
+
debugKV(key, value) {
|
|
219
|
+
this.status(style.gray(icon.debug), `${style.gray(key)}: ${style.gray(String(value))}`);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* 输出列表项
|
|
223
|
+
* @param label 列表项标签
|
|
224
|
+
* @param detail 列表项详情
|
|
225
|
+
*/
|
|
226
|
+
listItem(label, detail) {
|
|
227
|
+
const paddedLabel = label.padEnd(LIST_LABEL_WIDTH, ' ');
|
|
228
|
+
this.log(`${INDENT} ${style.dim(icon.bullet)} ${style.cyan(paddedLabel)} ${style.dim(detail)}`);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* 输出分隔线
|
|
232
|
+
*/
|
|
233
|
+
divider() {
|
|
234
|
+
const line = '─'.repeat(48);
|
|
235
|
+
this.log(`${INDENT}${style.dim(line)}`);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* 输出横幅标题
|
|
239
|
+
* @param title 标题
|
|
240
|
+
* @param subtitle 副标题(可选)
|
|
241
|
+
*/
|
|
242
|
+
banner(title, subtitle) {
|
|
243
|
+
this.log();
|
|
244
|
+
this.log(`${INDENT}${style.bold(style.magenta(title))}`);
|
|
245
|
+
if (subtitle) {
|
|
246
|
+
this.log(`${INDENT}${style.dim(subtitle)}`);
|
|
247
|
+
}
|
|
248
|
+
this.log();
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* 输出完成横幅
|
|
252
|
+
* @param message 完成消息
|
|
253
|
+
*/
|
|
254
|
+
done(message) {
|
|
255
|
+
this.log();
|
|
256
|
+
this.log(`${INDENT}${style.green(icon.success)} ${style.bold(message)}`);
|
|
257
|
+
this.log();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/** 日志单例 */
|
|
261
|
+
exports.logger = new Logger();
|
package/dist/utils/type.d.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { SwaggerParameter, SwaggerSchema, SwaggerResponses } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* 判断字符串是否为合法 TypeScript 标识符
|
|
4
|
+
* @param name 属性名
|
|
5
|
+
* @returns 是否为合法标识符
|
|
6
|
+
*/
|
|
7
|
+
export declare function isValidIdentifier(name: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* 格式化 TypeScript 属性名
|
|
10
|
+
* @param name 属性名
|
|
11
|
+
* @returns 可用于类型声明的属性名
|
|
12
|
+
*/
|
|
13
|
+
export declare function formatTsPropertyName(name: string): string;
|
|
2
14
|
/**
|
|
3
15
|
* 移除联合类型中的顶层 null 类型
|
|
4
16
|
* @param typeStr 类型字符串
|
package/dist/utils/type.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isValidIdentifier = isValidIdentifier;
|
|
4
|
+
exports.formatTsPropertyName = formatTsPropertyName;
|
|
3
5
|
exports.stripNullFromUnion = stripNullFromUnion;
|
|
4
6
|
exports.swaggerTypeToTsType = swaggerTypeToTsType;
|
|
5
7
|
exports.getRefName = getRefName;
|
|
@@ -7,6 +9,22 @@ exports.swaggerParameterToSchema = swaggerParameterToSchema;
|
|
|
7
9
|
exports.getResponseType = getResponseType;
|
|
8
10
|
exports.getSchemaFromContent = getSchemaFromContent;
|
|
9
11
|
const naming_1 = require("./naming");
|
|
12
|
+
/**
|
|
13
|
+
* 判断字符串是否为合法 TypeScript 标识符
|
|
14
|
+
* @param name 属性名
|
|
15
|
+
* @returns 是否为合法标识符
|
|
16
|
+
*/
|
|
17
|
+
function isValidIdentifier(name) {
|
|
18
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 格式化 TypeScript 属性名
|
|
22
|
+
* @param name 属性名
|
|
23
|
+
* @returns 可用于类型声明的属性名
|
|
24
|
+
*/
|
|
25
|
+
function formatTsPropertyName(name) {
|
|
26
|
+
return isValidIdentifier(name) ? name : JSON.stringify(name);
|
|
27
|
+
}
|
|
10
28
|
/**
|
|
11
29
|
* 移除联合类型中的顶层 null 类型
|
|
12
30
|
* @param typeStr 类型字符串
|
|
@@ -78,7 +96,7 @@ function swaggerTypeToTsType(schema, schemas) {
|
|
|
78
96
|
else if (schema.allOf) {
|
|
79
97
|
const refSchema = schema.allOf.find((item) => item.$ref);
|
|
80
98
|
const secondSchema = schema.allOf.find((item) => !item.$ref);
|
|
81
|
-
if (refSchema && secondSchema) {
|
|
99
|
+
if (refSchema && secondSchema && schema.allOf.length === 2) {
|
|
82
100
|
const refName = getRefName(refSchema.$ref || '');
|
|
83
101
|
const sanitizedRefName = (0, naming_1.sanitizeTypeName)(refName || '');
|
|
84
102
|
if (secondSchema.properties) {
|
|
@@ -97,7 +115,7 @@ function swaggerTypeToTsType(schema, schemas) {
|
|
|
97
115
|
let type = swaggerTypeToTsType(value, schemas);
|
|
98
116
|
if (optional === '?')
|
|
99
117
|
type = stripNullFromUnion(type);
|
|
100
|
-
return `${key}${optional}: ${type}`;
|
|
118
|
+
return `${formatTsPropertyName(key)}${optional}: ${type}`;
|
|
101
119
|
})
|
|
102
120
|
.join('; ')} }`;
|
|
103
121
|
baseType = `${sanitizedRefName}<${combinedType}>`;
|
|
@@ -112,9 +130,10 @@ function swaggerTypeToTsType(schema, schemas) {
|
|
|
112
130
|
}
|
|
113
131
|
else {
|
|
114
132
|
const types = schema.allOf
|
|
115
|
-
.map((item) => swaggerTypeToTsType(item))
|
|
133
|
+
.map((item) => swaggerTypeToTsType(item, schemas))
|
|
116
134
|
.filter((type) => type !== 'any');
|
|
117
|
-
|
|
135
|
+
const uniqueTypes = Array.from(new Set(types));
|
|
136
|
+
baseType = uniqueTypes.length > 0 ? uniqueTypes.join(' & ') : 'any';
|
|
118
137
|
}
|
|
119
138
|
}
|
|
120
139
|
else if (schema.anyOf || schema.oneOf) {
|
|
@@ -162,7 +181,7 @@ function swaggerTypeToTsType(schema, schemas) {
|
|
|
162
181
|
let type = swaggerTypeToTsType(value, schemas);
|
|
163
182
|
if (optional === '?')
|
|
164
183
|
type = stripNullFromUnion(type);
|
|
165
|
-
return ` ${key}${optional}: ${type};`;
|
|
184
|
+
return ` ${formatTsPropertyName(key)}${optional}: ${type};`;
|
|
166
185
|
})
|
|
167
186
|
.join('\n');
|
|
168
187
|
baseType = `{\n${properties}\n}`;
|
|
@@ -228,7 +247,7 @@ function toLiteralType(value) {
|
|
|
228
247
|
if (value === null)
|
|
229
248
|
return 'null';
|
|
230
249
|
if (typeof value === 'string')
|
|
231
|
-
return
|
|
250
|
+
return JSON.stringify(value);
|
|
232
251
|
return String(value);
|
|
233
252
|
}
|
|
234
253
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "swagger2api-v3",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.10",
|
|
4
4
|
"description": "A command-line tool for generating TypeScript API interfaces from OpenAPI 3.x / Swagger 3.0 documentation",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -18,20 +18,21 @@
|
|
|
18
18
|
"test:watch": "jest --watch",
|
|
19
19
|
"test:coverage": "jest --coverage",
|
|
20
20
|
"test:ci": "jest --ci --coverage --watchAll=false",
|
|
21
|
-
"
|
|
21
|
+
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
|
|
22
|
+
"build": "tsc && pnpm run copy:schema",
|
|
22
23
|
"copy:schema": "node build/copy-schema.js",
|
|
23
24
|
"clean": "rimraf dist",
|
|
24
|
-
"prebuild": "
|
|
25
|
+
"prebuild": "pnpm run clean",
|
|
25
26
|
"start": "node dist/cli/index.js",
|
|
26
|
-
"dev": "
|
|
27
|
+
"dev": "pnpm run build && pnpm start",
|
|
27
28
|
"lint": "prettier --write .",
|
|
28
|
-
"generate": "
|
|
29
|
-
"init": "
|
|
30
|
-
"validate": "
|
|
31
|
-
"swagger:generate": "
|
|
32
|
-
"swagger:run": "
|
|
33
|
-
"swagger:init": "
|
|
34
|
-
"swagger:validate": "
|
|
29
|
+
"generate": "pnpm run build && node dist/cli/index.js generate",
|
|
30
|
+
"init": "pnpm run build && node dist/cli/index.js init",
|
|
31
|
+
"validate": "pnpm run build && node dist/cli/index.js validate",
|
|
32
|
+
"swagger:generate": "pnpm run generate",
|
|
33
|
+
"swagger:run": "pnpm run generate",
|
|
34
|
+
"swagger:init": "pnpm run init",
|
|
35
|
+
"swagger:validate": "pnpm run validate"
|
|
35
36
|
},
|
|
36
37
|
"keywords": [
|
|
37
38
|
"swagger",
|
|
@@ -55,15 +56,16 @@
|
|
|
55
56
|
"packageManager": "pnpm@10.11.0",
|
|
56
57
|
"devDependencies": {
|
|
57
58
|
"@types/jest": "^30.0.0",
|
|
58
|
-
"@types/node": "^24.
|
|
59
|
+
"@types/node": "^24.13.2",
|
|
60
|
+
"conventional-changelog-cli": "^5.0.0",
|
|
59
61
|
"jest": "^30.4.2",
|
|
60
|
-
"prettier": "^3.
|
|
61
|
-
"rimraf": "^6.
|
|
62
|
+
"prettier": "^3.8.4",
|
|
63
|
+
"rimraf": "^6.1.3",
|
|
62
64
|
"ts-jest": "^29.4.11",
|
|
63
|
-
"typescript": "^
|
|
65
|
+
"typescript": "^6.0.3"
|
|
64
66
|
},
|
|
65
67
|
"dependencies": {
|
|
66
|
-
"axios": "^1.
|
|
67
|
-
"commander": "^12.
|
|
68
|
+
"axios": "^1.18.1",
|
|
69
|
+
"commander": "^12.1.0"
|
|
68
70
|
}
|
|
69
71
|
}
|