Skip to content

Commit 081de2d

Browse files
committed
add --help command
1 parent 2a364d1 commit 081de2d

9 files changed

+325
-29
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/hardhat/example.config.ts

+46-12
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,49 @@
1-
import {
2-
HardhatUserConfig,
3-
configVariable,
4-
overrideTask,
5-
task,
6-
} from "./src/config.js";
7-
8-
const exampleTaskOverride = overrideTask("example")
9-
.setAction(async (args, _hre, runSuper) => {
1+
import type { HardhatUserConfig } from "./src/config.js";
2+
3+
import { overrideTask, task, configVariable, emptyTask } from "./src/config.js";
4+
5+
const exampleEmptyTask = emptyTask("empty", "An example empty task").build();
6+
7+
const exampleEmptySubtask = task(["empty", "task"])
8+
.setDescription("An example empty subtask task")
9+
.setAction(async (_, _hre) => {
10+
console.log("empty task");
11+
})
12+
.build();
13+
14+
const exampleTaskOverride = task("example2")
15+
.setAction(async (_, _hre) => {
1016
console.log("from an override");
11-
await runSuper(args);
17+
})
18+
.setDescription("An example task")
19+
.addVariadicParameter({
20+
name: "testFiles",
21+
description: "An optional list of files to test",
22+
// defaultValue: [],
23+
})
24+
.addNamedParameter({
25+
name: "noCompile",
26+
description: "Don't compile before running this task",
27+
})
28+
.addFlag({
29+
name: "parallel",
30+
description: "Run tests in parallel",
31+
})
32+
.addFlag({
33+
name: "bail",
34+
description: "Stop running tests after the first test failure",
35+
})
36+
.addNamedParameter({
37+
name: "grep",
38+
description: "Only run tests matching the given string or regexp",
1239
})
1340
.build();
1441

1542
const testTask = task("test", "Runs mocha tests")
1643
.addVariadicParameter({
1744
name: "testFiles",
1845
description: "An optional list of files to test",
19-
defaultValue: [],
46+
// defaultValue: [],
2047
})
2148
.addNamedParameter({
2249
name: "noCompile",
@@ -52,6 +79,13 @@ const testSolidityTask = task(["test", "solidity"], "Runs Solidity tests")
5279
.build();
5380

5481
export default {
55-
tasks: [exampleTaskOverride, testTask, testTaskOverride, testSolidityTask],
82+
tasks: [
83+
exampleTaskOverride,
84+
testTask,
85+
testTaskOverride,
86+
testSolidityTask,
87+
exampleEmptyTask,
88+
exampleEmptySubtask,
89+
],
5690
privateKey: configVariable("privateKey"),
5791
} 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,41 @@
1+
import type { Task } from "@nomicfoundation/hardhat-core/types/tasks";
2+
3+
import { getVersionString } from "./getVersionString.js";
4+
import {
5+
GLOBAL_NAME_PADDING,
6+
GLOBAL_OPTIONS,
7+
getLongestNameLength,
8+
getSection,
9+
parseTasks,
10+
} from "./utils.js";
11+
12+
export async function getGlobalHelpString(
13+
rootTasks: Map<string, Task>,
14+
): Promise<string> {
15+
const version = await getVersionString();
16+
17+
const { tasks, subtasks } = parseTasks(rootTasks);
18+
19+
const namePadding =
20+
getLongestNameLength([...tasks, ...subtasks, ...GLOBAL_OPTIONS]) +
21+
GLOBAL_NAME_PADDING;
22+
23+
let output = `Hardhat version ${version}
24+
25+
Usage: hardhat [GLOBAL OPTIONS] <TASK> [SUBTASK] [TASK OPTIONS] [--] [TASK ARGUMENTS]
26+
`;
27+
28+
if (tasks.length > 0) {
29+
output += getSection("AVAILABLE TASKS", tasks, namePadding);
30+
}
31+
32+
if (subtasks.length > 0) {
33+
output += getSection("AVAILABLE SUBTASKS", subtasks, namePadding);
34+
}
35+
36+
output += getSection("GLOBAL OPTIONS", GLOBAL_OPTIONS, namePadding);
37+
38+
output += `\nTo get help for a specific task run: npx hardhat <TASK> [SUBTASK] --help`;
39+
40+
return output;
41+
}
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,7 @@
1+
export async function getVersionString(): Promise<string> {
2+
const { default: packageJson } = await import("../../../../package.json", {
3+
with: { type: "json" },
4+
});
5+
6+
return packageJson.version;
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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<{ name: string; description: string }>;
63+
} {
64+
const options = [];
65+
const positionalArguments = [];
66+
67+
for (const [optionName, option] of task.namedParameters) {
68+
options.push({
69+
name: formatOptionName(optionName),
70+
description: option.description,
71+
type: option.parameterType,
72+
});
73+
}
74+
75+
for (const argument of task.positionalParameters) {
76+
positionalArguments.push({
77+
name: argument.name,
78+
description: argument.description,
79+
});
80+
}
81+
82+
return { options, positionalArguments };
83+
}
84+
85+
export function formatOptionName(str: string): string {
86+
return `--${str
87+
.split("")
88+
.map((letter, idx) => {
89+
return letter.toUpperCase() === letter
90+
? `${idx !== 0 ? "-" : ""}${letter.toLowerCase()}`
91+
: letter;
92+
})
93+
.join("")}`;
94+
}
95+
96+
export function getLongestNameLength(tasks: Array<{ name: string }>): number {
97+
return tasks.reduce((acc, { name }) => Math.max(acc, name.length), 0);
98+
}
99+
100+
export function getSection(
101+
title: string,
102+
items: Array<{ name: string; description: string }>,
103+
namePadding: number,
104+
): string {
105+
return `\n${title}:\n\n${items.map(({ name, description }) => ` ${name.padEnd(namePadding)}${description}`).join("\n")}\n`;
106+
}
107+
108+
export function getUsageString(
109+
task: Task,
110+
options: ReturnType<typeof parseOptions>["options"],
111+
positionalArguments: ReturnType<typeof parseOptions>["positionalArguments"],
112+
): string {
113+
let output = `Usage: hardhat [GLOBAL OPTIONS] ${task.id.join(" ")}`;
114+
115+
if (options.length > 0) {
116+
output += ` ${options.map((o) => `[${o.name}${o.type === "BOOLEAN" ? "" : ` <${o.type}>`}]`).join(" ")}`;
117+
}
118+
119+
if (positionalArguments.length > 0) {
120+
output += ` [--] ${positionalArguments.map((a) => a.name).join(" ")}`;
121+
}
122+
123+
return output;
124+
}

0 commit comments

Comments
 (0)