trufflesuite/truffle

View on GitHub
packages/core/lib/debug/cli.js

Summary

Maintainability
B
5 hrs
Test Coverage
const debugModule = require("debug");
const debug = debugModule("lib:debug:cli");

const fs = require("fs-extra");
const path = require("path");

const Debugger = require("@truffle/debugger");
const DebugUtils = require("@truffle/debug-utils");
const Codec = require("@truffle/codec");
const { fetchAndCompileForDebugger } = require("@truffle/fetch-and-compile");

const { DebugInterpreter } = require("./interpreter");
const { DebugCompiler } = require("./compiler");

const Spinner = require("@truffle/spinners").Spinner;

class CLIDebugger {
  constructor(config, { compilations, txHash } = {}) {
    this.config = config;
    this.compilations = compilations;
    this.txHash = txHash;
  }

  async run() {
    this.config.logger.log("Starting Truffle Debugger...");

    const session = await this.connect();

    // initialize prompt/breakpoints/ui logic
    const interpreter = await this.buildInterpreter(session);

    return interpreter;
  }

  async connect() {
    // get compilations (either by shimming compiled artifacts,
    // or by doing a recompile)
    const compilations = this.compilations || (await this.getCompilations());

    // invoke @truffle/debugger
    const session = await this.startDebugger(compilations);

    return session;
  }

  async fetchExternalSources(bugger) {
    const fetchSpinner = new Spinner(
      "core:debug:cli:fetch",
      "Getting and compiling external sources..."
    );
    const {
      fetch: badAddresses,
      fetchers: badFetchers,
      compile: badCompilationAddresses
    } = await fetchAndCompileForDebugger(bugger, this.config); //Note: mutates bugger!!
    if (
      badAddresses.length === 0 &&
      badFetchers.length === 0 &&
      badCompilationAddresses.length === 0
    ) {
      fetchSpinner.succeed();
    } else {
      let warningStrings = [];
      if (badFetchers.length > 0) {
        warningStrings.push(
          `Errors occurred connecting to ${badFetchers.join(", ")}.`
        );
      }
      if (badAddresses.length > 0) {
        warningStrings.push(
          `Errors occurred while getting sources for addresses ${badAddresses.join(
            ", "
          )}.`
        );
      }
      if (badCompilationAddresses.length > 0) {
        warningStrings.push(
          `Errors occurred while compiling sources for addresses ${badCompilationAddresses.join(
            ", "
          )}.`
        );
      }
      // simulate ora's "warn" feature
      fetchSpinner.warn(warningStrings.join("  "));
    }
  }

  async getCompilations() {
    //if compileNone is true and configFileSkiped
    //we understand that user is debugging using --url and does not have a config file
    //so instead of resolving compilations, we return an empty value
    if (this.config.compileNone && this.config.configFileSkipped) {
      return [];
    }

    let artifacts;
    artifacts = await this.gatherArtifacts();
    if ((artifacts && !this.config.compileAll) || this.config.compileNone) {
      let shimmedCompilations =
        Codec.Compilations.Utils.shimArtifacts(artifacts);
      //if they were compiled simultaneously, yay, we can use it!
      //(or if we *force* it to...)
      if (
        this.config.compileNone ||
        shimmedCompilations.every(DebugUtils.isUsableCompilation)
      ) {
        debug("shimmed compilations usable");
        return shimmedCompilations;
      }
      debug("shimmed compilations unusable");
    }
    //if not, or if build directory doesn't exist, we have to recompile
    return await this.compileSources();
  }

  async compileSources() {
    const compileSpinner = new Spinner(
      "core:debug:cli:compile",
      "Compiling your contracts..."
    );

    const compilationResult = await new DebugCompiler(this.config).compile({
      withTests: this.config.compileTests
    });
    debug("compilationResult: %O", compilationResult);

    compileSpinner.succeed();

    return Codec.Compilations.Utils.shimCompilations(compilationResult);
  }

  async startDebugger(compilations) {
    const startMessage = DebugUtils.formatStartMessage(
      this.txHash !== undefined
    );
    const specifiedRegistry = this.config.noEns ? null : this.config.registry; //specified at the command line
    const registry =
      specifiedRegistry !== undefined
        ? specifiedRegistry
        : this.config.ensRegistry?.address;
    let bugger;
    if (!this.config.fetchExternal) {
      //ordinary case, not doing fetch-external
      const startSpinner = new Spinner("core:debug:cli:start", startMessage);
      bugger = await Debugger.forProject({
        provider: this.config.provider,
        compilations,
        ens: { registryAddress: registry }
      });
      if (this.txHash !== undefined) {
        try {
          debug("loading %s", this.txHash);
          await bugger.load(this.txHash);
          startSpinner.succeed();
        } catch (_) {
          debug("loading error");
          startSpinner.fail();
          //just start up unloaded
        }
      } else {
        startSpinner.succeed();
      }
    } else {
      //fetch-external case
      //note that in this case we start in light mode
      //and only wake up to full mode later!
      //also, in this case, we can be sure that txHash is defined
      bugger = await Debugger.forTx(this.txHash, {
        provider: this.config.provider,
        compilations,
        ens: { registryAddress: registry },
        lightMode: true
      }); //note: may throw!
      await this.fetchExternalSources(bugger); //note: mutates bugger!
      const startSpinner = new Spinner("core:debug:cli:start", startMessage);
      await bugger.startFullMode();
      //I'm removing the failure check here because I don't think that can
      //actually happen
      startSpinner.succeed();
    }
    return bugger;
  }

  async buildInterpreter(session) {
    return new DebugInterpreter(this.config, session, this.txHash);
  }

  async gatherArtifacts() {
    // Gather all available contract artifacts
    // if build directory doesn't exist, return undefined to signal that
    // a recompile is necessary
    if (!fs.existsSync(this.config.contracts_build_directory)) {
      return undefined;
    }
    const files = fs.readdirSync(this.config.contracts_build_directory);

    let contracts = files
      .filter(filePath => {
        return path.extname(filePath) === ".json";
      })
      .map(filePath => {
        return path.basename(filePath, ".json");
      })
      .map(contractName => {
        return this.config.resolver.require(contractName);
      });

    await Promise.all(
      contracts.map(abstraction => abstraction.detectNetwork())
    );

    return contracts;
  }
}

module.exports = {
  CLIDebugger
};