hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
306 lines • 14.5 kB
JavaScript
import { assertHardhatInvariant, HardhatError, } 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 {
#mutex = new AsyncMutex();
#projectRoot;
/**
* The context passed to hook handlers, except to the `config` ones, to break
* a circular dependency between the config and the hook handler.
*
* Initially `undefined` to be able to run the config hooks during
* initialization.
*/
#context;
/**
* Plugins that provide hook handlers for each category, in reverse order.
*
* Precomputed from the plugin list at construction.
*/
#pluginsByHookCategory = new Map();
/**
* Cached resolved category objects per hook category in reverse plugin
* order.
*
* Only written by #getStaticHookHandlerCategories, which uses a mutex to
* ensure that every Hook Category Factory is run once per HookManager
* instance.
*/
#resolvedStaticCategories = new Map();
/**
* A map of the dynamically registered handler categories.
*
* Each array is a list of categories, in reverse order of registration.
*
* Written by registerHandlers and unregisterHandlers.
*/
#dynamicHookHandlerCategories = new Map();
/**
* Cached combined (dynamic + static) handlers per (category, hook name) in
* chained running order.
*
* Only written by #getHandlersInChainedRunningOrder, and invalidated
* per-category on dynamic handlers register/unregister.
*/
#chainedHandlers = new Map();
/**
* Cached combined handlers per (category, hook name) in sequential running
* order (reverse of chained).
*
* Only written by #getHandlersInSequentialRunningOrder, and invalidated
* per-category on dynamic handlers register/unregister.
*/
#sequentialHandlers = new Map();
constructor(projectRoot, plugins) {
this.#projectRoot = projectRoot;
for (const plugin of plugins.toReversed()) {
if (plugin.hookHandlers === undefined) {
continue;
}
for (const hookCategoryName of Object.keys(plugin.hookHandlers)) {
if (plugin.hookHandlers[hookCategoryName] === undefined) {
continue;
}
let pluginsForCategory = this.#pluginsByHookCategory.get(hookCategoryName);
if (pluginsForCategory === undefined) {
pluginsForCategory = [];
this.#pluginsByHookCategory.set(hookCategoryName, pluginsForCategory);
}
pluginsForCategory.push(plugin);
}
}
}
setContext(context) {
this.#context = context;
}
registerHandlers(hookCategoryName, hookHandlerCategory) {
let categories = this.#dynamicHookHandlerCategories.get(hookCategoryName);
if (categories === undefined) {
categories = [];
this.#dynamicHookHandlerCategories.set(hookCategoryName, categories);
}
categories.unshift(hookHandlerCategory);
this.#invalidateResolvedHandlersCache(hookCategoryName);
}
unregisterHandlers(hookCategoryName, hookHandlerCategory) {
const categories = this.#dynamicHookHandlerCategories.get(hookCategoryName);
if (categories === undefined) {
return;
}
this.#dynamicHookHandlerCategories.set(hookCategoryName, categories.filter((c) => c !== hookHandlerCategory));
this.#invalidateResolvedHandlersCache(hookCategoryName);
}
async runHandlerChain(hookCategoryName, hookName, params, defaultImplementation) {
// Synchronous fast path for already cached handlers. This duplicates
// the check inside #getHandlersInChainedRunningOrder on purpose:
// calling that async method introduces a microtask tick even on a
// cache hit, whereas a direct Map lookup stays on the current tick.
// That tick matters here because runHandlerChain is on every hook's
// hot path, and this path pairs with the empty-handlers shortcut
// below to dispatch straight to defaultImplementation with no awaits.
const cachedHandlers = this.#chainedHandlers
.get(hookCategoryName)
?.get(hookName);
const handlers = cachedHandlers ??
(await this.#getHandlersInChainedRunningOrder(hookCategoryName, hookName));
let handlerParams;
if (hookCategoryName !== "config") {
assertHardhatInvariant(this.#context !== undefined, "Context must be set before running non-config hooks");
handlerParams = [this.#context, ...params];
}
else {
handlerParams = params;
}
// Fast path for the common case of no registered handlers: skip building
// handlerParams and the `next` closure, and call the default implementation
// directly.
if (handlers.length === 0) {
return (await defaultImplementation(...handlerParams));
}
const numberOfHandlers = handlers.length;
let index = 0;
const next = async (...nextParams) => {
const result = index < numberOfHandlers
? await handlers[index++](...nextParams, next)
: await defaultImplementation(...nextParams);
return result;
};
return await next(...handlerParams);
}
async runSequentialHandlers(hookCategoryName, hookName, params) {
const handlers = await this.#getHandlersInSequentialRunningOrder(hookCategoryName, hookName);
let handlerParams;
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(...handlerParams));
}
return result;
}
async runParallelHandlers(hookCategoryName, hookName, params) {
// The ordering of handlers is unimportant here, as they are run in parallel
const handlers = await this.#getHandlersInChainedRunningOrder(hookCategoryName, hookName);
let handlerParams;
if (hookCategoryName !== "config") {
assertHardhatInvariant(this.#context !== undefined, "Context must be set before running non-config hooks");
handlerParams = [this.#context, ...params];
}
else {
handlerParams = params;
}
return await Promise.all(handlers.map((handler) => handler(...handlerParams)));
}
async hasHandlers(hookCategoryName, hookName) {
// 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(hookCategoryName, hookName) {
let handlersByName = this.#chainedHandlers.get(hookCategoryName);
if (handlersByName === undefined) {
handlersByName = new Map();
this.#chainedHandlers.set(hookCategoryName, handlersByName);
}
const cached = handlersByName.get(hookName);
if (cached !== undefined) {
return cached;
}
const staticCategories = await this.#getStaticHookHandlerCategories(hookCategoryName);
// IMPORTANT NOTE: Accessing the dynamic hook handlers MUST happen
// after awaiting the static ones. See
// #invalidateResolvedHandlersCache for more info.
const dynamicCategories = this.#dynamicHookHandlerCategories.get(hookCategoryName);
const handlers = [];
if (dynamicCategories !== undefined) {
for (const category of dynamicCategories) {
const handler = category[hookName];
if (handler !== undefined) {
handlers.push(handler);
}
}
}
for (const category of staticCategories) {
const handler = category[hookName];
if (handler !== undefined) {
handlers.push(handler);
}
}
handlersByName.set(hookName, handlers);
return handlers;
}
async #getHandlersInSequentialRunningOrder(hookCategoryName, hookName) {
let handlersByName = this.#sequentialHandlers.get(hookCategoryName);
if (handlersByName === undefined) {
handlersByName = new Map();
this.#sequentialHandlers.set(hookCategoryName, handlersByName);
}
const cached = handlersByName.get(hookName);
if (cached !== undefined) {
return cached;
}
const chained = await this.#getHandlersInChainedRunningOrder(hookCategoryName, hookName);
const sequential = chained.toReversed();
handlersByName.set(hookName, sequential);
return sequential;
}
async #getStaticHookHandlerCategories(hookCategoryName) {
const cached = this.#resolvedStaticCategories.get(hookCategoryName);
if (cached !== undefined) {
return cached;
}
const plugins = this.#pluginsByHookCategory.get(hookCategoryName);
// We don't need to get the mutex to resolve this case, as it will always
// be an empty array, and won't execute any factory.
if (plugins === undefined) {
this.#resolvedStaticCategories.set(hookCategoryName, []);
return [];
}
return await this.#mutex.exclusiveRun(async () => {
// Re-check under the mutex in case another caller just populated it.
const recheck = this.#resolvedStaticCategories.get(hookCategoryName);
if (recheck !== undefined) {
return recheck;
}
const resolved = await Promise.all(plugins.map(async (plugin) => await this.#getPluginStaticHookCategory(plugin, hookCategoryName)));
this.#resolvedStaticCategories.set(hookCategoryName, resolved);
return resolved;
});
}
/**
* Returns the hook category object for a plugin that has the hook category
* defined.
*
* @param plugin A plugin that MUST have the given hook category defined.
* @param hookCategoryName The name of the hook category.
* @returns The hook category object.
*/
async #getPluginStaticHookCategory(plugin, hookCategoryName) {
const hookHandlerCategoryFactory = plugin.hookHandlers?.[hookCategoryName];
assertHardhatInvariant(hookHandlerCategoryFactory !== undefined, "#pluginsByHookCategory only contains plugins with this hook category");
let factory;
try {
factory = (await hookHandlerCategoryFactory()).default;
}
catch (error) {
ensureError(error);
await detectPluginNpmDependencyProblems(this.#projectRoot, plugin, error);
throw new HardhatError(HardhatError.ERRORS.CORE.HOOKS.FAILED_TO_LOAD_HOOK_HANDLER_FACTORY, { pluginId: plugin.id, hookCategoryName }, error);
}
assertHardhatInvariant(typeof factory === "function", `Plugin ${plugin.id} doesn't export a hook factory for category ${hookCategoryName}`);
let hookCategory;
try {
hookCategory = await factory();
}
catch (error) {
ensureError(error);
throw new HardhatError(HardhatError.ERRORS.CORE.HOOKS.FAILED_TO_RUN_HOOK_HANDLER_FACTORY, { pluginId: plugin.id, hookCategoryName }, error);
}
assertHardhatInvariant(hookCategory !== null && typeof hookCategory === "object", `Plugin ${plugin.id} doesn't export a valid factory for category ${hookCategoryName}, it didn't return an object`);
return hookCategory;
}
#invalidateResolvedHandlersCache(hookCategoryName) {
// Invalidation deletes the outer entry rather than clearing the inner
// map. This matters under concurrency.
//
// A reader of #getHandlersInChainedRunningOrder (or its sequential
// sibling) captures a reference to the inner map before awaiting the
// static categories, and writes its computed array back after the
// await. If invalidation runs during that await, deleting the outer
// entry leaves the reader's inner map orphaned: its write lands in a
// map no longer reachable from #chainedHandlers/#sequentialHandlers,
// so it cannot poison the shared cache. The next reader sees
// `undefined`, installs a fresh inner map, and rebuilds from the
// current dynamic state.
//
// Two distinct properties make this safe, guaranteed by two different
// things:
//
// 1. The in-flight reader's own return value is correct. This is
// because #getHandlersInChainedRunningOrder reads
// #dynamicHookHandlerCategories *after* awaiting the static
// categories. Any invalidation that happened during the await is
// visible to the reader when it resumes, so the array it builds
// reflects the current dynamic state.
//
// 2. The shared cache never holds a stale array. This is guaranteed
// by the orphaning-by-delete described above: a reader that
// started before the invalidation can only write into an
// unreachable inner map.
//
// Property 1 depends on the ordering of the dynamic handlers read relative
// to the await. If that read ever moved *before* the await, a reader
// could build a stale array and return it to its caller — the cache
// would still be protected by property 2, but the reader's caller
// would see the stale result.
this.#chainedHandlers.delete(hookCategoryName);
this.#sequentialHandlers.delete(hookCategoryName);
}
}
//# sourceMappingURL=hook-manager.js.map