packages/config/src/index.ts
import path from "path";
import merge from "lodash/merge";
import findUp from "find-up";
import Conf from "conf";
import TruffleError from "@truffle/error";
import originalRequire from "original-require";
import { getInitialConfig, configProps } from "./configDefaults";
import { EventManager } from "@truffle/events";
import debugModule from "debug";
const debug = debugModule("config");
const DEFAULT_CONFIG_FILENAME = "truffle-config.js";
const BACKUP_CONFIG_FILENAME = "truffle.js"; // old config filename
class TruffleConfig {
// eslint-disable-next-line no-undef
[key: string]: any;
private _deepCopy: string[];
private _values: any;
constructor(
truffleDirectory?: string,
workingDirectory?: string,
network?: any
) {
this._deepCopy = ["compilers", "mocha", "dashboard", "networks"];
this._values = getInitialConfig({
truffleDirectory,
workingDirectory,
network
});
this.events = new EventManager(this);
const props = configProps({ configObject: this });
Object.entries(props).forEach(([propName, descriptor]) =>
this.addProp(propName, descriptor)
);
}
public addProp(propertyName: string, descriptor: any): void {
// possible property descriptors
//
// supports `default` and `transform` in addition to `get` and `set`
//
// default: specify function to retrieve default value (used by get)
// transform: specify function to transform value when (used by set)
const self = this;
Object.defineProperty(this, propertyName, {
get:
descriptor.get ||
function () {
// value is specified
if (propertyName in self._values) {
return self._values[propertyName];
}
// default getter is specified
if (descriptor.default) {
return descriptor.default();
}
// descriptor is a function
return descriptor();
},
set:
descriptor.set ||
function (value) {
self._values[propertyName] = descriptor.transform
? descriptor.transform(value)
: value;
},
configurable: true,
enumerable: true
});
}
public normalize(obj: any): any {
const clone: any = {};
Object.keys(obj).forEach(key => {
try {
clone[key] = obj[key];
} catch (e) {
// Do nothing with values that throw.
}
});
return clone;
}
public with(obj: any): TruffleConfig {
//Normalized, or shallow clowning only copies an object's own enumerable
//properties ignoring properties up the prototype chain
const current = this.normalize(this);
const normalized = this.normalize(obj);
const newConfig = Object.assign(
Object.create(TruffleConfig.prototype),
current,
normalized
);
this.events.updateSubscriberOptions(newConfig);
return newConfig;
}
public merge(obj: any): TruffleConfig {
const clone = this.normalize(obj);
// Only set keys for values that don't throw.
const propertyNames = Object.keys(obj);
propertyNames.forEach(key => {
try {
if (typeof clone[key] === "object" && this._deepCopy.includes(key)) {
this[key] = merge(this[key], clone[key]);
} else {
debug("setting key -- %o -- to -- %o", key, clone[key]);
this[key] = clone[key];
}
} catch (e) {
// ignore
}
});
this.events.updateSubscriberOptions(this);
return this;
}
public static default(): TruffleConfig {
return new TruffleConfig();
}
public static search(options: any = {}, filename?: string): string | null {
const searchOptions = {
cwd: options.working_directory || options.workingDirectory
};
if (!filename) {
const isWin = process.platform === "win32";
const defaultConfig = findUp.sync(DEFAULT_CONFIG_FILENAME, searchOptions);
const backupConfig = findUp.sync(BACKUP_CONFIG_FILENAME, searchOptions);
if (defaultConfig && backupConfig) {
console.warn(
`Warning: Both ${DEFAULT_CONFIG_FILENAME} and ${BACKUP_CONFIG_FILENAME} were found. Using ${DEFAULT_CONFIG_FILENAME}.`
);
return defaultConfig;
} else if (backupConfig && !defaultConfig) {
if (isWin)
console.warn(
`Warning: Please rename ${BACKUP_CONFIG_FILENAME} to ${DEFAULT_CONFIG_FILENAME} to ensure Windows compatibility.`
);
return backupConfig;
} else {
return defaultConfig;
}
}
return findUp.sync(filename, searchOptions);
}
public static detect(options: any = {}, filename?: string): TruffleConfig {
debug("callling Config.detect with filename -- %o", filename);
let configFile;
const configPath = options.config;
if (configPath) {
configFile = path.isAbsolute(configPath)
? configPath
: path.resolve(configPath);
} else {
configFile = TruffleConfig.search(options, filename);
}
if (!configFile) {
throw new TruffleError("Could not find suitable configuration file.");
}
return TruffleConfig.load(configFile, options);
}
public static load(file: string, options: any = {}): TruffleConfig {
debug("calling Config.load with file -- %o", file);
const workingDirectory = options.config
? process.cwd()
: path.dirname(path.resolve(file));
const config = new TruffleConfig(undefined, workingDirectory, undefined);
const staticConfig = originalRequire(file);
debug("the static config is -- %o", staticConfig);
config.merge(staticConfig);
config.merge(options);
// When loading a user's config, ensure their subscribers are initialized
config.events.updateSubscriberOptions(config);
config.events.initializeUserSubscribers(config);
return config;
}
public static getUserConfig(): Conf {
return new Conf({ projectName: "truffle" });
}
public static getTruffleDataDirectory(): string {
const conf = TruffleConfig.getUserConfig();
return path.dirname(conf.path);
}
}
export = TruffleConfig;