Skip to content

Commit

Permalink
Merge pull request #5592 from NomicFoundation/galargh/console
Browse files Browse the repository at this point in the history
feat: add a built-in console task
  • Loading branch information
galargh authored Aug 14, 2024
2 parents f140e2d + 1870719 commit 98e98bf
Show file tree
Hide file tree
Showing 6 changed files with 306 additions and 2 deletions.
28 changes: 28 additions & 0 deletions v-next/hardhat/src/internal/builtin-plugins/console/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { HardhatPlugin } from "@ignored/hardhat-vnext-core/types/plugins";

import { task } from "@ignored/hardhat-vnext-core/config";

const hardhatPlugin: HardhatPlugin = {
id: "console",
tasks: [
task("console", "Opens a hardhat console")
.setAction(import.meta.resolve("./task-action.js"))
.addOption({
name: "history",
description: "Path to a history file",
defaultValue: "console-history.txt",
})
.addFlag({
name: "noCompile",
description: "Don't compile before running this task",
})
.addVariadicArgument({
name: "commands",
description: "Commands to run in the console",
defaultValue: [".help"],
})
.build(),
],
};

export default hardhatPlugin;
76 changes: 76 additions & 0 deletions v-next/hardhat/src/internal/builtin-plugins/console/task-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { NewTaskActionFunction } from "@ignored/hardhat-vnext-core/types/tasks";
import type { REPLServer } from "node:repl";

import path from "node:path";
import repl from "node:repl";

import { getCacheDir } from "@ignored/hardhat-vnext-core/global-dir";
import debug from "debug";

const log = debug("hardhat:core:tasks:console");

interface ConsoleActionArguments {
commands: string[];
history: string;
noCompile: boolean;
// We accept ReplOptions as an argument to allow tests overriding the IO streams
options?: repl.ReplOptions;
}

const consoleAction: NewTaskActionFunction<ConsoleActionArguments> = async (
{ commands, history, noCompile, options },
hre,
) => {
// Resolve the history path if it is not empty
let historyPath: string | undefined;
if (history !== "") {
// TODO(#5599): Replace with hre.config.paths.cache once it is available
const cacheDir = await getCacheDir();
historyPath = path.isAbsolute(history)
? history
: path.resolve(cacheDir, history);
}

// If noCompile is false, run the compile task first
if (!noCompile) {
// TODO(#5600): run compile task
}

return new Promise<REPLServer>(async (resolve) => {
// Start a new REPL server with the default options
const replServer = repl.start(options);

// Resolve the task action promise only when the REPL server exits
replServer.on("exit", () => {
resolve(replServer);
});

// Add the Hardhat Runtime Environment to the REPL context
replServer.context.hre = hre;
replServer.context.config = hre.config;
replServer.context.tasks = hre.tasks;
replServer.context.globalOptions = hre.globalOptions;
replServer.context.hooks = hre.hooks;
replServer.context.interruptions = hre.interruptions;

// Set up the REPL history file if the historyPath has been set
if (historyPath !== undefined) {
await new Promise<void>((resolveSetupHistory) => {
replServer.setupHistory(historyPath, (err: Error | null) => {
// Fail silently if the history file cannot be set up
if (err !== null) {
log("Failed to setup REPL history", err);
}
resolveSetupHistory();
});
});
}

// Execute each command in the REPL server
for (const command of commands) {
replServer.write(`${command}\n`);
}
});
};

export default consoleAction;
9 changes: 8 additions & 1 deletion v-next/hardhat/src/internal/builtin-plugins/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import type { HardhatPlugin } from "@ignored/hardhat-vnext-core/types/plugins";

import clean from "./clean/index.js";
import console from "./console/index.js";
import hardhatFoo from "./hardhat-foo/index.js";
import run from "./run/index.js";

// Note: When importing a plugin, you have to export its types, so that its
// type extensions, if any, also get loaded.
export type * from "./clean/index.js";
export type * from "./console/index.js";
export type * from "./hardhat-foo/index.js";
export type * from "./run/index.js";

export const builtinPlugins: HardhatPlugin[] = [clean, hardhatFoo, run];
export const builtinPlugins: HardhatPlugin[] = [
clean,
console,
hardhatFoo,
run,
];
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const runScriptWithHardhat: NewTaskActionFunction<RunActionArguments> = async (
}

if (!noCompile) {
// todo: run compile task
// TODO(#5600): run compile task
}

