czh-api 1.0.3 → 1.0.4
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/CHANGELOG.md +17 -5
- package/dist/commands/build.js +169 -106
- package/dist/core/parser.js +52 -6
- package/dist/templates/api.hbs +1 -6
- package/package.json +1 -1
- package/src/commands/build.ts +225 -149
- package/src/core/parser.ts +58 -10
- package/src/templates/api.hbs +1 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
-
所有重要的版本更新都会记录在此文件中。
|
|
4
|
-
|
|
5
|
-
## [1.0.
|
|
6
|
-
|
|
7
|
-
###
|
|
3
|
+
所有重要的版本更新都会记录在此文件中。
|
|
4
|
+
|
|
5
|
+
## [1.0.4] - 2026-03-19
|
|
6
|
+
|
|
7
|
+
### 修复
|
|
8
|
+
- 修复响应最外层为数组(如 `type: array` + `items.$ref`)时类型生成异常的问题,避免出现非法接口声明(如 `export interface Xxx[]`)。
|
|
9
|
+
- 修复仅在“数组响应”中被引用的实体类型可能退化为 `{ [key: string]: any }` 的问题,增强该场景下的类型补全稳定性。
|
|
10
|
+
- 修复类型增强阶段对 `export interface` 的替换逻辑在存在内联对象字段时可能截断内容的问题,避免生成残留字段到接口外。
|
|
11
|
+
- 修复部分 OpenAPI `$ref` 路径在类型编译阶段解析不一致导致的类型降级问题(兼容 `#/components/schemas/*` 场景)。
|
|
12
|
+
|
|
13
|
+
### 改进
|
|
14
|
+
- 优化数组响应类型推导策略:当响应为 `$ref` 数组时优先生成 `ItemType[]`,减少中间响应类型噪音。
|
|
15
|
+
- 增强类型生成兜底逻辑的类型名合法性校验,避免对泛型/数组等复杂类型名误生成 `interface`。
|
|
16
|
+
|
|
17
|
+
## [1.0.3] - 2026-03-17
|
|
18
|
+
|
|
19
|
+
### 新增
|
|
8
20
|
- 添加 `pathPrefixes` 配置项,支持自定义路径前缀分组和二级分包
|
|
9
21
|
- 支持配置多个路径前缀,每个前缀可指定自定义包名或自动驼峰命名
|
|
10
22
|
- 自动按路径前缀后的第一级路径进行二级分包
|
package/dist/commands/build.js
CHANGED
|
@@ -52,64 +52,100 @@ function convertOpenApiTypeToTypeScript(openApiType) {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
// 改进类型定义的辅助函数
|
|
55
|
+
function isValidTypeIdentifier(typeName) {
|
|
56
|
+
if (!typeName)
|
|
57
|
+
return false;
|
|
58
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(typeName);
|
|
59
|
+
}
|
|
60
|
+
function escapeRegex(value) {
|
|
61
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
62
|
+
}
|
|
63
|
+
function resolveInterfaceTargetType(typeName) {
|
|
64
|
+
if (typeof typeName !== 'string' || typeName.length === 0)
|
|
65
|
+
return null;
|
|
66
|
+
if (isValidTypeIdentifier(typeName))
|
|
67
|
+
return typeName;
|
|
68
|
+
const arrayMatch = typeName.match(/^([A-Za-z_$][A-Za-z0-9_$]*)\[\]$/);
|
|
69
|
+
if (arrayMatch) {
|
|
70
|
+
return arrayMatch[1];
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
function findInterfaceBlockRange(content, typeName) {
|
|
75
|
+
const startRegex = new RegExp(`export interface ${escapeRegex(typeName)}\\s*\\{`);
|
|
76
|
+
const match = startRegex.exec(content);
|
|
77
|
+
if (!match)
|
|
78
|
+
return null;
|
|
79
|
+
const start = match.index;
|
|
80
|
+
const openBraceIndex = start + match[0].lastIndexOf('{');
|
|
81
|
+
let depth = 0;
|
|
82
|
+
for (let i = openBraceIndex; i < content.length; i++) {
|
|
83
|
+
const ch = content[i];
|
|
84
|
+
if (ch === '{') {
|
|
85
|
+
depth++;
|
|
86
|
+
}
|
|
87
|
+
else if (ch === '}') {
|
|
88
|
+
depth--;
|
|
89
|
+
if (depth === 0) {
|
|
90
|
+
return { start, end: i + 1 };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
55
96
|
function improveTypeDefinitions(typesContent, endpoints) {
|
|
56
97
|
let improvedContent = typesContent;
|
|
57
98
|
// 收集所有需要改进的类型定义
|
|
58
99
|
const typeImprovements = {};
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (type
|
|
69
|
-
|
|
70
|
-
if (type.includes('null')) {
|
|
71
|
-
type = type.replace(/\s*\|\s*null/g, ' | null');
|
|
72
|
-
}
|
|
100
|
+
// 辅助函数:将 jsdoc params 转换为接口字段
|
|
101
|
+
function buildFieldsFromJsdocParams(jsdocParams) {
|
|
102
|
+
return jsdocParams
|
|
103
|
+
.filter((param) => param.name && param.type)
|
|
104
|
+
.map((param) => {
|
|
105
|
+
const optional = !param.required ? '?' : '';
|
|
106
|
+
let type = param.type;
|
|
107
|
+
// 处理联合类型,确保正确的 TypeScript 语法
|
|
108
|
+
if (type && type.includes(' | ')) {
|
|
109
|
+
if (type.includes('null')) {
|
|
110
|
+
type = type.replace(/\s*\|\s*null/g, ' | null');
|
|
73
111
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
type = convertOpenApiTypeToTypeScript(type);
|
|
115
|
+
}
|
|
116
|
+
const comment = param.description ? ` // ${param.description}` : '';
|
|
117
|
+
return ` ${param.name}${optional}: ${type};${comment}`;
|
|
118
|
+
})
|
|
119
|
+
.join('\n');
|
|
120
|
+
}
|
|
121
|
+
endpoints.forEach(endpoint => {
|
|
122
|
+
// 改进 params 类型 - 使用 paramsJsdocParams(仅包含 path/query 参数)
|
|
123
|
+
const requestParamsTypeName = endpoint.requestParamsTypeName;
|
|
124
|
+
if (requestParamsTypeName && isValidTypeIdentifier(requestParamsTypeName) && endpoint.paramsJsdocParams && endpoint.paramsJsdocParams.length > 0) {
|
|
125
|
+
const paramsFields = buildFieldsFromJsdocParams(endpoint.paramsJsdocParams);
|
|
82
126
|
if (paramsFields) {
|
|
83
|
-
typeImprovements[
|
|
84
|
-
${paramsFields}
|
|
127
|
+
typeImprovements[requestParamsTypeName] = `export interface ${requestParamsTypeName} {
|
|
128
|
+
${paramsFields}
|
|
85
129
|
}`;
|
|
86
130
|
}
|
|
87
131
|
}
|
|
88
|
-
// 改进 requestBody 类型
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
.map((param) => {
|
|
93
|
-
const optional = !param.required ? '?' : '';
|
|
94
|
-
let type = param.type;
|
|
95
|
-
// 处理联合类型,确保正确的 TypeScript 语法
|
|
96
|
-
if (type && type.includes(' | ')) {
|
|
97
|
-
// 如果包含 null,确保格式正确
|
|
98
|
-
if (type.includes('null')) {
|
|
99
|
-
type = type.replace(/\s*\|\s*null/g, ' | null');
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
// 处理基本类型转换
|
|
104
|
-
type = convertOpenApiTypeToTypeScript(type);
|
|
105
|
-
}
|
|
106
|
-
const comment = param.description ? ` // ${param.description}` : '';
|
|
107
|
-
return ` ${param.name}${optional}: ${type};${comment}`;
|
|
108
|
-
})
|
|
109
|
-
.join('\n');
|
|
132
|
+
// 改进 requestBody 类型 - 使用 dataJsdocParams(仅包含 requestBody 参数)
|
|
133
|
+
const requestBodyTypeName = endpoint.requestBodyTypeName;
|
|
134
|
+
if (requestBodyTypeName && isValidTypeIdentifier(requestBodyTypeName) && endpoint.dataJsdocParams && endpoint.dataJsdocParams.length > 0) {
|
|
135
|
+
const dataFields = buildFieldsFromJsdocParams(endpoint.dataJsdocParams);
|
|
110
136
|
if (dataFields) {
|
|
111
|
-
typeImprovements[
|
|
112
|
-
${dataFields}
|
|
137
|
+
typeImprovements[requestBodyTypeName] = `export interface ${requestBodyTypeName} {
|
|
138
|
+
${dataFields}
|
|
139
|
+
}`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// 改进 response 类型 - 使用 responseJsdocParams(包含响应字段信息)
|
|
143
|
+
const responseTargetType = resolveInterfaceTargetType(endpoint.responseTypeName);
|
|
144
|
+
if (responseTargetType && endpoint.responseTypeName !== 'void' && endpoint.responseJsdocParams && endpoint.responseJsdocParams.length > 0) {
|
|
145
|
+
const responseFields = buildFieldsFromJsdocParams(endpoint.responseJsdocParams);
|
|
146
|
+
if (responseFields) {
|
|
147
|
+
typeImprovements[responseTargetType] = `export interface ${responseTargetType} {
|
|
148
|
+
${responseFields}
|
|
113
149
|
}`;
|
|
114
150
|
}
|
|
115
151
|
}
|
|
@@ -117,9 +153,12 @@ ${dataFields}
|
|
|
117
153
|
// 替换现有的类型定义
|
|
118
154
|
Object.entries(typeImprovements).forEach(([typeName, newDefinition]) => {
|
|
119
155
|
// 查找并替换现有的接口定义
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
122
|
-
improvedContent =
|
|
156
|
+
const blockRange = findInterfaceBlockRange(improvedContent, typeName);
|
|
157
|
+
if (blockRange) {
|
|
158
|
+
improvedContent =
|
|
159
|
+
improvedContent.slice(0, blockRange.start) +
|
|
160
|
+
newDefinition +
|
|
161
|
+
improvedContent.slice(blockRange.end);
|
|
123
162
|
}
|
|
124
163
|
else {
|
|
125
164
|
// 如果没找到接口定义,添加到末尾
|
|
@@ -186,6 +225,33 @@ function removeUselessTypeAliases(typesContent) {
|
|
|
186
225
|
cleanedContent = cleanedContent.replace(/\/\*\*\n \* [^\n]*\n \*\/\n(?!export)/g, '');
|
|
187
226
|
return cleanedContent;
|
|
188
227
|
}
|
|
228
|
+
function normalizeRef(refValue) {
|
|
229
|
+
const match = refValue.match(/^#\/components\/schemas\/([^/]+)$/);
|
|
230
|
+
if (match) {
|
|
231
|
+
return `#/definitions/${match[1]}`;
|
|
232
|
+
}
|
|
233
|
+
return refValue;
|
|
234
|
+
}
|
|
235
|
+
function normalizeSchemaRefs(value) {
|
|
236
|
+
if (Array.isArray(value)) {
|
|
237
|
+
return value.map(item => normalizeSchemaRefs(item));
|
|
238
|
+
}
|
|
239
|
+
if (value && typeof value === 'object') {
|
|
240
|
+
const obj = value;
|
|
241
|
+
const normalized = {};
|
|
242
|
+
for (const key in obj) {
|
|
243
|
+
const currentValue = obj[key];
|
|
244
|
+
if (key === '$ref' && typeof currentValue === 'string') {
|
|
245
|
+
normalized[key] = normalizeRef(currentValue);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
normalized[key] = normalizeSchemaRefs(currentValue);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return normalized;
|
|
252
|
+
}
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
189
255
|
const handleBuild = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
190
256
|
try {
|
|
191
257
|
console.log(chalk_1.default.blue('Building API from source...'));
|
|
@@ -285,9 +351,10 @@ const handleBuild = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
285
351
|
if (Object.keys(module.schemas).length > 0) {
|
|
286
352
|
console.log(chalk_1.default.blue(`正在为模块 "${moduleName}" 生成类型文件,包含 ${Object.keys(module.schemas).length} 个 schema`));
|
|
287
353
|
try {
|
|
354
|
+
const normalizedModuleSchemas = normalizeSchemaRefs(module.schemas);
|
|
288
355
|
const schemasWithTitles = {};
|
|
289
|
-
for (const schemaName in
|
|
290
|
-
schemasWithTitles[schemaName] = Object.assign(Object.assign({},
|
|
356
|
+
for (const schemaName in normalizedModuleSchemas) {
|
|
357
|
+
schemasWithTitles[schemaName] = Object.assign(Object.assign({}, normalizedModuleSchemas[schemaName]), { title: schemaName });
|
|
291
358
|
}
|
|
292
359
|
const rootSchemaForCompiler = {
|
|
293
360
|
title: 'schemas',
|
|
@@ -334,7 +401,8 @@ const handleBuild = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
334
401
|
// 尝试逐个验证 schema,过滤掉有问题的
|
|
335
402
|
const validSchemas = {};
|
|
336
403
|
const invalidSchemas = [];
|
|
337
|
-
|
|
404
|
+
const normalizedModuleSchemas = normalizeSchemaRefs(module.schemas);
|
|
405
|
+
for (const schemaName in normalizedModuleSchemas) {
|
|
338
406
|
try {
|
|
339
407
|
// 尝试单独编译每个 schema
|
|
340
408
|
const testSchema = {
|
|
@@ -342,11 +410,11 @@ const handleBuild = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
342
410
|
type: 'object',
|
|
343
411
|
properties: {},
|
|
344
412
|
additionalProperties: false,
|
|
345
|
-
definitions: { [schemaName]:
|
|
346
|
-
components: { schemas: { [schemaName]:
|
|
413
|
+
definitions: { [schemaName]: normalizedModuleSchemas[schemaName] },
|
|
414
|
+
components: { schemas: { [schemaName]: normalizedModuleSchemas[schemaName] } }
|
|
347
415
|
};
|
|
348
416
|
yield (0, json_schema_to_typescript_1.compile)(testSchema, 'test', { unreachableDefinitions: true });
|
|
349
|
-
validSchemas[schemaName] = Object.assign(Object.assign({},
|
|
417
|
+
validSchemas[schemaName] = Object.assign(Object.assign({}, normalizedModuleSchemas[schemaName]), { title: schemaName });
|
|
350
418
|
}
|
|
351
419
|
catch (schemaError) {
|
|
352
420
|
invalidSchemas.push(schemaName);
|
|
@@ -415,68 +483,63 @@ const handleBuild = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
415
483
|
const typeDefinitions = {};
|
|
416
484
|
// 从 endpoints 中提取类型定义
|
|
417
485
|
module.endpoints.forEach(endpoint => {
|
|
418
|
-
//
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
// 如果包含 null,确保格式正确
|
|
428
|
-
if (type.includes('null')) {
|
|
429
|
-
type = type.replace(/\s*\|\s*null/g, ' | null');
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
else {
|
|
433
|
-
// 处理基本类型转换
|
|
434
|
-
type = convertOpenApiTypeToTypeScript(type);
|
|
486
|
+
// 辅助函数:将 jsdoc params 转换为字段
|
|
487
|
+
const buildFields = (params) => params
|
|
488
|
+
.filter((param) => param.name && param.type)
|
|
489
|
+
.map((param) => {
|
|
490
|
+
const optional = !param.required ? '?' : '';
|
|
491
|
+
let type = param.type;
|
|
492
|
+
if (type && type.includes(' | ')) {
|
|
493
|
+
if (type.includes('null')) {
|
|
494
|
+
type = type.replace(/\s*\|\s*null/g, ' | null');
|
|
435
495
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
type = convertOpenApiTypeToTypeScript(type);
|
|
499
|
+
}
|
|
500
|
+
const comment = param.description ? ` // ${param.description}` : '';
|
|
501
|
+
return ` ${param.name}${optional}: ${type};${comment}`;
|
|
502
|
+
})
|
|
503
|
+
.join('\n');
|
|
504
|
+
// 处理 params 类型 - 使用 paramsJsdocParams
|
|
505
|
+
const requestParamsTypeName = endpoint.requestParamsTypeName;
|
|
506
|
+
if (requestParamsTypeName && isValidTypeIdentifier(requestParamsTypeName) && endpoint.paramsJsdocParams && endpoint.paramsJsdocParams.length > 0) {
|
|
507
|
+
const paramsFields = buildFields(endpoint.paramsJsdocParams);
|
|
440
508
|
if (paramsFields) {
|
|
441
|
-
typeDefinitions[
|
|
442
|
-
${paramsFields}
|
|
509
|
+
typeDefinitions[requestParamsTypeName] = `export interface ${requestParamsTypeName} {
|
|
510
|
+
${paramsFields}
|
|
443
511
|
}`;
|
|
444
512
|
}
|
|
445
513
|
}
|
|
446
|
-
// 处理 data 类型 (requestBody)
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
.map(param => {
|
|
451
|
-
const optional = !param.required ? '?' : '';
|
|
452
|
-
let type = param.type;
|
|
453
|
-
// 处理联合类型
|
|
454
|
-
if (type && type.includes(' | ')) {
|
|
455
|
-
// 如果包含 null,确保格式正确
|
|
456
|
-
if (type.includes('null')) {
|
|
457
|
-
type = type.replace(/\s*\|\s*null/g, ' | null');
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
else {
|
|
461
|
-
// 处理基本类型转换
|
|
462
|
-
type = convertOpenApiTypeToTypeScript(type);
|
|
463
|
-
}
|
|
464
|
-
const comment = param.description ? ` // ${param.description}` : '';
|
|
465
|
-
return ` ${param.name}${optional}: ${type};${comment}`;
|
|
466
|
-
})
|
|
467
|
-
.join('\n');
|
|
514
|
+
// 处理 data 类型 (requestBody) - 使用 dataJsdocParams
|
|
515
|
+
const requestBodyTypeName = endpoint.requestBodyTypeName;
|
|
516
|
+
if (requestBodyTypeName && isValidTypeIdentifier(requestBodyTypeName) && endpoint.dataJsdocParams && endpoint.dataJsdocParams.length > 0) {
|
|
517
|
+
const dataFields = buildFields(endpoint.dataJsdocParams);
|
|
468
518
|
if (dataFields) {
|
|
469
|
-
typeDefinitions[
|
|
470
|
-
${dataFields}
|
|
519
|
+
typeDefinitions[requestBodyTypeName] = `export interface ${requestBodyTypeName} {
|
|
520
|
+
${dataFields}
|
|
521
|
+
}`;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
// 处理 response 类型 - 使用 responseJsdocParams
|
|
525
|
+
const responseTargetType = resolveInterfaceTargetType(endpoint.responseTypeName);
|
|
526
|
+
if (responseTargetType && endpoint.responseTypeName !== 'void' && endpoint.responseJsdocParams && endpoint.responseJsdocParams.length > 0) {
|
|
527
|
+
const responseFields = buildFields(endpoint.responseJsdocParams);
|
|
528
|
+
if (responseFields) {
|
|
529
|
+
typeDefinitions[responseTargetType] = `export interface ${responseTargetType} {
|
|
530
|
+
${responseFields}
|
|
471
531
|
}`;
|
|
472
532
|
}
|
|
473
533
|
}
|
|
474
534
|
});
|
|
475
535
|
// 为没有定义的类型添加基本定义
|
|
476
536
|
allReferencedTypes.forEach(typeName => {
|
|
537
|
+
if (!isValidTypeIdentifier(typeName)) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
477
540
|
if (!typeDefinitions[typeName]) {
|
|
478
|
-
typeDefinitions[typeName] = `export interface ${typeName} {
|
|
479
|
-
[key: string]: any;
|
|
541
|
+
typeDefinitions[typeName] = `export interface ${typeName} {
|
|
542
|
+
[key: string]: any;
|
|
480
543
|
}`;
|
|
481
544
|
}
|
|
482
545
|
});
|
package/dist/core/parser.js
CHANGED
|
@@ -238,6 +238,9 @@ const processApi = (api, excludePaths = [], includePaths = [], pathPrefixes = []
|
|
|
238
238
|
let requestBodyTypeName = undefined;
|
|
239
239
|
let contentType = undefined;
|
|
240
240
|
const jsdocParams = [];
|
|
241
|
+
const paramsJsdocParams = [];
|
|
242
|
+
const dataJsdocParams = [];
|
|
243
|
+
const responseJsdocParams = [];
|
|
241
244
|
// --- FormData Detection ---
|
|
242
245
|
let isFormData = false;
|
|
243
246
|
// Method 1: Check requestBody
|
|
@@ -331,13 +334,17 @@ const processApi = (api, excludePaths = [], includePaths = [], pathPrefixes = []
|
|
|
331
334
|
requestBodyTypeName = typeName;
|
|
332
335
|
modules[moduleName].schemas[typeName] = formDataSchema;
|
|
333
336
|
referencedTypes.push(typeName);
|
|
334
|
-
|
|
337
|
+
const formDataJsdoc = extractJsdocParamsFromSchema(formDataSchema, allSchemas);
|
|
338
|
+
jsdocParams.push(...formDataJsdoc);
|
|
339
|
+
dataJsdocParams.push(...formDataJsdoc);
|
|
335
340
|
}
|
|
336
341
|
if (Object.keys(paramsSchema.properties || {}).length > 0) {
|
|
337
342
|
requestParamsTypeName = `${functionName.charAt(0).toUpperCase() + functionName.slice(1)}Params`;
|
|
338
343
|
modules[moduleName].schemas[requestParamsTypeName] = paramsSchema;
|
|
339
344
|
referencedTypes.push(requestParamsTypeName);
|
|
340
|
-
|
|
345
|
+
const paramsJsdoc = extractJsdocParamsFromSchema(paramsSchema, allSchemas);
|
|
346
|
+
jsdocParams.push(...paramsJsdoc);
|
|
347
|
+
paramsJsdocParams.push(...paramsJsdoc);
|
|
341
348
|
}
|
|
342
349
|
}
|
|
343
350
|
// Handle standard Request Body
|
|
@@ -350,7 +357,9 @@ const processApi = (api, excludePaths = [], includePaths = [], pathPrefixes = []
|
|
|
350
357
|
requestBodyTypeName = name;
|
|
351
358
|
addSchemaWithDependencies(name, modules[moduleName], allSchemas);
|
|
352
359
|
referencedTypes.push(name);
|
|
353
|
-
|
|
360
|
+
const bodyJsdoc = extractJsdocParamsFromSchema(jsonContent.schema, allSchemas);
|
|
361
|
+
jsdocParams.push(...bodyJsdoc);
|
|
362
|
+
dataJsdocParams.push(...bodyJsdoc);
|
|
354
363
|
}
|
|
355
364
|
else {
|
|
356
365
|
// Handle inline schema (including arrays)
|
|
@@ -367,11 +376,15 @@ const processApi = (api, excludePaths = [], includePaths = [], pathPrefixes = []
|
|
|
367
376
|
addSchemaWithDependencies(itemSchemaName, modules[moduleName], allSchemas);
|
|
368
377
|
}
|
|
369
378
|
else {
|
|
370
|
-
|
|
379
|
+
const arrayJsdoc = extractJsdocParamsFromSchema(schema.items, allSchemas);
|
|
380
|
+
jsdocParams.push(...arrayJsdoc);
|
|
381
|
+
dataJsdocParams.push(...arrayJsdoc);
|
|
371
382
|
}
|
|
372
383
|
}
|
|
373
384
|
else if (schema.type === 'object') {
|
|
374
|
-
|
|
385
|
+
const objJsdoc = extractJsdocParamsFromSchema(schema, allSchemas);
|
|
386
|
+
jsdocParams.push(...objJsdoc);
|
|
387
|
+
dataJsdocParams.push(...objJsdoc);
|
|
375
388
|
}
|
|
376
389
|
}
|
|
377
390
|
}
|
|
@@ -385,7 +398,8 @@ const processApi = (api, excludePaths = [], includePaths = [], pathPrefixes = []
|
|
|
385
398
|
requestBodyTypeName = typeName;
|
|
386
399
|
modules[moduleName].schemas[typeName] = formDataContent.schema;
|
|
387
400
|
referencedTypes.push(typeName);
|
|
388
|
-
|
|
401
|
+
const formJsdoc = extractJsdocParamsFromSchema(formDataContent.schema, allSchemas);
|
|
402
|
+
dataJsdocParams.push(...formJsdoc);
|
|
389
403
|
}
|
|
390
404
|
}
|
|
391
405
|
// Handle Response Body
|
|
@@ -399,6 +413,35 @@ const processApi = (api, excludePaths = [], includePaths = [], pathPrefixes = []
|
|
|
399
413
|
responseTypeName = name;
|
|
400
414
|
addSchemaWithDependencies(name, modules[moduleName], allSchemas);
|
|
401
415
|
referencedTypes.push(name);
|
|
416
|
+
responseJsdocParams.push(...extractJsdocParamsFromSchema(schema, allSchemas));
|
|
417
|
+
}
|
|
418
|
+
else if (schema && !isReferenceObject(schema)) {
|
|
419
|
+
const inlineSchema = schema;
|
|
420
|
+
if (inlineSchema.type === 'object' || inlineSchema.properties) {
|
|
421
|
+
// Generate a named type for inline response schema
|
|
422
|
+
const typeName = `${functionName.charAt(0).toUpperCase() + functionName.slice(1)}Response`;
|
|
423
|
+
responseTypeName = typeName;
|
|
424
|
+
modules[moduleName].schemas[typeName] = inlineSchema;
|
|
425
|
+
referencedTypes.push(typeName);
|
|
426
|
+
responseJsdocParams.push(...extractJsdocParamsFromSchema(inlineSchema, allSchemas));
|
|
427
|
+
}
|
|
428
|
+
else if (inlineSchema.type === 'array' && inlineSchema.items) {
|
|
429
|
+
if (isReferenceObject(inlineSchema.items)) {
|
|
430
|
+
const itemSchemaName = getSchemaName(inlineSchema.items.$ref);
|
|
431
|
+
// Prefer direct `ItemType[]` for array responses to avoid unnecessary alias schema compilation.
|
|
432
|
+
responseTypeName = `${itemSchemaName}[]`;
|
|
433
|
+
referencedTypes.push(itemSchemaName);
|
|
434
|
+
addSchemaWithDependencies(itemSchemaName, modules[moduleName], allSchemas);
|
|
435
|
+
responseJsdocParams.push(...extractJsdocParamsFromSchema(inlineSchema.items, allSchemas));
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
const typeName = `${functionName.charAt(0).toUpperCase() + functionName.slice(1)}Response`;
|
|
439
|
+
responseTypeName = typeName;
|
|
440
|
+
modules[moduleName].schemas[typeName] = inlineSchema;
|
|
441
|
+
referencedTypes.push(typeName);
|
|
442
|
+
responseJsdocParams.push(...extractJsdocParamsFromSchema(inlineSchema, allSchemas));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
402
445
|
}
|
|
403
446
|
}
|
|
404
447
|
// Convert path to template literal string for path parameters
|
|
@@ -416,6 +459,9 @@ const processApi = (api, excludePaths = [], includePaths = [], pathPrefixes = []
|
|
|
416
459
|
hasData: !!requestBodyTypeName,
|
|
417
460
|
contentType,
|
|
418
461
|
jsdocParams,
|
|
462
|
+
paramsJsdocParams,
|
|
463
|
+
dataJsdocParams,
|
|
464
|
+
responseJsdocParams,
|
|
419
465
|
};
|
|
420
466
|
modules[moduleName].endpoints.push(endpoint);
|
|
421
467
|
}
|
package/dist/templates/api.hbs
CHANGED
|
@@ -10,12 +10,7 @@ export const {{functionName}} = ({{#if hasParams}}params: {{requestParamsTypeNam
|
|
|
10
10
|
return http.request<{{responseTypeName}}>({
|
|
11
11
|
url: `{{path}}`,
|
|
12
12
|
method: '{{method}}',
|
|
13
|
-
{{#if hasParams}}
|
|
14
|
-
params,
|
|
15
|
-
{{/if}}
|
|
16
|
-
{{#if hasData}}
|
|
17
|
-
data,
|
|
18
|
-
{{/if}}
|
|
13
|
+
{{#if hasParams}}{{#unless (eq method 'put')}}{{#unless (eq method 'post')}}params,{{/unless}}{{/unless}}{{/if}}{{#if hasData}}data,{{/if}}
|
|
19
14
|
{{#if contentType}}
|
|
20
15
|
headers: { 'Content-Type': '{{contentType}}' },
|
|
21
16
|
{{/if}}
|
package/package.json
CHANGED
package/src/commands/build.ts
CHANGED
|
@@ -40,83 +40,128 @@ function convertOpenApiTypeToTypeScript(openApiType: string | undefined): string
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// 改进类型定义的辅助函数
|
|
43
|
-
function
|
|
43
|
+
function isValidTypeIdentifier(typeName: string | undefined): boolean {
|
|
44
|
+
if (!typeName) return false;
|
|
45
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(typeName);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function escapeRegex(value: string): string {
|
|
49
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveInterfaceTargetType(typeName: string | undefined): string | null {
|
|
53
|
+
if (typeof typeName !== 'string' || typeName.length === 0) return null;
|
|
54
|
+
if (isValidTypeIdentifier(typeName)) return typeName;
|
|
55
|
+
|
|
56
|
+
const arrayMatch = typeName.match(/^([A-Za-z_$][A-Za-z0-9_$]*)\[\]$/);
|
|
57
|
+
if (arrayMatch) {
|
|
58
|
+
return arrayMatch[1];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function findInterfaceBlockRange(content: string, typeName: string): { start: number; end: number } | null {
|
|
65
|
+
const startRegex = new RegExp(`export interface ${escapeRegex(typeName)}\\s*\\{`);
|
|
66
|
+
const match = startRegex.exec(content);
|
|
67
|
+
if (!match) return null;
|
|
68
|
+
|
|
69
|
+
const start = match.index;
|
|
70
|
+
const openBraceIndex = start + match[0].lastIndexOf('{');
|
|
71
|
+
let depth = 0;
|
|
72
|
+
|
|
73
|
+
for (let i = openBraceIndex; i < content.length; i++) {
|
|
74
|
+
const ch = content[i];
|
|
75
|
+
if (ch === '{') {
|
|
76
|
+
depth++;
|
|
77
|
+
} else if (ch === '}') {
|
|
78
|
+
depth--;
|
|
79
|
+
if (depth === 0) {
|
|
80
|
+
return { start, end: i + 1 };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function improveTypeDefinitions(typesContent: string, endpoints: any[]): string {
|
|
44
89
|
let improvedContent = typesContent;
|
|
45
90
|
|
|
46
91
|
// 收集所有需要改进的类型定义
|
|
47
92
|
const typeImprovements: { [typeName: string]: string } = {};
|
|
93
|
+
|
|
94
|
+
// 辅助函数:将 jsdoc params 转换为接口字段
|
|
95
|
+
function buildFieldsFromJsdocParams(jsdocParams: any[]): string {
|
|
96
|
+
return jsdocParams
|
|
97
|
+
.filter((param: any) => param.name && param.type)
|
|
98
|
+
.map((param: any) => {
|
|
99
|
+
const optional = !param.required ? '?' : '';
|
|
100
|
+
let type = param.type;
|
|
101
|
+
|
|
102
|
+
// 处理联合类型,确保正确的 TypeScript 语法
|
|
103
|
+
if (type && type.includes(' | ')) {
|
|
104
|
+
if (type.includes('null')) {
|
|
105
|
+
type = type.replace(/\s*\|\s*null/g, ' | null');
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
type = convertOpenApiTypeToTypeScript(type);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const comment = param.description ? ` // ${param.description}` : '';
|
|
112
|
+
return ` ${param.name}${optional}: ${type};${comment}`;
|
|
113
|
+
})
|
|
114
|
+
.join('\n');
|
|
115
|
+
}
|
|
48
116
|
|
|
49
117
|
endpoints.forEach(endpoint => {
|
|
50
|
-
// 改进 params 类型
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (type.includes('null')) {
|
|
62
|
-
type = type.replace(/\s*\|\s*null/g, ' | null');
|
|
63
|
-
}
|
|
64
|
-
} else {
|
|
65
|
-
// 处理基本类型转换
|
|
66
|
-
type = convertOpenApiTypeToTypeScript(type);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const comment = param.description ? ` // ${param.description}` : '';
|
|
70
|
-
return ` ${param.name}${optional}: ${type};${comment}`;
|
|
71
|
-
})
|
|
72
|
-
.join('\n');
|
|
73
|
-
|
|
74
|
-
if (paramsFields) {
|
|
75
|
-
typeImprovements[endpoint.requestParamsTypeName] = `export interface ${endpoint.requestParamsTypeName} {
|
|
76
|
-
${paramsFields}
|
|
77
|
-
}`;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
118
|
+
// 改进 params 类型 - 使用 paramsJsdocParams(仅包含 path/query 参数)
|
|
119
|
+
const requestParamsTypeName = endpoint.requestParamsTypeName;
|
|
120
|
+
if (requestParamsTypeName && isValidTypeIdentifier(requestParamsTypeName) && endpoint.paramsJsdocParams && endpoint.paramsJsdocParams.length > 0) {
|
|
121
|
+
const paramsFields = buildFieldsFromJsdocParams(endpoint.paramsJsdocParams);
|
|
122
|
+
|
|
123
|
+
if (paramsFields) {
|
|
124
|
+
typeImprovements[requestParamsTypeName] = `export interface ${requestParamsTypeName} {
|
|
125
|
+
${paramsFields}
|
|
126
|
+
}`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
80
129
|
|
|
81
|
-
// 改进 requestBody 类型
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (dataFields) {
|
|
106
|
-
typeImprovements[endpoint.requestBodyTypeName] = `export interface ${endpoint.requestBodyTypeName} {
|
|
107
|
-
${dataFields}
|
|
108
|
-
}`;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
130
|
+
// 改进 requestBody 类型 - 使用 dataJsdocParams(仅包含 requestBody 参数)
|
|
131
|
+
const requestBodyTypeName = endpoint.requestBodyTypeName;
|
|
132
|
+
if (requestBodyTypeName && isValidTypeIdentifier(requestBodyTypeName) && endpoint.dataJsdocParams && endpoint.dataJsdocParams.length > 0) {
|
|
133
|
+
const dataFields = buildFieldsFromJsdocParams(endpoint.dataJsdocParams);
|
|
134
|
+
|
|
135
|
+
if (dataFields) {
|
|
136
|
+
typeImprovements[requestBodyTypeName] = `export interface ${requestBodyTypeName} {
|
|
137
|
+
${dataFields}
|
|
138
|
+
}`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 改进 response 类型 - 使用 responseJsdocParams(包含响应字段信息)
|
|
143
|
+
const responseTargetType = resolveInterfaceTargetType(endpoint.responseTypeName);
|
|
144
|
+
if (responseTargetType && endpoint.responseTypeName !== 'void' && endpoint.responseJsdocParams && endpoint.responseJsdocParams.length > 0) {
|
|
145
|
+
const responseFields = buildFieldsFromJsdocParams(endpoint.responseJsdocParams);
|
|
146
|
+
|
|
147
|
+
if (responseFields) {
|
|
148
|
+
typeImprovements[responseTargetType] = `export interface ${responseTargetType} {
|
|
149
|
+
${responseFields}
|
|
150
|
+
}`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
111
153
|
});
|
|
112
154
|
|
|
113
155
|
// 替换现有的类型定义
|
|
114
156
|
Object.entries(typeImprovements).forEach(([typeName, newDefinition]) => {
|
|
115
157
|
// 查找并替换现有的接口定义
|
|
116
|
-
const
|
|
117
|
-
if (
|
|
118
|
-
improvedContent =
|
|
119
|
-
|
|
158
|
+
const blockRange = findInterfaceBlockRange(improvedContent, typeName);
|
|
159
|
+
if (blockRange) {
|
|
160
|
+
improvedContent =
|
|
161
|
+
improvedContent.slice(0, blockRange.start) +
|
|
162
|
+
newDefinition +
|
|
163
|
+
improvedContent.slice(blockRange.end);
|
|
164
|
+
} else {
|
|
120
165
|
// 如果没找到接口定义,添加到末尾
|
|
121
166
|
improvedContent += '\n\n' + newDefinition;
|
|
122
167
|
}
|
|
@@ -126,7 +171,7 @@ ${dataFields}
|
|
|
126
171
|
}
|
|
127
172
|
|
|
128
173
|
// 移除无用的类型别名
|
|
129
|
-
function removeUselessTypeAliases(typesContent: string): string {
|
|
174
|
+
function removeUselessTypeAliases(typesContent: string): string {
|
|
130
175
|
let cleanedContent = typesContent;
|
|
131
176
|
|
|
132
177
|
// 收集要删除的类型别名及其对应的基础类型
|
|
@@ -193,10 +238,42 @@ function removeUselessTypeAliases(typesContent: string): string {
|
|
|
193
238
|
// 移除孤立的类型注释(没有对应类型定义的注释)
|
|
194
239
|
cleanedContent = cleanedContent.replace(/\/\*\*\n \* [^\n]*\n \*\/\n(?!export)/g, '');
|
|
195
240
|
|
|
196
|
-
return cleanedContent;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
241
|
+
return cleanedContent;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function normalizeRef(refValue: string): string {
|
|
245
|
+
const match = refValue.match(/^#\/components\/schemas\/([^/]+)$/);
|
|
246
|
+
if (match) {
|
|
247
|
+
return `#/definitions/${match[1]}`;
|
|
248
|
+
}
|
|
249
|
+
return refValue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function normalizeSchemaRefs<T>(value: T): T {
|
|
253
|
+
if (Array.isArray(value)) {
|
|
254
|
+
return value.map(item => normalizeSchemaRefs(item)) as T;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (value && typeof value === 'object') {
|
|
258
|
+
const obj = value as Record<string, unknown>;
|
|
259
|
+
const normalized: Record<string, unknown> = {};
|
|
260
|
+
|
|
261
|
+
for (const key in obj) {
|
|
262
|
+
const currentValue = obj[key];
|
|
263
|
+
if (key === '$ref' && typeof currentValue === 'string') {
|
|
264
|
+
normalized[key] = normalizeRef(currentValue);
|
|
265
|
+
} else {
|
|
266
|
+
normalized[key] = normalizeSchemaRefs(currentValue);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return normalized as T;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return value;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
interface IConfig {
|
|
200
277
|
url: string;
|
|
201
278
|
outputDir: string;
|
|
202
279
|
httpClientPath: string;
|
|
@@ -317,13 +394,14 @@ export const handleBuild = async () => {
|
|
|
317
394
|
if (Object.keys(module.schemas).length > 0) {
|
|
318
395
|
console.log(chalk.blue(`正在为模块 "${moduleName}" 生成类型文件,包含 ${Object.keys(module.schemas).length} 个 schema`));
|
|
319
396
|
try {
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
397
|
+
const normalizedModuleSchemas = normalizeSchemaRefs(module.schemas);
|
|
398
|
+
const schemasWithTitles: { [key: string]: OpenAPIV3.SchemaObject } = {};
|
|
399
|
+
for (const schemaName in normalizedModuleSchemas) {
|
|
400
|
+
schemasWithTitles[schemaName] = {
|
|
401
|
+
...normalizedModuleSchemas[schemaName],
|
|
402
|
+
title: schemaName,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
327
405
|
|
|
328
406
|
const rootSchemaForCompiler = {
|
|
329
407
|
title: 'schemas',
|
|
@@ -374,9 +452,10 @@ export const handleBuild = async () => {
|
|
|
374
452
|
|
|
375
453
|
// 尝试逐个验证 schema,过滤掉有问题的
|
|
376
454
|
const validSchemas: { [key: string]: OpenAPIV3.SchemaObject } = {};
|
|
377
|
-
const invalidSchemas: string[] = [];
|
|
455
|
+
const invalidSchemas: string[] = [];
|
|
456
|
+
const normalizedModuleSchemas = normalizeSchemaRefs(module.schemas);
|
|
378
457
|
|
|
379
|
-
for (const schemaName in
|
|
458
|
+
for (const schemaName in normalizedModuleSchemas) {
|
|
380
459
|
try {
|
|
381
460
|
// 尝试单独编译每个 schema
|
|
382
461
|
const testSchema = {
|
|
@@ -384,12 +463,12 @@ export const handleBuild = async () => {
|
|
|
384
463
|
type: 'object',
|
|
385
464
|
properties: {},
|
|
386
465
|
additionalProperties: false,
|
|
387
|
-
definitions: { [schemaName]:
|
|
388
|
-
components: { schemas: { [schemaName]:
|
|
466
|
+
definitions: { [schemaName]: normalizedModuleSchemas[schemaName] },
|
|
467
|
+
components: { schemas: { [schemaName]: normalizedModuleSchemas[schemaName] } }
|
|
389
468
|
};
|
|
390
469
|
await compile(testSchema as any, 'test', { unreachableDefinitions: true });
|
|
391
470
|
validSchemas[schemaName] = {
|
|
392
|
-
...
|
|
471
|
+
...normalizedModuleSchemas[schemaName],
|
|
393
472
|
title: schemaName
|
|
394
473
|
};
|
|
395
474
|
} catch (schemaError) {
|
|
@@ -467,77 +546,74 @@ export const handleBuild = async () => {
|
|
|
467
546
|
|
|
468
547
|
// 从 endpoints 中提取类型定义
|
|
469
548
|
module.endpoints.forEach(endpoint => {
|
|
470
|
-
//
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
.
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
// 如果包含 null,确保格式正确
|
|
481
|
-
if (type.includes('null')) {
|
|
482
|
-
type = type.replace(/\s*\|\s*null/g, ' | null');
|
|
483
|
-
}
|
|
484
|
-
} else {
|
|
485
|
-
// 处理基本类型转换
|
|
486
|
-
type = convertOpenApiTypeToTypeScript(type);
|
|
549
|
+
// 辅助函数:将 jsdoc params 转换为字段
|
|
550
|
+
const buildFields = (params: any[]) => params
|
|
551
|
+
.filter((param: any) => param.name && param.type)
|
|
552
|
+
.map((param: any) => {
|
|
553
|
+
const optional = !param.required ? '?' : '';
|
|
554
|
+
let type = param.type;
|
|
555
|
+
|
|
556
|
+
if (type && type.includes(' | ')) {
|
|
557
|
+
if (type.includes('null')) {
|
|
558
|
+
type = type.replace(/\s*\|\s*null/g, ' | null');
|
|
487
559
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
.
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
560
|
+
} else {
|
|
561
|
+
type = convertOpenApiTypeToTypeScript(type);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const comment = param.description ? ` // ${param.description}` : '';
|
|
565
|
+
return ` ${param.name}${optional}: ${type};${comment}`;
|
|
566
|
+
})
|
|
567
|
+
.join('\n');
|
|
568
|
+
|
|
569
|
+
// 处理 params 类型 - 使用 paramsJsdocParams
|
|
570
|
+
const requestParamsTypeName = endpoint.requestParamsTypeName;
|
|
571
|
+
if (requestParamsTypeName && isValidTypeIdentifier(requestParamsTypeName) && endpoint.paramsJsdocParams && endpoint.paramsJsdocParams.length > 0) {
|
|
572
|
+
const paramsFields = buildFields(endpoint.paramsJsdocParams);
|
|
573
|
+
|
|
574
|
+
if (paramsFields) {
|
|
575
|
+
typeDefinitions[requestParamsTypeName] = `export interface ${requestParamsTypeName} {
|
|
576
|
+
${paramsFields}
|
|
577
|
+
}`;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
500
580
|
|
|
501
|
-
// 处理 data 类型 (requestBody)
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
if (dataFields) {
|
|
526
|
-
typeDefinitions[endpoint.requestBodyTypeName] = `export interface ${endpoint.requestBodyTypeName} {
|
|
527
|
-
${dataFields}
|
|
528
|
-
}`;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
581
|
+
// 处理 data 类型 (requestBody) - 使用 dataJsdocParams
|
|
582
|
+
const requestBodyTypeName = endpoint.requestBodyTypeName;
|
|
583
|
+
if (requestBodyTypeName && isValidTypeIdentifier(requestBodyTypeName) && endpoint.dataJsdocParams && endpoint.dataJsdocParams.length > 0) {
|
|
584
|
+
const dataFields = buildFields(endpoint.dataJsdocParams);
|
|
585
|
+
|
|
586
|
+
if (dataFields) {
|
|
587
|
+
typeDefinitions[requestBodyTypeName] = `export interface ${requestBodyTypeName} {
|
|
588
|
+
${dataFields}
|
|
589
|
+
}`;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// 处理 response 类型 - 使用 responseJsdocParams
|
|
594
|
+
const responseTargetType = resolveInterfaceTargetType(endpoint.responseTypeName);
|
|
595
|
+
if (responseTargetType && endpoint.responseTypeName !== 'void' && endpoint.responseJsdocParams && endpoint.responseJsdocParams.length > 0) {
|
|
596
|
+
const responseFields = buildFields(endpoint.responseJsdocParams);
|
|
597
|
+
|
|
598
|
+
if (responseFields) {
|
|
599
|
+
typeDefinitions[responseTargetType] = `export interface ${responseTargetType} {
|
|
600
|
+
${responseFields}
|
|
601
|
+
}`;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
531
604
|
});
|
|
532
605
|
|
|
533
606
|
// 为没有定义的类型添加基本定义
|
|
534
|
-
allReferencedTypes.forEach(typeName => {
|
|
535
|
-
if (!
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
607
|
+
allReferencedTypes.forEach(typeName => {
|
|
608
|
+
if (!isValidTypeIdentifier(typeName)) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (!typeDefinitions[typeName]) {
|
|
612
|
+
typeDefinitions[typeName] = `export interface ${typeName} {
|
|
613
|
+
[key: string]: any;
|
|
614
|
+
}`;
|
|
615
|
+
}
|
|
616
|
+
});
|
|
541
617
|
|
|
542
618
|
const basicTypesContent = `/* eslint-disable */
|
|
543
619
|
/**
|
|
@@ -611,4 +687,4 @@ ${Object.values(typeDefinitions).join('\n\n')}
|
|
|
611
687
|
} catch (error) {
|
|
612
688
|
console.error(chalk.red('An error occurred during build process:'), error);
|
|
613
689
|
}
|
|
614
|
-
};
|
|
690
|
+
};
|
package/src/core/parser.ts
CHANGED
|
@@ -16,6 +16,11 @@ export interface Endpoint {
|
|
|
16
16
|
hasData: boolean;
|
|
17
17
|
contentType?: string;
|
|
18
18
|
jsdocParams: Array<{ name: string; type?: string; description?: string; required?: boolean }>;
|
|
19
|
+
// Separate jsdoc params for params and data to avoid merging
|
|
20
|
+
paramsJsdocParams: Array<{ name: string; type?: string; description?: string; required?: boolean }>;
|
|
21
|
+
dataJsdocParams: Array<{ name: string; type?: string; description?: string; required?: boolean }>;
|
|
22
|
+
// Jsdoc params for response type
|
|
23
|
+
responseJsdocParams: Array<{ name: string; type?: string; description?: string; required?: boolean }>;
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
export interface Module {
|
|
@@ -290,6 +295,9 @@ export const processApi = (api: OpenAPI.Document, excludePaths: string[] = [], i
|
|
|
290
295
|
let requestBodyTypeName: string | undefined = undefined;
|
|
291
296
|
let contentType: string | undefined = undefined;
|
|
292
297
|
const jsdocParams: Required<Endpoint>['jsdocParams'] = [];
|
|
298
|
+
const paramsJsdocParams: Required<Endpoint>['paramsJsdocParams'] = [];
|
|
299
|
+
const dataJsdocParams: Required<Endpoint>['dataJsdocParams'] = [];
|
|
300
|
+
const responseJsdocParams: Required<Endpoint>['responseJsdocParams'] = [];
|
|
293
301
|
|
|
294
302
|
// --- FormData Detection ---
|
|
295
303
|
let isFormData = false;
|
|
@@ -378,14 +386,18 @@ export const processApi = (api: OpenAPI.Document, excludePaths: string[] = [], i
|
|
|
378
386
|
requestBodyTypeName = typeName;
|
|
379
387
|
modules[moduleName].schemas[typeName] = formDataSchema;
|
|
380
388
|
referencedTypes.push(typeName);
|
|
381
|
-
|
|
389
|
+
const formDataJsdoc = extractJsdocParamsFromSchema(formDataSchema, allSchemas);
|
|
390
|
+
jsdocParams.push(...formDataJsdoc);
|
|
391
|
+
dataJsdocParams.push(...formDataJsdoc);
|
|
382
392
|
}
|
|
383
393
|
|
|
384
394
|
if (Object.keys(paramsSchema.properties || {}).length > 0) {
|
|
385
395
|
requestParamsTypeName = `${functionName.charAt(0).toUpperCase() + functionName.slice(1)}Params`;
|
|
386
396
|
modules[moduleName].schemas[requestParamsTypeName] = paramsSchema;
|
|
387
397
|
referencedTypes.push(requestParamsTypeName);
|
|
388
|
-
|
|
398
|
+
const paramsJsdoc = extractJsdocParamsFromSchema(paramsSchema, allSchemas);
|
|
399
|
+
jsdocParams.push(...paramsJsdoc);
|
|
400
|
+
paramsJsdocParams.push(...paramsJsdoc);
|
|
389
401
|
}
|
|
390
402
|
}
|
|
391
403
|
|
|
@@ -400,7 +412,9 @@ export const processApi = (api: OpenAPI.Document, excludePaths: string[] = [], i
|
|
|
400
412
|
requestBodyTypeName = name;
|
|
401
413
|
addSchemaWithDependencies(name, modules[moduleName], allSchemas);
|
|
402
414
|
referencedTypes.push(name);
|
|
403
|
-
|
|
415
|
+
const bodyJsdoc = extractJsdocParamsFromSchema(jsonContent.schema, allSchemas);
|
|
416
|
+
jsdocParams.push(...bodyJsdoc);
|
|
417
|
+
dataJsdocParams.push(...bodyJsdoc);
|
|
404
418
|
} else {
|
|
405
419
|
// Handle inline schema (including arrays)
|
|
406
420
|
const schema = jsonContent.schema as OpenAPIV3.SchemaObject;
|
|
@@ -416,10 +430,14 @@ export const processApi = (api: OpenAPI.Document, excludePaths: string[] = [], i
|
|
|
416
430
|
const itemSchemaName = getSchemaName(schema.items.$ref);
|
|
417
431
|
addSchemaWithDependencies(itemSchemaName, modules[moduleName], allSchemas);
|
|
418
432
|
} else {
|
|
419
|
-
|
|
433
|
+
const arrayJsdoc = extractJsdocParamsFromSchema(schema.items as OpenAPIV3.SchemaObject, allSchemas);
|
|
434
|
+
jsdocParams.push(...arrayJsdoc);
|
|
435
|
+
dataJsdocParams.push(...arrayJsdoc);
|
|
420
436
|
}
|
|
421
437
|
} else if (schema.type === 'object') {
|
|
422
|
-
|
|
438
|
+
const objJsdoc = extractJsdocParamsFromSchema(schema, allSchemas);
|
|
439
|
+
jsdocParams.push(...objJsdoc);
|
|
440
|
+
dataJsdocParams.push(...objJsdoc);
|
|
423
441
|
}
|
|
424
442
|
}
|
|
425
443
|
}
|
|
@@ -431,8 +449,9 @@ export const processApi = (api: OpenAPI.Document, excludePaths: string[] = [], i
|
|
|
431
449
|
const typeName = `${functionName.charAt(0).toUpperCase() + functionName.slice(1)}Data`;
|
|
432
450
|
requestBodyTypeName = typeName;
|
|
433
451
|
modules[moduleName].schemas[typeName] = formDataContent.schema as OpenAPIV3.SchemaObject;
|
|
434
|
-
|
|
435
|
-
|
|
452
|
+
referencedTypes.push(typeName);
|
|
453
|
+
const formJsdoc = extractJsdocParamsFromSchema(formDataContent.schema, allSchemas);
|
|
454
|
+
dataJsdocParams.push(...formJsdoc);
|
|
436
455
|
}
|
|
437
456
|
}
|
|
438
457
|
|
|
@@ -448,8 +467,34 @@ export const processApi = (api: OpenAPI.Document, excludePaths: string[] = [], i
|
|
|
448
467
|
responseTypeName = name;
|
|
449
468
|
addSchemaWithDependencies(name, modules[moduleName], allSchemas);
|
|
450
469
|
referencedTypes.push(name);
|
|
451
|
-
|
|
452
|
-
|
|
470
|
+
responseJsdocParams.push(...extractJsdocParamsFromSchema(schema, allSchemas));
|
|
471
|
+
} else if (schema && !isReferenceObject(schema)) {
|
|
472
|
+
const inlineSchema = schema as OpenAPIV3.SchemaObject;
|
|
473
|
+
if (inlineSchema.type === 'object' || inlineSchema.properties) {
|
|
474
|
+
// Generate a named type for inline response schema
|
|
475
|
+
const typeName = `${functionName.charAt(0).toUpperCase() + functionName.slice(1)}Response`;
|
|
476
|
+
responseTypeName = typeName;
|
|
477
|
+
modules[moduleName].schemas[typeName] = inlineSchema;
|
|
478
|
+
referencedTypes.push(typeName);
|
|
479
|
+
responseJsdocParams.push(...extractJsdocParamsFromSchema(inlineSchema, allSchemas));
|
|
480
|
+
} else if (inlineSchema.type === 'array' && inlineSchema.items) {
|
|
481
|
+
if (isReferenceObject(inlineSchema.items)) {
|
|
482
|
+
const itemSchemaName = getSchemaName(inlineSchema.items.$ref);
|
|
483
|
+
// Prefer direct `ItemType[]` for array responses to avoid unnecessary alias schema compilation.
|
|
484
|
+
responseTypeName = `${itemSchemaName}[]`;
|
|
485
|
+
referencedTypes.push(itemSchemaName);
|
|
486
|
+
addSchemaWithDependencies(itemSchemaName, modules[moduleName], allSchemas);
|
|
487
|
+
responseJsdocParams.push(...extractJsdocParamsFromSchema(inlineSchema.items, allSchemas));
|
|
488
|
+
} else {
|
|
489
|
+
const typeName = `${functionName.charAt(0).toUpperCase() + functionName.slice(1)}Response`;
|
|
490
|
+
responseTypeName = typeName;
|
|
491
|
+
modules[moduleName].schemas[typeName] = inlineSchema;
|
|
492
|
+
referencedTypes.push(typeName);
|
|
493
|
+
responseJsdocParams.push(...extractJsdocParamsFromSchema(inlineSchema, allSchemas));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
453
498
|
|
|
454
499
|
// Convert path to template literal string for path parameters
|
|
455
500
|
const urlTemplate = path.replace(/\{(\w+)\}/g, '${params.$1}');
|
|
@@ -467,6 +512,9 @@ export const processApi = (api: OpenAPI.Document, excludePaths: string[] = [], i
|
|
|
467
512
|
hasData: !!requestBodyTypeName,
|
|
468
513
|
contentType,
|
|
469
514
|
jsdocParams,
|
|
515
|
+
paramsJsdocParams,
|
|
516
|
+
dataJsdocParams,
|
|
517
|
+
responseJsdocParams,
|
|
470
518
|
};
|
|
471
519
|
|
|
472
520
|
modules[moduleName].endpoints.push(endpoint);
|
|
@@ -474,4 +522,4 @@ export const processApi = (api: OpenAPI.Document, excludePaths: string[] = [], i
|
|
|
474
522
|
}
|
|
475
523
|
|
|
476
524
|
return modules;
|
|
477
|
-
};
|
|
525
|
+
};
|
package/src/templates/api.hbs
CHANGED
|
@@ -10,12 +10,7 @@ export const {{functionName}} = ({{#if hasParams}}params: {{requestParamsTypeNam
|
|
|
10
10
|
return http.request<{{responseTypeName}}>({
|
|
11
11
|
url: `{{path}}`,
|
|
12
12
|
method: '{{method}}',
|
|
13
|
-
{{#if hasParams}}
|
|
14
|
-
params,
|
|
15
|
-
{{/if}}
|
|
16
|
-
{{#if hasData}}
|
|
17
|
-
data,
|
|
18
|
-
{{/if}}
|
|
13
|
+
{{#if hasParams}}{{#unless (eq method 'put')}}{{#unless (eq method 'post')}}params,{{/unless}}{{/unless}}{{/if}}{{#if hasData}}data,{{/if}}
|
|
19
14
|
{{#if contentType}}
|
|
20
15
|
headers: { 'Content-Type': '{{contentType}}' },
|
|
21
16
|
{{/if}}
|