@aetherframework/template-engine 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.
@@ -0,0 +1,279 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/template-engine/src/utils/ConfigLoader
6
+ */
7
+
8
+ /**
9
+ * Configuration Loader - Loads and manages configuration from environment variables and configuration files
10
+ * Supports loading from .env files, environment variables, and setting defaults
11
+ */
12
+ import fs from 'fs-extra';
13
+ import path from 'path';
14
+
15
+ class ConfigLoader {
16
+ /**
17
+ * Constructor for ConfigLoader
18
+ * @param {Object} options - Configuration options
19
+ * @param {string} options.configFile - Configuration file name (default: '.env')
20
+ * @param {string} options.configDir - Configuration directory path (default: current working directory)
21
+ */
22
+ constructor(options = {}) {
23
+ // Initialize configuration options with defaults
24
+ this.options = {
25
+ configFile: options.configFile || '.env',
26
+ configDir: options.configDir || process.cwd(),
27
+ ...options
28
+ };
29
+
30
+ // Configuration storage object
31
+ this.config = {};
32
+
33
+ // Flag to track if configuration has been loaded
34
+ this.loaded = false;
35
+ }
36
+
37
+ /**
38
+ * Load configuration from environment variables and configuration files
39
+ * @returns {Promise<Object>} Configuration object containing all loaded settings
40
+ */
41
+ async load() {
42
+ // Return cached configuration if already loaded
43
+ if (this.loaded) {
44
+ return this.config;
45
+ }
46
+
47
+ // Step 1: Load configuration from environment variables
48
+ this.loadFromEnv();
49
+
50
+ // Step 2: Load configuration from configuration file
51
+ await this.loadFromFile();
52
+
53
+ // Step 3: Set default values for any missing configuration options
54
+ this.setDefaults();
55
+
56
+ // Mark configuration as loaded
57
+ this.loaded = true;
58
+
59
+ return this.config;
60
+ }
61
+
62
+ /**
63
+ * Load configuration from environment variables
64
+ * Environment variables take precedence over file configuration
65
+ * @private
66
+ */
67
+ loadFromEnv() {
68
+ // Template Engine Configuration
69
+ this.config.mode = process.env.TEMPLATE_ENGINE_MODE || 'template';
70
+ this.config.defaultEngine = process.env.TEMPLATE_ENGINE || 'aether';
71
+ this.config.templateDir = process.env.TEMPLATE_DIR || './templates';
72
+
73
+ // Cache Configuration
74
+ this.config.cacheEnabled = process.env.CACHE_ENABLED !== 'false';
75
+ this.config.cacheTTL = parseInt(process.env.CACHE_TTL) || 300000; // 5 minutes default
76
+ this.config.cacheMaxSize = parseInt(process.env.CACHE_MAX_SIZE) || 1000;
77
+
78
+ // Debug Configuration
79
+ this.config.debug = process.env.NODE_ENV === 'development' || process.env.DEBUG === 'true';
80
+
81
+ // Server-Side Rendering (SSR) Configuration
82
+ this.config.ssrHydrate = process.env.SSR_HYDRATE !== 'false';
83
+ this.config.ssrStream = process.env.SSR_STREAM === 'true';
84
+
85
+ // Template Configuration
86
+ this.config.layoutSupport = process.env.LAYOUT_SUPPORT !== 'false';
87
+ this.config.includeSupport = process.env.INCLUDE_SUPPORT !== 'false';
88
+ this.config.cacheTemplates = process.env.CACHE_TEMPLATES !== 'false';
89
+ }
90
+
91
+ /**
92
+ * Load configuration from configuration file
93
+ * File configuration is used as fallback when environment variables are not set
94
+ * @private
95
+ */
96
+ async loadFromFile() {
97
+ // Construct full path to configuration file
98
+ const configPath = path.join(this.options.configDir, this.options.configFile);
99
+
100
+ try {
101
+ // Check if configuration file exists
102
+ if (await fs.pathExists(configPath)) {
103
+ // Read configuration file content
104
+ const content = await fs.readFile(configPath, 'utf-8');
105
+ const lines = content.split('\n');
106
+
107
+ // Parse each line in the configuration file
108
+ for (const line of lines) {
109
+ const trimmed = line.trim();
110
+
111
+ // Skip comments (lines starting with #) and empty lines
112
+ if (!trimmed || trimmed.startsWith('#')) {
113
+ continue;
114
+ }
115
+
116
+ // Parse key=value pairs
117
+ const equalsIndex = trimmed.indexOf('=');
118
+ if (equalsIndex > 0) {
119
+ const key = trimmed.substring(0, equalsIndex).trim();
120
+ const value = trimmed.substring(equalsIndex + 1).trim();
121
+
122
+ // Remove surrounding quotes if present
123
+ const cleanValue = value.replace(/['"]|['"]$/g, '');
124
+
125
+ // Only set configuration from file if environment variable is not already set
126
+ if (process.env[key] === undefined) {
127
+ this.config[key] = this.parseValue(cleanValue);
128
+ }
129
+ }
130
+ }
131
+
132
+ console.log(`📁 Configuration loaded from: ${configPath}`);
133
+ }
134
+ } catch (error) {
135
+ // Log warning but don't fail if configuration file cannot be loaded
136
+ console.warn(`⚠️ Could not load config file: ${error.message}`);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Parse configuration value from string to appropriate type
142
+ * @param {string} value - Raw string value from configuration
143
+ * @returns {any} Parsed value (boolean, number, or string)
144
+ * @private
145
+ */
146
+ parseValue(value) {
147
+ // Parse boolean values
148
+ if (value.toLowerCase() === 'true') return true;
149
+ if (value.toLowerCase() === 'false') return false;
150
+
151
+ // Parse numeric values
152
+ if (!isNaN(value) && value.trim() !== '') {
153
+ const num = Number(value);
154
+ if (!isNaN(num)) return num;
155
+ }
156
+
157
+ // Return as string if not boolean or number
158
+ return value;
159
+ }
160
+
161
+ /**
162
+ * Set default configuration values for any missing options
163
+ * This ensures all required configuration options have values
164
+ * @private
165
+ */
166
+ setDefaults() {
167
+ // Default configuration values
168
+ const defaults = {
169
+ mode: 'template',
170
+ defaultEngine: 'aether',
171
+ templateDir: './templates',
172
+ cacheEnabled: true,
173
+ cacheTTL: 300000, // 5 minutes in milliseconds
174
+ cacheMaxSize: 1000,
175
+ debug: false,
176
+ ssrHydrate: true,
177
+ ssrStream: false,
178
+ layoutSupport: true,
179
+ includeSupport: true,
180
+ cacheTemplates: true
181
+ };
182
+
183
+ // Apply defaults only for configuration options that are not already set
184
+ for (const [key, defaultValue] of Object.entries(defaults)) {
185
+ if (this.config[key] === undefined) {
186
+ this.config[key] = defaultValue;
187
+ }
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Get a specific configuration value
193
+ * @param {string} key - Configuration key to retrieve
194
+ * @param {any} defaultValue - Default value to return if key is not found
195
+ * @returns {any} Configuration value or default value
196
+ */
197
+ get(key, defaultValue = null) {
198
+ return this.config[key] !== undefined ? this.config[key] : defaultValue;
199
+ }
200
+
201
+ /**
202
+ * Set a configuration value
203
+ * @param {string} key - Configuration key to set
204
+ * @param {any} value - Value to set for the configuration key
205
+ * @returns {ConfigLoader} This instance for method chaining
206
+ */
207
+ set(key, value) {
208
+ this.config[key] = value;
209
+ return this;
210
+ }
211
+
212
+ /**
213
+ * Get all configuration as an object
214
+ * @returns {Object} Complete configuration object
215
+ */
216
+ getAll() {
217
+ // Return a copy to prevent external modification
218
+ return { ...this.config };
219
+ }
220
+
221
+ /**
222
+ * Reload configuration from sources
223
+ * Useful when configuration files have been updated
224
+ * @returns {Promise<Object>} Updated configuration object
225
+ */
226
+ async reload() {
227
+ // Reset loaded flag and clear existing configuration
228
+ this.loaded = false;
229
+ this.config = {};
230
+
231
+ // Load configuration again
232
+ return this.load();
233
+ }
234
+
235
+ /**
236
+ * Validate the current configuration
237
+ * Checks for required settings and valid values
238
+ * @returns {Object} Validation result with errors and warnings
239
+ */
240
+ validate() {
241
+ const errors = [];
242
+ const warnings = [];
243
+
244
+ // Validate rendering mode
245
+ if (!['ssr', 'template', 'disabled'].includes(this.config.mode)) {
246
+ errors.push(`Invalid mode: ${this.config.mode}. Must be 'ssr', 'template', or 'disabled'`);
247
+ }
248
+
249
+ // Validate template directory configuration
250
+ if (!this.config.templateDir) {
251
+ errors.push('Template directory not specified');
252
+ }
253
+
254
+ // Validate cache TTL (must be positive)
255
+ if (this.config.cacheTTL < 0) {
256
+ errors.push(`Invalid cache TTL: ${this.config.cacheTTL}. Must be positive number`);
257
+ }
258
+
259
+ // Validate cache maximum size (must be at least 1)
260
+ if (this.config.cacheMaxSize < 1) {
261
+ errors.push(`Invalid cache max size: ${this.config.cacheMaxSize}. Must be at least 1`);
262
+ }
263
+
264
+ // Check if template directory exists (warning only, not an error)
265
+ if (this.config.templateDir && !fs.existsSync(this.config.templateDir)) {
266
+ warnings.push(`Template directory does not exist: ${this.config.templateDir}`);
267
+ }
268
+
269
+ // Return validation results
270
+ return {
271
+ valid: errors.length === 0,
272
+ errors,
273
+ warnings,
274
+ config: this.config
275
+ };
276
+ }
277
+ }
278
+
279
+ export default ConfigLoader;
@@ -0,0 +1,276 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/template-engine/src/utils/ErrorHandler
6
+ */
7
+
8
+ /**
9
+ * Error Handler - Handles and formats template engine errors
10
+ * This class provides comprehensive error handling for template engine operations,
11
+ * including error formatting, logging, and user-friendly error messages.
12
+ */
13
+ class ErrorHandler {
14
+ /**
15
+ * Constructor for ErrorHandler class
16
+ * @param {Object} options - Configuration options for error handling
17
+ * @param {boolean} options.debug - Enable debug mode for detailed error information
18
+ * @param {boolean} options.logErrors - Enable error logging to console
19
+ * @param {boolean} options.formatErrors - Enable error formatting with context
20
+ */
21
+ constructor(options = {}) {
22
+ this.options = {
23
+ debug: options.debug || false,
24
+ logErrors: options.logErrors !== false,
25
+ formatErrors: options.formatErrors !== false,
26
+ ...options
27
+ };
28
+ }
29
+
30
+ /**
31
+ * Static method to handle template engine errors
32
+ * @param {Error} error - Original error object
33
+ * @param {Object} context - Error context information
34
+ * @returns {Error} Formatted error object
35
+ */
36
+ static handle(error, context = {}) {
37
+ const handler = new ErrorHandler();
38
+ return handler.formatError(error, context);
39
+ }
40
+
41
+ /**
42
+ * Format error with additional context information
43
+ * @param {Error} error - Original error object
44
+ * @param {Object} context - Error context information
45
+ * @returns {Error} Formatted error object
46
+ */
47
+ formatError(error, context = {}) {
48
+ const errorInfo = {
49
+ message: error.message,
50
+ stack: error.stack,
51
+ timestamp: new Date().toISOString(),
52
+ context: context
53
+ };
54
+
55
+ // Log error to console if logging is enabled
56
+ if (this.options.logErrors) {
57
+ console.error('Template Engine Error:', errorInfo);
58
+ }
59
+
60
+ // Format error if formatting is enabled
61
+ if (this.options.formatErrors) {
62
+ return this.createFormattedError(error, context);
63
+ }
64
+
65
+ return error;
66
+ }
67
+
68
+ /**
69
+ * Create formatted error with helpful message and context
70
+ * @param {Error} error - Original error object
71
+ * @param {Object} context - Error context information
72
+ * @returns {Error} Formatted error object
73
+ * @private
74
+ */
75
+ createFormattedError(error, context) {
76
+ let message = error.message;
77
+
78
+ // Add template context information to error message
79
+ if (context.template) {
80
+ const templatePreview = typeof context.template === 'string'
81
+ ? context.template.substring(0, 100) + (context.template.length > 100 ? '...' : '')
82
+ : 'Function';
83
+ message += `\nTemplate: ${templatePreview}`;
84
+ }
85
+
86
+ // Add engine context information
87
+ if (context.engine) {
88
+ message += `\nEngine: ${context.engine}`;
89
+ }
90
+
91
+ // Add mode context information
92
+ if (context.mode) {
93
+ message += `\nMode: ${context.mode}`;
94
+ }
95
+
96
+ // Add data keys context information
97
+ if (context.data && Object.keys(context.data).length > 0) {
98
+ message += `\nData Keys: ${Object.keys(context.data).join(', ')}`;
99
+ }
100
+
101
+ // Create new error with formatted message
102
+ const formattedError = new Error(message);
103
+ formattedError.originalError = error;
104
+ formattedError.context = context;
105
+ formattedError.stack = error.stack;
106
+
107
+ return formattedError;
108
+ }
109
+
110
+ /**
111
+ * Handle template compilation errors
112
+ * @param {Error} error - Compilation error object
113
+ * @param {string} template - Template content
114
+ * @param {string} templateName - Name of the template
115
+ * @returns {Error} Formatted compilation error
116
+ */
117
+ handleCompilationError(error, template, templateName = 'anonymous') {
118
+ const context = {
119
+ type: 'compilation',
120
+ templateName,
121
+ templateLength: template.length,
122
+ errorLocation: this.findErrorLocation(error, template)
123
+ };
124
+
125
+ return this.formatError(error, context);
126
+ }
127
+
128
+ /**
129
+ * Handle template rendering errors
130
+ * @param {Error} error - Rendering error object
131
+ * @param {string} templateName - Name of the template
132
+ * @param {Object} data - Template data object
133
+ * @returns {Error} Formatted rendering error
134
+ */
135
+ handleRenderingError(error, templateName, data = {}) {
136
+ const context = {
137
+ type: 'rendering',
138
+ templateName,
139
+ dataKeys: Object.keys(data),
140
+ dataSize: JSON.stringify(data).length
141
+ };
142
+
143
+ return this.formatError(error, context);
144
+ }
145
+
146
+ /**
147
+ * Handle file system errors
148
+ * @param {Error} error - File system error object
149
+ * @param {string} filePath - Path to the file
150
+ * @param {string} operation - File operation being performed
151
+ * @returns {Error} Formatted file system error
152
+ */
153
+ handleFileSystemError(error, filePath, operation = 'read') {
154
+ const context = {
155
+ type: 'filesystem',
156
+ filePath,
157
+ operation,
158
+ errorCode: error.code
159
+ };
160
+
161
+ return this.formatError(error, context);
162
+ }
163
+
164
+ /**
165
+ * Find error location in template content
166
+ * @param {Error} error - Error object
167
+ * @param {string} template - Template content
168
+ * @returns {Object|null} Error location information or null if not found
169
+ * @private
170
+ */
171
+ findErrorLocation(error, template) {
172
+ const stack = error.stack || '';
173
+ const lines = template.split('\n');
174
+
175
+ // Try to find line number from error message
176
+ const lineMatch = error.message.match(/line (\d+)/i) || stack.match(/line (\d+)/i);
177
+ if (lineMatch) {
178
+ const lineNumber = parseInt(lineMatch[1]) - 1;
179
+ if (lineNumber >= 0 && lineNumber < lines.length) {
180
+ return {
181
+ line: lineNumber + 1,
182
+ column: 0,
183
+ snippet: lines[lineNumber].substring(0, 100)
184
+ };
185
+ }
186
+ }
187
+
188
+ // Try to find column from error message
189
+ const columnMatch = error.message.match(/column (\d+)/i) || stack.match(/column (\d+)/i);
190
+ if (columnMatch) {
191
+ const column = parseInt(columnMatch[1]);
192
+ return {
193
+ line: 1,
194
+ column,
195
+ snippet: template.substring(column - 10, column + 10)
196
+ };
197
+ }
198
+
199
+ return null;
200
+ }
201
+
202
+ /**
203
+ * Create user-friendly error message for end users
204
+ * @param {Error} error - Error object
205
+ * @returns {string} User-friendly error message
206
+ */
207
+ getUserFriendlyMessage(error) {
208
+ const errorType = error.originalError ? error.originalError.name : error.name;
209
+
210
+ switch (errorType) {
211
+ case 'SyntaxError':
212
+ return 'Template syntax error, please check if the template syntax is correct.';
213
+ case 'ReferenceError':
214
+ return 'Template variable reference error, please check if the variable name is correct.';
215
+ case 'TypeError':
216
+ return 'Template type error, please check if the data types match.';
217
+ case 'RangeError':
218
+ return 'Template range error, please check loops or conditional statements.';
219
+ case 'EvalError':
220
+ return 'Template execution error, please check if the expressions are correct.';
221
+ case 'URIError':
222
+ return 'Template URL error, please check URL-related functions.';
223
+ default:
224
+ return 'An error occurred during template processing, please check the template and data.';
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Get error recovery suggestions based on error type
230
+ * @param {Error} error - Error object
231
+ * @returns {Array<string>} Array of recovery suggestions
232
+ */
233
+ getRecoverySuggestions(error) {
234
+ const suggestions = [];
235
+ const message = error.message.toLowerCase();
236
+
237
+ if (message.includes('not found') || message.includes('cannot find')) {
238
+ suggestions.push('Check if the template file path is correct');
239
+ suggestions.push('Confirm that the template file exists');
240
+ suggestions.push('Check template file permissions');
241
+ }
242
+
243
+ if (message.includes('syntax') || message.includes('syntax error')) {
244
+ suggestions.push('Check if the template syntax is correct');
245
+ suggestions.push('Confirm all directives are properly closed');
246
+ suggestions.push('Check variable reference format');
247
+ }
248
+
249
+ if (message.includes('variable') || message.includes('undefined')) {
250
+ suggestions.push('Check if the variable name is correct');
251
+ suggestions.push('Confirm the variable is defined in the data');
252
+ suggestions.push('Check variable scope');
253
+ }
254
+
255
+ if (message.includes('cache') || message.includes('caching')) {
256
+ suggestions.push('Try clearing the cache');
257
+ suggestions.push('Check cache configuration');
258
+ suggestions.push('Restart the template engine');
259
+ }
260
+
261
+ if (message.includes('permission') || message.includes('access denied')) {
262
+ suggestions.push('Check file read/write permissions');
263
+ suggestions.push('Confirm the running user has sufficient permissions');
264
+ suggestions.push('Check directory permissions');
265
+ }
266
+
267
+ // Always add general suggestions
268
+ suggestions.push('View detailed error logs');
269
+ suggestions.push('Check template engine configuration');
270
+ suggestions.push('Simplify the template and debug step by step');
271
+
272
+ return suggestions;
273
+ }
274
+ }
275
+
276
+ export default ErrorHandler;