@boost/config
Version:
Powerful convention based finder, loader, and manager of both configuration and ignore files.
101 lines (82 loc) • 2.99 kB
text/typescript
/* eslint-disable no-await-in-loop */
import { Contract, isObject } from '@boost/common';
import { type Blueprint, optimal, type Schemas } from '@boost/common/optimal';
import { createDebugger, type Debugger } from '@boost/debug';
import { color } from '@boost/internal';
import { mergeArray } from './helpers/mergeArray';
import { mergeObject } from './helpers/mergeObject';
import type { ConfigFile, Handler, ProcessorOptions } from './types';
export class Processor<T extends object> extends Contract<ProcessorOptions> {
protected readonly debug: Debugger;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected handlers: { [K in keyof T]?: Handler<any> } = {};
constructor(options: ProcessorOptions) {
super(options);
this.debug = createDebugger(['processor', this.options.name]);
}
blueprint(schemas: Schemas): Blueprint<ProcessorOptions> {
const { bool, string } = schemas;
return {
defaultWhenUndefined: bool(true),
name: string().required().camelCase(),
validate: bool(true),
};
}
/**
* Add a handler to process a key-value setting pair.
*/
addHandler<K extends keyof T, V = T[K]>(key: K, handler: Handler<V>): this {
this.debug('Adding process handler for %s', color.symbol(key));
this.handlers[key] = handler;
return this;
}
/**
* Return a handler, or null, for the setting key.
*/
getHandler<K extends keyof T, V = T[K]>(key: K): Handler<V> | null {
return (this.handlers[key] as Handler<V>) || null;
}
/**
* Process a list of loaded config files into a single config object.
* Use the defined process handlers, or the default processing rules,
* to generate the final config object.
*/
async process(
defaults: Required<T>,
configs: ConfigFile<T>[],
blueprint: Blueprint<T>,
): Promise<Required<T>> {
const { defaultWhenUndefined, validate } = this.options;
const config = { ...defaults };
this.debug('Processing %d configs into a single and final result', configs.length);
for (const next of configs) {
// Validate next config object
if (validate) {
optimal(blueprint, {
file: next.path.path(),
}).validate(next.config);
}
// Merge properties into previous object
for (const [key, value] of Object.entries(next.config)) {
const name = key as keyof T;
const nextValue = value as T[keyof T];
const prevValue = config[name];
const handler = this.getHandler(name);
if (handler) {
config[name] = (await handler(prevValue, nextValue))!;
} else if (isObject(prevValue) && isObject(nextValue)) {
config[name] = mergeObject(prevValue, nextValue);
} else if (Array.isArray(prevValue) && Array.isArray(nextValue)) {
config[name] = mergeArray(prevValue, nextValue);
} else {
config[name] = nextValue;
}
// Reset to default value if undefined is present
if (config[name] === undefined && defaultWhenUndefined) {
config[name] = defaults[name];
}
}
}
return config;
}
}