hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
753 lines (621 loc) • 20.2 kB
text/typescript
import type {
GlobalOptionDefinitions,
GlobalOptions,
} from "../../types/global-options.js";
import type { HardhatRuntimeEnvironment } from "../../types/hre.js";
import type { Task, TaskArguments } from "../../types/tasks.js";
import { fileURLToPath } from "node:url";
import {
HardhatError,
assertHardhatInvariant,
} from "@nomicfoundation/hardhat-errors";
import { isCi } from "@nomicfoundation/hardhat-utils/ci";
import { ensureError } from "@nomicfoundation/hardhat-utils/error";
import { getRealPath } from "@nomicfoundation/hardhat-utils/fs";
import {
findClosestPackageJson,
findDependencyPackageJson,
readClosestPackageJson,
} from "@nomicfoundation/hardhat-utils/package";
import { kebabToCamelCase } from "@nomicfoundation/hardhat-utils/string";
import debug from "debug";
import { register } from "tsx/esm/api";
import {
ArgumentType,
type OptionDefinition,
type PositionalArgumentDefinition,
} from "../../types/arguments.js";
import { BUILTIN_GLOBAL_OPTIONS_DEFINITIONS } from "../builtin-global-options.js";
import { builtinPlugins } from "../builtin-plugins/index.js";
import {
importUserConfig,
resolveHardhatConfigPath,
} from "../config-loading.js";
import { parseArgumentValue } from "../core/arguments.js";
import { buildGlobalOptionDefinitions } from "../core/global-options.js";
import { resolveProjectRoot } from "../core/hre.js";
import { resolvePluginList } from "../core/plugins/resolve-plugin-list.js";
import { setGlobalHardhatRuntimeEnvironment } from "../global-hre-instance.js";
import { createHardhatRuntimeEnvironment } from "../hre-initialization.js";
import { printErrorMessages } from "./error-handler.js";
import { getGlobalHelpString } from "./help/get-global-help-string.js";
import { getHelpString } from "./help/get-help-string.js";
import { sendTaskAnalytics } from "./telemetry/analytics/analytics.js";
import {
sendErrorTelemetry,
setCliHardhatConfigPath,
setupErrorTelemetryIfEnabled,
} from "./telemetry/sentry/reporter.js";
import { printVersionMessage } from "./version.js";
export interface MainOptions {
print?: (message: string) => void;
registerTsx?: boolean;
rethrowErrors?: true;
allowNonlocalHardhatInstallation?: true;
}
export async function main(
rawArguments: string[],
options: MainOptions = {},
): Promise<void> {
await setupErrorTelemetryIfEnabled();
const print = options.print ?? console.log;
const log = debug("hardhat:core:cli:main");
let builtinGlobalOptions;
let configPath;
log("Hardhat CLI started");
try {
const cliArguments = parseRawArguments(rawArguments);
const usedCliArguments: boolean[] = new Array(cliArguments.length).fill(
false,
);
builtinGlobalOptions = await parseBuiltinGlobalOptions(
cliArguments,
usedCliArguments,
);
log("Parsed builtin global options");
if (builtinGlobalOptions.version) {
return await printVersionMessage(print);
}
if (builtinGlobalOptions.init) {
const { initHardhat } = await import("./init/init.js");
return await initHardhat();
}
configPath = await resolveHardhatConfigPath(
builtinGlobalOptions.configPath,
);
if (
options.allowNonlocalHardhatInstallation !== true &&
!(await isHardhatInstalledLocallyOrLinked(configPath, log))
) {
throw new HardhatError(
HardhatError.ERRORS.CORE.GENERAL.NON_LOCAL_INSTALLATION,
);
}
setCliHardhatConfigPath(configPath);
const projectRoot = await resolveProjectRoot(configPath);
const esmErrorPrinted = await printEsmErrorMessageIfNecessary(
projectRoot,
print,
);
if (esmErrorPrinted) {
process.exitCode = 1;
return;
}
if (options.registerTsx === true) {
register();
}
const userConfig = await importUserConfig(configPath);
log("User config imported");
const configPlugins = Array.isArray(userConfig.plugins)
? userConfig.plugins
: [];
const plugins = [...builtinPlugins, ...configPlugins];
const resolvedPlugins = await resolvePluginList(projectRoot, plugins);
log("Resolved plugins");
const pluginGlobalOptionDefinitions =
buildGlobalOptionDefinitions(resolvedPlugins);
const globalOptionDefinitions = new Map([
...BUILTIN_GLOBAL_OPTIONS_DEFINITIONS,
...pluginGlobalOptionDefinitions,
]);
const userProvidedGlobalOptions = await parseGlobalOptions(
globalOptionDefinitions,
cliArguments,
usedCliArguments,
);
log("Creating Hardhat Runtime Environment");
const hre = await createHardhatRuntimeEnvironment(
userConfig,
{
...builtinGlobalOptions,
config: configPath,
...userProvidedGlobalOptions,
},
projectRoot,
{ resolvedPlugins, globalOptionDefinitions },
);
// This must be the first time we set it, otherwise we let it crash
setGlobalHardhatRuntimeEnvironment(hre);
const taskOrId = parseTask(cliArguments, usedCliArguments, hre);
if (Array.isArray(taskOrId)) {
if (taskOrId.length === 0) {
const globalHelp = await getGlobalHelpString(
hre.tasks.rootTasks,
globalOptionDefinitions,
);
print(globalHelp);
return;
}
throw new HardhatError(
HardhatError.ERRORS.CORE.TASK_DEFINITIONS.TASK_NOT_FOUND,
{ task: taskOrId.join(" ") },
);
}
const task = taskOrId;
if (task.isEmpty && usedCliArguments.includes(false)) {
const invalidSubtask = cliArguments[usedCliArguments.indexOf(false)];
throw new HardhatError(
HardhatError.ERRORS.CORE.TASK_DEFINITIONS.UNRECOGNIZED_SUBTASK,
{
task: task.id.join(" "),
invalidSubtask,
},
);
}
if (builtinGlobalOptions.help || task.isEmpty) {
const taskHelp = await getHelpString(task, globalOptionDefinitions);
print(taskHelp);
return;
}
const taskArguments = parseTaskArguments(
cliArguments,
usedCliArguments,
task,
);
log(`Running task "${task.id.join(" ")}"`);
await Promise.all([task.run(taskArguments), sendTaskAnalytics(task.id)]);
} catch (error) {
ensureError(error);
printErrorMessages(error, builtinGlobalOptions?.showStackTraces);
try {
await sendErrorTelemetry(error);
} catch (e) {
log("Couldn't report error to sentry: %O", e);
}
if (options.rethrowErrors) {
throw error;
}
process.exitCode = 1;
}
}
export async function parseBuiltinGlobalOptions(
cliArguments: string[],
usedCliArguments: boolean[],
): Promise<{
init: boolean;
configPath: string | undefined;
showStackTraces: boolean;
help: boolean;
version: boolean;
}> {
let configPath: string | undefined;
let showStackTraces: boolean = isCi();
let help: boolean = false;
let version: boolean = false;
let init: boolean = false;
// TODO: Use parseGlobalOptions(BUILTIN_GLOBAL_OPTIONS_DEFINITIONS, ...) instead
for (let i = 0; i < cliArguments.length; i++) {
const arg = cliArguments[i];
if (arg === "--init") {
usedCliArguments[i] = true;
init = true;
continue;
}
if (arg === "--config") {
usedCliArguments[i] = true;
if (configPath !== undefined) {
throw new HardhatError(
HardhatError.ERRORS.CORE.ARGUMENTS.DUPLICATED_NAME,
{
name: "--config",
},
);
}
if (
usedCliArguments[i + 1] === undefined ||
usedCliArguments[i + 1] === true
) {
throw new HardhatError(
HardhatError.ERRORS.CORE.ARGUMENTS.MISSING_CONFIG_FILE,
);
}
configPath = cliArguments[i + 1];
i++;
usedCliArguments[i] = true;
continue;
}
if (arg === "--show-stack-traces") {
usedCliArguments[i] = true;
showStackTraces = true;
continue;
}
if (arg === "--help" || arg === "-h") {
usedCliArguments[i] = true;
help = true;
continue;
}
if (arg === "--version") {
usedCliArguments[i] = true;
version = true;
continue;
}
}
if (init && configPath !== undefined) {
throw new HardhatError(
HardhatError.ERRORS.CORE.ARGUMENTS.CANNOT_COMBINE_INIT_AND_CONFIG_PATH,
);
}
return { init, configPath, showStackTraces, help, version };
}
export async function parseGlobalOptions(
globalOptionDefinitions: GlobalOptionDefinitions,
cliArguments: string[],
usedCliArguments: boolean[],
): Promise<Partial<GlobalOptions>> {
const globalOptions: Partial<GlobalOptions> = {};
const optionDefinitions = new Map(
[...globalOptionDefinitions].map(([key, value]) => [key, value.option]),
);
parseOptions(
cliArguments,
usedCliArguments,
optionDefinitions,
globalOptions,
true,
);
return globalOptions;
}
/**
* Parses the task from the cli args.
*
* @returns The task, or an array with the unrecognized task id.
* If no task id is provided, an empty array is returned.
*/
export function parseTask(
cliArguments: string[],
usedCliArguments: boolean[],
hre: HardhatRuntimeEnvironment,
): Task | string[] {
const taskOrId = getTaskFromCliArguments(cliArguments, usedCliArguments, hre);
return taskOrId;
}
function getTaskFromCliArguments(
cliArguments: string[],
usedCliArguments: boolean[],
hre: HardhatRuntimeEnvironment,
): string[] | Task {
const taskId = [];
let task: Task | undefined;
for (let i = 0; i < cliArguments.length; i++) {
if (usedCliArguments[i]) {
continue;
}
const arg = cliArguments[i];
if (arg.startsWith("-")) {
/* A standalone '--' is ok because it is used to separate CLI tool arguments
* from task arguments, ensuring the tool passes subsequent options directly
* to the task. Everything after "--" should be considered as a positional
* argument. */
if (arg === "--" || task !== undefined) {
break;
}
/* At this point in the code, the global options have already been parsed, so
* the remaining options starting with '--' are task options. Hence, if no task
* is defined, it means that the option is not assigned to any task, and it's
* an error. */
throw new HardhatError(
HardhatError.ERRORS.CORE.ARGUMENTS.UNRECOGNIZED_OPTION,
{
option: arg,
},
);
}
if (task === undefined) {
try {
task = hre.tasks.getTask(arg);
} catch (_error) {
return [arg]; // No task found
}
} else {
const subtask = task.subtasks.get(arg);
if (subtask === undefined) {
break;
}
task = subtask;
}
usedCliArguments[i] = true;
taskId.push(arg);
}
if (task === undefined) {
return taskId;
}
return task;
}
export function parseTaskArguments(
cliArguments: string[],
usedCliArguments: boolean[],
task: Task,
): TaskArguments {
const taskArguments: TaskArguments = {};
// Parse options
parseOptions(cliArguments, usedCliArguments, task.options, taskArguments);
parsePositionalAndVariadicArguments(
cliArguments,
usedCliArguments,
task,
taskArguments,
);
const unusedIndex = usedCliArguments.indexOf(false);
if (unusedIndex !== -1) {
throw new HardhatError(HardhatError.ERRORS.CORE.ARGUMENTS.UNUSED_ARGUMENT, {
value: cliArguments[unusedIndex],
});
}
return taskArguments;
}
/**
* Parses the raw arguments from the command line, returning an array of
* arguments. If an argument starts with "--" and contains "=" (i.e. "--option=123")
* it is split into two separate arguments: the option name and the option value.
*/
export function parseRawArguments(rawArguments: string[]): string[] {
return rawArguments.flatMap((arg) => {
if (arg.startsWith("--") && arg.includes("=")) {
const index = arg.indexOf("=");
const optionName = arg.substring(0, index);
const optionValue = arg.substring(index + 1);
return [optionName, optionValue];
}
return arg;
});
}
function parseOptions(
cliArguments: string[],
usedCliArguments: boolean[],
optionDefinitions: Map<string, OptionDefinition>,
providedArguments: TaskArguments,
ignoreUnknownOption = false,
) {
const optionDefinitionsByShortName = new Map<string, OptionDefinition>();
for (const optionDefinition of optionDefinitions.values()) {
if (optionDefinition.shortName !== undefined) {
optionDefinitionsByShortName.set(
optionDefinition.shortName,
optionDefinition,
);
}
}
for (let i = 0; i < cliArguments.length; i++) {
if (usedCliArguments[i]) {
continue;
}
if (cliArguments[i] === "--") {
/* A standalone '--' is ok because it is used to separate CLI tool arguments
* from task arguments, ensuring the tool passes subsequent options directly
* to the task. Everything after "--" should be considered as a positional
* argument. */
break;
}
const arg = cliArguments[i];
let optionDefinition: OptionDefinition | undefined;
const providedByName = arg.startsWith("--");
const providedByShortName = !providedByName && arg.startsWith("-");
if (providedByName) {
const name = kebabToCamelCase(arg.substring(2));
optionDefinition = optionDefinitions.get(name);
} else if (providedByShortName) {
const shortName = arg[1];
// Check if the short name is valid
if (Array.from(arg.substring(1)).some((c) => c !== shortName)) {
throw new HardhatError(
HardhatError.ERRORS.CORE.ARGUMENTS.CANNOT_GROUP_OPTIONS,
{
option: arg,
},
);
}
optionDefinition = optionDefinitionsByShortName.get(shortName);
} else {
continue;
}
if (optionDefinition === undefined) {
if (ignoreUnknownOption === true) {
continue;
}
// Only throw an error when the argument is not a global option, because
// it might be a option related to a task
throw new HardhatError(
HardhatError.ERRORS.CORE.ARGUMENTS.UNRECOGNIZED_OPTION,
{
option: arg,
},
);
}
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
// E.g. --flag --flag
const optionAlreadyProvided = providedArguments[optionName] !== undefined;
// E.g. -ff
const shortOptionGroupedAndRepeated = providedByShortName && arg.length > 2;
const isLevelOption = optionDefinition.type === ArgumentType.LEVEL;
if (
optionAlreadyProvided ||
(shortOptionGroupedAndRepeated && !isLevelOption)
) {
throw new HardhatError(
HardhatError.ERRORS.CORE.ARGUMENTS.CANNOT_REPEAT_OPTIONS,
{
option: arg,
type: optionDefinition.type,
},
);
}
usedCliArguments[i] = true;
if (optionDefinition.type === ArgumentType.FLAG) {
providedArguments[optionName] = true;
continue;
} else if (
optionDefinition.type === ArgumentType.LEVEL &&
providedByShortName
) {
providedArguments[optionName] = arg.length - 1;
continue;
} else if (
usedCliArguments[i + 1] !== undefined &&
usedCliArguments[i + 1] === false
) {
i++;
providedArguments[optionName] = parseArgumentValue(
cliArguments[i],
optionDefinition.type,
optionName,
);
usedCliArguments[i] = true;
continue;
}
throw new HardhatError(
HardhatError.ERRORS.CORE.ARGUMENTS.MISSING_VALUE_FOR_ARGUMENT,
{
argument: arg,
},
);
}
}
function parsePositionalAndVariadicArguments(
cliArguments: string[],
usedCliArguments: boolean[],
task: Task,
providedArguments: TaskArguments,
) {
let argIndex = 0;
for (let i = 0; i < cliArguments.length; i++) {
if (usedCliArguments[i] === true) {
continue;
}
if (cliArguments[i] === "--") {
/* A standalone '--' is ok because it is used to separate CLI tool arguments
* from task arguments, ensuring the tool passes subsequent options directly
* to the task. Everything after "--" should be considered as a positional
* argument. */
usedCliArguments[i] = true;
continue;
}
const argumentDefinition = task.positionalArguments[argIndex];
if (argumentDefinition === undefined) {
break;
}
usedCliArguments[i] = true;
const formattedValue = parseArgumentValue(
cliArguments[i],
argumentDefinition.type,
argumentDefinition.name,
);
if (argumentDefinition.isVariadic === false) {
providedArguments[argumentDefinition.name] = formattedValue;
argIndex++;
continue;
}
// Handle variadic arguments. No longer increment "argIndex" because there can
// only be one variadic argument, and it will consume all remaining arguments.
providedArguments[argumentDefinition.name] =
providedArguments[argumentDefinition.name] ?? [];
const variadicTaskArg = providedArguments[argumentDefinition.name];
assertHardhatInvariant(
Array.isArray(variadicTaskArg),
"Variadic argument values should be an array",
);
variadicTaskArg.push(formattedValue);
}
// Check if all the required arguments have been used
validateRequiredArguments(task.positionalArguments, providedArguments);
}
function validateRequiredArguments(
argumentDefinitions: PositionalArgumentDefinition[],
taskArguments: TaskArguments,
) {
const missingRequiredArgument = argumentDefinitions.find(
({ defaultValue, name }) =>
defaultValue === undefined && taskArguments[name] === undefined,
);
if (missingRequiredArgument === undefined) {
return;
}
throw new HardhatError(
HardhatError.ERRORS.CORE.ARGUMENTS.MISSING_VALUE_FOR_ARGUMENT,
{ argument: missingRequiredArgument.name },
);
}
/**
* Prints an error message if the user is running Hardhat on CJS mode, returning
* `true` if the message was printed.
*/
async function printEsmErrorMessageIfNecessary(
projectRoot: string,
print: (message: string) => void,
): Promise<boolean> {
const packageJson = await readClosestPackageJson(projectRoot);
if (packageJson.type !== "module") {
print(`Hardhat only supports ESM projects.
Please make sure you have \`"type": "module"\` in your package.json.
You can set it automatically by running:
npm pkg set type="module"
`);
return true;
}
return false;
}
/**
* Returns true if Hardhat is installed locally or linked from its repository,
* by looking for it using the node module resolution logic.
*
* If a config file is provided, we start looking for it from there. Otherwise,
* we use the current working directory.
*/
async function isHardhatInstalledLocallyOrLinked(
configPath: string,
log: debug.Debugger,
) {
try {
// Based on Node.js resolution algorithm find the real path
// of the project's version of Hardhat
const realPathToResolvedPackageJson = await findDependencyPackageJson(
configPath ?? process.cwd(),
"hardhat",
);
// Find the executing code's Hardhat Package.json
const thisPackageJson = await findClosestPackageJson(
fileURLToPath(import.meta.url),
);
// We need to get the realpaths here, as hardhat may be linked and
// running with `node --preserve-symlinks`
const isLocalOrLinked =
realPathToResolvedPackageJson === (await getRealPath(thisPackageJson));
if (!isLocalOrLinked) {
log("Determined that Hardhat is not installed locally/linked");
log(` resolved package.json: ${realPathToResolvedPackageJson}`);
log(` current package.json: ${thisPackageJson}`);
}
return isLocalOrLinked;
} catch (error) {
log("Error during installed locally/linked test", error);
return false;
}
}