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

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.
@@ -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,25 @@ export interface EndpointInfo {
22
72
  summary: string;
23
73
  }
24
74
 
75
+ /**
76
+ * Group-local schema 類型生成選項
77
+ */
78
+ export interface LocalSchemaOptions {
79
+ /** 此 group 專屬的 schema interfaces(會直接生成在 types.ts 中) */
80
+ localSchemaInterfaces?: Record<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration>;
81
+ /** 此 group 專屬的 schema 名稱集合(用於判斷引用時使用本地名稱還是 Schema.*) */
82
+ localSchemaNames?: Set<string>;
83
+ }
84
+
25
85
  export function generateTypesFile(
26
86
  endpointInfos: EndpointInfo[],
27
87
  _options: GenerationOptions,
28
88
  schemaInterfaces?: Record<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration>,
29
- operationDefinitions?: any[]
89
+ operationDefinitions?: any[],
90
+ localSchemaOptions?: LocalSchemaOptions
30
91
  ) {
92
+ const localSchemaNames = localSchemaOptions?.localSchemaNames ?? new Set<string>();
93
+ const localSchemaInterfaces = localSchemaOptions?.localSchemaInterfaces ?? {};
31
94
 
32
95
  // 創建 schema 類型名稱映射表 - 使用實際生成的類型名稱
33
96
  const schemaTypeMap: Record<string, string> = {};
@@ -51,15 +114,19 @@ export function generateTypesFile(
51
114
  });
52
115
  }
53
116
 
117
+ // 判斷是否有 shared schema 類型需要引用(排除 local 之後仍有 shared 的情況)
118
+ const hasSharedSchemaTypes = schemaInterfaces && Object.keys(schemaInterfaces).some(
119
+ name => !localSchemaNames.has(name)
120
+ );
121
+
54
122
  // 生成 import 語句
55
123
  let importStatement = `/* eslint-disable */
56
- // [Warning] Generated automatically - do not edit manually
57
-
124
+ // [Warning] Generated automatically - do not edit manually
125
+
58
126
  `;
59
127
 
