UNPKG

quickload

Version:

A simple utility to load json config files.

250 lines (249 loc) 8.23 kB
/** * @fileoverview Configuration Loader * A robust and type-safe configuration loader for Node.js applications with * support for environment-specific configs, validation, and caching. * Compatible with both Backend and Frontend environments. * * Features: * - Environment-specific configuration management * - JSON Schema validation using Ajv * - Configuration caching for performance optimization * - Deep merging of configuration objects * - Async/Promise-based operations * - TypeScript support with strict typing * - Comprehensive error handling * * @module config-loader * @author Nasr Aldin <ns@nasraldin.com> * @copyright 2024 Nasr Aldin * @license MIT * @version 1.0.0 * * @see {@link https://github.com/nasraldin/quickload|GitHub Repository} * @see {@link https://www.npmjs.com/package/quickload|NPM Package} */ import Ajv from 'ajv'; import ajvFormats from 'ajv-formats'; import { ENV, isSSR } from './env.checker'; let fsPromises = null; let pathModule = null; // Initialize Node.js modules only on server side const loadServerModules = async () => { if (typeof process === 'undefined') return false; try { if (!fsPromises) { fsPromises = await import('fs/promises').catch(() => null); } if (!pathModule) { pathModule = await import('path').catch(() => null); } return true; } catch (error) { // eslint-disable-next-line no-console console.warn('Server-side modules not available:', error); return false; } }; /** * Configuration error codes */ export var ConfigErrorCode; (function (ConfigErrorCode) { ConfigErrorCode["INVALID_SCHEMA"] = "INVALID_SCHEMA"; ConfigErrorCode["FILE_NOT_FOUND"] = "FILE_NOT_FOUND"; ConfigErrorCode["PARSE_ERROR"] = "PARSE_ERROR"; ConfigErrorCode["VALIDATION_ERROR"] = "VALIDATION_ERROR"; ConfigErrorCode["MERGE_ERROR"] = "MERGE_ERROR"; ConfigErrorCode["LOAD_ERROR"] = "LOAD_ERROR"; ConfigErrorCode["FILE_READ_ERROR"] = "FILE_READ_ERROR"; ConfigErrorCode["DIR_READ_ERROR"] = "DIR_READ_ERROR"; })(ConfigErrorCode || (ConfigErrorCode = {})); /** * Custom error class for configuration-related errors * @class ConfigurationError * @extends Error */ export class ConfigurationError extends Error { code; errors; /** * @param message - Error message * @param errors - Additional error details */ constructor(message, code, errors) { super(message); this.code = code; this.errors = errors; this.name = 'ConfigurationError'; } } /** Cache storage for configurations */ const configCache = new Map(); const defaultConfigDir = 'config'; /** * Type-safe configuration loader */ export class ConfigLoader { options; ajv; constructor(options) { this.options = { configDir: options.configDir ?? defaultConfigDir, schema: options.schema, cache: options.cache ?? false, defaultEnv: options.defaultEnv ?? ENV.Development, includeBaseConfig: options.includeBaseConfig ?? false, }; this.ajv = new Ajv({ allErrors: true, strict: true, strictTypes: true, strictTuples: true, }); ajvFormats(this.ajv); } /** * Loads and validates configuration */ async load() { const env = process.env.NODE_ENV ?? this.options.defaultEnv; if (!isSSR()) { // Client-side implementation return this.validateConfig({}); } const isServerModulesLoaded = await loadServerModules(); if (!isServerModulesLoaded) { // eslint-disable-next-line no-console console.warn('Server-side modules not available, returning empty config'); return this.validateConfig({}); } try { const config = await this.loadAndMergeConfigs(env); return this.validateConfig(config); } catch (error) { if (error instanceof ConfigurationError) { throw error; } throw new ConfigurationError('Failed to load configuration', ConfigErrorCode.LOAD_ERROR, error); } } /** * Reads and parses a JSON configuration file */ async loadFile(filePath) { if (!isSSR() || !fsPromises) { return {}; } try { const content = await fsPromises.readFile(filePath, 'utf-8'); return JSON.parse(content); } catch (err) { if (err instanceof Error) { throw new ConfigurationError(`Failed to load file: ${filePath}`, ConfigErrorCode.FILE_READ_ERROR, err); } throw err; } } /** * Loads configuration files from directory */ async loadConfigFiles(dirPath) { if (!isSSR() || !fsPromises || !pathModule) { return {}; } try { const files = await fsPromises.readdir(dirPath); const jsonFiles = files.filter((file) => file.endsWith('.json')); const configs = await Promise.all(jsonFiles.map(async (file) => { const filePath = pathModule?.join(dirPath, file); if (!filePath) { return {}; } return this.loadFile(filePath); })); return configs.reduce((acc, config) => this.mergeConfigs(acc, config), {}); } catch (err) { if (err instanceof Error && err.code === 'ENOENT') { return {}; } throw new ConfigurationError(`Failed to load config files from directory: ${dirPath}`, ConfigErrorCode.DIR_READ_ERROR, err); } } /** * Deep merges two configuration objects * @param target - Target configuration object * @param source - Source configuration object to merge * @returns Merged configuration object */ mergeConfigs(target, source) { return Object.entries(source).reduce((merged, [key, sourceValue]) => { const targetValue = merged[key]; // If both target and source values are objects, recursively merge them if (this.isConfigObject(sourceValue) && this.isConfigObject(targetValue)) { return { ...merged, [key]: this.mergeConfigs(targetValue, sourceValue), }; } // If source value is not undefined, update or add it to the merged config if (sourceValue !== undefined) { return { ...merged, [key]: sourceValue, }; } return merged; }, { ...target }); } /** * Type guard for config objects */ isConfigObject(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); } /** * Loads and merges configuration files */ async loadAndMergeConfigs(env) { if (!pathModule) { return {}; } const defaultPath = pathModule.join(this.options.configDir, 'default'); const envPath = pathModule.join(this.options.configDir, env); const [defaultConfig, envConfig] = await Promise.all([ this.loadConfigFiles(defaultPath), this.loadConfigFiles(envPath), ]); return this.mergeConfigs(defaultConfig, envConfig); } /** * Validates configuration against schema */ validateConfig(config) { const validate = this.ajv.compile(this.options.schema); if (!validate(config)) { throw new ConfigurationError('Configuration validation failed', ConfigErrorCode.VALIDATION_ERROR, validate.errors); } return config; } } /** * Helper function to create a config loader with default options */ export function loadConfig(schema, options = {}) { return new ConfigLoader({ schema, ...options, }); } /** * Clears the configuration cache */ export function clearConfigCache() { configCache.clear(); }