czh-api 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -5
- package/README.md +4 -11
- package/dist/commands/build.js +320 -107
- package/dist/core/parser.js +134 -16
- package/dist/templates/api.hbs +1 -6
- package/package.json +1 -1
- package/src/commands/build.ts +427 -159
- package/src/core/parser.ts +170 -34
- package/src/templates/api.hbs +1 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
-
所有重要的版本更新都会记录在此文件中。
|
|
4
|
-
|
|
5
|
-
## [1.0.
|
|
6
|
-
|
|
7
|
-
###
|
|
3
|
+
所有重要的版本更新都会记录在此文件中。
|
|
4
|
+
|
|
5
|
+
## [1.0.5] - 2026-03-19
|
|
6
|
+
|
|
7
|
+
### 修复
|
|
8
|
+
- 修复 FastAPI 场景下返回类型字段被错误降级为 `any` 的问题(如 `items.$ref`、`anyOf`、`oneOf`、`allOf` 组合场景)。
|
|
9
|
+
- 修复数组响应与嵌套对象类型在 fallback 生成流程中丢失字段的问题,避免出现仅有 `{ [key: string]: any }` 的空壳接口。
|
|
10
|
+
- 修复 `group_ids` 等多层结构类型解析不完整的问题,支持递归解析到更深层级(如 `number[] | null`)。
|
|
11
|
+
- 修复类型增强阶段对复杂类型名(数组、联合类型)处理不稳定导致的类型声明异常问题。
|
|
12
|
+
|
|
13
|
+
### 改进
|
|
14
|
+
- 增强 fallback 类型生成策略:优先基于 `module.schemas` 还原字段结构,再回退到 `any`,生成结果更可用。
|
|
15
|
+
- 增强类型依赖收集能力:从字段类型表达式中提取嵌套引用类型并自动补齐声明。
|
|
16
|
+
- 完善 README 模板示例并与当前默认模板保持一致(`api.hbs`、`index.hbs`、`types.hbs`)。
|
|
17
|
+
|
|
18
|
+
## [1.0.4] - 2026-03-19
|
|
19
|
+
|
|
20
|
+
### 修复
|
|
21
|
+
- 修复响应最外层为数组(如 `type: array` + `items.$ref`)时类型生成异常的问题,避免出现非法接口声明(如 `export interface Xxx[]`)。
|
|
22
|
+
- 修复仅在“数组响应”中被引用的实体类型可能退化为 `{ [key: string]: any }` 的问题,增强该场景下的类型补全稳定性。
|
|
23
|
+
- 修复类型增强阶段对 `export interface` 的替换逻辑在存在内联对象字段时可能截断内容的问题,避免生成残留字段到接口外。
|
|
24
|
+
- 修复部分 OpenAPI `$ref` 路径在类型编译阶段解析不一致导致的类型降级问题(兼容 `#/components/schemas/*` 场景)。
|
|
25
|
+
|
|
26
|
+
### 改进
|
|
27
|
+
- 优化数组响应类型推导策略:当响应为 `$ref` 数组时优先生成 `ItemType[]`,减少中间响应类型噪音。
|
|
28
|
+
- 增强类型生成兜底逻辑的类型名合法性校验,避免对泛型/数组等复杂类型名误生成 `interface`。
|
|
29
|
+
|
|
30
|
+
## [1.0.3] - 2026-03-17
|
|
31
|
+
|
|
32
|
+
### 新增
|
|
8
33
|
- 添加 `pathPrefixes` 配置项,支持自定义路径前缀分组和二级分包
|
|
9
34
|
- 支持配置多个路径前缀,每个前缀可指定自定义包名或自动驼峰命名
|
|
10
35
|
- 自动按路径前缀后的第一级路径进行二级分包
|
package/README.md
CHANGED
|
@@ -190,12 +190,7 @@ export const {{functionName}} = ({{#if hasParams}}params: {{requestParamsTypeNam
|
|
|
190
190
|
return http.request<{{responseTypeName}}>({
|
|
191
191
|
url: `{{path}}`,
|
|
192
192
|
method: '{{method}}',
|
|
193
|
-
{{#if hasParams}}
|
|
194
|
-
params,
|
|
195
|
-
{{/if}}
|
|
196
|
-
{{#if hasData}}
|
|
197
|
-
data,
|
|
198
|
-
{{/if}}
|
|
193
|
+
{{#if hasParams}}{{#unless (eq method 'put')}}{{#unless (eq method 'post')}}params,{{/unless}}{{/unless}}{{/if}}{{#if hasData}}data,{{/if}}
|
|
199
194
|
{{#if contentType}}
|
|
200
195
|
headers: { 'Content-Type': '{{contentType}}' },
|
|
201
196
|
{{/if}}
|
|
@@ -209,11 +204,9 @@ export const {{functionName}} = ({{#if hasParams}}params: {{requestParamsTypeNam
|
|
|
209
204
|
export const {{functionName}} = ({{#if hasParams}}params: {{requestParamsTypeName}}{{/if}}{{#if hasData}}{{#if hasParams}}, {{/if}}data: {{requestBodyTypeName}}{{/if}}) => {
|
|
210
205
|
return request.{{#if (eq method "delete")}}del{{else}}{{method}}{{/if}}<{{responseTypeName}}>({
|
|
211
206
|
url: `{{path}}`,
|
|
212
|
-
{{#if hasParams}}
|
|
213
|
-
|
|
214
|
-
{{
|
|
215
|
-
{{#if hasData}}
|
|
216
|
-
data,
|
|
207
|
+
{{#if hasParams}}{{#unless (eq method 'put')}}{{#unless (eq method 'post')}}params,{{/unless}}{{/unless}}{{/if}}{{#if hasData}}data,{{/if}}
|
|
208
|
+
{{#if contentType}}
|
|
209
|
+
headers: { 'Content-Type': '{{contentType}}' },
|
|
217
210
|
{{/if}}
|
|
218
211
|
});
|
|
219
212
|
};
|
package/dist/commands/build.js
CHANGED
|
@@ -48,68 +48,236 @@ function convertOpenApiTypeToTypeScript(openApiType) {
|
|
|
48
48
|
case 'number':
|
|
49
49
|
return 'number';
|
|
50
50
|
default:
|
|
51
|
-
return
|
|
51
|
+
return openApiType;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
// 改进类型定义的辅助函数
|
|
55
|
+
function isValidTypeIdentifier(typeName) {
|
|
56
|
+
if (!typeName)
|
|
57
|
+
return false;
|
|
58
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(typeName);
|
|
59
|
+
}
|
|
60
|
+
function escapeRegex(value) {
|
|
61
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
62
|
+
}
|
|
63
|
+
function resolveInterfaceTargetType(typeName) {
|
|
64
|
+
if (typeof typeName !== 'string' || typeName.length === 0)
|
|
65
|
+
return null;
|
|
66
|
+
if (isValidTypeIdentifier(typeName))
|
|
67
|
+
return typeName;
|
|
68
|
+
const arrayMatch = typeName.match(/^([A-Za-z_$][A-Za-z0-9_$]*)\[\]$/);
|
|
69
|
+
if (arrayMatch) {
|
|
70
|
+
return arrayMatch[1];
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
function findInterfaceBlockRange(content, typeName) {
|
|
75
|
+
const startRegex = new RegExp(`export interface ${escapeRegex(typeName)}\\s*\\{`);
|
|
76
|
+
const match = startRegex.exec(content);
|
|
77
|
+
if (!match)
|
|
78
|
+
return null;
|
|
79
|
+
const start = match.index;
|
|
80
|
+
const openBraceIndex = start + match[0].lastIndexOf('{');
|
|
81
|
+
let depth = 0;
|
|
82
|
+
for (let i = openBraceIndex; i < content.length; i++) {
|
|
83
|
+
const ch = content[i];
|
|
84
|
+
if (ch === '{') {
|
|
85
|
+
depth++;
|
|
86
|
+
}
|
|
87
|
+
else if (ch === '}') {
|
|
88
|
+
depth--;
|
|
89
|
+
if (depth === 0) {
|
|
90
|
+
return { start, end: i + 1 };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const TS_BUILTIN_TYPE_NAMES = new Set([
|
|
97
|
+
'string',
|
|
98
|
+
'number',
|
|
99
|
+
'boolean',
|
|
100
|
+
'null',
|
|
101
|
+
'undefined',
|
|
102
|
+
'void',
|
|
103
|
+
'any',
|
|
104
|
+
'unknown',
|
|
105
|
+
'never',
|
|
106
|
+
'object',
|
|
107
|
+
'Record',
|
|
108
|
+
'Array',
|
|
109
|
+
'Date',
|
|
110
|
+
'Promise',
|
|
111
|
+
'true',
|
|
112
|
+
'false',
|
|
113
|
+
]);
|
|
114
|
+
function extractTypeIdentifiers(typeExpr) {
|
|
115
|
+
if (!typeExpr)
|
|
116
|
+
return [];
|
|
117
|
+
const matches = typeExpr.match(/[A-Za-z_$][A-Za-z0-9_$]*/g) || [];
|
|
118
|
+
return [...new Set(matches.filter(name => !TS_BUILTIN_TYPE_NAMES.has(name)))];
|
|
119
|
+
}
|
|
120
|
+
function getSchemaNameFromRef(ref) {
|
|
121
|
+
if (!ref)
|
|
122
|
+
return '';
|
|
123
|
+
return ref.split('/').pop() || '';
|
|
124
|
+
}
|
|
125
|
+
function resolveSchemaTypeFromModuleSchemas(schema, moduleSchemas, visitedRefs = new Set()) {
|
|
126
|
+
if (!schema)
|
|
127
|
+
return 'any';
|
|
128
|
+
if (schema.$ref) {
|
|
129
|
+
const refName = getSchemaNameFromRef(schema.$ref);
|
|
130
|
+
return refName || 'any';
|
|
131
|
+
}
|
|
132
|
+
const schemaObj = schema;
|
|
133
|
+
if (schemaObj.anyOf && Array.isArray(schemaObj.anyOf) && schemaObj.anyOf.length > 0) {
|
|
134
|
+
const types = schemaObj.anyOf
|
|
135
|
+
.map(item => resolveSchemaTypeFromModuleSchemas(item, moduleSchemas, visitedRefs))
|
|
136
|
+
.filter(Boolean);
|
|
137
|
+
if (types.length > 0) {
|
|
138
|
+
return [...new Set(types)].join(' | ');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (schemaObj.oneOf && Array.isArray(schemaObj.oneOf) && schemaObj.oneOf.length > 0) {
|
|
142
|
+
const types = schemaObj.oneOf
|
|
143
|
+
.map(item => resolveSchemaTypeFromModuleSchemas(item, moduleSchemas, visitedRefs))
|
|
144
|
+
.filter(Boolean);
|
|
145
|
+
if (types.length > 0) {
|
|
146
|
+
return [...new Set(types)].join(' | ');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (schemaObj.type === 'array') {
|
|
150
|
+
const itemType = resolveSchemaTypeFromModuleSchemas(schemaObj.items, moduleSchemas, visitedRefs);
|
|
151
|
+
return `${itemType || 'any'}[]`;
|
|
152
|
+
}
|
|
153
|
+
if (schemaObj.type === 'object' && schemaObj.additionalProperties) {
|
|
154
|
+
if (schemaObj.additionalProperties === true) {
|
|
155
|
+
return 'Record<string, any>';
|
|
156
|
+
}
|
|
157
|
+
const valueType = resolveSchemaTypeFromModuleSchemas(schemaObj.additionalProperties, moduleSchemas, visitedRefs);
|
|
158
|
+
return `Record<string, ${valueType || 'any'}>`;
|
|
159
|
+
}
|
|
160
|
+
if (schemaObj.type === 'object' && schemaObj.properties && Object.keys(schemaObj.properties).length > 0) {
|
|
161
|
+
const requiredSet = new Set(schemaObj.required || []);
|
|
162
|
+
const inlineFields = Object.entries(schemaObj.properties).map(([key, value]) => {
|
|
163
|
+
const fieldType = resolveSchemaTypeFromModuleSchemas(value, moduleSchemas, visitedRefs);
|
|
164
|
+
const optional = requiredSet.has(key) ? '' : '?';
|
|
165
|
+
return `${key}${optional}: ${fieldType || 'any'}`;
|
|
166
|
+
});
|
|
167
|
+
return `{ ${inlineFields.join('; ')} }`;
|
|
168
|
+
}
|
|
169
|
+
const baseType = convertOpenApiTypeToTypeScript(schemaObj.type);
|
|
170
|
+
if (!baseType)
|
|
171
|
+
return 'any';
|
|
172
|
+
return schemaObj.nullable ? `${baseType} | null` : baseType;
|
|
173
|
+
}
|
|
174
|
+
function collectSchemaProperties(schema, moduleSchemas, visitedRefs = new Set()) {
|
|
175
|
+
const collected = {
|
|
176
|
+
properties: {},
|
|
177
|
+
required: new Set(),
|
|
178
|
+
};
|
|
179
|
+
if (!schema)
|
|
180
|
+
return collected;
|
|
181
|
+
if (schema.$ref) {
|
|
182
|
+
const ref = schema.$ref;
|
|
183
|
+
if (!visitedRefs.has(ref)) {
|
|
184
|
+
visitedRefs.add(ref);
|
|
185
|
+
const refName = getSchemaNameFromRef(ref);
|
|
186
|
+
const refSchema = moduleSchemas[refName];
|
|
187
|
+
if (refSchema) {
|
|
188
|
+
const nested = collectSchemaProperties(refSchema, moduleSchemas, visitedRefs);
|
|
189
|
+
Object.assign(collected.properties, nested.properties);
|
|
190
|
+
nested.required.forEach(field => collected.required.add(field));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return collected;
|
|
194
|
+
}
|
|
195
|
+
const schemaObj = schema;
|
|
196
|
+
if (schemaObj.allOf && Array.isArray(schemaObj.allOf)) {
|
|
197
|
+
schemaObj.allOf.forEach(item => {
|
|
198
|
+
const nested = collectSchemaProperties(item, moduleSchemas, visitedRefs);
|
|
199
|
+
Object.assign(collected.properties, nested.properties);
|
|
200
|
+
nested.required.forEach(field => collected.required.add(field));
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
if (schemaObj.properties) {
|
|
204
|
+
Object.entries(schemaObj.properties).forEach(([name, value]) => {
|
|
205
|
+
collected.properties[name] = value;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
(schemaObj.required || []).forEach(field => collected.required.add(field));
|
|
209
|
+
return collected;
|
|
210
|
+
}
|
|
211
|
+
function buildInterfaceDefinitionFromModuleSchemas(typeName, moduleSchemas) {
|
|
212
|
+
const schema = moduleSchemas[typeName];
|
|
213
|
+
if (!schema)
|
|
214
|
+
return null;
|
|
215
|
+
const { properties, required } = collectSchemaProperties(schema, moduleSchemas);
|
|
216
|
+
const entries = Object.entries(properties);
|
|
217
|
+
if (entries.length === 0)
|
|
218
|
+
return null;
|
|
219
|
+
const lines = entries.map(([propName, propSchema]) => {
|
|
220
|
+
const optional = required.has(propName) ? '' : '?';
|
|
221
|
+
const propType = resolveSchemaTypeFromModuleSchemas(propSchema, moduleSchemas) || 'any';
|
|
222
|
+
const propDescription = propSchema.description || '';
|
|
223
|
+
const comment = propDescription ? ` // ${propDescription}` : '';
|
|
224
|
+
return ` ${propName}${optional}: ${propType};${comment}`;
|
|
225
|
+
});
|
|
226
|
+
return `export interface ${typeName} {\n${lines.join('\n')}\n}`;
|
|
227
|
+
}
|
|
55
228
|
function improveTypeDefinitions(typesContent, endpoints) {
|
|
56
229
|
let improvedContent = typesContent;
|
|
57
230
|
// 收集所有需要改进的类型定义
|
|
58
231
|
const typeImprovements = {};
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (type
|
|
69
|
-
|
|
70
|
-
if (type.includes('null')) {
|
|
71
|
-
type = type.replace(/\s*\|\s*null/g, ' | null');
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
// 处理基本类型转换
|
|
76
|
-
type = convertOpenApiTypeToTypeScript(type);
|
|
232
|
+
// 辅助函数:将 jsdoc params 转换为接口字段
|
|
233
|
+
function buildFieldsFromJsdocParams(jsdocParams) {
|
|
234
|
+
return jsdocParams
|
|
235
|
+
.filter((param) => param.name && param.type)
|
|
236
|
+
.map((param) => {
|
|
237
|
+
const optional = !param.required ? '?' : '';
|
|
238
|
+
let type = param.type;
|
|
239
|
+
// 处理联合类型,确保正确的 TypeScript 语法
|
|
240
|
+
if (type && type.includes(' | ')) {
|
|
241
|
+
if (type.includes('null')) {
|
|
242
|
+
type = type.replace(/\s*\|\s*null/g, ' | null');
|
|
77
243
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
type = convertOpenApiTypeToTypeScript(type);
|
|
247
|
+
}
|
|
248
|
+
const comment = param.description ? ` // ${param.description}` : '';
|
|
249
|
+
return ` ${param.name}${optional}: ${type};${comment}`;
|
|
250
|
+
})
|
|
251
|
+
.join('\n');
|
|
252
|
+
}
|
|
253
|
+
endpoints.forEach(endpoint => {
|
|
254
|
+
// 改进 params 类型 - 使用 paramsJsdocParams(仅包含 path/query 参数)
|
|
255
|
+
const requestParamsTypeName = endpoint.requestParamsTypeName;
|
|
256
|
+
if (requestParamsTypeName && isValidTypeIdentifier(requestParamsTypeName) && endpoint.paramsJsdocParams && endpoint.paramsJsdocParams.length > 0) {
|
|
257
|
+
const paramsFields = buildFieldsFromJsdocParams(endpoint.paramsJsdocParams);
|
|
82
258
|
if (paramsFields) {
|
|
83
|
-
typeImprovements[
|
|
84
|
-
${paramsFields}
|
|
259
|
+
typeImprovements[requestParamsTypeName] = `export interface ${requestParamsTypeName} {
|
|
260
|
+
${paramsFields}
|
|
85
261
|
}`;
|
|
86
262
|
}
|
|
87
263
|
}
|
|
88
|
-
// 改进 requestBody 类型
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
.map((param) => {
|
|
93
|
-
const optional = !param.required ? '?' : '';
|
|
94
|
-
let type = param.type;
|
|
95
|
-
// 处理联合类型,确保正确的 TypeScript 语法
|
|
96
|
-
if (type && type.includes(' | ')) {
|
|
97
|
-
// 如果包含 null,确保格式正确
|
|
98
|
-
if (type.includes('null')) {
|
|
99
|
-
type = type.replace(/\s*\|\s*null/g, ' | null');
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
// 处理基本类型转换
|
|
104
|
-
type = convertOpenApiTypeToTypeScript(type);
|
|
105
|
-
}
|
|
106
|
-
const comment = param.description ? ` // ${param.description}` : '';
|
|
107
|
-
return ` ${param.name}${optional}: ${type};${comment}`;
|
|
108
|
-
})
|
|
109
|
-
.join('\n');
|
|
264
|
+
// 改进 requestBody 类型 - 使用 dataJsdocParams(仅包含 requestBody 参数)
|
|
265
|
+
const requestBodyTypeName = endpoint.requestBodyTypeName;
|
|
266
|
+
if (requestBodyTypeName && isValidTypeIdentifier(requestBodyTypeName) && endpoint.dataJsdocParams && endpoint.dataJsdocParams.length > 0) {
|
|
267
|
+
const dataFields = buildFieldsFromJsdocParams(endpoint.dataJsdocParams);
|
|
110
268
|
if (dataFields) {
|
|
111
|
-
typeImprovements[
|
|
112
|
-
${dataFields}
|
|
269
|
+
typeImprovements[requestBodyTypeName] = `export interface ${requestBodyTypeName} {
|
|
270
|
+
${dataFields}
|
|
271
|
+
}`;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// 改进 response 类型 - 使用 responseJsdocParams(包含响应字段信息)
|
|
275
|
+
const responseTargetType = resolveInterfaceTargetType(endpoint.responseTypeName);
|
|
276
|
+
if (responseTargetType && endpoint.responseTypeName !== 'void' && endpoint.responseJsdocParams && endpoint.responseJsdocParams.length > 0) {
|
|
277
|
+
const responseFields = buildFieldsFromJsdocParams(endpoint.responseJsdocParams);
|
|
278
|
+
if (responseFields) {
|
|
279
|
+
typeImprovements[responseTargetType] = `export interface ${responseTargetType} {
|
|
280
|
+
${responseFields}
|
|
113
281
|
}`;
|
|
114
282
|
}
|
|
115
283
|
}
|
|
@@ -117,9 +285,12 @@ ${dataFields}
|
|
|
117
285
|
// 替换现有的类型定义
|
|
118
286
|
Object.entries(typeImprovements).forEach(([typeName, newDefinition]) => {
|
|
119
287
|
// 查找并替换现有的接口定义
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
122
|
-
improvedContent =
|
|
288
|
+
const blockRange = findInterfaceBlockRange(improvedContent, typeName);
|
|
289
|
+
if (blockRange) {
|
|
290
|
+
improvedContent =
|
|
291
|
+
improvedContent.slice(0, blockRange.start) +
|
|
292
|
+
newDefinition +
|
|
293
|
+
improvedContent.slice(blockRange.end);
|
|
123
294
|
}
|
|
124
295
|
else {
|
|
125
296
|
// 如果没找到接口定义,添加到末尾
|
|
@@ -186,6 +357,33 @@ function removeUselessTypeAliases(typesContent) {
|
|
|
186
357
|
cleanedContent = cleanedContent.replace(/\/\*\*\n \* [^\n]*\n \*\/\n(?!export)/g, '');
|
|
187
358
|
return cleanedContent;
|
|
188
359
|
}
|
|
360
|
+
function normalizeRef(refValue) {
|
|
361
|
+
const match = refValue.match(/^#\/components\/schemas\/([^/]+)$/);
|
|
362
|
+
if (match) {
|
|
363
|
+
return `#/definitions/${match[1]}`;
|
|
364
|
+
}
|
|
365
|
+
return refValue;
|
|
366
|
+
}
|
|
367
|
+
function normalizeSchemaRefs(value) {
|
|
368
|
+
if (Array.isArray(value)) {
|
|
369
|
+
return value.map(item => normalizeSchemaRefs(item));
|
|
370
|
+
}
|
|
371
|
+
if (value && typeof value === 'object') {
|
|
372
|
+
const obj = value;
|
|
373
|
+
const normalized = {};
|
|
374
|
+
for (const key in obj) {
|
|
375
|
+
const currentValue = obj[key];
|
|
376
|
+
if (key === '$ref' && typeof currentValue === 'string') {
|
|
377
|
+
normalized[key] = normalizeRef(currentValue);
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
normalized[key] = normalizeSchemaRefs(currentValue);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return normalized;
|
|
384
|
+
}
|
|
385
|
+
return value;
|
|
386
|
+
}
|
|
189
387
|
const handleBuild = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
190
388
|
try {
|
|
191
389
|
console.log(chalk_1.default.blue('Building API from source...'));
|
|
@@ -285,9 +483,10 @@ const handleBuild = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
285
483
|
if (Object.keys(module.schemas).length > 0) {
|
|
286
484
|
console.log(chalk_1.default.blue(`正在为模块 "${moduleName}" 生成类型文件,包含 ${Object.keys(module.schemas).length} 个 schema`));
|
|
287
485
|
try {
|
|
486
|
+
const normalizedModuleSchemas = normalizeSchemaRefs(module.schemas);
|
|
288
487
|
const schemasWithTitles = {};
|
|
289
|
-
for (const schemaName in
|
|
290
|
-
schemasWithTitles[schemaName] = Object.assign(Object.assign({},
|
|
488
|
+
for (const schemaName in normalizedModuleSchemas) {
|
|
489
|
+
schemasWithTitles[schemaName] = Object.assign(Object.assign({}, normalizedModuleSchemas[schemaName]), { title: schemaName });
|
|
291
490
|
}
|
|
292
491
|
const rootSchemaForCompiler = {
|
|
293
492
|
title: 'schemas',
|
|
@@ -334,7 +533,8 @@ const handleBuild = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
334
533
|
// 尝试逐个验证 schema,过滤掉有问题的
|
|
335
534
|
const validSchemas = {};
|
|
336
535
|
const invalidSchemas = [];
|
|
337
|
-
|
|
536
|
+
const normalizedModuleSchemas = normalizeSchemaRefs(module.schemas);
|
|
537
|
+
for (const schemaName in normalizedModuleSchemas) {
|
|
338
538
|
try {
|
|
339
539
|
// 尝试单独编译每个 schema
|
|
340
540
|
const testSchema = {
|
|
@@ -342,11 +542,11 @@ const handleBuild = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
342
542
|
type: 'object',
|
|
343
543
|
properties: {},
|
|
344
544
|
additionalProperties: false,
|
|
345
|
-
definitions: { [schemaName]:
|
|
346
|
-
components: { schemas: { [schemaName]:
|
|
545
|
+
definitions: { [schemaName]: normalizedModuleSchemas[schemaName] },
|
|
546
|
+
components: { schemas: { [schemaName]: normalizedModuleSchemas[schemaName] } }
|
|
347
547
|
};
|
|
348
548
|
yield (0, json_schema_to_typescript_1.compile)(testSchema, 'test', { unreachableDefinitions: true });
|
|
349
|
-
validSchemas[schemaName] = Object.assign(Object.assign({},
|
|
549
|
+
validSchemas[schemaName] = Object.assign(Object.assign({}, normalizedModuleSchemas[schemaName]), { title: schemaName });
|
|
350
550
|
}
|
|
351
551
|
catch (schemaError) {
|
|
352
552
|
invalidSchemas.push(schemaName);
|
|
@@ -413,70 +613,83 @@ const handleBuild = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
413
613
|
console.log(chalk_1.default.blue(`为模块 "${moduleName}" 生成基于 JSDoc 的类型定义...`));
|
|
414
614
|
// 创建类型定义映射
|
|
415
615
|
const typeDefinitions = {};
|
|
616
|
+
const nestedReferencedTypes = new Set();
|
|
416
617
|
// 从 endpoints 中提取类型定义
|
|
417
618
|
module.endpoints.forEach(endpoint => {
|
|
418
|
-
//
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
// 如果包含 null,确保格式正确
|
|
428
|
-
if (type.includes('null')) {
|
|
429
|
-
type = type.replace(/\s*\|\s*null/g, ' | null');
|
|
430
|
-
}
|
|
619
|
+
// 辅助函数:将 jsdoc params 转换为字段
|
|
620
|
+
const buildFields = (params) => params
|
|
621
|
+
.filter((param) => param.name && param.type)
|
|
622
|
+
.map((param) => {
|
|
623
|
+
const optional = !param.required ? '?' : '';
|
|
624
|
+
let type = param.type;
|
|
625
|
+
if (type && type.includes(' | ')) {
|
|
626
|
+
if (type.includes('null')) {
|
|
627
|
+
type = type.replace(/\s*\|\s*null/g, ' | null');
|
|
431
628
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
type = convertOpenApiTypeToTypeScript(type);
|
|
632
|
+
}
|
|
633
|
+
extractTypeIdentifiers(type).forEach(typeName => {
|
|
634
|
+
if (isValidTypeIdentifier(typeName)) {
|
|
635
|
+
nestedReferencedTypes.add(typeName);
|
|
435
636
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
|
|
637
|
+
});
|
|
638
|
+
const comment = param.description ? ` // ${param.description}` : '';
|
|
639
|
+
return ` ${param.name}${optional}: ${type};${comment}`;
|
|
640
|
+
})
|
|
641
|
+
.join('\n');
|
|
642
|
+
// 处理 params 类型 - 使用 paramsJsdocParams
|
|
643
|
+
const requestParamsTypeName = endpoint.requestParamsTypeName;
|
|
644
|
+
if (requestParamsTypeName && isValidTypeIdentifier(requestParamsTypeName) && endpoint.paramsJsdocParams && endpoint.paramsJsdocParams.length > 0) {
|
|
645
|
+
const paramsFields = buildFields(endpoint.paramsJsdocParams);
|
|
440
646
|
if (paramsFields) {
|
|
441
|
-
typeDefinitions[
|
|
442
|
-
${paramsFields}
|
|
647
|
+
typeDefinitions[requestParamsTypeName] = `export interface ${requestParamsTypeName} {
|
|
648
|
+
${paramsFields}
|
|
443
649
|
}`;
|
|
444
650
|
}
|
|
445
651
|
}
|
|
446
|
-
// 处理 data 类型 (requestBody)
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
.map(param => {
|
|
451
|
-
const optional = !param.required ? '?' : '';
|
|
452
|
-
let type = param.type;
|
|
453
|
-
// 处理联合类型
|
|
454
|
-
if (type && type.includes(' | ')) {
|
|
455
|
-
// 如果包含 null,确保格式正确
|
|
456
|
-
if (type.includes('null')) {
|
|
457
|
-
type = type.replace(/\s*\|\s*null/g, ' | null');
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
else {
|
|
461
|
-
// 处理基本类型转换
|
|
462
|
-
type = convertOpenApiTypeToTypeScript(type);
|
|
463
|
-
}
|
|
464
|
-
const comment = param.description ? ` // ${param.description}` : '';
|
|
465
|
-
return ` ${param.name}${optional}: ${type};${comment}`;
|
|
466
|
-
})
|
|
467
|
-
.join('\n');
|
|
652
|
+
// 处理 data 类型 (requestBody) - 使用 dataJsdocParams
|
|
653
|
+
const requestBodyTypeName = endpoint.requestBodyTypeName;
|
|
654
|
+
if (requestBodyTypeName && isValidTypeIdentifier(requestBodyTypeName) && endpoint.dataJsdocParams && endpoint.dataJsdocParams.length > 0) {
|
|
655
|
+
const dataFields = buildFields(endpoint.dataJsdocParams);
|
|
468
656
|
if (dataFields) {
|
|
469
|
-
typeDefinitions[
|
|
470
|
-
${dataFields}
|
|
657
|
+
typeDefinitions[requestBodyTypeName] = `export interface ${requestBodyTypeName} {
|
|
658
|
+
${dataFields}
|
|
659
|
+
}`;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
// 处理 response 类型 - 使用 responseJsdocParams
|
|
663
|
+
const responseTargetType = resolveInterfaceTargetType(endpoint.responseTypeName);
|
|
664
|
+
if (responseTargetType && endpoint.responseTypeName !== 'void' && endpoint.responseJsdocParams && endpoint.responseJsdocParams.length > 0) {
|
|
665
|
+
const responseFields = buildFields(endpoint.responseJsdocParams);
|
|
666
|
+
if (responseFields) {
|
|
667
|
+
typeDefinitions[responseTargetType] = `export interface ${responseTargetType} {
|
|
668
|
+
${responseFields}
|
|
471
669
|
}`;
|
|
472
670
|
}
|
|
473
671
|
}
|
|
474
672
|
});
|
|
475
673
|
// 为没有定义的类型添加基本定义
|
|
476
|
-
|
|
674
|
+
const fallbackReferencedTypes = [
|
|
675
|
+
...new Set([
|
|
676
|
+
...allReferencedTypes,
|
|
677
|
+
...Object.keys(module.schemas || {}),
|
|
678
|
+
...nestedReferencedTypes,
|
|
679
|
+
])
|
|
680
|
+
];
|
|
681
|
+
fallbackReferencedTypes.forEach(typeName => {
|
|
682
|
+
if (!isValidTypeIdentifier(typeName)) {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
477
685
|
if (!typeDefinitions[typeName]) {
|
|
478
|
-
|
|
479
|
-
|
|
686
|
+
const schemaBasedDefinition = buildInterfaceDefinitionFromModuleSchemas(typeName, module.schemas);
|
|
687
|
+
if (schemaBasedDefinition) {
|
|
688
|
+
typeDefinitions[typeName] = schemaBasedDefinition;
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
typeDefinitions[typeName] = `export interface ${typeName} {
|
|
692
|
+
[key: string]: any;
|
|
480
693
|
}`;
|
|
481
694
|
}
|
|
482
695
|
});
|