UNPKG

hardhat

Version:

Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.

326 lines (292 loc) 10.9 kB
import type { UnsafeHardhatRuntimeEnvironmentOptions } from "./types.js"; import type { ArtifactManager } from "../../types/artifacts.js"; import type { HardhatUserConfig, HardhatConfig, ProjectPathsUserConfig, ProjectPathsConfig, TestPathsConfig, SourcePathsConfig, } from "../../types/config.js"; import type { GlobalOptions, GlobalOptionDefinitions, } from "../../types/global-options.js"; import type { HardhatUserConfigValidationError, HookContext, HookManager, } from "../../types/hooks.js"; import type { HardhatRuntimeEnvironment } from "../../types/hre.js"; import type { NetworkManager } from "../../types/network.js"; import type { HardhatPlugin } from "../../types/plugins.js"; import type { SolidityBuildSystem } from "../../types/solidity/build-system.js"; import type { TaskManager } from "../../types/tasks.js"; import type { UserInterruptionManager } from "../../types/user-interruptions.js"; import type { CoverageManager } from "../builtin-plugins/coverage/types.js"; import type { GasAnalyticsManager } from "../builtin-plugins/gas-analytics/types.js"; import { HardhatError } from "@nomicfoundation/hardhat-errors"; import { findClosestPackageRoot } from "@nomicfoundation/hardhat-utils/package"; import { resolveFromRoot } from "@nomicfoundation/hardhat-utils/path"; import { getEdrVersion, getHardhatVersion } from "../utils/package.js"; import { validateUserConfig } from "./config-validation.js"; import { resolveConfigurationVariable } from "./configuration-variables.js"; import { buildGlobalOptionDefinitions, resolveGlobalOptions, } from "./global-options.js"; import { HookManagerImplementation } from "./hook-manager.js"; import { resolvePluginList } from "./plugins/resolve-plugin-list.js"; import { TaskManagerImplementation } from "./tasks/task-manager.js"; import { UserInterruptionManagerImplementation } from "./user-interruptions.js"; export class HardhatRuntimeEnvironmentImplementation implements HardhatRuntimeEnvironment { // NOTE: This is a small architectural violation, as these shouldn't be needed // here, because they are added by plugins. But as those plugins are builtin, // their type extensions also affect this module. public network!: NetworkManager; public artifacts!: ArtifactManager; public solidity!: SolidityBuildSystem; // NOTE: These underscore-prefixed properties are architectural violations intended // for internal use only. They are declared here rather than through module // augmentation to hide them from TypeScript users (keeping them out of the public // HardhatRuntimeEnvironment interface). They are initialized by their respective // plugins in the `created` hook. public _coverage!: CoverageManager; public _gasAnalytics!: GasAnalyticsManager; public static async create( inputUserConfig: HardhatUserConfig, userProvidedGlobalOptions: Partial<GlobalOptions>, projectRoot?: string, unsafeOptions?: UnsafeHardhatRuntimeEnvironmentOptions, ): Promise<HardhatRuntimeEnvironmentImplementation> { const resolvedProjectRoot = await resolveProjectRoot(projectRoot); const resolvedPlugins = unsafeOptions?.resolvedPlugins ?? (await resolvePluginList(resolvedProjectRoot, inputUserConfig.plugins)); const [hardhatVersion, edrVersion] = await Promise.all([ getHardhatVersion(), getEdrVersion(), ]); const versions = { hardhat: hardhatVersion, edr: edrVersion, }; const hooks = new HookManagerImplementation( resolvedProjectRoot, resolvedPlugins, ); const configResolutionResult = await resolveUserConfigToHardhatConfig( inputUserConfig, hooks, resolvedProjectRoot, userProvidedGlobalOptions.config, resolvedPlugins, ); if (!configResolutionResult.success) { throw new HardhatError(HardhatError.ERRORS.CORE.GENERAL.INVALID_CONFIG, { errors: `\t${configResolutionResult.userConfigValidationErrors .map( (error) => `* Config error in config.${error.path.join(".")}: ${error.message}`, ) .join("\n\t")}`, }); } const { config, extendedUserConfig } = configResolutionResult; const globalOptionDefinitions = unsafeOptions?.globalOptionDefinitions ?? buildGlobalOptionDefinitions(resolvedPlugins); const globalOptions = resolveGlobalOptions( userProvidedGlobalOptions, globalOptionDefinitions, ); // Set the HookContext in the hook manager so that non-config hooks can // use it const interruptions = new UserInterruptionManagerImplementation(hooks); const hre = new HardhatRuntimeEnvironmentImplementation( extendedUserConfig, config, hooks, interruptions, globalOptions, versions, globalOptionDefinitions, ); // We create an object with the HRE as its prototype, and overwrite the // tasks property with undefined, so that hooks don't have access to the // task runner. // // The reason we do this with a prototype instead of a shallow copy is that // the handlers hooked into hre/created may assign new properties to the // HRE and we want those to be accessible to all the handlers. const hookContext: HookContext = Object.create(hre, { tasks: { value: undefined }, }); hooks.setContext(hookContext); await hooks.runSequentialHandlers("hre", "created", [hre]); return hre; } public readonly tasks: TaskManager; private constructor( public readonly userConfig: HardhatUserConfig, public readonly config: HardhatConfig, public readonly hooks: HookManager, public readonly interruptions: UserInterruptionManager, public readonly globalOptions: GlobalOptions, public readonly versions: { readonly hardhat: string; readonly edr: string; }, globalOptionDefinitions: GlobalOptionDefinitions, ) { this.tasks = new TaskManagerImplementation(this, globalOptionDefinitions); } } /** * Resolves the project root of a Hardhat project based on the config file or * another path within the project. If not provided, it will be resolved from * the current working directory. * * @param absolutePathWithinProject An absolute path within the project, usually * the config file. */ export async function resolveProjectRoot( absolutePathWithinProject: string | undefined, ): Promise<string> { return findClosestPackageRoot(absolutePathWithinProject ?? process.cwd()); } /** * Runs the provided Hardhat user config through the resolution process, * invoking relevant plugin hooks (both internal and external) to extend * and transform the config into a full HardhatConfig. * * @param hooks - The HookManager used to run config extension and validation * hooks. * @param inputUserConfig - The initial user provided Hardhat config object. * @param resolvedProjectRoot - The project root path. * @param userProvidedConfigPath - The user provided Hardhat config file path. * @param resolvedPlugins - The list of plugins, we do not want the plugins * overwriting them so we re-add them to the final HardhatConfig. * @returns Either an object containing the resolved HardhatConfig and the * extended version of the user config, or a list of validation errors. */ export async function resolveUserConfigToHardhatConfig( inputUserConfig: HardhatUserConfig, hooks: HookManager, resolvedProjectRoot: string, userProvidedConfigPath: string | undefined, resolvedPlugins: HardhatPlugin[], ): Promise< | { success: true; config: HardhatConfig; extendedUserConfig: HardhatUserConfig; } | { success: false; userConfigValidationErrors: HardhatUserConfigValidationError[]; } > { // extend user config: const extendedUserConfig = await runUserConfigExtensions( hooks, inputUserConfig, ); // validate config const userConfigValidationErrors = await validateUserConfig( hooks, extendedUserConfig, ); if (userConfigValidationErrors.length > 0) { return { success: false, userConfigValidationErrors, }; } // Resolve config const resolvedConfig = await resolveUserConfig( resolvedProjectRoot, userProvidedConfigPath, hooks, resolvedPlugins, extendedUserConfig, ); // We override the plugins and the project root, as we want to prevent // the plugins from changing them const config: HardhatConfig = { ...resolvedConfig, paths: { ...resolvedConfig.paths, root: resolvedProjectRoot, }, plugins: resolvedPlugins, }; return { success: true, config, extendedUserConfig }; } async function runUserConfigExtensions( hooks: HookManager, config: HardhatUserConfig, ): Promise<HardhatUserConfig> { return hooks.runHandlerChain( "config", "extendUserConfig", [config], async (c) => { return c; }, ); } async function resolveUserConfig( projectRoot: string, configPath: string | undefined, hooks: HookManager, sortedPlugins: HardhatPlugin[], config: HardhatUserConfig, ): Promise<HardhatConfig> { /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- The config resolution is type-unsafe, as plugins augment the HardhatConfig type. This means that: (1) we can't fully initialize a valid HardhatConfig here, and (2) when writing a hook handler, the value returned by next() is probably invalid with respect to your own augmentations. */ const initialResolvedConfig = { plugins: sortedPlugins, tasks: config.tasks ?? [], paths: resolvePaths(projectRoot, configPath, config.paths), } as HardhatConfig; return hooks.runHandlerChain( "config", "resolveUserConfig", [config, (variable) => resolveConfigurationVariable(hooks, variable)], async (_, __) => { return initialResolvedConfig; }, ); } function resolvePaths( projectRoot: string, configPath: string | undefined, userProvidedPaths: ProjectPathsUserConfig = {}, ): ProjectPathsConfig { return { root: projectRoot, config: configPath !== undefined ? resolveFromRoot(projectRoot, configPath) : undefined, cache: resolveFromRoot(projectRoot, userProvidedPaths.cache ?? "cache"), artifacts: resolveFromRoot( projectRoot, userProvidedPaths.artifacts ?? "artifacts", ), /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We cast as the builtin plugins' type extensions are also applied here, making an empty object incompatible, but it's the correct value when you ignore the plugins. */ tests: {} as TestPathsConfig, /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- See the comment in tests. */ sources: {} as SourcePathsConfig, }; }