czh-api 1.0.2 → 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 +43 -1
- package/README.md +110 -1
- package/dist/commands/build.js +521 -44
- package/dist/core/parser.js +178 -29
- package/dist/templates/api.hbs +1 -6
- package/package.json +2 -1
- package/src/commands/build.ts +594 -56
- package/src/core/parser.ts +186 -31
- package/src/templates/api.hbs +1 -6
package/src/commands/build.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @Author: czh
|
|
4
4
|
* @Date: 2025-07-02 10:39:30
|
|
5
5
|
* @LastEditors: Czh
|
|
6
|
-
* @LastEditTime:
|
|
6
|
+
* @LastEditTime: 2026-03-17 09:18:39
|
|
7
7
|
*/
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import path from 'path';
|
|
@@ -15,8 +15,265 @@ import Handlebars from 'handlebars';
|
|
|
15
15
|
import { rimraf } from 'rimraf';
|
|
16
16
|
import axios from 'axios';
|
|
17
17
|
import { compile } from 'json-schema-to-typescript';
|
|
18
|
+
import { Converter } from '@apiture/openapi-down-convert';
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
// 统一的类型转换函数
|
|
21
|
+
function convertOpenApiTypeToTypeScript(openApiType: string | undefined): string {
|
|
22
|
+
if (!openApiType) return 'any';
|
|
23
|
+
|
|
24
|
+
switch (openApiType) {
|
|
25
|
+
case 'integer':
|
|
26
|
+
return 'number';
|
|
27
|
+
case 'object':
|
|
28
|
+
return 'any';
|
|
29
|
+
case 'array':
|
|
30
|
+
return 'any[]';
|
|
31
|
+
case 'boolean':
|
|
32
|
+
return 'boolean';
|
|
33
|
+
case 'string':
|
|
34
|
+
return 'string';
|
|
35
|
+
case 'number':
|
|
36
|
+
return 'number';
|
|
37
|
+
default:
|
|
38
|
+
return 'any';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
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
|
+
function improveTypeDefinitions(typesContent: string, endpoints: any[]): string {
|
|
89
|
+
let improvedContent = typesContent;
|
|
90
|
+
|
|
91
|
+
// 收集所有需要改进的类型定义
|
|
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
|
+
}
|
|
116
|
+
|
|
117
|
+
endpoints.forEach(endpoint => {
|
|
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
|
+
}
|
|
129
|
+
|
|
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
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// 替换现有的类型定义
|
|
156
|
+
Object.entries(typeImprovements).forEach(([typeName, newDefinition]) => {
|
|
157
|
+
// 查找并替换现有的接口定义
|
|
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 {
|
|
165
|
+
// 如果没找到接口定义,添加到末尾
|
|
166
|
+
improvedContent += '\n\n' + newDefinition;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return improvedContent;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 移除无用的类型别名
|
|
174
|
+
function removeUselessTypeAliases(typesContent: string): string {
|
|
175
|
+
let cleanedContent = typesContent;
|
|
176
|
+
|
|
177
|
+
// 收集要删除的类型别名及其对应的基础类型
|
|
178
|
+
const typeAliasMap: { [aliasName: string]: string } = {};
|
|
179
|
+
|
|
180
|
+
// 匹配简单的类型别名 (export type Name = string;)
|
|
181
|
+
const simpleTypeAliasRegex = /export type (\w+\d*) = (string|number|boolean)(\s*\|\s*null)?;?\n?/g;
|
|
182
|
+
let match;
|
|
183
|
+
while ((match = simpleTypeAliasRegex.exec(cleanedContent)) !== null) {
|
|
184
|
+
const aliasName = match[1];
|
|
185
|
+
const baseType = match[2] + (match[3] || '');
|
|
186
|
+
typeAliasMap[aliasName] = baseType;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 匹配带注释的类型别名
|
|
190
|
+
const commentedTypeAliasRegex = /\/\*\*[^*]*\*\/\nexport type (\w+\d*) = (string|number|boolean)(\s*\|\s*null)?;?\n?/g;
|
|
191
|
+
while ((match = commentedTypeAliasRegex.exec(cleanedContent)) !== null) {
|
|
192
|
+
const aliasName = match[1];
|
|
193
|
+
const baseType = match[2] + (match[3] || '');
|
|
194
|
+
typeAliasMap[aliasName] = baseType;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 匹配空的 JSON 类型别名 (export type Name = {};)
|
|
198
|
+
const jsonTypeAliasRegex = /export type (\w+) = \{\}(\s*\|\s*null)?;?\n?/g;
|
|
199
|
+
while ((match = jsonTypeAliasRegex.exec(cleanedContent)) !== null) {
|
|
200
|
+
const aliasName = match[1];
|
|
201
|
+
const baseType = 'any' + (match[2] || '');
|
|
202
|
+
typeAliasMap[aliasName] = baseType;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 安全地替换接口中对这些类型别名的引用
|
|
206
|
+
Object.entries(typeAliasMap).forEach(([aliasName, baseType]) => {
|
|
207
|
+
// 创建更精确的正则表达式,确保只替换完整的类型引用
|
|
208
|
+
const escapedAliasName = aliasName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
209
|
+
|
|
210
|
+
// 替换接口字段中的类型引用 (fieldName: AliasName;)
|
|
211
|
+
const fieldTypeRegex = new RegExp(`(\\w+\\??:\\s*)${escapedAliasName}(\\s*;)`, 'g');
|
|
212
|
+
cleanedContent = cleanedContent.replace(fieldTypeRegex, `$1${baseType}$2`);
|
|
213
|
+
|
|
214
|
+
// 替换接口字段中的类型引用,包括注释 (fieldName: AliasName; // comment)
|
|
215
|
+
const fieldTypeWithCommentRegex = new RegExp(`(\\w+\\??:\\s*)${escapedAliasName}(\\s*;\\s*//[^\\n]*)`, 'g');
|
|
216
|
+
cleanedContent = cleanedContent.replace(fieldTypeWithCommentRegex, `$1${baseType}$2`);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// 移除类型别名定义(按行移除,避免破坏其他内容)
|
|
220
|
+
const lines = cleanedContent.split('\n');
|
|
221
|
+
const filteredLines = lines.filter(line => {
|
|
222
|
+
// 移除简单类型别名
|
|
223
|
+
if (/^export type \w+\d* = (string|number|boolean)(\s*\|\s*null)?;?\s*$/.test(line)) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
// 移除空对象类型别名
|
|
227
|
+
if (/^export type \w+ = \{\}(\s*\|\s*null)?;?\s*$/.test(line)) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
return true;
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
cleanedContent = filteredLines.join('\n');
|
|
234
|
+
|
|
235
|
+
// 清理多余的空行和孤立的注释
|
|
236
|
+
cleanedContent = cleanedContent.replace(/\n{3,}/g, '\n\n');
|
|
237
|
+
|
|
238
|
+
// 移除孤立的类型注释(没有对应类型定义的注释)
|
|
239
|
+
cleanedContent = cleanedContent.replace(/\/\*\*\n \* [^\n]*\n \*\/\n(?!export)/g, '');
|
|
240
|
+
|
|
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 {
|
|
20
277
|
url: string;
|
|
21
278
|
outputDir: string;
|
|
22
279
|
httpClientPath: string;
|
|
@@ -24,6 +281,7 @@ interface IConfig {
|
|
|
24
281
|
customImports?: string[];
|
|
25
282
|
excludePaths?: string[];
|
|
26
283
|
includePaths?: string[];
|
|
284
|
+
pathPrefixes?: Array<{ path: string; packageName?: string }>;
|
|
27
285
|
}
|
|
28
286
|
|
|
29
287
|
export const handleBuild = async () => {
|
|
@@ -41,11 +299,58 @@ export const handleBuild = async () => {
|
|
|
41
299
|
// Fetch and parse Swagger/OpenAPI document
|
|
42
300
|
console.log(chalk.blue(`正在从 ${config.url} 获取 API 文档...`));
|
|
43
301
|
const response = await axios.get(config.url);
|
|
44
|
-
|
|
45
|
-
|
|
302
|
+
let apiDoc = response.data;
|
|
303
|
+
let isConverted = false;
|
|
304
|
+
|
|
305
|
+
// 检查是否为 OpenAPI 3.1.0,如果是则转换为 3.0.x
|
|
306
|
+
if (apiDoc.openapi && apiDoc.openapi.startsWith('3.1')) {
|
|
307
|
+
console.log(chalk.yellow('检测到 OpenAPI 3.1.0 规范,正在转换为 3.0.x...'));
|
|
308
|
+
try {
|
|
309
|
+
// 使用更保守的转换选项
|
|
310
|
+
const converter = new Converter(apiDoc, {
|
|
311
|
+
verbose: false,
|
|
312
|
+
allOfTransform: false, // 不使用 allOf 转换,避免引用问题
|
|
313
|
+
deleteExampleWithId: false,
|
|
314
|
+
convertSchemaComments: false
|
|
315
|
+
});
|
|
316
|
+
apiDoc = converter.convert();
|
|
317
|
+
isConverted = true;
|
|
318
|
+
console.log(chalk.green('OpenAPI 3.1.0 已成功转换为 3.0.x'));
|
|
319
|
+
} catch (convertError: any) {
|
|
320
|
+
console.warn(chalk.yellow('OpenAPI 版本转换失败,尝试直接解析原始文档...'));
|
|
321
|
+
console.warn(chalk.yellow('转换错误:'), convertError?.message || convertError);
|
|
322
|
+
// 如果转换失败,尝试直接使用原始文档
|
|
323
|
+
apiDoc = response.data;
|
|
324
|
+
isConverted = false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
let api;
|
|
329
|
+
try {
|
|
330
|
+
api = await SwaggerParser.bundle(apiDoc);
|
|
331
|
+
console.log(chalk.green('Swagger document fetched and bundled successfully.'));
|
|
332
|
+
} catch (bundleError: any) {
|
|
333
|
+
console.warn(chalk.yellow('SwaggerParser.bundle 失败,尝试直接使用文档...'));
|
|
334
|
+
console.warn(chalk.yellow('Bundle 错误:'), bundleError?.message || bundleError);
|
|
335
|
+
|
|
336
|
+
// 如果 bundle 失败,直接使用转换后的文档
|
|
337
|
+
if (isConverted) {
|
|
338
|
+
console.log(chalk.blue('使用转换后的文档继续处理...'));
|
|
339
|
+
api = apiDoc;
|
|
340
|
+
} else {
|
|
341
|
+
// 如果是 OpenAPI 3.1.0 且之前转换失败,提供更详细的错误信息
|
|
342
|
+
if (apiDoc.openapi && apiDoc.openapi.startsWith('3.1')) {
|
|
343
|
+
console.error(chalk.red('建议解决方案:'));
|
|
344
|
+
console.error(chalk.red('1. 检查 FastAPI 应用是否可以生成 OpenAPI 3.0.x 版本的文档'));
|
|
345
|
+
console.error(chalk.red('2. 或者在 FastAPI 中配置: app = FastAPI(openapi_version="3.0.2")'));
|
|
346
|
+
console.error(chalk.red('3. 或者使用 excludePaths 排除有问题的接口路径'));
|
|
347
|
+
}
|
|
348
|
+
throw bundleError;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
46
351
|
|
|
47
352
|
// Process the API document
|
|
48
|
-
const modules = processApi(api, config.excludePaths || [], config.includePaths || []);
|
|
353
|
+
const modules = processApi(api, config.excludePaths || [], config.includePaths || [], config.pathPrefixes || []);
|
|
49
354
|
console.log(chalk.blue('API processed into modules.'));
|
|
50
355
|
|
|
51
356
|
// Clean output directory
|
|
@@ -78,70 +383,303 @@ export const handleBuild = async () => {
|
|
|
78
383
|
const module = modules[moduleName];
|
|
79
384
|
const moduleDir = path.join(outputDir, moduleName);
|
|
80
385
|
fs.mkdirSync(moduleDir, { recursive: true });
|
|
386
|
+
|
|
387
|
+
// 提取模块的基础名称(用于文件名)
|
|
388
|
+
const moduleBaseName = moduleName.includes('/')
|
|
389
|
+
? moduleName.split('/').pop()
|
|
390
|
+
: moduleName;
|
|
81
391
|
|
|
82
392
|
let typesContent = '';
|
|
83
393
|
// Generate types.ts
|
|
84
394
|
if (Object.keys(module.schemas).length > 0) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
395
|
+
console.log(chalk.blue(`正在为模块 "${moduleName}" 生成类型文件,包含 ${Object.keys(module.schemas).length} 个 schema`));
|
|
396
|
+
try {
|
|
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
|
+
}
|
|
405
|
+
|
|
406
|
+
const rootSchemaForCompiler = {
|
|
407
|
+
title: 'schemas',
|
|
408
|
+
type: 'object',
|
|
409
|
+
properties: {},
|
|
410
|
+
additionalProperties: false,
|
|
411
|
+
definitions: schemasWithTitles,
|
|
412
|
+
components: {
|
|
413
|
+
schemas: schemasWithTitles
|
|
414
|
+
}
|
|
90
415
|
};
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
416
|
+
|
|
417
|
+
console.log(chalk.blue(`开始编译 ${Object.keys(schemasWithTitles).length} 个类型定义...`));
|
|
418
|
+
typesContent = await compile(rootSchemaForCompiler as any, 'schemas', {
|
|
419
|
+
bannerComment: '/* eslint-disable */\n/**\n* This file was automatically generated by czh-api.\n* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,\n* and run czh-api build to regenerate this file.\n*/',
|
|
420
|
+
unreachableDefinitions: true,
|
|
421
|
+
additionalProperties: false,
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
typesContent = typesContent.replace(/export interface \w+ \{\s*\}\n/g, '');
|
|
425
|
+
const refCommentRegex = /\/\*\*\n \* This interface was referenced by `\w+`'s JSON-Schema[\s\S]*?\*\/\n/g;
|
|
426
|
+
typesContent = typesContent.replace(refCommentRegex, '');
|
|
427
|
+
typesContent = typesContent.replace(/:\s*\{\}\[\]/g, ': any[]');
|
|
428
|
+
typesContent = typesContent.replace(/\[k: string\]: \{\}/g, '[k: string]: any');
|
|
429
|
+
const inlineIndexSignatureRegex = /:\s*\{\s*\/\*\*[\s\S]*?\*\/[\s\n\r]*\[k: string\]: any;\s*\};/g;
|
|
430
|
+
typesContent = typesContent.replace(inlineIndexSignatureRegex, ': any;');
|
|
431
|
+
typesContent = typesContent.replace(/: \{\}/g, ': any');
|
|
432
|
+
typesContent = typesContent.replace(/\}\n/g, '}\n\n');
|
|
433
|
+
|
|
434
|
+
// 移除无用的类型别名
|
|
435
|
+
typesContent = removeUselessTypeAliases(typesContent);
|
|
436
|
+
|
|
437
|
+
// 改进类型定义:用 JSDoc 信息补充接口定义
|
|
438
|
+
const improvedTypes = improveTypeDefinitions(typesContent, module.endpoints);
|
|
439
|
+
if (improvedTypes !== typesContent) {
|
|
440
|
+
typesContent = improvedTypes;
|
|
441
|
+
console.log(chalk.blue(`✓ 使用 JSDoc 信息改进了类型定义`));
|
|
101
442
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
443
|
+
|
|
444
|
+
fs.writeFileSync(path.join(moduleDir, 'types.ts'), typesContent);
|
|
445
|
+
console.log(chalk.green(`✓ 模块 "${moduleName}" 类型文件生成成功`));
|
|
446
|
+
} catch (typeError: any) {
|
|
447
|
+
// 检查是否是引用错误
|
|
448
|
+
const errorMessage = typeError?.message || typeError;
|
|
449
|
+
if (errorMessage.includes('Missing $ref pointer')) {
|
|
450
|
+
console.warn(chalk.yellow(`警告: 模块 "${moduleName}" 存在 OpenAPI 引用错误,尝试过滤有问题的 schema...`));
|
|
451
|
+
console.warn(chalk.yellow('引用错误:'), errorMessage);
|
|
452
|
+
|
|
453
|
+
// 尝试逐个验证 schema,过滤掉有问题的
|
|
454
|
+
const validSchemas: { [key: string]: OpenAPIV3.SchemaObject } = {};
|
|
455
|
+
const invalidSchemas: string[] = [];
|
|
456
|
+
const normalizedModuleSchemas = normalizeSchemaRefs(module.schemas);
|
|
457
|
+
|
|
458
|
+
for (const schemaName in normalizedModuleSchemas) {
|
|
459
|
+
try {
|
|
460
|
+
// 尝试单独编译每个 schema
|
|
461
|
+
const testSchema = {
|
|
462
|
+
title: 'test',
|
|
463
|
+
type: 'object',
|
|
464
|
+
properties: {},
|
|
465
|
+
additionalProperties: false,
|
|
466
|
+
definitions: { [schemaName]: normalizedModuleSchemas[schemaName] },
|
|
467
|
+
components: { schemas: { [schemaName]: normalizedModuleSchemas[schemaName] } }
|
|
468
|
+
};
|
|
469
|
+
await compile(testSchema as any, 'test', { unreachableDefinitions: true });
|
|
470
|
+
validSchemas[schemaName] = {
|
|
471
|
+
...normalizedModuleSchemas[schemaName],
|
|
472
|
+
title: schemaName
|
|
473
|
+
};
|
|
474
|
+
} catch (schemaError) {
|
|
475
|
+
invalidSchemas.push(schemaName);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (Object.keys(validSchemas).length > 0) {
|
|
480
|
+
console.log(chalk.blue(`找到 ${Object.keys(validSchemas).length} 个有效 schema,${invalidSchemas.length} 个无效 schema`));
|
|
481
|
+
if (invalidSchemas.length > 0) {
|
|
482
|
+
console.warn(chalk.yellow('无效的 schema:'), invalidSchemas.join(', '));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
const rootSchemaForCompiler = {
|
|
487
|
+
title: 'schemas',
|
|
488
|
+
type: 'object',
|
|
489
|
+
properties: {},
|
|
490
|
+
additionalProperties: false,
|
|
491
|
+
definitions: validSchemas,
|
|
492
|
+
components: { schemas: validSchemas }
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
typesContent = await compile(rootSchemaForCompiler as any, 'schemas', {
|
|
496
|
+
bannerComment: '/* eslint-disable */\n/**\n* This file was automatically generated by czh-api.\n* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,\n* and run czh-api build to regenerate this file.\n*/',
|
|
497
|
+
unreachableDefinitions: true,
|
|
498
|
+
additionalProperties: false,
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// 应用相同的后处理
|
|
502
|
+
typesContent = typesContent.replace(/export interface \w+ \{\s*\}\n/g, '');
|
|
503
|
+
const refCommentRegex = /\/\*\*\n \* This interface was referenced by `\w+`'s JSON-Schema[\s\S]*?\*\/\n/g;
|
|
504
|
+
typesContent = typesContent.replace(refCommentRegex, '');
|
|
505
|
+
typesContent = typesContent.replace(/:\s*\{\}\[\]/g, ': any[]');
|
|
506
|
+
typesContent = typesContent.replace(/\[k: string\]: \{\}/g, '[k: string]: any');
|
|
507
|
+
const inlineIndexSignatureRegex = /:\s*\{\s*\/\*\*[\s\S]*?\*\/[\s\n\r]*\[k: string\]: any;\s*\};/g;
|
|
508
|
+
typesContent = typesContent.replace(inlineIndexSignatureRegex, ': any;');
|
|
509
|
+
typesContent = typesContent.replace(/: \{\}/g, ': any');
|
|
510
|
+
typesContent = typesContent.replace(/\}\n/g, '}\n\n');
|
|
511
|
+
|
|
512
|
+
typesContent = removeUselessTypeAliases(typesContent);
|
|
513
|
+
|
|
514
|
+
const improvedTypes = improveTypeDefinitions(typesContent, module.endpoints);
|
|
515
|
+
if (improvedTypes !== typesContent) {
|
|
516
|
+
typesContent = improvedTypes;
|
|
517
|
+
console.log(chalk.blue(`✓ 使用 JSDoc 信息改进了类型定义`));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
fs.writeFileSync(path.join(moduleDir, 'types.ts'), typesContent);
|
|
521
|
+
console.log(chalk.green(`✓ 模块 "${moduleName}" 部分类型文件生成成功 (${Object.keys(validSchemas).length}/${Object.keys(module.schemas).length} 个 schema)`));
|
|
522
|
+
} catch (partialError) {
|
|
523
|
+
console.warn(chalk.yellow(`部分 schema 编译也失败,回退到 JSDoc 类型生成`));
|
|
524
|
+
throw typeError; // 回退到原来的错误处理逻辑
|
|
525
|
+
}
|
|
526
|
+
} else {
|
|
527
|
+
console.warn(chalk.yellow(`所有 schema 都无效,回退到 JSDoc 类型生成`));
|
|
528
|
+
// 继续执行 JSDoc 类型生成逻辑
|
|
529
|
+
}
|
|
530
|
+
} else {
|
|
531
|
+
// 非引用错误,继续执行 JSDoc 类型生成逻辑
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// JSDoc 类型生成逻辑
|
|
535
|
+
console.warn(chalk.yellow(`警告: 模块 "${moduleName}" 的类型生成失败,跳过类型文件生成`));
|
|
536
|
+
console.warn(chalk.yellow('类型生成错误:'), typeError?.message || typeError);
|
|
537
|
+
console.warn(chalk.yellow('Schema 列表:'), Object.keys(module.schemas));
|
|
538
|
+
|
|
539
|
+
// 生成基于 JSDoc 参数的类型文件作为备选方案
|
|
540
|
+
const allReferencedTypes = [...new Set(module.endpoints.flatMap(e => e.referencedTypes))];
|
|
541
|
+
if (allReferencedTypes.length > 0) {
|
|
542
|
+
console.log(chalk.blue(`为模块 "${moduleName}" 生成基于 JSDoc 的类型定义...`));
|
|
543
|
+
|
|
544
|
+
// 创建类型定义映射
|
|
545
|
+
const typeDefinitions: { [typeName: string]: string } = {};
|
|
546
|
+
|
|
547
|
+
// 从 endpoints 中提取类型定义
|
|
548
|
+
module.endpoints.forEach(endpoint => {
|
|
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');
|
|
559
|
+
}
|
|
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
|
+
}
|
|
580
|
+
|
|
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
|
+
}
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
// 为没有定义的类型添加基本定义
|
|
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
|
+
});
|
|
617
|
+
|
|
618
|
+
const basicTypesContent = `/* eslint-disable */
|
|
619
|
+
/**
|
|
620
|
+
* This file was automatically generated by czh-api.
|
|
621
|
+
* Type definitions based on JSDoc parameters (fallback when schema compilation fails)
|
|
622
|
+
*/
|
|
623
|
+
|
|
624
|
+
${Object.values(typeDefinitions).join('\n\n')}
|
|
625
|
+
`;
|
|
626
|
+
fs.writeFileSync(path.join(moduleDir, 'types.ts'), basicTypesContent);
|
|
627
|
+
typesContent = basicTypesContent;
|
|
628
|
+
console.log(chalk.green(`✓ 模块 "${moduleName}" 基于 JSDoc 的类型文件生成成功`));
|
|
629
|
+
} else {
|
|
630
|
+
typesContent = '';
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
} else {
|
|
634
|
+
console.log(chalk.yellow(`模块 "${moduleName}" 没有 schema,跳过类型文件生成`));
|
|
121
635
|
}
|
|
122
636
|
|
|
123
637
|
// Generate API file
|
|
124
638
|
const allReferencedTypes = [...new Set(module.endpoints.flatMap(e => e.referencedTypes))];
|
|
125
639
|
const customImports = config.customImports || [`import http from "${config.httpClientPath}";`];
|
|
126
640
|
|
|
127
|
-
|
|
641
|
+
try {
|
|
642
|
+
let apiFileContent = '';
|
|
128
643
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
644
|
+
if (module.description) {
|
|
645
|
+
apiFileContent += `/**\n * @description ${module.description}\n */\n\n`;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
apiFileContent += `${customImports.join('\n')}\n`;
|
|
649
|
+
|
|
650
|
+
// 只导入存在的类型(包括 interface 和 type)
|
|
651
|
+
const existingTypes = allReferencedTypes.filter(type =>
|
|
652
|
+
typesContent && (
|
|
653
|
+
typesContent.includes(`export interface ${type}`) ||
|
|
654
|
+
typesContent.includes(`export type ${type}`)
|
|
655
|
+
)
|
|
656
|
+
);
|
|
657
|
+
|
|
658
|
+
if (existingTypes.length > 0) {
|
|
659
|
+
apiFileContent += `import type { ${existingTypes.join(', ')} } from './types';\n\n`;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
apiFileContent += module.endpoints.map(endpoint => {
|
|
663
|
+
try {
|
|
664
|
+
return apiTemplate(endpoint);
|
|
665
|
+
} catch (templateError: any) {
|
|
666
|
+
console.warn(chalk.yellow(`警告: 生成函数 "${endpoint.functionName}" 时出错,跳过此函数`));
|
|
667
|
+
console.warn(chalk.yellow('模板错误:'), templateError?.message || templateError);
|
|
668
|
+
return `// 函数 ${endpoint.functionName} 生成失败`;
|
|
669
|
+
}
|
|
670
|
+
}).join('\n\n');
|
|
671
|
+
|
|
672
|
+
fs.writeFileSync(path.join(moduleDir, `${moduleBaseName}.ts`), apiFileContent);
|
|
673
|
+
|
|
674
|
+
// Generate index.ts
|
|
675
|
+
const exportTypes = typesContent ? `\nexport * from './types';` : '';
|
|
676
|
+
fs.writeFileSync(path.join(moduleDir, 'index.ts'), `export * from './${moduleBaseName}';${exportTypes}\n`);
|
|
677
|
+
|
|
678
|
+
console.log(chalk.green(`✓ 模块 "${moduleName}" 生成成功 (${module.endpoints.length} 个接口)`));
|
|
679
|
+
} catch (moduleError: any) {
|
|
680
|
+
console.warn(chalk.yellow(`警告: 模块 "${moduleName}" 生成失败,跳过此模块`));
|
|
681
|
+
console.warn(chalk.yellow('模块生成错误:'), moduleError?.message || moduleError);
|
|
137
682
|
}
|
|
138
|
-
|
|
139
|
-
apiFileContent += module.endpoints.map(endpoint => apiTemplate(endpoint)).join('\n\n');
|
|
140
|
-
fs.writeFileSync(path.join(moduleDir, `${moduleName}.ts`), apiFileContent);
|
|
141
|
-
|
|
142
|
-
// Generate index.ts
|
|
143
|
-
const exportTypes = typesContent ? `\nexport * from './types';` : '';
|
|
144
|
-
fs.writeFileSync(path.join(moduleDir, 'index.ts'), `export * from './${moduleName}';${exportTypes}\n`);
|
|
145
683
|
}
|
|
146
684
|
|
|
147
685
|
console.log(chalk.green.bold('API code generated successfully!'));
|
|
@@ -149,4 +687,4 @@ export const handleBuild = async () => {
|
|
|
149
687
|
} catch (error) {
|
|
150
688
|
console.error(chalk.red('An error occurred during build process:'), error);
|
|
151
689
|
}
|
|
152
|
-
};
|
|
690
|
+
};
|