Skip to content

Commit 047191a

Browse files
committed
refactor: update toolkit generator
1 parent b33bbef commit 047191a

28 files changed

+1454
-660
lines changed

toolkit/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@ The toolkit provides clients for all ReactPress backend modules:
8989
You can create a custom API instance with specific configuration:
9090

9191
```typescript
92-
import { createApiInstance } from '@fecommunity/reactpress-toolkit';
92+
import { api } from '@fecommunity/reactpress-toolkit';
9393

94-
const customApi = createApiInstance({
94+
const customApi = api.createApiInstance({
9595
baseURL: 'https://api.yourdomain.com',
9696
timeout: 5000,
9797
// ... other axios configuration options

toolkit/demos/simple.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
// src/services/articleService.ts
2-
import api, { types, utils } from '../dist';
2+
import api, { types, utils, http, HttpClient, createApiInstance } from '../dist';
33

44
// 文章服务层
55
export class ArticleService {
66
// 获取所有文章
77
static async getAllArticles() {
88
try {
9-
const articles = await api.article.getRecommendArticles({});
10-
9+
const articles = await api.article.checkPassword('test');
10+
1111
// 格式化数据
12-
return articles.data.map(article => ({
12+
return articles.data.map((article) => ({
1313
...article,
1414
formattedDate: utils.formatDate(new Date(article.createdAt || Date.now())),
1515
}));
@@ -59,7 +59,7 @@ export class ArticleService {
5959
if (utils.ApiError.isInstance(error)) {
6060
return error;
6161
}
62-
63-
return new utils.ApiError(500, defaultMessage, error);
62+
63+
return new utils.ApiError(defaultMessage, 500, error);
6464
}
65-
}
65+
}

toolkit/scripts/generate-api-types.js

Lines changed: 193 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,28 @@ async function generateApiTypes() {
3535
httpClientType: 'axios',
3636
typePrefix: 'I',
3737
generateClient: true,
38+
generateResponses: true,
39+
extractRequestParams: true,
40+
extractResponseBody: true,
41+
singleHttpClient: true,
42+
unwrapResponseData: true,
3843
hooks: {
3944
onPrepareConfig: (currentConfiguration) => {
4045
const config = currentConfiguration.config;
41-
config.fileNames.httpClient = 'HttpClient'; // 使用大驼峰命名
46+
config.fileNames.httpClient = 'HttpClient';
4247
return { ...currentConfiguration, config };
4348
},
49+
onParseSchema: (originalSchema, parsedSchema) => {
50+
// 确保所有属性都有类型
51+
if (parsedSchema.properties) {
52+
Object.keys(parsedSchema.properties).forEach(prop => {
53+
if (!parsedSchema.properties[prop].type) {
54+
parsedSchema.properties[prop].type = 'any';
55+
}
56+
});
57+
}
58+
return parsedSchema;
59+
},
4460
},
4561
});
4662

@@ -86,13 +102,14 @@ async function organizeGeneratedFiles() {
86102
// API 文件:首字母大写的文件或特定的客户端文件
87103
fs.moveSync(filePath, path.join(apiDir, file), { overwrite: true });
88104
console.log(`📄 移动 API 文件: ${file} -> api/`);
89-
} else if (file.includes('contract') || file === 'types.ts') {
105+
} else if (file.includes('contract') || file === 'types.ts' || file.includes('response')) {
90106
// 类型定义文件
91-
fs.moveSync(filePath, path.join(typesDir, file === 'types.ts' ? 'index.ts' : file), { overwrite: true });
92-
console.log(`📄 移动类型文件: {file} -> types/`);
107+
const targetName = file === 'types.ts' ? 'index.ts' : file;
108+
fs.moveSync(filePath, path.join(typesDir, targetName), { overwrite: true });
109+
console.log(`📄 移动类型文件: ${file} -> types/`);
93110
} else {
94111
// 其他文件留在根目录
95-
console.log(`📄 保留文件: {file}`);
112+
console.log(`📄 保留文件: ${file}`);
96113
}
97114
}
98115
});
@@ -103,9 +120,15 @@ async function organizeGeneratedFiles() {
103120
// 修复 API 文件中的导入路径
104121
await fixApiImports();
105122

123+
// 修复缺失的类型导入
124+
await fixMissingImports();
125+
106126
// 重命名 API 方法,使其更专业
107127
await renameApiMethods();
108128

129+
// 修复请求和响应类型
130+
await fixRequestResponseTypes();
131+
109132
// 创建统一的类型索引
110133
await createTypeIndex();
111134

@@ -171,6 +194,12 @@ async function fixApiImports() {
171194
"from '../types/data-contracts'"
172195
);
173196

197+
// 修复响应类型导入路径
198+
content = content.replace(
199+
/from '\.\/(.*responses.*)'/g,
200+
"from '../types/$1'"
201+
);
202+
174203
// 修复 HttpClient 导入路径
175204
content = content.replace(
176205
/from '\.\/httpClient'/g,
@@ -182,7 +211,7 @@ async function fixApiImports() {
182211
/from '\.\/([^']+)'/g,
183212
(match, importPath) => {
184213
// 如果导入路径是类型文件,则重定向到 types 目录
185-
if (importPath.includes('contract') || importPath === 'types') {
214+
if (importPath.includes('contract') || importPath.includes('response') || importPath === 'types') {
186215
return `from '../types/${importPath}'`;
187216
}
188217
return match;
@@ -197,6 +226,63 @@ async function fixApiImports() {
197226
console.log('✅ 所有 API 文件的导入路径已修复');
198227
}
199228

229+
// 修复缺失的类型导入
230+
async function fixMissingImports() {
231+
console.log('🔧 修复缺失的类型导入...');
232+
233+
const apiDir = path.join(CONFIG.output, 'api');
234+
const typesDir = path.join(CONFIG.output, 'types');
235+
236+
// 获取所有 API 文件
237+
const apiFiles = fs.readdirSync(apiDir).filter(file => file.endsWith('.ts') && file !== 'index.ts');
238+
239+
// 读取数据契约文件,获取所有可用的类型
240+
const dataContractsPath = path.join(typesDir, 'data-contracts.ts');
241+
let availableTypes = [];
242+
243+
if (fs.existsSync(dataContractsPath)) {
244+
const dataContractsContent = fs.readFileSync(dataContractsPath, 'utf8');
245+
// 提取所有接口和类型定义
246+
const typeRegex = /(export interface|export type) (I[A-Z][a-zA-Z]*)/g;
247+
let match;
248+
while ((match = typeRegex.exec(dataContractsContent)) !== null) {
249+
availableTypes.push(match[2]);
250+
}
251+
}
252+
253+
for (const file of apiFiles) {
254+
const filePath = path.join(apiDir, file);
255+
let content = fs.readFileSync(filePath, 'utf8');
256+
257+
// 检查当前导入语句
258+
const importMatch = content.match(/import\s*{([^}]+)}\s*from\s*['"]\.\.\/types\/data-contracts['"]/);
259+
260+
if (importMatch) {
261+
const importedTypes = importMatch[1].split(',').map(t => t.trim());
262+
263+
// 查找文件中使用的类型
264+
const usedTypes = [];
265+
for (const type of availableTypes) {
266+
if (content.includes(type) && !importedTypes.includes(type)) {
267+
usedTypes.push(type);
268+
}
269+
}
270+
271+
// 如果有缺失的类型,添加到导入语句中
272+
if (usedTypes.length > 0) {
273+
const newImport = `import { ${importedTypes.join(', ')}, ${usedTypes.join(', ')} } from '../types/data-contracts';`;
274+
content = content.replace(/import\s*{([^}]+)}\s*from\s*['"]\.\.\/types\/data-contracts['"]/, newImport);
275+
276+
// 写入修复后的内容
277+
fs.writeFileSync(filePath, content);
278+
console.log(`✅ 为 ${file} 添加缺失的类型导入: ${usedTypes.join(', ')}`);
279+
}
280+
}
281+
}
282+
283+
console.log('✅ 缺失的类型导入已修复');
284+
}
285+
200286
// 重命名 API 方法,使其更专业
201287
async function renameApiMethods() {
202288
console.log('🔧 重命名 API 方法...');
@@ -215,24 +301,113 @@ async function renameApiMethods() {
215301
const filePath = path.join(apiDir, file);
216302
let content = fs.readFileSync(filePath, 'utf8');
217303

218-
// 使用更精确的正则表达式匹配并重命名方法
219-
// 匹配模式: 方法名以 Controller 开头,后面跟着大写字母
220-
// 例如: articleControllerFindById -> findById
304+
// 提取控制器名称 (从文件名)
305+
const controllerName = file.replace('.ts', '');
306+
307+
// 构建正则表达式来匹配方法定义
308+
const methodRegex = new RegExp(`${controllerName.toLowerCase()}Controller([A-Z]\\w+)\\s*=`, 'g');
309+
310+
// 替换方法名
311+
content = content.replace(methodRegex, (match, methodPart) => {
312+
// 将方法名的首字母小写
313+
const newMethodName = methodPart.charAt(0).toLowerCase() + methodPart.slice(1);
314+
return `${newMethodName} =`;
315+
});
316+
317+
// 写入修复后的内容
318+
fs.writeFileSync(filePath, content);
319+
console.log(`✅ 重命名 ${file} 中的方法`);
320+
}
321+
322+
console.log('✅ 所有 API 方法已重命名');
323+
}
324+
325+
// 修复请求和响应类型
326+
async function fixRequestResponseTypes() {
327+
console.log('🔧 修复请求和响应类型...');
328+
329+
const apiDir = path.join(CONFIG.output, 'api');
330+
const typesDir = path.join(CONFIG.output, 'types');
331+
332+
// 获取所有 API 文件
333+
const apiFiles = fs.readdirSync(apiDir).filter(file =>
334+
file.endsWith('.ts') &&
335+
file !== 'index.ts' &&
336+
file !== 'HttpClient.ts' &&
337+
/[A-Z]/.test(file[0]) // 首字母大写的文件
338+
);
339+
340+
// 读取数据契约类型
341+
let dataContractsContent = '';
342+
const dataContractsPath = path.join(typesDir, 'data-contracts.ts');
343+
if (fs.existsSync(dataContractsPath)) {
344+
dataContractsContent = fs.readFileSync(dataContractsPath, 'utf8');
345+
}
346+
347+
for (const file of apiFiles) {
348+
const filePath = path.join(apiDir, file);
349+
let content = fs.readFileSync(filePath, 'utf8');
350+
351+
// 修复 GET 请求的错误数据参数
352+
content = content.replace(
353+
/(GET|DELETE|HEAD|OPTIONS).*\n.*\(data:.*params: RequestParams = {}\)/g,
354+
(match) => {
355+
// 对于 GET/DELETE/HEAD/OPTIONS 请求,不应该有 data 参数
356+
return match.replace('data: any, ', '').replace('data: I\\w+, ', '');
357+
}
358+
);
359+
360+
// 修复请求和响应类型
361+
content = content.replace(
362+
/this\.http\.request<([^,]+),\s*any>\({/g,
363+
(match, responseType) => {
364+
// 如果响应类型是 void 或 any,尝试推断正确的类型
365+
if (responseType === 'void' || responseType === 'any') {
366+
// 根据方法名和类名推断类型
367+
const className = file.replace('.ts', '');
368+
const methodMatch = content.match(/(\w+)\s*=\s*\([^)]*\)\s*=>/);
369+
if (methodMatch) {
370+
const methodName = methodMatch[1];
371+
372+
// 根据方法名推断响应类型
373+
let inferredType = 'any';
374+
if (methodName.includes('find') || methodName.includes('get')) {
375+
inferredType = `I${className}[]`;
376+
} else if (methodName.includes('create') || methodName.includes('update')) {
377+
inferredType = `I${className}`;
378+
}
379+
380+
return match.replace(responseType, inferredType);
381+
}
382+
}
383+
return match;
384+
}
385+
);
386+
387+
// 修复请求参数类型
221388
content = content.replace(
222-
/(\w+)Controller([A-Z]\w+)/g,
223-
(match, className, methodName) => {
224-
// 将方法名的首字母小写
225-
const newMethodName = methodName.charAt(0).toLowerCase() + methodName.slice(1);
226-
return newMethodName;
389+
/\(params:\s*RequestParams\s*=\s*{}\)\s*=>/g,
390+
(match) => {
391+
// 根据方法名推断请求体类型
392+
const methodMatch = content.match(/(\w+)\s*=\s*\([^)]*\)\s*=>/);
393+
if (methodMatch) {
394+
const methodName = methodMatch[1];
395+
const className = file.replace('.ts', '');
396+
397+
if (methodName.includes('create') || methodName.includes('update')) {
398+
return `(data: I${className}, params: RequestParams = {}) =>`;
399+
}
400+
}
401+
return match;
227402
}
228403
);
229404

230405
// 写入修复后的内容
231406
fs.writeFileSync(filePath, content);
232-
console.log(`✅ 重命名 ${file} 中的方法`);
407+
console.log(`✅ 修复 ${file} 的请求和响应类型`);
233408
}
234409

235-
console.log('✅ 所有 API 方法已重命名');
410+
console.log('✅ 所有 API 的请求和响应类型已修复');
236411
}
237412

238413
// 创建统一的类型索引
@@ -398,7 +573,7 @@ async function generateUtils() {
398573
const utilsDir = path.join(CONFIG.output, 'utils');
399574
fs.ensureDirSync(utilsDir);
400575

401-
// HTTP 客户端配置 - 修复类型错误
576+
// HTTP 客户端配置
402577
const httpUtilsContent = `import axios, { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
403578
404579
// 创建自定义 axios 实例
@@ -411,7 +586,7 @@ export const createHttpClient = (baseURL?: string) => {
411586
},
412587
});
413588
414-
// 请求拦截器 - 修复类型错误
589+
// 请求拦截器
415590
instance.interceptors.request.use(
416591
(config: InternalAxiosRequestConfig) => {
417592
// 添加认证令牌

0 commit comments

Comments
 (0)