czh-api 1.0.3 → 1.0.5
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 +30 -5
- package/README.md +4 -11
- package/dist/commands/build.js +320 -107
- package/dist/core/parser.js +134 -16
- package/dist/templates/api.hbs +1 -6
- package/package.json +1 -1
- package/src/commands/build.ts +427 -159
- package/src/core/parser.ts +170 -34
- package/src/templates/api.hbs +1 -6
package/src/commands/build.ts
CHANGED
|
@@ -18,8 +18,8 @@ import { compile } from 'json-schema-to-typescript';
|
|
|
18
18
|
import { Converter } from '@apiture/openapi-down-convert';
|
|
19
19
|
|
|
20
20
|
// 统一的类型转换函数
|
|
21
|
-
function convertOpenApiTypeToTypeScript(openApiType: string | undefined): string {
|
|
22
|
-
if (!openApiType) return 'any';
|
|
21
|
+
function convertOpenApiTypeToTypeScript(openApiType: string | undefined): string {
|
|
22
|
+
if (!openApiType) return 'any';
|
|
23
23
|
|
|
24
24
|
switch (openApiType) {
|
|
25
25
|
case 'integer':
|
|
@@ -32,91 +32,306 @@ function convertOpenApiTypeToTypeScript(openApiType: string | undefined): string
|
|
|
32
32
|
return 'boolean';
|
|
33
33
|
case 'string':
|
|
34
34
|
return 'string';
|
|
35
|
-
case 'number':
|
|
36
|
-
return 'number';
|
|
37
|
-
default:
|
|
38
|
-
return
|
|
39
|
-
}
|
|
40
|
-
}
|
|
35
|
+
case 'number':
|
|
36
|
+
return 'number';
|
|
37
|
+
default:
|
|
38
|
+
return openApiType;
|
|
39
|
+
}
|
|
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
|
+
const TS_BUILTIN_TYPE_NAMES = new Set([
|
|
89
|
+
'string',
|
|
90
|
+
'number',
|
|
91
|
+
'boolean',
|
|
92
|
+
'null',
|
|
93
|
+
'undefined',
|
|
94
|
+
'void',
|
|
95
|
+
'any',
|
|
96
|
+
'unknown',
|
|
97
|
+
'never',
|
|
98
|
+
'object',
|
|
99
|
+
'Record',
|
|
100
|
+
'Array',
|
|
101
|
+
'Date',
|
|
102
|
+
'Promise',
|
|
103
|
+
'true',
|
|
104
|
+
'false',
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
function extractTypeIdentifiers(typeExpr: string | undefined): string[] {
|
|
108
|
+
if (!typeExpr) return [];
|
|
109
|
+
const matches = typeExpr.match(/[A-Za-z_$][A-Za-z0-9_$]*/g) || [];
|
|
110
|
+
return [...new Set(matches.filter(name => !TS_BUILTIN_TYPE_NAMES.has(name)))];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getSchemaNameFromRef(ref: string | undefined): string {
|
|
114
|
+
if (!ref) return '';
|
|
115
|
+
return ref.split('/').pop() || '';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolveSchemaTypeFromModuleSchemas(
|
|
119
|
+
schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined,
|
|
120
|
+
moduleSchemas: { [typeName: string]: OpenAPIV3.SchemaObject },
|
|
121
|
+
visitedRefs: Set<string> = new Set()
|
|
122
|
+
): string {
|
|
123
|
+
if (!schema) return 'any';
|
|
124
|
+
|
|
125
|
+
if ((schema as OpenAPIV3.ReferenceObject).$ref) {
|
|
126
|
+
const refName = getSchemaNameFromRef((schema as OpenAPIV3.ReferenceObject).$ref);
|
|
127
|
+
return refName || 'any';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const schemaObj = schema as OpenAPIV3.SchemaObject;
|
|
131
|
+
|
|
132
|
+
if (schemaObj.anyOf && Array.isArray(schemaObj.anyOf) && schemaObj.anyOf.length > 0) {
|
|
133
|
+
const types = schemaObj.anyOf
|
|
134
|
+
.map(item => resolveSchemaTypeFromModuleSchemas(item as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, moduleSchemas, visitedRefs))
|
|
135
|
+
.filter(Boolean);
|
|
136
|
+
if (types.length > 0) {
|
|
137
|
+
return [...new Set(types)].join(' | ');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (schemaObj.oneOf && Array.isArray(schemaObj.oneOf) && schemaObj.oneOf.length > 0) {
|
|
142
|
+
const types = schemaObj.oneOf
|
|
143
|
+
.map(item => resolveSchemaTypeFromModuleSchemas(item as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, moduleSchemas, visitedRefs))
|
|
144
|
+
.filter(Boolean);
|
|
145
|
+
if (types.length > 0) {
|
|
146
|
+
return [...new Set(types)].join(' | ');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (schemaObj.type === 'array') {
|
|
151
|
+
const itemType = resolveSchemaTypeFromModuleSchemas(
|
|
152
|
+
schemaObj.items as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined,
|
|
153
|
+
moduleSchemas,
|
|
154
|
+
visitedRefs
|
|
155
|
+
);
|
|
156
|
+
return `${itemType || 'any'}[]`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (schemaObj.type === 'object' && schemaObj.additionalProperties) {
|
|
160
|
+
if (schemaObj.additionalProperties === true) {
|
|
161
|
+
return 'Record<string, any>';
|
|
162
|
+
}
|
|
163
|
+
const valueType = resolveSchemaTypeFromModuleSchemas(
|
|
164
|
+
schemaObj.additionalProperties as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
|
|
165
|
+
moduleSchemas,
|
|
166
|
+
visitedRefs
|
|
167
|
+
);
|
|
168
|
+
return `Record<string, ${valueType || 'any'}>`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (schemaObj.type === 'object' && schemaObj.properties && Object.keys(schemaObj.properties).length > 0) {
|
|
172
|
+
const requiredSet = new Set(schemaObj.required || []);
|
|
173
|
+
const inlineFields = Object.entries(schemaObj.properties).map(([key, value]) => {
|
|
174
|
+
const fieldType = resolveSchemaTypeFromModuleSchemas(
|
|
175
|
+
value as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
|
|
176
|
+
moduleSchemas,
|
|
177
|
+
visitedRefs
|
|
178
|
+
);
|
|
179
|
+
const optional = requiredSet.has(key) ? '' : '?';
|
|
180
|
+
return `${key}${optional}: ${fieldType || 'any'}`;
|
|
181
|
+
});
|
|
182
|
+
return `{ ${inlineFields.join('; ')} }`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const baseType = convertOpenApiTypeToTypeScript(schemaObj.type);
|
|
186
|
+
if (!baseType) return 'any';
|
|
187
|
+
return schemaObj.nullable ? `${baseType} | null` : baseType;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function collectSchemaProperties(
|
|
191
|
+
schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined,
|
|
192
|
+
moduleSchemas: { [typeName: string]: OpenAPIV3.SchemaObject },
|
|
193
|
+
visitedRefs: Set<string> = new Set()
|
|
194
|
+
): { properties: Record<string, OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject>; required: Set<string> } {
|
|
195
|
+
const collected: { properties: Record<string, OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject>; required: Set<string> } = {
|
|
196
|
+
properties: {},
|
|
197
|
+
required: new Set<string>(),
|
|
198
|
+
};
|
|
199
|
+
if (!schema) return collected;
|
|
200
|
+
|
|
201
|
+
if ((schema as OpenAPIV3.ReferenceObject).$ref) {
|
|
202
|
+
const ref = (schema as OpenAPIV3.ReferenceObject).$ref;
|
|
203
|
+
if (!visitedRefs.has(ref)) {
|
|
204
|
+
visitedRefs.add(ref);
|
|
205
|
+
const refName = getSchemaNameFromRef(ref);
|
|
206
|
+
const refSchema = moduleSchemas[refName];
|
|
207
|
+
if (refSchema) {
|
|
208
|
+
const nested = collectSchemaProperties(refSchema, moduleSchemas, visitedRefs);
|
|
209
|
+
Object.assign(collected.properties, nested.properties);
|
|
210
|
+
nested.required.forEach(field => collected.required.add(field));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return collected;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const schemaObj = schema as OpenAPIV3.SchemaObject;
|
|
217
|
+
|
|
218
|
+
if (schemaObj.allOf && Array.isArray(schemaObj.allOf)) {
|
|
219
|
+
schemaObj.allOf.forEach(item => {
|
|
220
|
+
const nested = collectSchemaProperties(item as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, moduleSchemas, visitedRefs);
|
|
221
|
+
Object.assign(collected.properties, nested.properties);
|
|
222
|
+
nested.required.forEach(field => collected.required.add(field));
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (schemaObj.properties) {
|
|
227
|
+
Object.entries(schemaObj.properties).forEach(([name, value]) => {
|
|
228
|
+
collected.properties[name] = value as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject;
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
(schemaObj.required || []).forEach(field => collected.required.add(field));
|
|
233
|
+
return collected;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function buildInterfaceDefinitionFromModuleSchemas(
|
|
237
|
+
typeName: string,
|
|
238
|
+
moduleSchemas: { [typeName: string]: OpenAPIV3.SchemaObject }
|
|
239
|
+
): string | null {
|
|
240
|
+
const schema = moduleSchemas[typeName];
|
|
241
|
+
if (!schema) return null;
|
|
242
|
+
|
|
243
|
+
const { properties, required } = collectSchemaProperties(schema, moduleSchemas);
|
|
244
|
+
const entries = Object.entries(properties);
|
|
245
|
+
if (entries.length === 0) return null;
|
|
246
|
+
|
|
247
|
+
const lines = entries.map(([propName, propSchema]) => {
|
|
248
|
+
const optional = required.has(propName) ? '' : '?';
|
|
249
|
+
const propType = resolveSchemaTypeFromModuleSchemas(propSchema, moduleSchemas) || 'any';
|
|
250
|
+
const propDescription = (propSchema as OpenAPIV3.SchemaObject).description || '';
|
|
251
|
+
const comment = propDescription ? ` // ${propDescription}` : '';
|
|
252
|
+
return ` ${propName}${optional}: ${propType};${comment}`;
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return `export interface ${typeName} {\n${lines.join('\n')}\n}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function improveTypeDefinitions(typesContent: string, endpoints: any[]): string {
|
|
44
259
|
let improvedContent = typesContent;
|
|
45
260
|
|
|
46
261
|
// 收集所有需要改进的类型定义
|
|
47
262
|
const typeImprovements: { [typeName: string]: string } = {};
|
|
263
|
+
|
|
264
|
+
// 辅助函数:将 jsdoc params 转换为接口字段
|
|
265
|
+
function buildFieldsFromJsdocParams(jsdocParams: any[]): string {
|
|
266
|
+
return jsdocParams
|
|
267
|
+
.filter((param: any) => param.name && param.type)
|
|
268
|
+
.map((param: any) => {
|
|
269
|
+
const optional = !param.required ? '?' : '';
|
|
270
|
+
let type = param.type;
|
|
271
|
+
|
|
272
|
+
// 处理联合类型,确保正确的 TypeScript 语法
|
|
273
|
+
if (type && type.includes(' | ')) {
|
|
274
|
+
if (type.includes('null')) {
|
|
275
|
+
type = type.replace(/\s*\|\s*null/g, ' | null');
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
type = convertOpenApiTypeToTypeScript(type);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const comment = param.description ? ` // ${param.description}` : '';
|
|
282
|
+
return ` ${param.name}${optional}: ${type};${comment}`;
|
|
283
|
+
})
|
|
284
|
+
.join('\n');
|
|
285
|
+
}
|
|
48
286
|
|
|
49
287
|
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
|
-
}
|
|
288
|
+
// 改进 params 类型 - 使用 paramsJsdocParams(仅包含 path/query 参数)
|
|
289
|
+
const requestParamsTypeName = endpoint.requestParamsTypeName;
|
|
290
|
+
if (requestParamsTypeName && isValidTypeIdentifier(requestParamsTypeName) && endpoint.paramsJsdocParams && endpoint.paramsJsdocParams.length > 0) {
|
|
291
|
+
const paramsFields = buildFieldsFromJsdocParams(endpoint.paramsJsdocParams);
|
|
292
|
+
|
|
293
|
+
if (paramsFields) {
|
|
294
|
+
typeImprovements[requestParamsTypeName] = `export interface ${requestParamsTypeName} {
|
|
295
|
+
${paramsFields}
|
|
296
|
+
}`;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
80
299
|
|
|
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
|
-
}
|
|
300
|
+
// 改进 requestBody 类型 - 使用 dataJsdocParams(仅包含 requestBody 参数)
|
|
301
|
+
const requestBodyTypeName = endpoint.requestBodyTypeName;
|
|
302
|
+
if (requestBodyTypeName && isValidTypeIdentifier(requestBodyTypeName) && endpoint.dataJsdocParams && endpoint.dataJsdocParams.length > 0) {
|
|
303
|
+
const dataFields = buildFieldsFromJsdocParams(endpoint.dataJsdocParams);
|
|
304
|
+
|
|
305
|
+
if (dataFields) {
|
|
306
|
+
typeImprovements[requestBodyTypeName] = `export interface ${requestBodyTypeName} {
|
|
307
|
+
${dataFields}
|
|
308
|
+
}`;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// 改进 response 类型 - 使用 responseJsdocParams(包含响应字段信息)
|
|
313
|
+
const responseTargetType = resolveInterfaceTargetType(endpoint.responseTypeName);
|
|
314
|
+
if (responseTargetType && endpoint.responseTypeName !== 'void' && endpoint.responseJsdocParams && endpoint.responseJsdocParams.length > 0) {
|
|
315
|
+
const responseFields = buildFieldsFromJsdocParams(endpoint.responseJsdocParams);
|
|
316
|
+
|
|
317
|
+
if (responseFields) {
|
|
318
|
+
typeImprovements[responseTargetType] = `export interface ${responseTargetType} {
|
|
319
|
+
${responseFields}
|
|
320
|
+
}`;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
111
323
|
});
|
|
112
324
|
|
|
113
325
|
// 替换现有的类型定义
|
|
114
326
|
Object.entries(typeImprovements).forEach(([typeName, newDefinition]) => {
|
|
115
327
|
// 查找并替换现有的接口定义
|
|
116
|
-
const
|
|
117
|
-
if (
|
|
118
|
-
improvedContent =
|
|
119
|
-
|
|
328
|
+
const blockRange = findInterfaceBlockRange(improvedContent, typeName);
|
|
329
|
+
if (blockRange) {
|
|
330
|
+
improvedContent =
|
|
331
|
+
improvedContent.slice(0, blockRange.start) +
|
|
332
|
+
newDefinition +
|
|
333
|
+
improvedContent.slice(blockRange.end);
|
|
334
|
+
} else {
|
|
120
335
|
// 如果没找到接口定义,添加到末尾
|
|
121
336
|
improvedContent += '\n\n' + newDefinition;
|
|
122
337
|
}
|
|
@@ -126,7 +341,7 @@ ${dataFields}
|
|
|
126
341
|
}
|
|
127
342
|
|
|
128
343
|
// 移除无用的类型别名
|
|
129
|
-
function removeUselessTypeAliases(typesContent: string): string {
|
|
344
|
+
function removeUselessTypeAliases(typesContent: string): string {
|
|
130
345
|
let cleanedContent = typesContent;
|
|
131
346
|
|
|
132
347
|
// 收集要删除的类型别名及其对应的基础类型
|
|
@@ -193,10 +408,42 @@ function removeUselessTypeAliases(typesContent: string): string {
|
|
|
193
408
|
// 移除孤立的类型注释(没有对应类型定义的注释)
|
|
194
409
|
cleanedContent = cleanedContent.replace(/\/\*\*\n \* [^\n]*\n \*\/\n(?!export)/g, '');
|
|
195
410
|
|
|
196
|
-
return cleanedContent;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
411
|
+
return cleanedContent;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function normalizeRef(refValue: string): string {
|
|
415
|
+
const match = refValue.match(/^#\/components\/schemas\/([^/]+)$/);
|
|
416
|
+
if (match) {
|
|
417
|
+
return `#/definitions/${match[1]}`;
|
|
418
|
+
}
|
|
419
|
+
return refValue;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function normalizeSchemaRefs<T>(value: T): T {
|
|
423
|
+
if (Array.isArray(value)) {
|
|
424
|
+
return value.map(item => normalizeSchemaRefs(item)) as T;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (value && typeof value === 'object') {
|
|
428
|
+
const obj = value as Record<string, unknown>;
|
|
429
|
+
const normalized: Record<string, unknown> = {};
|
|
430
|
+
|
|
431
|
+
for (const key in obj) {
|
|
432
|
+
const currentValue = obj[key];
|
|
433
|
+
if (key === '$ref' && typeof currentValue === 'string') {
|
|
434
|
+
normalized[key] = normalizeRef(currentValue);
|
|
435
|
+
} else {
|
|
436
|
+
normalized[key] = normalizeSchemaRefs(currentValue);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return normalized as T;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return value;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
interface IConfig {
|
|
200
447
|
url: string;
|
|
201
448
|
outputDir: string;
|
|
202
449
|
httpClientPath: string;
|
|
@@ -317,13 +564,14 @@ export const handleBuild = async () => {
|
|
|
317
564
|
if (Object.keys(module.schemas).length > 0) {
|
|
318
565
|
console.log(chalk.blue(`正在为模块 "${moduleName}" 生成类型文件,包含 ${Object.keys(module.schemas).length} 个 schema`));
|
|
319
566
|
try {
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
567
|
+
const normalizedModuleSchemas = normalizeSchemaRefs(module.schemas);
|
|
568
|
+
const schemasWithTitles: { [key: string]: OpenAPIV3.SchemaObject } = {};
|
|
569
|
+
for (const schemaName in normalizedModuleSchemas) {
|
|
570
|
+
schemasWithTitles[schemaName] = {
|
|
571
|
+
...normalizedModuleSchemas[schemaName],
|
|
572
|
+
title: schemaName,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
327
575
|
|
|
328
576
|
const rootSchemaForCompiler = {
|
|
329
577
|
title: 'schemas',
|
|
@@ -374,9 +622,10 @@ export const handleBuild = async () => {
|
|
|
374
622
|
|
|
375
623
|
// 尝试逐个验证 schema,过滤掉有问题的
|
|
376
624
|
const validSchemas: { [key: string]: OpenAPIV3.SchemaObject } = {};
|
|
377
|
-
const invalidSchemas: string[] = [];
|
|
625
|
+
const invalidSchemas: string[] = [];
|
|
626
|
+
const normalizedModuleSchemas = normalizeSchemaRefs(module.schemas);
|
|
378
627
|
|
|
379
|
-
for (const schemaName in
|
|
628
|
+
for (const schemaName in normalizedModuleSchemas) {
|
|
380
629
|
try {
|
|
381
630
|
// 尝试单独编译每个 schema
|
|
382
631
|
const testSchema = {
|
|
@@ -384,12 +633,12 @@ export const handleBuild = async () => {
|
|
|
384
633
|
type: 'object',
|
|
385
634
|
properties: {},
|
|
386
635
|
additionalProperties: false,
|
|
387
|
-
definitions: { [schemaName]:
|
|
388
|
-
components: { schemas: { [schemaName]:
|
|
636
|
+
definitions: { [schemaName]: normalizedModuleSchemas[schemaName] },
|
|
637
|
+
components: { schemas: { [schemaName]: normalizedModuleSchemas[schemaName] } }
|
|
389
638
|
};
|
|
390
639
|
await compile(testSchema as any, 'test', { unreachableDefinitions: true });
|
|
391
640
|
validSchemas[schemaName] = {
|
|
392
|
-
...
|
|
641
|
+
...normalizedModuleSchemas[schemaName],
|
|
393
642
|
title: schemaName
|
|
394
643
|
};
|
|
395
644
|
} catch (schemaError) {
|
|
@@ -463,81 +712,100 @@ export const handleBuild = async () => {
|
|
|
463
712
|
console.log(chalk.blue(`为模块 "${moduleName}" 生成基于 JSDoc 的类型定义...`));
|
|
464
713
|
|
|
465
714
|
// 创建类型定义映射
|
|
466
|
-
const typeDefinitions: { [typeName: string]: string } = {};
|
|
715
|
+
const typeDefinitions: { [typeName: string]: string } = {};
|
|
716
|
+
const nestedReferencedTypes = new Set<string>();
|
|
467
717
|
|
|
468
718
|
// 从 endpoints 中提取类型定义
|
|
469
719
|
module.endpoints.forEach(endpoint => {
|
|
470
|
-
//
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
.
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
}
|
|
492
|
-
.
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
720
|
+
// 辅助函数:将 jsdoc params 转换为字段
|
|
721
|
+
const buildFields = (params: any[]) => params
|
|
722
|
+
.filter((param: any) => param.name && param.type)
|
|
723
|
+
.map((param: any) => {
|
|
724
|
+
const optional = !param.required ? '?' : '';
|
|
725
|
+
let type = param.type;
|
|
726
|
+
|
|
727
|
+
if (type && type.includes(' | ')) {
|
|
728
|
+
if (type.includes('null')) {
|
|
729
|
+
type = type.replace(/\s*\|\s*null/g, ' | null');
|
|
730
|
+
}
|
|
731
|
+
} else {
|
|
732
|
+
type = convertOpenApiTypeToTypeScript(type);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
extractTypeIdentifiers(type).forEach(typeName => {
|
|
736
|
+
if (isValidTypeIdentifier(typeName)) {
|
|
737
|
+
nestedReferencedTypes.add(typeName);
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
const comment = param.description ? ` // ${param.description}` : '';
|
|
742
|
+
return ` ${param.name}${optional}: ${type};${comment}`;
|
|
743
|
+
})
|
|
744
|
+
.join('\n');
|
|
745
|
+
|
|
746
|
+
// 处理 params 类型 - 使用 paramsJsdocParams
|
|
747
|
+
const requestParamsTypeName = endpoint.requestParamsTypeName;
|
|
748
|
+
if (requestParamsTypeName && isValidTypeIdentifier(requestParamsTypeName) && endpoint.paramsJsdocParams && endpoint.paramsJsdocParams.length > 0) {
|
|
749
|
+
const paramsFields = buildFields(endpoint.paramsJsdocParams);
|
|
750
|
+
|
|
751
|
+
if (paramsFields) {
|
|
752
|
+
typeDefinitions[requestParamsTypeName] = `export interface ${requestParamsTypeName} {
|
|
753
|
+
${paramsFields}
|
|
754
|
+
}`;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
500
757
|
|
|
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
|
-
}
|
|
758
|
+
// 处理 data 类型 (requestBody) - 使用 dataJsdocParams
|
|
759
|
+
const requestBodyTypeName = endpoint.requestBodyTypeName;
|
|
760
|
+
if (requestBodyTypeName && isValidTypeIdentifier(requestBodyTypeName) && endpoint.dataJsdocParams && endpoint.dataJsdocParams.length > 0) {
|
|
761
|
+
const dataFields = buildFields(endpoint.dataJsdocParams);
|
|
762
|
+
|
|
763
|
+
if (dataFields) {
|
|
764
|
+
typeDefinitions[requestBodyTypeName] = `export interface ${requestBodyTypeName} {
|
|
765
|
+
${dataFields}
|
|
766
|
+
}`;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// 处理 response 类型 - 使用 responseJsdocParams
|
|
771
|
+
const responseTargetType = resolveInterfaceTargetType(endpoint.responseTypeName);
|
|
772
|
+
if (responseTargetType && endpoint.responseTypeName !== 'void' && endpoint.responseJsdocParams && endpoint.responseJsdocParams.length > 0) {
|
|
773
|
+
const responseFields = buildFields(endpoint.responseJsdocParams);
|
|
774
|
+
|
|
775
|
+
if (responseFields) {
|
|
776
|
+
typeDefinitions[responseTargetType] = `export interface ${responseTargetType} {
|
|
777
|
+
${responseFields}
|
|
778
|
+
}`;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
531
781
|
});
|
|
532
782
|
|
|
533
783
|
// 为没有定义的类型添加基本定义
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
784
|
+
const fallbackReferencedTypes = [
|
|
785
|
+
...new Set([
|
|
786
|
+
...allReferencedTypes,
|
|
787
|
+
...Object.keys(module.schemas || {}),
|
|
788
|
+
...nestedReferencedTypes,
|
|
789
|
+
])
|
|
790
|
+
];
|
|
791
|
+
fallbackReferencedTypes.forEach(typeName => {
|
|
792
|
+
if (!isValidTypeIdentifier(typeName)) {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
if (!typeDefinitions[typeName]) {
|
|
796
|
+
const schemaBasedDefinition = buildInterfaceDefinitionFromModuleSchemas(
|
|
797
|
+
typeName,
|
|
798
|
+
module.schemas as { [typeName: string]: OpenAPIV3.SchemaObject }
|
|
799
|
+
);
|
|
800
|
+
if (schemaBasedDefinition) {
|
|
801
|
+
typeDefinitions[typeName] = schemaBasedDefinition;
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
typeDefinitions[typeName] = `export interface ${typeName} {
|
|
805
|
+
[key: string]: any;
|
|
806
|
+
}`;
|
|
807
|
+
}
|
|
808
|
+
});
|
|
541
809
|
|
|
542
810
|
const basicTypesContent = `/* eslint-disable */
|
|
543
811
|
/**
|
|
@@ -611,4 +879,4 @@ ${Object.values(typeDefinitions).join('\n\n')}
|
|
|
611
879
|
} catch (error) {
|
|
612
880
|
console.error(chalk.red('An error occurred during build process:'), error);
|
|
613
881
|
}
|
|
614
|
-
};
|
|
882
|
+
};
|