diff --git a/packages/core/cli.js b/packages/core/cli.js index 9152eaa4c6d..2aac802358d 100755 --- a/packages/core/cli.js +++ b/packages/core/cli.js @@ -41,8 +41,8 @@ listeners.forEach(listener => process.removeListener("warning", listener)); let options = { logger: console }; const inputArguments = process.argv.slice(2); -const userWantsGeneralHelp = - inputArguments.length === 1 && ["help", "--help"].includes(inputArguments[0]); +const userWantsGeneralHelp = + inputArguments.length === 1 && ['help', '--help'].includes(inputArguments[0]); if (userWantsGeneralHelp) { command.displayGeneralHelp(); diff --git a/packages/core/index.js b/packages/core/index.js index dfb602225cb..db0f74e0f19 100644 --- a/packages/core/index.js +++ b/packages/core/index.js @@ -4,6 +4,7 @@ var pkg = require("./package.json"); module.exports = { build: require("./lib/build"), create: require("./lib/commands/create/helpers"), + console: require("./lib/repl"), contracts: require("@truffle/workflow-compile"), package: require("./lib/package"), test: require("./lib/test"), diff --git a/packages/core/lib/command.js b/packages/core/lib/command.js index 1e9d629352c..365574f9128 100644 --- a/packages/core/lib/command.js +++ b/packages/core/lib/command.js @@ -11,7 +11,7 @@ class Command { let args = yargs(); - Object.keys(this.commands).forEach(function (command) { + Object.keys(this.commands).forEach(function(command) { args = args.command(commands[command]); }); @@ -68,7 +68,7 @@ class Command { return { name: chosenCommand, argv, - command, + command }; } @@ -134,11 +134,10 @@ class Command { const newOptions = Object.assign({}, clone, argv); result.command.run(newOptions, callback); - analytics.send({ command: result.name ? result.name : "other", args: result.argv._, - version: bundled || "(unbundled) " + core, + version: bundled || "(unbundled) " + core }); } catch (err) { callback(err); diff --git a/packages/core/lib/console-child.js b/packages/core/lib/console-child.js deleted file mode 100644 index 6c6d77da51e..00000000000 --- a/packages/core/lib/console-child.js +++ /dev/null @@ -1,46 +0,0 @@ -const Command = require("../lib/command"); -const TruffleError = require("@truffle/error"); -const Config = require("@truffle/config"); -const Web3 = require("web3"); -const yargs = require("yargs"); - -const input = process.argv[2].split(" -- "); -const inputStrings = input[1]; - -//detect config so we can get the provider and resolver without having to serialize -//and deserialize them -const detectedConfig = Config.detect({ network: yargs(input[0]).network }); -const customConfig = detectedConfig.networks.develop || {}; - -//need host and port for provider url -const ganacheOptions = { - host: customConfig.host || "127.0.0.1", - port: customConfig.port || 9545 -}; -const url = `http://${ganacheOptions.host}:${ganacheOptions.port}/`; - -//set up the develop network to use, including setting up provider -detectedConfig.networks.develop = { - host: customConfig.host || "127.0.0.1", - port: customConfig.port || 9545, - network_id: customConfig.network_id || 5777, - provider: function () { - return new Web3.providers.HttpProvider(url, { keepAlive: false }); - } -}; - -const command = new Command(require("../lib/commands")); - -command.run(inputStrings, detectedConfig, error => { - if (error) { - // Perform error handling ourselves. - if (error instanceof TruffleError) { - console.log(error.message); - } else { - // Bubble up all other unexpected errors. - console.log(error.stack || error.toString()); - } - process.exit(1); - } - process.exit(0); -}); diff --git a/packages/core/lib/console.js b/packages/core/lib/console.js index 0f6b537f63a..d741122d38a 100644 --- a/packages/core/lib/console.js +++ b/packages/core/lib/console.js @@ -1,4 +1,4 @@ -const repl = require("repl"); +const ReplManager = require("./repl"); const Command = require("./command"); const provision = require("@truffle/provisioner"); const { @@ -12,7 +12,6 @@ const TruffleError = require("@truffle/error"); const fse = require("fs-extra"); const path = require("path"); const EventEmitter = require("events"); -const spawnSync = require("child_process").spawnSync; const processInput = input => { const inputComponents = input.trim().split(" "); @@ -44,10 +43,9 @@ class Console extends EventEmitter { this.options = options; + this.repl = options.repl || new ReplManager(options); this.command = new Command(tasks); - this.repl = null; - this.interfaceAdapter = createInterfaceAdapter({ provider: options.provider, networkType: options.networks[options.network].type @@ -56,22 +54,37 @@ class Console extends EventEmitter { provider: options.provider, networkType: options.networks[options.network].type }); + + // Bubble the ReplManager's exit event + this.repl.on("exit", () => this.emit("exit")); + + // Bubble the ReplManager's reset event + this.repl.on("reset", () => this.emit("reset")); } - start() { + start(callback) { + if (!this.repl) this.repl = new Repl(this.options); + + // TODO: This should probalby be elsewhere. + // It's here to ensure the repl manager instance gets + // passed down to commands. + this.options.repl = this.repl; + try { this.interfaceAdapter.getAccounts().then(fetchedAccounts => { const abstractions = this.provision(); - this.repl = repl.start({ + this.repl.start({ prompt: "truffle(" + this.options.network + ")> ", - eval: this.interpret.bind(this) + context: { + web3: this.web3, + interfaceAdapter: this.interfaceAdapter, + accounts: fetchedAccounts + }, + interpreter: this.interpret.bind(this), + done: callback }); - this.repl.context.web3 = this.web3; - this.repl.context.interfaceAdapter = this.interfaceAdapter; - this.repl.context.accounts = fetchedAccounts; - this.resetContractsInConsoleContext(abstractions); }); } catch (error) { @@ -130,41 +143,8 @@ class Console extends EventEmitter { abstractions.forEach(abstraction => { contextVars[abstraction.contract_name] = abstraction; }); - } - - runSpawn(inputStrings, options, callback) { - let childPath; - if (typeof BUNDLE_CONSOLE_CHILD_FILENAME !== "undefined") { - childPath = path.join(__dirname, BUNDLE_CONSOLE_CHILD_FILENAME); - } else { - childPath = path.join(__dirname, "../lib/console-child.js"); - } - const spawnOptions = { stdio: ["inherit", "inherit", "inherit"] }; - - const spawnInput = "--network " + options.network + " -- " + inputStrings; - - try { - spawnSync( - "node", - ["--no-deprecation", childPath, spawnInput], - spawnOptions - ); - - try { - this.provision(); - } catch (e) { - console.log(e); - } - } catch (err) { - callback(err); - } - //want repl to exit when it receives an exit command - this.repl.on("exit", () => { - process.exit(); - }); - //display prompt when child repl process is finished - this.repl.displayPrompt(); + this.repl.setContextVars(contextVars); } interpret(input, context, filename, callback) { @@ -172,7 +152,7 @@ class Console extends EventEmitter { if ( this.command.getCommand(processedInput, this.options.noAliases) != null ) { - return this.runSpawn(processedInput, this.options, error => { + return this.command.run(processedInput, this.options, error => { if (error) { // Perform error handling ourselves. if (error instanceof TruffleError) { diff --git a/packages/core/lib/debug/interpreter.js b/packages/core/lib/debug/interpreter.js index c3d627f550e..8e27e8a56c5 100644 --- a/packages/core/lib/debug/interpreter.js +++ b/packages/core/lib/debug/interpreter.js @@ -10,7 +10,7 @@ const selectors = require("@truffle/debugger").selectors; const { session, solidity, trace, evm, controller } = selectors; const analytics = require("../services/analytics"); -const repl = require("repl"); +const ReplManager = require("../repl"); const { DebugPrinter } = require("./printer"); @@ -40,8 +40,9 @@ class DebugInterpreter { this.printer = new DebugPrinter(config, session); this.txHash = txHash; this.lastCommand = "n"; + + this.repl = config.repl || new ReplManager(config); this.enabledExpressions = new Set(); - this.repl = null; } async setOrClearBreakpoint(args, setOrClear) { @@ -275,14 +276,24 @@ class DebugInterpreter { ? DebugUtils.formatPrompt(this.network, this.txHash) : DebugUtils.formatPrompt(this.network); - this.repl = repl.start({ - prompt: prompt, - eval: util.callbackify(this.interpreter.bind(this)), + this.repl.start({ + prompt, + interpreter: util.callbackify(this.interpreter.bind(this)), ignoreUndefined: true, done: terminate }); } + setPrompt(prompt) { + this.repl.activate.bind(this.repl)({ + prompt, + context: {}, + //this argument only *adds* things, so it's safe to set it to {} + ignoreUndefined: true + //set to true because it's set to true below :P + }); + } + async interpreter(cmd) { cmd = cmd.trim(); let cmdArgs, splitArgs; @@ -293,7 +304,10 @@ class DebugInterpreter { } //split arguments for commands that want that; split on runs of spaces - splitArgs = cmd.trim().split(/ +/).slice(1); + splitArgs = cmd + .trim() + .split(/ +/) + .slice(1); debug("splitArgs %O", splitArgs); //warning: this bit *alters* cmd! @@ -310,7 +324,7 @@ class DebugInterpreter { //quit if that's what we were given if (cmd === "q") { - process.exit(); + return await util.promisify(this.repl.stop.bind(this.repl))(); } let alreadyFinished = this.session.view(trace.finishedOrUnloaded); @@ -393,7 +407,7 @@ class DebugInterpreter { if (this.session.view(selectors.session.status.success)) { txSpinner.succeed(); //if successful, change prompt - this.repl.setPrompt(DebugUtils.formatPrompt(this.network, cmdArgs)); + this.setPrompt(DebugUtils.formatPrompt(this.network, cmdArgs)); } else { txSpinner.fail(); loadFailed = true; @@ -416,7 +430,7 @@ class DebugInterpreter { if (this.session.view(selectors.session.status.loaded)) { await this.session.unload(); this.printer.print("Transaction unloaded."); - this.repl.setPrompt(DebugUtils.formatPrompt(this.network)); + this.setPrompt(DebugUtils.formatPrompt(this.network)); } else { this.printer.print("No transaction to unload."); this.printer.print(""); diff --git a/packages/core/lib/repl.js b/packages/core/lib/repl.js new file mode 100644 index 00000000000..fb69a7e1389 --- /dev/null +++ b/packages/core/lib/repl.js @@ -0,0 +1,132 @@ +var repl = require("repl"); +var expect = require("@truffle/expect"); +var EventEmitter = require("events"); +var inherits = require("util").inherits; + +inherits(ReplManager, EventEmitter); + +function ReplManager(options) { + EventEmitter.call(this); + + expect.options(options, [ + "working_directory", + "contracts_directory", + "contracts_build_directory", + "migrations_directory", + "network", + "network_id", + "provider", + "resolver", + "build_directory" + ]); + + this.options = options; + this.repl = options.repl; + + this.contexts = []; +} + +ReplManager.prototype.start = function(options) { + var self = this; + + this.contexts.push({ + prompt: options.prompt, + interpreter: options.interpreter, + ignoreUndefined: options.ignoreUndefined || false, + done: options.done + }); + + var currentContext = this.contexts[this.contexts.length - 1]; + + if (!this.repl) { + this.repl = repl.start({ + prompt: currentContext.prompt, + eval: this.interpret.bind(this) + }); + + this.repl.on("exit", async function() { + // If we exit for some reason, call done functions for good measure + // then ensure the process is completely killed. Once the repl exits, + // the process is in a bad state and can't be recovered (e.g., stdin is closed). + try { + for (const context of self.contexts) { + if (context.done) await context.done(); + } + } catch (error) { + throw error; + } finally { + process.exit(); + } + }); + } + + // Bubble the internal repl's exit event + this.repl.on("exit", function() { + self.emit("exit"); + }); + + // Bubble the internal repl's reset event + this.repl.on("reset", function() { + process.stdout.write("\u001B[2J\u001B[0;0f"); + self.emit("reset"); + }); + + this.repl.setPrompt(options.prompt); + this.setContextVars(options.context || {}); + this.activate(options); +}; + +ReplManager.prototype.setContextVars = function(obj) { + var self = this; + if (this.repl) { + Object.keys(obj || {}).forEach(function(key) { + self.repl.context[key] = obj[key]; + }); + } +}; + +ReplManager.prototype.activate = function(session) { + const { prompt, context, ignoreUndefined } = session; + this.repl.setPrompt(prompt); + this.repl.ignoreUndefined = ignoreUndefined; + this.setContextVars(context); +}; + +ReplManager.prototype.stop = function(callback) { + var oldContext = this.contexts.pop(); + + if (oldContext.done) { + oldContext.done(); + } + + var currentContext = this.contexts[this.contexts.length - 1]; + + if (currentContext) { + this.activate(currentContext); + } else { + // If there's no new context, stop the process altogether. + // Though this might seem like an out of place process.exit(), + // once the Node repl closes, the state of the process is not + // recoverable; e.g., stdin is closed and can't be reopened. + // Since we can't recover to a state before the repl was opened, + // we should just exit. He're, we'll exit after we've popped + // off the stack of all repl contexts. + process.exit(); + } + + if (callback) { + callback(); + } +}; + +ReplManager.prototype.interpret = function( + replInput, + context, + filename, + callback +) { + const currentContext = this.contexts[this.contexts.length - 1]; + currentContext.interpreter(replInput, context, filename, callback); +}; + +module.exports = ReplManager; diff --git a/packages/truffle/test/scenarios/library/api.js b/packages/truffle/test/scenarios/library/api.js index 6511ec30c7a..f69d92ec4b6 100644 --- a/packages/truffle/test/scenarios/library/api.js +++ b/packages/truffle/test/scenarios/library/api.js @@ -5,7 +5,7 @@ describe("Truffle Library APIs [ @standalone ]", () => { if (process.env.NO_BUILD) return; let truffle; - before(function () { + before(function() { this.timeout(10000); truffle = require("../../../build/library.bundled.js"); }); @@ -23,6 +23,11 @@ describe("Truffle Library APIs [ @standalone ]", () => { assert(truffle.create.migration, "create.migration undefined"); }); + it("truffle.console API definition", () => { + // This one returns a constructor. + assert(truffle.console, "console undefined"); + }); + it("truffle.contracts API definition", () => { assert(truffle.contracts.compile, "contracts.compile undefined"); assert( diff --git a/packages/truffle/webpack.config.js b/packages/truffle/webpack.config.js index d4a651fc223..c2e63314e43 100644 --- a/packages/truffle/webpack.config.js +++ b/packages/truffle/webpack.config.js @@ -40,21 +40,13 @@ module.exports = { "@truffle/core", "index.js" ), - consoleChild: path.join( - __dirname, - "../..", - "node_modules", - "@truffle/core", - "lib", - "console-child.js" - ) }, target: "node", node: { // For this option, see here: https://github.com/webpack/webpack/issues/1599 __dirname: false, - __filename: false + __filename: false, }, context: rootDir, @@ -62,12 +54,12 @@ module.exports = { path: outputDir, filename: "[name].bundled.js", library: "", - libraryTarget: "commonjs" + libraryTarget: "commonjs", }, devtool: "source-map", optimization: { - minimize: false + minimize: false, }, module: { @@ -77,11 +69,11 @@ module.exports = { test: /\.js$/, include: [ path.resolve(__dirname, "../core"), - path.resolve(__dirname, "../environment") + path.resolve(__dirname, "../environment"), ], - use: "shebang-loader" - } - ] + use: "shebang-loader", + }, + ], }, externals: [ @@ -89,7 +81,7 @@ module.exports = { // Here, we leave it as an external, and use the original-require // module that's a dependency of Truffle instead. /^original-require$/, - /^mocha$/ + /^mocha$/, ], resolve: { @@ -104,12 +96,12 @@ module.exports = { "bn.js" ), "original-fs": path.join(__dirname, "./nil.js"), - "scrypt": "js-scrypt" - } + "scrypt": "js-scrypt", + }, }, stats: { - warnings: false + warnings: false, }, plugins: [ @@ -118,7 +110,6 @@ module.exports = { BUNDLE_CHAIN_FILENAME: JSON.stringify("chain.bundled.js"), BUNDLE_ANALYTICS_FILENAME: JSON.stringify("analytics.bundled.js"), BUNDLE_LIBRARY_FILENAME: JSON.stringify("library.bundled.js"), - BUNDLE_CONSOLE_CHILD_FILENAME: JSON.stringify("consoleChild.bundled.js") }), // Put the shebang back on. @@ -137,7 +128,7 @@ module.exports = { "init", "initSource" ), - to: "initSource" + to: "initSource", }, { from: path.join( @@ -148,7 +139,7 @@ module.exports = { "lib", "testing", "Assert.sol" - ) + ), }, { from: path.join( @@ -159,7 +150,7 @@ module.exports = { "lib", "testing", "AssertAddress.sol" - ) + ), }, { from: path.join( @@ -170,7 +161,7 @@ module.exports = { "lib", "testing", "AssertAddressArray.sol" - ) + ), }, { from: path.join( @@ -181,7 +172,7 @@ module.exports = { "lib", "testing", "AssertAddressPayableArray.sol" - ) + ), }, { from: path.join( @@ -192,7 +183,7 @@ module.exports = { "lib", "testing", "AssertBalance.sol" - ) + ), }, { from: path.join( @@ -203,7 +194,7 @@ module.exports = { "lib", "testing", "AssertBool.sol" - ) + ), }, { from: path.join( @@ -214,7 +205,7 @@ module.exports = { "lib", "testing", "AssertBytes32.sol" - ) + ), }, { from: path.join( @@ -225,7 +216,7 @@ module.exports = { "lib", "testing", "AssertBytes32Array.sol" - ) + ), }, { from: path.join( @@ -236,7 +227,7 @@ module.exports = { "lib", "testing", "AssertGeneral.sol" - ) + ), }, { from: path.join( @@ -247,7 +238,7 @@ module.exports = { "lib", "testing", "AssertInt.sol" - ) + ), }, { from: path.join( @@ -258,7 +249,7 @@ module.exports = { "lib", "testing", "AssertIntArray.sol" - ) + ), }, { from: path.join( @@ -269,7 +260,7 @@ module.exports = { "lib", "testing", "AssertString.sol" - ) + ), }, { from: path.join( @@ -280,7 +271,7 @@ module.exports = { "lib", "testing", "AssertUint.sol" - ) + ), }, { from: path.join( @@ -291,7 +282,7 @@ module.exports = { "lib", "testing", "AssertUintArray.sol" - ) + ), }, { from: path.join( @@ -302,7 +293,7 @@ module.exports = { "lib", "testing", "NewSafeSend.sol" - ) + ), }, { from: path.join( @@ -313,7 +304,7 @@ module.exports = { "lib", "testing", "OldSafeSend.sol" - ) + ), }, { from: path.join( @@ -327,13 +318,13 @@ module.exports = { "templates/" ), to: "templates", - flatten: true - } + flatten: true, + }, ]), new CleanWebpackPlugin(), // Make web3 1.0 packable - new webpack.IgnorePlugin(/^electron$/) - ] + new webpack.IgnorePlugin(/^electron$/), + ], };