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