Skip to content

Commit 98e98bf

Browse files
authored
Merge pull request #5592 from NomicFoundation/galargh/console
feat: add a built-in console task
2 parents f140e2d + 1870719 commit 98e98bf

File tree

6 files changed

+306
-2
lines changed

6 files changed

+306
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { HardhatPlugin } from "@ignored/hardhat-vnext-core/types/plugins";
2+
3+
import { task } from "@ignored/hardhat-vnext-core/config";
4+
5+
const hardhatPlugin: HardhatPlugin = {
6+
id: "console",
7+
tasks: [
8+
task("console", "Opens a hardhat console")
9+
.setAction(import.meta.resolve("./task-action.js"))
10+
.addOption({
11+
name: "history",
12+
description: "Path to a history file",
13+
defaultValue: "console-history.txt",
14+
})
15+
.addFlag({
16+
name: "noCompile",
17+
description: "Don't compile before running this task",
18+
})
19+
.addVariadicArgument({
20+
name: "commands",
21+
description: "Commands to run in the console",
22+
defaultValue: [".help"],
23+
})
24+
.build(),
25+
],
26+
};
27+
28+
export default hardhatPlugin;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import type { NewTaskActionFunction } from "@ignored/hardhat-vnext-core/types/tasks";
2+
import type { REPLServer } from "node:repl";
3+
4+
import path from "node:path";
5+
import repl from "node:repl";
6+
7+
import { getCacheDir } from "@ignored/hardhat-vnext-core/global-dir";
8+
import debug from "debug";
9+
10+
const log = debug("hardhat:core:tasks:console");
11+
12+
interface ConsoleActionArguments {
13+
commands: string[];
14+
history: string;
15+
noCompile: boolean;
16+
// We accept ReplOptions as an argument to allow tests overriding the IO streams
17+
options?: repl.ReplOptions;
18+
}
19+
20+
const consoleAction: NewTaskActionFunction<ConsoleActionArguments> = async (
21+
{ commands, history, noCompile, options },
22+
hre,
23+
) => {
24+
// Resolve the history path if it is not empty
25+
let historyPath: string | undefined;
26+
if (history !== "") {
27+
// TODO(#5599): Replace with hre.config.paths.cache once it is available
28+
const cacheDir = await getCacheDir();
29+
historyPath = path.isAbsolute(history)
30+
? history
31+
: path.resolve(cacheDir, history);
32+
}
33+
34+
// If noCompile is false, run the compile task first
35+
if (!noCompile) {
36+
// TODO(#5600): run compile task
37+
}
38+
39+
return new Promise<REPLServer>(async (resolve) => {
40+
// Start a new REPL server with the default options
41+
const replServer = repl.start(options);
42+
43+
// Resolve the task action promise only when the REPL server exits
44+
replServer.on("exit", () => {
45+
resolve(replServer);
46+
});
47+
48+
// Add the Hardhat Runtime Environment to the REPL context
49+
replServer.context.hre = hre;
50+
replServer.context.config = hre.config;
51+
replServer.context.tasks = hre.tasks;
52+
replServer.context.globalOptions = hre.globalOptions;
53+
replServer.context.hooks = hre.hooks;
54+
replServer.context.interruptions = hre.interruptions;
55+
56+
// Set up the REPL history file if the historyPath has been set
57+
if (historyPath !== undefined) {
58+
await new Promise<void>((resolveSetupHistory) => {
59+
replServer.setupHistory(historyPath, (err: Error | null) => {
60+
// Fail silently if the history file cannot be set up
61+
if (err !== null) {
62+
log("Failed to setup REPL history", err);
63+
}
64+
resolveSetupHistory();
65+
});
66+
});
67+
}
68+
69+
// Execute each command in the REPL server
70+
for (const command of commands) {
71+
replServer.write(`${command}\n`);
72+
}
73+
});
74+
};
75+
76+
export default consoleAction;
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import type { HardhatPlugin } from "@ignored/hardhat-vnext-core/types/plugins";
22

33
import clean from "./clean/index.js";
4+
import console from "./console/index.js";
45
import hardhatFoo from "./hardhat-foo/index.js";
56
import run from "./run/index.js";
67

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

13-
export const builtinPlugins: HardhatPlugin[] = [clean, hardhatFoo, run];
15+
export const builtinPlugins: HardhatPlugin[] = [
16+
clean,
17+
console,
18+
hardhatFoo,
19+
run,
20+
];

v-next/hardhat/src/internal/builtin-plugins/run/task-action.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const runScriptWithHardhat: NewTaskActionFunction<RunActionArguments> = async (
2727
}
2828

2929
if (!noCompile) {
30-
// todo: run compile task
30+
// TODO(#5600): run compile task
3131
}
3232

3333
try {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import type { HardhatRuntimeEnvironment } from "@ignored/hardhat-vnext-core/types/hre";
2+
import type repl from "node:repl";
3+
4+
import assert from "node:assert/strict";
5+
import fsPromises from "node:fs/promises";
6+
import os from "node:os";
7+
import path from "node:path";
8+
import { PassThrough } from "node:stream";
9+
import { afterEach, before, beforeEach, describe, it } from "node:test";
10+
11+
import { ensureError } from "@ignored/hardhat-vnext-utils/error";
12+
import { exists, remove } from "@ignored/hardhat-vnext-utils/fs";
13+
import debug from "debug";
14+
15+
import { createHardhatRuntimeEnvironment } from "../../../../src/hre.js";
16+
import consoleAction from "../../../../src/internal/builtin-plugins/console/task-action.js";
17+
import { useFixtureProject } from "../../../helpers/project.js";
18+
19+
const log = debug("hardhat:test:console:task-action");
20+
21+
describe("console/task-action", function () {
22+
let hre: HardhatRuntimeEnvironment;
23+
let options: repl.ReplOptions;
24+
25+
before(async function () {
26+
hre = await createHardhatRuntimeEnvironment({});
27+
});
28+
29+
beforeEach(function () {
30+
// Using process.stdin for the input during tests is not reliable as it
31+
// causes the test runner to hang indefinitely. We use a PassThrough stream
32+
// instead. This, in turn, prevents us from using process.stdout for output.
33+
// Hence, we use a PassThrough stream for output as well.
34+
const input = new PassThrough();
35+
const output = new PassThrough();
36+
output.pipe(process.stdout);
37+
options = {
38+
input,
39+
output,
40+
};
41+
});
42+
43+
describe("javascript", function () {
44+
useFixtureProject("run-js-script");
45+
46+
it("should throw inside the console if script does not exist", async function () {
47+
const replServer = await consoleAction(
48+
{
49+
commands: ['await import("./scripts/non-existent.js");', ".exit"],
50+
history: "",
51+
noCompile: false,
52+
options,
53+
},
54+
hre,
55+
);
56+
ensureError(replServer.lastError);
57+
});
58+
59+
it("should run a script inside the console successfully", async function () {
60+
const replServer = await consoleAction(
61+
{
62+
commands: [".help", 'await import("./scripts/success.js");', ".exit"],
63+
history: "",
64+
noCompile: false,
65+
options,
66+
},
67+
hre,
68+
);
69+
assert.equal(replServer.lastError, undefined);
70+
});
71+
72+
it("should throw inside the console if the script throws", async function () {
73+
const replServer = await consoleAction(
74+
{
75+
commands: ['await import("./scripts/throws.js");', ".exit"],
76+
history: "",
77+
noCompile: false,
78+
options,
79+
},
80+
hre,
81+
);
82+
ensureError(replServer.lastError);
83+
});
84+
});
85+
86+
describe("typescript", function () {
87+
useFixtureProject("run-ts-script");
88+
89+
it("should throw inside the console if script does not exist", async function () {
90+
const replServer = await consoleAction(
91+
{
92+
commands: ['await import("./scripts/non-existent.ts");', ".exit"],
93+
history: "",
94+
noCompile: false,
95+
options,
96+
},
97+
hre,
98+
);
99+
ensureError(replServer.lastError);
100+
});
101+
102+
it("should run a script inside the console successfully", async function () {
103+
const replServer = await consoleAction(
104+
{
105+
commands: ['await import("./scripts/success.ts");', ".exit"],
106+
history: "",
107+
noCompile: false,
108+
options,
109+
},
110+
hre,
111+
);
112+
assert.equal(replServer.lastError, undefined);
113+
});
114+
115+
it("should throw inside the console if the script throws", async function () {
116+
const replServer = await consoleAction(
117+
{
118+
commands: ['await import("./scripts/throws.ts");', ".exit"],
119+
history: "",
120+
noCompile: false,
121+
options,
122+
},
123+
hre,
124+
);
125+
ensureError(replServer.lastError);
126+
});
127+
});
128+
129+
describe("context", function () {
130+
it("should expose the Hardhat Runtime Environment", async function () {
131+
const replServer = await consoleAction(
132+
{
133+
commands: ["console.log(hre);", ".exit"],
134+
history: "",
135+
noCompile: false,
136+
options,
137+
},
138+
hre,
139+
);
140+
assert.equal(replServer.lastError, undefined);
141+
});
142+
});
143+
144+
describe("history", function () {
145+
let cacheDir: string;
146+
let history: string;
147+
148+
beforeEach(async function () {
149+
// TODO(#5601): Use the mkdtemp from hardhat-utils once it's available
150+
// We use a temporary cache dir to avoid conflicts with other tests
151+
// and global user settings.
152+
cacheDir = await fsPromises.mkdtemp(
153+
path.resolve(os.tmpdir(), "console-action-test-"),
154+
);
155+
history = path.resolve(cacheDir, "console-history.txt");
156+
});
157+
158+
afterEach(async function () {
159+
// We try to remove the temporary cache dir after each test, but we don't
160+
// fail the test if it fails. For example, we have observed that in GHA
161+
// on Windows, the temp dir cannot be removed due to permission issues.
162+
try {
163+
await remove(cacheDir);
164+
} catch (error) {
165+
log("Failed to remove temporary cache dir", error);
166+
}
167+
});
168+
169+
it("should create a history file", async function () {
170+
let historyExists = await exists(history);
171+
assert.ok(
172+
!historyExists,
173+
"History file exists before running the console",
174+
);
175+
const replServer = await consoleAction(
176+
{
177+
commands: [".help", ".exit"],
178+
history,
179+
noCompile: false,
180+
options,
181+
},
182+
hre,
183+
);
184+
assert.equal(replServer.lastError, undefined);
185+
historyExists = await exists(history);
186+
assert.ok(
187+
historyExists,
188+
"History file does not exist after running the console",
189+
);
190+
});
191+
});
192+
});

v-next/hardhat/test/internal/cli/main.ts

+1
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ Usage: hardhat [GLOBAL OPTIONS] <TASK> [SUBTASK] [TASK OPTIONS] [--] [TASK ARGUM
223223
AVAILABLE TASKS:
224224
225225
clean Clears the cache and deletes all artifacts
226+
console Opens a hardhat console
226227
example Example task
227228
run Runs a user-defined script after compiling the project
228229
task A task that uses arg1

0 commit comments

Comments
 (0)