@acrool/rtk-query-codegen-openapi 1.3.0 → 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.
- package/lib/bin/cli.mjs +41 -39
- package/lib/bin/cli.mjs.map +1 -1
- package/lib/index.js +297 -35
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +297 -35
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/generators/component-schema-generator.ts +10 -1
- package/src/generators/types-generator.ts +121 -32
- package/src/services/unified-code-generator.ts +100 -14
- package/src/utils/schema-ref-analyzer.ts +218 -0
|
@@ -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
|
-
//
|
|
61
|
-
|
|
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
|
-
//
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
463
|
+
export * from "./schema";
|
|
464
|
+
${groupExports}
|
|
379
465
|
`;
|
|
380
466
|
}
|
|
381
467
|
|