The easiest, declarative, type-safe, and traceable way to write configurations.
The easiest, declarative, type-safe, and traceable way to write configurations.
> ⚠️⚠️ This package is under active development ⚠️⚠️
- chimera-config
- Features
- ♻️ Polymorph
- 👮 Type safe
- 📜 Declarative
- 🛤️ Trackable
- 🔍 Generate config templates
- 🍃 No dependencies
- Non-Features
- Roadmap
- Code Example
- APIs
- Core
- c.useStores()
- c.Store
- c.config()
- Config Parsers
- c.ValueParser
- c.ValueParser.with
- c.boolean()
- c.enumeration()
- c.fnParser()
- c.json()
- c.number()
- c.integer()
- c.bigint()
- c.port()
- c.string()
- c.url()
- Config Modifiers
- c.addProps()
- c.condition()
- c.description()
- c.fallback()
- c.fallbackDescription()
- c.mapProps()
- c.map()
- c.betweenExcl()
- c.betweenIncl()
- c.lessThan()
- c.lessThanOrEqual()
- c.largerThan()
- c.largerThanOrEqual()
- c.optional()
- c.valueHint()
- Env Store
- c.EnvStore
- Generators
- c.generateDotEnvTemplate()
- Modifiers
- c.envVarName()
- c.envVarAlias()
- Args Store
- c.ArgsStore
- Generators
- c.argsHelpTextBuilder()
- Modifiers
- c.argAlias()
- c.argName()
Use env files, command line arguments, config files (like JSON or YAML) or
write your own.
No more any for your configs. All your configs are properly inferred and
validated.
Easy to read, easy to write, easy to understand. No surprises where values come
from or where they were modified.
When enabled, track where your configurations come from and store meta data for
them.
By tracing where your configs come from and adding meta-data, generate example.env files, default configs, or Markdown tables for documentation. All within
your code.
Pure JS for a small footprint and overhead.
- 🙅 Handle complex structures, like docker-compose.yml.
- 🙅 Input validation, beyond basic type-checks. However, you can bring your
own, like zod (WIP).
- 🙅 Being a sophisticated CLI args parser. This library is meant to set
configs/flags via CLI args. But nothing fancy like the docker or aws CLI.
- More supported configs
- JSON (WIP)
- YML
- TOML
- More supported generators
- JSON schema
- YML
- TOML
- Support for validators/transformers
- Standard Schema
implementations, like zod (WIP)
- Async configs (like using fetch)
- Consider migrating from TS to pure JS with JSDoc type annotations.
``ts
import * as c from 'chimera-config';
// Define where to get values from
c.useStores([new c.EnvStore()]);
// Print all paths relative to dirname
c.setRootDir(import.meta.dirname);
// 1️⃣ Define your config
const dbConfig = c.config(
'db', // Prefix of this config
// Define your config as object
{
host: c.string('localhost'),
port: c.port(5432),
auth: {
username: c.string('admin'),
password: c.string(),
},
transaction: {
timeout: c
.integer(5_000)
.with(
c.betweenIncl(1_000, 60_000),
c.description(
'After which time (in milliseconds) transactions are aborted'
)
),
},
}
);
// 2️⃣ Access props of your config with type-safety
const userName: string = dbConfig.auth.username;
console.log(userName);
// 3️⃣ Generate a .env template
console.log(c.generateDotEnvTemplate());
`
This will produce a template for your .env file similar to this:
`shThis file was generated by running script from-readme.ts:40:15
APIs
_This list of APIs currently manually curated. Help to get this automated/queryable would be greatly appreciated!_
Core
$3
Stores to use by default.Any config that does not explicitly override the store will use the provided
stores.
⚠️ This function should be called before any other
c.config() is being
created. That prevents ugly to understand errors (e.g. error is thrown
because config is missing, even though it is present in the process.env) and
race conditions.To prevent this, split the configuration of
chimera-config into its own
file, which is then imported at the very top of you app:`ts
// setup-chimera-config.ts
import * as c from 'chimera-config';// Set the stores you want to use
c.useStores([new c.EnvStore()]);
// ...set up other chimera-config stuff...
``ts
// main.ts
import './setup-chimera-config.ts';
// Leave an empty line, so that code-formatters don't move the above import
// ⬇️⬇️⬇️// ⬆️⬆️⬆️
// Start of your app's code
import * as c from 'chimera-config';
const appConfig = c.config({
port: c.port(3000),
});
const app = new Server();
await app.listen(appConfig.port());
`$3
This basic interface describes the shape of a store.
It's task is to take a
ConfigFieldDescriptor and return if the value for this
descriptor was found or not, and if yes, the value of the resolved config._Example_
`ts
class LocalStorageStore implements c.Store {
resolve({
path,
}: ConfigFieldDescriptor): [found: boolean, value: string | undefined] {
const key = path.join('.');
const value = localStorage.getItem(key);
return [value != null, value];
}
}
`$3
Defines a new config object from the given spec.
`ts
export function config(
spec: Spec,
store?: Store,
): ResolvedConfigSpec;
export function config(
prefix: string,
spec: Spec,
store?: Store,
): ResolvedConfigSpec;
export function config(
prefix: string[],
spec: Spec,
store?: Store,
): ResolvedConfigSpec;
`_Example_
`ts
const simpleConfig = c.config({
isProduction: c.boolean(),
});const prefixedConfig = c.config('external-api', {
apiKey: c.string(),
});
const customStore = c.config(
'db',
{
url: c.url(),
},
new EnvStore(),
);
`Config Parsers
$3
Container class that defines how a config value should be parsed to a type-safe
value.
`ts
export class ValueParser {
constructor(
public readonly resolve: ValueParserFn,
public readonly props: ValueParserProps & ExtraProps,
) {}
}
`_Example_
`ts
const stringArrayParser = new c.ValueParser(
({ storeValue }) => {
if (!Array.isArray(storeValue)) {
storeValue = [storeValue];
}
if (!storeValue.every(elem => typeof elem === 'string')) {
throw new Error('Value must be a string array');
}
return storeValue;
}
{}
)
`####
c.ValueParser.withThis method allows you to map the value parser to a new
ValueParser. You can
modify the attached metadata in the c.ValueParser.props, or update the parsing
behavior by updating the c.ValueParser.resolve.See Config Modifiers for more information.
$3
Defines a boolean config value.
`ts
export function boolean(
options: BooleanParserOptions | boolean = {},
): ValueParser<
boolean,
{
fallback?: (() => boolean) | undefined;
fallbackDescription?: string | undefined;
primitiveType: BooleanConstructor;
valueMust: string[];
[truthyStringsSymbol]: Set;
[falsyStringsSymbol]: Set;
}
>;
`$3
Define a config value which must be one of the given possible values.
`ts
export function enumeration(
values: Value[],
fallback?: Value,
): ValueParser<
Value,
{
fallback?: (() => NonNullable) | undefined;
fallbackDescription?: string | undefined;
primitiveType: undefined;
valueMust: string[];
}
>;
`_Example_
`ts
const appConfig = c.config({
env: c.enumeration(['prod', 'dev', 'local']),
});
`$3
Defines a config value based on a value parser function.
`ts
export function fnParser>(
fn: Fn,
): ValueParser, {}>;
export function fnParser<
Fn extends ValueParserFn,
Props extends ValueParserProps,
>(fn: Fn, props: Props): ValueParser, Props>;
`_Example_
`ts
const uniqueId = crypt.randomUUID();const parsedString = c.fnParser(({ storeValue }) => {
if (typeof storedValue !== 'string') {
throw new Error('Must be a string');
}
// Inject this instance's ID into a config value.
return storedValue.replaceAll('{{ID}}', uniqueId);
});
const exampleConfig = c.config({
add: parsedString,
});
`$3
Define a JSON object config value.
`ts
export function json(
fallback?: object,
fallbackDescription?: string,
): ValueParser<
object,
{
fallback?: (() => object) | undefined;
fallbackDescription?: string | undefined;
primitiveType: StringConstructor;
valueMust: string[];
}
>;
`_Example_
`ts
const loggerConfig = c.config('logging', {
logFormat: c.json(),
});
`$3
Define a number config value.
`ts
export function number(fallback?: number): ValueParser<
number,
{
fallback?: (() => number) | undefined;
fallbackDescription?: string | undefined;
primitiveType: NumberConstructor;
valueMust: string[];
}
>;
`_Example_
`ts
const magicValue = c.config({
factor: c.number();
});
`$3
Define an integer config value.
`ts
export function integer(fallback?: number): ValueParser<
number,
{
fallback?: (() => number) | undefined;
fallbackDescription?: string | undefined;
primitiveType: NumberConstructor;
valueMust: string[];
}
>;
`_Example_
`ts
const elevatorConfig = c.config({
minFloor: c.integer(),
maxFloor: c.integer(),
});
`$3
Define a bigint config value.
`ts
export function bigint(fallback?: bigint): ValueParser<
bigint,
{
fallback?: (() => bigint) | undefined;
fallbackDescription?: string | undefined;
primitiveType: BigIntConstructor;
valueMust: string[];
}
>;
`_Example_
`ts
const myBankAccount = c.config({
cents: c.bigint(),
});
`$3
Defines a port number config value.
`ts
export function port(fallback?: number): ValueParser<
number,
{
fallback?: (() => number) | undefined;
fallbackDescription?: string | undefined;
primitiveType: NumberConstructor;
valueMust: string[];
}
>;
`_Example_
`ts
const appConfig = c.config({
listenPort: c.port(3_000),
});
`$3
Defines a string config value.
`ts
export function string(fallback?: string): ValueParser;
`_Example_
`ts
const apiConfig = c.config({
apiKey: c.string(),
});
`$3
Defines a URL config value.
`ts
export function url(fallback?: URL): ValueParser;
`_Example_
`ts
const dbConfig = c.config('db', {
url: c.url(),
});
`Config Modifiers
Config modifiers, essentially, take in a config parser and return a new config
parser. This allows you to add more meta data, modify what to do before/after
parsing, doing validation of inputs, etc etc.
ValueParser.with.$3
Allows you to set arbitrary properties to the current config.
`ts
export function addProps(props: T): AddPropsModifier;
`_Example_
`ts
const appConfig = c.config({
value: c.string().with(
c.addProps({
important: true,
name: 'Value',
}),
),
});
`$3
Add an condition to a config value.
When this condition is a type-guard, the resulting value will be the guaranteed
type.
`ts
export function condition(
cond: (value: A) => value is B,
msg?: string,
): ValueModifier;
export function condition(
cond: (value: T) => boolean,
msg?: string,
): ValueModifier;
`_Example_
`ts
const isEvent = (value: number) => value % 2 === 0;const appConfig = c.config('app', {
port: c.port().with(c.condition(isEvent)),
});
`$3
Sets the description of the current config.
This description is used by generators to append helpful text to the output.
`ts
export function description(
description: string,
): AddPropsModifier<{ description: string }>;
`_Example_
`ts
const appConfig = c.config('app', {
port: c.port().with(c.description('The port this app listens on.')),
});
`$3
Set the fallback to use for the current config.
`ts
export function fallback(
fallback: () => Fallback,
): AddPropsModifier<{ fallback: () => Fallback }>;
`_Example_
`ts
const appConfig = c.config('app', {
port: c.port().with(c.fallback(() => 3_000)),
});
`$3
Use this to add a string description of the fallback value. Usually the string
representation of the fallback value itself.
If this is not present, then the stringified version of the passed fallback
lambda will be used.
Mostly used internally. Only use if the default fallback stringification is not
working for you.
`ts
export function fallbackDescription(
hint: string,
): AddPropsModifier<{ fallbackDescription: string }>;
`_Example_
`ts
const loggingConfig = c.config('db', {
pinoConfig: c.string().with(),
});
`$3
Modifies the properties of the current config via the given mapping function.
`ts
export function mapProps(
propsFn: (
props: SourceProps,
) => ValueParserProps & TargetProps,
): SetPropsModifier;
`_Example_
`ts
const appConfig = c.config('app', {
port: c.port().with(
c.description(
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' +
'Nunc vel diam leo. Mauris eleifend massa sit amet auctor porttitor. ' +
'Donec laoreet felis nec condimentum molestie',
),
c.mapProps((props) => ({
...props,
// Keep the description to a max length of 100
description: props.description.slice(0, 100),
})),
),
});
`$3
Map the value of a config.
`ts
export function map(mapper: (value: A) => B): ValueModifier;
`_Example_
`ts
const exampleConfig = c.config({
double: c.number().with(c.map((num) => num * 2)),
});
`$3
Adds a validator that the config value is between a upper and lower bound (both
exclusive).
`ts
export function betweenExcl(
a: number,
b: number,
): ValueModifier;
`$3
Adds a validator that the config value is between a upper and lower bound (both
inclusive).
`ts
export function betweenIncl(
a: number,
b: number,
): ValueModifier;
`$3
Adds a validator that the config value is less than the given value.
`ts
export function lessThan(upperBound: number): ValueModifier;
`$3
Adds a validator that the config value is less than or equal to the given value.
`ts
export function lessThanOrEqual(
upperBound: number,
): ValueModifier;
`$3
Adds a validator that the config value is larger than the given value.
`ts
export function largerThan(upperBound: number): ValueModifier;
`$3
Adds a validator that the config value is larger than or equal to the given
value.
`ts
export function largerThanOrEqual(
upperBound: number,
): ValueModifier;
`$3
Make this config optional.
Is an alias for
fallback(() => undefined).c.fallback().`ts
export function optional();
`$3
Adds a value hint to the current config. This hint will be printed in
auto-generated messages, like a .env or the CLI help message.
`ts
export function valueHint(
hint: string,
): AddPropsModifier<{ valueMust: string[] }>;
`Env Store
The
EnvStore takes values from environment variables. By default, it will
convert the path of a config value to a environment variable name. For example:`ts
const postgresConfig = c.config(['db', 'postgres'], {
host: c.string(),
port: c.string(),
auth: {
user: c.string(),
password: c.string(),
},
});
`This will generate the env variable names
DB_POSTGRES_HOST,
DB_POSTGRES_PORT, DB_POSTGRES_AUTH_USER, and DB_POSTGRES_AUTH_PASSWORD.$3
`ts
export class EnvStore implements Store {
constructor(public readonly options: EnvStoreOptions = {});
}export interface EnvStoreOptions {
/**
* The prefix to use for all env variables.
*
* @example "MY_APP_"
*/
prefix?: string;
/**
* The env to extract values from.
*
* Defaults to {@link process.env}.
*/
env?: Record;
/**
* Customize how a config path is transformed to an env var name.
*/
toEnvVariableName?: (path: readonly string[]) => string;
}
`$3
####
c.generateDotEnvTemplate()Generates a .env template file for the passed config descriptors.
Only config descriptors, that were configured to use an {@link EnvStore} will
be included in the template.
`ts
export function generateDotEnvTemplate({
descriptors = trackedConfigFieldDescriptors,
rootDir,
header = # This file was generated by running script ${getCallerLocation().toString(,
}: {
header?: string;
descriptors?: ConfigFieldDescriptor[];
rootDir?: string;
} = {});
`$3
####
c.envVarName()Overrides the environment variable name of the current config.
`ts
export function envVarName(
newName: string,
): AddPropsModifier<{ [envVarSymbol]: string }>;
`_Example_
`ts
const dbConfig = c.config('db', {
url: c.url().with(c.envVarName('DB_FULL_URL')),
});
`####
c.envVarAlias()Add environment variable aliases for the current config.
`ts
export function envVarAlias(
...aliases: string[]
): SetPropsModifier<{ [envVarAliasSymbol]: string[] }>;
`_Example_
`ts
const dbConfig = c.config('db', {
url: c.url().with(c.envVarAlias('POSTGRES_URL', 'SOME_LEGACY_ENV_VAR_NAME')),
});
`Args Store
The
ArgsStore takes values from environment variables. By default, it will
convert the path of a config value to a CLI arg name. For example:`ts
const postgresConfig = c.config(['db', 'postgres'], {
host: c.string(),
port: c.string(),
auth: {
user: c.string(),
password: c.string(),
},
});
`This will generate the CLI arg names
--db-postgres-host, --db-postgres-port,
--db-postgres-auth-user, and --db-postgres-auth-password.$3
`ts
export class ArgsStore implements Store {
constructor(private readonly options: ArgsStoreOptions = {}) {
this.args = options.args ?? process.argv;
}
}export interface ArgsStoreOptions {
/**
* Prefix to use for all arguments that use this {@link ArgsStore}.
*
* @example "app".
*/
prefix?: string;
/**
* The arguments to parse and extract values from.
*
* Defaults to {@link process.argv}.
*/
args?: readonly string[];
}
`$3
####
c.argsHelpTextBuilder()Function to auto-generate a help text based on the configs that are configured
with the
c.ArgsStore.`ts
export function argsHelpTextBuilder({
descriptors = trackedConfigFieldDescriptors,
intro,
outro,
sort,
maxWidth = process.stdout.isTTY ? process.stdout.columns : Infinity,
sliceLine = sliceLinePreservingWords,
additionalInfo = addConfigMeta,
includeValueHints,
}: ArgsHelpTextBuilderOptions = {}): string;export interface ArgsHelpTextBuilderOptions {
/**
* Any leading text that comes before the generated help text. Defaults to no
* intro.
*/
intro?: string;
/**
* Any text after the arguments have been printed. Defaults to no outro.
*/
outro?: string;
/**
* The configs to include in the help text.
*
* Only config fields that are configured with the {@link ArgsStore} will be
* processed. All others are filtered out.
*/
descriptors?: ConfigFieldDescriptor[];
/**
* The max line width in characters to limit the help text to.
*
* To disable max width, set to Infinity.
*
* Defaults to {@link process.stdout.columns} if {@link process.stdout.isTTY}
* is true. Null otherwise.
*/
maxWidth?: number;
/**
* Define the order in which the arguments should be included in the help
* text.
*
* If none is provided, the the help text will be generated in the order they
* are in the given {@link ArgsHelpTextBuilderOptions#descriptors} array.
*/
sort?: (a: ArgsWithTexts, b: ArgsWithTexts) => number;
/**
* In case any line (description, intro, outro) is too long to fit into the
* {@link ArgsHelpTextBuilderOptions#maxWidth}, this function will be called
* to split the line into multiple lines which are each no longer than
* maxLength long.
*
* Defaults to breaking the line after the nth word that would make the line
* longer tha max Length characters.
*/
sliceLine?: (descriptionLine: string, maxLength: number) => string[];
/**
* If set to true, all value hints will be included in the generated help
* text. Can be rather verbose, so it's set to false by default.
*/
includeValueHints?: boolean;
/**
* The additional info to print after the description. This text will be
* appended to the description. Returning an empty string is equal to a noop.
*
* Defaults to printing out the env vars overrides for this config.
*/
additionalInfo?: (args: AdditionalInfoArgs) => string;
}
`$3
####
c.argAlias()Add argument aliases for the current config.
If a given alias starts with a dash ("-"), then this alias must be exactly
matching. Otherwise the same logic as for other args applies.
`ts
export function argAlias(
...aliases: string[]
): SetPropsModifier<{ [argAliasName]: string[] }>;
`####
c.argName()Override the argument name of the current config.
`ts
export function argName(
newArgName: string,
): SetPropsModifier<{ [argNameOverride]: string }>;
``