diff --git a/.changeset/short-penguins-cross.md b/.changeset/short-penguins-cross.md new file mode 100644 index 00000000000..bd9558fe507 --- /dev/null +++ b/.changeset/short-penguins-cross.md @@ -0,0 +1,6 @@ +--- +"@nomicfoundation/hardhat-errors": patch +"hardhat": patch +--- + +Add ability for task options to be hidden from the CLI ([#7426](https://github.com/NomicFoundation/hardhat/issues/7426)) diff --git a/v-next/example-project/hardhat.config.ts b/v-next/example-project/hardhat.config.ts index 675a1e83e9a..33225f85c34 100644 --- a/v-next/example-project/hardhat.config.ts +++ b/v-next/example-project/hardhat.config.ts @@ -69,6 +69,13 @@ const greeting = task("hello", "Print a greeting") description: "The greeting to print", defaultValue: "Hello, World!", }) + .addOption({ + name: "programmatic", + description: "An example to show a hidden option", + type: ArgumentType.BOOLEAN, + defaultValue: false, + hidden: true, + }) .setAction(async () => ({ default: async ({ greeting }, _) => { console.log(greeting); diff --git a/v-next/hardhat-errors/src/descriptors.ts b/v-next/hardhat-errors/src/descriptors.ts index cc3089faa67..6cb479a1b67 100644 --- a/v-next/hardhat-errors/src/descriptors.ts +++ b/v-next/hardhat-errors/src/descriptors.ts @@ -491,6 +491,12 @@ Please resolve the errors before rerunning the command.`, Please install Hardhat locally using pnpm, npm or yarn, and try again.`, }, + GLOBAL_OPTION_HIDDEN_NOT_SUPPORTED: { + number: 23, + messageTemplate: `Global option "{globalOption}" from plugin "{plugin}" cannot be hidden`, + websiteTitle: "Global option cannot be hidden", + websiteDescription: `A global option was defined as hidden, but global options cannot be hidden.`, + }, }, INTERNAL: { ASSERTION_ERROR: { @@ -800,6 +806,13 @@ Please double check your arguments.`, Please double check your arguments.`, }, + NO_HIDDEN_OPTION_CLI: { + number: 512, + messageTemplate: + 'The option "{option}" is hidden and cannot be used from the CLI.', + websiteTitle: "Hidden options cannot be used from the CLI", + websiteDescription: `You are trying to use a hidden option from the CLI, which is not allowed.`, + }, }, BUILTIN_TASKS: { RUN_FILE_NOT_FOUND: { diff --git a/v-next/hardhat/src/internal/cli/help/utils.ts b/v-next/hardhat/src/internal/cli/help/utils.ts index 67fc300a576..e6ab829b6eb 100644 --- a/v-next/hardhat/src/internal/cli/help/utils.ts +++ b/v-next/hardhat/src/internal/cli/help/utils.ts @@ -69,6 +69,10 @@ export function parseOptions(task: Task): { const positionalArguments = []; for (const [optionName, option] of task.options) { + if (option.hidden === true) { + continue; + } + options.push({ name: toCommandLineOption(optionName), shortName: toShortCommandLineOption(option.shortName), diff --git a/v-next/hardhat/src/internal/cli/main.ts b/v-next/hardhat/src/internal/cli/main.ts index bf53967a938..9359bc41989 100644 --- a/v-next/hardhat/src/internal/cli/main.ts +++ b/v-next/hardhat/src/internal/cli/main.ts @@ -539,6 +539,15 @@ function parseOptions( ); } + if (optionDefinition.hidden === true) { + throw new HardhatError( + HardhatError.ERRORS.CORE.ARGUMENTS.NO_HIDDEN_OPTION_CLI, + { + option: arg, + }, + ); + } + const optionName = optionDefinition.name; // Check if the short name is valid again now that we know its type diff --git a/v-next/hardhat/src/internal/core/config.ts b/v-next/hardhat/src/internal/core/config.ts index cdbf6684dba..4c6651ee783 100644 --- a/v-next/hardhat/src/internal/core/config.ts +++ b/v-next/hardhat/src/internal/core/config.ts @@ -1,6 +1,6 @@ import type { ArgumentTypeToValueType, - OptionDefinition, + GlobalOptionDefinition, } from "../../types/arguments.js"; import type { ConfigurationVariable } from "../../types/config.js"; import type { @@ -75,7 +75,7 @@ export function globalOption(options: { description: string; type?: T; defaultValue: ArgumentTypeToValueType; -}): OptionDefinition { +}): GlobalOptionDefinition { return buildGlobalOptionDefinition(options); } @@ -86,7 +86,7 @@ export function globalFlag(options: { name: string; shortName?: string; description: string; -}): OptionDefinition { +}): GlobalOptionDefinition { return buildGlobalOptionDefinition({ ...options, type: ArgumentType.FLAG, @@ -102,7 +102,7 @@ export function globalLevel(options: { shortName?: string; description: string; defaultValue?: number; -}): OptionDefinition { +}): GlobalOptionDefinition { return buildGlobalOptionDefinition({ ...options, type: ArgumentType.LEVEL, diff --git a/v-next/hardhat/src/internal/core/global-options.ts b/v-next/hardhat/src/internal/core/global-options.ts index 08fa7d24b76..988766f6dad 100644 --- a/v-next/hardhat/src/internal/core/global-options.ts +++ b/v-next/hardhat/src/internal/core/global-options.ts @@ -1,7 +1,7 @@ import type { ArgumentTypeToValueType, ArgumentValue, - OptionDefinition, + GlobalOptionDefinition, } from "../../types/arguments.js"; import type { GlobalOptions, @@ -108,7 +108,7 @@ export function buildGlobalOptionDefinition< description: string; type?: T; defaultValue: ArgumentTypeToValueType; -}): OptionDefinition { +}): GlobalOptionDefinition { const argumentType = type ?? ArgumentType.STRING; validateArgumentName(name); diff --git a/v-next/hardhat/src/internal/core/tasks/builders.ts b/v-next/hardhat/src/internal/core/tasks/builders.ts index d7d6a9359db..29ba2b48c3c 100644 --- a/v-next/hardhat/src/internal/core/tasks/builders.ts +++ b/v-next/hardhat/src/internal/core/tasks/builders.ts @@ -95,12 +95,14 @@ export class NewTaskDefinitionBuilderImplementation< description = "", type, defaultValue, + hidden, }: { name: NameT; shortName?: string; description?: string; type?: TypeT; defaultValue: ArgumentTypeToValueType; + hidden?: boolean; }): NewTaskDefinitionBuilder< ExtendTaskArguments > { @@ -112,6 +114,7 @@ export class NewTaskDefinitionBuilderImplementation< description, type: argumentType, defaultValue, + hidden, }; validateOption(optionDefinition, this.#usedNames, this.#id); @@ -125,6 +128,7 @@ export class NewTaskDefinitionBuilderImplementation< name: NameT; shortName?: string; description?: string; + hidden?: boolean; }): NewTaskDefinitionBuilder< ExtendTaskArguments > { @@ -132,6 +136,7 @@ export class NewTaskDefinitionBuilderImplementation< ...flagConfig, type: ArgumentType.FLAG, defaultValue: false, + hidden: flagConfig.hidden, }); } diff --git a/v-next/hardhat/src/types/arguments.ts b/v-next/hardhat/src/types/arguments.ts index f1cb6441431..42824e97ff1 100644 --- a/v-next/hardhat/src/types/arguments.ts +++ b/v-next/hardhat/src/types/arguments.ts @@ -68,8 +68,16 @@ export interface OptionDefinition { description: string; type: T; defaultValue: ArgumentTypeToValueType; + hidden?: boolean; } +/** + * A global option is essentially identical to a regular OptionDefinition, + * except that it cannot be hidden. + */ +export type GlobalOptionDefinition = + Omit, "hidden">; + /** * A positional argument is used as `` in the CLI, where its position * matters. For example, `mv ` has two positional arguments. diff --git a/v-next/hardhat/src/types/global-options.ts b/v-next/hardhat/src/types/global-options.ts index bfeb69f3f67..6ea53b7d9a5 100644 --- a/v-next/hardhat/src/types/global-options.ts +++ b/v-next/hardhat/src/types/global-options.ts @@ -1,4 +1,4 @@ -import type { OptionDefinition } from "./arguments.js"; +import type { GlobalOptionDefinition } from "./arguments.js"; /** * The values of each global option for a certain instance of the Hardhat @@ -30,7 +30,7 @@ export interface GlobalOptions { */ export interface GlobalOptionDefinitionsEntry { pluginId: string; - option: OptionDefinition; + option: GlobalOptionDefinition; } /** diff --git a/v-next/hardhat/src/types/plugins.ts b/v-next/hardhat/src/types/plugins.ts index e9b83819745..61a683adcba 100644 --- a/v-next/hardhat/src/types/plugins.ts +++ b/v-next/hardhat/src/types/plugins.ts @@ -1,4 +1,4 @@ -import type { OptionDefinition } from "./arguments.js"; +import type { GlobalOptionDefinition } from "./arguments.js"; import type { HardhatHooks } from "./hooks.js"; import type { TaskDefinition } from "./tasks.js"; @@ -76,7 +76,7 @@ export interface HardhatPlugin { /** * An array of the global options that this plugin defines. */ - globalOptions?: OptionDefinition[]; + globalOptions?: GlobalOptionDefinition[]; /** * An array of type definitions, which should be created using their builders. diff --git a/v-next/hardhat/src/types/tasks.ts b/v-next/hardhat/src/types/tasks.ts index f002030376c..66d916f75c7 100644 --- a/v-next/hardhat/src/types/tasks.ts +++ b/v-next/hardhat/src/types/tasks.ts @@ -200,6 +200,7 @@ export interface NewTaskDefinitionBuilder< description?: string; type?: TypeT; defaultValue: ArgumentTypeToValueType; + hidden?: boolean; }): NewTaskDefinitionBuilder< ExtendTaskArguments >; @@ -211,6 +212,7 @@ export interface NewTaskDefinitionBuilder< name: NameT; shortName?: string; description?: string; + hidden?: boolean; }): NewTaskDefinitionBuilder< ExtendTaskArguments >; @@ -321,6 +323,7 @@ export interface TaskOverrideDefinitionBuilder< description?: string; type?: TypeT; defaultValue: ArgumentTypeToValueType; + hidden?: boolean; }): TaskOverrideDefinitionBuilder< ExtendTaskArguments >; diff --git a/v-next/hardhat/test/fixture-projects/cli/parsing/hidden-option/hardhat.config.ts b/v-next/hardhat/test/fixture-projects/cli/parsing/hidden-option/hardhat.config.ts new file mode 100644 index 00000000000..90fb88dc486 --- /dev/null +++ b/v-next/hardhat/test/fixture-projects/cli/parsing/hidden-option/hardhat.config.ts @@ -0,0 +1,26 @@ +import type { HardhatUserConfig } from "../../../../../src/config.js"; + +import { task } from "../../../../../src/config.js"; + +const customTask = task("test-task", "description") + .setAction(async () => ({ + default: () => {}, + })) + .addOption({ + name: "opt", + description: "opt description", + defaultValue: "opt default value", + hidden: true, + }) + .addFlag({ + name: "flag", + description: "flag description", + hidden: true, + }) + .build(); + +const config: HardhatUserConfig = { + tasks: [customTask], +}; + +export default config; diff --git a/v-next/hardhat/test/internal/cli/main.ts b/v-next/hardhat/test/internal/cli/main.ts index 8c2a4d5c0f5..bcac7d66f58 100644 --- a/v-next/hardhat/test/internal/cli/main.ts +++ b/v-next/hardhat/test/internal/cli/main.ts @@ -228,6 +228,34 @@ describe("main", function () { }); }); + describe("task with hidden option", function () { + useFixtureProject("cli/parsing/hidden-option"); + + it("should throw when passing a hidden option from the CLI", async function () { + const command = "npx hardhat test-task --opt "; + + await assertRejectsWithHardhatError( + () => runMain(command), + HardhatError.ERRORS.CORE.ARGUMENTS.NO_HIDDEN_OPTION_CLI, + { + option: "--opt", + }, + ); + }); + + it("should throw when passing a hidden flag from the CLI", async function () { + const command = "npx hardhat test-task --flag"; + + await assertRejectsWithHardhatError( + () => runMain(command), + HardhatError.ERRORS.CORE.ARGUMENTS.NO_HIDDEN_OPTION_CLI, + { + option: "--flag", + }, + ); + }); + }); + describe("global help", function () { useFixtureProject("cli/parsing/base-project"); diff --git a/v-next/hardhat/test/internal/core/tasks/builders.ts b/v-next/hardhat/test/internal/core/tasks/builders.ts index eec07084a22..51e3fc381e9 100644 --- a/v-next/hardhat/test/internal/core/tasks/builders.ts +++ b/v-next/hardhat/test/internal/core/tasks/builders.ts @@ -284,6 +284,7 @@ describe("Task builders", () => { description: "", type: ArgumentType.STRING, defaultValue: "default", + hidden: undefined, }, }, positionalArguments: [], @@ -316,6 +317,7 @@ describe("Task builders", () => { description: "Argument description", type: ArgumentType.STRING, defaultValue: "default", + hidden: undefined, }, }, positionalArguments: [], @@ -348,6 +350,7 @@ describe("Task builders", () => { description: "", type: ArgumentType.INT, defaultValue: 1, + hidden: undefined, }, }, positionalArguments: [], @@ -380,6 +383,40 @@ describe("Task builders", () => { description: "", type: ArgumentType.STRING, defaultValue: "default", + hidden: undefined, + }, + }, + positionalArguments: [], + }); + }); + + it("should add a hidden option", () => { + const builder = new NewTaskDefinitionBuilderImplementation("task-id"); + const taskAction = async () => ({ + default: () => {}, + }); + const taskDefinition = builder + .setAction(taskAction) + .addOption({ + name: "arg", + defaultValue: "default", + hidden: true, + }) + .build(); + + assert.deepEqual(taskDefinition, { + type: TaskDefinitionType.NEW_TASK, + id: ["task-id"], + description: "", + action: taskAction, + options: { + arg: { + name: "arg", + shortName: undefined, + description: "", + type: ArgumentType.STRING, + defaultValue: "default", + hidden: true, }, }, positionalArguments: [], @@ -410,6 +447,7 @@ describe("Task builders", () => { description: "", type: ArgumentType.FLAG, defaultValue: false, + hidden: undefined, }, }, positionalArguments: [], @@ -438,6 +476,7 @@ describe("Task builders", () => { description: "Flag description", type: ArgumentType.FLAG, defaultValue: false, + hidden: undefined, }, }, positionalArguments: [], @@ -466,20 +505,21 @@ describe("Task builders", () => { description: "", type: ArgumentType.FLAG, defaultValue: false, + hidden: undefined, }, }, positionalArguments: [], }); }); - it("should add a flag with a short name", () => { + it("should add a hidden flag", () => { const builder = new NewTaskDefinitionBuilderImplementation("task-id"); const taskAction = async () => ({ default: () => {}, }); const taskDefinition = builder .setAction(taskAction) - .addFlag({ name: "flag", shortName: "f" }) + .addFlag({ name: "flag", hidden: true }) .build(); assert.deepEqual(taskDefinition, { @@ -490,10 +530,11 @@ describe("Task builders", () => { options: { flag: { name: "flag", - shortName: "f", + shortName: undefined, description: "", type: ArgumentType.FLAG, defaultValue: false, + hidden: true, }, }, positionalArguments: [],