@acrool/rtk-query-codegen-openapi 1.3.0 → 1.4.0-alpha.1

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.
@@ -21,11 +21,46 @@ function renameIdentifier(node: ts.Node, oldName: string, newName: string): ts.N
21
21
  ]).transformed[0];
22
22
  }
23
23
 
24
+ /**
25
+ * 判斷 TypeAliasDeclaration 是否為 string literal union(即 OpenAPI enum)
26
+ * 例如: type ETaskCategory = "feat" | "fix" | "refactor"
27
+ */
28
+ function isStringEnumType(node: ts.TypeAliasDeclaration): string[] | null {
29
+ if (!ts.isUnionTypeNode(node.type)) return null;
30
+
31
+ const members: string[] = [];
32
+ for (const member of node.type.types) {
33
+ if (ts.isLiteralTypeNode(member) && ts.isStringLiteral(member.literal)) {
34
+ members.push(member.literal.text);
35
+ } else {
36
+ return null; // 含有非 string literal 的成員,不是 enum
37
+ }
38
+ }
39
+ return members.length > 0 ? members : null;
40
+ }
41
+
42
+ /**
43
+ * 將 string union type 轉換為 enum 宣告字串
44
+ * 例如: export enum ETaskCategory { Feat = "feat", Fix = "fix", Refactor = "refactor" }
45
+ */
46
+ function generateEnumDeclaration(name: string, members: string[]): string {
47
+ const enumMembers = members.map(value => {
48
+ // enum key: 首字母大寫的 camelCase
49
+ const key = value.charAt(0).toUpperCase() + value.slice(1);
50
+ return ` ${key} = "${value}"`;
51
+ });
52
+ return `export enum ${name} {\n${enumMembers.join(',\n')}\n}`
53
+ }
54
+
24
55
  /**
25
56
  * 產生 component-schema.ts 內容
26
57
  * @param interfaces
58
+ * @param includeOnly - 若提供,則只輸出此 Set 中的類型名稱
27
59
  */
