UNPKG

@fwdslsh/unify

Version:

A lightweight, framework-free static site generator with Bun native APIs

320 lines (276 loc) 10.1 kB
/** * Custom error classes for unify * Provides specific error types with actionable guidance for different failure scenarios */ /** * Base error class for unify errors */ export class UnifyError extends Error { constructor(message, filePath = null, lineNumber = null, suggestions = []) { super(message); this.name = this.constructor.name; this.filePath = filePath; this.lineNumber = lineNumber; this.suggestions = Array.isArray(suggestions) ? suggestions : []; // Include file context in message if available if (filePath) { const location = lineNumber ? `${filePath}:${lineNumber}` : filePath; this.message = `${message} in ${location}`; } // Don't add suggestions to message here - let formatForCLI() handle it // This prevents duplication when both constructor and formatForCLI() add suggestions } /** * Determine if this error should be handled gracefully (as a warning) * Override in subclasses to customize behavior * @returns {boolean} True if error should be treated as a warning */ isRecoverable() { return false; } /** * Generate a warning comment to replace the failed include * @returns {string} HTML comment with error details */ toWarningComment() { const errorMsg = this.message.split(' in ')[0]; return `<!-- WARNING: ${errorMsg} -->`; } /** * Format error for CLI display with colors and structure */ formatForCLI() { const location = this.lineNumber ? `${this.filePath}:${this.lineNumber}` : this.filePath; let output = `ERROR ${this.name}: ${this.message.split(' in ')[0]}`; if (this.filePath) { output += `\n File: ${location}`; } if (this.suggestions.length > 0) { output += '\n\nSuggestions:'; output += '\n' + this.suggestions.map(s => ` - ${s}`).join('\n'); } return output; } } /** * Error thrown when an include file is not found */ export class IncludeNotFoundError extends UnifyError { constructor(includePath, parentFile, searchPaths = [], componentsDir = '.components') { const suggestions = [ `Create the missing file: ${includePath}`, `Verify the file exists: ${includePath}`, searchPaths.length > 0 ? `Searched in: ${searchPaths.join(', ')}` : `Place include files in the ${componentsDir}/ directory`, `Check for typos in the include path`, `Ensure the path is relative to ${parentFile} (for file="...") or source root (for virtual="...")` ]; super(`Include not found: ${includePath}`, parentFile, null, suggestions); this.includePath = includePath; this.parentFile = parentFile; this.searchPaths = searchPaths; } /** * Include not found errors are recoverable - continue processing with warning */ isRecoverable() { return true; } } /** * Error thrown when a circular dependency is detected in includes */ export class CircularDependencyError extends UnifyError { constructor(filePath, dependencyChain) { const chain = dependencyChain.join(' → '); const suggestions = [ 'Remove one of the include statements to break the cycle', 'Consider restructuring your components to avoid circular references', 'Use conditional includes if the circular dependency is intentional' ]; super(`Circular dependency detected: ${chain}${filePath}`, filePath, null, suggestions); this.dependencyChain = dependencyChain; } } /** * Error thrown when a path escapes the source directory (security) */ export class PathTraversalError extends UnifyError { constructor(attemptedPath, sourceRoot) { const suggestions = [ 'Use relative paths within your source directory', 'Avoid using "../" to escape the source directory', 'Place all includes within the source tree for security', `Ensure all paths are within: ${sourceRoot}` ]; super(`Path traversal attempt blocked: ${attemptedPath}`, null, null, suggestions); this.attemptedPath = attemptedPath; this.sourceRoot = sourceRoot; } /** * Path traversal attempts are recoverable - log security warning but continue build * The alternative would be to fail builds due to malicious or accidental path traversal attempts */ isRecoverable() { return true; } } /** * Error thrown when include directive syntax is malformed */ export class MalformedDirectiveError extends UnifyError { constructor(directive, filePath, lineNumber) { const suggestions = [ 'Use correct syntax: <!--#include file="path.html" --> or <!--#include virtual="/path.html" -->', 'Ensure quotes around the file path', 'Check for typos in "file" or "virtual" keywords', 'Verify the directive is properly closed with -->' ]; super(`Malformed include directive: ${directive}`, filePath, lineNumber, suggestions); this.directive = directive; } } /** * Error thrown when maximum include depth is exceeded */ export class MaxDepthExceededError extends UnifyError { constructor(filePath, depth, maxDepth) { const suggestions = [ `Reduce the depth of nested includes to ${maxDepth} or fewer levels`, 'Check for circular dependencies in your include structure', 'Consider flattening your component hierarchy' ]; super(`Maximum include depth (${maxDepth}) exceeded at depth ${depth}`, filePath, null, suggestions); this.depth = depth; this.maxDepth = maxDepth; } /** * Max depth errors are recoverable - stop processing this branch but continue with others */ isRecoverable() { return true; } } /** * Error thrown when file system operations fail */ export class FileSystemError extends UnifyError { constructor(operation, filePath, originalError) { const suggestions = []; if (operation === 'read') { suggestions.push( 'Check if the file exists and is readable', 'Verify file permissions', 'Ensure the path is correct' ); } else if (operation === 'write') { suggestions.push( 'Check if the output directory exists', 'Verify write permissions to the output directory', 'Ensure there is enough disk space' ); } else if (operation === 'mkdir') { suggestions.push( 'Verify parent directory exists', 'Check directory creation permissions' ); } super(`File system error during ${operation}: ${originalError.message}`, filePath, null, suggestions); this.operation = operation; this.originalError = originalError; } } /** * Error thrown when CLI arguments are invalid */ export class InvalidArgumentError extends UnifyError { constructor(argument, value, reason) { const suggestions = [ `Check the ${argument} value: ${value}`, 'Use --help to see valid options', 'Verify paths exist and are accessible' ]; super(`Invalid argument ${argument}: ${value} (${reason})`, null, null, suggestions); this.argument = argument; this.value = value; this.reason = reason; } } /** * Error thrown when build process fails */ export class BuildError extends UnifyError { constructor(message, errors = []) { const suggestions = []; if (errors.length > 0) { suggestions.push(`Fix the ${errors.length} error(s) listed above`); // Analyze common error patterns const includeErrors = errors.filter(e => e.error?.includes('Include file not found')); const circularErrors = errors.filter(e => e.error?.includes('Circular dependency')); if (includeErrors.length > 0) { suggestions.push('Check that all include files exist in the correct locations'); } if (circularErrors.length > 0) { suggestions.push('Review your include structure to remove circular dependencies'); } } suggestions.push('Run with DEBUG=* for more detailed error information'); super(`Build failed: ${message}`, null, null, suggestions); this.errors = errors; } } /** * Error thrown when development server fails to start */ export class ServerError extends UnifyError { constructor(message, port = null) { const suggestions = []; if (port) { suggestions.push( `Try a different port: --port ${port + 1}`, 'Check if another process is using this port', 'Use --port 0 to automatically find an available port' ); } suggestions.push('Verify the output directory exists and contains files to serve'); super(`Server error: ${message}`, null, null, suggestions); this.port = port; } } /** * Error thrown when layout files are not found or invalid */ export class LayoutError extends UnifyError { constructor(layoutPath, reason, alternatives = []) { const suggestions = [ `Create the layout file: ${layoutPath}`, 'Verify the layout directory path in your configuration' ]; if (alternatives.length > 0) { suggestions.push(`Alternative layout locations: ${alternatives.join(', ')}`); } suggestions.push('Use {{ content }} placeholder in your layout for page content'); super(`Layout not found: ${layoutPath} (${reason})`, layoutPath, null, suggestions); this.layoutPath = layoutPath; this.reason = reason; this.alternatives = alternatives; } } /** * Error thrown when component files have issues */ export class ComponentError extends UnifyError { constructor(componentPath, reason, parentFile = null) { const suggestions = [ `Check the component file: ${componentPath}`, 'Verify the component directory path in your configuration', 'Ensure component files are properly formatted HTML' ]; if (parentFile) { suggestions.push(`Referenced from: ${parentFile}`); } super(`Component error: ${reason}`, componentPath, null, suggestions); this.componentPath = componentPath; this.reason = reason; this.parentFile = parentFile; } }