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/core/parser.ts
CHANGED
|
@@ -16,6 +16,11 @@ export interface Endpoint {
|
|
|
16
16
|
hasData: boolean;
|
|
17
17
|
contentType?: string;
|
|
18
18
|
jsdocParams: Array<{ name: string; type?: string; description?: string; required?: boolean }>;
|
|
19
|
+
// Separate jsdoc params for params and data to avoid merging
|
|
20
|
+
paramsJsdocParams: Array<{ name: string; type?: string; description?: string; required?: boolean }>;
|
|
21
|
+
dataJsdocParams: Array<{ name: string; type?: string; description?: string; required?: boolean }>;
|
|
22
|
+
// Jsdoc params for response type
|
|
23
|
+
responseJsdocParams: Array<{ name: string; type?: string; description?: string; required?: boolean }>;
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
export interface Module {
|
|
@@ -42,26 +47,91 @@ function addSchemaWithDependencies(name: string, module: Module, allSchemas: { [
|
|
|
42
47
|
|
|
43
48
|
const schema = allSchemas[name];
|
|
44
49
|
if (!schema) {
|
|
50
|
+
console.warn(`警告: Schema "${name}" 未找到,跳过此引用`);
|
|
45
51
|
return; // Schema not found
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
module.schemas[name] = schema as OpenAPIV3.SchemaObject;
|
|
49
55
|
|
|
50
|
-
// Recursively add dependencies
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
// Recursively add dependencies with error handling
|
|
57
|
+
try {
|
|
58
|
+
JSON.stringify(schema, (key, value) => {
|
|
59
|
+
if (key === '$ref' && typeof value === 'string') {
|
|
60
|
+
const depName = getSchemaName(value);
|
|
61
|
+
if (depName && allSchemas[depName]) {
|
|
62
|
+
addSchemaWithDependencies(depName, module, allSchemas);
|
|
63
|
+
} else {
|
|
64
|
+
console.warn(`警告: 依赖 Schema "${depName}" 未找到,跳过此引用`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.warn(`警告: 处理 Schema "${name}" 的依赖时出错:`, error);
|
|
71
|
+
}
|
|
58
72
|
}
|
|
59
73
|
|
|
60
|
-
function
|
|
74
|
+
function toCamelCase(str: string): string {
|
|
75
|
+
return str
|
|
76
|
+
.split('/')
|
|
77
|
+
.filter(p => p)
|
|
78
|
+
.map((part, index) => {
|
|
79
|
+
const cleaned = part.replace(/[^a-zA-Z0-9]/g, '');
|
|
80
|
+
if (index === 0) {
|
|
81
|
+
return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
|
|
82
|
+
}
|
|
83
|
+
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
84
|
+
})
|
|
85
|
+
.join('');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getModuleName(path: string, pathPrefixes: Array<{ path: string; packageName?: string }> = []): string {
|
|
89
|
+
// 尝试匹配配置的路径前缀
|
|
90
|
+
for (const prefix of pathPrefixes) {
|
|
91
|
+
if (path.startsWith(prefix.path)) {
|
|
92
|
+
// 获取前缀后的路径部分
|
|
93
|
+
const remainingPath = path.substring(prefix.path.length);
|
|
94
|
+
const parts = remainingPath.split('/').filter(p => p && !p.startsWith('{'));
|
|
95
|
+
|
|
96
|
+
// 确定包名
|
|
97
|
+
const packageName = prefix.packageName || toCamelCase(prefix.path);
|
|
98
|
+
|
|
99
|
+
// 如果有二级路径,使用二级路径作为子模块
|
|
100
|
+
if (parts.length > 0 && parts[0]) {
|
|
101
|
+
return `${packageName}/${parts[0]}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 如果没有二级路径,直接使用包名
|
|
105
|
+
return packageName;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 如果没有匹配到配置的前缀,使用默认逻辑
|
|
61
110
|
const parts = path.split('/').filter(p => p && !p.startsWith('{'));
|
|
62
111
|
return parts[0] || 'default';
|
|
63
112
|
}
|
|
64
113
|
|
|
114
|
+
function convertOpenApiTypeToTypeScript(openApiType: string | undefined): string {
|
|
115
|
+
if (!openApiType) return 'any';
|
|
116
|
+
|
|
117
|
+
switch (openApiType) {
|
|
118
|
+
case 'integer':
|
|
119
|
+
return 'number';
|
|
120
|
+
case 'object':
|
|
121
|
+
return 'any';
|
|
122
|
+
case 'array':
|
|
123
|
+
return 'any[]';
|
|
124
|
+
case 'boolean':
|
|
125
|
+
return 'boolean';
|
|
126
|
+
case 'string':
|
|
127
|
+
return 'string';
|
|
128
|
+
case 'number':
|
|
129
|
+
return 'number';
|
|
130
|
+
default:
|
|
131
|
+
return 'any';
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
65
135
|
function extractJsdocParamsFromSchema(schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, allSchemas: { [key: string]: any }): Required<Endpoint>['jsdocParams'] {
|
|
66
136
|
const params: Required<Endpoint>['jsdocParams'] = [];
|
|
67
137
|
if (!schema) return params;
|
|
@@ -78,12 +148,60 @@ function extractJsdocParamsFromSchema(schema: OpenAPIV3.SchemaObject | OpenAPIV3
|
|
|
78
148
|
for (const propName in targetSchema.properties) {
|
|
79
149
|
const prop = targetSchema.properties[propName] as OpenAPIV3.SchemaObject;
|
|
80
150
|
|
|
81
|
-
|
|
82
|
-
|
|
151
|
+
let propType = 'any';
|
|
152
|
+
|
|
153
|
+
// 处理 anyOf 数组 (FastAPI 常用的联合类型)
|
|
154
|
+
if (prop.anyOf && Array.isArray(prop.anyOf)) {
|
|
155
|
+
const types = prop.anyOf.map(item => {
|
|
156
|
+
if (isReferenceObject(item)) {
|
|
157
|
+
return getSchemaName(item.$ref);
|
|
158
|
+
} else {
|
|
159
|
+
const itemSchema = item as OpenAPIV3.SchemaObject;
|
|
160
|
+
// 处理 null 类型 (在 OpenAPI 中可能以不同方式表示)
|
|
161
|
+
if ((itemSchema as any).type === 'null' ||
|
|
162
|
+
itemSchema.type === undefined && (itemSchema as any).nullable === true ||
|
|
163
|
+
JSON.stringify(itemSchema) === '{"type":"null"}' ||
|
|
164
|
+
Object.keys(itemSchema).length === 0) {
|
|
165
|
+
return 'null';
|
|
166
|
+
}
|
|
167
|
+
return convertOpenApiTypeToTypeScript(itemSchema.type);
|
|
168
|
+
}
|
|
169
|
+
}).filter(type => type && type !== ''); // 只过滤掉空字符串,保留 any
|
|
170
|
+
|
|
171
|
+
if (types.length > 0) {
|
|
172
|
+
propType = types.join(' | ');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// 处理 oneOf 数组
|
|
176
|
+
else if (prop.oneOf && Array.isArray(prop.oneOf)) {
|
|
177
|
+
const types = prop.oneOf.map(item => {
|
|
178
|
+
if (isReferenceObject(item)) {
|
|
179
|
+
return getSchemaName(item.$ref);
|
|
180
|
+
} else {
|
|
181
|
+
const itemSchema = item as OpenAPIV3.SchemaObject;
|
|
182
|
+
// 处理 null 类型 (在 OpenAPI 中可能以不同方式表示)
|
|
183
|
+
if ((itemSchema as any).type === 'null' ||
|
|
184
|
+
itemSchema.type === undefined && (itemSchema as any).nullable === true ||
|
|
185
|
+
JSON.stringify(itemSchema) === '{"type":"null"}' ||
|
|
186
|
+
Object.keys(itemSchema).length === 0) {
|
|
187
|
+
return 'null';
|
|
188
|
+
}
|
|
189
|
+
return convertOpenApiTypeToTypeScript(itemSchema.type);
|
|
190
|
+
}
|
|
191
|
+
}).filter(type => type && type !== ''); // 只过滤掉空字符串,保留 any
|
|
192
|
+
|
|
193
|
+
if (types.length > 0) {
|
|
194
|
+
propType = types.join(' | ');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// 处理普通类型
|
|
198
|
+
else if (prop.type) {
|
|
199
|
+
propType = convertOpenApiTypeToTypeScript(prop.type);
|
|
200
|
+
}
|
|
83
201
|
|
|
84
202
|
params.push({
|
|
85
203
|
name: propName,
|
|
86
|
-
type:
|
|
204
|
+
type: propType,
|
|
87
205
|
description: prop?.description || '',
|
|
88
206
|
required: targetSchema.required?.includes(propName)
|
|
89
207
|
});
|
|
@@ -111,9 +229,10 @@ function getModuleNameFromPackage(operation: OpenAPIV3.OperationObject): string
|
|
|
111
229
|
* @param api The bundled OpenAPI document.
|
|
112
230
|
* @param excludePaths Paths to exclude (by prefix).
|
|
113
231
|
* @param includePaths Paths to include (by prefix). If provided, only these paths will be processed.
|
|
232
|
+
* @param pathPrefixes Path prefix configurations for custom module grouping.
|
|
114
233
|
* @returns A structured representation of modules and their endpoints.
|
|
115
234
|
*/
|
|
116
|
-
export const processApi = (api: OpenAPI.Document, excludePaths: string[] = [], includePaths: string[] = []): Modules => {
|
|
235
|
+
export const processApi = (api: OpenAPI.Document, excludePaths: string[] = [], includePaths: string[] = [], pathPrefixes: Array<{ path: string; packageName?: string }> = []): Modules => {
|
|
117
236
|
const modules: Modules = {};
|
|
118
237
|
const allSchemas = (api as OpenAPIV3.Document).components?.schemas || (api as any).definitions ||{};
|
|
119
238
|
|
|
@@ -141,7 +260,7 @@ export const processApi = (api: OpenAPI.Document, excludePaths: string[] = [], i
|
|
|
141
260
|
if (!operation.tags) continue;
|
|
142
261
|
|
|
143
262
|
const moduleNameFromPackage = getModuleNameFromPackage(operation);
|
|
144
|
-
const moduleName = moduleNameFromPackage || getModuleName(path);
|
|
263
|
+
const moduleName = moduleNameFromPackage || getModuleName(path, pathPrefixes);
|
|
145
264
|
|
|
146
265
|
if (!modules[moduleName]) {
|
|
147
266
|
modules[moduleName] = {
|
|
@@ -176,6 +295,9 @@ export const processApi = (api: OpenAPI.Document, excludePaths: string[] = [], i
|
|
|
176
295
|
let requestBodyTypeName: string | undefined = undefined;
|
|
177
296
|
let contentType: string | undefined = undefined;
|
|
178
297
|
const jsdocParams: Required<Endpoint>['jsdocParams'] = [];
|
|
298
|
+
const paramsJsdocParams: Required<Endpoint>['paramsJsdocParams'] = [];
|
|
299
|
+
const dataJsdocParams: Required<Endpoint>['dataJsdocParams'] = [];
|
|
300
|
+
const responseJsdocParams: Required<Endpoint>['responseJsdocParams'] = [];
|
|
179
301
|
|
|
180
302
|
// --- FormData Detection ---
|
|
181
303
|
let isFormData = false;
|
|
@@ -227,13 +349,6 @@ export const processApi = (api: OpenAPI.Document, excludePaths: string[] = [], i
|
|
|
227
349
|
|
|
228
350
|
for (const param of operation.parameters) {
|
|
229
351
|
if (!isReferenceObject(param)) {
|
|
230
|
-
const paramToAdd = {
|
|
231
|
-
name: param.name,
|
|
232
|
-
description: param.description || '',
|
|
233
|
-
required: param.required,
|
|
234
|
-
type: (param.schema as any)?.type || 'any'
|
|
235
|
-
};
|
|
236
|
-
|
|
237
352
|
// If the parameter's schema is a reference, expand its properties
|
|
238
353
|
if (param.in === 'query' && param.schema && isReferenceObject(param.schema)) {
|
|
239
354
|
const schemaName = getSchemaName(param.schema.$ref);
|
|
@@ -271,14 +386,18 @@ export const processApi = (api: OpenAPI.Document, excludePaths: string[] = [], i
|
|
|
271
386
|
requestBodyTypeName = typeName;
|
|
272
387
|
modules[moduleName].schemas[typeName] = formDataSchema;
|
|
273
388
|
referencedTypes.push(typeName);
|
|
274
|
-
|
|
389
|
+
const formDataJsdoc = extractJsdocParamsFromSchema(formDataSchema, allSchemas);
|
|
390
|
+
jsdocParams.push(...formDataJsdoc);
|
|
391
|
+
dataJsdocParams.push(...formDataJsdoc);
|
|
275
392
|
}
|
|
276
393
|
|
|
277
394
|
if (Object.keys(paramsSchema.properties || {}).length > 0) {
|
|
278
395
|
requestParamsTypeName = `${functionName.charAt(0).toUpperCase() + functionName.slice(1)}Params`;
|
|
279
396
|
modules[moduleName].schemas[requestParamsTypeName] = paramsSchema;
|
|
280
397
|
referencedTypes.push(requestParamsTypeName);
|
|
281
|
-
|
|
398
|
+
const paramsJsdoc = extractJsdocParamsFromSchema(paramsSchema, allSchemas);
|
|
399
|
+
jsdocParams.push(...paramsJsdoc);
|
|
400
|
+
paramsJsdocParams.push(...paramsJsdoc);
|
|
282
401
|
}
|
|
283
402
|
}
|
|
284
403
|
|
|
@@ -293,7 +412,9 @@ export const processApi = (api: OpenAPI.Document, excludePaths: string[] = [], i
|
|
|
293
412
|
requestBodyTypeName = name;
|
|
294
413
|
addSchemaWithDependencies(name, modules[moduleName], allSchemas);
|
|
295
414
|
referencedTypes.push(name);
|
|
296
|
-
|
|
415
|
+
const bodyJsdoc = extractJsdocParamsFromSchema(jsonContent.schema, allSchemas);
|
|
416
|
+
jsdocParams.push(...bodyJsdoc);
|
|
417
|
+
dataJsdocParams.push(...bodyJsdoc);
|
|
297
418
|
} else {
|
|
298
419
|
// Handle inline schema (including arrays)
|
|
299
420
|
const schema = jsonContent.schema as OpenAPIV3.SchemaObject;
|
|
@@ -309,10 +430,14 @@ export const processApi = (api: OpenAPI.Document, excludePaths: string[] = [], i
|
|
|
309
430
|
const itemSchemaName = getSchemaName(schema.items.$ref);
|
|
310
431
|
addSchemaWithDependencies(itemSchemaName, modules[moduleName], allSchemas);
|
|
311
432
|
} else {
|
|
312
|
-
|
|
433
|
+
const arrayJsdoc = extractJsdocParamsFromSchema(schema.items as OpenAPIV3.SchemaObject, allSchemas);
|
|
434
|
+
jsdocParams.push(...arrayJsdoc);
|
|
435
|
+
dataJsdocParams.push(...arrayJsdoc);
|
|
313
436
|
}
|
|
314
437
|
} else if (schema.type === 'object') {
|
|
315
|
-
|
|
438
|
+
const objJsdoc = extractJsdocParamsFromSchema(schema, allSchemas);
|
|
439
|
+
jsdocParams.push(...objJsdoc);
|
|
440
|
+
dataJsdocParams.push(...objJsdoc);
|
|
316
441
|
}
|
|
317
442
|
}
|
|
318
443
|
}
|
|
@@ -324,8 +449,9 @@ export const processApi = (api: OpenAPI.Document, excludePaths: string[] = [], i
|
|
|
324
449
|
const typeName = `${functionName.charAt(0).toUpperCase() + functionName.slice(1)}Data`;
|
|
325
450
|
requestBodyTypeName = typeName;
|
|
326
451
|
modules[moduleName].schemas[typeName] = formDataContent.schema as OpenAPIV3.SchemaObject;
|
|
327
|
-
|
|
328
|
-
|
|
452
|
+
referencedTypes.push(typeName);
|
|
453
|
+
const formJsdoc = extractJsdocParamsFromSchema(formDataContent.schema, allSchemas);
|
|
454
|
+
dataJsdocParams.push(...formJsdoc);
|
|
329
455
|
}
|
|
330
456
|
}
|
|
331
457
|
|
|
@@ -341,8 +467,34 @@ export const processApi = (api: OpenAPI.Document, excludePaths: string[] = [], i
|
|
|
341
467
|
responseTypeName = name;
|
|
342
468
|
addSchemaWithDependencies(name, modules[moduleName], allSchemas);
|
|
343
469
|
referencedTypes.push(name);
|
|
344
|
-
|
|
345
|
-
|
|
470
|
+
responseJsdocParams.push(...extractJsdocParamsFromSchema(schema, allSchemas));
|
|
471
|
+
} else if (schema && !isReferenceObject(schema)) {
|
|
472
|
+
const inlineSchema = schema as OpenAPIV3.SchemaObject;
|
|
473
|
+
if (inlineSchema.type === 'object' || inlineSchema.properties) {
|
|
474
|
+
// Generate a named type for inline response schema
|
|
475
|
+
const typeName = `${functionName.charAt(0).toUpperCase() + functionName.slice(1)}Response`;
|
|
476
|
+
responseTypeName = typeName;
|
|
477
|
+
modules[moduleName].schemas[typeName] = inlineSchema;
|
|
478
|
+
referencedTypes.push(typeName);
|
|
479
|
+
responseJsdocParams.push(...extractJsdocParamsFromSchema(inlineSchema, allSchemas));
|
|
480
|
+
} else if (inlineSchema.type === 'array' && inlineSchema.items) {
|
|
481
|
+
if (isReferenceObject(inlineSchema.items)) {
|
|
482
|
+
const itemSchemaName = getSchemaName(inlineSchema.items.$ref);
|
|
483
|
+
// Prefer direct `ItemType[]` for array responses to avoid unnecessary alias schema compilation.
|
|
484
|
+
responseTypeName = `${itemSchemaName}[]`;
|
|
485
|
+
referencedTypes.push(itemSchemaName);
|
|
486
|
+
addSchemaWithDependencies(itemSchemaName, modules[moduleName], allSchemas);
|
|
487
|
+
responseJsdocParams.push(...extractJsdocParamsFromSchema(inlineSchema.items, allSchemas));
|
|
488
|
+
} else {
|
|
489
|
+
const typeName = `${functionName.charAt(0).toUpperCase() + functionName.slice(1)}Response`;
|
|
490
|
+
responseTypeName = typeName;
|
|
491
|
+
modules[moduleName].schemas[typeName] = inlineSchema;
|
|
492
|
+
referencedTypes.push(typeName);
|
|
493
|
+
responseJsdocParams.push(...extractJsdocParamsFromSchema(inlineSchema, allSchemas));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
346
498
|
|
|
347
499
|
// Convert path to template literal string for path parameters
|
|
348
500
|
const urlTemplate = path.replace(/\{(\w+)\}/g, '${params.$1}');
|
|
@@ -360,6 +512,9 @@ export const processApi = (api: OpenAPI.Document, excludePaths: string[] = [], i
|
|
|
360
512
|
hasData: !!requestBodyTypeName,
|
|
361
513
|
contentType,
|
|
362
514
|
jsdocParams,
|
|
515
|
+
paramsJsdocParams,
|
|
516
|
+
dataJsdocParams,
|
|
517
|
+
responseJsdocParams,
|
|
363
518
|
};
|
|
364
519
|
|
|
365
520
|
modules[moduleName].endpoints.push(endpoint);
|
|
@@ -367,4 +522,4 @@ export const processApi = (api: OpenAPI.Document, excludePaths: string[] = [], i
|
|
|
367
522
|
}
|
|
368
523
|
|
|
369
524
|
return modules;
|
|
370
|
-
};
|
|
525
|
+
};
|
package/src/templates/api.hbs
CHANGED
|
@@ -10,12 +10,7 @@ export const {{functionName}} = ({{#if hasParams}}params: {{requestParamsTypeNam
|
|
|
10
10
|
return http.request<{{responseTypeName}}>({
|
|
11
11
|
url: `{{path}}`,
|
|
12
12
|
method: '{{method}}',
|
|
13
|
-
{{#if hasParams}}
|
|
14
|
-
params,
|
|
15
|
-
{{/if}}
|
|
16
|
-
{{#if hasData}}
|
|
17
|
-
data,
|
|
18
|
-
{{/if}}
|
|
13
|
+
{{#if hasParams}}{{#unless (eq method 'put')}}{{#unless (eq method 'post')}}params,{{/unless}}{{/unless}}{{/if}}{{#if hasData}}data,{{/if}}
|
|
19
14
|
{{#if contentType}}
|
|
20
15
|
headers: { 'Content-Type': '{{contentType}}' },
|
|
21
16
|
{{/if}}
|