28
- export function generateComponentSchemaFile(interfaces: Record<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration>) {
60
+ export function generateComponentSchemaFile(
61
+ interfaces: Record<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration>,
62
+ includeOnly?: Set<string>
63
+ ) {
29
64
  const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
30
65
 
31
66
  const resultFile = ts.createSourceFile(
@@ -44,6 +79,20 @@ export function generateComponentSchemaFile(interfaces: Record<string, ts.Interf
44
79
  const pascalCaseName = toPascalCase(originalName);
45
80
  typeNameMapping[originalName] = pascalCaseName;
46
81
 
82
+ // 如果有過濾條件,跳過不在名單中的類型
83
+ if (includeOnly && !includeOnly.has(originalName)) {
84
+ return;
85
+ }
86
+
87
+ // 偵測 string union type 並轉換為 enum
88
+ if (ts.isTypeAliasDeclaration(node)) {
89
+ const enumMembers = isStringEnumType(node);
90
+ if (enumMembers) {
91
+ renamedInterfaces.push(generateEnumDeclaration(pascalCaseName, enumMembers));
92
+ return;
93
+ }
94
+ }
95
+
47
96
  // 重新命名節點
48
97
  const renamedNode = renameIdentifier(node, originalName, pascalCaseName);
49
98
  const printed = printer.printNode(ts.EmitHint.Unspecified, renamedNode, resultFile);
@@ -8,6 +8,56 @@ const toPascalCase = (name: string): string => {
8
8
  return name.charAt(0).toUpperCase() + name.slice(1);
9
9
  };
10
10
 
11
+ /**
12
+ * 重新命名 TypeScript 節點中的標識符
13
+ */
14
+ function renameIdentifier(node: ts.Node, oldName: string, newName: string): ts.Node {
15
+ return ts.transform(node, [
16
+ context => rootNode => ts.visitNode(rootNode, function visit(node): ts.Node {
17
+ if (ts.isIdentifier(node) && node.text === oldName) {
18
+ return ts.factory.createIdentifier(newName);
19
+ }
20
+ return ts.visitEachChild(node, visit, context);
21
+ })
22
+ ]).transformed[0];
23
+ }
24
+
25
+ /**
26
+ * 將節點中引用到 shared schema 的標識符加上 Schema. 前綴
27
+ * 例如: TaskDto → Schema.TaskDto(如果 TaskDto 是 shared schema)
28
+ */
29
+ function prefixSharedSchemaRefs(
30
+ node: ts.Node,
31
+ allSchemaNames: Set<string>,
32
+ localSchemaNames: Set<string>,
33
+ declarationName: string
34
+ ): ts.Node {
35
+ // 建立需要加前綴的 schema 名稱集合(shared = all - local)
36
+ const sharedNames = new Set<string>();
37
+ for (const name of allSchemaNames) {
38
+ if (!localSchemaNames.has(name)) {
39
+ sharedNames.add(toPascalCase(name));
40
+ }
41
+ }
42
+
43
+ return ts.transform(node, [
44
+ context => rootNode => ts.visitNode(rootNode, function visit(node): ts.Node {
45
+ // 對於類型引用中的標識符,如果是 shared schema 名稱,加上 Schema. 前綴
46
+ if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
47
+ const name = node.typeName.text;
48
+ if (sharedNames.has(name) && name !== declarationName) {
49
+ const qualifiedName = ts.factory.createQualifiedName(
50
+ ts.factory.createIdentifier('Schema'),
51
+ ts.factory.createIdentifier(name)
52
+ );
53
+ return ts.factory.createTypeReferenceNode(qualifiedName, node.typeArguments);
54
+ }
55
+ }
56
+ return ts.visitEachChild(node, visit, context);
57
+ })
58
+ ]).transformed[0];
59
+ }
60
+
11
61
  export interface EndpointInfo {
12
62
  operationName: string;
13
63
  argTypeName: string;
@@ -22,12 +72,53 @@ export interface EndpointInfo {
22
72
  summary: string;
23
73
  }
24
74
 
75
+ /**
76
+ * 判斷 TypeAliasDeclaration 是否為 string literal union(即 OpenAPI enum)
77
+ */
78
+ function isStringEnumType(node: ts.TypeAliasDeclaration): string[] | null {
79
+ if (!ts.isUnionTypeNode(node.type)) return null;
80
+
81
+ const members: string[] = [];
82
+ for (const member of node.type.types) {
83
+ if (ts.isLiteralTypeNode(member) && ts.isStringLiteral(member.literal)) {
84
+ members.push(member.literal.text);
85
+ } else {
86
+ return null;
87
+ }
88
+ }
89
+ return members.length > 0 ? members : null;
90
+ }
91
+
92
+ /**
93
+ * 將 string union type 轉換為 enum 宣告字串
94
+ */
95
+ function generateEnumDeclaration(name: string, members: string[]): string {
96
+ const enumMembers = members.map(value => {
97
+ const key = value.charAt(0).toUpperCase() + value.slice(1);
98
+ return ` ${key} = "${value}"`;
99
+ });
100
+ return `export enum ${name} {\n${enumMembers.join(',\n')}\n}`;
101
+ }
102
+
103
+ /**
104
+ * Group-local schema 類型生成選項
105
+ */
106
+ export interface LocalSchemaOptions {
107
+ /** 此 group 專屬的 schema interfaces(會直接生成在 types.ts 中) */
108
+ localSchemaInterfaces?: Record<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration>;
109
+ /** 此 group 專屬的 schema 名稱集合(用於判斷引用時使用本地名稱還是 Schema.*) */
110
+ localSchemaNames?: Set<string>;
111
+ }
112
+
25
113
  export function generateTypesFile(
26
114
  endpointInfos: EndpointInfo[],
27
115
  _options: GenerationOptions,
28
116
  schemaInterfaces?: Record<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration>,
29
- operationDefinitions?: any[]
117
+ operationDefinitions?: any[],
118
+ localSchemaOptions?: LocalSchemaOptions
30
119
  ) {
120
+ const localSchemaNames = localSchemaOptions?.localSchemaNames ?? new Set<string>();
121
+ const localSchemaInterfaces = localSchemaOptions?.localSchemaInterfaces ?? {};
31
122
 
32
123
  // 創建 schema 類型名稱映射表 - 使用實際生成的類型名稱
33
124
  const schemaTypeMap: Record<string, string> = {};
@@ -51,15 +142,19 @@ export function generateTypesFile(
51
142
  });
52
143
  }
53
144
 
145
+ // 判斷是否有 shared schema 類型需要引用(排除 local 之後仍有 shared 的情況)
146
+ const hasSharedSchemaTypes = schemaInterfaces && Object.keys(schemaInterfaces).some(
147
+ name => !localSchemaNames.has(name)
148
+ );
149
+
54
150
  // 生成 import 語句
55
151
  let importStatement = `/* eslint-disable */
56
- // [Warning] Generated automatically - do not edit manually
57
-
152
+ // [Warning] Generated automatically - do not edit manually
153
+
58
154
  `;
59
155
 
60
- // 檢查是否需要引入 schema.ts
61
- const hasSchemaTypes = schemaInterfaces && Object.keys(schemaInterfaces).length > 0;
62
- if (hasSchemaTypes) {
156
+ // 只有在有 shared schema 類型時才引入 schema.ts
157
+ if (hasSharedSchemaTypes) {
63
158
  importStatement += `import * as Schema from "../schema";\n`;
64
159
  }
65
160
 
@@ -68,8 +163,39 @@ export function generateTypesFile(
68
163
  // 收集所有需要的類型定義
69
164
  const typeDefinitions: string[] = [];
70
165
 
71
- // 注意:不再在 types.ts 中重複生成 schema 類型
72
- // schema 類型已經在 schema.ts 中生成,這裡直接使用 Schema.* 引用
166
+ // 生成 group-local schema 類型定義(原本在 schema.ts,現在移到此 group types.ts)
167
+ if (Object.keys(localSchemaInterfaces).length > 0) {
168
+ const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
169
+ const resultFile = ts.createSourceFile('types.ts', '', ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
170
+ const localTypeDefs: string[] = [];
171
+
172
+ // 建立所有 schema 名稱集合(用於判斷哪些引用需要加 Schema. 前綴)
173
+ const allSchemaNameSet = new Set(schemaInterfaces ? Object.keys(schemaInterfaces) : []);
174
+
175
+ for (const [originalName, node] of Object.entries(localSchemaInterfaces)) {
176
+ const pascalCaseName = toPascalCase(originalName);
177
+
178
+ // 偵測 string union type 並轉換為 enum
179
+ if (ts.isTypeAliasDeclaration(node)) {
180
+ const enumMembers = isStringEnumType(node);
181
+ if (enumMembers) {
182
+ localTypeDefs.push(generateEnumDeclaration(pascalCaseName, enumMembers));
183
+ continue;
184
+ }
185
+ }
186
+
187
+ // 重新命名節點中的宣告名稱
188
+ let transformedNode = renameIdentifier(node, originalName, pascalCaseName);
189
+ // 將引用到 shared schema 的標識符加上 Schema. 前綴
190
+ transformedNode = prefixSharedSchemaRefs(transformedNode, allSchemaNameSet, localSchemaNames, pascalCaseName);
191
+ const printed = printer.printNode(ts.EmitHint.Unspecified, transformedNode, resultFile);
192
+ localTypeDefs.push(printed);
193
+ }
194
+
195
+ if (localTypeDefs.length > 0) {
196
+ typeDefinitions.push(localTypeDefs.join('\n'));
197
+ }
198
+ }
73
199
 
74
200
  // 無論是否有 schema,都要生成 endpoint 特定的 Req/Res 類型
75
201
  const endpointTypes: string[] = [];
@@ -82,7 +208,7 @@ export function generateTypesFile(
82
208
 
83
209
  // 生成 Request 類型(總是生成)
84
210
  if (reqTypeName) {
85
- const requestTypeContent = generateRequestTypeContent(endpoint, operationDefinitions, schemaTypeMap);
211
+ const requestTypeContent = generateRequestTypeContent(endpoint, operationDefinitions, schemaTypeMap, localSchemaNames);
86
212
  if (requestTypeContent.trim() === '') {
87
213
  // 如果沒有實際內容,使用 void
88
214
  endpointTypes.push(
@@ -102,7 +228,7 @@ export function generateTypesFile(
102
228
 
103
229
  // 生成 Response 類型(總是生成)
104
230
  if (resTypeName) {
105
- const responseTypeResult = generateResponseTypeContent(endpoint, operationDefinitions, schemaTypeMap);
231
+ const responseTypeResult = generateResponseTypeContent(endpoint, operationDefinitions, schemaTypeMap, localSchemaNames);
106
232
  if (responseTypeResult.content.trim() === '') {
107
233
  // 如果沒有實際內容,使用 void
108
234
  endpointTypes.push(
@@ -146,14 +272,14 @@ export function generateTypesFile(
146
272
  /**
147
273
  * 生成 Request 類型的內容
148
274
  */
149
- function generateRequestTypeContent(endpoint: EndpointInfo, operationDefinitions?: any[], schemaTypeMap: Record<string, string> = {}): string {
275
+ function generateRequestTypeContent(endpoint: EndpointInfo, operationDefinitions?: any[], schemaTypeMap: Record<string, string> = {}, localSchemaNames: Set<string> = new Set()): string {
150
276
  const properties: string[] = [];
151
277
 
152
278
  // 如果有 query 參數
153
279
  if (endpoint.queryParams && endpoint.queryParams.length > 0) {
154
280
  endpoint.queryParams.forEach(param => {
155
281
  const optional = param.required ? '' : '?';
156
- const paramType = getTypeFromParameter(param, schemaTypeMap);
282
+ const paramType = getTypeFromParameter(param, schemaTypeMap, localSchemaNames);
157
283
  properties.push(` ${param.name}${optional}: ${paramType};`);
158
284
  });
159
285
  }
@@ -162,7 +288,7 @@ function generateRequestTypeContent(endpoint: EndpointInfo, operationDefinitions
162
288
  if (endpoint.pathParams && endpoint.pathParams.length > 0) {
163
289
  endpoint.pathParams.forEach(param => {
164
290
  const optional = param.required ? '' : '?';
165
- const paramType = getTypeFromParameter(param, schemaTypeMap);
291
+ const paramType = getTypeFromParameter(param, schemaTypeMap, localSchemaNames);
166
292
  properties.push(` ${param.name}${optional}: ${paramType};`);
167
293
  });
168
294
  }
@@ -185,18 +311,16 @@ function generateRequestTypeContent(endpoint: EndpointInfo, operationDefinitions
185
311
  const formContent = content['multipart/form-data'] || content['application/x-www-form-urlencoded'];
186
312
 
187
313
  if (jsonContent?.schema) {
188
- // indentLevel=1 因為 body 屬性已經在類型定義內(有 2 個空格縮排)
189
- const bodyType = getTypeFromSchema(jsonContent.schema, schemaTypeMap, 1);
314
+ const bodyType = getTypeFromSchema(jsonContent.schema, schemaTypeMap, 1, localSchemaNames);
190
315
  properties.push(` body: ${bodyType};`);
191
316
  } else if (formContent?.schema) {
192
- // indentLevel=1 因為 body 屬性已經在類型定義內(有 2 個空格縮排)
193
- const bodyType = getTypeFromSchema(formContent.schema, schemaTypeMap, 1);
317
+ const bodyType = getTypeFromSchema(formContent.schema, schemaTypeMap, 1, localSchemaNames);
194
318
  properties.push(` body: ${bodyType};`);
195
319
  } else {
196
320
  // fallback 到第一個可用的 content-type
197
321
  const firstContent = Object.values(content)[0] as any;
198
322
  if (firstContent?.schema) {
199
- const bodyType = getTypeFromSchema(firstContent.schema, schemaTypeMap, 1);
323
+ const bodyType = getTypeFromSchema(firstContent.schema, schemaTypeMap, 1, localSchemaNames);
200
324
  properties.push(` body: ${bodyType};`);
201
325
  } else {
202
326
  properties.push(` body?: any; // Request body from OpenAPI`);
@@ -221,7 +345,7 @@ interface ResponseTypeResult {
221
345
  /**
222
346
  * 生成 Response 類型的內容
223
347
  */
224
- function generateResponseTypeContent(endpoint: EndpointInfo, operationDefinitions?: any[], schemaTypeMap: Record<string, string> = {}): ResponseTypeResult {
348
+ function generateResponseTypeContent(endpoint: EndpointInfo, operationDefinitions?: any[], schemaTypeMap: Record<string, string> = {}, localSchemaNames: Set<string> = new Set()): ResponseTypeResult {
225
349
  // 嘗試從 operationDefinitions 中獲取響應結構
226
350
  const operationDef = operationDefinitions?.find(op => {
227
351
  // 嘗試多種匹配方式
@@ -247,14 +371,14 @@ function generateResponseTypeContent(endpoint: EndpointInfo, operationDefinition
247
371
 
248
372
  // 如果 schema 是 $ref 引用、array、或 primitive,直接使用 getTypeFromSchema
249
373
  if (schema.$ref || schema.type !== 'object' || !schema.properties) {
250
- const directType = getTypeFromSchema(schema, schemaTypeMap, 0);
374
+ const directType = getTypeFromSchema(schema, schemaTypeMap, 0, localSchemaNames);
251
375
  if (directType && directType !== 'any') {
252
376
  return { content: directType, isDirectType: true };
253
377
  }
254
378
  }
255
379
 
256
380
  // 如果是有 properties 的 object,展開為屬性列表
257
- const responseProps = parseSchemaProperties(schema, schemaTypeMap);
381
+ const responseProps = parseSchemaProperties(schema, schemaTypeMap, localSchemaNames);
258
382
  if (responseProps.length > 0) {
259
383
  return { content: responseProps.join('\n'), isDirectType: false };
260
384
  }
@@ -269,7 +393,7 @@ function generateResponseTypeContent(endpoint: EndpointInfo, operationDefinition
269
393
  /**
270
394
  * 解析 OpenAPI schema 的 properties 並生成 TypeScript 屬性定義
271
395
  */
272
- function parseSchemaProperties(schema: any, schemaTypeMap: Record<string, string> = {}): string[] {
396
+ function parseSchemaProperties(schema: any, schemaTypeMap: Record<string, string> = {}, localSchemaNames: Set<string> = new Set()): string[] {
273
397
  const properties: string[] = [];
274
398
 
275
399
  if (schema.type === 'object' && schema.properties) {
@@ -279,7 +403,7 @@ function parseSchemaProperties(schema: any, schemaTypeMap: Record<string, string
279
403
  const isRequired = required.includes(propName);
280
404
  const optional = isRequired ? '' : '?';
281
405
  // indentLevel=1 因為屬性已經在類型定義內(有 2 個空格縮排)
282
- const propType = getTypeFromSchema(propSchema, schemaTypeMap, 1);
406
+ const propType = getTypeFromSchema(propSchema, schemaTypeMap, 1, localSchemaNames);
283
407
 
284
408
  // 如果屬性名包含特殊字符(如 -),需要加上引號
285
409
  const needsQuotes = /[^a-zA-Z0-9_$]/.test(propName);
@@ -302,10 +426,10 @@ function parseSchemaProperties(schema: any, schemaTypeMap: Record<string, string
302
426
  * @param schemaTypeMap 類型名稱映射表
303
427
  * @param indentLevel 縮排層級,用於格式化內嵌物件
304
428
  */
305
- function getTypeFromSchema(schema: any, schemaTypeMap: Record<string, string> = {}, indentLevel: number = 0): string {
429
+ function getTypeFromSchema(schema: any, schemaTypeMap: Record<string, string> = {}, indentLevel: number = 0, localSchemaNames: Set<string> = new Set()): string {
306
430
  if (!schema) return 'any';
307
431
 
308
- // 處理 $ref 引用,使用 Schema.TypeName 格式
432
+ // 處理 $ref 引用
309
433
  if (schema.$ref) {
310
434
  const refPath = schema.$ref;
311
435
  if (refPath.startsWith('#/components/schemas/')) {
@@ -313,7 +437,10 @@ function getTypeFromSchema(schema: any, schemaTypeMap: Record<string, string> =
313
437
  // 使用映射表查找實際的類型名稱,並轉換為大駝峰
314
438
  const actualTypeName = schemaTypeMap[originalTypeName] || originalTypeName;
315
439
  const pascalCaseTypeName = toPascalCase(actualTypeName);
316
- const baseType = `Schema.${pascalCaseTypeName}`;
440
+ // 如果是 local schema,直接使用本地名稱;否則使用 Schema.TypeName
441
+ const baseType = localSchemaNames.has(originalTypeName)
442
+ ? pascalCaseTypeName
443
+ : `Schema.${pascalCaseTypeName}`;
317
444
  // 處理 nullable
318
445
  return schema.nullable ? `${baseType} | null` : baseType;
319
446
  }
@@ -340,7 +467,7 @@ function getTypeFromSchema(schema: any, schemaTypeMap: Record<string, string> =
340
467
  baseType = 'boolean';
341
468
  break;
342
469
  case 'array':
343
- const itemType = schema.items ? getTypeFromSchema(schema.items, schemaTypeMap, indentLevel) : 'any';
470
+ const itemType = schema.items ? getTypeFromSchema(schema.items, schemaTypeMap, indentLevel, localSchemaNames) : 'any';
344
471
  // 如果 itemType 包含聯合類型(包含 |),需要加括號
345
472
  const needsParentheses = itemType.includes('|');
346
473
  baseType = needsParentheses ? `(${itemType})[]` : `${itemType}[]`;
@@ -355,7 +482,7 @@ function getTypeFromSchema(schema: any, schemaTypeMap: Record<string, string> =
355
482
  if (schema.additionalProperties) {
356
483
  const valueType = schema.additionalProperties === true
357
484
  ? 'any'
358
- : getTypeFromSchema(schema.additionalProperties, schemaTypeMap, indentLevel);
485
+ : getTypeFromSchema(schema.additionalProperties, schemaTypeMap, indentLevel, localSchemaNames);
359
486
  baseType = `Record<string, ${valueType}>`;
360
487
  } else {
361
488
  baseType = '{}';
@@ -369,7 +496,7 @@ function getTypeFromSchema(schema: any, schemaTypeMap: Record<string, string> =
369
496
  entries.forEach(([key, propSchema]: [string, any]) => {
370
497
  const required = schema.required || [];
371
498
  const optional = required.includes(key) ? '' : '?';
372
- const type = getTypeFromSchema(propSchema, schemaTypeMap, indentLevel + 1);
499
+ const type = getTypeFromSchema(propSchema, schemaTypeMap, indentLevel + 1, localSchemaNames);
373
500
 
374
501
  // 如果屬性名包含特殊字符(如 -),需要加上引號
375
502
  const needsQuotes = /[^a-zA-Z0-9_$]/.test(key);
@@ -388,7 +515,7 @@ function getTypeFromSchema(schema: any, schemaTypeMap: Record<string, string> =
388
515
  // 如果沒有 properties 但有 additionalProperties
389
516
  const valueType = schema.additionalProperties === true
390
517
  ? 'any'
391
- : getTypeFromSchema(schema.additionalProperties, schemaTypeMap, indentLevel);
518
+ : getTypeFromSchema(schema.additionalProperties, schemaTypeMap, indentLevel, localSchemaNames);
392
519
  baseType = `Record<string, ${valueType}>`;
393
520
  } else {
394
521
  baseType = 'any';
@@ -406,7 +533,7 @@ function getTypeFromSchema(schema: any, schemaTypeMap: Record<string, string> =
406
533
  /**
407
534
  * 從參數定義中獲取 TypeScript 類型
408
535
  */
409
- function getTypeFromParameter(param: any, schemaTypeMap: Record<string, string> = {}): string {
536
+ function getTypeFromParameter(param: any, schemaTypeMap: Record<string, string> = {}, localSchemaNames: Set<string> = new Set()): string {
410
537
  if (!param.schema) return 'any';
411
- return getTypeFromSchema(param.schema, schemaTypeMap);
538
+ return getTypeFromSchema(param.schema, schemaTypeMap, 0, localSchemaNames);
412
539
  }
@@ -8,10 +8,13 @@ import { OpenApiParserService } from './openapi-parser-service';
8
8
  import { generateCommonTypesFile } from '../generators/common-types-generator';
9
9
  import { generateComponentSchemaFile } from '../generators/component-schema-generator';
10
10
  import { generateDoNotModifyFile } from '../generators/do-not-modify-generator';
11
- import type { GenerationOptions, CommonOptions } from '../types';
11
+ import type { GenerationOptions, CommonOptions, OperationDefinition } from '../types';
12
12
  import { ApiCodeGenerator } from './api-code-generator';
13
+ import { EndpointInfoExtractor } from './endpoint-info-extractor';
13
14
  import { generateUtilsFile } from '../generators/utils-generator';
14
15
  import { generateTagTypesFile } from '../generators/tag-types-generator';
16
+ import { analyzeSchemaRefs } from '../utils/schema-ref-analyzer';
17
+ import { generateTypesFile } from '../generators/types-generator';
15
18
 
16
19
  /**
17
20
  * 統一代碼生成器選項
@@ -82,6 +85,11 @@ export class UnifiedCodeGenerator {
82
85
  // 收集所有 tags
83
86
  private allTags: Set<string> = new Set();
84
87
 
88
+ // 收集每個 group 的 operation definitions(用於 schema 引用分析)
89
+ private groupOperationDefs: Map<string, OperationDefinition[]> = new Map();
90
+ // 收集每個 group 的生成選項(用於重新生成 types)
91
+ private groupGenerationOptions: Map<string, GenerationOptions> = new Map();
92
+
85
93
 
86
94
  constructor(options: UnifiedGenerationOptions) {
87
95
  this._options = options;
@@ -95,16 +103,18 @@ export class UnifiedCodeGenerator {
95
103
  async generateAll(): Promise<UnifiedGenerationResult> {
96
104
  await this.prepare();
97
105
 
98
- // 生成各API文件
106
+ // 生成各API文件(第一階段:生成所有 group 的內容)
99
107
  await this.generateApi();
100
- // await this.generateQuery();
108
+
109
+ // 分析 schema 引用並拆分(第二階段:將 group-local schema 移到各 group 的 types.ts)
110
+ this.splitSchemaByUsage();
101
111
 
102
112
  // 生成共用
103
- this.generateCommonTypesContent()
104
- this.generateSchemaContent()
105
- this.generateUtilsContent()
106
- this.generateDoNotModifyContent()
107
- this.generateTagTypesContent()
113
+ this.generateCommonTypesContent();
114
+ this.generateSchemaContent();
115
+ this.generateUtilsContent();
116
+ this.generateDoNotModifyContent();
117
+ this.generateTagTypesContent();
108
118
 
109
119
  return await this.release();
110
120
  }
@@ -200,6 +210,74 @@ export class UnifiedCodeGenerator {
200
210
 
201
211
 
202
212
 
213
+ /**
214
+ * 分析 schema 引用,將只被單一 group 使用的 schema 移到該 group 的 types.ts
215
+ */
216
+ private splitSchemaByUsage(): void {
217
+ if (!this.openApiDoc || this.groupOperationDefs.size === 0) return;
218
+
219
+ // 獲取 OpenAPI 原始 schema 定義(用於遞迴分析 $ref 依賴)
220
+ const rawSchemas = this.openApiDoc.components?.schemas ?? {};
221
+ const allSchemaNames = Object.keys(this.schemaInterfaces);
222
+
223
+ // 分析引用
224
+ const analysis = analyzeSchemaRefs(
225
+ this.groupOperationDefs,
226
+ rawSchemas as Record<string, any>,
227
+ allSchemaNames
228
+ );
229
+
230
+ // 儲存 shared schema 名稱(供 generateSchemaContent 使用)
231
+ this.sharedSchemaNames = analysis.sharedSchemas;
232
+
233
+ // 為每個 group 重新生成 types.ts(包含 local schema 定義)
234
+ for (const group of this.generatedContent.groups) {
235
+ const localNames = analysis.groupLocalSchemas.get(group.groupKey);
236
+ if (!localNames || localNames.size === 0) continue;
237
+
238
+ const groupOptions = this.groupGenerationOptions.get(group.groupKey);
239
+ if (!groupOptions || !this.parserService) continue;
240
+
241
+ // 收集 local schema interfaces
242
+ const localSchemaInterfaces: Record<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration> = {};
243
+ for (const name of localNames) {
244
+ if (this.schemaInterfaces[name]) {
245
+ localSchemaInterfaces[name] = this.schemaInterfaces[name];
246
+ }
247
+ }
248
+
249
+ // 獲取 endpoint infos(需要重新提取以重新生成 types)
250
+ const infoExtractor = new EndpointInfoExtractor(groupOptions);
251
+ const operationDefs = this.groupOperationDefs.get(group.groupKey) ?? [];
252
+ const endpointInfos = infoExtractor.extractEndpointInfos(operationDefs);
253
+
254
+ const generatorOptions = {
255
+ ...groupOptions,
256
+ apiConfiguration: groupOptions.apiConfiguration || {
257
+ file: '@/store/webapi',
258
+ importName: 'WebApiConfiguration'
259
+ }
260
+ };
261
+
262
+ // 重新生成 types.ts,帶入 local schema 資訊
263
+ const newTypesContent = generateTypesFile(
264
+ endpointInfos,
265
+ generatorOptions,
266
+ this.schemaInterfaces,
267
+ operationDefs,
268
+ {
269
+ localSchemaInterfaces,
270
+ localSchemaNames: localNames,
271
+ }
272
+ );
273
+
274
+ group.content.files.types = newTypesContent;
275
+ }
276
+ }
277
+
278
+ // 儲存 shared schema 名稱(只有這些會寫入 schema.ts)
279
+ private sharedSchemaNames: Set<string> | null = null;
280
+
203
281
  /**
204
282
  * 生成 common types
205
283
  */
@@ -208,10 +286,13 @@ export class UnifiedCodeGenerator {
208
286
  }
209
287
 
210
288
  /**
211
- * 生成Schema
289
+ * 生成Schema(只包含 shared types,排除 group-local 和未使用的 types)
212
290
  */
213
291
  private async generateSchemaContent(): Promise<void> {
214
- this.generatedContent.componentSchema = generateComponentSchemaFile(this.schemaInterfaces);
292
+ this.generatedContent.componentSchema = generateComponentSchemaFile(
293
+ this.schemaInterfaces,
294
+ this.sharedSchemaNames ?? undefined
295
+ );
215
296
  }
216
297
 
217
298
  /**
@@ -358,11 +439,15 @@ export class UnifiedCodeGenerator {
358
439
  if (!this.openApiDoc || !this.parserService) {
359
440
  throw new Error('OpenAPI 文檔未初始化,請先調用 prepare()');
360
441
  }
361
-
442
+
443
+ // 收集此 group 的 operation definitions(用於後續 schema 引用分析)
444
+ const groupOpDefs = this.parserService.getOperationDefinitions(groupOptions.filterEndpoints);
445
+ this.groupOperationDefs.set(groupInfo.groupKey, groupOpDefs);
446
+ this.groupGenerationOptions.set(groupInfo.groupKey, groupOptions);
447
+
362
448
  const apiGenerator = new ApiCodeGenerator(this.parserService, groupOptions);
363
449
  const result = await apiGenerator.generate();
364
450
 
365
-
366
451
  return result;
367
452
  }
368
453
 
@@ -370,12 +455,13 @@ export class UnifiedCodeGenerator {
370
455
  * 生成主 index.ts 檔案
371
456
  */
372
457
  private generateMainIndex(generatedGroups: string[]): string {
373
- const exports = generatedGroups.map(groupKey => `export * from "./${groupKey}";`).join('\n');
458
+ const groupExports = generatedGroups.map(groupKey => `export * from "./${groupKey}";`).join('\n');
374
459
 
375
460
  return `/* eslint-disable */
376
461
  // [Warning] Generated automatically - do not edit manually
377
462
 
378
- ${exports}
463
+ export * from "./schema";
464
+ ${groupExports}
379
465
  `;
380
466
  }
381
467