hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
447 lines (444 loc) • 19.6 kB
JavaScript
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, } 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 async function main(rawArguments, options = {}) {
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 = 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, usedCliArguments) {
let configPath;
let showStackTraces = isCi();
let help = false;
let version = false;
let init = 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, cliArguments, usedCliArguments) {
const 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, usedCliArguments, hre) {
const taskOrId = getTaskFromCliArguments(cliArguments, usedCliArguments, hre);
return taskOrId;
}
function getTaskFromCliArguments(cliArguments, usedCliArguments, hre) {
const taskId = [];
let task;
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, usedCliArguments, task) {
const 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) {
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, usedCliArguments, optionDefinitions, providedArguments, ignoreUnknownOption = false) {
const optionDefinitionsByShortName = new Map();
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;
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, usedCliArguments, task, providedArguments) {
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, 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, print) {
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, log) {
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;
}
}
//# sourceMappingURL=main.js.map