UNPKG

mikroconf

Version:

A flexible, zero-dependency, type-safe configuration manager that just makes sense.

234 lines (230 loc) 8.21 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/MikroConf.ts var MikroConf_exports = {}; __export(MikroConf_exports, { MikroConf: () => MikroConf }); module.exports = __toCommonJS(MikroConf_exports); var import_node_fs = require("fs"); // src/errors/index.ts var ValidationError = class extends Error { constructor(message) { super(message); this.name = "ValidationError"; this.message = message || "Validation did not pass"; this.cause = { statusCode: 400 }; } }; // src/MikroConf.ts var MikroConf = class { config = {}; options = []; validators = []; autoValidate = true; /** * @description Creates a new MikroConf instance. */ constructor(options) { const configFilePath = options?.configFilePath; const args = options?.args || []; const configuration = options?.config || {}; this.options = options?.options || []; this.validators = options?.validators || []; if (options?.autoValidate !== void 0) this.autoValidate = options.autoValidate; this.config = this.createConfig(configFilePath, args, configuration); } /** * @description Deep merges two objects. */ deepMerge(target, source) { const result = { ...target }; for (const key in source) { if (source[key] === void 0) continue; if (source[key] !== null && typeof source[key] === "object" && !Array.isArray(source[key]) && key in target && target[key] !== null && typeof target[key] === "object" && !Array.isArray(target[key])) { result[key] = this.deepMerge(target[key], source[key]); } else if (source[key] !== void 0) result[key] = source[key]; } return result; } /** * @description Sets a value at a nested path in an object. */ setValueAtPath(obj, path, value) { const parts = path.split("."); let current = obj; for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]; if (!(part in current) || current[part] === null) current[part] = {}; else if (typeof current[part] !== "object") current[part] = {}; current = current[part]; } const lastPart = parts[parts.length - 1]; current[lastPart] = value; } /** * @description Gets a value from a nested path in an object. */ getValueAtPath(obj, path) { const parts = path.split("."); let current = obj; for (const part of parts) { if (current === void 0 || current === null) return void 0; current = current[part]; } return current; } /** * @description Creates a configuration object by merging defaults, config file settings, * explicit input, and CLI arguments. */ createConfig(configFilePath, args = [], configuration = {}) { const defaults = {}; for (const option of this.options) { if (option.defaultValue !== void 0) this.setValueAtPath(defaults, option.path, option.defaultValue); } let fileConfig = {}; if (configFilePath && (0, import_node_fs.existsSync)(configFilePath)) { try { const fileContent = (0, import_node_fs.readFileSync)(configFilePath, "utf8"); fileConfig = JSON.parse(fileContent); console.log(`Loaded configuration from ${configFilePath}`); } catch (error) { console.error( `Error reading config file: ${error instanceof Error ? error.message : String(error)}` ); } } const cliConfig = this.parseCliArgs(args); let mergedConfig = this.deepMerge({}, defaults); mergedConfig = this.deepMerge(mergedConfig, fileConfig); mergedConfig = this.deepMerge(mergedConfig, configuration); mergedConfig = this.deepMerge(mergedConfig, cliConfig); return mergedConfig; } /** * @description Parses command line arguments into a configuration object based on defined options. */ parseCliArgs(args) { const cliConfig = {}; let i = args[0]?.endsWith("node") || args[0]?.endsWith("node.exe") ? 2 : 0; while (i < args.length) { const arg = args[i++]; const option = this.options.find((opt) => opt.flag === arg); if (option) { if (option.isFlag) { this.setValueAtPath(cliConfig, option.path, true); } else if (i < args.length && !args[i].startsWith("-")) { let value = args[i++]; if (option.parser) { try { value = option.parser(value); } catch (error) { console.error( `Error parsing value for ${option.flag}: ${error instanceof Error ? error.message : String(error)}` ); continue; } } if (option.validator) { const validationResult = option.validator(value); if (validationResult !== true && typeof validationResult === "string") { console.error(`Invalid value for ${option.flag}: ${validationResult}`); continue; } if (validationResult === false) { console.error(`Invalid value for ${option.flag}`); continue; } } this.setValueAtPath(cliConfig, option.path, value); } else { console.error(`Missing value for option ${arg}`); } } } return cliConfig; } /** * @description Validates the configuration against defined validators. */ validate() { for (const validator of this.validators) { const value = this.getValueAtPath(this.config, validator.path); const result = validator.validator(value, this.config); if (result === false) throw new ValidationError(validator.message); if (typeof result === "string") throw new ValidationError(result); } } /** * @description Returns the complete configuration. * @returns The configuration object. */ get() { if (this.autoValidate) this.validate(); return this.config; } /** * @description Gets a specific configuration value by path. * @param path The dot-notation path to the configuration value. * @param defaultValue Optional default value if the path doesn't exist. */ getValue(path, defaultValue) { const value = this.getValueAtPath(this.config, path); return value !== void 0 ? value : defaultValue; } /** * @description Sets a specific configuration value by path. * @param path The dot-notation path to set. * @param value The value to set. */ setValue(path, value) { if (typeof value === "object" && value !== null && !Array.isArray(value)) { const currentValue = this.getValueAtPath(this.config, path) || {}; if (typeof currentValue === "object" && !Array.isArray(currentValue)) { const mergedValue = this.deepMerge(currentValue, value); this.setValueAtPath(this.config, path, mergedValue); return; } } this.setValueAtPath(this.config, path, value); } /** * @description Generates help text based on the defined options. */ getHelpText() { let help = "Available configuration options:\n\n"; for (const option of this.options) { help += `${option.flag}${option.isFlag ? "" : " <value>"} `; if (option.description) help += ` ${option.description} `; if (option.defaultValue !== void 0) help += ` Default: ${JSON.stringify(option.defaultValue)} `; help += "\n"; } return help; } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { MikroConf });