hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
348 lines (293 loc) • 10.9 kB
text/typescript
/* eslint-disable @typescript-eslint/consistent-type-assertions -- Typescript
can handle the generic types in this file correctly. It can do it for function
signatures, but not for function bodies. */
import type {
ChainedHook,
HookContext,
HookManager,
InitialHookParams as InitialHookParams,
InitialChainedHookParams,
HardhatHooks,
} from "../../types/hooks.js";
import type { HardhatPlugin } from "../../types/plugins.js";
import type { LastParameter, Return } from "../../types/utils.js";
import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors";
import { ensureError } from "@nomicfoundation/hardhat-utils/error";
import { AsyncMutex } from "@nomicfoundation/hardhat-utils/synchronization";
import { detectPluginNpmDependencyProblems } from "./plugins/detect-plugin-npm-dependency-problems.js";
export class HookManagerImplementation implements HookManager {
readonly #mutex: AsyncMutex = new AsyncMutex();
readonly #projectRoot: string;
readonly #pluginsInReverseOrder: HardhatPlugin[];
/**
* Initially `undefined` to be able to run the config hooks during
* initialization.
*/
#context: HookContext | undefined;
/**
* The initialized handler categories for each plugin.
*/
readonly #staticHookHandlerCategories: Map<
string,
Map<keyof HardhatHooks, Partial<HardhatHooks[keyof HardhatHooks]>>
> = new Map();
/**
* A map of the dynamically registered handler categories.
*
* Each array is a list of categories, in reverse order of registration.
*/
readonly #dynamicHookHandlerCategories: Map<
keyof HardhatHooks,
Array<Partial<HardhatHooks[keyof HardhatHooks]>>
> = new Map();
constructor(projectRoot: string, plugins: HardhatPlugin[]) {
this.#projectRoot = projectRoot;
this.#pluginsInReverseOrder = plugins.toReversed();
}
public setContext(context: HookContext): void {
this.#context = context;
}
public registerHandlers<HookCategoryNameT extends keyof HardhatHooks>(
hookCategoryName: HookCategoryNameT,
hookHandlerCategory: Partial<HardhatHooks[HookCategoryNameT]>,
): void {
let categories = this.#dynamicHookHandlerCategories.get(hookCategoryName);
if (categories === undefined) {
categories = [];
this.#dynamicHookHandlerCategories.set(hookCategoryName, categories);
}
categories.unshift(hookHandlerCategory);
}
public unregisterHandlers<HookCategoryNameT extends keyof HardhatHooks>(
hookCategoryName: HookCategoryNameT,
hookHandlerCategory: Partial<HardhatHooks[HookCategoryNameT]>,
): void {
const categories = this.#dynamicHookHandlerCategories.get(hookCategoryName);
if (categories === undefined) {
return;
}
this.#dynamicHookHandlerCategories.set(
hookCategoryName,
categories.filter((c) => c !== hookHandlerCategory),
);
}
public async runHandlerChain<
HookCategoryNameT extends keyof HardhatHooks,
HookNameT extends keyof HardhatHooks[HookCategoryNameT],
HookT extends ChainedHook<HardhatHooks[HookCategoryNameT][HookNameT]>,
>(
hookCategoryName: HookCategoryNameT,
hookName: HookNameT,
params: InitialChainedHookParams<HookCategoryNameT, HookT>,
defaultImplementation: LastParameter<HookT>,
): Promise<Awaited<Return<HardhatHooks[HookCategoryNameT][HookNameT]>>> {
const handlers = await this.#getHandlersInChainedRunningOrder(
hookCategoryName,
hookName,
);
let handlerParams: Parameters<typeof defaultImplementation>;
if (hookCategoryName !== "config") {
assertHardhatInvariant(
this.#context !== undefined,
"Context must be set before running non-config hooks",
);
handlerParams = [this.#context, ...params] as any;
} else {
handlerParams = params as any;
}
const numberOfHandlers = handlers.length;
let index = 0;
const next = async (...nextParams: typeof handlerParams) => {
const result =
index < numberOfHandlers
? await (handlers[index++] as any)(...nextParams, next)
: await defaultImplementation(...nextParams);
return result;
};
return next(...handlerParams);
}
public async runSequentialHandlers<
HookCategoryNameT extends keyof HardhatHooks,
HookNameT extends keyof HardhatHooks[HookCategoryNameT],
HookT extends HardhatHooks[HookCategoryNameT][HookNameT],
>(
hookCategoryName: HookCategoryNameT,
hookName: HookNameT,
params: InitialHookParams<HookCategoryNameT, HookT>,
): Promise<
Array<Awaited<Return<HardhatHooks[HookCategoryNameT][HookNameT]>>>
> {
const handlers = await this.#getHandlersInSequentialRunningOrder(
hookCategoryName,
hookName,
);
let handlerParams: any;
if (hookCategoryName !== "config") {
assertHardhatInvariant(
this.#context !== undefined,
"Context must be set before running non-config hooks",
);
handlerParams = [this.#context, ...params];
} else {
handlerParams = params;
}
const result = [];
for (const handler of handlers) {
result.push(await (handler as any)(...handlerParams));
}
return result;
}
public async runParallelHandlers<
HookCategoryNameT extends keyof HardhatHooks,
HookNameT extends keyof HardhatHooks[HookCategoryNameT],
HookT extends HardhatHooks[HookCategoryNameT][HookNameT],
>(
hookCategoryName: HookCategoryNameT,
hookName: HookNameT,
params: InitialHookParams<HookCategoryNameT, HookT>,
): Promise<
Array<Awaited<Return<HardhatHooks[HookCategoryNameT][HookNameT]>>>
> {
// The ordering of handlers is unimportant here, as they are run in parallel
const handlers = await this.#getHandlersInChainedRunningOrder(
hookCategoryName,
hookName,
);
let handlerParams: any;
if (hookCategoryName !== "config") {
assertHardhatInvariant(
this.#context !== undefined,
"Context must be set before running non-config hooks",
);
handlerParams = [this.#context, ...params];
} else {
handlerParams = params;
}
return Promise.all(
handlers.map((handler) => (handler as any)(...handlerParams)),
);
}
public async hasHandlers<
HookCategoryNameT extends keyof HardhatHooks,
HookNameT extends keyof HardhatHooks[HookCategoryNameT],
>(
hookCategoryName: HookCategoryNameT,
hookName: HookNameT,
): Promise<boolean> {
// The ordering of handlers is unimportant here, as we only check if any exist
const handlers = await this.#getHandlersInChainedRunningOrder(
hookCategoryName,
hookName,
);
return handlers.length > 0;
}
async #getHandlersInChainedRunningOrder<
HookCategoryNameT extends keyof HardhatHooks,
HookNameT extends keyof HardhatHooks[HookCategoryNameT],
>(
hookCategoryName: HookCategoryNameT,
hookName: HookNameT,
): Promise<Array<HardhatHooks[HookCategoryNameT][HookNameT]>> {
const pluginHooks = await this.#getPluginHooks(hookCategoryName, hookName);
const dynamicHooks = await this.#getDynamicHooks(
hookCategoryName,
hookName,
);
return [...dynamicHooks, ...pluginHooks];
}
async #getHandlersInSequentialRunningOrder<
HookCategoryNameT extends keyof HardhatHooks,
HookNameT extends keyof HardhatHooks[HookCategoryNameT],
>(
hookCategoryName: HookCategoryNameT,
hookName: HookNameT,
): Promise<Array<HardhatHooks[HookCategoryNameT][HookNameT]>> {
const handlersInChainedOrder = await this.#getHandlersInChainedRunningOrder(
hookCategoryName,
hookName,
);
return handlersInChainedOrder.reverse();
}
async #getDynamicHooks<
HookCategoryNameT extends keyof HardhatHooks,
HookNameT extends keyof HardhatHooks[HookCategoryNameT],
>(
hookCategoryName: HookCategoryNameT,
hookName: HookNameT,
): Promise<Array<HardhatHooks[HookCategoryNameT][HookNameT]>> {
const categories = this.#dynamicHookHandlerCategories.get(
hookCategoryName,
) as Array<Partial<HardhatHooks[HookCategoryNameT]>> | undefined;
if (categories === undefined) {
return [];
}
return categories.flatMap((hookCategory) => {
return (hookCategory[hookName] ?? []) as Array<
HardhatHooks[HookCategoryNameT][HookNameT]
>;
});
}
async #getPluginHooks<
HookCategoryNameT extends keyof HardhatHooks,
HookNameT extends keyof HardhatHooks[HookCategoryNameT],
>(
hookCategoryName: HookCategoryNameT,
hookName: HookNameT,
): Promise<Array<HardhatHooks[HookCategoryNameT][HookNameT]>> {
const categories: Array<
Partial<HardhatHooks[HookCategoryNameT]> | undefined
> = await this.#mutex.exclusiveRun(async () => {
return Promise.all(
this.#pluginsInReverseOrder.map(async (plugin) => {
const existingCategory = this.#staticHookHandlerCategories
.get(plugin.id)
?.get(hookCategoryName);
if (existingCategory !== undefined) {
return existingCategory as Partial<HardhatHooks[HookCategoryNameT]>;
}
const hookHandlerCategoryFactory =
plugin.hookHandlers?.[hookCategoryName];
if (hookHandlerCategoryFactory === undefined) {
return;
}
let factory;
try {
factory = (await hookHandlerCategoryFactory()).default;
} catch (error) {
ensureError(error);
await detectPluginNpmDependencyProblems(
this.#projectRoot,
plugin,
error,
);
throw error;
}
assertHardhatInvariant(
typeof factory === "function",
`Plugin ${plugin.id} doesn't export a hook factory for category ${hookCategoryName}`,
);
const hookCategory = await factory();
assertHardhatInvariant(
hookCategory !== null && typeof hookCategory === "object",
`Plugin ${plugin.id} doesn't export a valid factory for category ${hookCategoryName}, it didn't return an object`,
);
if (!this.#staticHookHandlerCategories.has(plugin.id)) {
this.#staticHookHandlerCategories.set(plugin.id, new Map());
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Defined right above
this.#staticHookHandlerCategories
.get(plugin.id)!
.set(hookCategoryName, hookCategory);
return hookCategory;
}),
);
});
return categories.flatMap((category) => {
const handler = category?.[hookName];
if (handler === undefined) {
return [];
}
return handler as HardhatHooks[HookCategoryNameT][HookNameT];
});
}
}