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.
@@ -48,7 +48,7 @@ function convertOpenApiTypeToTypeScript(openApiType) {
48
48
  case 'number':
49
49
  return 'number';
50
50
  default:
51
- return 'any';
51
+ return openApiType;
52
52
  }
53
53
  }
54
54
  // 改进类型定义的辅助函数
@@ -93,6 +93,138 @@ function findInterfaceBlockRange(content, typeName) {
93
93
  }
94
94
  return null;
95
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
+ }
96
228
  function improveTypeDefinitions(typesContent, endpoints) {
97
229
  let improvedContent = typesContent;
98
230
  // 收集所有需要改进的类型定义
@@ -124,8 +256,8 @@ function improveTypeDefinitions(typesContent, endpoints) {
124
256
  if (requestParamsTypeName && isValidTypeIdentifier(requestParamsTypeName) && endpoint.paramsJsdocParams && endpoint.paramsJsdocParams.length > 0) {
125
257
  const paramsFields = buildFieldsFromJsdocParams(endpoint.paramsJsdocParams);
126
258
  if (paramsFields) {
127
- typeImprovements[requestParamsTypeName] = `export interface ${requestParamsTypeName} {
128
- ${paramsFields}
259
+ typeImprovements[requestParamsTypeName] = `export interface ${requestParamsTypeName} {
260
+ ${paramsFields}
129
261
  }`;
130
262
  }
131
263
  }
@@ -134,8 +266,8 @@ ${paramsFields}
134
266
  if (requestBodyTypeName && isValidTypeIdentifier(requestBodyTypeName) && endpoint.dataJsdocParams && endpoint.dataJsdocParams.length > 0) {
135
267
  const dataFields = buildFieldsFromJsdocParams(endpoint.dataJsdocParams);
136
268
  if (dataFields) {
137
- typeImprovements[requestBodyTypeName] = `export interface ${requestBodyTypeName} {
138
- ${dataFields}
269
+ typeImprovements[requestBodyTypeName] = `export interface ${requestBodyTypeName} {
270
+ ${dataFields}
139
271
  }`;
140
272
  }
141
273
  }
@@ -144,8 +276,8 @@ ${dataFields}
144
276
  if (responseTargetType && endpoint.responseTypeName !== 'void' && endpoint.responseJsdocParams && endpoint.responseJsdocParams.length > 0) {
145
277
  const responseFields = buildFieldsFromJsdocParams(endpoint.responseJsdocParams);
146
278
  if (responseFields) {
147
- typeImprovements[responseTargetType] = `export interface ${responseTargetType} {
148
- ${responseFields}
279
+ typeImprovements[responseTargetType] = `export interface ${responseTargetType} {
280
+ ${responseFields}
149
281
  }`;
150
282
  }
151
283
  }
@@ -252,6 +384,25 @@ function normalizeSchemaRefs(value) {
252
384
  }
253
385
  return value;
254
386
  }
387
+ function stripNestedSchemaTitles(value, depth = 0) {
388
+ if (Array.isArray(value)) {
389
+ return value.map(item => stripNestedSchemaTitles(item, depth + 1));
390
+ }
391
+ if (value && typeof value === 'object') {
392
+ const obj = value;
393
+ const normalized = {};
394
+ for (const [key, currentValue] of Object.entries(obj)) {
395
+ // json-schema-to-typescript may generate invalid type aliases from nested title values
396
+ // (e.g. Chinese punctuation or numeric-leading titles), so keep only root-level titles.
397
+ if (key === 'title' && depth > 0) {
398
+ continue;
399
+ }
400
+ normalized[key] = stripNestedSchemaTitles(currentValue, depth + 1);
401
+ }
402
+ return normalized;
403
+ }
404
+ return value;
405
+ }
255
406
  const handleBuild = () => __awaiter(void 0, void 0, void 0, function* () {
256
407
  try {
257
408
  console.log(chalk_1.default.blue('Building API from source...'));
@@ -351,7 +502,7 @@ const handleBuild = () => __awaiter(void 0, void 0, void 0, function* () {
351
502
  if (Object.keys(module.schemas).length > 0) {
352
503
  console.log(chalk_1.default.blue(`正在为模块 "${moduleName}" 生成类型文件,包含 ${Object.keys(module.schemas).length} 个 schema`));
353
504
  try {
354
- const normalizedModuleSchemas = normalizeSchemaRefs(module.schemas);
505
+ const normalizedModuleSchemas = stripNestedSchemaTitles(normalizeSchemaRefs(module.schemas));
355
506
  const schemasWithTitles = {};
356
507
  for (const schemaName in normalizedModuleSchemas) {
357
508
  schemasWithTitles[schemaName] = Object.assign(Object.assign({}, normalizedModuleSchemas[schemaName]), { title: schemaName });
@@ -401,7 +552,7 @@ const handleBuild = () => __awaiter(void 0, void 0, void 0, function* () {
401
552
  // 尝试逐个验证 schema,过滤掉有问题的
402
553
  const validSchemas = {};
403
554
  const invalidSchemas = [];
404
- const normalizedModuleSchemas = normalizeSchemaRefs(module.schemas);
555
+ const normalizedModuleSchemas = stripNestedSchemaTitles(normalizeSchemaRefs(module.schemas));
405
556
  for (const schemaName in normalizedModuleSchemas) {
406
557
  try {
407
558
  // 尝试单独编译每个 schema
@@ -481,6 +632,7 @@ const handleBuild = () => __awaiter(void 0, void 0, void 0, function* () {
481
632
  console.log(chalk_1.default.blue(`为模块 "${moduleName}" 生成基于 JSDoc 的类型定义...`));
482
633
  // 创建类型定义映射
483
634
  const typeDefinitions = {};
635
+ const nestedReferencedTypes = new Set();
484
636
  // 从 endpoints 中提取类型定义
485
637
  module.endpoints.forEach(endpoint => {
486
638
  // 辅助函数:将 jsdoc params 转换为字段
@@ -497,6 +649,11 @@ const handleBuild = () => __awaiter(void 0, void 0, void 0, function* () {
497
649
  else {
498
650
  type = convertOpenApiTypeToTypeScript(type);
499
651
  }
652
+ extractTypeIdentifiers(type).forEach(typeName => {
653
+ if (isValidTypeIdentifier(typeName)) {
654
+ nestedReferencedTypes.add(typeName);
655
+ }
656
+ });
500
657
  const comment = param.description ? ` // ${param.description}` : '';
501
658
  return ` ${param.name}${optional}: ${type};${comment}`;
502
659
  })
@@ -506,8 +663,8 @@ const handleBuild = () => __awaiter(void 0, void 0, void 0, function* () {
506
663
  if (requestParamsTypeName && isValidTypeIdentifier(requestParamsTypeName) && endpoint.paramsJsdocParams && endpoint.paramsJsdocParams.length > 0) {
507
664
  const paramsFields = buildFields(endpoint.paramsJsdocParams);
508
665
  if (paramsFields) {
509
- typeDefinitions[requestParamsTypeName] = `export interface ${requestParamsTypeName} {
510
- ${paramsFields}
666
+ typeDefinitions[requestParamsTypeName] = `export interface ${requestParamsTypeName} {
667
+ ${paramsFields}
511
668
  }`;
512
669
  }
513
670
  }
@@ -516,8 +673,8 @@ ${paramsFields}
516
673
  if (requestBodyTypeName && isValidTypeIdentifier(requestBodyTypeName) && endpoint.dataJsdocParams && endpoint.dataJsdocParams.length > 0) {
517
674
  const dataFields = buildFields(endpoint.dataJsdocParams);
518
675
  if (dataFields) {
519
- typeDefinitions[requestBodyTypeName] = `export interface ${requestBodyTypeName} {
520
- ${dataFields}
676
+ typeDefinitions[requestBodyTypeName] = `export interface ${requestBodyTypeName} {
677
+ ${dataFields}
521
678
  }`;
522
679
  }
523
680
  }
@@ -526,20 +683,32 @@ ${dataFields}
526
683
  if (responseTargetType && endpoint.responseTypeName !== 'void' && endpoint.responseJsdocParams && endpoint.responseJsdocParams.length > 0) {
527
684
  const responseFields = buildFields(endpoint.responseJsdocParams);
528
685
  if (responseFields) {
529
- typeDefinitions[responseTargetType] = `export interface ${responseTargetType} {
530
- ${responseFields}
686
+ typeDefinitions[responseTargetType] = `export interface ${responseTargetType} {
687
+ ${responseFields}
531
688
  }`;
532
689
  }
533
690
  }
534
691
  });
535
692
  // 为没有定义的类型添加基本定义
536
- allReferencedTypes.forEach(typeName => {
693
+ const fallbackReferencedTypes = [
694
+ ...new Set([
695
+ ...allReferencedTypes,
696
+ ...Object.keys(module.schemas || {}),
697
+ ...nestedReferencedTypes,
698
+ ])
699
+ ];
700
+ fallbackReferencedTypes.forEach(typeName => {
537
701
  if (!isValidTypeIdentifier(typeName)) {
538
702
  return;
539
703
  }
540
704
  if (!typeDefinitions[typeName]) {
541
- typeDefinitions[typeName] = `export interface ${typeName} {
542
- [key: string]: any;
705
+ const schemaBasedDefinition = buildInterfaceDefinitionFromModuleSchemas(typeName, module.schemas);
706
+ if (schemaBasedDefinition) {
707
+ typeDefinitions[typeName] = schemaBasedDefinition;
708
+ return;
709
+ }
710
+ typeDefinitions[typeName] = `export interface ${typeName} {
711
+ [key: string]: any;
543
712
  }`;
544
713
  }
545
714
  });
@@ -87,11 +87,68 @@ function convertOpenApiTypeToTypeScript(openApiType) {
87
87
  case 'number':
88
88
  return 'number';
89
89
  default:
90
- return 'any';
90
+ return openApiType;
91
91
  }
92
92
  }
93
+ function resolveSchemaToTypeScript(schema, allSchemas) {
94
+ if (!schema)
95
+ return 'any';
96
+ if (isReferenceObject(schema)) {
97
+ return getSchemaName(schema.$ref) || 'any';
98
+ }
99
+ if (schema.anyOf && Array.isArray(schema.anyOf) && schema.anyOf.length > 0) {
100
+ const unionTypes = schema.anyOf
101
+ .map(item => resolveSchemaToTypeScript(item, allSchemas))
102
+ .filter(Boolean);
103
+ if (unionTypes.length > 0) {
104
+ return [...new Set(unionTypes)].join(' | ');
105
+ }
106
+ }
107
+ if (schema.oneOf && Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {
108
+ const unionTypes = schema.oneOf
109
+ .map(item => resolveSchemaToTypeScript(item, allSchemas))
110
+ .filter(Boolean);
111
+ if (unionTypes.length > 0) {
112
+ return [...new Set(unionTypes)].join(' | ');
113
+ }
114
+ }
115
+ if (schema.allOf && Array.isArray(schema.allOf) && schema.allOf.length > 0) {
116
+ const intersectionTypes = schema.allOf
117
+ .map(item => resolveSchemaToTypeScript(item, allSchemas))
118
+ .filter(Boolean);
119
+ if (intersectionTypes.length > 0) {
120
+ return [...new Set(intersectionTypes)].join(' & ');
121
+ }
122
+ }
123
+ if (schema.type === 'array') {
124
+ const itemType = resolveSchemaToTypeScript(schema.items, allSchemas);
125
+ return `${itemType || 'any'}[]`;
126
+ }
127
+ if (schema.type === 'object') {
128
+ if (schema.properties && Object.keys(schema.properties).length > 0) {
129
+ const requiredSet = new Set(schema.required || []);
130
+ const inlineFields = Object.entries(schema.properties).map(([key, value]) => {
131
+ const fieldType = resolveSchemaToTypeScript(value, allSchemas);
132
+ const optional = requiredSet.has(key) ? '' : '?';
133
+ return `${key}${optional}: ${fieldType || 'any'}`;
134
+ });
135
+ return `{ ${inlineFields.join('; ')} }`;
136
+ }
137
+ if (schema.additionalProperties) {
138
+ if (schema.additionalProperties === true) {
139
+ return 'Record<string, any>';
140
+ }
141
+ const valueType = resolveSchemaToTypeScript(schema.additionalProperties, allSchemas);
142
+ return `Record<string, ${valueType || 'any'}>`;
143
+ }
144
+ return 'any';
145
+ }
146
+ const baseType = convertOpenApiTypeToTypeScript(schema.type);
147
+ const nullable = schema.nullable ? ' | null' : '';
148
+ return `${baseType}${nullable}`;
149
+ }
93
150
  function extractJsdocParamsFromSchema(schema, allSchemas) {
94
- var _a;
151
+ var _a, _b;
95
152
  const params = [];
96
153
  if (!schema)
97
154
  return params;
@@ -106,9 +163,12 @@ function extractJsdocParamsFromSchema(schema, allSchemas) {
106
163
  if (targetSchema === null || targetSchema === void 0 ? void 0 : targetSchema.properties) {
107
164
  for (const propName in targetSchema.properties) {
108
165
  const prop = targetSchema.properties[propName];
109
- let propType = 'any';
166
+ let propType = resolveSchemaToTypeScript(prop, allSchemas);
167
+ const propDescription = isReferenceObject(prop)
168
+ ? (((_a = allSchemas[getSchemaName(prop.$ref)]) === null || _a === void 0 ? void 0 : _a.description) || '')
169
+ : ((prop === null || prop === void 0 ? void 0 : prop.description) || '');
110
170
  // 处理 anyOf 数组 (FastAPI 常用的联合类型)
111
- if (prop.anyOf && Array.isArray(prop.anyOf)) {
171
+ if (false && prop.anyOf && Array.isArray(prop.anyOf)) {
112
172
  const types = prop.anyOf.map(item => {
113
173
  if (isReferenceObject(item)) {
114
174
  return getSchemaName(item.$ref);
@@ -130,7 +190,7 @@ function extractJsdocParamsFromSchema(schema, allSchemas) {
130
190
  }
131
191
  }
132
192
  // 处理 oneOf 数组
133
- else if (prop.oneOf && Array.isArray(prop.oneOf)) {
193
+ else if (false && prop.oneOf && Array.isArray(prop.oneOf)) {
134
194
  const types = prop.oneOf.map(item => {
135
195
  if (isReferenceObject(item)) {
136
196
  return getSchemaName(item.$ref);
@@ -152,14 +212,26 @@ function extractJsdocParamsFromSchema(schema, allSchemas) {
152
212
  }
153
213
  }
154
214
  // 处理普通类型
155
- else if (prop.type) {
156
- propType = convertOpenApiTypeToTypeScript(prop.type);
215
+ else if (false && prop.type) {
216
+ const propSchema = prop;
217
+ if (propSchema.type === 'array' && propSchema.items) {
218
+ if (isReferenceObject(propSchema.items)) {
219
+ propType = `${getSchemaName(propSchema.items.$ref)}[]`;
220
+ }
221
+ else {
222
+ const itemType = convertOpenApiTypeToTypeScript(propSchema.items.type);
223
+ propType = `${itemType}[]`;
224
+ }
225
+ }
226
+ else {
227
+ propType = convertOpenApiTypeToTypeScript(propSchema.type);
228
+ }
157
229
  }
158
230
  params.push({
159
231
  name: propName,
160
- type: propType,
161
- description: (prop === null || prop === void 0 ? void 0 : prop.description) || '',
162
- required: (_a = targetSchema.required) === null || _a === void 0 ? void 0 : _a.includes(propName)
232
+ type: propType || 'any',
233
+ description: propDescription,
234
+ required: (_b = targetSchema.required) === null || _b === void 0 ? void 0 : _b.includes(propName)
163
235
  });
164
236
  }
165
237
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "czh-api",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "A CLI tool to generate TypeScript API clients from Swagger/OpenAPI documents.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {