Skip to content

Commit 733fa6a

Browse files
authoredJun 24, 2024··
Merge pull request #5409 from NomicFoundation/feat/help-option
Implement the `--help` command
·
2 parents ffc1468 + bb8dfe3 commit 733fa6a

File tree

14 files changed

+1077
-54
lines changed

14 files changed

+1077
-54
lines changed
 

‎pnpm-lock.yaml

+4-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎v-next/core/test/internal/hook-manager.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ describe("HookManager", () => {
9090
plugins: [],
9191
},
9292
hooks: hookManager,
93-
globalArguments: {},
93+
globalOptions: {},
9494
interruptions: userInterruptionsManager,
9595
});
9696

@@ -295,7 +295,7 @@ describe("HookManager", () => {
295295
plugins: [],
296296
},
297297
hooks: hookManager,
298-
globalArguments: {},
298+
globalOptions: {},
299299
interruptions: userInterruptionsManager,
300300
});
301301

@@ -509,7 +509,7 @@ describe("HookManager", () => {
509509
plugins: [],
510510
},
511511
hooks: hookManager,
512-
globalArguments: {},
512+
globalOptions: {},
513513
interruptions: userInterruptionsManager,
514514
});
515515

@@ -634,7 +634,7 @@ describe("HookManager", () => {
634634
plugins: [],
635635
},
636636
hooks: hookManager,
637-
globalArguments: {},
637+
globalOptions: {},
638638
interruptions: userInterruptionsManager,
639639
});
640640

@@ -778,7 +778,7 @@ describe("HookManager", () => {
778778
plugins: [],
779779
},
780780
hooks: hookManager,
781-
globalArguments: {},
781+
globalOptions: {},
782782
interruptions: userInterruptionsManager,
783783
});
784784

@@ -935,7 +935,7 @@ function buildMockHardhatRuntimeEnvironment(
935935
plugins: [],
936936
},
937937
tasks: mockTaskManager,
938-
globalArguments: {},
938+
globalOptions: {},
939939
interruptions: mockInteruptionManager,
940940
};
941941

‎v-next/hardhat/example.config.ts

+43-5
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,51 @@ import {
33
overrideTask,
44
configVariable,
55
task,
6+
emptyTask,
67
} from "./src/config.js";
78

