czh-api 1.0.4 → 1.0.6
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 +22 -0
- package/README.md +4 -11
- package/api.json +39529 -0
- package/dist/commands/build.js +187 -18
- package/dist/core/parser.js +82 -10
- package/package.json +1 -1
- package/src/commands/build.ts +379 -159
- package/src/core/parser.ts +111 -23
package/src/commands/build.ts
CHANGED
|
@@ -35,57 +35,227 @@ function convertOpenApiTypeToTypeScript(openApiType: string | undefined): string
|
|
|
35
35
|
case 'number':
|
|
36
36
|
return 'number';
|
|
37
37
|
default:
|
|
38
|
-
return
|
|
38
|
+
return openApiType;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// 改进类型定义的辅助函数
|
|
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
|
-
|
|
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 {
|
|
89
259
|
let improvedContent = typesContent;
|
|
90
260
|
|
|
91
261
|
// 收集所有需要改进的类型定义
|
|
@@ -116,52 +286,52 @@ function improveTypeDefinitions(typesContent: string, endpoints: any[]): string
|
|
|
116
286
|
|
|
117
287
|
endpoints.forEach(endpoint => {
|
|
118
288
|
// 改进 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
|
-
}
|
|
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
|
+
}
|
|
129
299
|
|
|
130
300
|
// 改进 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
|
-
}
|
|
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
|
+
}
|
|
141
311
|
|
|
142
312
|
// 改进 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
|
-
}
|
|
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
|
+
}
|
|
153
323
|
});
|
|
154
324
|
|
|
155
325
|
// 替换现有的类型定义
|
|
156
326
|
Object.entries(typeImprovements).forEach(([typeName, newDefinition]) => {
|
|
157
327
|
// 查找并替换现有的接口定义
|
|
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 {
|
|
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 {
|
|
165
335
|
// 如果没找到接口定义,添加到末尾
|
|
166
336
|
improvedContent += '\n\n' + newDefinition;
|
|
167
337
|
}
|
|
@@ -171,7 +341,7 @@ ${responseFields}
|
|
|
171
341
|
}
|
|
172
342
|
|
|
173
343
|
// 移除无用的类型别名
|
|
174
|
-
function removeUselessTypeAliases(typesContent: string): string {
|
|
344
|
+
function removeUselessTypeAliases(typesContent: string): string {
|
|
175
345
|
let cleanedContent = typesContent;
|
|
176
346
|
|
|
177
347
|
// 收集要删除的类型别名及其对应的基础类型
|
|
@@ -238,33 +408,57 @@ function removeUselessTypeAliases(typesContent: string): string {
|
|
|
238
408
|
// 移除孤立的类型注释(没有对应类型定义的注释)
|
|
239
409
|
cleanedContent = cleanedContent.replace(/\/\*\*\n \* [^\n]*\n \*\/\n(?!export)/g, '');
|
|
240
410
|
|
|
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;
|
|
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;
|
|
250
444
|
}
|
|
251
445
|
|
|
252
|
-
function
|
|
446
|
+
function stripNestedSchemaTitles<T>(value: T, depth: number = 0): T {
|
|
253
447
|
if (Array.isArray(value)) {
|
|
254
|
-
return value.map(item =>
|
|
448
|
+
return value.map(item => stripNestedSchemaTitles(item, depth + 1)) as T;
|
|
255
449
|
}
|
|
256
450
|
|
|
257
451
|
if (value && typeof value === 'object') {
|
|
258
452
|
const obj = value as Record<string, unknown>;
|
|
259
453
|
const normalized: Record<string, unknown> = {};
|
|
260
454
|
|
|
261
|
-
for (const key
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
normalized[key] = normalizeSchemaRefs(currentValue);
|
|
455
|
+
for (const [key, currentValue] of Object.entries(obj)) {
|
|
456
|
+
// json-schema-to-typescript may generate invalid type aliases from nested title values
|
|
457
|
+
// (e.g. Chinese punctuation or numeric-leading titles), so keep only root-level titles.
|
|
458
|
+
if (key === 'title' && depth > 0) {
|
|
459
|
+
continue;
|
|
267
460
|
}
|
|
461
|
+
normalized[key] = stripNestedSchemaTitles(currentValue, depth + 1);
|
|
268
462
|
}
|
|
269
463
|
|
|
270
464
|
return normalized as T;
|
|
@@ -272,8 +466,8 @@ function normalizeSchemaRefs<T>(value: T): T {
|
|
|
272
466
|
|
|
273
467
|
return value;
|
|
274
468
|
}
|
|
275
|
-
|
|
276
|
-
interface IConfig {
|
|
469
|
+
|
|
470
|
+
interface IConfig {
|
|
277
471
|
url: string;
|
|
278
472
|
outputDir: string;
|
|
279
473
|
httpClientPath: string;
|
|
@@ -394,14 +588,16 @@ export const handleBuild = async () => {
|
|
|
394
588
|
if (Object.keys(module.schemas).length > 0) {
|
|
395
589
|
console.log(chalk.blue(`正在为模块 "${moduleName}" 生成类型文件,包含 ${Object.keys(module.schemas).length} 个 schema`));
|
|
396
590
|
try {
|
|
397
|
-
const normalizedModuleSchemas =
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
591
|
+
const normalizedModuleSchemas = stripNestedSchemaTitles(
|
|
592
|
+
normalizeSchemaRefs(module.schemas)
|
|
593
|
+
);
|
|
594
|
+
const schemasWithTitles: { [key: string]: OpenAPIV3.SchemaObject } = {};
|
|
595
|
+
for (const schemaName in normalizedModuleSchemas) {
|
|
596
|
+
schemasWithTitles[schemaName] = {
|
|
597
|
+
...normalizedModuleSchemas[schemaName],
|
|
598
|
+
title: schemaName,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
405
601
|
|
|
406
602
|
const rootSchemaForCompiler = {
|
|
407
603
|
title: 'schemas',
|
|
@@ -452,10 +648,12 @@ export const handleBuild = async () => {
|
|
|
452
648
|
|
|
453
649
|
// 尝试逐个验证 schema,过滤掉有问题的
|
|
454
650
|
const validSchemas: { [key: string]: OpenAPIV3.SchemaObject } = {};
|
|
455
|
-
const invalidSchemas: string[] = [];
|
|
456
|
-
const normalizedModuleSchemas =
|
|
651
|
+
const invalidSchemas: string[] = [];
|
|
652
|
+
const normalizedModuleSchemas = stripNestedSchemaTitles(
|
|
653
|
+
normalizeSchemaRefs(module.schemas)
|
|
654
|
+
);
|
|
457
655
|
|
|
458
|
-
for (const schemaName in normalizedModuleSchemas) {
|
|
656
|
+
for (const schemaName in normalizedModuleSchemas) {
|
|
459
657
|
try {
|
|
460
658
|
// 尝试单独编译每个 schema
|
|
461
659
|
const testSchema = {
|
|
@@ -463,12 +661,12 @@ export const handleBuild = async () => {
|
|
|
463
661
|
type: 'object',
|
|
464
662
|
properties: {},
|
|
465
663
|
additionalProperties: false,
|
|
466
|
-
definitions: { [schemaName]: normalizedModuleSchemas[schemaName] },
|
|
467
|
-
components: { schemas: { [schemaName]: normalizedModuleSchemas[schemaName] } }
|
|
664
|
+
definitions: { [schemaName]: normalizedModuleSchemas[schemaName] },
|
|
665
|
+
components: { schemas: { [schemaName]: normalizedModuleSchemas[schemaName] } }
|
|
468
666
|
};
|
|
469
667
|
await compile(testSchema as any, 'test', { unreachableDefinitions: true });
|
|
470
668
|
validSchemas[schemaName] = {
|
|
471
|
-
...normalizedModuleSchemas[schemaName],
|
|
669
|
+
...normalizedModuleSchemas[schemaName],
|
|
472
670
|
title: schemaName
|
|
473
671
|
};
|
|
474
672
|
} catch (schemaError) {
|
|
@@ -543,6 +741,7 @@ export const handleBuild = async () => {
|
|
|
543
741
|
|
|
544
742
|
// 创建类型定义映射
|
|
545
743
|
const typeDefinitions: { [typeName: string]: string } = {};
|
|
744
|
+
const nestedReferencedTypes = new Set<string>();
|
|
546
745
|
|
|
547
746
|
// 从 endpoints 中提取类型定义
|
|
548
747
|
module.endpoints.forEach(endpoint => {
|
|
@@ -560,6 +759,12 @@ export const handleBuild = async () => {
|
|
|
560
759
|
} else {
|
|
561
760
|
type = convertOpenApiTypeToTypeScript(type);
|
|
562
761
|
}
|
|
762
|
+
|
|
763
|
+
extractTypeIdentifiers(type).forEach(typeName => {
|
|
764
|
+
if (isValidTypeIdentifier(typeName)) {
|
|
765
|
+
nestedReferencedTypes.add(typeName);
|
|
766
|
+
}
|
|
767
|
+
});
|
|
563
768
|
|
|
564
769
|
const comment = param.description ? ` // ${param.description}` : '';
|
|
565
770
|
return ` ${param.name}${optional}: ${type};${comment}`;
|
|
@@ -567,53 +772,68 @@ export const handleBuild = async () => {
|
|
|
567
772
|
.join('\n');
|
|
568
773
|
|
|
569
774
|
// 处理 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
|
-
}
|
|
775
|
+
const requestParamsTypeName = endpoint.requestParamsTypeName;
|
|
776
|
+
if (requestParamsTypeName && isValidTypeIdentifier(requestParamsTypeName) && endpoint.paramsJsdocParams && endpoint.paramsJsdocParams.length > 0) {
|
|
777
|
+
const paramsFields = buildFields(endpoint.paramsJsdocParams);
|
|
778
|
+
|
|
779
|
+
if (paramsFields) {
|
|
780
|
+
typeDefinitions[requestParamsTypeName] = `export interface ${requestParamsTypeName} {
|
|
781
|
+
${paramsFields}
|
|
782
|
+
}`;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
580
785
|
|
|
581
786
|
// 处理 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
|
-
}
|
|
787
|
+
const requestBodyTypeName = endpoint.requestBodyTypeName;
|
|
788
|
+
if (requestBodyTypeName && isValidTypeIdentifier(requestBodyTypeName) && endpoint.dataJsdocParams && endpoint.dataJsdocParams.length > 0) {
|
|
789
|
+
const dataFields = buildFields(endpoint.dataJsdocParams);
|
|
790
|
+
|
|
791
|
+
if (dataFields) {
|
|
792
|
+
typeDefinitions[requestBodyTypeName] = `export interface ${requestBodyTypeName} {
|
|
793
|
+
${dataFields}
|
|
794
|
+
}`;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
592
797
|
|
|
593
798
|
// 处理 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
|
-
}
|
|
799
|
+
const responseTargetType = resolveInterfaceTargetType(endpoint.responseTypeName);
|
|
800
|
+
if (responseTargetType && endpoint.responseTypeName !== 'void' && endpoint.responseJsdocParams && endpoint.responseJsdocParams.length > 0) {
|
|
801
|
+
const responseFields = buildFields(endpoint.responseJsdocParams);
|
|
802
|
+
|
|
803
|
+
if (responseFields) {
|
|
804
|
+
typeDefinitions[responseTargetType] = `export interface ${responseTargetType} {
|
|
805
|
+
${responseFields}
|
|
806
|
+
}`;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
604
809
|
});
|
|
605
810
|
|
|
606
811
|
// 为没有定义的类型添加基本定义
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
812
|
+
const fallbackReferencedTypes = [
|
|
813
|
+
...new Set([
|
|
814
|
+
...allReferencedTypes,
|
|
815
|
+
...Object.keys(module.schemas || {}),
|
|
816
|
+
...nestedReferencedTypes,
|
|
817
|
+
])
|
|
818
|
+
];
|
|
819
|
+
fallbackReferencedTypes.forEach(typeName => {
|
|
820
|
+
if (!isValidTypeIdentifier(typeName)) {
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
if (!typeDefinitions[typeName]) {
|
|
824
|
+
const schemaBasedDefinition = buildInterfaceDefinitionFromModuleSchemas(
|
|
825
|
+
typeName,
|
|
826
|
+
module.schemas as { [typeName: string]: OpenAPIV3.SchemaObject }
|
|
827
|
+
);
|
|
828
|
+
if (schemaBasedDefinition) {
|
|
829
|
+
typeDefinitions[typeName] = schemaBasedDefinition;
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
typeDefinitions[typeName] = `export interface ${typeName} {
|
|
833
|
+
[key: string]: any;
|
|
834
|
+
}`;
|
|
835
|
+
}
|
|
836
|
+
});
|
|
617
837
|
|
|
618
838
|
const basicTypesContent = `/* eslint-disable */
|
|
619
839
|
/**
|
|
@@ -687,4 +907,4 @@ ${Object.values(typeDefinitions).join('\n\n')}
|
|
|
687
907
|
} catch (error) {
|
|
688
908
|
console.error(chalk.red('An error occurred during build process:'), error);
|
|
689
909
|
}
|
|
690
|
-
};
|
|
910
|
+
};
|