@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.
@@ -0,0 +1,328 @@
1
+ const ora = require('ora');
2
+ const prompts = require('prompts');
3
+ const chalk = require('chalk');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const loadOpenAPI = require('../utils/load-openapi');
8
+ const { schemaToSample } = require('../utils/sampler');
9
+ const quicktype = require('./quicktype');
10
+
11
+ const { generateApiFile } = require('./writer');
12
+ const { pascalCase } = require('../utils/name');
13
+ const { loadConfig } = require('./config');
14
+
15
+ async function openapiMode(source) {
16
+ const config = loadConfig();
17
+
18
+ // ===== 阶段 1:读取 OpenAPI =====
19
+ const loadSpinner = ora('📖 读取 OpenAPI...').start();
20
+
21
+ let openapi;
22
+ try {
23
+ openapi = await loadOpenAPI(source);
24
+ loadSpinner.succeed('✅ OpenAPI 读取完成');
25
+ } catch (error) {
26
+ loadSpinner.fail(`❌ OpenAPI 读取失败: ${error.message}`);
27
+ console.log(chalk.red('💡 请检查文件路径或 URL 是否正确'));
28
+ process.exit(1);
29
+ }
30
+
31
+ // ===== 将所有接口拍平成列表 =====
32
+ const apis = [];
33
+
34
+ for (const [url, methods] of Object.entries(openapi?.paths || {})) {
35
+ for (const [method, api] of Object.entries(methods || {})) {
36
+ const schema =
37
+ api.responses?.['200']?.content?.['application/json']?.schema;
38
+
39
+ if (!schema) continue;
40
+
41
+ apis.push({ url, method, api, schema });
42
+ }
43
+ }
44
+
45
+ if (apis.length === 0) {
46
+ ora().warn('⚠️ 未发现可生成的接口');
47
+ return;
48
+ }
49
+
50
+ // ===== 阶段 2:选择生成模式 =====
51
+ const { generateMode } = await prompts({
52
+ type: 'select',
53
+ name: 'generateMode',
54
+ message: `🚀 发现 ${apis.length} 个接口,请选择生成模式:`,
55
+ choices: [
56
+ { title: '📝 逐个生成(可自定义名称)', value: 'manual' },
57
+ { title: '⚡ 批量生成(自动命名)', value: 'batch' },
58
+ ],
59
+ });
60
+
61
+ if (!generateMode) {
62
+ console.log(chalk.yellow('\n✋ 操作已取消'));
63
+ return;
64
+ }
65
+
66
+ if (generateMode === 'batch') {
67
+ // ===== 批量生成模式 =====
68
+
69
+ // 🔑 关键:只询问一次输出目录
70
+ const desktopPath = path.join(os.homedir(), 'Desktop');
71
+ const currentPath = process.cwd();
72
+
73
+ const outputPathChoice = await prompts({
74
+ type: 'select',
75
+ name: 'outputPath',
76
+ message: '📂 输出目录(所有接口统一使用):',
77
+ choices: [
78
+ { title: '💻 桌面', value: desktopPath },
79
+ { title: '📁 当前目录', value: currentPath },
80
+ { title: '🔍 自定义路径', value: 'custom' },
81
+ ],
82
+ initial: config.defaultOutputPath === 'desktop' ? 0 : 1,
83
+ });
84
+
85
+ if (!outputPathChoice.outputPath) {
86
+ console.log(chalk.yellow('\n✋ 操作已取消'));
87
+ return;
88
+ }
89
+
90
+ let baseDir = outputPathChoice.outputPath;
91
+
92
+ if (baseDir === 'custom') {
93
+ const customPathResponse = await prompts({
94
+ type: 'text',
95
+ name: 'customPath',
96
+ message: '📁 请输入保存路径:',
97
+ initial: currentPath,
98
+ validate: input => {
99
+ const resolved = path.resolve(input);
100
+ return fs.existsSync(resolved) || '路径不存在';
101
+ },
102
+ });
103
+
104
+ if (!customPathResponse.customPath) {
105
+ console.log(chalk.yellow('\n✋ 操作已取消'));
106
+ return;
107
+ }
108
+
109
+ baseDir = customPathResponse.customPath;
110
+ }
111
+
112
+ const batchSpinner = ora(`⚡ 批量生成中...`).start();
113
+
114
+ let successCount = 0;
115
+ let failCount = 0;
116
+
117
+ for (let i = 0; i < apis.length; i++) {
118
+ const { url, method, api, schema } = apis[i];
119
+
120
+ batchSpinner.text = `⚡ 生成中 (${i + 1}/${
121
+ apis.length
122
+ }): ${method.toUpperCase()} ${url}`;
123
+
124
+ try {
125
+ // 1️⃣ schema → sample
126
+ const sample = schemaToSample(schema);
127
+
128
+ // 2️⃣ 类型名(自动生成)
129
+ const typeName =
130
+ pascalCase(method) + pascalCase(url.replace(/\//g, '_')) + 'Response';
131
+
132
+ // 3️⃣ API 方法名(自动生成)
133
+ const apiName =
134
+ api.operationId ||
135
+ method.toLowerCase() + pascalCase(url.replace(/\//g, '_'));
136
+
137
+ // 4️⃣ 生成类型
138
+ const typesContent = await quicktype.generateTypes(sample, typeName);
139
+
140
+ // 5️⃣ 判断是否需要请求体
141
+ const hasRequestBody = ['post', 'put', 'patch'].includes(
142
+ method.toLowerCase(),
143
+ );
144
+
145
+ // 6️⃣ 写入文件(不再询问输出目录)
146
+ const outputDir = path.join(baseDir, apiName);
147
+ fs.mkdirSync(outputDir, { recursive: true });
148
+
149
+ // 写入 types.ts
150
+ const typesFilePath = path.join(outputDir, 'types.ts');
151
+ fs.writeFileSync(typesFilePath, typesContent);
152
+
153
+ // 写入 api.ts
154
+ const apiContent = generateApiFile({
155
+ apiName,
156
+ typeName,
157
+ url,
158
+ method: method.toUpperCase(),
159
+ hasRequestBody,
160
+ });
161
+
162
+ const apiFilePath = path.join(outputDir, 'api.ts');
163
+ fs.writeFileSync(apiFilePath, apiContent);
164
+
165
+ successCount++;
166
+ } catch (error) {
167
+ failCount++;
168
+ console.log(
169
+ chalk.yellow(
170
+ `\n⚠️ 跳过 ${method.toUpperCase()} ${url}: ${error.message}`,
171
+ ),
172
+ );
173
+ }
174
+ }
175
+
176
+ batchSpinner.succeed(
177
+ `✅ 批量生成完成!成功: ${successCount},失败: ${failCount}`,
178
+ );
179
+ console.log(chalk.green(`成功: ${successCount},失败: ${failCount}`));
180
+ console.log(chalk.cyan(`📂 输出目录: ${baseDir}\n`));
181
+ } else {
182
+ // ===== 逐个生成模式 =====
183
+ const parseSpinner = ora(`🔍 解析接口(共 ${apis.length} 个)...`).start();
184
+ parseSpinner.succeed('✅ 接口解析完成');
185
+
186
+ // 🔑 关键:询问是否对所有接口使用相同的输出目录
187
+ const { useSameDir } = await prompts({
188
+ type: 'confirm',
189
+ name: 'useSameDir',
190
+ message: '📂 是否对所有接口使用相同的输出目录?',
191
+ initial: true,
192
+ });
193
+
194
+ let baseDir = null;
195
+
196
+ if (useSameDir) {
197
+ const desktopPath = path.join(os.homedir(), 'Desktop');
198
+ const currentPath = process.cwd();
199
+
200
+ const outputPathChoice = await prompts({
201
+ type: 'select',
202
+ name: 'outputPath',
203
+ message: '📂 选择输出目录:',
204
+ choices: [
205
+ { title: '💻 桌面', value: desktopPath },
206
+ { title: '📁 当前目录', value: currentPath },
207
+ { title: '🔍 自定义路径', value: 'custom' },
208
+ ],
209
+ initial: 1,
210
+ });
211
+
212
+ baseDir = outputPathChoice.outputPath;
213
+
214
+ if (baseDir === 'custom') {
215
+ const customPathResponse = await prompts({
216
+ type: 'text',
217
+ name: 'customPath',
218
+ message: '📁 请输入保存路径:',
219
+ initial: currentPath,
220
+ });
221
+
222
+ baseDir = customPathResponse.customPath;
223
+ }
224
+ }
225
+
226
+ for (let i = 0; i < apis.length; i++) {
227
+ const { url, method, api, schema } = apis[i];
228
+
229
+ console.log(
230
+ chalk.cyan(
231
+ `\n[${i + 1}/${apis.length}] ${method.toUpperCase()} ${url}`,
232
+ ),
233
+ );
234
+
235
+ try {
236
+ // 1️⃣ schema → sample
237
+ const sample = schemaToSample(schema);
238
+
239
+ // 2️⃣ 类型名
240
+ const typeName =
241
+ pascalCase(method) + pascalCase(url.replace(/\//g, '_')) + 'Response';
242
+
243
+ // 3️⃣ API 方法名(支持 operationId)
244
+ const defaultApiName =
245
+ api.operationId ||
246
+ method.toLowerCase() + pascalCase(url.replace(/\//g, '_'));
247
+
248
+ const { apiName } = await prompts({
249
+ type: 'text',
250
+ name: 'apiName',
251
+ message: `📦 API 方法名:`,
252
+ initial: defaultApiName,
253
+ });
254
+
255
+ if (!apiName) {
256
+ console.log(chalk.yellow('跳过此接口'));
257
+ continue;
258
+ }
259
+
260
+ // 4️⃣ 生成类型
261
+ const typesContent = await quicktype.generateTypes(sample, typeName);
262
+
263
+ // 5️⃣ 判断是否需要请求体
264
+ const hasRequestBody = ['post', 'put', 'patch'].includes(
265
+ method.toLowerCase(),
266
+ );
267
+
268
+ // 6️⃣ 写入文件
269
+ let outputDir;
270
+
271
+ if (baseDir) {
272
+ // 使用统一的输出目录
273
+ outputDir = path.join(baseDir, apiName);
274
+ } else {
275
+ // 每次询问输出目录
276
+ const outputPathChoice = await prompts({
277
+ type: 'select',
278
+ name: 'outputPath',
279
+ message: '📂 输出目录:',
280
+ choices: [
281
+ { title: '💻 桌面', value: path.join(os.homedir(), 'Desktop') },
282
+ { title: '📁 当前目录', value: process.cwd() },
283
+ ],
284
+ initial: 1,
285
+ });
286
+
287
+ outputDir = path.join(outputPathChoice.outputPath, apiName);
288
+ }
289
+
290
+ fs.mkdirSync(outputDir, { recursive: true });
291
+
292
+ // 写入 types.ts
293
+ const typesFilePath = path.join(outputDir, 'types.ts');
294
+ fs.writeFileSync(typesFilePath, typesContent);
295
+
296
+ // 写入 api.ts
297
+ const apiContent = generateApiFile({
298
+ apiName,
299
+ typeName,
300
+ url,
301
+ method: method.toUpperCase(),
302
+ hasRequestBody,
303
+ });
304
+
305
+ const apiFilePath = path.join(outputDir, 'api.ts');
306
+ fs.writeFileSync(apiFilePath, apiContent);
307
+
308
+ console.log(chalk.green(`✅ 已生成: ${outputDir}`));
309
+ } catch (error) {
310
+ console.log(chalk.red(`❌ 生成失败: ${error.message}`));
311
+
312
+ const resp = await prompts({
313
+ type: 'confirm',
314
+ name: 'continueGen',
315
+ message: '是否继续生成其他接口?',
316
+ initial: true,
317
+ });
318
+
319
+ const continueGen = resp && resp.continueGen;
320
+ if (!continueGen) break;
321
+ }
322
+ }
323
+
324
+ console.log(chalk.green('\n✨ 所有接口处理完成!\n'));
325
+ }
326
+ }
327
+
328
+ module.exports = openapiMode;
@@ -0,0 +1,31 @@
1
+ const {
2
+ quicktype,
3
+ InputData,
4
+ jsonInputForTargetLanguage,
5
+ } = require('quicktype-core');
6
+
7
+ async function generateTypes(jsonData, typeName) {
8
+ const jsonInput = jsonInputForTargetLanguage('typescript');
9
+ await jsonInput.addSource({
10
+ name: typeName,
11
+ samples: [JSON.stringify(jsonData)],
12
+ });
13
+
14
+ const inputData = new InputData();
15
+ inputData.addInput(jsonInput);
16
+
17
+ const result = await quicktype({
18
+ inputData,
19
+ lang: 'typescript',
20
+ rendererOptions: {
21
+ 'just-types': 'true',
22
+ 'acronym-style': 'camel',
23
+ 'prefer-unions': 'true',
24
+ 'explicit-unions': 'true',
25
+ },
26
+ });
27
+
28
+ return result.lines.join('\n');
29
+ }
30
+
31
+ module.exports = { generateTypes };