UNPKG

@nomicfoundation/hardhat-foundry

Version:

Hardhat plugin that provides compatibility with Foundry-based projects remappings.

119 lines (101 loc) 3.46 kB
import { exec } from "node:child_process"; import * as path from "node:path"; import { promisify } from "node:util"; import { HardhatError } from "@nomicfoundation/hardhat-errors"; import { ensureError } from "@nomicfoundation/hardhat-utils/error"; import { exists } from "@nomicfoundation/hardhat-utils/fs"; const execAsync = promisify(exec); const FORGE_TIMEOUT_MS = 500; const FORGE_VERSION_TIMEOUT_MS = 500; // 0.5 seconds // Type for the exec function (used for dependency injection in tests) export type ExecAsyncFn = ( command: string, options?: any, ) => Promise<{ stdout: string; stderr: string }>; // Module-level mock for testing let execMock: ExecAsyncFn | undefined; /** * Set a mock exec function for testing. * @param mockExec - The mock exec function to use. */ export function setExecMock(mockExec: ExecAsyncFn): void { execMock = mockExec; } /** * Reset the mock exec function. */ export function resetExecMock(): void { execMock = undefined; } /** * Check if a package has a foundry.toml configuration file. * * @param packagePath - The absolute filesystem path to the package root. * @returns True if foundry.toml exists in the package. */ export async function hasFoundryConfig(packagePath: string): Promise<boolean> { const foundryTomlPath = path.join(packagePath, "foundry.toml"); return await exists(foundryTomlPath); } /** * Check if forge is installed and available in PATH. * * @returns True if forge is installed, false otherwise. */ export async function isForgeInstalled(): Promise<boolean> { const _execAsync: ExecAsyncFn = execMock ?? execAsync; try { await _execAsync("forge --version", { timeout: FORGE_VERSION_TIMEOUT_MS }); return true; } catch { return false; } } /** * Execute `forge remappings` in the given package directory to get Solidity remappings. * * @param packagePath - The absolute filesystem path to the package root. * @returns An array of remapping strings (e.g., ["@openzeppelin/=lib/openzeppelin-contracts/"]). * @throws {HardhatError} If the command fails. */ export async function getForgeRemappings( packagePath: string, ): Promise<string[]> { const _execAsync: ExecAsyncFn = execMock ?? execAsync; try { const { stdout, stderr } = await _execAsync("forge remappings", { cwd: packagePath, timeout: FORGE_TIMEOUT_MS, env: process.env, }); // If there's stderr output but no stdout, it might be an error if (stderr.length > 0 && stdout.length === 0) { throw new HardhatError( HardhatError.ERRORS.HARDHAT_FOUNDRY.GENERAL.FORGE_REMAPPINGS_FAILED, { packagePath, stderr: stderr.trim() }, ); } // Parse the output into individual remappings const remappings = stdout .split(/\r?\n/) // Handle both Unix and Windows line endings .map((line) => line.trim()) .filter((line) => line.length > 0); return remappings; } catch (error) { ensureError(error); // Re-throw HardhatErrors as-is, as it's a FORGE_REMAPPINGS_FAILED error if (HardhatError.isHardhatError(error)) { throw error; } const errorMessage = "stderr" in error && typeof error.stderr === "string" && error.stderr.length > 0 ? error.stderr : error.message; throw new HardhatError( HardhatError.ERRORS.HARDHAT_FOUNDRY.GENERAL.FORGE_REMAPPINGS_FAILED, { packagePath, stderr: errorMessage }, ); } }