UNPKG

express-hale

Version:

๐Ÿš€ Interactive Express.js scaffold CLI with comprehensive error handling, TypeScript/JavaScript, database integrations, Git Flow, and development tools

502 lines โ€ข 20.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ErrorHandler = exports.ResponseFormatter = exports.DatabaseError = exports.ExternalServiceError = exports.RateLimitError = exports.ConflictError = exports.NotFoundError = exports.AuthorizationError = exports.AuthenticationError = exports.ValidationError = exports.HttpError = exports.CLIError = exports.ErrorCategory = exports.HttpStatusCode = void 0; exports.setupGlobalErrorHandlers = setupGlobalErrorHandlers; const chalk_1 = __importDefault(require("chalk")); const fs_extra_1 = __importDefault(require("fs-extra")); const path_1 = __importDefault(require("path")); var HttpStatusCode; (function (HttpStatusCode) { // Success HttpStatusCode[HttpStatusCode["OK"] = 200] = "OK"; HttpStatusCode[HttpStatusCode["CREATED"] = 201] = "CREATED"; HttpStatusCode[HttpStatusCode["ACCEPTED"] = 202] = "ACCEPTED"; HttpStatusCode[HttpStatusCode["NO_CONTENT"] = 204] = "NO_CONTENT"; // Redirection HttpStatusCode[HttpStatusCode["MOVED_PERMANENTLY"] = 301] = "MOVED_PERMANENTLY"; HttpStatusCode[HttpStatusCode["FOUND"] = 302] = "FOUND"; HttpStatusCode[HttpStatusCode["NOT_MODIFIED"] = 304] = "NOT_MODIFIED"; // Client Errors HttpStatusCode[HttpStatusCode["BAD_REQUEST"] = 400] = "BAD_REQUEST"; HttpStatusCode[HttpStatusCode["UNAUTHORIZED"] = 401] = "UNAUTHORIZED"; HttpStatusCode[HttpStatusCode["FORBIDDEN"] = 403] = "FORBIDDEN"; HttpStatusCode[HttpStatusCode["NOT_FOUND"] = 404] = "NOT_FOUND"; HttpStatusCode[HttpStatusCode["METHOD_NOT_ALLOWED"] = 405] = "METHOD_NOT_ALLOWED"; HttpStatusCode[HttpStatusCode["CONFLICT"] = 409] = "CONFLICT"; HttpStatusCode[HttpStatusCode["UNPROCESSABLE_ENTITY"] = 422] = "UNPROCESSABLE_ENTITY"; HttpStatusCode[HttpStatusCode["TOO_MANY_REQUESTS"] = 429] = "TOO_MANY_REQUESTS"; // Server Errors HttpStatusCode[HttpStatusCode["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR"; HttpStatusCode[HttpStatusCode["BAD_GATEWAY"] = 502] = "BAD_GATEWAY"; HttpStatusCode[HttpStatusCode["SERVICE_UNAVAILABLE"] = 503] = "SERVICE_UNAVAILABLE"; HttpStatusCode[HttpStatusCode["GATEWAY_TIMEOUT"] = 504] = "GATEWAY_TIMEOUT"; })(HttpStatusCode || (exports.HttpStatusCode = HttpStatusCode = {})); var ErrorCategory; (function (ErrorCategory) { ErrorCategory["VALIDATION"] = "VALIDATION"; ErrorCategory["AUTHENTICATION"] = "AUTHENTICATION"; ErrorCategory["AUTHORIZATION"] = "AUTHORIZATION"; ErrorCategory["NOT_FOUND"] = "NOT_FOUND"; ErrorCategory["BUSINESS_LOGIC"] = "BUSINESS_LOGIC"; ErrorCategory["EXTERNAL_SERVICE"] = "EXTERNAL_SERVICE"; ErrorCategory["DATABASE"] = "DATABASE"; ErrorCategory["NETWORK"] = "NETWORK"; ErrorCategory["SYSTEM"] = "SYSTEM"; ErrorCategory["UNKNOWN"] = "UNKNOWN"; })(ErrorCategory || (exports.ErrorCategory = ErrorCategory = {})); class CLIError extends Error { constructor(message, code, context, originalError) { super(message); this.name = 'CLIError'; this.code = code; this.context = context; this.originalError = originalError; // Maintain proper stack trace if (Error.captureStackTrace) { Error.captureStackTrace(this, CLIError); } } } exports.CLIError = CLIError; class HttpError extends Error { constructor(message, statusCode = HttpStatusCode.INTERNAL_SERVER_ERROR, code = 'INTERNAL_ERROR', category = ErrorCategory.SYSTEM, isOperational = true, context, originalError) { super(message); this.name = 'HttpError'; this.statusCode = statusCode; this.code = code; this.category = category; this.isOperational = isOperational; this.context = context; this.originalError = originalError; if (Error.captureStackTrace) { Error.captureStackTrace(this, HttpError); } } } exports.HttpError = HttpError; // Specific HTTP Error Classes class ValidationError extends HttpError { constructor(message, details) { super(message, HttpStatusCode.BAD_REQUEST, 'VALIDATION_ERROR', ErrorCategory.VALIDATION, true, { details }); } } exports.ValidationError = ValidationError; class AuthenticationError extends HttpError { constructor(message = 'Authentication required') { super(message, HttpStatusCode.UNAUTHORIZED, 'AUTHENTICATION_ERROR', ErrorCategory.AUTHENTICATION); } } exports.AuthenticationError = AuthenticationError; class AuthorizationError extends HttpError { constructor(message = 'Insufficient permissions') { super(message, HttpStatusCode.FORBIDDEN, 'AUTHORIZATION_ERROR', ErrorCategory.AUTHORIZATION); } } exports.AuthorizationError = AuthorizationError; class NotFoundError extends HttpError { constructor(resource = 'Resource') { super(`${resource} not found`, HttpStatusCode.NOT_FOUND, 'NOT_FOUND_ERROR', ErrorCategory.NOT_FOUND); } } exports.NotFoundError = NotFoundError; class ConflictError extends HttpError { constructor(message) { super(message, HttpStatusCode.CONFLICT, 'CONFLICT_ERROR', ErrorCategory.BUSINESS_LOGIC); } } exports.ConflictError = ConflictError; class RateLimitError extends HttpError { constructor(message = 'Too many requests') { super(message, HttpStatusCode.TOO_MANY_REQUESTS, 'RATE_LIMIT_ERROR', ErrorCategory.SYSTEM); } } exports.RateLimitError = RateLimitError; class ExternalServiceError extends HttpError { constructor(service, message) { super(message || `External service ${service} is unavailable`, HttpStatusCode.SERVICE_UNAVAILABLE, 'EXTERNAL_SERVICE_ERROR', ErrorCategory.EXTERNAL_SERVICE, true, { service }); } } exports.ExternalServiceError = ExternalServiceError; class DatabaseError extends HttpError { constructor(message, operation) { super(message, HttpStatusCode.INTERNAL_SERVER_ERROR, 'DATABASE_ERROR', ErrorCategory.DATABASE, true, { operation }); } } exports.DatabaseError = DatabaseError; class ResponseFormatter { static success(data, meta) { return { success: true, data, meta: { timestamp: new Date().toISOString(), version: '1.0.0', ...meta, }, }; } static error(error, requestId) { let code = 'INTERNAL_ERROR'; let category = ErrorCategory.SYSTEM; const message = error.message || 'An unexpected error occurred'; let details; if (error instanceof HttpError) { code = error.code; category = error.category; details = error.context; } return { success: false, error: { code, message, details, category, timestamp: new Date().toISOString(), requestId, }, meta: { timestamp: new Date().toISOString(), version: '1.0.0', requestId, }, }; } } exports.ResponseFormatter = ResponseFormatter; class ErrorHandler { constructor() { this.retryStrategies = new Map(); this.setupDefaultRetryStrategies(); } static getInstance() { if (!ErrorHandler.instance) { ErrorHandler.instance = new ErrorHandler(); } return ErrorHandler.instance; } setupDefaultRetryStrategies() { // Database retry strategy this.retryStrategies.set('DATABASE_ERROR', { maxAttempts: 3, initialDelay: 1000, backoffMultiplier: 2, maxDelay: 10000, }); // External service retry strategy this.retryStrategies.set('EXTERNAL_SERVICE_ERROR', { maxAttempts: 5, initialDelay: 500, backoffMultiplier: 1.5, maxDelay: 5000, }); // Network retry strategy this.retryStrategies.set('NETWORK_ERROR', { maxAttempts: 3, initialDelay: 2000, backoffMultiplier: 2, maxDelay: 8000, }); } setLogFile(projectPath) { if (projectPath) { this.logFile = path_1.default.join(projectPath, 'express-hale-errors.log'); } } async handleError(error, exitCode = 1) { const timestamp = new Date().toISOString(); if (error instanceof CLIError) { await this.logStructuredError(error, timestamp); this.displayUserFriendlyError(error); } else { await this.logGenericError(error, timestamp); this.displayGenericError(error); } process.exit(exitCode); } async logStructuredError(error, timestamp) { const logEntry = { timestamp, level: 'ERROR', code: error.code, message: error.message, context: error.context, stack: error.stack, originalError: error.originalError ? { message: error.originalError.message, stack: error.originalError.stack, } : undefined, }; await this.writeLog(JSON.stringify(logEntry, null, 2)); } async logGenericError(error, timestamp) { const logEntry = { timestamp, level: 'ERROR', message: error.message, stack: error.stack, }; await this.writeLog(JSON.stringify(logEntry, null, 2)); } async writeLog(content) { if (this.logFile) { try { await fs_extra_1.default.ensureFile(this.logFile); await fs_extra_1.default.appendFile(this.logFile, content + '\n\n'); } catch (logError) { // If we can't write to log file, at least log to console console.error('Failed to write error log:', logError); } } } displayUserFriendlyError(error) { console.error(chalk_1.default.red.bold('\nโŒ Error occurred during project generation\n')); console.error(chalk_1.default.yellow('What happened:')); console.error(` ${error.message}\n`); console.error(chalk_1.default.yellow('Error details:')); console.error(` Code: ${chalk_1.default.cyan(error.code)}`); console.error(` Phase: ${chalk_1.default.cyan(error.context.phase)}`); console.error(` Operation: ${chalk_1.default.cyan(error.context.operation)}\n`); if (error.context.projectName) { console.error(chalk_1.default.yellow('Project info:')); console.error(` Name: ${chalk_1.default.cyan(error.context.projectName)}`); if (error.context.projectPath) { console.error(` Path: ${chalk_1.default.cyan(error.context.projectPath)}`); } console.error(''); } this.displaySolutions(error.code); this.displaySupportInfo(); } displayGenericError(error) { console.error(chalk_1.default.red.bold('\nโŒ Unexpected error occurred\n')); console.error(chalk_1.default.yellow('Error message:')); console.error(` ${error.message}\n`); if (process.env.NODE_ENV === 'development') { console.error(chalk_1.default.yellow('Stack trace:')); console.error(` ${error.stack}\n`); } this.displaySupportInfo(); } displaySolutions(errorCode) { const solutions = { PROJECT_EXISTS: [ 'Use the overwrite option when prompted', 'Choose a different project name', 'Manually remove the existing directory', ], PERMISSION_DENIED: [ 'Run the command with appropriate permissions', 'Check if the target directory is writable', 'Try running in a different directory', ], NETWORK_ERROR: [ 'Check your internet connection', 'Try using a different package manager', 'Wait a moment and try again', ], TEMPLATE_ERROR: [ 'Report this issue on GitHub', 'Try with different configuration options', 'Check if you have enough disk space', ], GIT_ERROR: [ 'Ensure Git is installed and accessible', 'Check your Git configuration', 'Try without Git Flow option', ], }; const solutionList = solutions[errorCode]; if (solutionList) { console.error(chalk_1.default.yellow('Possible solutions:')); solutionList.forEach((solution, index) => { console.error(` ${index + 1}. ${solution}`); }); console.error(''); } } displaySupportInfo() { console.error(chalk_1.default.blue('Need help?')); console.error(' ๐Ÿ“– Documentation: https://github.com/GaoWenHan/Express-Hale#readme'); console.error(' ๐Ÿ› Report issues: https://github.com/GaoWenHan/Express-Hale/issues'); console.error(' ๐Ÿ’ฌ Discussions: https://github.com/GaoWenHan/Express-Hale/discussions'); if (this.logFile) { console.error(` ๐Ÿ“ Error log: ${chalk_1.default.cyan(this.logFile)}`); } } async cleanup(projectPath) { if (projectPath && (await fs_extra_1.default.pathExists(projectPath))) { try { const isEmpty = (await fs_extra_1.default.readdir(projectPath)).length === 0; if (isEmpty) { await fs_extra_1.default.remove(projectPath); console.log(chalk_1.default.yellow(`๐Ÿงน Cleaned up empty directory: ${projectPath}`)); } } catch (cleanupError) { console.warn(chalk_1.default.yellow(`โš ๏ธ Could not cleanup directory: ${projectPath}`)); } } } // Express.js Error Handling Middleware createErrorMiddleware() { return (error, req, res) => { const requestId = req.headers['x-request-id'] || this.generateRequestId(); // Log the error this.logHttpError(error, req, requestId); // Determine if this is an operational error const isOperational = error instanceof HttpError ? error.isOperational : false; // If it's not operational, this might be a programming error if (!isOperational) { console.error('Non-operational error:', error); } const response = ResponseFormatter.error(error, requestId); const statusCode = error instanceof HttpError ? error.statusCode : HttpStatusCode.INTERNAL_SERVER_ERROR; res.status(statusCode).json(response); }; } // 404 Not Found Middleware createNotFoundMiddleware() { return (req, res, next) => { const error = new NotFoundError(`Route ${req.originalUrl}`); next(error); }; } // Request ID Middleware createRequestIdMiddleware() { return (req, res, next) => { req.headers['x-request-id'] = req.headers['x-request-id'] || this.generateRequestId(); res.setHeader('x-request-id', req.headers['x-request-id']); next(); }; } // Async Error Wrapper asyncHandler(fn) { return (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; } // Retry mechanism with exponential backoff async retry(operation, errorCode, context) { const strategy = this.retryStrategies.get(errorCode); if (!strategy) { return operation(); } let lastError; let delay = strategy.initialDelay; for (let attempt = 1; attempt <= strategy.maxAttempts; attempt++) { try { return await operation(); } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); if (attempt === strategy.maxAttempts) { break; } console.warn(`Attempt ${attempt} failed, retrying in ${delay}ms...`, { error: lastError.message, context, }); await this.delay(delay); delay = Math.min(delay * strategy.backoffMultiplier, strategy.maxDelay); } } throw new Error(`Operation failed after ${strategy.maxAttempts} attempts: ${lastError.message}`); } async logHttpError(error, req, requestId) { const logEntry = { timestamp: new Date().toISOString(), level: 'ERROR', requestId, method: req.method, url: req.originalUrl, ip: req.ip, userAgent: req.get('User-Agent'), error: { name: error.name, message: error.message, stack: error.stack, ...(error instanceof HttpError && { statusCode: error.statusCode, code: error.code, category: error.category, context: error.context, }), }, }; await this.writeLog(JSON.stringify(logEntry, null, 2)); } generateRequestId() { return `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } // Circuit Breaker Pattern (Simple Implementation) createCircuitBreaker(operation, config) { let failureCount = 0; let lastFailureTime = 0; let state = 'CLOSED'; return (async (...args) => { const now = Date.now(); if (state === 'OPEN') { if (now - lastFailureTime < config.resetTimeout) { throw new ExternalServiceError('Circuit breaker', 'Service temporarily unavailable'); } state = 'HALF_OPEN'; failureCount = 0; } try { const result = await operation(...args); if (state === 'HALF_OPEN') { state = 'CLOSED'; } failureCount = 0; return result; } catch (error) { failureCount++; lastFailureTime = now; if (failureCount >= config.failureThreshold) { state = 'OPEN'; } throw error; } }); } } exports.ErrorHandler = ErrorHandler; // Global error handlers for unhandled errors function setupGlobalErrorHandlers() { const errorHandler = ErrorHandler.getInstance(); process.on('uncaughtException', async (error) => { console.error(chalk_1.default.red.bold('\n๐Ÿ’ฅ Uncaught Exception:')); await errorHandler.handleError(error, 1); }); process.on('unhandledRejection', async (reason) => { console.error(chalk_1.default.red.bold('\n๐Ÿ’ฅ Unhandled Promise Rejection:')); const error = reason instanceof Error ? reason : new Error(String(reason)); await errorHandler.handleError(error, 1); }); // Graceful shutdown on SIGINT (Ctrl+C) process.on('SIGINT', () => { console.log(chalk_1.default.yellow('\n\nโš ๏ธ Process interrupted by user. Exiting gracefully...')); process.exit(0); }); // Graceful shutdown on SIGTERM process.on('SIGTERM', () => { console.log(chalk_1.default.yellow('\n\nโš ๏ธ Process terminated. Exiting gracefully...')); process.exit(0); }); } //# sourceMappingURL=error-handler.js.map