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/dist/core/parser.js
CHANGED
|
@@ -13,22 +13,83 @@ function addSchemaWithDependencies(name, module, allSchemas) {
|
|
|
13
13
|
}
|
|
14
14
|
const schema = allSchemas[name];
|
|
15
15
|
if (!schema) {
|
|
16
|
+
console.warn(`警告: Schema "${name}" 未找到,跳过此引用`);
|
|
16
17
|
return; // Schema not found
|
|
17
18
|
}
|
|
18
19
|
module.schemas[name] = schema;
|
|
19
|
-
// Recursively add dependencies
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
// Recursively add dependencies with error handling
|
|
21
|
+
try {
|
|
22
|
+
JSON.stringify(schema, (key, value) => {
|
|
23
|
+
if (key === '$ref' && typeof value === 'string') {
|
|
24
|
+
const depName = getSchemaName(value);
|
|
25
|
+
if (depName && allSchemas[depName]) {
|
|
26
|
+
addSchemaWithDependencies(depName, module, allSchemas);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
console.warn(`警告: 依赖 Schema "${depName}" 未找到,跳过此引用`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.warn(`警告: 处理 Schema "${name}" 的依赖时出错:`, error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function toCamelCase(str) {
|
|
40
|
+
return str
|
|
41
|
+
.split('/')
|
|
42
|
+
.filter(p => p)
|
|
43
|
+
.map((part, index) => {
|
|
44
|
+
const cleaned = part.replace(/[^a-zA-Z0-9]/g, '');
|
|
45
|
+
if (index === 0) {
|
|
46
|
+
return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
|
|
24
47
|
}
|
|
25
|
-
return
|
|
26
|
-
})
|
|
48
|
+
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
49
|
+
})
|
|
50
|
+
.join('');
|
|
27
51
|
}
|
|
28
|
-
function getModuleName(path) {
|
|
52
|
+
function getModuleName(path, pathPrefixes = []) {
|
|
53
|
+
// 尝试匹配配置的路径前缀
|
|
54
|
+
for (const prefix of pathPrefixes) {
|
|
55
|
+
if (path.startsWith(prefix.path)) {
|
|
56
|
+
// 获取前缀后的路径部分
|
|
57
|
+
const remainingPath = path.substring(prefix.path.length);
|
|
58
|
+
const parts = remainingPath.split('/').filter(p => p && !p.startsWith('{'));
|
|
59
|
+
// 确定包名
|
|
60
|
+
const packageName = prefix.packageName || toCamelCase(prefix.path);
|
|
61
|
+
// 如果有二级路径,使用二级路径作为子模块
|
|
62
|
+
if (parts.length > 0 && parts[0]) {
|
|
63
|
+
return `${packageName}/${parts[0]}`;
|
|
64
|
+
}
|
|
65
|
+
// 如果没有二级路径,直接使用包名
|
|
66
|
+
return packageName;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// 如果没有匹配到配置的前缀,使用默认逻辑
|
|
29
70
|
const parts = path.split('/').filter(p => p && !p.startsWith('{'));
|
|
30
71
|
return parts[0] || 'default';
|
|
31
72
|
}
|
|
73
|
+
function convertOpenApiTypeToTypeScript(openApiType) {
|
|
74
|
+
if (!openApiType)
|
|
75
|
+
return 'any';
|
|
76
|
+
switch (openApiType) {
|
|
77
|
+
case 'integer':
|
|
78
|
+
return 'number';
|
|
79
|
+
case 'object':
|
|
80
|
+
return 'any';
|
|
81
|
+
case 'array':
|
|
82
|
+
return 'any[]';
|
|
83
|
+
case 'boolean':
|
|
84
|
+
return 'boolean';
|
|
85
|
+
case 'string':
|
|
86
|
+
return 'string';
|
|
87
|
+
case 'number':
|
|
88
|
+
return 'number';
|
|
89
|
+
default:
|
|
90
|
+
return 'any';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
32
93
|
function extractJsdocParamsFromSchema(schema, allSchemas) {
|
|
33
94
|
var _a;
|
|
34
95
|
const params = [];
|
|
@@ -45,11 +106,58 @@ function extractJsdocParamsFromSchema(schema, allSchemas) {
|
|
|
45
106
|
if (targetSchema === null || targetSchema === void 0 ? void 0 : targetSchema.properties) {
|
|
46
107
|
for (const propName in targetSchema.properties) {
|
|
47
108
|
const prop = targetSchema.properties[propName];
|
|
48
|
-
|
|
49
|
-
//
|
|
109
|
+
let propType = 'any';
|
|
110
|
+
// 处理 anyOf 数组 (FastAPI 常用的联合类型)
|
|
111
|
+
if (prop.anyOf && Array.isArray(prop.anyOf)) {
|
|
112
|
+
const types = prop.anyOf.map(item => {
|
|
113
|
+
if (isReferenceObject(item)) {
|
|
114
|
+
return getSchemaName(item.$ref);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
const itemSchema = item;
|
|
118
|
+
// 处理 null 类型 (在 OpenAPI 中可能以不同方式表示)
|
|
119
|
+
if (itemSchema.type === 'null' ||
|
|
120
|
+
itemSchema.type === undefined && itemSchema.nullable === true ||
|
|
121
|
+
JSON.stringify(itemSchema) === '{"type":"null"}' ||
|
|
122
|
+
Object.keys(itemSchema).length === 0) {
|
|
123
|
+
return 'null';
|
|
124
|
+
}
|
|
125
|
+
return convertOpenApiTypeToTypeScript(itemSchema.type);
|
|
126
|
+
}
|
|
127
|
+
}).filter(type => type && type !== ''); // 只过滤掉空字符串,保留 any
|
|
128
|
+
if (types.length > 0) {
|
|
129
|
+
propType = types.join(' | ');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// 处理 oneOf 数组
|
|
133
|
+
else if (prop.oneOf && Array.isArray(prop.oneOf)) {
|
|
134
|
+
const types = prop.oneOf.map(item => {
|
|
135
|
+
if (isReferenceObject(item)) {
|
|
136
|
+
return getSchemaName(item.$ref);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
const itemSchema = item;
|
|
140
|
+
// 处理 null 类型 (在 OpenAPI 中可能以不同方式表示)
|
|
141
|
+
if (itemSchema.type === 'null' ||
|
|
142
|
+
itemSchema.type === undefined && itemSchema.nullable === true ||
|
|
143
|
+
JSON.stringify(itemSchema) === '{"type":"null"}' ||
|
|
144
|
+
Object.keys(itemSchema).length === 0) {
|
|
145
|
+
return 'null';
|
|
146
|
+
}
|
|
147
|
+
return convertOpenApiTypeToTypeScript(itemSchema.type);
|
|
148
|
+
}
|
|
149
|
+
}).filter(type => type && type !== ''); // 只过滤掉空字符串,保留 any
|
|
150
|
+
if (types.length > 0) {
|
|
151
|
+
propType = types.join(' | ');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// 处理普通类型
|
|
155
|
+
else if (prop.type) {
|
|
156
|
+
propType = convertOpenApiTypeToTypeScript(prop.type);
|
|
157
|
+
}
|
|
50
158
|
params.push({
|
|
51
159
|
name: propName,
|
|
52
|
-
type:
|
|
160
|
+
type: propType,
|
|
53
161
|
description: (prop === null || prop === void 0 ? void 0 : prop.description) || '',
|
|
54
162
|
required: (_a = targetSchema.required) === null || _a === void 0 ? void 0 : _a.includes(propName)
|
|
55
163
|
});
|
|
@@ -75,10 +183,11 @@ function getModuleNameFromPackage(operation) {
|
|
|
75
183
|
* @param api The bundled OpenAPI document.
|
|
76
184
|
* @param excludePaths Paths to exclude (by prefix).
|
|
77
185
|
* @param includePaths Paths to include (by prefix). If provided, only these paths will be processed.
|
|
186
|
+
* @param pathPrefixes Path prefix configurations for custom module grouping.
|
|
78
187
|
* @returns A structured representation of modules and their endpoints.
|
|
79
188
|
*/
|
|
80
|
-
const processApi = (api, excludePaths = [], includePaths = []) => {
|
|
81
|
-
var _a, _b, _c, _d, _e, _f
|
|
189
|
+
const processApi = (api, excludePaths = [], includePaths = [], pathPrefixes = []) => {
|
|
190
|
+
var _a, _b, _c, _d, _e, _f;
|
|
82
191
|
const modules = {};
|
|
83
192
|
const allSchemas = ((_a = api.components) === null || _a === void 0 ? void 0 : _a.schemas) || api.definitions || {};
|
|
84
193
|
const moduleFunctionNames = {};
|
|
@@ -101,7 +210,7 @@ const processApi = (api, excludePaths = [], includePaths = []) => {
|
|
|
101
210
|
if (!operation.tags)
|
|
102
211
|
continue;
|
|
103
212
|
const moduleNameFromPackage = getModuleNameFromPackage(operation);
|
|
104
|
-
const moduleName = moduleNameFromPackage || getModuleName(path);
|
|
213
|
+
const moduleName = moduleNameFromPackage || getModuleName(path, pathPrefixes);
|
|
105
214
|
if (!modules[moduleName]) {
|
|
106
215
|
modules[moduleName] = {
|
|
107
216
|
name: moduleName,
|
|
@@ -129,6 +238,9 @@ const processApi = (api, excludePaths = [], includePaths = []) => {
|
|
|
129
238
|
let requestBodyTypeName = undefined;
|
|
130
239
|
let contentType = undefined;
|
|
131
240
|
const jsdocParams = [];
|
|
241
|
+
const paramsJsdocParams = [];
|
|
242
|
+
const dataJsdocParams = [];
|
|
243
|
+
const responseJsdocParams = [];
|
|
132
244
|
// --- FormData Detection ---
|
|
133
245
|
let isFormData = false;
|
|
134
246
|
// Method 1: Check requestBody
|
|
@@ -178,12 +290,6 @@ const processApi = (api, excludePaths = [], includePaths = []) => {
|
|
|
178
290
|
};
|
|
179
291
|
for (const param of operation.parameters) {
|
|
180
292
|
if (!isReferenceObject(param)) {
|
|
181
|
-
const paramToAdd = {
|
|
182
|
-
name: param.name,
|
|
183
|
-
description: param.description || '',
|
|
184
|
-
required: param.required,
|
|
185
|
-
type: ((_c = param.schema) === null || _c === void 0 ? void 0 : _c.type) || 'any'
|
|
186
|
-
};
|
|
187
293
|
// If the parameter's schema is a reference, expand its properties
|
|
188
294
|
if (param.in === 'query' && param.schema && isReferenceObject(param.schema)) {
|
|
189
295
|
const schemaName = getSchemaName(param.schema.$ref);
|
|
@@ -228,26 +334,32 @@ const processApi = (api, excludePaths = [], includePaths = []) => {
|
|
|
228
334
|
requestBodyTypeName = typeName;
|
|
229
335
|
modules[moduleName].schemas[typeName] = formDataSchema;
|
|
230
336
|
referencedTypes.push(typeName);
|
|
231
|
-
|
|
337
|
+
const formDataJsdoc = extractJsdocParamsFromSchema(formDataSchema, allSchemas);
|
|
338
|
+
jsdocParams.push(...formDataJsdoc);
|
|
339
|
+
dataJsdocParams.push(...formDataJsdoc);
|
|
232
340
|
}
|
|
233
341
|
if (Object.keys(paramsSchema.properties || {}).length > 0) {
|
|
234
342
|
requestParamsTypeName = `${functionName.charAt(0).toUpperCase() + functionName.slice(1)}Params`;
|
|
235
343
|
modules[moduleName].schemas[requestParamsTypeName] = paramsSchema;
|
|
236
344
|
referencedTypes.push(requestParamsTypeName);
|
|
237
|
-
|
|
345
|
+
const paramsJsdoc = extractJsdocParamsFromSchema(paramsSchema, allSchemas);
|
|
346
|
+
jsdocParams.push(...paramsJsdoc);
|
|
347
|
+
paramsJsdocParams.push(...paramsJsdoc);
|
|
238
348
|
}
|
|
239
349
|
}
|
|
240
350
|
// Handle standard Request Body
|
|
241
351
|
if (operation.requestBody && !isReferenceObject(operation.requestBody) && !isFormData) {
|
|
242
352
|
const requestBody = operation.requestBody;
|
|
243
|
-
const jsonContent = (
|
|
353
|
+
const jsonContent = (_c = requestBody.content) === null || _c === void 0 ? void 0 : _c['application/json'];
|
|
244
354
|
if (jsonContent === null || jsonContent === void 0 ? void 0 : jsonContent.schema) {
|
|
245
355
|
if (isReferenceObject(jsonContent.schema)) {
|
|
246
356
|
const name = getSchemaName(jsonContent.schema.$ref);
|
|
247
357
|
requestBodyTypeName = name;
|
|
248
358
|
addSchemaWithDependencies(name, modules[moduleName], allSchemas);
|
|
249
359
|
referencedTypes.push(name);
|
|
250
|
-
|
|
360
|
+
const bodyJsdoc = extractJsdocParamsFromSchema(jsonContent.schema, allSchemas);
|
|
361
|
+
jsdocParams.push(...bodyJsdoc);
|
|
362
|
+
dataJsdocParams.push(...bodyJsdoc);
|
|
251
363
|
}
|
|
252
364
|
else {
|
|
253
365
|
// Handle inline schema (including arrays)
|
|
@@ -264,11 +376,15 @@ const processApi = (api, excludePaths = [], includePaths = []) => {
|
|
|
264
376
|
addSchemaWithDependencies(itemSchemaName, modules[moduleName], allSchemas);
|
|
265
377
|
}
|
|
266
378
|
else {
|
|
267
|
-
|
|
379
|
+
const arrayJsdoc = extractJsdocParamsFromSchema(schema.items, allSchemas);
|
|
380
|
+
jsdocParams.push(...arrayJsdoc);
|
|
381
|
+
dataJsdocParams.push(...arrayJsdoc);
|
|
268
382
|
}
|
|
269
383
|
}
|
|
270
384
|
else if (schema.type === 'object') {
|
|
271
|
-
|
|
385
|
+
const objJsdoc = extractJsdocParamsFromSchema(schema, allSchemas);
|
|
386
|
+
jsdocParams.push(...objJsdoc);
|
|
387
|
+
dataJsdocParams.push(...objJsdoc);
|
|
272
388
|
}
|
|
273
389
|
}
|
|
274
390
|
}
|
|
@@ -276,26 +392,56 @@ const processApi = (api, excludePaths = [], includePaths = []) => {
|
|
|
276
392
|
}
|
|
277
393
|
else if (isFormData && operation.requestBody && !isReferenceObject(operation.requestBody) && !requestBodyTypeName) {
|
|
278
394
|
// Handle cases where FormData is defined in requestBody but we haven't processed it yet
|
|
279
|
-
const formDataContent = (
|
|
395
|
+
const formDataContent = (_d = operation.requestBody.content) === null || _d === void 0 ? void 0 : _d['multipart/form-data'];
|
|
280
396
|
if (formDataContent === null || formDataContent === void 0 ? void 0 : formDataContent.schema) {
|
|
281
397
|
const typeName = `${functionName.charAt(0).toUpperCase() + functionName.slice(1)}Data`;
|
|
282
398
|
requestBodyTypeName = typeName;
|
|
283
399
|
modules[moduleName].schemas[typeName] = formDataContent.schema;
|
|
284
400
|
referencedTypes.push(typeName);
|
|
285
|
-
|
|
401
|
+
const formJsdoc = extractJsdocParamsFromSchema(formDataContent.schema, allSchemas);
|
|
402
|
+
dataJsdocParams.push(...formJsdoc);
|
|
286
403
|
}
|
|
287
404
|
}
|
|
288
405
|
// Handle Response Body
|
|
289
406
|
let responseTypeName = 'void';
|
|
290
407
|
const successResponse = operation.responses['200'];
|
|
291
408
|
if (successResponse) {
|
|
292
|
-
const mediaType = ((
|
|
409
|
+
const mediaType = ((_e = successResponse.content) === null || _e === void 0 ? void 0 : _e['*/*']) || ((_f = successResponse.content) === null || _f === void 0 ? void 0 : _f['application/json']);
|
|
293
410
|
const schema = mediaType === null || mediaType === void 0 ? void 0 : mediaType.schema;
|
|
294
411
|
if (schema && isReferenceObject(schema)) {
|
|
295
412
|
const name = getSchemaName(schema.$ref);
|
|
296
413
|
responseTypeName = name;
|
|
297
414
|
addSchemaWithDependencies(name, modules[moduleName], allSchemas);
|
|
298
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
|
+
}
|
|
299
445
|
}
|
|
300
446
|
}
|
|
301
447
|
// Convert path to template literal string for path parameters
|
|
@@ -313,6 +459,9 @@ const processApi = (api, excludePaths = [], includePaths = []) => {
|
|
|
313
459
|
hasData: !!requestBodyTypeName,
|
|
314
460
|
contentType,
|
|
315
461
|
jsdocParams,
|
|
462
|
+
paramsJsdocParams,
|
|
463
|
+
dataJsdocParams,
|
|
464
|
+
responseJsdocParams,
|
|
316
465
|
};
|
|
317
466
|
modules[moduleName].endpoints.push(endpoint);
|
|
318
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "czh-api",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "A CLI tool to generate TypeScript API clients from Swagger/OpenAPI documents.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"author": "Czh",
|
|
20
20
|
"license": "ISC",
|
|
21
21
|
"dependencies": {
|
|
22
|
+
"@apiture/openapi-down-convert": "^0.14.2",
|
|
22
23
|
"@types/pinyin": "^2.10.2",
|
|
23
24
|
"axios": "^1.10.0",
|
|
24
25
|
"chalk": "^4.1.2",
|