diff --git a/.changeset/twenty-pillows-switch.md b/.changeset/twenty-pillows-switch.md new file mode 100644 index 00000000000..660de945728 --- /dev/null +++ b/.changeset/twenty-pillows-switch.md @@ -0,0 +1,9 @@ +--- +"@nomicfoundation/example-project": patch +"@nomicfoundation/hardhat-ignition": patch +"@nomicfoundation/hardhat-mocha": patch +"@nomicfoundation/hardhat-node-test-runner": patch +"hardhat": patch +--- + +Allow undefined for default values in global options and other argument types ([#6596](https://github.com/NomicFoundation/hardhat/issues/6596) diff --git a/v-next/example-project/hardhat.config.ts b/v-next/example-project/hardhat.config.ts index 18909a69158..df8edaed819 100644 --- a/v-next/example-project/hardhat.config.ts +++ b/v-next/example-project/hardhat.config.ts @@ -13,6 +13,7 @@ import hardhatEthersPlugin from "@nomicfoundation/hardhat-ethers"; import hardhatChaiMatchersPlugin from "@nomicfoundation/hardhat-ethers-chai-matchers"; import hardhatTypechain from "@nomicfoundation/hardhat-typechain"; import hardhatIgnitionViem from "@nomicfoundation/hardhat-ignition-viem"; +import { ArgumentType } from "hardhat/types/arguments"; util.inspect.defaultOptions.depth = null; @@ -50,7 +51,8 @@ const exampleTaskOverride = task("example2") .addOption({ name: "grep", description: "Only run tests matching the given string or regexp", - defaultValue: "", + type: ArgumentType.STRING_WITHOUT_DEFAULT, + defaultValue: undefined, }) .build(); diff --git a/v-next/hardhat-ignition/src/index.ts b/v-next/hardhat-ignition/src/index.ts index 5ec8f4a51c7..5c8706eb171 100644 --- a/v-next/hardhat-ignition/src/index.ts +++ b/v-next/hardhat-ignition/src/index.ts @@ -26,22 +26,22 @@ const hardhatIgnitionPlugin: HardhatPlugin = { }) .addOption({ name: "parameters", - type: ArgumentType.FILE, + type: ArgumentType.FILE_WITHOUT_DEFAULT, description: "A relative path to a JSON file to use for the module parameters", - defaultValue: "", // TODO: HH3 check this comes through correctly + defaultValue: undefined, }) .addOption({ name: "deploymentId", - type: ArgumentType.STRING, + type: ArgumentType.STRING_WITHOUT_DEFAULT, description: "Set the id of the deployment", - defaultValue: "", // TODO: HH3 check this comes through correctly + defaultValue: undefined, }) .addOption({ name: "defaultSender", - type: ArgumentType.STRING, + type: ArgumentType.STRING_WITHOUT_DEFAULT, description: "Set the default sender for the deployment", - defaultValue: "", // TODO: HH3 check this comes through correctly + defaultValue: undefined, }) .addOption({ name: "strategy", diff --git a/v-next/hardhat-ignition/src/internal/tasks/deploy.ts b/v-next/hardhat-ignition/src/internal/tasks/deploy.ts index 394a7802997..51c72ae0242 100644 --- a/v-next/hardhat-ignition/src/internal/tasks/deploy.ts +++ b/v-next/hardhat-ignition/src/internal/tasks/deploy.ts @@ -177,8 +177,7 @@ const taskDeploy: NewTaskActionFunction = async ( } let parameters: DeploymentParameters | undefined; - // TODO: HH3 Remove the use of "" as a default value - if (parametersInput === undefined || parametersInput === "") { + if (parametersInput === undefined) { parameters = await resolveParametersFromModuleName( userModule.id, hre.config.paths.ignition, diff --git a/v-next/hardhat-mocha/src/index.ts b/v-next/hardhat-mocha/src/index.ts index e2937a0baa8..24c4e4e9b63 100644 --- a/v-next/hardhat-mocha/src/index.ts +++ b/v-next/hardhat-mocha/src/index.ts @@ -1,6 +1,7 @@ import type { HardhatPlugin } from "hardhat/types/plugins"; import { task } from "hardhat/config"; +import { ArgumentType } from "hardhat/types/arguments"; import "./type-extensions.js"; @@ -20,7 +21,8 @@ const hardhatPlugin: HardhatPlugin = { .addOption({ name: "grep", description: "Only run tests matching the given string or regexp", - defaultValue: "", + type: ArgumentType.STRING_WITHOUT_DEFAULT, + defaultValue: undefined, }) .addFlag({ name: "noCompile", diff --git a/v-next/hardhat-node-test-runner/src/index.ts b/v-next/hardhat-node-test-runner/src/index.ts index d342e3a00a2..00788cd5f65 100644 --- a/v-next/hardhat-node-test-runner/src/index.ts +++ b/v-next/hardhat-node-test-runner/src/index.ts @@ -1,6 +1,7 @@ import type { HardhatPlugin } from "hardhat/types/plugins"; import { task } from "hardhat/config"; +import { ArgumentType } from "hardhat/types/arguments"; import "./type-extensions.js"; @@ -20,7 +21,8 @@ const hardhatPlugin: HardhatPlugin = { .addOption({ name: "grep", description: "Only run tests matching the given string or regexp", - defaultValue: "", + type: ArgumentType.STRING_WITHOUT_DEFAULT, + defaultValue: undefined, }) .addFlag({ name: "noCompile", diff --git a/v-next/hardhat/src/internal/builtin-global-options.ts b/v-next/hardhat/src/internal/builtin-global-options.ts index 9d82b6e74cd..29562df90ff 100644 --- a/v-next/hardhat/src/internal/builtin-global-options.ts +++ b/v-next/hardhat/src/internal/builtin-global-options.ts @@ -12,8 +12,8 @@ export const BUILTIN_GLOBAL_OPTIONS_DEFINITIONS: GlobalOptionDefinitions = option: globalOption({ name: "config", description: "A Hardhat config file.", - type: ArgumentType.STRING, - defaultValue: "", + type: ArgumentType.STRING_WITHOUT_DEFAULT, + defaultValue: undefined, }), }, ], diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/hre.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/hre.ts index 5a05ba79d6a..cf67c9472ef 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/hre.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/hre.ts @@ -15,7 +15,7 @@ export default async (): Promise> => ({ if (networkManager === undefined) { networkManager = new NetworkManagerImplementation( - hre.globalOptions.network !== "" + hre.globalOptions.network !== undefined ? hre.globalOptions.network : hre.config.defaultNetwork, hre.config.defaultChainType, diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/index.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/index.ts index bfb4b00af42..706f3ccff46 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/index.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/index.ts @@ -1,5 +1,6 @@ import type { HardhatPlugin } from "../../../types/plugins.js"; +import { ArgumentType } from "../../../types/arguments.js"; import { globalOption } from "../../core/config.js"; import "./type-extensions/config.js"; @@ -18,7 +19,8 @@ const hardhatPlugin: HardhatPlugin = { globalOption({ name: "network", description: "The network to connect to", - defaultValue: "", + type: ArgumentType.STRING_WITHOUT_DEFAULT, + defaultValue: undefined, }), ], npmPackage: "hardhat", diff --git a/v-next/hardhat/src/internal/builtin-plugins/node/index.ts b/v-next/hardhat/src/internal/builtin-plugins/node/index.ts index 2538dc7663f..ce8cae58df8 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/node/index.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/node/index.ts @@ -11,7 +11,8 @@ const hardhatPlugin: HardhatPlugin = { name: "hostname", description: "The host to which to bind to for new connections (Defaults to 127.0.0.1 running locally, and 0.0.0.0 in Docker)", - defaultValue: "", + type: ArgumentType.STRING_WITHOUT_DEFAULT, + defaultValue: undefined, }) .addOption({ name: "port", @@ -23,7 +24,8 @@ const hardhatPlugin: HardhatPlugin = { name: "chainType", description: "The chain type to connect to. If not specified, the default chain type will be used.", - defaultValue: "", + type: ArgumentType.STRING_WITHOUT_DEFAULT, + defaultValue: undefined, }) .addOption({ name: "chainId", @@ -35,7 +37,8 @@ const hardhatPlugin: HardhatPlugin = { .addOption({ name: "fork", description: "The URL of the JSON-RPC server to fork from", - defaultValue: "", + type: ArgumentType.STRING_WITHOUT_DEFAULT, + defaultValue: undefined, }) .addOption({ name: "forkBlockNumber", diff --git a/v-next/hardhat/src/internal/builtin-plugins/node/task-action.ts b/v-next/hardhat/src/internal/builtin-plugins/node/task-action.ts index 13b15b94fcd..0dc86e8537d 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/node/task-action.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/node/task-action.ts @@ -27,7 +27,7 @@ const nodeAction: NewTaskActionFunction = async ( hre, ) => { const network = - hre.globalOptions.network !== "" + hre.globalOptions.network !== undefined ? hre.globalOptions.network : hre.config.defaultNetwork; @@ -49,7 +49,7 @@ const nodeAction: NewTaskActionFunction = async ( // as much as needed. const networkConfigOverride: EdrNetworkConfigOverride = {}; - if (args.chainType !== "") { + if (args.chainType !== undefined) { if (!isEdrSupportedChainType(args.chainType)) { // NOTE: We could make the error more specific here. throw new HardhatError( @@ -69,7 +69,7 @@ const nodeAction: NewTaskActionFunction = async ( } // NOTE: --fork-block-number is only valid if --fork is specified - if (args.fork !== "") { + if (args.fork !== undefined) { networkConfigOverride.forking = { enabled: true, url: args.fork, @@ -104,7 +104,7 @@ const nodeAction: NewTaskActionFunction = async ( // the default hostname is "127.0.0.1" unless we are inside a docker // container, in that case we use "0.0.0.0" let hostname = args.hostname; - if (hostname === "") { + if (hostname === undefined) { const insideDocker = await exists("/.dockerenv"); if (insideDocker) { hostname = "0.0.0.0"; diff --git a/v-next/hardhat/src/internal/cli/help/utils.ts b/v-next/hardhat/src/internal/cli/help/utils.ts index a2cb1dc1435..c73e6bd27e4 100644 --- a/v-next/hardhat/src/internal/cli/help/utils.ts +++ b/v-next/hardhat/src/internal/cli/help/utils.ts @@ -109,13 +109,30 @@ export function getSection( return `\n${title}:\n\n${items .sort((a, b) => a.name.localeCompare(b.name)) .map(({ name, description, defaultValue }) => { - const defaultValueStr = - defaultValue !== undefined ? ` (default: ${defaultValue})` : ""; + const defaultValueStr = getDefaultValueString(defaultValue); return ` ${name.padEnd(namePadding)}${description}${defaultValueStr}`; }) .join("\n")}\n`; } +function getDefaultValueString( + defaultValue: ArgumentTypeToValueType, +): string { + if (Array.isArray(defaultValue)) { + if (defaultValue.length === 0) { + return ""; + } else { + return ` (default: ${JSON.stringify(defaultValue)})`; + } + } + + if (defaultValue === undefined) { + return ""; + } + + return ` (default: ${defaultValue})`; +} + export function getUsageString( task: Task, options: ReturnType["options"], diff --git a/v-next/hardhat/src/internal/core/arguments.ts b/v-next/hardhat/src/internal/core/arguments.ts index e48818fe0f4..f022b40a49a 100644 --- a/v-next/hardhat/src/internal/core/arguments.ts +++ b/v-next/hardhat/src/internal/core/arguments.ts @@ -106,6 +106,10 @@ const argumentTypeValidators: Record< [ArgumentType.BIGINT]: (value): value is bigint => typeof value === "bigint", [ArgumentType.FLOAT]: (value): value is number => typeof value === "number", [ArgumentType.FILE]: (value): value is string => typeof value === "string", + [ArgumentType.STRING_WITHOUT_DEFAULT]: (value): value is string | undefined => + typeof value === "string" || value === undefined, + [ArgumentType.FILE_WITHOUT_DEFAULT]: (value): value is string | undefined => + typeof value === "string" || value === undefined, }; /** @@ -121,6 +125,8 @@ export function parseArgumentValue( name: string, ): ArgumentValue { switch (type) { + case ArgumentType.STRING_WITHOUT_DEFAULT: + case ArgumentType.FILE_WITHOUT_DEFAULT: case ArgumentType.STRING: case ArgumentType.FILE: return value; diff --git a/v-next/hardhat/src/internal/core/config-validation.ts b/v-next/hardhat/src/internal/core/config-validation.ts index b887b2b9440..3d2e34c20e0 100644 --- a/v-next/hardhat/src/internal/core/config-validation.ts +++ b/v-next/hardhat/src/internal/core/config-validation.ts @@ -375,7 +375,11 @@ export function validateOptions( }); } - if (option.defaultValue === undefined) { + if ( + option.type !== ArgumentType.STRING_WITHOUT_DEFAULT && + option.type !== ArgumentType.FILE_WITHOUT_DEFAULT && + option.defaultValue === undefined + ) { validationErrors.push({ path: [...path, name, "defaultValue"], message: "option defaultValue must be defined", @@ -392,6 +396,19 @@ export function validateOptions( } break; } + case ArgumentType.FILE_WITHOUT_DEFAULT: + case ArgumentType.STRING_WITHOUT_DEFAULT: { + if ( + typeof option.defaultValue !== "string" && + option.defaultValue !== undefined + ) { + validationErrors.push({ + path: [...path, name, "defaultValue"], + message: "option defaultValue must be a string or undefined", + }); + } + break; + } case ArgumentType.BOOLEAN: { if (typeof option.defaultValue !== "boolean") { validationErrors.push({ @@ -457,6 +474,8 @@ export function validatePositionalArguments( if (arg.defaultValue !== undefined) { switch (arg.type) { + case ArgumentType.STRING_WITHOUT_DEFAULT: + case ArgumentType.FILE_WITHOUT_DEFAULT: case ArgumentType.STRING: case ArgumentType.FILE: { if ( diff --git a/v-next/hardhat/src/internal/core/global-options.ts b/v-next/hardhat/src/internal/core/global-options.ts index b5274c13a3d..0a432387f49 100644 --- a/v-next/hardhat/src/internal/core/global-options.ts +++ b/v-next/hardhat/src/internal/core/global-options.ts @@ -69,7 +69,9 @@ export function buildGlobalOptionDefinitions( * Builds a global option definition, validating the name, type, and default * value. */ -export function buildGlobalOptionDefinition({ +export function buildGlobalOptionDefinition< + T extends ArgumentType = ArgumentType.STRING, +>({ name, description, type, diff --git a/v-next/hardhat/src/types/arguments.ts b/v-next/hardhat/src/types/arguments.ts index b4e69488a27..49664081d9f 100644 --- a/v-next/hardhat/src/types/arguments.ts +++ b/v-next/hardhat/src/types/arguments.ts @@ -8,6 +8,8 @@ export enum ArgumentType { BIGINT = "BIGINT", FLOAT = "FLOAT", FILE = "FILE", + STRING_WITHOUT_DEFAULT = "STRING_WITHOUT_DEFAULT", + FILE_WITHOUT_DEFAULT = "FILE_WITHOUT_DEFAULT", } /** @@ -20,6 +22,8 @@ export interface ArgumentValueTypes { [ArgumentType.BIGINT]: bigint; [ArgumentType.FLOAT]: number; [ArgumentType.FILE]: string; + [ArgumentType.STRING_WITHOUT_DEFAULT]: string | undefined; + [ArgumentType.FILE_WITHOUT_DEFAULT]: string | undefined; } /** diff --git a/v-next/hardhat/test/internal/builtin-plugins/node/json-rpc/handler.ts b/v-next/hardhat/test/internal/builtin-plugins/node/json-rpc/handler.ts index 61b55b5c1cf..22e14c7678a 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/node/json-rpc/handler.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/node/json-rpc/handler.ts @@ -19,7 +19,7 @@ describe("JSON-RPC handler", function () { it("should respond to a request with undefined params", async function () { const hostname = (await exists("/.dockerenv")) ? "0.0.0.0" : "127.0.0.1"; - const port = 8545; + const port = 8546; const connection = await hre.network.connect(); const server = new JsonRpcServerImplementation({ diff --git a/v-next/hardhat/test/internal/core/tasks/task-manager.ts b/v-next/hardhat/test/internal/core/tasks/task-manager.ts index 5884f15c3e0..22a0b2ca152 100644 --- a/v-next/hardhat/test/internal/core/tasks/task-manager.ts +++ b/v-next/hardhat/test/internal/core/tasks/task-manager.ts @@ -55,8 +55,8 @@ describe("TaskManagerImplementation", () => { globalOption({ name: "globalOption1", description: "", - type: ArgumentType.STRING, - defaultValue: "", + type: ArgumentType.STRING_WITHOUT_DEFAULT, + defaultValue: undefined, }), ], }, @@ -422,8 +422,8 @@ describe("TaskManagerImplementation", () => { globalOption({ name: "arg1", description: "", - type: ArgumentType.STRING, - defaultValue: "", + type: ArgumentType.STRING_WITHOUT_DEFAULT, + defaultValue: undefined, }), ], }, @@ -462,8 +462,8 @@ describe("TaskManagerImplementation", () => { globalOption({ name: "arg1", description: "", - type: ArgumentType.STRING, - defaultValue: "", + type: ArgumentType.STRING_WITHOUT_DEFAULT, + defaultValue: undefined, }), ], }, diff --git a/v-next/hardhat/test/internal/hre-initialization.ts b/v-next/hardhat/test/internal/hre-initialization.ts index 88741aa374a..6b57221464d 100644 --- a/v-next/hardhat/test/internal/hre-initialization.ts +++ b/v-next/hardhat/test/internal/hre-initialization.ts @@ -253,7 +253,7 @@ describe("HRE initialization", () => { verbose: false, version: false, myGlobalOption: "default", - network: "", + network: undefined, }); }); });