trufflesuite/truffle

View on GitHub
packages/config/src/index.ts

Summary

Maintainability
A
1 hr
Test Coverage
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;