UNPKG

env-typed-config

Version:

Intuitive, type-safe configuration library for Node.js

236 lines (232 loc) 7.08 kB
// src/index.ts import merge from "lodash.merge"; import chalk from "chalk"; import { plainToClass } from "class-transformer"; import { validateSync } from "class-validator"; import "reflect-metadata"; // src/loader/fileLoader.ts import { basename, dirname } from "path"; import { cosmiconfigSync } from "cosmiconfig"; import parseToml from "@iarna/toml"; var loadToml = function loadToml2(filepath, content) { try { const result = parseToml.parse(content); return result; } catch (error) { error.message = `TOML Error in ${filepath}: ${error.message}`; throw error; } }; var getSearchOptions = (options) => { if (options.absolutePath) { return { searchPlaces: [basename(options.absolutePath)], searchFrom: dirname(options.absolutePath) }; } const { basename: name = ".env", loaders = {} } = options; const additionalFormats = Object.keys(loaders).map((ext) => ext.slice(1)); const formats = [...additionalFormats, "toml", "yaml", "yml", "json", "js"]; return { searchPlaces: [ ...formats.map((format) => `${name}.${process.env.NODE_ENV}.${format}`), ...formats.map((format) => `${name}.${format}`) ], searchFrom: options.searchFrom }; }; var placeholderResolver = (template, data, disallowUndefinedEnvironmentVariables) => { const replace = (placeholder, key) => { let value = data; for (const property of key.split(".")) value = value[property]; if (!value && disallowUndefinedEnvironmentVariables) { throw new Error( `Environment variable is not set for variable name: '${key}'` ); } return String(value); }; const braceRegex = /\${(\d+|[a-z$_][\w\-$]*?(?:\.[\w\-$]*?)*?)}/gi; return template.replace(braceRegex, replace); }; var fileLoader = (options = {}) => { return () => { const { searchPlaces, searchFrom } = getSearchOptions(options); const loaders = { ".toml": loadToml, ...options.loaders }; const explorer = cosmiconfigSync("env", { searchPlaces, ...options, loaders }); const result = explorer.search(searchFrom); if (!result) throw new Error("Failed to find configuration file."); let config = result.config; if (!(options.ignoreEnvironmentVariableSubstitution ?? true)) { const replacedConfig = placeholderResolver( JSON.stringify(result.config), process.env, options.disallowUndefinedEnvironmentVariables ?? true ); config = JSON.parse(replacedConfig); } return config; }; }; // src/loader/dotEnvLoader.ts import * as fs from "fs"; import { resolve } from "path"; import set from "lodash.set"; import dotenv from "dotenv"; import dotenvExpand from "dotenv-expand"; var loadEnvFile = (options) => { const envFilePaths = Array.isArray(options.envFilePath) ? options.envFilePath : [options.envFilePath || resolve(process.cwd(), ".env")]; let config = {}; for (const envFilePath of envFilePaths) { if (fs.existsSync(envFilePath)) { config = Object.assign( dotenv.parse(fs.readFileSync(envFilePath)), config ); if (options.expandVariables) config = dotenvExpand.expand({ parsed: config }).parsed; } else { console.warn(`env file not found: ${envFilePath}`); process.exit(0); } Object.entries(config).forEach(([key, value]) => { if (!Object.prototype.hasOwnProperty.call(process.env, key)) process.env[key] = value; }); } return config; }; var dotenvLoader = (options = {}) => { return () => { const { ignoreEnvFile, ignoreEnvVars, separator } = options; let config = ignoreEnvFile ? {} : loadEnvFile(options); if (!ignoreEnvVars) { config = { ...config, ...process.env }; } if (typeof separator === "string") { const temp = {}; Object.entries(config).forEach(([key, value]) => { set(temp, key.split(separator), value); }); config = temp; } return config; }; }; // src/index.ts var { blue, red, cyan, yellow } = chalk; var TypedConfig = class { async init(options) { const rawConfig = await this.getRawConfigAsync(options.load); return this.processConfig(options, rawConfig); } async processConfig(options, rawConfig) { const { schema: Config, validationOptions, normalize = (config2) => config2, validate = this.validateWithClassValidator.bind(this) } = options; if (typeof rawConfig !== "object") { throw new TypeError( `Configuration should be an object, received: ${rawConfig}. Please check the return value of \`load()\`` ); } const normalized = normalize(rawConfig); const config = validate(normalized, Config, validationOptions); return config; } async getRawConfigAsync(load) { if (Array.isArray(load)) { const config = {}; for (const fn of load) { try { const conf = await fn(); merge(config, conf); } catch (err) { console.log(`Config load failed: ${err.message}`); } } return config; } return load(); } validateWithClassValidator(rawConfig, Config, options) { const config = plainToClass(Config, rawConfig, { exposeDefaultValues: true }); const schemaErrors = validateSync(config, { forbidUnknownValues: true, whitelist: true, ...options }); if (schemaErrors.length > 0) { const configErrorMessage = this.getConfigErrorMessage(schemaErrors); throw new Error(configErrorMessage); } return config; } getConfigErrorMessage(errors) { const messages = this.formatValidationError(errors).map(({ property, value, constraints }) => { const constraintMessage = Object.entries( constraints || {} ).map( ([key, val]) => ` - ${key}: ${yellow(val)}, current config is \`${blue( JSON.stringify(value) )}\`` ).join("\n"); const msg = [ ` - config ${cyan(property)} does not match the following rules:`, `${constraintMessage}` ].join("\n"); return msg; }).filter(Boolean).join("\n"); const configErrorMessage = red( `Configuration is not valid: ${messages} ` ); return configErrorMessage; } formatValidationError(errors) { const result = []; const helper = ({ property, constraints, children, value }, prefix) => { const keyPath = prefix ? `${prefix}.${property}` : property; if (constraints) { result.push({ property: keyPath, constraints, value }); } if (children && children.length) children.forEach((child) => helper(child, keyPath)); }; errors.forEach((error) => helper(error, "")); return result; } }; async function defineConfig(options) { const typedConfigTarget = new TypedConfig(); const typedConfig = await typedConfigTarget.init(options); return typedConfig; } export { TypedConfig, defineConfig, dotenvLoader, fileLoader };