hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
272 lines (242 loc) • 7.87 kB
text/typescript
import type {
ArgumentValue,
OptionDefinition,
PositionalArgumentDefinition,
} from "../../../types/arguments.js";
import type { HardhatRuntimeEnvironment } from "../../../types/hre.js";
import type {
LazyActionObject,
NewTaskActionFunction,
Task,
TaskActions,
TaskArguments,
TaskOverrideActionFunction,
} from "../../../types/tasks.js";
import {
HardhatError,
assertHardhatInvariant,
} from "@nomicfoundation/hardhat-errors";
import { ensureError } from "@nomicfoundation/hardhat-utils/error";
import { detectPluginNpmDependencyProblems } from "../plugins/detect-plugin-npm-dependency-problems.js";
import { formatTaskId } from "./utils.js";
import { validateTaskArgumentValue } from "./validations.js";
export class ResolvedTask implements Task {
readonly #hre: HardhatRuntimeEnvironment;
public static createEmptyTask(
hre: HardhatRuntimeEnvironment,
id: string[],
description: string,
pluginId?: string,
): ResolvedTask {
return new ResolvedTask(
id,
description,
[{ pluginId, action: undefined }],
new Map(),
[],
pluginId,
new Map(),
hre,
);
}
public static createNewTask(
hre: HardhatRuntimeEnvironment,
id: string[],
description: string,
action: LazyActionObject<NewTaskActionFunction>,
options: Record<string, OptionDefinition>,
positionalArguments: PositionalArgumentDefinition[],
pluginId?: string,
): ResolvedTask {
return new ResolvedTask(
id,
description,
[{ pluginId, action }],
new Map(Object.entries(options)),
positionalArguments,
pluginId,
new Map(),
hre,
);
}
constructor(
public readonly id: string[],
public readonly description: string,
public readonly actions: TaskActions,
public readonly options: Map<string, OptionDefinition>,
public readonly positionalArguments: PositionalArgumentDefinition[],
public readonly pluginId: string | undefined,
public readonly subtasks: Map<string, Task>,
hre: HardhatRuntimeEnvironment,
) {
this.#hre = hre;
}
public get isEmpty(): boolean {
return this.actions.length === 1 && this.actions[0].action === undefined;
}
/**
* This method runs the task with the given arguments.
* It validates the arguments, resolves the file actions, and runs the task
* actions by calling them in order.
*
* @param taskArguments The arguments to run the task with.
* @returns The result of running the task.
* @throws HardhatError if the task is empty, a required argument is missing,
* a argument has an invalid type, or the file actions can't be resolved.
*/
public async run(taskArguments: TaskArguments = {}): Promise<any> {
if (this.isEmpty) {
throw new HardhatError(
HardhatError.ERRORS.CORE.TASK_DEFINITIONS.EMPTY_TASK,
{
task: formatTaskId(this.id),
},
);
}
const providedArgumentNames = new Set(Object.keys(taskArguments));
const argumentDefinitions = [
...this.options.values(),
...this.positionalArguments,
];
const validatedTaskArguments: TaskArguments = {};
for (const argumentDefinition of argumentDefinitions) {
const value = taskArguments[argumentDefinition.name];
const isPositional = "isVariadic" in argumentDefinition;
if (isPositional) {
this.#validateRequiredArgument(argumentDefinition, value);
}
if (value !== undefined) {
validateTaskArgumentValue(
argumentDefinition.name,
argumentDefinition.type,
value,
isPositional && argumentDefinition.isVariadic,
this.id,
);
}
// resolve defaults for optional arguments
validatedTaskArguments[argumentDefinition.name] =
value ?? argumentDefinition.defaultValue;
providedArgumentNames.delete(argumentDefinition.name);
}
// At this point, the set should be empty as all the task arguments have
// been processed. If there are any extra arguments, an error is thrown
this.#validateExtraArguments(providedArgumentNames);
const next = async (
nextTaskArguments: TaskArguments,
currentIndex = this.actions.length - 1,
): Promise<any> => {
// The first action may be empty if the task was originally an empty task
const currentAction =
this.actions[currentIndex].action ??
(async () => ({
default: () => {},
}));
const actionFn = await this.#resolveImportAction(
currentAction,
this.actions[currentIndex].pluginId,
);
if (currentIndex === 0) {
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions --
We know that the first action in the array is a NewTaskActionFunction */
return (actionFn as NewTaskActionFunction)(
nextTaskArguments,
this.#hre,
);
}
return actionFn(
nextTaskArguments,
this.#hre,
async (newTaskArguments: TaskArguments) => {
return next(newTaskArguments, currentIndex - 1);
},
);
};
return next(validatedTaskArguments);
}
/**
* Validates that a required argument has a value. A argument is required if
* it doesn't have a default value.
*
* @throws HardhatError if the argument is required and doesn't have a value.
*/
#validateRequiredArgument(
argument: PositionalArgumentDefinition,
value: ArgumentValue | ArgumentValue[],
) {
if (argument.defaultValue === undefined && value === undefined) {
throw new HardhatError(
HardhatError.ERRORS.CORE.TASK_DEFINITIONS.MISSING_VALUE_FOR_TASK_ARGUMENT,
{
argument: argument.name,
task: formatTaskId(this.id),
},
);
}
}
/**
* Validates that no extra arguments were provided in the task arguments.
*
* @throws HardhatError if extra arguments were provided. The error message
* includes the name of the first extra argument.
*/
#validateExtraArguments(providedArgumentNames: Set<string>) {
if (providedArgumentNames.size > 0) {
throw new HardhatError(
HardhatError.ERRORS.CORE.TASK_DEFINITIONS.UNRECOGNIZED_TASK_OPTION,
{
option: [...providedArgumentNames][0],
task: formatTaskId(this.id),
},
);
}
}
/**
* Resolves the action file for a task. The action file is imported and the
* default export function is returned.
*
* @throws HardhatError if the module can't be imported or doesn't have a
* default export function.
*/
async #resolveImportAction(
action: LazyActionObject<TaskOverrideActionFunction<TaskArguments>>,
actionPluginId?: string,
): Promise<NewTaskActionFunction | TaskOverrideActionFunction> {
let resolvedActionFn;
try {
resolvedActionFn = await action();
} catch (error) {
ensureError(error);
if (actionPluginId !== undefined) {
const plugin = this.#hre.config.plugins.find(
(p) => p.id === actionPluginId,
);
assertHardhatInvariant(
plugin !== undefined,
`Plugin with id ${actionPluginId} not found.`,
);
await detectPluginNpmDependencyProblems(
this.#hre.config.paths.root,
plugin,
error,
);
}
throw new HardhatError(
HardhatError.ERRORS.CORE.TASK_DEFINITIONS.INVALID_ACTION_IMPORT,
{
task: formatTaskId(this.id),
},
error,
);
}
if (typeof resolvedActionFn.default !== "function") {
throw new HardhatError(
HardhatError.ERRORS.CORE.TASK_DEFINITIONS.INVALID_ACTION,
{
task: formatTaskId(this.id),
},
);
}
return resolvedActionFn.default;
}
}