@acrool/rtk-query-codegen-openapi 0.0.6 → 0.0.9

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/src/index.ts CHANGED
@@ -3,14 +3,12 @@ import { createRequire } from 'node:module';
3
3
  import path from 'node:path';
4
4
  import { generateApi } from './generate';
5
5
  import type { CommonOptions, ConfigFile, GenerationOptions, OutputFileOptions } from './types';
6
- import { isValidUrl, prettify, getV3Doc } from './utils';
6
+ import { prettify, getV3Doc, downloadSchemaFile } from './utils';
7
7
  import camelCase from 'lodash.camelcase';
8
8
  export type { OutputFilesConfig, ConfigFile } from './types';
9
9
 
10
10
  const require = createRequire(__filename);
11
11
 
12
-
13
-
14
12
  // 確保目錄存在的函數
15
13
  async function ensureDirectoryExists(filePath: string) {
16
14
  const dirname = path.dirname(filePath);
@@ -19,7 +17,6 @@ async function ensureDirectoryExists(filePath: string) {
19
17
  }
20
18
  }
21
19
 
22
-
23
20
  // 檢查檔案是否存在的函數
24
21
  function fileExists(filePath: string): boolean {
25
22
  try {
@@ -36,17 +33,23 @@ function getApiNameFromDir(dirPath: string): string {
36
33
  }
37
34
 
38
35
  // 確保基礎文件存在的函數
39
- async function ensureBaseFilesExist(outputDir: string) {
36
+ async function ensureBaseFilesExist(outputDir: string, operationNames: string[]) {
40
37
  const enhanceEndpointsPath = path.join(outputDir, 'enhanceEndpoints.ts');
41
38
  const indexPath = path.join(outputDir, 'index.ts');
42
39
  const apiName = getApiNameFromDir(outputDir);
43
40
 
44
41
  // 如果 enhanceEndpoints.ts 不存在,創建它
45
42
  if (!fileExists(enhanceEndpointsPath)) {
43
+ // 生成操作名稱的字符串
44
+ const operationNamesString = operationNames
45
+ .map(name => ` ${name}: {},`)
46
+ .join('\n');
47
+
46
48
  const enhanceEndpointsContent = `import api from './query.generated';
47
49
 
48
50
  const enhancedApi = api.enhanceEndpoints({
49
51
  endpoints: {
52
+ ${operationNamesString}
50
53
  },
51
54
  });
52
55
 
@@ -54,6 +57,7 @@ export default enhancedApi;
54
57
  `;
55
58
  await fs.promises.writeFile(enhanceEndpointsPath, enhanceEndpointsContent, 'utf-8');
56
59
  }
60
+ // 如果文件已存在,不做任何修改
57
61
 
58
62
  // 如果 index.ts 不存在,創建它
59
63
  if (!fileExists(indexPath)) {
@@ -64,73 +68,63 @@ export {default as ${apiName}} from './enhanceEndpoints';
64
68
  }
65
69
  }
66
70
 
67
-
68
- // 從路徑中提取分類名稱
69
- function getGroupNameFromPath(path: string, pattern: RegExp): string {
70
- // console.log('pattern', pattern);
71
-
72
- const match = path.match(pattern);
73
- // console.log('match', path, match);
74
-
75
- if (match && match[1]) {
76
- return camelCase(match[1]);
71
+ export async function generateEndpoints(options: GenerationOptions): Promise<string | void> {
72
+ // 如果有 remoteFile,先下載到 schemaFile 路徑
73
+ let actualSchemaFile = options.schemaFile;
74
+
75
+ if (options.remoteFile) {
76
+ actualSchemaFile = await downloadSchemaFile(options.remoteFile, options.schemaFile);
77
77
  }
78
- return 'common';
79
- }
80
-
78
+
79
+ // 更新 options 中的 schemaFile 為實際的檔案路徑
80
+ const updatedOptions = {
81
+ ...options,
82
+ schemaFile: actualSchemaFile
83
+ };
81
84
 
85
+ const schemaLocation = updatedOptions.schemaFile;
82
86
 
87
+ const schemaAbsPath = path.resolve(process.cwd(), schemaLocation);
83
88
 
84
-
85
- export async function generateEndpoints(options: GenerationOptions): Promise<string | void> {
86
- const schemaLocation = options.schemaFile;
87
-
88
- const schemaAbsPath = isValidUrl(options.schemaFile)
89
- ? options.schemaFile
90
- : path.resolve(process.cwd(), schemaLocation);
91
-
92
- // 如果是 URL 且使用 outputFiles 配置,需要特殊處理
93
- if (isValidUrl(options.schemaFile) && 'outputFiles' in options) {
94
- const { outputFiles, ...commonConfig } = options as any;
89
+ // 如果是使用 outputFiles 配置,需要特殊處理
90
+ if ('outputFiles' in options) {
91
+ const { outputFiles, ...commonConfig } = updatedOptions as any;
95
92
 
96
93
  // 異步獲取 OpenAPI 文檔
97
- const openApiDoc = await getV3Doc(options.schemaFile, options.httpResolverOptions);
94
+ const openApiDoc = await getV3Doc(actualSchemaFile, updatedOptions.httpResolverOptions);
98
95
  const paths = Object.keys(openApiDoc.paths);
99
96
 
100
97
  // 從配置中獲取分類規則
101
- const outputFilesEntries = Object.entries(outputFiles);
102
- const [outputPath, config] = outputFilesEntries[0];
103
- const patterns = (config as any).groupMatch;
104
- const filterEndpoint = (config as any).filterEndpoint;
98
+ const { groupKeyMatch, outputDir, filterEndpoint, queryMatch } = outputFiles;
105
99
 
106
- const pattern = patterns;
107
100
  // 根據路徑自動分類
108
101
  const groupedPaths = paths.reduce((acc, path) => {
109
- const groupName = getGroupNameFromPath(path, pattern);
110
- if (!acc[groupName]) {
111
- acc[groupName] = [];
102
+ // 使用 groupKeyMatch 方法獲取 groupKey,並轉換為小駝峰格式
103
+ const groupKey = camelCase(groupKeyMatch(path));
104
+ if (!acc[groupKey]) {
105
+ acc[groupKey] = [];
112
106
  }
113
- acc[groupName].push(path);
107
+ acc[groupKey].push(path);
114
108
  return acc;
115
109
  }, {} as Record<string, string[]>);
116
110
 
117
111
  // 為每個分類生成配置並執行
118
- for (const [groupName, paths] of Object.entries(groupedPaths)) {
119
- const finalOutputPath = outputPath.replace('$1', groupName);
112
+ for (const [groupKey, paths] of Object.entries(groupedPaths)) {
113
+ const finalOutputPath = `${outputDir}/${groupKey}/query.generated.ts`;
120
114
 
121
115
  if (filterEndpoint) {
122
116
  // 如果有 filterEndpoint,使用基於路徑的篩選函數
123
117
  const pathBasedFilter = (operationName: string, operationDefinition: any) => {
124
118
  const path = operationDefinition.path;
125
-
119
+
126
120
  // 檢查路徑是否匹配當前分組
127
- const pathGroupName = getGroupNameFromPath(path, pattern);
128
- if (pathGroupName !== groupName) {
121
+ const pathGroupKey = camelCase(groupKeyMatch(path));
122
+ if (pathGroupKey !== groupKey) {
129
123
  return false;
130
124
  }
131
125
 
132
126
  // 使用 filterEndpoint 進行額外篩選
133
- const endpointFilter = filterEndpoint(groupName);
127
+ const endpointFilter = filterEndpoint(groupKey);
134
128
  if (endpointFilter instanceof RegExp) {
135
129
  return endpointFilter.test(operationName);
136
130
  }
@@ -141,7 +135,9 @@ export async function generateEndpoints(options: GenerationOptions): Promise<str
141
135
  const groupOptions = {
142
136
  ...commonConfig,
143
137
  outputFile: finalOutputPath,
138
+ sharedTypesFile: `${outputDir}/shared-types.ts`,
144
139
  filterEndpoints: pathBasedFilter,
140
+ queryMatch,
145
141
  };
146
142
 
147
143
  await generateSingleEndpoint(groupOptions);
@@ -149,16 +145,18 @@ export async function generateEndpoints(options: GenerationOptions): Promise<str
149
145
  // 如果沒有 filterEndpoint,只使用路徑分組
150
146
  const pathBasedFilter = (operationName: string, operationDefinition: any) => {
151
147
  const path = operationDefinition.path;
152
-
148
+
153
149
  // 檢查路徑是否匹配當前分組
154
- const pathGroupName = getGroupNameFromPath(path, pattern);
155
- return pathGroupName === groupName;
150
+ const pathGroupKey = camelCase(groupKeyMatch(path));
151
+ return pathGroupKey === groupKey;
156
152
  };
157
153
 
158
154
  const groupOptions = {
159
155
  ...commonConfig,
160
156
  outputFile: finalOutputPath,
157
+ sharedTypesFile: `${outputDir}/shared-types.ts`,
161
158
  filterEndpoints: pathBasedFilter,
159
+ queryMatch,
162
160
  };
163
161
 
164
162
  await generateSingleEndpoint(groupOptions);
@@ -168,19 +166,18 @@ export async function generateEndpoints(options: GenerationOptions): Promise<str
168
166
  }
169
167
 
170
168
  // 原有的邏輯處理非 outputFiles 配置或本地文件
171
- await generateSingleEndpoint(options);
169
+ await generateSingleEndpoint(updatedOptions);
172
170
  }
173
171
 
174
172
  async function generateSingleEndpoint(options: GenerationOptions): Promise<string | void> {
175
173
  const schemaLocation = options.schemaFile;
176
174
 
177
- const schemaAbsPath = isValidUrl(options.schemaFile)
178
- ? options.schemaFile
179
- : path.resolve(process.cwd(), schemaLocation);
175
+ const schemaAbsPath = path.resolve(process.cwd(), schemaLocation);
180
176
 
181
- const sourceCode = await enforceOazapftsTsVersion(async () => {
177
+ const result = await enforceOazapftsTsVersion(async () => {
182
178
  return generateApi(schemaAbsPath, options);
183
179
  });
180
+
184
181
  const { outputFile, prettierConfigFile } = options;
185
182
  if (outputFile) {
186
183
  const outputPath = path.resolve(process.cwd(), outputFile);
@@ -188,14 +185,14 @@ async function generateSingleEndpoint(options: GenerationOptions): Promise<strin
188
185
 
189
186
  // 確保基礎文件存在
190
187
  const outputDir = path.dirname(outputPath);
191
- await ensureBaseFilesExist(outputDir);
188
+ await ensureBaseFilesExist(outputDir, result.operationNames);
192
189
 
193
190
  fs.writeFileSync(
194
191
  outputPath,
195
- await prettify(outputFile, sourceCode, prettierConfigFile)
192
+ await prettify(outputFile, result.sourceCode, prettierConfigFile)
196
193
  );
197
194
  } else {
198
- return await prettify(null, sourceCode, prettierConfigFile);
195
+ return await prettify(null, result.sourceCode, prettierConfigFile);
199
196
  }
200
197
  }
201
198
 
@@ -205,10 +202,11 @@ export function parseConfig(fullConfig: ConfigFile) {
205
202
  if ('outputFiles' in fullConfig) {
206
203
  const { outputFiles, ...commonConfig } = fullConfig;
207
204
 
208
- // 讀取 OpenAPI 文檔 - 支援 URL 和本地文件
205
+ // 讀取 OpenAPI 文檔 - 支援本地文件
209
206
  let openApiDoc: any;
210
- if (isValidUrl(fullConfig.schemaFile)) {
211
- // 如果是 URL,直接返回原始配置,讓 generateEndpoints 處理
207
+
208
+ // 如果有 remoteFile,直接返回原始配置,讓 generateEndpoints 處理下載
209
+ if (fullConfig.remoteFile) {
212
210
  outFiles.push(fullConfig as any);
213
211
  return outFiles;
214
212
  } else {
@@ -219,39 +217,36 @@ export function parseConfig(fullConfig: ConfigFile) {
219
217
  const paths = Object.keys(openApiDoc.paths);
220
218
 
221
219
  // 從配置中獲取分類規則
222
- const outputFilesEntries = Object.entries(outputFiles);
223
- const [outputPath, config] = outputFilesEntries[0];
224
- const patterns = (config as any).groupMatch;
225
- const filterEndpoint = (config as any).filterEndpoint;
220
+ const { groupKeyMatch, outputDir, filterEndpoint, queryMatch } = outputFiles;
226
221
 
227
- const pattern = patterns;
228
222
  // 根據路徑自動分類
229
223
  const groupedPaths = paths.reduce((acc, path) => {
230
- const groupName = getGroupNameFromPath(path, pattern);
231
- if (!acc[groupName]) {
232
- acc[groupName] = [];
224
+ // 使用 groupKeyMatch 方法獲取 groupKey,並轉換為小駝峰格式
225
+ const groupKey = camelCase(groupKeyMatch(path));
226
+ if (!acc[groupKey]) {
227
+ acc[groupKey] = [];
233
228
  }
234
- acc[groupName].push(path);
229
+ acc[groupKey].push(path);
235
230
  return acc;
236
231
  }, {} as Record<string, string[]>);
237
232
 
238
233
  // 為每個分類生成配置
239
- Object.entries(groupedPaths).forEach(([groupName, paths]) => {
240
- const finalOutputPath = outputPath.replace('$1', groupName);
234
+ Object.entries(groupedPaths).forEach(([groupKey, paths]) => {
235
+ const finalOutputPath = `${outputDir}/${groupKey}/query.generated.ts`;
241
236
 
242
237
  if (filterEndpoint) {
243
238
  // 如果有 filterEndpoint,使用基於路徑的篩選函數
244
239
  const pathBasedFilter = (operationName: string, operationDefinition: any) => {
245
240
  const path = operationDefinition.path;
246
-
241
+
247
242
  // 檢查路徑是否匹配當前分組
248
- const pathGroupName = getGroupNameFromPath(path, pattern);
249
- if (pathGroupName !== groupName) {
243
+ const pathGroupKey = camelCase(groupKeyMatch(path));
244
+ if (pathGroupKey !== groupKey) {
250
245
  return false;
251
246
  }
252
247
 
253
248
  // 使用 filterEndpoint 進行額外篩選
254
- const endpointFilter = filterEndpoint(groupName);
249
+ const endpointFilter = filterEndpoint(groupKey);
255
250
  if (endpointFilter instanceof RegExp) {
256
251
  return endpointFilter.test(operationName);
257
252
  }
@@ -262,22 +257,26 @@ export function parseConfig(fullConfig: ConfigFile) {
262
257
  outFiles.push({
263
258
  ...commonConfig,
264
259
  outputFile: finalOutputPath,
260
+ sharedTypesFile: `${outputDir}/shared-types.ts`,
265
261
  filterEndpoints: pathBasedFilter,
262
+ queryMatch,
266
263
  });
267
264
  } else {
268
265
  // 如果沒有 filterEndpoint,只使用路徑分組
269
266
  const pathBasedFilter = (operationName: string, operationDefinition: any) => {
270
267
  const path = operationDefinition.path;
271
-
268
+
272
269
  // 檢查路徑是否匹配當前分組
273
- const pathGroupName = getGroupNameFromPath(path, pattern);
274
- return pathGroupName === groupName;
270
+ const pathGroupKey = camelCase(groupKeyMatch(path));
271
+ return pathGroupKey === groupKey;
275
272
  };
276
273
 
277
274
  outFiles.push({
278
275
  ...commonConfig,
279
276
  outputFile: finalOutputPath,
277
+ sharedTypesFile: `${outputDir}/shared-types.ts`,
280
278
  filterEndpoints: pathBasedFilter,
279
+ queryMatch,
281
280
  });
282
281
  }
283
282
  });
package/src/types.ts CHANGED
@@ -34,9 +34,13 @@ export type GenerationOptions = Id<
34
34
  export interface CommonOptions {
35
35
  apiFile: string;
36
36
  /**
37
- * filename or url
37
+ * local schema file path (only supports local files)
38
38
  */
39
39
  schemaFile: string;
40
+ /**
41
+ * remote schema file URL (when provided, will download to schemaFile path)
42
+ */
43
+ remoteFile?: string;
40
44
  /**
41
45
  * defaults to "api"
42
46
  */
@@ -111,6 +115,11 @@ export interface CommonOptions {
111
115
  * resolution mechanism will be used.
112
116
  */
113
117
  prettierConfigFile?: string;
118
+ /**
119
+ * defaults to "@acrool/react-fetcher"
120
+ * File path for importing IRestFulEndpointsQueryReturn type
121
+ */
122
+ endpointsQueryReturnTypeFile?: string;
114
123
  }
115
124
 
116
125
  export type TextMatcher = string | RegExp | (string | RegExp)[];
@@ -127,6 +136,7 @@ export interface OutputFileOptions extends Partial<CommonOptions> {
127
136
  outputFile: string;
128
137
  filterEndpoints?: EndpointMatcher;
129
138
  endpointOverrides?: EndpointOverrides[];
139
+ queryMatch?: (method: string, path: string) => boolean;
130
140
  /**
131
141
  * defaults to false
132
142
  * If passed as true it will generate TS enums instead of union of strings
@@ -143,10 +153,10 @@ export type EndpointOverrides = {
143
153
  }>;
144
154
 
145
155
  export type OutputFilesConfig = {
146
- [outputFile: string]: {
147
- groupMatch: RegExp,
148
- filterEndpoint?: (groupName: string) => RegExp
149
- }
156
+ groupKeyMatch: (path: string) => string;
157
+ outputDir: string;
158
+ queryMatch?: (method: string, path: string) => boolean;
159
+ filterEndpoint?: (groupName: string) => RegExp;
150
160
  };
151
161
 
152
162
  export type ConfigFile =
@@ -157,3 +167,8 @@ export type ConfigFile =
157
167
  outputFiles: OutputFilesConfig
158
168
  }
159
169
  >;
170
+
171
+ export type GenerateApiResult = {
172
+ sourceCode: string;
173
+ operationNames: string[];
174
+ };
@@ -0,0 +1,33 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { isValidUrl } from './isValidUrl';
4
+
5
+ export async function downloadSchemaFile(remoteFile: string, targetPath: string): Promise<string> {
6
+ // 如果不是網址,拋出錯誤
7
+ if (!isValidUrl(remoteFile)) {
8
+ throw new Error(`remoteFile must be a valid URL: ${remoteFile}`);
9
+ }
10
+
11
+ try {
12
+ // 確保目錄存在
13
+ const dir = path.dirname(targetPath);
14
+ if (!fs.existsSync(dir)) {
15
+ await fs.promises.mkdir(dir, { recursive: true });
16
+ }
17
+
18
+ // 下載檔案
19
+ const response = await fetch(remoteFile);
20
+ if (!response.ok) {
21
+ throw new Error(`Failed to download schema from ${remoteFile}: ${response.statusText}`);
22
+ }
23
+
24
+ const content = await response.text();
25
+ await fs.promises.writeFile(targetPath, content, 'utf-8');
26
+
27
+ console.log(`Schema downloaded from ${remoteFile} to ${targetPath}`);
28
+ return targetPath;
29
+ } catch (error) {
30
+ console.error(`Error downloading schema from ${remoteFile}:`, error);
31
+ throw error;
32
+ }
33
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './capitalize';
2
+ export * from './downloadSchema';
2
3
  export * from './getOperationDefinitions';
3
4
  export * from './getV3Doc';
4
5
  export * from './isQuery';
@@ -1,6 +1,14 @@
1
1
  import type { EndpointOverrides, operationKeys } from '../types';
2
2
 
3
- export function isQuery(verb: (typeof operationKeys)[number], overrides: EndpointOverrides | undefined) {
3
+ export function isQuery(
4
+ verb: (typeof operationKeys)[number],
5
+ path: string,
6
+ overrides: EndpointOverrides | undefined,
7
+ queryMatch?: (method: string, path: string) => boolean
8
+ ) {
9
+ if (queryMatch) {
10
+ return queryMatch(verb, path);
11
+ }
4
12
  if (overrides?.type) {
5
13
  return overrides.type === 'query';
6
14
  }