@gogenger/go-gen 1.0.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/core/writer.js ADDED
@@ -0,0 +1,413 @@
1
+ const path = require('path');
2
+ const shell = require('shelljs');
3
+ const chalk = require('chalk');
4
+ const inquirer = require('inquirer');
5
+ const os = require('os');
6
+ const ora = require('ora');
7
+ const fs = require('fs');
8
+ const { loadConfig } = require('./config');
9
+
10
+ const desktopPath = path.join(os.homedir(), 'Desktop');
11
+ const currentPath = process.cwd();
12
+
13
+ function generateApiFile({
14
+ apiName,
15
+ typeName,
16
+ url,
17
+ method = 'GET',
18
+ hasRequestBody = false,
19
+ }) {
20
+ const config = loadConfig();
21
+ const requestModule = config.requestModule;
22
+ const finalTypeName = typeName.charAt(0).toUpperCase() + typeName.slice(1);
23
+ const methodLower = method.toLowerCase();
24
+
25
+ let imports = `import type { ${finalTypeName}`;
26
+ let params = '';
27
+ let requestCall = '';
28
+
29
+ if (hasRequestBody) {
30
+ imports += `, ${finalTypeName}Request`;
31
+ params = `data: ${finalTypeName}Request`;
32
+ requestCall = `request.${methodLower}<${finalTypeName}>("${url}", data)`;
33
+ } else {
34
+ requestCall = `request.${methodLower}<${finalTypeName}>("${url}")`;
35
+ }
36
+
37
+ imports += ` } from "./types";`;
38
+
39
+ return `
40
+ import request from "${requestModule}";
41
+ ${imports}
42
+
43
+ export function ${apiName}(${params}) {
44
+ return ${requestCall};
45
+ }
46
+ `.trim();
47
+ }
48
+
49
+ function parseExistingTypes(typesFilePath) {
50
+ if (!fs.existsSync(typesFilePath)) {
51
+ return { types: [], content: '' };
52
+ }
53
+
54
+ const content = fs.readFileSync(typesFilePath, 'utf-8');
55
+ const typeRegex = /export\s+(?:interface|type)\s+(\w+)/g;
56
+ const types = [];
57
+ let match;
58
+
59
+ while ((match = typeRegex.exec(content)) !== null) {
60
+ types.push(match[1]);
61
+ }
62
+
63
+ return { types, content };
64
+ }
65
+
66
+ function parseExistingApis(apiFilePath) {
67
+ if (!fs.existsSync(apiFilePath)) {
68
+ return { functions: [], content: '' };
69
+ }
70
+
71
+ const content = fs.readFileSync(apiFilePath, 'utf-8');
72
+ const funcRegex = /export\s+function\s+(\w+)\s*\(/g;
73
+ const functions = [];
74
+ let match;
75
+
76
+ while ((match = funcRegex.exec(content)) !== null) {
77
+ functions.push(match[1]);
78
+ }
79
+
80
+ return { functions, content };
81
+ }
82
+
83
+ function extractTypeDefinitions(newTypesContent) {
84
+ const lines = newTypesContent.split('\n');
85
+ const definitions = [];
86
+ let currentDef = [];
87
+ let inDefinition = false;
88
+ let braceCount = 0;
89
+
90
+ for (const line of lines) {
91
+ if (/export\s+(?:interface|type)\s+\w+/.test(line)) {
92
+ inDefinition = true;
93
+ currentDef = [line];
94
+ braceCount =
95
+ (line.match(/{/g) || []).length - (line.match(/}/g) || []).length;
96
+
97
+ if (braceCount === 0 && line.includes('=')) {
98
+ definitions.push(currentDef.join('\n'));
99
+ inDefinition = false;
100
+ currentDef = [];
101
+ }
102
+ continue;
103
+ }
104
+
105
+ if (inDefinition) {
106
+ currentDef.push(line);
107
+ braceCount += (line.match(/{/g) || []).length;
108
+ braceCount -= (line.match(/}/g) || []).length;
109
+
110
+ if (braceCount === 0) {
111
+ definitions.push(currentDef.join('\n'));
112
+ inDefinition = false;
113
+ currentDef = [];
114
+ }
115
+ }
116
+ }
117
+
118
+ return definitions;
119
+ }
120
+
121
+ function resolveTypeNameConflict(existingTypes, typeName) {
122
+ let finalTypeName = typeName;
123
+ let suffix = 1;
124
+
125
+ while (existingTypes.includes(finalTypeName)) {
126
+ finalTypeName = `${typeName}${suffix}`;
127
+ suffix++;
128
+ }
129
+
130
+ return { finalTypeName, hasConflict: finalTypeName !== typeName };
131
+ }
132
+
133
+ function mergeTypesContent(existingContent, newTypesContent, typeName) {
134
+ const typeRegex = /export\s+(?:interface|type)\s+(\w+)/g;
135
+ const existingTypes = [];
136
+ let match;
137
+
138
+ while ((match = typeRegex.exec(existingContent)) !== null) {
139
+ existingTypes.push(match[1]);
140
+ }
141
+
142
+ const { finalTypeName, hasConflict } = resolveTypeNameConflict(
143
+ existingTypes,
144
+ typeName,
145
+ );
146
+
147
+ if (hasConflict) {
148
+ newTypesContent = newTypesContent.replace(
149
+ new RegExp(`\\b${typeName}\\b`, 'g'),
150
+ finalTypeName,
151
+ );
152
+ }
153
+
154
+ const newDefinitions = extractTypeDefinitions(newTypesContent);
155
+
156
+ const uniqueDefinitions = newDefinitions.filter(def => {
157
+ const typeMatch = def.match(/export\s+(?:interface|type)\s+(\w+)/);
158
+ if (!typeMatch) return false;
159
+ return !existingTypes.includes(typeMatch[1]);
160
+ });
161
+
162
+ if (uniqueDefinitions.length === 0) {
163
+ return { merged: existingContent, isDuplicate: true, finalTypeName };
164
+ }
165
+
166
+ // 确保有换行分隔
167
+ const merged =
168
+ existingContent.trim() + '\n\n' + uniqueDefinitions.join('\n\n');
169
+
170
+ return { merged, isDuplicate: false, finalTypeName, hasConflict };
171
+ }
172
+
173
+ function extractImportedTypes(apiContent) {
174
+ const importMatch = apiContent.match(/import\s+type\s+{\s*([^}]+)\s*}/);
175
+ if (!importMatch) return [];
176
+
177
+ return importMatch[1]
178
+ .split(',')
179
+ .map(t => t.trim())
180
+ .filter(Boolean);
181
+ }
182
+
183
+ function mergeApiContent(existingContent, newApiContent, newApiName) {
184
+ const funcRegex = /export\s+function\s+(\w+)\s*\(/g;
185
+ const existingFunctions = [];
186
+ let match;
187
+
188
+ while ((match = funcRegex.exec(existingContent)) !== null) {
189
+ existingFunctions.push(match[1]);
190
+ }
191
+
192
+ if (existingFunctions.includes(newApiName)) {
193
+ return { merged: existingContent, isDuplicate: true };
194
+ }
195
+
196
+ const newTypes = extractImportedTypes(newApiContent);
197
+
198
+ const existingImportMatch = existingContent.match(
199
+ /import\s+type\s+{\s*([^}]+)\s*}/,
200
+ );
201
+ const existingTypes = existingImportMatch
202
+ ? existingImportMatch[1]
203
+ .split(',')
204
+ .map(t => t.trim())
205
+ .filter(Boolean)
206
+ : [];
207
+
208
+ const allTypes = [...new Set([...existingTypes, ...newTypes])];
209
+
210
+ const newFunctionMatch = newApiContent.match(/(export\s+function[\s\S]+)/);
211
+ const newFunction = newFunctionMatch ? newFunctionMatch[1] : '';
212
+
213
+ let merged = existingContent;
214
+
215
+ if (allTypes.length > existingTypes.length) {
216
+ const importStatement = `import type { ${allTypes.join(', ')} } from "./types";`;
217
+ merged = merged.replace(
218
+ /import\s+type\s+{[^}]+}\s+from\s+"\.\/types";/,
219
+ importStatement,
220
+ );
221
+ }
222
+
223
+ if (newFunction) {
224
+ merged = merged.trim() + '\n\n' + newFunction;
225
+ }
226
+
227
+ return { merged, isDuplicate: false };
228
+ }
229
+
230
+ function validatePath(inputPath) {
231
+ const resolved = path.resolve(inputPath);
232
+
233
+ // Windows 系统路径(C:\Windows, C:\Program Files 等)
234
+ const windowsDangerousPaths = [
235
+ 'C:\\Windows',
236
+ 'C:\\Program Files',
237
+ 'C:\\System',
238
+ ];
239
+
240
+ // Unix/Linux 系统路径
241
+ const unixDangerousPaths = ['/System', '/usr', '/bin', '/sbin', '/etc'];
242
+
243
+ // 检查 Windows 路径
244
+ for (const dangerousPath of windowsDangerousPaths) {
245
+ if (resolved.toUpperCase().startsWith(dangerousPath.toUpperCase())) {
246
+ throw new Error('⛔ 不允许写入系统目录');
247
+ }
248
+ }
249
+
250
+ // 检查 Unix 路径
251
+ for (const dangerousPath of unixDangerousPaths) {
252
+ if (resolved.startsWith(dangerousPath)) {
253
+ throw new Error('⛔ 不允许写入系统目录');
254
+ }
255
+ }
256
+
257
+ return resolved;
258
+ }
259
+
260
+ async function writeFiles({
261
+ apiName,
262
+ typeName,
263
+ url,
264
+ typesContent,
265
+ method = 'GET',
266
+ hasRequestBody = false,
267
+ interactive = true,
268
+ }) {
269
+ const config = loadConfig();
270
+
271
+ let baseDir;
272
+
273
+ if (interactive) {
274
+ const { outputPath } = await inquirer.prompt([
275
+ {
276
+ type: 'list',
277
+ name: 'outputPath',
278
+ message: '📂 输出目录:',
279
+ default: config.defaultOutputPath,
280
+ choices: [
281
+ { name: '💻 桌面', value: desktopPath },
282
+ { name: '📁 当前目录', value: currentPath },
283
+ { name: '🔍 自定义路径', value: 'custom' },
284
+ ],
285
+ },
286
+ ]);
287
+
288
+ baseDir = outputPath;
289
+ if (outputPath === 'custom') {
290
+ const { customPath } = await inquirer.prompt([
291
+ {
292
+ type: 'input',
293
+ name: 'customPath',
294
+ message: '📁 请输入保存路径:',
295
+ default: config.customPath || currentPath,
296
+ validate: input => {
297
+ try {
298
+ validatePath(input);
299
+ return shell.test('-d', input) || '路径不存在';
300
+ } catch (error) {
301
+ return error.message;
302
+ }
303
+ },
304
+ },
305
+ ]);
306
+ baseDir = customPath;
307
+ }
308
+ } else {
309
+ baseDir = currentPath;
310
+ }
311
+
312
+ const outputDir = path.join(baseDir, apiName);
313
+ const dirExists = fs.existsSync(outputDir);
314
+
315
+ if (dirExists && interactive) {
316
+ console.log(chalk.yellow(`\n📁 目录已存在,将进行增量写入: ${outputDir}`));
317
+ }
318
+
319
+ shell.mkdir('-p', outputDir);
320
+
321
+ const typesFilePath = path.join(outputDir, 'types.ts');
322
+ const apiFilePath = path.join(outputDir, 'api.ts');
323
+
324
+ let finalTypesContent = typesContent;
325
+ let finalTypeName = typeName;
326
+ let typeSkipped = false;
327
+ let typeConflict = false;
328
+
329
+ if (fs.existsSync(typesFilePath)) {
330
+ const existingTypes = fs.readFileSync(typesFilePath, 'utf-8');
331
+ const {
332
+ merged,
333
+ isDuplicate,
334
+ finalTypeName: resolvedName,
335
+ hasConflict,
336
+ } = mergeTypesContent(existingTypes, typesContent, typeName);
337
+
338
+ if (hasConflict && interactive) {
339
+ console.log(
340
+ chalk.yellow(
341
+ `⚠️ 类型名冲突,已自动重命名: ${typeName} → ${resolvedName}`,
342
+ ),
343
+ );
344
+ typeConflict = true;
345
+ finalTypeName = resolvedName;
346
+ }
347
+
348
+ if (isDuplicate && !hasConflict && interactive) {
349
+ console.log(chalk.yellow(`⚠️ 类型 ${typeName} 已存在,跳过写入`));
350
+ typeSkipped = true;
351
+ }
352
+
353
+ finalTypesContent = merged;
354
+ }
355
+
356
+ fs.writeFileSync(typesFilePath, finalTypesContent);
357
+
358
+ const newApiContent = generateApiFile({
359
+ apiName,
360
+ typeName: finalTypeName,
361
+ url,
362
+ method,
363
+ hasRequestBody,
364
+ });
365
+ let finalApiContent = newApiContent;
366
+ let apiSkipped = false;
367
+
368
+ if (fs.existsSync(apiFilePath)) {
369
+ const existingApi = fs.readFileSync(apiFilePath, 'utf-8');
370
+ const { merged, isDuplicate } = mergeApiContent(
371
+ existingApi,
372
+ newApiContent,
373
+ apiName,
374
+ );
375
+
376
+ if (isDuplicate && interactive) {
377
+ console.log(chalk.yellow(`⚠️ API 函数 ${apiName} 已存在,跳过写入`));
378
+ apiSkipped = true;
379
+ }
380
+
381
+ finalApiContent = merged;
382
+ }
383
+
384
+ fs.writeFileSync(apiFilePath, finalApiContent);
385
+
386
+ if (interactive) {
387
+ const spinner = ora();
388
+ spinner.text = chalk.cyan('📂 输出目录:') + outputDir;
389
+
390
+ if (typeSkipped && apiSkipped) {
391
+ spinner.warn('⚠️ 内容已存在,无新增内容');
392
+ } else if (typeConflict) {
393
+ spinner.succeed(`✨ 生成成功!(类型已重命名为 ${finalTypeName})`);
394
+ } else if (dirExists) {
395
+ spinner.succeed('✨ 增量写入成功!');
396
+ } else {
397
+ spinner.succeed('🎉 文件生成成功!');
398
+ }
399
+ }
400
+
401
+ return { success: true, outputDir, finalTypeName };
402
+ }
403
+
404
+ module.exports = {
405
+ writeFiles,
406
+ generateApiFile,
407
+ parseExistingTypes,
408
+ parseExistingApis,
409
+ mergeTypesContent,
410
+ mergeApiContent,
411
+ resolveTypeNameConflict,
412
+ validatePath,
413
+ };
@@ -0,0 +1,327 @@
1
+ # 🎓 最佳实践
2
+
3
+ ## 1. 团队规范化
4
+
5
+ ### 推荐做法
6
+
7
+ 在项目根目录创建 `.apirc.json`:
8
+
9
+ ```json
10
+ {
11
+ "requestModule": "@/api/request",
12
+ "typePrefix": "I",
13
+ "apiPrefix": "api"
14
+ }
15
+ ```
16
+
17
+ ### 优势
18
+
19
+ - ✅ 团队成员自动使用统一配置
20
+ - ✅ 新成员无需额外培训
21
+ - ✅ 代码风格高度一致
22
+ - ✅ 减少 Code Review 时间
23
+
24
+ ### 提交规范
25
+
26
+ ```bash
27
+ git add .apirc.json
28
+ git commit -m "chore: add api generator config"
29
+ ```
30
+
31
+ ---
32
+
33
+ ## 2. 命名规范
34
+
35
+ ### 类型名:PascalCase + Response 后缀
36
+
37
+ ✅ **推荐:**
38
+
39
+ ```typescript
40
+ UserResponse;
41
+ CreateOrderResponse;
42
+ UpdateProfileResponse;
43
+ ```
44
+
45
+ ❌ **不推荐:**
46
+
47
+ ```typescript
48
+ userResponse; // 首字母小写
49
+ User; // 缺少后缀
50
+ user_response; // 下划线命名
51
+ ```
52
+
53
+ ### API 方法名:camelCase + 动词前缀
54
+
55
+ ✅ **推荐:**
56
+
57
+ ```typescript
58
+ getUsers();
59
+ createOrder();
60
+ updateUserProfile();
61
+ deletePost();
62
+ ```
63
+
64
+ ❌ **不推荐:**
65
+
66
+ ```typescript
67
+ Users(); // 缺少动词
68
+ GetUsers(); // 首字母大写
69
+ user_list(); // 下划线命名
70
+ ```
71
+
72
+ ### 动词建议
73
+
74
+ | HTTP 方法 | 推荐动词前缀 |
75
+ | --------- | ---------------------- |
76
+ | GET | get, fetch, query |
77
+ | POST | create, add |
78
+ | PUT | update, replace |
79
+ | PATCH | update, modify |
80
+ | DELETE | delete, remove, revoke |
81
+
82
+ ---
83
+
84
+ ## 3. 目录组织
85
+
86
+ ### 推荐结构
87
+
88
+ ```
89
+ src/api/
90
+ ├── user/
91
+ │ ├── api.ts # 用户相关 API
92
+ │ └── types.ts # 用户相关类型
93
+ ├── order/
94
+ │ ├── api.ts # 订单相关 API
95
+ │ └── types.ts # 订单相关类型
96
+ ├── product/
97
+ │ ├── api.ts # 产品相关 API
98
+ │ └── types.ts # 产品相关类型
99
+ └── common/
100
+ ├── api.ts # 通用 API
101
+ └── types.ts # 通用类型
102
+ ```
103
+
104
+ ### 优势
105
+
106
+ - ✅ 模块化清晰
107
+ - ✅ 便于维护和查找
108
+ - ✅ 减少命名冲突
109
+ - ✅ 支持按需导入
110
+
111
+ ---
112
+
113
+ ## 4. 版本控制
114
+
115
+ ### 应该提交的文件
116
+
117
+ ```bash
118
+ # ✅ 生成的代码(推荐提交)
119
+ git add src/api/
120
+
121
+ # ✅ 项目配置(强烈推荐)
122
+ git add .apirc.json
123
+
124
+ # ✅ 文档更新
125
+ git add docs/
126
+ ```
127
+
128
+ ### 不应该提交的文件
129
+
130
+ ```bash
131
+ # ❌ 全局配置(个人配置)
132
+ # ~/.apirc.json 不要提交
133
+
134
+ # ❌ 临时文件
135
+ # *.tmp, *.bak 等
136
+ ```
137
+
138
+ ### Commit 规范
139
+
140
+ ```bash
141
+ # 新增接口
142
+ git commit -m "feat: add user api"
143
+
144
+ # 更新接口
145
+ git commit -m "feat: update order api"
146
+
147
+ # 添加配置
148
+ git commit -m "chore: add api-gen config"
149
+
150
+ # 修复问题
151
+ git commit -m "fix: correct api type definition"
152
+ ```
153
+
154
+ ---
155
+
156
+ ## 5. 类型安全
157
+
158
+ ### 使用泛型
159
+
160
+ ```typescript
161
+ // ✅ 推荐:使用泛型
162
+ export function getUsers() {
163
+ return request.get<UserResponse>('/api/users');
164
+ }
165
+
166
+ // ❌ 不推荐:不指定类型
167
+ export function getUsers() {
168
+ return request.get('/api/users');
169
+ }
170
+ ```
171
+
172
+ ### 避免 any
173
+
174
+ ```typescript
175
+ // ✅ 推荐:明确类型
176
+ export interface CreateUserRequest {
177
+ name: string;
178
+ email: string;
179
+ }
180
+
181
+ // ❌ 不推荐:使用 any
182
+ export interface CreateUserRequest {
183
+ [key: string]: any;
184
+ }
185
+ ```
186
+
187
+ ---
188
+
189
+ ## 6. 错误处理
190
+
191
+ ### 统一错误处理
192
+
193
+ 在 `request` 模块中统一处理:
194
+
195
+ ```typescript
196
+ // @/utils/request.ts
197
+ import axios from 'axios';
198
+
199
+ const request = axios.create({
200
+ baseURL: '/api',
201
+ timeout: 10000,
202
+ });
203
+
204
+ // 统一错误处理
205
+ request.interceptors.response.use(
206
+ response => response.data,
207
+ error => {
208
+ console.error('API Error:', error);
209
+ return Promise.reject(error);
210
+ },
211
+ );
212
+
213
+ export default request;
214
+ ```
215
+
216
+ ---
217
+
218
+ ## 7. 增量开发
219
+
220
+ ### 新增接口时
221
+
222
+ ```bash
223
+ # 输出到已存在的目录
224
+ go-gen fetch
225
+ # 选择相同的输出目录
226
+ ```
227
+
228
+ ### 注意事项
229
+
230
+ - ✅ 新接口会自动追加
231
+ - ✅ 已有代码不会被覆盖
232
+ - ⚠️ 类型名冲突会自动重命名
233
+ - ⚠️ 检查是否有重复的 API 方法
234
+
235
+ ---
236
+
237
+ ## 8. 性能优化
238
+
239
+ ### 批量生成时
240
+
241
+ ```bash
242
+ # OpenAPI 批量模式
243
+ go-gen openapi https://api.example.com/swagger.json
244
+ # 选择 "批量生成"
245
+ ```
246
+
247
+ ### 单个生成时
248
+
249
+ ```bash
250
+ # 只生成需要的接口
251
+ go-gen fetch
252
+ ```
253
+
254
+ ### 建议
255
+
256
+ - ✅ 大型项目用批量模式
257
+ - ✅ 小型项目或单个接口用 fetch 模式
258
+ - ✅ 定期清理不用的接口
259
+
260
+ ---
261
+
262
+ ## 9. 团队协作流程
263
+
264
+ ### 步骤 1:项目负责人
265
+
266
+ ```bash
267
+ # 初始化配置
268
+ go-gen init
269
+
270
+ # 编辑 .apirc.json
271
+ vim .apirc.json
272
+
273
+ # 提交配置
274
+ git add .apirc.json
275
+ git commit -m "chore: setup api generator config"
276
+ git push
277
+ ```
278
+
279
+ ### 步骤 2:团队成员
280
+
281
+ ```bash
282
+ # 拉取配置
283
+ git pull
284
+
285
+ # 直接使用
286
+ go-gen fetch
287
+ ```
288
+
289
+ ### 步骤 3:代码审查
290
+
291
+ ```bash
292
+ # 检查生成的代码
293
+ git diff src/api/
294
+
295
+ # 确认无误后提交
296
+ git add src/api/
297
+ git commit -m "feat: add new api endpoints"
298
+ ```
299
+
300
+ ---
301
+
302
+ ## 10. 文档维护
303
+
304
+ ### 在 README 中说明
305
+
306
+ ```markdown
307
+ ## API 生成
308
+
309
+ 本项目使用 `go-gen` 生成 API 代码。
310
+
311
+ ### 生成新接口
312
+
313
+ \`\`\`bash
314
+ go-gen fetch
315
+ \`\`\`
316
+
317
+ ### 配置
318
+
319
+ 查看 `.apirc.json` 了解项目配置。
320
+ \`\`\`
321
+ ```
322
+
323
+ ### 保持文档同步
324
+
325
+ - 更新 API 时更新文档
326
+ - 记录重要的配置变更
327
+ - 说明特殊的使用方式