UNPKG

hardhat

Version:

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

365 lines (307 loc) 11 kB
import type StackTraceParserT from "stacktrace-parser"; import picocolors from "picocolors"; import debug from "debug"; import fsExtra from "fs-extra"; import path from "path"; import semver from "semver"; import { HardhatArguments, HardhatConfig, HardhatUserConfig, SolcConfig, } from "../../../types"; import { HardhatContext } from "../../context"; import { findClosestPackageJson } from "../../util/packageInfo"; import { HardhatError } from "../errors"; import { ERRORS } from "../errors-list"; import { getUserConfigPath } from "../project-structure"; import { SUPPORTED_SOLIDITY_VERSION_RANGE } from "../../hardhat-network/stack-traces/constants"; import { resolveConfig } from "./config-resolution"; import { DEFAULT_SOLC_VERSION } from "./default-config"; const log = debug("hardhat:core:config"); export function importCsjOrEsModule(filePath: string): any { try { const imported = require(filePath); return imported.default !== undefined ? imported.default : imported; } catch (e: any) { // An ESM project that has a Hardhat config with a .js extension will fail to be loaded, // because Hardhat configs can only be CJS but a .js extension will be interpreted as ESM. // The kind of error we get in these cases depends on the Node.js version. const node20Heuristic = e.code === "ERR_REQUIRE_ESM"; // Also works for node v24 const node22Heuristic = e.message === "module is not defined" || e.message === "require is not defined"; if (node20Heuristic || node22Heuristic) { throw new HardhatError( ERRORS.GENERAL.ESM_PROJECT_WITHOUT_CJS_CONFIG, {}, e ); } // eslint-disable-next-line @nomicfoundation/hardhat-internal-rules/only-hardhat-error throw e; } } export function resolveConfigPath(configPath: string | undefined) { if (configPath === undefined) { configPath = getUserConfigPath(); } else { if (!path.isAbsolute(configPath)) { configPath = path.join(process.cwd(), configPath); configPath = path.normalize(configPath); } } return configPath; } export function loadConfigAndTasks( hardhatArguments?: Partial<HardhatArguments>, { showEmptyConfigWarning = false, showSolidityConfigWarnings = false, }: { showEmptyConfigWarning?: boolean; showSolidityConfigWarnings?: boolean; } = { showEmptyConfigWarning: false, showSolidityConfigWarnings: false, } ): { resolvedConfig: HardhatConfig; userConfig: HardhatUserConfig } { const { validateConfig, validateResolvedConfig } = require("./config-validation") as typeof import("./config-validation"); let configPath = hardhatArguments !== undefined ? hardhatArguments.config : undefined; configPath = resolveConfigPath(configPath); log(`Loading Hardhat config from ${configPath}`); // Before loading the builtin tasks, the default and user's config we expose // the config env in the global object. const configEnv = require("./config-env"); const globalAsAny: any = global; Object.entries(configEnv).forEach( ([key, value]) => (globalAsAny[key] = value) ); const ctx = HardhatContext.getHardhatContext(); ctx.setConfigLoadingAsStarted(); let userConfig; try { require("../tasks/builtin-tasks"); userConfig = importCsjOrEsModule(configPath); } catch (e) { analyzeModuleNotFoundError(e, configPath); // eslint-disable-next-line @nomicfoundation/hardhat-internal-rules/only-hardhat-error throw e; } finally { ctx.setConfigLoadingAsFinished(); } if (showEmptyConfigWarning) { checkEmptyConfig(userConfig, { showSolidityConfigWarnings }); } validateConfig(userConfig); if (showSolidityConfigWarnings) { checkMissingSolidityConfig(userConfig); } // To avoid bad practices we remove the previously exported stuff Object.keys(configEnv).forEach((key) => (globalAsAny[key] = undefined)); const frozenUserConfig = deepFreezeUserConfig(userConfig); const resolved = resolveConfig(configPath, userConfig); for (const extender of HardhatContext.getHardhatContext().configExtenders) { extender(resolved, frozenUserConfig); } validateResolvedConfig(resolved); if (showSolidityConfigWarnings) { checkUnsupportedSolidityConfig(resolved); checkUnsupportedRemappings(resolved); } return { resolvedConfig: resolved, userConfig: frozenUserConfig }; } function deepFreezeUserConfig( config: any, propertyPath: Array<string | number | symbol> = [] ) { if (typeof config !== "object" || config === null) { return config; } return new Proxy(config, { get(target: any, property: string | number | symbol, receiver: any): any { return deepFreezeUserConfig(Reflect.get(target, property, receiver), [ ...propertyPath, property, ]); }, set( target: any, property: string | number | symbol, _value: any, _receiver: any ): boolean { throw new HardhatError(ERRORS.GENERAL.USER_CONFIG_MODIFIED, { path: [...propertyPath, property] .map((pathPart) => pathPart.toString()) .join("."), }); }, }); } /** * Receives an Error and checks if it's a MODULE_NOT_FOUND and the reason that * caused it. * * If it can infer the reason, it throws an appropriate error. Otherwise it does * nothing. */ export function analyzeModuleNotFoundError(error: any, configPath: string) { const stackTraceParser = require("stacktrace-parser") as typeof StackTraceParserT; if (error.code !== "MODULE_NOT_FOUND") { return; } const stackTrace = stackTraceParser.parse(error.stack); const throwingFile = stackTrace .filter((x) => x.file !== null) .map((x) => x.file!) // ignore frames related to source map support .filter((x) => !x.includes(path.join("@cspotcode", "source-map-support"))) .find((x) => path.isAbsolute(x)); if (throwingFile === null || throwingFile === undefined) { return; } // if the error comes from the config file, we ignore it because we know it's // a direct import that's missing if (throwingFile === configPath) { return; } const packageJsonPath = findClosestPackageJson(throwingFile); if (packageJsonPath === undefined) { return; } const packageJson = fsExtra.readJsonSync(packageJsonPath); const peerDependencies: { [name: string]: string } = packageJson.peerDependencies ?? {}; if (peerDependencies["@nomiclabs/buidler"] !== undefined) { throw new HardhatError(ERRORS.PLUGINS.BUIDLER_PLUGIN, { plugin: packageJson.name, }); } // if the problem doesn't come from a hardhat plugin, we ignore it if (peerDependencies.hardhat === undefined) { return; } const missingPeerDependencies: { [name: string]: string } = {}; for (const [peerDependency, version] of Object.entries(peerDependencies)) { const peerDependencyPackageJson = readPackageJson( peerDependency, configPath ); if (peerDependencyPackageJson === undefined) { missingPeerDependencies[peerDependency] = version; } } const missingPeerDependenciesNames = Object.keys(missingPeerDependencies); if (missingPeerDependenciesNames.length > 0) { throw new HardhatError(ERRORS.PLUGINS.MISSING_DEPENDENCIES, { plugin: packageJson.name, missingDependencies: missingPeerDependenciesNames.join(", "), missingDependenciesVersions: Object.entries(missingPeerDependencies) .map(([name, version]) => `"${name}@${version}"`) .join(" "), }); } } interface PackageJson { name: string; version: string; peerDependencies?: { [name: string]: string; }; } function readPackageJson( packageName: string, configPath: string ): PackageJson | undefined { const resolve = require("resolve") as typeof import("resolve"); try { const packageJsonPath = resolve.sync( path.join(packageName, "package.json"), { basedir: path.dirname(configPath), } ); return require(packageJsonPath); } catch { return undefined; } } function checkEmptyConfig( userConfig: any, { showSolidityConfigWarnings }: { showSolidityConfigWarnings: boolean } ) { if (userConfig === undefined || Object.keys(userConfig).length === 0) { let warning = `Hardhat config is returning an empty config object, check the export from the config file if this is unexpected.\n`; // This 'learn more' section is also printed by the solidity config warning, // so we need to check to avoid printing it twice if (!showSolidityConfigWarnings) { warning += `\nLearn more about configuring Hardhat at https://hardhat.org/config\n`; } console.warn(picocolors.yellow(warning)); } } function checkMissingSolidityConfig(userConfig: any) { if (userConfig.solidity === undefined) { console.warn( picocolors.yellow( `Solidity compiler is not configured. Version ${DEFAULT_SOLC_VERSION} will be used by default. Add a 'solidity' entry to your configuration to suppress this warning. Learn more about compiler configuration at https://hardhat.org/config ` ) ); } } function checkUnsupportedSolidityConfig(resolvedConfig: HardhatConfig) { const configuredCompilers = getConfiguredCompilers(resolvedConfig.solidity); const solcVersions = configuredCompilers.map((x) => x.version); const unsupportedVersions: string[] = []; for (const solcVersion of solcVersions) { if ( !semver.satisfies(solcVersion, SUPPORTED_SOLIDITY_VERSION_RANGE) && !unsupportedVersions.includes(solcVersion) ) { unsupportedVersions.push(solcVersion); } } if (unsupportedVersions.length > 0) { console.warn( picocolors.yellow( `Solidity ${unsupportedVersions.join(", ")} ${ unsupportedVersions.length === 1 ? "is" : "are" } not fully supported yet. You can still use Hardhat, but some features, like stack traces, might not work correctly. Learn more at https://hardhat.org/hardhat-runner/docs/reference/solidity-support ` ) ); } } function checkUnsupportedRemappings({ solidity }: HardhatConfig) { const solcConfigs = [ ...solidity.compilers, ...Object.values(solidity.overrides), ]; const remappings = solcConfigs.filter( ({ settings }) => settings.remappings !== undefined ); if (remappings.length > 0) { console.warn( picocolors.yellow( `Solidity remappings are not currently supported; you may experience unexpected compilation results. Remove any 'remappings' fields from your configuration to suppress this warning. Learn more about compiler configuration at https://hardhat.org/config ` ) ); } } export function getConfiguredCompilers( solidityConfig: HardhatConfig["solidity"] ): SolcConfig[] { const compilerVersions = solidityConfig.compilers; const overrideVersions = Object.values(solidityConfig.overrides); return [...compilerVersions, ...overrideVersions]; }