fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at feat/use-query-options 289 lines 8.3 kB view raw
1import fs from 'node:fs'; 2import path from 'node:path'; 3 4import colors from 'ansi-colors'; 5import open from 'open'; 6 7import { ensureDirSync } from './fs'; 8import { loadPackageJson } from './tsConfig'; 9 10type IJobError = { 11 error: Error; 12 jobIndex: number; 13}; 14 15/** 16 * Represents a single configuration error. 17 * 18 * Used for reporting issues with a specific config instance. 19 */ 20export class ConfigError extends Error { 21 constructor(message: string) { 22 super(message); 23 this.name = 'ConfigError'; 24 } 25} 26 27/** 28 * Aggregates multiple config errors with their job indices for reporting. 29 */ 30export class ConfigValidationError extends Error { 31 readonly errors: ReadonlyArray<IJobError>; 32 33 constructor(errors: Array<IJobError>) { 34 super(`Found ${errors.length} configuration ${errors.length === 1 ? 'error' : 'errors'}.`); 35 this.name = 'ConfigValidationError'; 36 this.errors = errors; 37 } 38} 39 40/** 41 * Represents an error caused by invalid or inaccessible input. 42 * 43 * Used for errors like file not found, URL not reachable, etc. 44 */ 45export class InputError extends Error { 46 readonly originalError: Error & { source?: string }; 47 48 constructor(message: string, originalError: Error & { source?: string }) { 49 super(message); 50 this.name = 'InputError'; 51 this.originalError = originalError; 52 } 53} 54 55/** 56 * Represents a runtime error originating from a specific job. 57 * 58 * Used for reporting job-level failures that are not config validation errors. 59 */ 60export class JobError extends Error { 61 readonly originalError: IJobError; 62 63 constructor(message: string, error: IJobError) { 64 super(message); 65 this.name = 'JobError'; 66 this.originalError = error; 67 } 68} 69 70export class HeyApiError extends Error { 71 args: ReadonlyArray<unknown>; 72 event: string; 73 pluginName: string; 74 75 constructor({ 76 args, 77 error, 78 event, 79 name, 80 pluginName, 81 }: { 82 args: unknown[]; 83 error: Error; 84 event: string; 85 name: string; 86 pluginName: string; 87 }) { 88 const message = error instanceof Error ? error.message : 'Unknown error'; 89 super(message); 90 91 this.args = args; 92 this.cause = error.cause; 93 this.event = event; 94 this.name = name || error.name; 95 this.pluginName = pluginName; 96 this.stack = error.stack; 97 } 98} 99 100export function logCrashReport(error: unknown, logsDir: string): string | undefined { 101 if ( 102 error instanceof ConfigError || 103 error instanceof ConfigValidationError || 104 error instanceof InputError 105 ) { 106 return; 107 } 108 109 if (error instanceof JobError) { 110 error = error.originalError.error; 111 } 112 113 const logName = `openapi-ts-error-${Date.now()}.log`; 114 const fullDir = path.resolve(process.cwd(), logsDir); 115 ensureDirSync(fullDir); 116 const logPath = path.resolve(fullDir, logName); 117 118 let logContent = `[${new Date().toISOString()}] `; 119 120 if (error instanceof HeyApiError) { 121 logContent += `${error.name} during event "${error.event}"\n`; 122 if (error.pluginName) { 123 logContent += `Plugin: ${error.pluginName}\n`; 124 } 125 logContent += `Arguments: ${JSON.stringify(error.args, null, 2)}\n\n`; 126 } 127 128 const message = error instanceof Error ? error.message : String(error); 129 const stack = error instanceof Error ? error.stack : undefined; 130 131 logContent += `Error: ${message}\n`; 132 if (stack) { 133 logContent += `Stack:\n${stack}\n`; 134 } 135 136 fs.writeFileSync(logPath, logContent); 137 138 return logPath; 139} 140 141export async function openGitHubIssueWithCrashReport( 142 error: unknown, 143 initialDir: string, 144): Promise<void> { 145 const packageJson = loadPackageJson(initialDir); 146 if (!packageJson?.bugs.url) return; 147 148 if (error instanceof JobError) { 149 error = error.originalError.error; 150 } 151 152 let body = ''; 153 154 if (error instanceof HeyApiError) { 155 if (error.pluginName) { 156 body += `**Plugin**: \`${error.pluginName}\`\n`; 157 } 158 body += `**Event**: \`${error.event}\`\n`; 159 body += `**Arguments**:\n\`\`\`ts\n${JSON.stringify(error.args, null, 2)}\n\`\`\`\n\n`; 160 } 161 162 const message = error instanceof Error ? error.message : String(error); 163 const stack = error instanceof Error ? error.stack : undefined; 164 165 body += `**Error**: \`${message}\`\n`; 166 if (stack) { 167 body += `\n**Stack Trace**:\n\`\`\`\n${stack}\n\`\`\``; 168 } 169 170 const search = new URLSearchParams({ 171 body, 172 labels: 'bug 🔥', 173 title: 'Crash Report', 174 }); 175 const url = `${packageJson.bugs.url}new?${search.toString()}`; 176 await open(url); 177} 178 179export function getInputError(error: unknown): InputError | undefined { 180 if (error instanceof InputError) { 181 return error; 182 } 183 if (error instanceof JobError && error.originalError.error instanceof InputError) { 184 return error.originalError.error; 185 } 186} 187 188export function printCrashReport({ 189 error, 190 logPath, 191}: { 192 error: unknown; 193 logPath: string | undefined; 194}): void { 195 if (error instanceof ConfigValidationError && error.errors.length) { 196 const groupByJob = new Map<number, Array<Error>>(); 197 for (const { error: err, jobIndex } of error.errors) { 198 if (!groupByJob.has(jobIndex)) { 199 groupByJob.set(jobIndex, []); 200 } 201 groupByJob.get(jobIndex)!.push(err); 202 } 203 204 for (const [jobIndex, errors] of groupByJob.entries()) { 205 const jobPrefix = colors.gray(`[Job ${jobIndex + 1}] `); 206 const count = errors.length; 207 const baseString = colors.red( 208 `Found ${count} configuration ${count === 1 ? 'error' : 'errors'}:`, 209 ); 210 console.error(`${jobPrefix}❗️ ${baseString}`); 211 errors.forEach((err, index) => { 212 const itemPrefixStr = ` [${index + 1}] `; 213 const itemPrefix = colors.red(itemPrefixStr); 214 console.error(`${jobPrefix}${itemPrefix}${colors.white(err.message)}`); 215 }); 216 } 217 } else { 218 let jobPrefix = colors.gray('[root] '); 219 if (error instanceof JobError) { 220 jobPrefix = colors.gray(`[Job ${error.originalError.jobIndex + 1}] `); 221 error = error.originalError.error; 222 } 223 224 if (error instanceof InputError) { 225 const source = (error.originalError as { source?: string }).source; 226 const itemPrefixStr = ` `; 227 228 const isNetworkError = error.message.startsWith('Input request failed'); 229 if (isNetworkError) { 230 console.error(`${jobPrefix}${colors.red(`${error.message}`)}`); 231 if (source) console.error(colors.gray(source)); 232 console.error(colors.gray('\nPlease verify that:')); 233 console.error(colors.gray(`${itemPrefixStr}• The URL is correct`)); 234 console.error(colors.gray(`${itemPrefixStr}• Your API key is valid`)); 235 console.error(colors.gray(`${itemPrefixStr}• You have network access`)); 236 return; 237 } 238 239 console.error(`${jobPrefix}${colors.red('❌ Input file not found:')}`); 240 if (source) console.error(colors.gray(source)); 241 console.error(colors.gray('\nPlease verify that:')); 242 console.error(colors.gray(`${itemPrefixStr}• The file exists`)); 243 console.error(colors.gray(`${itemPrefixStr}• The path is correct`)); 244 console.error(colors.gray(`${itemPrefixStr}• You have read permissions`)); 245 return; 246 } 247 248 const baseString = colors.red('Failed with the message:'); 249 console.error(`${jobPrefix}${baseString}`); 250 const itemPrefixStr = ` `; 251 const itemPrefix = colors.red(itemPrefixStr); 252 console.error( 253 `${jobPrefix}${itemPrefix}${typeof error === 'string' ? error : error instanceof Error ? error.message : 'Unknown error'}`, 254 ); 255 } 256 257 if (logPath) { 258 const jobPrefix = colors.gray('[root] '); 259 console.error(`${jobPrefix}${colors.cyan('📄 Crash log saved to:')} ${colors.gray(logPath)}`); 260 } 261} 262 263export async function shouldReportCrash({ 264 error, 265 isInteractive, 266}: { 267 error: unknown; 268 isInteractive: boolean | undefined; 269}): Promise<boolean> { 270 if ( 271 !isInteractive || 272 error instanceof ConfigError || 273 error instanceof ConfigValidationError || 274 error instanceof InputError 275 ) { 276 return false; 277 } 278 279 return new Promise((resolve) => { 280 const jobPrefix = colors.gray('[root] '); 281 console.log( 282 `${jobPrefix}${colors.yellow('📢 Open a GitHub issue with crash details? (y/N):')}`, 283 ); 284 process.stdin.setEncoding('utf8'); 285 process.stdin.once('data', (data: string) => { 286 resolve(data.trim().toLowerCase() === 'y'); 287 }); 288 }); 289}