Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add a built-in console task #5592

Merged
merged 13 commits into from
Aug 14, 2024
Merged
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;
Comment on lines +16 to +17
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really interesting! I never thought about it.

}

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);
}
Comment on lines +159 to +166
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a try-catch and a comment about an issue I observed in https://github.com/NomicFoundation/hardhat/actions/runs/10332073748/job/28602920453, for example. Have you seen something similar before? Do you think it's worth ensuring we can always delete the temp dirs in CI? In this particular case I think it might have something to do that the temp dir is on C: while the runner is set up to have perms and execute in the context of drive D:.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's actually sirprising, but I think we can live with it.

Maybe we should move it to the test utils package so that we don't hit this edge case again.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created an issue for this - #5602 If I cannot resolve it, I'm going to add the utility function for this.

});

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