env-typed-config
Version:
Intuitive, type-safe configuration library for Node.js
236 lines (226 loc) • 8.32 kB
JavaScript
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;
;