@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/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/README.en.md +151 -0
- package/README.md +151 -0
- package/bin/index.js +86 -0
- package/core/config.js +176 -0
- package/core/fetch-mode.js +400 -0
- package/core/openapi-mode.js +328 -0
- package/core/quicktype.js +31 -0
- package/core/writer.js +413 -0
- package/docs/BEST_PRACTICES.md +327 -0
- package/docs/CONFIGURATION.md +161 -0
- package/docs/FEATURES.md +136 -0
- package/docs/TROUBLESHOOTING.md +386 -0
- package/docs/USE_CASES.md +247 -0
- package/package.json +68 -0
- package/utils/load-openapi.js +23 -0
- package/utils/name.js +17 -0
- package/utils/sampler.js +28 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
const prompts = require('prompts');
|
|
2
|
+
const fetch = require('node-fetch');
|
|
3
|
+
const ora = require('ora');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const { generateTypes } = require('./quicktype');
|
|
6
|
+
const { writeFiles } = require('./writer');
|
|
7
|
+
const { loadConfig } = require('./config');
|
|
8
|
+
|
|
9
|
+
// 全局取消控制器
|
|
10
|
+
let globalAbortController = null;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 增强版 fetch 函数,支持用户取消
|
|
14
|
+
* @param {string} url
|
|
15
|
+
* @param {object} options
|
|
16
|
+
* @param {number} maxRetries
|
|
17
|
+
* @returns {Promise<any>}
|
|
18
|
+
*/
|
|
19
|
+
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
|
|
20
|
+
let attempt = 0;
|
|
21
|
+
let lastError;
|
|
22
|
+
|
|
23
|
+
const { timeout = 10000, signal: externalSignal, ...fetchOptions } = options;
|
|
24
|
+
|
|
25
|
+
while (attempt < maxRetries) {
|
|
26
|
+
attempt++;
|
|
27
|
+
|
|
28
|
+
const controller = new AbortController();
|
|
29
|
+
const { signal } = controller;
|
|
30
|
+
|
|
31
|
+
let timeoutId;
|
|
32
|
+
let abortListener;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
// ========= 外部 abort 透传 =========
|
|
36
|
+
if (externalSignal) {
|
|
37
|
+
if (externalSignal.aborted) {
|
|
38
|
+
throw createAbortError();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
abortListener = () => controller.abort();
|
|
42
|
+
externalSignal.addEventListener('abort', abortListener);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ========= timeout =========
|
|
46
|
+
if (timeout > 0) {
|
|
47
|
+
timeoutId = setTimeout(() => {
|
|
48
|
+
controller.abort();
|
|
49
|
+
}, timeout);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const res = await fetch(url, {
|
|
53
|
+
...fetchOptions,
|
|
54
|
+
signal,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
throw new Error(`HTTP ${res.status}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return await res.json();
|
|
62
|
+
} catch (err) {
|
|
63
|
+
// ====== Abort 错误语义统一 ======
|
|
64
|
+
if (err.name === 'AbortError') {
|
|
65
|
+
if (externalSignal?.aborted) {
|
|
66
|
+
throw new Error('用户取消了请求');
|
|
67
|
+
}
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
lastError = err;
|
|
72
|
+
|
|
73
|
+
if (attempt >= maxRetries) {
|
|
74
|
+
throw lastError;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 重试前等待
|
|
78
|
+
if (attempt < maxRetries) {
|
|
79
|
+
console.log(
|
|
80
|
+
chalk.yellow(
|
|
81
|
+
`⚠️ 请求失败 (尝试 ${attempt}/${maxRetries}),2秒后重试...`,
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
85
|
+
}
|
|
86
|
+
} finally {
|
|
87
|
+
// ========= 清理资源 =========
|
|
88
|
+
if (timeoutId) {
|
|
89
|
+
clearTimeout(timeoutId);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (
|
|
93
|
+
externalSignal &&
|
|
94
|
+
abortListener &&
|
|
95
|
+
typeof externalSignal.removeEventListener === 'function'
|
|
96
|
+
) {
|
|
97
|
+
externalSignal.removeEventListener('abort', abortListener);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
throw lastError;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 创建 AbortError(兼容测试) */
|
|
106
|
+
function createAbortError() {
|
|
107
|
+
const err = new Error('Aborted');
|
|
108
|
+
err.name = 'AbortError';
|
|
109
|
+
return err;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 监听用户输入,允许按 Ctrl+C 取消请求
|
|
114
|
+
* 🔥 关键修复: 使用 once + 立即清理机制
|
|
115
|
+
*/
|
|
116
|
+
function setupCancelListener(spinner) {
|
|
117
|
+
console.log(chalk.gray('\n💡 提示: 请求过程中可以按 Ctrl+C 取消\n'));
|
|
118
|
+
|
|
119
|
+
let isHandled = false; // 防止重复处理
|
|
120
|
+
|
|
121
|
+
// Ctrl+C 处理器
|
|
122
|
+
const abortHandler = () => {
|
|
123
|
+
if (isHandled) return; // 已处理过,直接返回
|
|
124
|
+
|
|
125
|
+
if (globalAbortController && !globalAbortController.signal.aborted) {
|
|
126
|
+
isHandled = true;
|
|
127
|
+
// spinner.fail(chalk.yellow("⚠️ 用户取消了请求"));
|
|
128
|
+
globalAbortController.abort();
|
|
129
|
+
|
|
130
|
+
// 🔥 关键: 立即移除监听器,避免影响后续 prompts
|
|
131
|
+
cleanup();
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
process.on('SIGINT', abortHandler);
|
|
136
|
+
|
|
137
|
+
// 返回清理函数
|
|
138
|
+
const cleanup = () => {
|
|
139
|
+
process.removeListener('SIGINT', abortHandler);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return cleanup;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function fetchMode() {
|
|
146
|
+
const config = loadConfig();
|
|
147
|
+
|
|
148
|
+
// 第一步:基本信息
|
|
149
|
+
const basicInfo = await prompts([
|
|
150
|
+
{
|
|
151
|
+
type: 'text',
|
|
152
|
+
name: 'url',
|
|
153
|
+
message: '🌐 请输入 API URL:',
|
|
154
|
+
validate: v => {
|
|
155
|
+
try {
|
|
156
|
+
new URL(v);
|
|
157
|
+
return true;
|
|
158
|
+
} catch {
|
|
159
|
+
return '请输入合法 URL';
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
type: 'select',
|
|
165
|
+
name: 'method',
|
|
166
|
+
message: '🔧 请求方法:',
|
|
167
|
+
choices: [
|
|
168
|
+
{ title: 'GET', value: 'GET' },
|
|
169
|
+
{ title: 'POST', value: 'POST' },
|
|
170
|
+
{ title: 'PUT', value: 'PUT' },
|
|
171
|
+
{ title: 'DELETE', value: 'DELETE' },
|
|
172
|
+
{ title: 'PATCH', value: 'PATCH' },
|
|
173
|
+
],
|
|
174
|
+
initial: 0,
|
|
175
|
+
},
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
// 检查是否取消
|
|
179
|
+
if (!basicInfo.url) {
|
|
180
|
+
console.log(chalk.yellow('\n✋ 操作已取消'));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 第二步:认证信息
|
|
185
|
+
const authInfo = await prompts([
|
|
186
|
+
{
|
|
187
|
+
type: 'select',
|
|
188
|
+
name: 'authType',
|
|
189
|
+
message: '🔐 是否需要认证?',
|
|
190
|
+
choices: [
|
|
191
|
+
{ title: '不需要', value: 'none' },
|
|
192
|
+
{ title: 'Bearer Token', value: 'token' },
|
|
193
|
+
{ title: 'Cookie', value: 'cookie' },
|
|
194
|
+
],
|
|
195
|
+
initial: 0,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
type: prev => (prev === 'token' ? 'password' : null),
|
|
199
|
+
name: 'token',
|
|
200
|
+
message: '🔑 请输入 Bearer Token:',
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
type: (prev, values) => (values.authType === 'cookie' ? 'text' : null),
|
|
204
|
+
name: 'cookie',
|
|
205
|
+
message: '🍪 请输入 Cookie:',
|
|
206
|
+
},
|
|
207
|
+
]);
|
|
208
|
+
|
|
209
|
+
// 第三步:是否需要请求体
|
|
210
|
+
let hasRequestBody = false;
|
|
211
|
+
let requestBodyData = null;
|
|
212
|
+
|
|
213
|
+
if (['POST', 'PUT', 'PATCH'].includes(basicInfo.method)) {
|
|
214
|
+
const bodyQuestion = await prompts({
|
|
215
|
+
type: 'confirm',
|
|
216
|
+
name: 'needBody',
|
|
217
|
+
message: '📦 该接口是否需要请求体?',
|
|
218
|
+
initial: false,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
hasRequestBody = bodyQuestion.needBody;
|
|
222
|
+
|
|
223
|
+
// 如果需要请求体,让用户输入示例数据
|
|
224
|
+
if (hasRequestBody) {
|
|
225
|
+
console.log(
|
|
226
|
+
chalk.cyan(
|
|
227
|
+
'\n💡 提示: 请输入请求体的 JSON 示例数据(用于生成 Request 类型)',
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
console.log(
|
|
231
|
+
chalk.gray(
|
|
232
|
+
'示例: {"name": "张三", "age": 25, "email": "test@example.com"}',
|
|
233
|
+
),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const bodyInput = await prompts({
|
|
237
|
+
type: 'text',
|
|
238
|
+
name: 'data',
|
|
239
|
+
message: '📝 请输入请求体 JSON:',
|
|
240
|
+
initial: '{"name": "string", "id": 0}',
|
|
241
|
+
validate: v => {
|
|
242
|
+
try {
|
|
243
|
+
JSON.parse(v);
|
|
244
|
+
return true;
|
|
245
|
+
} catch {
|
|
246
|
+
return '请输入合法的 JSON 格式';
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (bodyInput.data) {
|
|
252
|
+
requestBodyData = JSON.parse(bodyInput.data);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 第四步:类型和方法名
|
|
258
|
+
const naming = await prompts([
|
|
259
|
+
{
|
|
260
|
+
type: 'text',
|
|
261
|
+
name: 'typeName',
|
|
262
|
+
message: '📄 Response Type 名称:',
|
|
263
|
+
initial: 'ApiResponse',
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
type: 'text',
|
|
267
|
+
name: 'apiName',
|
|
268
|
+
message: '📦 API 方法名:',
|
|
269
|
+
initial: 'getData',
|
|
270
|
+
},
|
|
271
|
+
]);
|
|
272
|
+
|
|
273
|
+
if (!naming.typeName) {
|
|
274
|
+
console.log(chalk.yellow('\n✋ 操作已取消'));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 合并所有响应
|
|
279
|
+
const response = {
|
|
280
|
+
...basicInfo,
|
|
281
|
+
...authInfo,
|
|
282
|
+
...naming,
|
|
283
|
+
hasRequestBody,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// 🆕 创建全局 AbortController
|
|
287
|
+
globalAbortController = new AbortController();
|
|
288
|
+
|
|
289
|
+
const fetchSpinner = ora('🚀 请求 API 数据中...').start();
|
|
290
|
+
|
|
291
|
+
// 🆕 设置取消监听器
|
|
292
|
+
const cleanup = setupCancelListener(fetchSpinner);
|
|
293
|
+
|
|
294
|
+
// 🔥 标记请求是否被用户取消
|
|
295
|
+
let userCancelled = false;
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
const headers = {
|
|
299
|
+
'Content-Type': 'application/json',
|
|
300
|
+
};
|
|
301
|
+
if (response.token) headers.Authorization = `Bearer ${response.token}`;
|
|
302
|
+
if (response.cookie) headers.Cookie = response.cookie;
|
|
303
|
+
|
|
304
|
+
const fetchOptions = {
|
|
305
|
+
method: response.method,
|
|
306
|
+
headers,
|
|
307
|
+
timeout: config.timeout || 10000,
|
|
308
|
+
signal: globalAbortController.signal, // 🔥 传递 signal
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// 如果有请求体数据,添加到请求中
|
|
312
|
+
if (hasRequestBody && requestBodyData) {
|
|
313
|
+
fetchOptions.body = JSON.stringify(requestBodyData);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const json = await fetchWithRetry(
|
|
317
|
+
response.url,
|
|
318
|
+
fetchOptions,
|
|
319
|
+
config.maxRetries || 3,
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
// 🔥 请求成功后立即清理监听器
|
|
323
|
+
cleanup();
|
|
324
|
+
globalAbortController = null;
|
|
325
|
+
|
|
326
|
+
fetchSpinner.succeed('✅ API 数据获取完成');
|
|
327
|
+
|
|
328
|
+
// 生成 Response 类型
|
|
329
|
+
const typeSpinner = ora('🧠 生成 TypeScript 类型...').start();
|
|
330
|
+
const typesContent = await generateTypes(json, response.typeName);
|
|
331
|
+
typeSpinner.succeed('✅ Response 类型生成完成');
|
|
332
|
+
|
|
333
|
+
// 如果需要请求体,生成 Request 类型
|
|
334
|
+
let finalTypesContent = typesContent;
|
|
335
|
+
if (hasRequestBody && requestBodyData) {
|
|
336
|
+
const requestSpinner = ora('🧠 生成 Request 类型...').start();
|
|
337
|
+
const requestTypeName = `${response.typeName}Request`;
|
|
338
|
+
const requestTypes = await generateTypes(
|
|
339
|
+
requestBodyData,
|
|
340
|
+
requestTypeName,
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
// 合并 Response 和 Request 类型
|
|
344
|
+
finalTypesContent = typesContent + '\n\n' + requestTypes;
|
|
345
|
+
requestSpinner.succeed('✅ Request 类型生成完成');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const result = await writeFiles({
|
|
349
|
+
apiName: response.apiName,
|
|
350
|
+
typeName: response.typeName,
|
|
351
|
+
url: response.url,
|
|
352
|
+
typesContent: finalTypesContent,
|
|
353
|
+
method: response.method,
|
|
354
|
+
hasRequestBody,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
return result;
|
|
358
|
+
} catch (error) {
|
|
359
|
+
// 🔥 捕获错误后立即清理
|
|
360
|
+
cleanup();
|
|
361
|
+
globalAbortController = null;
|
|
362
|
+
|
|
363
|
+
// 区分用户取消和真实错误
|
|
364
|
+
if (error.message === '用户取消了请求' || error.name === 'AbortError') {
|
|
365
|
+
userCancelled = true;
|
|
366
|
+
fetchSpinner.fail(chalk.yellow('⚠️ 请求已被取消'));
|
|
367
|
+
console.log(chalk.gray('\n提示: 您可以重新开始或退出\n'));
|
|
368
|
+
|
|
369
|
+
// 🔥 用户取消后不再继续执行
|
|
370
|
+
return null;
|
|
371
|
+
} else {
|
|
372
|
+
fetchSpinner.fail(`❌ 请求失败: ${error.message}`);
|
|
373
|
+
|
|
374
|
+
if (config.autoRetry) {
|
|
375
|
+
const retry = await prompts({
|
|
376
|
+
type: 'confirm',
|
|
377
|
+
name: 'value',
|
|
378
|
+
message: '🔄 是否重新开始?',
|
|
379
|
+
initial: true,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
if (retry.value) {
|
|
383
|
+
return fetchMode();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
console.log(chalk.red('\n💡 提示: 请检查网络连接和 URL 是否正确'));
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
} finally {
|
|
391
|
+
// 🆕 最终清理:确保监听器一定被移除
|
|
392
|
+
if (cleanup && typeof cleanup === 'function') {
|
|
393
|
+
cleanup();
|
|
394
|
+
}
|
|
395
|
+
globalAbortController = null;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
module.exports = fetchMode;
|
|
400
|
+
module.exports.fetchWithRetry = fetchWithRetry;
|