mikroconf
Version:
A flexible, zero-dependency, type-safe configuration manager that just makes sense.
234 lines (230 loc) • 8.21 kB
JavaScript
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
});
;