try {
Expand Down
192 changes: 192 additions & 0 deletions v-next/hardhat/test/internal/builtin-plugins/console/task-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import type { HardhatRuntimeEnvironment } from "@ignored/hardhat-vnext-core/types/hre";
import type repl from "node:repl";

import assert from "node:assert/strict";
import fsPromises from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { PassThrough } from "node:stream";
import { afterEach, before, beforeEach, describe, it } from "node:test";

import { ensureError } from "@ignored/hardhat-vnext-utils/error";
import { exists, remove } from "@ignored/hardhat-vnext-utils/fs";
import debug from "debug";

import { createHardhatRuntimeEnvironment } from "../../../../src/hre.js";
import consoleAction from "../../../../src/internal/builtin-plugins/console/task-action.js";
import { useFixtureProject } from "../../../helpers/project.js";

const log = debug("hardhat:test:console:task-action");

describe("console/task-action", function () {
let hre: HardhatRuntimeEnvironment;
let options: repl.ReplOptions;

before(async function () {
hre = await createHardhatRuntimeEnvironment({});
});

beforeEach(function () {
// Using process.stdin for the input during tests is not reliable as it
// causes the test runner to hang indefinitely. We use a PassThrough stream
// instead. This, in turn, prevents us from using process.stdout for output.
// Hence, we use a PassThrough stream for output as well.
const input = new PassThrough();
const output = new PassThrough();
output.pipe(process.stdout);
options = {
input,
output,
};
});

describe("javascript", function () {
useFixtureProject("run-js-script");

it("should throw inside the console if script does not exist", async function () {
const replServer = await consoleAction(
{
commands: ['await import("./scripts/non-existent.js");', ".exit"],
history: "",
noCompile: false,
options,
},
hre,
);
ensureError(replServer.lastError);
});

it("should run a script inside the console successfully", async function () {
const replServer = await consoleAction(
{
commands: [".help", 'await import("./scripts/success.js");', ".exit"],
history: "",
noCompile: false,
options,
},
hre,
);
assert.equal(replServer.lastError, undefined);
});

it("should throw inside the console if the script throws", async function () {
const replServer = await consoleAction(
{
commands: ['await import("./scripts/throws.js");', ".exit"],
history: "",
noCompile: false,
options,
},
hre,
);
ensureError(replServer.lastError);
});
});

describe("typescript", function () {
useFixtureProject("run-ts-script");

it("should throw inside the console if script does not exist", async function () {
const replServer = await consoleAction(
{
commands: ['await import("./scripts/non-existent.ts");', ".exit"],
history: "",
noCompile: false,
options,
},
hre,
);
ensureError(replServer.lastError);
});

it("should run a script inside the console successfully", async function () {
const replServer = await consoleAction(
{
commands: ['await import("./scripts/success.ts");', ".exit"],
history: "",
noCompile: false,
options,
},
hre,
);
assert.equal(replServer.lastError, undefined);
});

it("should throw inside the console if the script throws", async function () {
const replServer = await consoleAction(
{
commands: ['await import("./scripts/throws.ts");', ".exit"],
history: "",
noCompile: false,
options,
},
hre,
);
ensureError(replServer.lastError);
});
});

describe("context", function () {
it("should expose the Hardhat Runtime Environment", async function () {
const replServer = await consoleAction(
{
commands: ["console.log(hre);", ".exit"],
history: "",
noCompile: false,
options,
},
hre,
);
assert.equal(replServer.lastError, undefined);
});
});

describe("history", function () {
let cacheDir: string;
let history: string;

beforeEach(async function () {
// TODO(#5601): Use the mkdtemp from hardhat-utils once it's available
// We use a temporary cache dir to avoid conflicts with other tests
// and global user settings.
cacheDir = await fsPromises.mkdtemp(
path.resolve(os.tmpdir(), "console-action-test-"),
);
history = path.resolve(cacheDir, "console-history.txt");
});

afterEach(async function () {
// We try to remove the temporary cache dir after each test, but we don't
// fail the test if it fails. For example, we have observed that in GHA
// on Windows, the temp dir cannot be removed due to permission issues.
try {
await remove(cacheDir);
} catch (error) {
log("Failed to remove temporary cache dir", error);
}
});

it("should create a history file", async function () {
let historyExists = await exists(history);
assert.ok(
!historyExists,
"History file exists before running the console",
);
const replServer = await consoleAction(
{
commands: [".help", ".exit"],
history,
noCompile: false,
options,
},
hre,
);
assert.equal(replServer.lastError, undefined);
historyExists = await exists(history);
assert.ok(
historyExists,
"History file does not exist after running the console",
);
});
});
});
1 change: 1 addition & 0 deletions v-next/hardhat/test/internal/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ Usage: hardhat [GLOBAL OPTIONS] <TASK> [SUBTASK] [TASK OPTIONS] [--] [TASK ARGUM
AVAILABLE TASKS:
clean Clears the cache and deletes all artifacts
console Opens a hardhat console
example Example task
run Runs a user-defined script after compiling the project
task A task that uses arg1
Expand Down

0 comments on commit 98e98bf

Please sign in to comment.