swagger2api-v3 1.1.9 → 1.1.11
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 +5 -3
- package/dist/.swagger2api.schema.json +6 -2
- package/dist/cli/index.js +22 -21
- package/dist/config/validator.js +94 -9
- package/dist/core/generator.d.ts +83 -11
- package/dist/core/generator.js +295 -94
- package/dist/core/parser.d.ts +20 -16
- package/dist/core/parser.js +72 -103
- package/dist/core/query-parameter-generator.d.ts +26 -0
- package/dist/core/query-parameter-generator.js +114 -0
- package/dist/core/type-generator.d.ts +73 -0
- package/dist/core/type-generator.js +215 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +26 -21
- package/dist/types/index.d.ts +31 -6
- package/dist/utils/comment.d.ts +4 -1
- package/dist/utils/comment.js +6 -2
- package/dist/utils/file.d.ts +10 -3
- package/dist/utils/file.js +49 -19
- 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/naming.js +3 -2
- package/dist/utils/schema.d.ts +79 -0
- package/dist/utils/schema.js +536 -0
- package/dist/utils/type.d.ts +7 -25
- package/dist/utils/type.js +29 -241
- package/package.json +21 -19
package/dist/core/generator.js
CHANGED
|
@@ -35,16 +35,23 @@ 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");
|
|
42
|
+
const query_parameter_generator_1 = require("./query-parameter-generator");
|
|
41
43
|
const execPromise = (0, util_1.promisify)(child_process_1.exec);
|
|
42
44
|
/**
|
|
43
45
|
* 代码生成器
|
|
44
46
|
*/
|
|
45
47
|
class CodeGenerator {
|
|
48
|
+
/**
|
|
49
|
+
* 创建代码生成器
|
|
50
|
+
* @param config 生成配置
|
|
51
|
+
*/
|
|
46
52
|
constructor(config) {
|
|
47
53
|
this.config = config;
|
|
54
|
+
this.outputRoot = path.resolve(config.output);
|
|
48
55
|
}
|
|
49
56
|
/**
|
|
50
57
|
* 生成所有文件
|
|
@@ -53,17 +60,27 @@ class CodeGenerator {
|
|
|
53
60
|
* @param groupedApis 按标签分组的API
|
|
54
61
|
*/
|
|
55
62
|
async generateAll(apis, types, groupedApis) {
|
|
63
|
+
this.assertSafeOutputDirectory();
|
|
64
|
+
if (this.config.options?.generateApis !== false &&
|
|
65
|
+
this.config.groupByTags) {
|
|
66
|
+
this.validateTagFolders(groupedApis);
|
|
67
|
+
}
|
|
56
68
|
// 根据overwrite配置决定是否清空目录
|
|
57
69
|
if (this.config.overwrite !== false) {
|
|
58
70
|
// 默认为true,清空输出目录
|
|
59
|
-
(0, utils_1.removeDirectory)(this.
|
|
71
|
+
(0, utils_1.removeDirectory)(this.outputRoot);
|
|
60
72
|
}
|
|
61
73
|
// 确保输出目录存在
|
|
62
|
-
(0, utils_1.ensureDirectoryExists)(this.
|
|
74
|
+
(0, utils_1.ensureDirectoryExists)(this.outputRoot);
|
|
63
75
|
// 生成类型文件(仅在 TypeScript 模式下生成)
|
|
64
76
|
if (this.config.generator === 'typescript' &&
|
|
65
77
|
this.config.options?.generateModels !== false) {
|
|
66
|
-
|
|
78
|
+
const rootTypes = this.config.groupByTags
|
|
79
|
+
? types
|
|
80
|
+
: this.config.options?.generateApis === false
|
|
81
|
+
? types
|
|
82
|
+
: this.mergeTypeDefinitions(types, (0, query_parameter_generator_1.generateQueryParameterTypes)(apis, types.map((type) => type.name)));
|
|
83
|
+
await this.generateTypesFile(rootTypes);
|
|
67
84
|
}
|
|
68
85
|
// 生成API文件
|
|
69
86
|
if (this.config.options?.generateApis !== false) {
|
|
@@ -89,8 +106,8 @@ class CodeGenerator {
|
|
|
89
106
|
*/
|
|
90
107
|
async generateTypesFile(types) {
|
|
91
108
|
const content = this.generateTypesContent(types);
|
|
92
|
-
const filePath =
|
|
93
|
-
|
|
109
|
+
const filePath = this.resolveOutputPath('types.ts');
|
|
110
|
+
this.writeGeneratedFile(filePath, content);
|
|
94
111
|
}
|
|
95
112
|
/**
|
|
96
113
|
* 生成类型文件内容
|
|
@@ -104,23 +121,66 @@ class CodeGenerator {
|
|
|
104
121
|
' * 此文件由 swagger2api-v3 自动生成,请勿手动修改',
|
|
105
122
|
' */'
|
|
106
123
|
]);
|
|
107
|
-
const typeDefinitions = types
|
|
124
|
+
const typeDefinitions = this.renderTypeDefinitions(types);
|
|
125
|
+
return `${header}\n\n${typeDefinitions}\n`;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 渲染类型定义列表
|
|
129
|
+
* @param types 类型定义数组
|
|
130
|
+
* @returns 类型定义内容
|
|
131
|
+
*/
|
|
132
|
+
renderTypeDefinitions(types) {
|
|
133
|
+
return types
|
|
108
134
|
.map((type) => {
|
|
109
135
|
const comment = type.description
|
|
110
136
|
? `/**\n * ${type.description}\n */\n`
|
|
111
137
|
: '';
|
|
112
|
-
|
|
113
|
-
let definition = type.definition;
|
|
114
|
-
if (this.isGenericResponseContainer(type, definition)) {
|
|
115
|
-
const typeName = type.name;
|
|
116
|
-
definition = definition
|
|
117
|
-
.replace(`export interface ${typeName} {`, `export interface ${typeName}<T = Record<string, any>> {`)
|
|
118
|
-
.replace('data: Record<string, any>;', 'data: T;');
|
|
119
|
-
}
|
|
120
|
-
return `${comment}${definition}`;
|
|
138
|
+
return `${comment}${type.definition}`;
|
|
121
139
|
})
|
|
122
140
|
.join('\n\n');
|
|
123
|
-
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* 合并类型定义
|
|
144
|
+
* @param modelTypes 组件模型类型
|
|
145
|
+
* @param parameterTypes 查询参数类型
|
|
146
|
+
* @returns 合并后的类型定义
|
|
147
|
+
*/
|
|
148
|
+
mergeTypeDefinitions(modelTypes, parameterTypes) {
|
|
149
|
+
return [...modelTypes, ...parameterTypes];
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* 判断是否应提取查询参数类型
|
|
153
|
+
* @returns 是否提取查询参数类型
|
|
154
|
+
*/
|
|
155
|
+
shouldExtractQueryParameterTypes() {
|
|
156
|
+
return (this.config.generator === 'typescript' &&
|
|
157
|
+
this.config.options?.generateModels !== false);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* 生成 tag 内的查询参数类型文件
|
|
161
|
+
* @param folderName tag 目录名
|
|
162
|
+
* @param apis 当前 tag 的 API 接口
|
|
163
|
+
* @param modelTypes 组件模型类型
|
|
164
|
+
*/
|
|
165
|
+
generateTagParameterTypesFile(folderName, apis, modelTypes) {
|
|
166
|
+
const parameterTypes = (0, query_parameter_generator_1.generateQueryParameterTypes)(apis, modelTypes.map((type) => type.name));
|
|
167
|
+
if (parameterTypes.length === 0)
|
|
168
|
+
return;
|
|
169
|
+
const usedModelTypes = (0, query_parameter_generator_1.collectQueryParameterModelTypes)(apis, modelTypes);
|
|
170
|
+
const content = [
|
|
171
|
+
this.generateHeader([
|
|
172
|
+
'/**',
|
|
173
|
+
' * API 查询参数类型',
|
|
174
|
+
' * 此文件由 swagger2api-v3 自动生成,请勿手动修改',
|
|
175
|
+
' */'
|
|
176
|
+
])
|
|
177
|
+
];
|
|
178
|
+
if (usedModelTypes.length > 0) {
|
|
179
|
+
content.push(`import type { ${usedModelTypes.join(', ')} } from '../../types';`);
|
|
180
|
+
}
|
|
181
|
+
content.push('', this.renderTypeDefinitions(parameterTypes), '');
|
|
182
|
+
const filePath = this.resolveOutputPath(folderName, 'types', 'index.ts');
|
|
183
|
+
this.writeGeneratedFile(filePath, content.join('\n'));
|
|
124
184
|
}
|
|
125
185
|
/**
|
|
126
186
|
* 按标签生成API文件
|
|
@@ -130,13 +190,16 @@ class CodeGenerator {
|
|
|
130
190
|
async generateApiFilesByTags(groupedApis, types) {
|
|
131
191
|
for (const [tag, apis] of groupedApis) {
|
|
132
192
|
const folderName = this.getTagFileName(tag);
|
|
133
|
-
const tagFolderPath =
|
|
193
|
+
const tagFolderPath = this.resolveOutputPath(folderName);
|
|
134
194
|
// 确保tag文件夹存在
|
|
135
195
|
(0, utils_1.ensureDirectoryExists)(tagFolderPath);
|
|
196
|
+
if (this.shouldExtractQueryParameterTypes()) {
|
|
197
|
+
this.generateTagParameterTypesFile(folderName, apis, types);
|
|
198
|
+
}
|
|
136
199
|
const ext = this.config.generator === 'javascript' ? 'js' : 'ts';
|
|
137
200
|
const filePath = path.join(tagFolderPath, `index.${ext}`);
|
|
138
201
|
const content = this.generateApiFileContent(apis, types, tag);
|
|
139
|
-
|
|
202
|
+
this.writeGeneratedFile(filePath, content);
|
|
140
203
|
}
|
|
141
204
|
}
|
|
142
205
|
/**
|
|
@@ -146,9 +209,9 @@ class CodeGenerator {
|
|
|
146
209
|
*/
|
|
147
210
|
async generateSingleApiFile(apis, types) {
|
|
148
211
|
const ext = this.config.generator === 'javascript' ? 'js' : 'ts';
|
|
149
|
-
const filePath =
|
|
212
|
+
const filePath = this.resolveOutputPath(`api.${ext}`);
|
|
150
213
|
const content = this.generateApiFileContent(apis, types);
|
|
151
|
-
|
|
214
|
+
this.writeGeneratedFile(filePath, content);
|
|
152
215
|
}
|
|
153
216
|
/**
|
|
154
217
|
* 生成API文件内容
|
|
@@ -168,39 +231,56 @@ class CodeGenerator {
|
|
|
168
231
|
]),
|
|
169
232
|
importTemplate + ';'
|
|
170
233
|
];
|
|
234
|
+
const extractQueryParameterTypes = this.shouldExtractQueryParameterTypes();
|
|
235
|
+
const queryParameterTypes = extractQueryParameterTypes
|
|
236
|
+
? (0, query_parameter_generator_1.generateQueryParameterTypes)(apis, types.map((type) => type.name))
|
|
237
|
+
: [];
|
|
238
|
+
const queryParameterTypeNames = queryParameterTypes.map((type) => type.name);
|
|
171
239
|
// 收集当前文件实际使用的类型
|
|
172
|
-
const usedTypes = this.collectUsedTypes(apis, types);
|
|
240
|
+
const usedTypes = this.collectUsedTypes(apis, types, extractQueryParameterTypes);
|
|
241
|
+
const queryParameterTypeByApi = new Map(queryParameterTypes.map((type) => [type.api, type.name]));
|
|
173
242
|
// 添加类型导入(仅在 TypeScript 且生成类型文件时)
|
|
174
243
|
if (this.config.generator === 'typescript' &&
|
|
175
244
|
this.config.options?.generateModels !== false &&
|
|
176
|
-
usedTypes.length > 0) {
|
|
177
|
-
const typeNames =
|
|
245
|
+
(usedTypes.length > 0 || (!tag && queryParameterTypeNames.length > 0))) {
|
|
246
|
+
const typeNames = Array.from(new Set([...usedTypes, ...(tag ? [] : queryParameterTypeNames)]))
|
|
247
|
+
.sort()
|
|
248
|
+
.join(', ');
|
|
178
249
|
const typesPath = tag ? '../types' : './types';
|
|
179
250
|
header.push(`import type { ${typeNames} } from '${typesPath}';`);
|
|
180
251
|
}
|
|
252
|
+
if (tag && queryParameterTypeNames.length > 0) {
|
|
253
|
+
const typeNames = queryParameterTypeNames.sort().join(', ');
|
|
254
|
+
header.push(`import type { ${typeNames} } from './types';`);
|
|
255
|
+
}
|
|
181
256
|
header.push('');
|
|
182
257
|
const apiImplementations = apis
|
|
183
|
-
.map((api) => this.generateApiFunction(api
|
|
258
|
+
.map((api) => this.generateApiFunction(api, extractQueryParameterTypes
|
|
259
|
+
? queryParameterTypeByApi.get(api)
|
|
260
|
+
: undefined))
|
|
184
261
|
.join('\n\n');
|
|
185
262
|
return `${header.join('\n')}${apiImplementations}\n`;
|
|
186
263
|
}
|
|
187
264
|
/**
|
|
188
265
|
* 生成单个API函数
|
|
189
266
|
* @param api API接口信息
|
|
267
|
+
* @param queryParameterTypeName 查询参数类型名
|
|
190
268
|
* @returns API函数代码
|
|
191
269
|
*/
|
|
192
|
-
generateApiFunction(api) {
|
|
270
|
+
generateApiFunction(api, queryParameterTypeName) {
|
|
193
271
|
const parts = [];
|
|
194
272
|
// 生成注释
|
|
195
273
|
if (this.config.options?.addComments !== false) {
|
|
274
|
+
const pathParameterNames = this.createPathParameterNames(api.parameters.filter((parameter) => parameter.in === 'path'));
|
|
275
|
+
let pathParameterIndex = 0;
|
|
196
276
|
const swaggerParams = api.parameters.map((p) => ({
|
|
197
|
-
name: p.name,
|
|
277
|
+
name: p.in === 'path' ? pathParameterNames[pathParameterIndex++] : p.name,
|
|
198
278
|
in: p.in,
|
|
199
279
|
required: p.required,
|
|
200
280
|
type: p.type,
|
|
201
281
|
description: p.description
|
|
202
282
|
}));
|
|
203
|
-
const comment = (0, utils_1.generateApiComment)({ summary: api.description, deprecated:
|
|
283
|
+
const comment = (0, utils_1.generateApiComment)({ summary: api.description, deprecated: api.deprecated }, swaggerParams);
|
|
204
284
|
parts.push(comment);
|
|
205
285
|
}
|
|
206
286
|
// 生成函数签名
|
|
@@ -212,8 +292,10 @@ class CodeGenerator {
|
|
|
212
292
|
schema: p.schema || { type: p.type }
|
|
213
293
|
}));
|
|
214
294
|
// 生成直接参数形式
|
|
215
|
-
const functionParams = this.generateDirectParameters(swaggerParameters, this.config.generator === 'javascript');
|
|
216
|
-
const responseType =
|
|
295
|
+
const functionParams = this.generateDirectParameters(swaggerParameters, this.config.generator === 'javascript', queryParameterTypeName);
|
|
296
|
+
const responseType = this.config.options?.generateModels === false
|
|
297
|
+
? 'any'
|
|
298
|
+
: api.responseType || 'any';
|
|
217
299
|
const functionName = (0, utils_1.toCamelCase)(api.name);
|
|
218
300
|
parts.push(`export const ${functionName} = (${functionParams}) => {`);
|
|
219
301
|
// 生成请求配置
|
|
@@ -237,30 +319,41 @@ class CodeGenerator {
|
|
|
237
319
|
/**
|
|
238
320
|
* 生成直接参数形式
|
|
239
321
|
* @param parameters OpenAPI 参数数组
|
|
322
|
+
* @param isJavaScript 是否生成 JavaScript 参数
|
|
323
|
+
* @param queryParameterTypeName 查询参数类型名
|
|
240
324
|
* @returns 函数参数字符串
|
|
241
325
|
*/
|
|
242
|
-
generateDirectParameters(parameters, isJavaScript = false) {
|
|
326
|
+
generateDirectParameters(parameters, isJavaScript = false, queryParameterTypeName) {
|
|
243
327
|
const params = [];
|
|
244
328
|
const queryParams = parameters.filter((p) => p.in === 'query');
|
|
245
329
|
const pathParams = parameters.filter((p) => p.in === 'path');
|
|
246
330
|
const bodyParams = parameters.filter((p) => p.in === 'body');
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
|
|
331
|
+
const bodyParam = bodyParams[0];
|
|
332
|
+
const hasRequiredBody = bodyParam?.required === true;
|
|
333
|
+
const pathParameterNames = this.createPathParameterNames(pathParams);
|
|
334
|
+
pathParams.forEach((parameter, index) => {
|
|
335
|
+
const parameterName = pathParameterNames[index];
|
|
336
|
+
if (isJavaScript) {
|
|
337
|
+
params.push(parameterName);
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
params.push(`${parameterName}: ${this.getSafeGeneratedType(parameter.type)}`);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
if (queryParams.length > 0) {
|
|
250
344
|
if (isJavaScript) {
|
|
251
345
|
params.push('params');
|
|
252
346
|
}
|
|
253
347
|
else {
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
params.push(`params${optionalModifier}: { ${paramType} }`);
|
|
348
|
+
const allOptional = queryParams.every((p) => !p.required);
|
|
349
|
+
const optionalModifier = allOptional && !hasRequiredBody ? '?' : '';
|
|
350
|
+
if (queryParameterTypeName) {
|
|
351
|
+
params.push(`params${optionalModifier}: ${queryParameterTypeName}`);
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
const paramType = this.renderInlineQueryParameterMembers(queryParams);
|
|
355
|
+
params.push(`params${optionalModifier}: { ${paramType} }`);
|
|
356
|
+
}
|
|
264
357
|
}
|
|
265
358
|
}
|
|
266
359
|
// 请求体参数
|
|
@@ -269,11 +362,9 @@ class CodeGenerator {
|
|
|
269
362
|
params.push('data');
|
|
270
363
|
}
|
|
271
364
|
else {
|
|
272
|
-
const
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
: bodyParam.type;
|
|
276
|
-
params.push(`data: ${bodyType}`);
|
|
365
|
+
const bodyType = bodyParam.type;
|
|
366
|
+
const optionalModifier = bodyParam.required ? '' : '?';
|
|
367
|
+
params.push(`data${optionalModifier}: ${this.getSafeGeneratedType(bodyType)}`);
|
|
277
368
|
}
|
|
278
369
|
}
|
|
279
370
|
// 添加可选的config参数
|
|
@@ -281,20 +372,41 @@ class CodeGenerator {
|
|
|
281
372
|
return params.join(', ');
|
|
282
373
|
}
|
|
283
374
|
/**
|
|
284
|
-
*
|
|
285
|
-
* @param
|
|
286
|
-
* @returns
|
|
375
|
+
* 获取当前配置下可安全输出的类型
|
|
376
|
+
* @param type 类型字符串
|
|
377
|
+
* @returns 安全类型字符串
|
|
287
378
|
*/
|
|
288
|
-
|
|
289
|
-
|
|
379
|
+
getSafeGeneratedType(type) {
|
|
380
|
+
if (!type)
|
|
381
|
+
return 'any';
|
|
382
|
+
if (this.config.options?.generateModels !== false)
|
|
383
|
+
return type;
|
|
384
|
+
return this.isPrimitiveType(type) ? type : 'any';
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* 渲染内联查询参数类型成员
|
|
388
|
+
* @param parameters 查询参数数组
|
|
389
|
+
* @returns 内联类型成员
|
|
390
|
+
*/
|
|
391
|
+
renderInlineQueryParameterMembers(parameters) {
|
|
392
|
+
return parameters
|
|
393
|
+
.map((parameter) => {
|
|
394
|
+
const optional = parameter.required ? '' : '?';
|
|
395
|
+
let type = this.getSafeGeneratedType(parameter.type);
|
|
396
|
+
if (optional)
|
|
397
|
+
type = (0, utils_1.stripNullFromUnion)(type);
|
|
398
|
+
return `${(0, utils_1.formatTsPropertyName)(parameter.name)}${optional}: ${type}`;
|
|
399
|
+
})
|
|
400
|
+
.join(', ');
|
|
290
401
|
}
|
|
291
402
|
/**
|
|
292
403
|
* 收集API数组中实际使用的类型
|
|
293
404
|
* @param apis API接口数组
|
|
294
405
|
* @param definedTypes 已定义的类型数组
|
|
406
|
+
* @param excludeQueryParameters 是否排除查询参数
|
|
295
407
|
* @returns 使用的类型名称数组
|
|
296
408
|
*/
|
|
297
|
-
collectUsedTypes(apis, definedTypes) {
|
|
409
|
+
collectUsedTypes(apis, definedTypes, excludeQueryParameters = false) {
|
|
298
410
|
const usedTypes = new Set();
|
|
299
411
|
apis.forEach((api) => {
|
|
300
412
|
// 收集响应类型
|
|
@@ -306,17 +418,15 @@ class CodeGenerator {
|
|
|
306
418
|
}
|
|
307
419
|
// 收集参数类型
|
|
308
420
|
api.parameters.forEach((param) => {
|
|
309
|
-
if (param.
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
421
|
+
if (excludeQueryParameters && param.in === 'query')
|
|
422
|
+
return;
|
|
423
|
+
if (param.type && param.type !== 'any') {
|
|
424
|
+
if (!this.isPrimitiveType(param.type)) {
|
|
425
|
+
this.extractTypeNames(param.type).forEach((typeName) => {
|
|
313
426
|
usedTypes.add(typeName);
|
|
314
427
|
});
|
|
315
428
|
}
|
|
316
429
|
}
|
|
317
|
-
else if (param.type && !this.isPrimitiveType(param.type)) {
|
|
318
|
-
usedTypes.add(param.type);
|
|
319
|
-
}
|
|
320
430
|
});
|
|
321
431
|
});
|
|
322
432
|
// 如果提供了 definedTypes,则只保留已定义的类型
|
|
@@ -359,29 +469,13 @@ class CodeGenerator {
|
|
|
359
469
|
'object',
|
|
360
470
|
'array',
|
|
361
471
|
'any',
|
|
472
|
+
'blob',
|
|
362
473
|
'void',
|
|
363
474
|
'null',
|
|
364
475
|
'undefined'
|
|
365
476
|
];
|
|
366
477
|
return primitiveTypes.includes(type.toLowerCase());
|
|
367
478
|
}
|
|
368
|
-
/**
|
|
369
|
-
* 检测是否为通用响应容器类型
|
|
370
|
-
* @param type 类型信息
|
|
371
|
-
* @param definition 类型定义
|
|
372
|
-
* @returns 是否为通用响应容器类型
|
|
373
|
-
*/
|
|
374
|
-
isGenericResponseContainer(type, definition) {
|
|
375
|
-
// 检查是否为接口定义
|
|
376
|
-
if (!definition.includes(`export interface ${type.name} {`)) {
|
|
377
|
-
return false;
|
|
378
|
-
}
|
|
379
|
-
// 检查是否包含 data 字段且类型为 Record<string, any>
|
|
380
|
-
const hasDataField = definition.includes('data: Record<string, any>;');
|
|
381
|
-
// 检查是否包含其他常见的响应容器字段
|
|
382
|
-
const hasCommonFields = ['code', 'message', 'success', 'status'].some((field) => new RegExp(`\\b${field}\\??:`).test(definition));
|
|
383
|
-
return hasDataField && hasCommonFields;
|
|
384
|
-
}
|
|
385
479
|
/**
|
|
386
480
|
* 生成请求配置
|
|
387
481
|
* @param api API接口信息
|
|
@@ -397,19 +491,20 @@ class CodeGenerator {
|
|
|
397
491
|
}
|
|
398
492
|
const pathParams = api.parameters.filter((p) => p.in === 'path');
|
|
399
493
|
const queryParams = api.parameters.filter((p) => p.in === 'query');
|
|
494
|
+
const pathParameterNames = this.createPathParameterNames(pathParams);
|
|
400
495
|
if (pathParams.length > 0) {
|
|
401
|
-
|
|
402
|
-
pathParams.forEach((param) => {
|
|
403
|
-
|
|
496
|
+
url = this.escapeTemplateLiteral(url);
|
|
497
|
+
pathParams.forEach((param, index) => {
|
|
498
|
+
const placeholder = this.escapeTemplateLiteral(`{${param.name}}`);
|
|
499
|
+
const replacement = `\${${pathParameterNames[index]}}`;
|
|
500
|
+
url = url.split(placeholder).join(replacement);
|
|
404
501
|
});
|
|
405
502
|
config.push(`url: \`${url}\``);
|
|
406
503
|
}
|
|
407
504
|
else {
|
|
408
|
-
config.push(`url:
|
|
505
|
+
config.push(`url: ${this.toSingleQuotedString(url)}`);
|
|
409
506
|
}
|
|
410
|
-
// 查询参数和路径参数都从params对象中获取
|
|
411
507
|
if (queryParams.length > 0) {
|
|
412
|
-
// 直接传递params对象,让axios自动过滤undefined值
|
|
413
508
|
config.push('params');
|
|
414
509
|
}
|
|
415
510
|
// 请求体数据
|
|
@@ -425,6 +520,28 @@ class CodeGenerator {
|
|
|
425
520
|
config.push('...config');
|
|
426
521
|
return `{\n ${config.join(',\n ')}\n }`;
|
|
427
522
|
}
|
|
523
|
+
/**
|
|
524
|
+
* 创建不重复的路径参数变量名
|
|
525
|
+
* @param parameters OpenAPI 路径参数
|
|
526
|
+
* @returns 路径参数变量名
|
|
527
|
+
*/
|
|
528
|
+
createPathParameterNames(parameters) {
|
|
529
|
+
const usedNames = new Set(['params', 'data', 'config']);
|
|
530
|
+
return parameters.map((parameter, index) => {
|
|
531
|
+
const normalizedName = parameter.name.replace(/[^A-Za-z0-9_$]/g, '_');
|
|
532
|
+
const baseName = (0, utils_1.isValidIdentifier)(normalizedName)
|
|
533
|
+
? normalizedName
|
|
534
|
+
: `pathParam${index + 1}`;
|
|
535
|
+
let parameterName = baseName;
|
|
536
|
+
let suffix = 2;
|
|
537
|
+
while (usedNames.has(parameterName)) {
|
|
538
|
+
parameterName = `${baseName}_${suffix}`;
|
|
539
|
+
suffix++;
|
|
540
|
+
}
|
|
541
|
+
usedNames.add(parameterName);
|
|
542
|
+
return parameterName;
|
|
543
|
+
});
|
|
544
|
+
}
|
|
428
545
|
/**
|
|
429
546
|
* 生成入口文件
|
|
430
547
|
* @param groupedApis 按标签分组的API
|
|
@@ -441,7 +558,7 @@ class CodeGenerator {
|
|
|
441
558
|
// 按标签导出
|
|
442
559
|
for (const tag of groupedApis.keys()) {
|
|
443
560
|
const folderName = this.getTagFileName(tag);
|
|
444
|
-
exports.push(`export * from
|
|
561
|
+
exports.push(`export * from ${this.toSingleQuotedString(`./${folderName}`)};`);
|
|
445
562
|
}
|
|
446
563
|
}
|
|
447
564
|
else {
|
|
@@ -451,7 +568,40 @@ class CodeGenerator {
|
|
|
451
568
|
}
|
|
452
569
|
const content = [this.generateHeader(), '', ...exports, ''].join('\n');
|
|
453
570
|
const ext = this.config.generator === 'javascript' ? 'js' : 'ts';
|
|
454
|
-
const filePath =
|
|
571
|
+
const filePath = this.resolveOutputPath(`index.${ext}`);
|
|
572
|
+
this.writeGeneratedFile(filePath, content);
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* 校验输出目录不会覆盖当前工作目录或其父目录
|
|
576
|
+
*/
|
|
577
|
+
assertSafeOutputDirectory() {
|
|
578
|
+
const currentDirectory = path.resolve(process.cwd());
|
|
579
|
+
if (this.outputRoot === currentDirectory ||
|
|
580
|
+
(0, utils_1.isPathInside)(this.outputRoot, currentDirectory)) {
|
|
581
|
+
throw new Error('输出目录不能是当前工作目录或其父目录');
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* 解析并校验输出目录内的文件路径
|
|
586
|
+
* @param segments 输出目录下的路径片段
|
|
587
|
+
* @returns 安全的绝对路径
|
|
588
|
+
*/
|
|
589
|
+
resolveOutputPath(...segments) {
|
|
590
|
+
const targetPath = path.resolve(this.outputRoot, ...segments);
|
|
591
|
+
if (!(0, utils_1.isPathInside)(this.outputRoot, targetPath)) {
|
|
592
|
+
throw new Error(`生成路径超出输出目录: ${targetPath}`);
|
|
593
|
+
}
|
|
594
|
+
return targetPath;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* 写入生成文件,overwrite=false 时保留已存在文件
|
|
598
|
+
* @param filePath 文件路径
|
|
599
|
+
* @param content 文件内容
|
|
600
|
+
*/
|
|
601
|
+
writeGeneratedFile(filePath, content) {
|
|
602
|
+
if (this.config.overwrite === false && fs.existsSync(filePath)) {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
455
605
|
(0, utils_1.writeFile)(filePath, content);
|
|
456
606
|
}
|
|
457
607
|
/**
|
|
@@ -461,16 +611,67 @@ class CodeGenerator {
|
|
|
461
611
|
*/
|
|
462
612
|
getTagFileName(tag) {
|
|
463
613
|
const cleanTag = (0, utils_1.sanitizeFilename)(tag);
|
|
614
|
+
let folderName;
|
|
464
615
|
switch (this.config.tagGrouping?.fileNaming) {
|
|
465
616
|
case 'camelCase':
|
|
466
|
-
|
|
617
|
+
folderName = (0, utils_1.toCamelCase)(cleanTag);
|
|
618
|
+
break;
|
|
467
619
|
case 'kebab-case':
|
|
468
|
-
|
|
620
|
+
folderName = (0, utils_1.toKebabCase)(cleanTag);
|
|
621
|
+
break;
|
|
469
622
|
case 'tag':
|
|
470
|
-
|
|
623
|
+
folderName = cleanTag.toLowerCase();
|
|
624
|
+
break;
|
|
471
625
|
default:
|
|
472
|
-
|
|
626
|
+
folderName = (0, utils_1.toCamelCase)(cleanTag);
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
if (!folderName || folderName === '.' || folderName === '..') {
|
|
630
|
+
throw new Error(`无效的标签名称: ${JSON.stringify(tag)}`);
|
|
473
631
|
}
|
|
632
|
+
return folderName;
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* 校验所有标签生成的目录名称安全且不重复
|
|
636
|
+
* @param groupedApis 按标签分组的 API
|
|
637
|
+
*/
|
|
638
|
+
validateTagFolders(groupedApis) {
|
|
639
|
+
const folderOwners = new Map();
|
|
640
|
+
for (const tag of groupedApis.keys()) {
|
|
641
|
+
const folderName = this.getTagFileName(tag);
|
|
642
|
+
const normalizedFolderName = folderName.toLowerCase();
|
|
643
|
+
const existingTag = folderOwners.get(normalizedFolderName);
|
|
644
|
+
if (existingTag !== undefined && existingTag !== tag) {
|
|
645
|
+
throw new Error(`标签目录名称冲突: ${existingTag} 和 ${tag} 都会生成 ${folderName}`);
|
|
646
|
+
}
|
|
647
|
+
folderOwners.set(normalizedFolderName, tag);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* 转义模板字符串中的特殊字符
|
|
652
|
+
* @param value 原始字符串
|
|
653
|
+
* @returns 可安全写入模板字符串的内容
|
|
654
|
+
*/
|
|
655
|
+
escapeTemplateLiteral(value) {
|
|
656
|
+
return value
|
|
657
|
+
.replace(/\\/g, '\\\\')
|
|
658
|
+
.replace(/`/g, '\\`')
|
|
659
|
+
.replace(/\$\{/g, '\\${');
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* 将字符串转换为安全的单引号字面量
|
|
663
|
+
* @param value 原始字符串
|
|
664
|
+
* @returns 单引号字符串字面量
|
|
665
|
+
*/
|
|
666
|
+
toSingleQuotedString(value) {
|
|
667
|
+
const escapedValue = value
|
|
668
|
+
.replace(/\\/g, '\\\\')
|
|
669
|
+
.replace(/'/g, "\\'")
|
|
670
|
+
.replace(/\r/g, '\\r')
|
|
671
|
+
.replace(/\n/g, '\\n')
|
|
672
|
+
.replace(/\u2028/g, '\\u2028')
|
|
673
|
+
.replace(/\u2029/g, '\\u2029');
|
|
674
|
+
return `'${escapedValue}'`;
|
|
474
675
|
}
|
|
475
676
|
/**
|
|
476
677
|
* 生成文件头部注释
|
|
@@ -492,15 +693,15 @@ class CodeGenerator {
|
|
|
492
693
|
if (!this.config.lint)
|
|
493
694
|
return;
|
|
494
695
|
try {
|
|
495
|
-
|
|
696
|
+
utils_1.logger.info(`执行 Lint 命令: ${this.config.lint} ${this.config.output}`);
|
|
496
697
|
const result = await execPromise(`${this.config.lint} ${this.config.output}`);
|
|
497
698
|
if (result.stdout) {
|
|
498
|
-
|
|
699
|
+
utils_1.logger.info(result.stdout.trim());
|
|
499
700
|
}
|
|
500
|
-
|
|
701
|
+
utils_1.logger.success('Lint 执行完成');
|
|
501
702
|
}
|
|
502
703
|
catch (error) {
|
|
503
|
-
|
|
704
|
+
utils_1.logger.warn(`Lint 命令执行失败: ${error.message}`);
|
|
504
705
|
}
|
|
505
706
|
}
|
|
506
707
|
}
|
package/dist/core/parser.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { SwaggerDocument, ApiInfo, TypeInfo, SwaggerConfig } from '../types';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* OpenAPI 3.0 文档解析器
|
|
4
4
|
*/
|
|
5
5
|
export declare class SwaggerParser {
|
|
6
6
|
private document;
|
|
7
7
|
private config;
|
|
8
|
+
/**
|
|
9
|
+
* 创建 OpenAPI 文档解析器
|
|
10
|
+
* @param document OpenAPI 文档
|
|
11
|
+
* @param config 生成配置
|
|
12
|
+
*/
|
|
8
13
|
constructor(document: SwaggerDocument, config: SwaggerConfig);
|
|
9
14
|
/**
|
|
10
15
|
* 获取 OpenAPI components.schemas
|
|
@@ -30,27 +35,26 @@ export declare class SwaggerParser {
|
|
|
30
35
|
* @returns 类型信息数组
|
|
31
36
|
*/
|
|
32
37
|
parseTypes(): TypeInfo[];
|
|
33
|
-
/**
|
|
34
|
-
* 解析单个类型定义
|
|
35
|
-
* @param name 类型名称
|
|
36
|
-
* @param schema 模式对象
|
|
37
|
-
* @returns 类型信息
|
|
38
|
-
*/
|
|
39
|
-
private parseTypeDefinition;
|
|
40
|
-
/**
|
|
41
|
-
* 获取枚举成员名称
|
|
42
|
-
* @param schema 枚举 schema
|
|
43
|
-
* @param value 枚举值
|
|
44
|
-
* @param index 枚举值索引
|
|
45
|
-
* @returns 枚举成员名称
|
|
46
|
-
*/
|
|
47
|
-
private getEnumMemberName;
|
|
48
38
|
/**
|
|
49
39
|
* 解析本地 $ref 引用对象
|
|
50
40
|
* @param value 可能带有 $ref 的对象
|
|
41
|
+
* @param seenRefs 已访问的引用
|
|
42
|
+
* @param depth 当前解析深度
|
|
51
43
|
* @returns 引用解析后的对象
|
|
52
44
|
*/
|
|
53
45
|
private resolveReference;
|
|
46
|
+
/**
|
|
47
|
+
* 解析响应对象中的本地引用
|
|
48
|
+
* @param responses OpenAPI 响应集合
|
|
49
|
+
* @returns 已解析的响应集合
|
|
50
|
+
*/
|
|
51
|
+
private resolveResponses;
|
|
52
|
+
/**
|
|
53
|
+
* 合并同名同位置参数,后出现的 operation 级参数覆盖 path 级参数
|
|
54
|
+
* @param parameters 参数数组
|
|
55
|
+
* @returns 合并后的参数数组
|
|
56
|
+
*/
|
|
57
|
+
private mergeParameters;
|
|
54
58
|
/**
|
|
55
59
|
* 按标签分组API
|
|
56
60
|
* @param apis API接口数组
|