@nomicfoundation/hardhat-foundry
Version:
Hardhat plugin that provides compatibility with Foundry-based projects remappings.
119 lines (101 loc) • 3.46 kB
text/typescript
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 },
);
}
}