8-
const exampleTaskOverride = overrideTask("example")
9-
.setAction(async (args, _hre, runSuper) => {
9+
const exampleEmptyTask = emptyTask("empty", "An example empty task").build();
10+
11+
const exampleEmptySubtask = task(["empty", "task"])
12+
.setDescription("An example empty subtask task")
13+
.setAction(async (_, _hre) => {
14+
console.log("empty task");
15+
})
16+
.build();
17+
18+
const exampleTaskOverride = task("example2")
19+
.setAction(async (_, _hre) => {
1020
console.log("from an override");
11-
await runSuper(args);
21+
})
22+
.setDescription("An example task")
23+
.addVariadicParameter({
24+
name: "testFiles",
25+
description: "An optional list of files to test",
26+
// defaultValue: [],
27+
})
28+
.addOption({
29+
name: "noCompile",
30+
description: "Don't compile before running this task",
31+
})
32+
.addFlag({
33+
name: "parallel",
34+
description: "Run tests in parallel",
35+
})
36+
.addFlag({
37+
name: "bail",
38+
description: "Stop running tests after the first test failure",
39+
})
40+
.addOption({
41+
name: "grep",
42+
description: "Only run tests matching the given string or regexp",
1243
})
1344
.build();
1445

1546
const testTask = task("test", "Runs mocha tests")
1647
.addVariadicParameter({
1748
name: "testFiles",
1849
description: "An optional list of files to test",
19-
defaultValue: [],
50+
// defaultValue: [],
2051
})
2152
.addOption({
2253
name: "noCompile",
@@ -52,6 +83,13 @@ const testSolidityTask = task(["test", "solidity"], "Runs Solidity tests")
5283
.build();
5384

5485
export default {
55-
tasks: [exampleTaskOverride, testTask, testTaskOverride, testSolidityTask],
86+
tasks: [
87+
exampleTaskOverride,
88+
testTask,
89+
testTaskOverride,
90+
testSolidityTask,
91+
exampleEmptyTask,
92+
exampleEmptySubtask,
93+
],
5694
privateKey: configVariable("privateKey"),
5795
} satisfies HardhatUserConfig;

‎v-next/hardhat/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
"@nomicfoundation/hardhat-errors": "workspace:^3.0.0",
7474
"@nomicfoundation/hardhat-utils": "workspace:^3.0.0",
7575
"@nomicfoundation/hardhat-zod-utils": "workspace:^3.0.0",
76+
"chalk": "^5.3.0",
7677
"tsx": "^4.11.0",
7778
"zod": "^3.23.8"
7879
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { Task } from "@nomicfoundation/hardhat-core/types/tasks";
2+
3+
import { getHardhatVersion } from "../../utils/package.js";
4+
5+
import {
6+
GLOBAL_NAME_PADDING,
7+
GLOBAL_OPTIONS,
8+
getLongestNameLength,
9+
getSection,
10+
parseTasks,
11+
} from "./utils.js";
12+
13+
export async function getGlobalHelpString(
14+
rootTasks: Map<string, Task>,
15+
): Promise<string> {
16+
const version = await getHardhatVersion();
17+
18+
const { tasks, subtasks } = parseTasks(rootTasks);
19+
20+
const namePadding =
21+
getLongestNameLength([...tasks, ...subtasks, ...GLOBAL_OPTIONS]) +
22+
GLOBAL_NAME_PADDING;
23+
24+
let output = `Hardhat version ${version}
25+
26+
Usage: hardhat [GLOBAL OPTIONS] <TASK> [SUBTASK] [TASK OPTIONS] [--] [TASK ARGUMENTS]
27+
`;
28+
29+
if (tasks.length > 0) {
30+
output += getSection("AVAILABLE TASKS", tasks, namePadding);
31+
}
32+
33+
if (subtasks.length > 0) {
34+
output += getSection("AVAILABLE SUBTASKS", subtasks, namePadding);
35+
}
36+
37+
output += getSection("GLOBAL OPTIONS", GLOBAL_OPTIONS, namePadding);
38+
39+
output += `\nTo get help for a specific task run: npx hardhat <TASK> [SUBTASK] --help`;
40+
41+
return output;
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { Task } from "@nomicfoundation/hardhat-core/types/tasks";
2+
3+
import {
4+
GLOBAL_NAME_PADDING,
5+
parseOptions,
6+
getLongestNameLength,
7+
getSection,
8+
parseSubtasks,
9+
getUsageString,
10+
} from "./utils.js";
11+
12+
export async function getHelpString(task: Task): Promise<string> {
13+
const { default: chalk } = await import("chalk");
14+
15+
const { options, positionalArguments } = parseOptions(task);
16+
17+
const subtasks = parseSubtasks(task);
18+
19+
const namePadding =
20+
getLongestNameLength([...options, ...positionalArguments, ...subtasks]) +
21+
GLOBAL_NAME_PADDING;
22+
23+
let output = `${chalk.bold(task.description)}`;
24+
25+
if (task.isEmpty) {
26+
output += `\n\nUsage: hardhat [GLOBAL OPTIONS] ${task.id.join(" ")} <SUBTASK> [SUBTASK OPTIONS] [--] [SUBTASK POSITIONAL ARGUMENTS]\n`;
27+
28+
if (subtasks.length > 0) {
29+
output += getSection("AVAILABLE SUBTASKS", subtasks, namePadding);
30+
31+
output += `\nTo get help for a specific task run: npx hardhat ${task.id.join(" ")} <SUBTASK> --help`;
32+
}
33+
34+
return output;
35+
}
36+
37+
const usage = getUsageString(task, options, positionalArguments);
38+
39+
output += `\n\n${usage}\n`;
40+
41+
if (options.length > 0) {
42+
output += getSection("OPTIONS", options, namePadding);
43+
}
44+
45+
if (positionalArguments.length > 0) {
46+
output += getSection(
47+
"POSITIONAL ARGUMENTS",
48+
positionalArguments,
49+
namePadding,
50+
);
51+
}
52+
53+
if (subtasks.length > 0) {
54+
output += getSection("AVAILABLE SUBTASKS", subtasks, namePadding);
55+
}
56+
57+
output += `\nFor global options help run: hardhat --help`;
58+
59+
return output;
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import type { ParameterType } from "@nomicfoundation/hardhat-core/config";
2+
import type { Task } from "@nomicfoundation/hardhat-core/types/tasks";
3+
4+
export const GLOBAL_OPTIONS = [
5+
{
6+
name: "--config",
7+
description: "A Hardhat config file.",
8+
},
9+
{
10+
name: "--help",
11+
description: "Shows this message, or a task's help if its name is provided",
12+
},
13+
{
14+
name: "--show-stack-traces",
15+
description: "Show stack traces (always enabled on CI servers).",
16+
},
17+
{
18+
name: "--version",
19+
description: "Shows hardhat's version.",
20+
},
21+
];
22+
23+
export const GLOBAL_NAME_PADDING = 6;
24+
25+
export function parseTasks(taskMap: Map<string, Task>): {
26+
tasks: Array<{ name: string; description: string }>;
27+
subtasks: Array<{ name: string; description: string }>;
28+
} {
29+
const tasks = [];
30+
const subtasks = [];
31+
32+
for (const [taskName, task] of taskMap) {
33+
subtasks.push(...parseSubtasks(task));
34+
35+
if (task.isEmpty) {
36+
continue;
37+
}
38+
39+
tasks.push({ name: taskName, description: task.description });
40+
}
41+
42+
return { tasks, subtasks };
43+
}
44+
45+
export function parseSubtasks(
46+
task: Task,
47+
): Array<{ name: string; description: string }> {
48+
const subtasks = [];
49+
50+
for (const [, subtask] of task.subtasks) {
51+
subtasks.push({
52+
name: subtask.id.join(" "),
53+
description: subtask.description,
54+
});
55+
}
56+
57+
return subtasks;
58+
}
59+
60+
export function parseOptions(task: Task): {
61+
options: Array<{ name: string; description: string; type: ParameterType }>;
62+
positionalArguments: Array<{
63+
name: string;
64+
description: string;
65+
isRequired: boolean;
66+
}>;
67+
} {
68+
const options = [];
69+
const positionalArguments = [];
70+
71+
for (const [optionName, option] of task.options) {
72+
options.push({
73+
name: formatOptionName(optionName),
74+
description: option.description,
75+
type: option.parameterType,
76+
});
77+
}
78+
79+
for (const argument of task.positionalParameters) {
80+
positionalArguments.push({
81+
name: argument.name,
82+
description: argument.description,
83+
isRequired: argument.defaultValue === undefined,
84+
});
85+
}
86+
87+
return { options, positionalArguments };
88+
}
89+
90+
export function formatOptionName(str: string): string {
91+
return `--${str
92+
.split("")
93+
.map((letter, idx) => {
94+
return letter.toUpperCase() === letter
95+
? `${idx !== 0 ? "-" : ""}${letter.toLowerCase()}`
96+
: letter;
97+
})
98+
.join("")}`;
99+
}
100+
101+
export function getLongestNameLength(tasks: Array<{ name: string }>): number {
102+
return tasks.reduce((acc, { name }) => Math.max(acc, name.length), 0);
103+
}
104+
105+
export function getSection(
106+
title: string,
107+
items: Array<{ name: string; description: string }>,
108+
namePadding: number,
109+
): string {
110+
return `\n${title}:\n\n${items.map(({ name, description }) => ` ${name.padEnd(namePadding)}${description}`).join("\n")}\n`;
111+
}
112+
113+
export function getUsageString(
114+
task: Task,
115+
options: ReturnType<typeof parseOptions>["options"],
116+
positionalArguments: ReturnType<typeof parseOptions>["positionalArguments"],
117+
): string {
118+
let output = `Usage: hardhat [GLOBAL OPTIONS] ${task.id.join(" ")}`;
119+
120+
if (options.length > 0) {
121+
output += ` ${options.map((o) => `[${o.name}${o.type === "BOOLEAN" ? "" : ` <${o.type}>`}]`).join(" ")}`;
122+
}
123+
124+
if (positionalArguments.length > 0) {
125+
output += ` [--] ${positionalArguments.map((a) => (a.isRequired ? a.name : `[${a.name}]`)).join(" ")}`;
126+
}
127+
128+
return output;
129+
}

‎v-next/hardhat/src/internal/cli/main.ts

+39-15
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import {
3333
} from "../helpers/config-loading.js";
3434
import { getHardhatRuntimeEnvironmentSingleton } from "../hre-singleton.js";
3535

36+
import { getGlobalHelpString } from "./helpers/getGlobalHelpString.js";
37+
import { getHelpString } from "./helpers/getHelpString.js";
3638
import { printVersionMessage } from "./version.js";
3739

3840
export async function main(cliArguments: string[], print = console.log) {
@@ -89,32 +91,37 @@ export async function main(cliArguments: string[], print = console.log) {
8991

9092
const taskParsingStart = performance.now();
9193

92-
const result = parseTaskAndArguments(cliArguments, usedCliArguments, hre);
94+
const taskOrId = parseTask(cliArguments, usedCliArguments, hre);
9395

94-
if (Array.isArray(result)) {
95-
if (result.length === 0) {
96-
// TODO: Print the global help
97-
print("Global help");
96+
if (Array.isArray(taskOrId)) {
97+
if (taskOrId.length === 0) {
98+
const globalHelp = await getGlobalHelpString(hre.tasks.rootTasks);
99+
100+
print(globalHelp);
98101
return;
99102
}
100103

101-
throw new Error(`Unrecognized task ${result.join(" ")}`);
104+
throw new HardhatError(
105+
HardhatError.ERRORS.TASK_DEFINITIONS.TASK_NOT_FOUND,
106+
{ task: taskOrId.join(" ") },
107+
);
102108
}
103109

104-
const { task, taskArguments } = result;
110+
const task = taskOrId;
105111

106112
if (hardhatSpecialArgs.help) {
107-
if (task.isEmpty) {
108-
// TODO: Print information about its subtasks
109-
print("Info about subtasks");
110-
return;
111-
}
113+
const taskHelp = await getHelpString(task);
112114

113-
// TODO: Print the help message for this task
114-
print("Help message of the task");
115+
print(taskHelp);
115116
return;
116117
}
117118

119+
const taskArguments = parseTaskArguments(
120+
cliArguments,
121+
usedCliArguments,
122+
task,
123+
);
124+
118125
const taskParsingEnd = performance.now();
119126

120127
print("Time to parse the task (ms):", taskParsingEnd - taskParsingStart);
@@ -226,12 +233,29 @@ export async function parseGlobalOptions(
226233
return globalOptions;
227234
}
228235

236+
/**
237+
* Parses the task from the cli args.
238+
*
239+
* @returns The task, or an array with the unrecognized task id.
240+
* If no task id is provided, an empty array is returned.
241+
*/
242+
export function parseTask(
243+
cliArguments: string[],
244+
usedCliArguments: boolean[],
245+
hre: HardhatRuntimeEnvironment,
246+
): Task | string[] {
247+
const taskOrId = getTaskFromCliArguments(cliArguments, usedCliArguments, hre);
248+
249+
return taskOrId;
250+
}
251+
229252
/**
230253
* Parses the task id and its arguments.
231254
*
232255
* @returns The task and its arguments, or an array with the unrecognized task
233256
* id. If no task id is provided, an empty array is returned.
234257
*/
258+
// todo: this function isn't used anymore and needs to be removed
235259
export function parseTaskAndArguments(
236260
cliArguments: string[],
237261
usedCliArguments: boolean[],
@@ -242,7 +266,7 @@ export function parseTaskAndArguments(
242266
taskArguments: TaskArguments;
243267
}
244268
| string[] {
245-
const taskOrId = getTaskFromCliArguments(cliArguments, usedCliArguments, hre);
269+
const taskOrId = parseTask(cliArguments, usedCliArguments, hre);
246270
if (Array.isArray(taskOrId)) {
247271
return taskOrId;
248272
}

‎v-next/hardhat/test/fixture-projects/cli/parsing/base-project/hardhat.config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const tasksResults = {
77
};
88

99
const customTask = task("task")
10+
.setDescription("A task that uses param1")
1011
.setAction(() => {
1112
tasksResults.wasParam1Used = true;
1213
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import type { Task } from "@nomicfoundation/hardhat-core/types/tasks";
2+
3+
import assert from "node:assert/strict";
4+
import { describe, it } from "node:test";
5+
6+
import packageJson from "../../../../package.json";
7+
import { getGlobalHelpString } from "../../../../src/internal/cli/helpers/getGlobalHelpString.js";
8+
9+
describe("getGlobalHelpString", function () {
10+
describe("when there are no tasks", function () {
11+
it("should return the global help string", async function () {
12+
const tasks = new Map();
13+
const help = await getGlobalHelpString(tasks);
14+
15+
const expected = `Hardhat version ${packageJson.version}
16+
17+
Usage: hardhat [GLOBAL OPTIONS] <TASK> [SUBTASK] [TASK OPTIONS] [--] [TASK ARGUMENTS]
18+
19+
GLOBAL OPTIONS:
20+
21+
--config A Hardhat config file.
22+
--help Shows this message, or a task's help if its name is provided
23+
--show-stack-traces Show stack traces (always enabled on CI servers).
24+
--version Shows hardhat's version.
25+
26+
To get help for a specific task run: npx hardhat <TASK> [SUBTASK] --help`;
27+
28+
assert.equal(help, expected);
29+
});
30+
});
31+
32+
describe("when there are tasks", function () {
33+
it("should return the global help string with the tasks", async function () {
34+
const tasks: Map<string, Task> = new Map([
35+
[
36+
"task1",
37+
{
38+
id: ["task1"],
39+
description: "task1 description",
40+
actions: [{ pluginId: "task1-plugin-id", action: () => {} }],
41+
options: new Map(),
42+
positionalParameters: [],
43+
pluginId: "task1-plugin-id",
44+
subtasks: new Map(),
45+
isEmpty: false,
46+
run: async () => {},
47+
},
48+
],
49+
[
50+
"task2",
51+
{
52+
id: ["task2"],
53+
description: "task2 description",
54+
actions: [{ pluginId: "task2-plugin-id", action: () => {} }],
55+
options: new Map(),
56+
positionalParameters: [],
57+
pluginId: "task2-plugin-id",
58+
subtasks: new Map(),
59+
isEmpty: false,
60+
run: async () => {},
61+
},
62+
],
63+
]);
64+
65+
const help = await getGlobalHelpString(tasks);
66+
67+
const expected = `Hardhat version ${packageJson.version}
68+
69+
Usage: hardhat [GLOBAL OPTIONS] <TASK> [SUBTASK] [TASK OPTIONS] [--] [TASK ARGUMENTS]
70+
71+
AVAILABLE TASKS:
72+
73+
task1 task1 description
74+
task2 task2 description
75+
76+
GLOBAL OPTIONS:
77+
78+
--config A Hardhat config file.
79+
--help Shows this message, or a task's help if its name is provided
80+
--show-stack-traces Show stack traces (always enabled on CI servers).
81+
--version Shows hardhat's version.
82+
83+
To get help for a specific task run: npx hardhat <TASK> [SUBTASK] --help`;
84+
85+
assert.equal(help, expected);
86+
});
87+
});
88+
89+
describe("when there are subtasks", function () {
90+
it("should return the global help string with the tasks", async function () {
91+
const tasks: Map<string, Task> = new Map([
92+
[
93+
"task1",
94+
{
95+
id: ["task1"],
96+
description: "task1 description",
97+
actions: [{ pluginId: "task1-plugin-id", action: () => {} }],
98+
options: new Map(),
99+
positionalParameters: [],
100+
pluginId: "task1-plugin-id",
101+
subtasks: new Map().set("subtask1", {
102+
id: ["task1", "subtask1"],
103+
description: "subtask1 description",
104+
actions: [{ pluginId: "task1-plugin-id", action: () => {} }],
105+
namedParameters: new Map(),
106+
positionalParameters: [],
107+
pluginId: "task1-plugin-id",
108+
subtasks: new Map(),
109+
isEmpty: false,
110+
run: async () => {},
111+
}),
112+
isEmpty: false,
113+
run: async () => {},
114+
},
115+
],
116+
[
117+
"task2",
118+
{
119+
id: ["task2"],
120+
description: "task2 description",
121+
actions: [{ pluginId: "task2-plugin-id", action: () => {} }],
122+
options: new Map(),
123+
positionalParameters: [],
124+
pluginId: "task2-plugin-id",
125+
subtasks: new Map(),
126+
isEmpty: false,
127+
run: async () => {},
128+
},
129+
],
130+
]);
131+
132+
const help = await getGlobalHelpString(tasks);
133+
134+
const expected = `Hardhat version ${packageJson.version}
135+
136+
Usage: hardhat [GLOBAL OPTIONS] <TASK> [SUBTASK] [TASK OPTIONS] [--] [TASK ARGUMENTS]
137+
138+
AVAILABLE TASKS:
139+
140+
task1 task1 description
141+
task2 task2 description
142+
143+
AVAILABLE SUBTASKS:
144+
145+
task1 subtask1 subtask1 description
146+
147+
GLOBAL OPTIONS:
148+
149+
--config A Hardhat config file.
150+
--help Shows this message, or a task's help if its name is provided
151+
--show-stack-traces Show stack traces (always enabled on CI servers).
152+
--version Shows hardhat's version.
153+
154+
To get help for a specific task run: npx hardhat <TASK> [SUBTASK] --help`;
155+
156+
assert.equal(help, expected);
157+
});
158+
});
159+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import type { Task } from "@nomicfoundation/hardhat-core/types/tasks";
2+
3+
import assert from "node:assert/strict";
4+
import { describe, it } from "node:test";
5+
6+
import { ParameterType } from "@nomicfoundation/hardhat-core/config";
7+
import chalk from "chalk";
8+
9+
import { getHelpString } from "../../../../src/internal/cli/helpers/getHelpString.js";
10+
11+
describe("getHelpString", function () {
12+
describe("when the task is empty", function () {
13+
it("should return the task's help string", async function () {
14+
const task: Task = {
15+
id: ["task"],
16+
description: "task description",
17+
actions: [{ pluginId: "task-plugin-id", action: () => {} }],
18+
options: new Map(),
19+
positionalParameters: [],
20+
pluginId: "task-plugin-id",
21+
subtasks: new Map().set("subtask", {
22+
id: ["task", "subtask"],
23+
description: "An example empty subtask task",
24+
isEmpty: false,
25+
run: async () => {},
26+
}),
27+
isEmpty: true,
28+
run: async () => {},
29+
};
30+
31+
const help = await getHelpString(task);
32+
33+
const expected = `${chalk.bold("task description")}
34+
35+
Usage: hardhat [GLOBAL OPTIONS] task <SUBTASK> [SUBTASK OPTIONS] [--] [SUBTASK POSITIONAL ARGUMENTS]
36+
37+
AVAILABLE SUBTASKS:
38+
39+
task subtask An example empty subtask task
40+
41+
To get help for a specific task run: npx hardhat task <SUBTASK> --help`;
42+
43+
assert.equal(help, expected);
44+
});
45+
});
46+
47+
describe("when the task is not empty", function () {
48+
describe("when there are options", function () {
49+
it("should return the task's help string", async function () {
50+
const task: Task = {
51+
id: ["task"],
52+
description: "task description",
53+
actions: [{ pluginId: "task-plugin-id", action: () => {} }],
54+
options: new Map()
55+
.set("option", {
56+
name: "option",
57+
description: "An example option",
58+
parameterType: "STRING",
59+
})
60+
.set("anotherOption", {
61+
name: "anotherOption",
62+
description: "Another example option",
63+
parameterType: "BOOLEAN",
64+
}),
65+
positionalParameters: [],
66+
pluginId: "task-plugin-id",
67+
subtasks: new Map(),
68+
isEmpty: false,
69+
run: async () => {},
70+
};
71+
72+
const help = await getHelpString(task);
73+
74+
const expected = `${chalk.bold("task description")}
75+
76+
Usage: hardhat [GLOBAL OPTIONS] task [--option <STRING>] [--another-option]
77+
78+
OPTIONS:
79+
80+
--option An example option
81+
--another-option Another example option
82+
83+
For global options help run: hardhat --help`;
84+
85+
assert.equal(help, expected);
86+
});
87+
});
88+
89+
describe("when there are positional arguments", function () {
90+
it("should return the task's help string", async function () {
91+
const task: Task = {
92+
id: ["task"],
93+
description: "task description",
94+
actions: [{ pluginId: "task-plugin-id", action: () => {} }],
95+
options: new Map()
96+
.set("option", {
97+
name: "option",
98+
description: "An example option",
99+
parameterType: "STRING",
100+
})
101+
.set("anotherOption", {
102+
name: "anotherOption",
103+
description: "Another example option",
104+
parameterType: "BOOLEAN",
105+
}),
106+
positionalParameters: [
107+
{
108+
name: "positionalArgument",
109+
description: "An example positional argument",
110+
parameterType: ParameterType.STRING,
111+
isVariadic: false,
112+
},
113+
],
114+
pluginId: "task-plugin-id",
115+
subtasks: new Map(),
116+
isEmpty: false,
117+
run: async () => {},
118+
};
119+
120+
const help = await getHelpString(task);
121+
122+
const expected = `${chalk.bold("task description")}
123+
124+
Usage: hardhat [GLOBAL OPTIONS] task [--option <STRING>] [--another-option] [--] positionalArgument
125+
126+
OPTIONS:
127+
128+
--option An example option
129+
--another-option Another example option
130+
131+
POSITIONAL ARGUMENTS:
132+
133+
positionalArgument An example positional argument
134+
135+
For global options help run: hardhat --help`;
136+
137+
assert.equal(help, expected);
138+
});
139+
});
140+
141+
describe("when there are subtasks", function () {
142+
it("should return the task's help string", async function () {
143+
const task: Task = {
144+
id: ["task"],
145+
description: "task description",
146+
actions: [{ pluginId: "task-plugin-id", action: () => {} }],
147+
options: new Map()
148+
.set("option", {
149+
name: "option",
150+
description: "An example option",
151+
parameterType: "STRING",
152+
})
153+
.set("anotherOption", {
154+
name: "anotherOption",
155+
description: "Another example option",
156+
parameterType: "BOOLEAN",
157+
}),
158+
positionalParameters: [
159+
{
160+
name: "positionalArgument",
161+
description: "An example positional argument",
162+
parameterType: ParameterType.STRING,
163+
isVariadic: false,
164+
},
165+
],
166+
pluginId: "task-plugin-id",
167+
subtasks: new Map().set("subtask", {
168+
id: ["task", "subtask"],
169+
description: "An example subtask",
170+
}),
171+
isEmpty: false,
172+
run: async () => {},
173+
};
174+
175+
const help = await getHelpString(task);
176+
177+
const expected = `${chalk.bold("task description")}
178+
179+
Usage: hardhat [GLOBAL OPTIONS] task [--option <STRING>] [--another-option] [--] positionalArgument
180+
181+
OPTIONS:
182+
183+
--option An example option
184+
--another-option Another example option
185+
186+
POSITIONAL ARGUMENTS:
187+
188+
positionalArgument An example positional argument
189+
190+
AVAILABLE SUBTASKS:
191+
192+
task subtask An example subtask
193+
194+
For global options help run: hardhat --help`;
195+
196+
assert.equal(help, expected);
197+
});
198+
});
199+
});
200+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
import type { Task } from "@nomicfoundation/hardhat-core/types/tasks";
2+
3+
import assert from "node:assert/strict";
4+
import { describe, it } from "node:test";
5+
6+
import { ParameterType } from "@nomicfoundation/hardhat-core/config";
7+
8+
import {
9+
parseTasks,
10+
parseSubtasks,
11+
parseOptions,
12+
formatOptionName,
13+
getLongestNameLength,
14+
getSection,
15+
getUsageString,
16+
} from "../../../../src/internal/cli/helpers/utils.js";
17+
18+
describe("utils", function () {
19+
describe("parseTasks", function () {
20+
it("should return tasks and subtasks", function () {
21+
const task: Task = {
22+
id: ["task"],
23+
description: "task description",
24+
actions: [{ pluginId: "task-plugin-id", action: () => {} }],
25+
options: new Map(),
26+
positionalParameters: [],
27+
pluginId: "task-plugin-id",
28+
subtasks: new Map().set("subtask", {
29+
id: ["task", "subtask"],
30+
description: "An example empty subtask task",
31+
isEmpty: false,
32+
run: async () => {},
33+
}),
34+
isEmpty: false,
35+
run: async () => {},
36+
};
37+
38+
const result = parseTasks(new Map().set("task", task));
39+
40+
assert.deepEqual(result, {
41+
tasks: [{ name: "task", description: "task description" }],
42+
subtasks: [
43+
{
44+
name: "task subtask",
45+
description: "An example empty subtask task",
46+
},
47+
],
48+
});
49+
});
50+
51+
it("should not include empty tasks in the tasks list", function () {
52+
const task: Task = {
53+
id: ["task"],
54+
description: "task description",
55+
actions: [{ pluginId: "task-plugin-id", action: () => {} }],
56+
options: new Map(),
57+
positionalParameters: [],
58+
pluginId: "task-plugin-id",
59+
subtasks: new Map().set("subtask", {
60+
id: ["task", "subtask"],
61+
description: "An example empty subtask task",
62+
isEmpty: false,
63+
run: async () => {},
64+
}),
65+
isEmpty: true,
66+
run: async () => {},
67+
};
68+
69+
const result = parseTasks(new Map().set("task", task));
70+
71+
assert.deepEqual(result, {
72+
tasks: [],
73+
subtasks: [
74+
{
75+
name: "task subtask",
76+
description: "An example empty subtask task",
77+
},
78+
],
79+
});
80+
});
81+
});
82+
83+
describe("parseSubtasks", function () {
84+
it("should return subtasks", function () {
85+
const task: Task = {
86+
id: ["task"],
87+
description: "task description",
88+
actions: [{ pluginId: "task-plugin-id", action: () => {} }],
89+
options: new Map(),
90+
positionalParameters: [],
91+
pluginId: "task-plugin-id",
92+
subtasks: new Map().set("subtask", {
93+
id: ["task", "subtask"],
94+
description: "An example empty subtask task",
95+
isEmpty: false,
96+
run: async () => {},
97+
}),
98+
isEmpty: false,
99+
run: async () => {},
100+
};
101+
102+
const result = parseSubtasks(task);
103+
104+
assert.deepEqual(result, [
105+
{
106+
name: "task subtask",
107+
description: "An example empty subtask task",
108+
},
109+
]);
110+
});
111+
});
112+
113+
describe("parseOptions", function () {
114+
it("should return options and positional arguments", function () {
115+
const task: Task = {
116+
id: ["task"],
117+
description: "task description",
118+
actions: [{ pluginId: "task-plugin-id", action: () => {} }],
119+
options: new Map()
120+
.set("option", {
121+
name: "option",
122+
description: "An example option",
123+
parameterType: ParameterType.STRING,
124+
})
125+
.set("anotherOption", {
126+
name: "anotherOption",
127+
description: "Another example option",
128+
parameterType: ParameterType.BOOLEAN,
129+
}),
130+
positionalParameters: [
131+
{
132+
name: "positionalArgument",
133+
description: "An example argument",
134+
parameterType: ParameterType.STRING,
135+
isVariadic: false,
136+
},
137+
{
138+
name: "anotherPositionalArgument",
139+
description: "Another example argument",
140+
parameterType: ParameterType.STRING,
141+
isVariadic: false,
142+
defaultValue: "default",
143+
},
144+
],
145+
pluginId: "task-plugin-id",
146+
subtasks: new Map(),
147+
isEmpty: false,
148+
run: async () => {},
149+
};
150+
151+
const result = parseOptions(task);
152+
153+
assert.deepEqual(result, {
154+
options: [
155+
{
156+
name: "--option",
157+
description: "An example option",
158+
type: "STRING",
159+
},
160+
{
161+
name: "--another-option",
162+
description: "Another example option",
163+
type: "BOOLEAN",
164+
},
165+
],
166+
positionalArguments: [
167+
{
168+
name: "positionalArgument",
169+
description: "An example argument",
170+
isRequired: true,
171+
},
172+
{
173+
name: "anotherPositionalArgument",
174+
description: "Another example argument",
175+
isRequired: false,
176+
},
177+
],
178+
});
179+
});
180+
});
181+
182+
describe("formatOptionName", function () {
183+
it("should format option names", function () {
184+
assert.equal(formatOptionName("option"), "--option");
185+
assert.equal(formatOptionName("anotherOption"), "--another-option");
186+
});
187+
});
188+
189+
describe("getLongestNameLength", function () {
190+
it("should return the length of the longest name", function () {
191+
assert.equal(
192+
getLongestNameLength([{ name: "name" }, { name: "anotherName" }]),
193+
11,
194+
);
195+
});
196+
});
197+
198+
describe("getSection", function () {
199+
it("should return a section", function () {
200+
const section = getSection(
201+
"Section Title",
202+
[
203+
{ name: "content", description: "content description" },
204+
{ name: "content2", description: "content description2" },
205+
{ name: "content3", description: "content description3" },
206+
],
207+
14,
208+
);
209+
210+
const expected = `
211+
Section Title:
212+
213+
content content description
214+
content2 content description2
215+
content3 content description3
216+
`;
217+
218+
assert.equal(section, expected);
219+
});
220+
});
221+
222+
describe("getUsageString", function () {
223+
describe("with a required positional parameter", function () {
224+
it("should return a usage string", function () {
225+
const task: Task = {
226+
id: ["task"],
227+
description: "task description",
228+
actions: [{ pluginId: "task-plugin-id", action: () => {} }],
229+
options: new Map()
230+
.set("option", {
231+
name: "option",
232+
description: "An example option",
233+
parameterType: ParameterType.STRING,
234+
})
235+
.set("anotherOption", {
236+
name: "anotherOption",
237+
description: "Another example option",
238+
parameterType: ParameterType.BOOLEAN,
239+
}),
240+
positionalParameters: [
241+
{
242+
name: "positionalArgument",
243+
description: "An example argument",
244+
parameterType: ParameterType.STRING,
245+
isVariadic: false,
246+
},
247+
],
248+
pluginId: "task-plugin-id",
249+
subtasks: new Map(),
250+
isEmpty: false,
251+
run: async () => {},
252+
};
253+
254+
const usageString = getUsageString(
255+
task,
256+
[
257+
{
258+
name: "--option",
259+
description: "An example option",
260+
type: ParameterType.STRING,
261+
},
262+
{
263+
name: "--another-option",
264+
description: "Another example option",
265+
type: ParameterType.BOOLEAN,
266+
},
267+
],
268+
[
269+
{
270+
name: "positionalArgument",
271+
description: "An example argument",
272+
isRequired: true,
273+
},
274+
],
275+
);
276+
277+
const expected = `Usage: hardhat [GLOBAL OPTIONS] task [--option <STRING>] [--another-option] [--] positionalArgument`;
278+
279+
assert.equal(usageString, expected);
280+
});
281+
});
282+
283+
describe("with an optional positional parameter", function () {
284+
it("should return a usage string", function () {
285+
const task: Task = {
286+
id: ["task"],
287+
description: "task description",
288+
actions: [{ pluginId: "task-plugin-id", action: () => {} }],
289+
options: new Map()
290+
.set("option", {
291+
name: "option",
292+
description: "An example option",
293+
parameterType: ParameterType.STRING,
294+
})
295+
.set("anotherOption", {
296+
name: "anotherOption",
297+
description: "Another example option",
298+
parameterType: ParameterType.BOOLEAN,
299+
}),
300+
positionalParameters: [
301+
{
302+
name: "positionalArgument",
303+
description: "An example argument",
304+
parameterType: ParameterType.STRING,
305+
isVariadic: false,
306+
},
307+
],
308+
pluginId: "task-plugin-id",
309+
subtasks: new Map(),
310+
isEmpty: false,
311+
run: async () => {},
312+
};
313+
314+
const usageString = getUsageString(
315+
task,
316+
[
317+
{
318+
name: "--option",
319+
description: "An example option",
320+
type: ParameterType.STRING,
321+
},
322+
{
323+
name: "--another-option",
324+
description: "Another example option",
325+
type: ParameterType.BOOLEAN,
326+
},
327+
],
328+
[
329+
{
330+
name: "positionalArgument",
331+
description: "An example argument",
332+
isRequired: false,
333+
},
334+
],
335+
);
336+
337+
const expected = `Usage: hardhat [GLOBAL OPTIONS] task [--option <STRING>] [--another-option] [--] [positionalArgument]`;
338+
339+
assert.equal(usageString, expected);
340+
});
341+
});
342+
});
343+
});

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

+47-26
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
} from "@nomicfoundation/hardhat-core/config";
2222
import { HardhatError } from "@nomicfoundation/hardhat-errors";
2323
import { isCi } from "@nomicfoundation/hardhat-utils/ci";
24+
import chalk from "chalk";
2425

2526
import {
2627
main,
@@ -207,63 +208,83 @@ describe("main", function () {
207208
});
208209
});
209210

210-
// TODO: as soon as the 'help task' is done, this test should be updated
211211
describe("global help", function () {
212212
useFixtureProject("cli/parsing/base-project");
213213

214-
it.todo("should print the global help", async function () {
215-
const lines: string[] = [];
214+
it("should print the global help", async function () {
215+
let lines: string = "";
216216

217217
const command = "npx hardhat";
218218
const cliArguments = command.split(" ").slice(2);
219219

220220
await main(cliArguments, (msg) => {
221-
lines.push(msg);
221+
lines = msg;
222222
});
223223

224-
assert.equal(lines.length, 2);
225-
assert.equal(lines[1], "Global help");
224+
const expected = `Hardhat version 3.0.0
225+
226+
Usage: hardhat [GLOBAL OPTIONS] <TASK> [SUBTASK] [TASK OPTIONS] [--] [TASK ARGUMENTS]
227+
228+
AVAILABLE TASKS:
229+
230+
example Example task
231+
task A task that uses param1
232+
233+
GLOBAL OPTIONS:
234+
235+
--config A Hardhat config file.
236+
--help Shows this message, or a task's help if its name is provided
237+
--show-stack-traces Show stack traces (always enabled on CI servers).
238+
--version Shows hardhat's version.
239+
240+
To get help for a specific task run: npx hardhat <TASK> [SUBTASK] --help`;
241+
242+
assert.equal(lines, expected);
226243
});
227244
});
228245

229-
// TODO: as soon as the 'help task' is done, this test should be updated
230246
describe("subtask help", function () {
231247
useFixtureProject("cli/parsing/subtask-help");
232248

233-
it.todo(
234-
"should print an help message for the task's subtask",
235-
async function () {
236-
const lines: string[] = [];
249+
it("should print an help message for the task's subtask", async function () {
250+
let lines: string = "";
237251

238-
const command = "npx hardhat empty-task --help";
239-
const cliArguments = command.split(" ").slice(2);
252+
const command = "npx hardhat empty-task --help";
253+
const cliArguments = command.split(" ").slice(2);
240254

241-
await main(cliArguments, (msg) => {
242-
lines.push(msg);
243-
});
255+
await main(cliArguments, (msg) => {
256+
lines = msg;
257+
});
244258

245-
assert.equal(lines.length, 2);
246-
assert.equal(lines[1], "Info about subtasks");
247-
},
248-
);
259+
const expected = `${chalk.bold("empty task description")}
260+
261+
Usage: hardhat [GLOBAL OPTIONS] empty-task <SUBTASK> [SUBTASK OPTIONS] [--] [SUBTASK POSITIONAL ARGUMENTS]
262+
`;
263+
264+
assert.equal(lines, expected);
265+
});
249266
});
250267

251-
// TODO: as soon as the 'help task' is done, this test should be updated
252268
describe("task help", function () {
253269
useFixtureProject("cli/parsing/base-project");
254270

255-
it.todo("should print an help message for the task", async function () {
256-
const lines: string[] = [];
271+
it("should print an help message for the task", async function () {
272+
let lines: string = "";
257273

258274
const command = "npx hardhat task --help";
259275
const cliArguments = command.split(" ").slice(2);
260276

261277
await main(cliArguments, (msg) => {
262-
lines.push(msg);
278+
lines = msg;
263279
});
264280

265-
assert.equal(lines.length, 2);
266-
assert.equal(lines[1], "Help message of the task");
281+
const expected = `${chalk.bold("A task that uses param1")}
282+
283+
Usage: hardhat [GLOBAL OPTIONS] task
284+
285+
For global options help run: hardhat --help`;
286+
287+
assert.equal(lines, expected);
267288
});
268289
});
269290
});

‎v-next/hardhat/tsconfig.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
"compilerOptions": {
44
"outDir": "./dist",
55
"composite": true,
6-
"incremental": true
6+
"incremental": true,
7+
"resolveJsonModule": true
78
},
9+
"include": ["**/*", "./package.json"],
810
"exclude": [
911
"./dist",
1012
"./node_modules",

0 commit comments

Comments
 (0)
Please sign in to comment.