UNPKG

env-typed-config

Version:

Intuitive, type-safe configuration library for Node.js

236 lines (226 loc) 8.32 kB
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }// src/index.ts var _lodashmerge = require('lodash.merge'); var _lodashmerge2 = _interopRequireDefault(_lodashmerge); var _chalk = require('chalk'); var _chalk2 = _interopRequireDefault(_chalk); var _classtransformer = require('class-transformer'); var _classvalidator = require('class-validator'); require('reflect-metadata'); // src/loader/fileLoader.ts var _path = require('path'); var _cosmiconfig = require('cosmiconfig'); var _toml = require('@iarna/toml'); var _toml2 = _interopRequireDefault(_toml); var loadToml = function loadToml2(filepath, content) { try { const result = _toml2.default.parse(content); return result; } catch (error) { error.message = `TOML Error in ${filepath}: ${error.message}`; throw error; } }; var getSearchOptions = (options) => { if (options.absolutePath) { return { searchPlaces: [_path.basename.call(void 0, options.absolutePath)], searchFrom: _path.dirname.call(void 0, 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 = _cosmiconfig.cosmiconfigSync.call(void 0, "env", { searchPlaces, ...options, loaders }); const result = explorer.search(searchFrom); if (!result) throw new Error("Failed to find configuration file."); let config = result.config; if (!(_nullishCoalesce(options.ignoreEnvironmentVariableSubstitution, () => ( true)))) { const replacedConfig = placeholderResolver( JSON.stringify(result.config), process.env, _nullishCoalesce(options.disallowUndefinedEnvironmentVariables, () => ( true)) ); config = JSON.parse(replacedConfig); } return config; }; }; // src/loader/dotEnvLoader.ts var _fs = require('fs'); var fs = _interopRequireWildcard(_fs); var _lodashset = require('lodash.set'); var _lodashset2 = _interopRequireDefault(_lodashset); var _dotenv = require('dotenv'); var _dotenv2 = _interopRequireDefault(_dotenv); var _dotenvexpand = require('dotenv-expand'); var _dotenvexpand2 = _interopRequireDefault(_dotenvexpand); var loadEnvFile = (options) => { const envFilePaths = Array.isArray(options.envFilePath) ? options.envFilePath : [options.envFilePath || _path.resolve.call(void 0, process.cwd(), ".env")]; let config = {}; for (const envFilePath of envFilePaths) { if (fs.existsSync(envFilePath)) { config = Object.assign( _dotenv2.default.parse(fs.readFileSync(envFilePath)), config ); if (options.expandVariables) config = _dotenvexpand2.default.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]) => { _lodashset2.default.call(void 0, temp, key.split(separator), value); }); config = temp; } return config; }; }; // src/index.ts var { blue, red, cyan, yellow } = _chalk2.default; 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(); _lodashmerge2.default.call(void 0, config, conf); } catch (err) { console.log(`Config load failed: ${err.message}`); } } return config; } return load(); } validateWithClassValidator(rawConfig, Config, options) { const config = _classtransformer.plainToClass.call(void 0, Config, rawConfig, { exposeDefaultValues: true }); const schemaErrors = _classvalidator.validateSync.call(void 0, 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; } exports.TypedConfig = TypedConfig; exports.defineConfig = defineConfig; exports.dotenvLoader = dotenvLoader; exports.fileLoader = fileLoader;