60
- // 檢查是否需要引入 schema.ts
61
- const hasSchemaTypes = schemaInterfaces && Object.keys(schemaInterfaces).length > 0;
62
- if (hasSchemaTypes) {
128
+ // 只有在有 shared schema 類型時才引入 schema.ts
129
+ if (hasSharedSchemaTypes) {
63
130
  importStatement += `import * as Schema from "../schema";\n`;
64
131
  }
65
132
 
@@ -68,8 +135,29 @@ export function generateTypesFile(
68
135
  // 收集所有需要的類型定義
69
136
  const typeDefinitions: string[] = [];
70
137
 
71
- // 注意:不再在 types.ts 中重複生成 schema 類型
72
- // schema 類型已經在 schema.ts 中生成,這裡直接使用 Schema.* 引用
138
+ // 生成 group-local schema 類型定義(原本在 schema.ts,現在移到此 group types.ts)
139
+ if (Object.keys(localSchemaInterfaces).length > 0) {
140
+ const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
141
+ const resultFile = ts.createSourceFile('types.ts', '', ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
142
+ const localTypeDefs: string[] = [];
143
+
144
+ // 建立所有 schema 名稱集合(用於判斷哪些引用需要加 Schema. 前綴)
145
+ const allSchemaNameSet = new Set(schemaInterfaces ? Object.keys(schemaInterfaces) : []);
146
+
147
+ for (const [originalName, node] of Object.entries(localSchemaInterfaces)) {
148
+ const pascalCaseName = toPascalCase(originalName);
149
+ // 重新命名節點中的宣告名稱
150
+ let transformedNode = renameIdentifier(node, originalName, pascalCaseName);
151
+ // 將引用到 shared schema 的標識符加上 Schema. 前綴
152
+ transformedNode = prefixSharedSchemaRefs(transformedNode, allSchemaNameSet, localSchemaNames, pascalCaseName);
153
+ const printed = printer.printNode(ts.EmitHint.Unspecified, transformedNode, resultFile);
154
+ localTypeDefs.push(printed);
155
+ }
156
+
157
+ if (localTypeDefs.length > 0) {
158
+ typeDefinitions.push(localTypeDefs.join('\n'));
159
+ }
160
+ }
73
161
 
74
162
  // 無論是否有 schema,都要生成 endpoint 特定的 Req/Res 類型
75
163
  const endpointTypes: string[] = [];
@@ -82,7 +170,7 @@ export function generateTypesFile(
82
170
 
83
171
  // 生成 Request 類型(總是生成)
84
172
  if (reqTypeName) {
85
- const requestTypeContent = generateRequestTypeContent(endpoint, operationDefinitions, schemaTypeMap);
173
+ const requestTypeContent = generateRequestTypeContent(endpoint, operationDefinitions, schemaTypeMap, localSchemaNames);
86
174
  if (requestTypeContent.trim() === '') {
87
175
  // 如果沒有實際內容,使用 void
88
176
  endpointTypes.push(
@@ -102,7 +190,7 @@ export function generateTypesFile(
102
190
 
103
191
  // 生成 Response 類型(總是生成)
104
192
  if (resTypeName) {
105
- const responseTypeResult = generateResponseTypeContent(endpoint, operationDefinitions, schemaTypeMap);
193
+ const responseTypeResult = generateResponseTypeContent(endpoint, operationDefinitions, schemaTypeMap, localSchemaNames);
106
194
  if (responseTypeResult.content.trim() === '') {
107
195
  // 如果沒有實際內容,使用 void
108
196
  endpointTypes.push(
@@ -146,14 +234,14 @@ export function generateTypesFile(
146
234
  /**
147
235
  * 生成 Request 類型的內容
148
236
  */
149
- function generateRequestTypeContent(endpoint: EndpointInfo, operationDefinitions?: any[], schemaTypeMap: Record<string, string> = {}): string {
237
+ function generateRequestTypeContent(endpoint: EndpointInfo, operationDefinitions?: any[], schemaTypeMap: Record<string, string> = {}, localSchemaNames: Set<string> = new Set()): string {
150
238
  const properties: string[] = [];
151
239
 
152
240
  // 如果有 query 參數
153
241
  if (endpoint.queryParams && endpoint.queryParams.length > 0) {
154
242
  endpoint.queryParams.forEach(param => {
155
243
  const optional = param.required ? '' : '?';
156
- const paramType = getTypeFromParameter(param, schemaTypeMap);
244
+ const paramType = getTypeFromParameter(param, schemaTypeMap, localSchemaNames);
157
245
  properties.push(` ${param.name}${optional}: ${paramType};`);
158
246
  });
159
247
  }
@@ -162,7 +250,7 @@ function generateRequestTypeContent(endpoint: EndpointInfo, operationDefinitions
162
250
  if (endpoint.pathParams && endpoint.pathParams.length > 0) {
163
251
  endpoint.pathParams.forEach(param => {
164
252
  const optional = param.required ? '' : '?';
165
- const paramType = getTypeFromParameter(param, schemaTypeMap);
253
+ const paramType = getTypeFromParameter(param, schemaTypeMap, localSchemaNames);
166
254
  properties.push(` ${param.name}${optional}: ${paramType};`);
167
255
  });
168
256
  }
@@ -185,18 +273,16 @@ function generateRequestTypeContent(endpoint: EndpointInfo, operationDefinitions
185
273
  const formContent = content['multipart/form-data'] || content['application/x-www-form-urlencoded'];
186
274
 
187
275
  if (jsonContent?.schema) {
188
- // indentLevel=1 因為 body 屬性已經在類型定義內(有 2 個空格縮排)
189
- const bodyType = getTypeFromSchema(jsonContent.schema, schemaTypeMap, 1);
276
+ const bodyType = getTypeFromSchema(jsonContent.schema, schemaTypeMap, 1, localSchemaNames);
190
277
  properties.push(` body: ${bodyType};`);
191
278
  } else if (formContent?.schema) {
192
- // indentLevel=1 因為 body 屬性已經在類型定義內(有 2 個空格縮排)
193
- const bodyType = getTypeFromSchema(formContent.schema, schemaTypeMap, 1);
279
+ const bodyType = getTypeFromSchema(formContent.schema, schemaTypeMap, 1, localSchemaNames);
194
280
  properties.push(` body: ${bodyType};`);
195
281
  } else {
196
282
  // fallback 到第一個可用的 content-type
197
283
  const firstContent = Object.values(content)[0] as any;
198
284
  if (firstContent?.schema) {
199
- const bodyType = getTypeFromSchema(firstContent.schema, schemaTypeMap, 1);
285
+ const bodyType = getTypeFromSchema(firstContent.schema, schemaTypeMap, 1, localSchemaNames);
200
286
  properties.push(` body: ${bodyType};`);
201
287
  } else {
202
288
  properties.push(` body?: any; // Request body from OpenAPI`);
@@ -221,7 +307,7 @@ interface ResponseTypeResult {
221
307
  /**
222
308
  * 生成 Response 類型的內容
223
309
  */
224
- function generateResponseTypeContent(endpoint: EndpointInfo, operationDefinitions?: any[], schemaTypeMap: Record<string, string> = {}): ResponseTypeResult {
310
+ function generateResponseTypeContent(endpoint: EndpointInfo, operationDefinitions?: any[], schemaTypeMap: Record<string, string> = {}, localSchemaNames: Set<string> = new Set()): ResponseTypeResult {
225
311
  // 嘗試從 operationDefinitions 中獲取響應結構
226
312
  const operationDef = operationDefinitions?.find(op => {
227
313
  // 嘗試多種匹配方式
@@ -247,14 +333,14 @@ function generateResponseTypeContent(endpoint: EndpointInfo, operationDefinition
247
333
 
248
334
  // 如果 schema 是 $ref 引用、array、或 primitive,直接使用 getTypeFromSchema
249
335
  if (schema.$ref || schema.type !== 'object' || !schema.properties) {
250
- const directType = getTypeFromSchema(schema, schemaTypeMap, 0);
336
+ const directType = getTypeFromSchema(schema, schemaTypeMap, 0, localSchemaNames);
251
337
  if (directType && directType !== 'any') {
252
338
  return { content: directType, isDirectType: true };
253
339
  }
254
340
  }
255
341
 
256
342
  // 如果是有 properties 的 object,展開為屬性列表
257
- const responseProps = parseSchemaProperties(schema, schemaTypeMap);
343
+ const responseProps = parseSchemaProperties(schema, schemaTypeMap, localSchemaNames);
258
344
  if (responseProps.length > 0) {
259
345
  return { content: responseProps.join('\n'), isDirectType: false };
260
346
  }
@@ -269,7 +355,7 @@ function generateResponseTypeContent(endpoint: EndpointInfo, operationDefinition
269
355
  /**
270
356
  * 解析 OpenAPI schema 的 properties 並生成 TypeScript 屬性定義
271
357
  */
272
- function parseSchemaProperties(schema: any, schemaTypeMap: Record<string, string> = {}): string[] {
358
+ function parseSchemaProperties(schema: any, schemaTypeMap: Record<string, string> = {}, localSchemaNames: Set<string> = new Set()): string[] {
273
359
  const properties: string[] = [];
274
360
 
275
361
  if (schema.type === 'object' && schema.properties) {
@@ -279,7 +365,7 @@ function parseSchemaProperties(schema: any, schemaTypeMap: Record<string, string
279
365
  const isRequired = required.includes(propName);
280
366
  const optional = isRequired ? '' : '?';
281
367
  // indentLevel=1 因為屬性已經在類型定義內(有 2 個空格縮排)
282
- const propType = getTypeFromSchema(propSchema, schemaTypeMap, 1);
368
+ const propType = getTypeFromSchema(propSchema, schemaTypeMap, 1, localSchemaNames);
283
369
 
284
370
  // 如果屬性名包含特殊字符(如 -),需要加上引號
285
371
  const needsQuotes = /[^a-zA-Z0-9_$]/.test(propName);
@@ -302,10 +388,10 @@ function parseSchemaProperties(schema: any, schemaTypeMap: Record<string, string
302
388
  * @param schemaTypeMap 類型名稱映射表
303
389
  * @param indentLevel 縮排層級,用於格式化內嵌物件
304
390
  */
305
- function getTypeFromSchema(schema: any, schemaTypeMap: Record<string, string> = {}, indentLevel: number = 0): string {
391
+ function getTypeFromSchema(schema: any, schemaTypeMap: Record<string, string> = {}, indentLevel: number = 0, localSchemaNames: Set<string> = new Set()): string {
306
392
  if (!schema) return 'any';
307
393
 
308
- // 處理 $ref 引用,使用 Schema.TypeName 格式
394
+ // 處理 $ref 引用
309
395
  if (schema.$ref) {
310
396
  const refPath = schema.$ref;
311
397
  if (refPath.startsWith('#/components/schemas/')) {
@@ -313,7 +399,10 @@ function getTypeFromSchema(schema: any, schemaTypeMap: Record<string, string> =
313
399
  // 使用映射表查找實際的類型名稱,並轉換為大駝峰
314
400
  const actualTypeName = schemaTypeMap[originalTypeName] || originalTypeName;
315
401
  const pascalCaseTypeName = toPascalCase(actualTypeName);
316
- const baseType = `Schema.${pascalCaseTypeName}`;
402
+ // 如果是 local schema,直接使用本地名稱;否則使用 Schema.TypeName
403
+ const baseType = localSchemaNames.has(originalTypeName)
404
+ ? pascalCaseTypeName
405
+ : `Schema.${pascalCaseTypeName}`;
317
406
  // 處理 nullable
318
407
  return schema.nullable ? `${baseType} | null` : baseType;
319
408
  }
@@ -340,7 +429,7 @@ function getTypeFromSchema(schema: any, schemaTypeMap: Record<string, string> =
340
429
  baseType = 'boolean';
341
430
  break;
342
431
  case 'array':
343
- const itemType = schema.items ? getTypeFromSchema(schema.items, schemaTypeMap, indentLevel) : 'any';
432
+ const itemType = schema.items ? getTypeFromSchema(schema.items, schemaTypeMap, indentLevel, localSchemaNames) : 'any';
344
433
  // 如果 itemType 包含聯合類型(包含 |),需要加括號
345
434
  const needsParentheses = itemType.includes('|');
346
435
  baseType = needsParentheses ? `(${itemType})[]` : `${itemType}[]`;
@@ -355,7 +444,7 @@ function getTypeFromSchema(schema: any, schemaTypeMap: Record<string, string> =
355
444
  if (schema.additionalProperties) {
356
445
  const valueType = schema.additionalProperties === true
357
446
  ? 'any'
358
- : getTypeFromSchema(schema.additionalProperties, schemaTypeMap, indentLevel);
447
+ : getTypeFromSchema(schema.additionalProperties, schemaTypeMap, indentLevel, localSchemaNames);
359
448
  baseType = `Record<string, ${valueType}>`;
360
449
  } else {
361
450
  baseType = '{}';
@@ -369,7 +458,7 @@ function getTypeFromSchema(schema: any, schemaTypeMap: Record<string, string> =
369
458
  entries.forEach(([key, propSchema]: [string, any]) => {
370
459
  const required = schema.required || [];
371
460
  const optional = required.includes(key) ? '' : '?';
372
- const type = getTypeFromSchema(propSchema, schemaTypeMap, indentLevel + 1);
461
+ const type = getTypeFromSchema(propSchema, schemaTypeMap, indentLevel + 1, localSchemaNames);
373
462
 
374
463
  // 如果屬性名包含特殊字符(如 -),需要加上引號
375
464
  const needsQuotes = /[^a-zA-Z0-9_$]/.test(key);
@@ -388,7 +477,7 @@ function getTypeFromSchema(schema: any, schemaTypeMap: Record<string, string> =
388
477
  // 如果沒有 properties 但有 additionalProperties
389
478
  const valueType = schema.additionalProperties === true
390
479
  ? 'any'
391
- : getTypeFromSchema(schema.additionalProperties, schemaTypeMap, indentLevel);
480
+ : getTypeFromSchema(schema.additionalProperties, schemaTypeMap, indentLevel, localSchemaNames);
392
481
  baseType = `Record<string, ${valueType}>`;
393
482
  } else {
394
483
  baseType = 'any';
@@ -406,7 +495,7 @@ function getTypeFromSchema(schema: any, schemaTypeMap: Record<string, string> =
406
495
  /**
407
496
  * 從參數定義中獲取 TypeScript 類型
408
497
  */
409
- function getTypeFromParameter(param: any, schemaTypeMap: Record<string, string> = {}): string {
498
+ function getTypeFromParameter(param: any, schemaTypeMap: Record<string, string> = {}, localSchemaNames: Set<string> = new Set()): string {
410
499
  if (!param.schema) return 'any';
411
- return getTypeFromSchema(param.schema, schemaTypeMap);
500
+ return getTypeFromSchema(param.schema, schemaTypeMap, 0, localSchemaNames);
412
501
  }
@@ -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