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
JavaScript
;
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