@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.
- package/lib/bin/cli.mjs +49 -41
- package/lib/bin/cli.mjs.map +1 -1
- package/lib/index.js +353 -35
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +353 -35
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/generators/component-schema-generator.ts +50 -1
- package/src/generators/types-generator.ts +159 -32
- package/src/services/unified-code-generator.ts +100 -14
- package/src/utils/schema-ref-analyzer.ts +218 -0
|
@@ -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(
|
|
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
|
-
//
|
|
61
|
-
|
|
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
|
-
//
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|