hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
684 lines (573 loc) • 19.1 kB
text/typescript
import chalk from "chalk";
import fsExtra from "fs-extra";
import path from "path";
import { HARDHAT_NAME } from "../constants";
import { assertHardhatInvariant, HardhatError } from "../core/errors";
import { ERRORS } from "../core/errors-list";
import { getRecommendedGitIgnore } from "../core/project-structure";
import { getAllFilesMatching } from "../util/fs-utils";
import { hasConsentedTelemetry } from "../util/global-dir";
import { fromEntries } from "../util/lang";
import {
getPackageJson,
getPackageRoot,
PackageJson,
} from "../util/packageInfo";
import { pluralize } from "../util/strings";
import { isRunningOnCiServer } from "../util/ci-detection";
import {
confirmRecommendedDepsInstallation,
confirmProjectCreation,
} from "./prompt";
import { emoji } from "./emoji";
import { Dependencies, PackageManager } from "./types";
import { requestTelemetryConsent } from "./analytics";
enum Action {
CREATE_JAVASCRIPT_PROJECT_ACTION = "Create a JavaScript project",
CREATE_TYPESCRIPT_PROJECT_ACTION = "Create a TypeScript project",
CREATE_TYPESCRIPT_VIEM_PROJECT_ACTION = "Create a TypeScript project (with Viem)",
CREATE_EMPTY_HARDHAT_CONFIG_ACTION = "Create an empty hardhat.config.js",
QUIT_ACTION = "Quit",
}
type SampleProjectTypeCreationAction =
| Action.CREATE_JAVASCRIPT_PROJECT_ACTION
| Action.CREATE_TYPESCRIPT_PROJECT_ACTION
| Action.CREATE_TYPESCRIPT_VIEM_PROJECT_ACTION;
const HARDHAT_PACKAGE_NAME = "hardhat";
const PROJECT_DEPENDENCIES: Dependencies = {};
const ETHERS_PROJECT_DEPENDENCIES: Dependencies = {
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
};
const VIEM_PROJECT_DEPENDENCIES: Dependencies = {
"@nomicfoundation/hardhat-toolbox-viem": "^3.0.0",
};
const PEER_DEPENDENCIES: Dependencies = {
hardhat: "^2.14.0",
"@nomicfoundation/hardhat-network-helpers": "^1.0.0",
"@nomicfoundation/hardhat-verify": "^2.0.0",
chai: "^4.2.0",
"hardhat-gas-reporter": "^1.0.8",
"solidity-coverage": "^0.8.0",
"@nomicfoundation/hardhat-ignition": "^0.15.0",
};
const ETHERS_PEER_DEPENDENCIES: Dependencies = {
"@nomicfoundation/hardhat-chai-matchers": "^2.0.0",
"@nomicfoundation/hardhat-ethers": "^3.0.0",
ethers: "^6.4.0",
"@typechain/hardhat": "^9.0.0",
typechain: "^8.3.0",
"@typechain/ethers-v6": "^0.5.0",
"@nomicfoundation/hardhat-ignition-ethers": "^0.15.0",
};
const VIEM_PEER_DEPENDENCIES: Dependencies = {
"@nomicfoundation/hardhat-viem": "^2.0.0",
viem: "^2.7.6",
"@nomicfoundation/hardhat-ignition-viem": "^0.15.0",
};
const TYPESCRIPT_DEPENDENCIES: Dependencies = {};
const TYPESCRIPT_PEER_DEPENDENCIES: Dependencies = {
"@types/chai": "^4.2.0",
"@types/mocha": ">=9.1.0",
"@types/node": ">=18.0.0",
"ts-node": ">=8.0.0",
typescript: ">=4.5.0",
};
const TYPESCRIPT_ETHERS_PEER_DEPENDENCIES: Dependencies = {
typescript: ">=4.5.0",
};
const TYPESCRIPT_VIEM_PEER_DEPENDENCIES: Dependencies = {
"@types/chai-as-promised": "^7.1.6",
typescript: "~5.0.4",
};
// generated with the "colossal" font
function printAsciiLogo() {
console.log(
chalk.blue("888 888 888 888 888")
);
console.log(
chalk.blue("888 888 888 888 888")
);
console.log(
chalk.blue("888 888 888 888 888")
);
console.log(
chalk.blue("8888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888")
);
console.log(
chalk.blue('888 888 "88b 888P" d88" 888 888 "88b "88b 888')
);
console.log(
chalk.blue("888 888 .d888888 888 888 888 888 888 .d888888 888")
);
console.log(
chalk.blue("888 888 888 888 888 Y88b 888 888 888 888 888 Y88b.")
);
console.log(
chalk.blue('888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888')
);
console.log("");
}
async function printWelcomeMessage() {
const packageJson = await getPackageJson();
console.log(
chalk.cyan(
`${emoji("👷 ")}Welcome to ${HARDHAT_NAME} v${packageJson.version}${emoji(
" 👷"
)}\n`
)
);
}
async function copySampleProject(
projectRoot: string,
projectType: SampleProjectTypeCreationAction,
isEsm: boolean
) {
const packageRoot = getPackageRoot();
let sampleProjectName: string;
if (projectType === Action.CREATE_JAVASCRIPT_PROJECT_ACTION) {
if (isEsm) {
sampleProjectName = "javascript-esm";
} else {
sampleProjectName = "javascript";
}
} else {
if (isEsm) {
assertHardhatInvariant(
false,
"Shouldn't try to create a TypeScript project in an ESM based project"
);
} else if (projectType === Action.CREATE_TYPESCRIPT_VIEM_PROJECT_ACTION) {
sampleProjectName = "typescript-viem";
} else {
sampleProjectName = "typescript";
}
}
await fsExtra.ensureDir(projectRoot);
const sampleProjectPath = path.join(
packageRoot,
"sample-projects",
sampleProjectName
);
// relative paths to all the sample project files
const sampleProjectFiles = (await getAllFilesMatching(sampleProjectPath)).map(
(file) => path.relative(sampleProjectPath, file)
);
// check if the target directory already has files that clash with the sample
// project files
const existingFiles: string[] = [];
for (const file of sampleProjectFiles) {
const targetProjectFile = path.resolve(projectRoot, file);
// if the project already has a README.md file, we'll skip it when
// we copy the files
if (file !== "README.md" && fsExtra.existsSync(targetProjectFile)) {
existingFiles.push(file);
}
}
if (existingFiles.length > 0) {
const errorMsg = `We couldn't initialize the sample project because ${pluralize(
existingFiles.length,
"this file already exists",
"these files already exist"
)}: ${existingFiles.join(", ")}
Please delete or rename ${pluralize(
existingFiles.length,
"it",
"them"
)} and try again.`;
console.log(chalk.red(errorMsg));
process.exit(1);
}
// copy the files
for (const file of sampleProjectFiles) {
const sampleProjectFile = path.resolve(sampleProjectPath, file);
const targetProjectFile = path.resolve(projectRoot, file);
if (file === "README.md" && fsExtra.existsSync(targetProjectFile)) {
// we don't override the readme if it exists
continue;
}
if (file === "LICENSE.md") {
// we don't copy the license
continue;
}
fsExtra.copySync(sampleProjectFile, targetProjectFile);
}
}
async function addGitIgnore(projectRoot: string) {
const gitIgnorePath = path.join(projectRoot, ".gitignore");
let content = await getRecommendedGitIgnore();
if (await fsExtra.pathExists(gitIgnorePath)) {
const existingContent = await fsExtra.readFile(gitIgnorePath, "utf-8");
content = `${existingContent}
${content}`;
}
await fsExtra.writeFile(gitIgnorePath, content);
}
async function printRecommendedDepsInstallationInstructions(
projectType: SampleProjectTypeCreationAction
) {
console.log(
`You need to install these dependencies to run the sample project:`
);
const cmd = await getRecommendedDependenciesInstallationCommand(
await getDependencies(projectType)
);
console.log(` ${cmd.join(" ")}`);
}
// exported so we can test that it uses the latest supported version of solidity
export const EMPTY_HARDHAT_CONFIG = `/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: "0.8.24",
};
`;
async function writeEmptyHardhatConfig(isEsm: boolean) {
const hardhatConfigFilename = isEsm
? "hardhat.config.cjs"
: "hardhat.config.js";
return fsExtra.writeFile(
hardhatConfigFilename,
EMPTY_HARDHAT_CONFIG,
"utf-8"
);
}
async function getAction(isEsm: boolean): Promise<Action> {
if (
process.env.HARDHAT_CREATE_JAVASCRIPT_PROJECT_WITH_DEFAULTS !== undefined
) {
return Action.CREATE_JAVASCRIPT_PROJECT_ACTION;
} else if (
process.env.HARDHAT_CREATE_TYPESCRIPT_PROJECT_WITH_DEFAULTS !== undefined
) {
return Action.CREATE_TYPESCRIPT_PROJECT_ACTION;
} else if (
process.env.HARDHAT_CREATE_TYPESCRIPT_VIEM_PROJECT_WITH_DEFAULTS !==
undefined
) {
return Action.CREATE_TYPESCRIPT_VIEM_PROJECT_ACTION;
}
const { default: enquirer } = await import("enquirer");
try {
const actionResponse = await enquirer.prompt<{ action: string }>([
{
name: "action",
type: "select",
message: "What do you want to do?",
initial: 0,
choices: Object.values(Action)
.filter((a: Action) => {
if (isEsm && a === Action.CREATE_TYPESCRIPT_VIEM_PROJECT_ACTION) {
// we omit the viem option for ESM projects to avoid showing
// two disabled options
return false;
}
return true;
})
.map((a: Action) => {
let message: string;
if (isEsm) {
if (a === Action.CREATE_EMPTY_HARDHAT_CONFIG_ACTION) {
message = a.replace(".js", ".cjs");
} else if (a === Action.CREATE_TYPESCRIPT_PROJECT_ACTION) {
message = `${a} (not available for ESM projects)`;
} else {
message = a;
}
} else {
message = a;
}
return {
name: a,
message,
value: a,
};
}),
},
]);
if ((Object.values(Action) as string[]).includes(actionResponse.action)) {
return actionResponse.action as Action;
} else {
throw new HardhatError(ERRORS.GENERAL.UNSUPPORTED_OPERATION, {
operation: `Responding with "${actionResponse.action}" to the project initialization wizard`,
});
}
} catch (e) {
if (e === "") {
return Action.QUIT_ACTION;
}
// eslint-disable-next-line @nomicfoundation/hardhat-internal-rules/only-hardhat-error
throw e;
}
}
async function createPackageJson() {
await fsExtra.writeJson(
"package.json",
{
name: "hardhat-project",
},
{ spaces: 2 }
);
}
function showStarOnGitHubMessage() {
console.log(
chalk.cyan("Give Hardhat a star on Github if you're enjoying it!") +
emoji(" ⭐️✨")
);
console.log();
console.log(chalk.cyan(" https://github.com/NomicFoundation/hardhat"));
}
export function showSoliditySurveyMessage() {
if (new Date() > new Date("2024-01-07 23:39")) {
// the survey has finished
return;
}
console.log();
console.log(
chalk.cyan(
"Please take a moment to complete the 2023 Solidity Survey: https://hardhat.org/solidity-survey-2023"
)
);
}
export async function createProject() {
printAsciiLogo();
await printWelcomeMessage();
let packageJson: PackageJson | undefined;
if (await fsExtra.pathExists("package.json")) {
packageJson = await fsExtra.readJson("package.json");
}
const isEsm = packageJson?.type === "module";
const action = await getAction(isEsm);
if (action === Action.QUIT_ACTION) {
return;
}
if (isEsm && action === Action.CREATE_TYPESCRIPT_PROJECT_ACTION) {
throw new HardhatError(ERRORS.GENERAL.ESM_TYPESCRIPT_PROJECT_CREATION);
}
if (packageJson === undefined) {
await createPackageJson();
}
if (action === Action.CREATE_EMPTY_HARDHAT_CONFIG_ACTION) {
await writeEmptyHardhatConfig(isEsm);
console.log(
`${emoji("✨ ")}${chalk.cyan(`Config file created`)}${emoji(" ✨")}`
);
if (!isInstalled(HARDHAT_PACKAGE_NAME)) {
console.log("");
console.log(`You need to install hardhat locally to use it. Please run:`);
const cmd = await getRecommendedDependenciesInstallationCommand({
[HARDHAT_PACKAGE_NAME]: `^${(await getPackageJson()).version}`,
});
console.log("");
console.log(cmd.join(" "));
console.log("");
}
console.log();
showStarOnGitHubMessage();
showSoliditySurveyMessage();
return;
}
let responses: {
projectRoot: string;
shouldAddGitIgnore: boolean;
};
const useDefaultPromptResponses =
process.env.HARDHAT_CREATE_JAVASCRIPT_PROJECT_WITH_DEFAULTS !== undefined ||
process.env.HARDHAT_CREATE_TYPESCRIPT_PROJECT_WITH_DEFAULTS !== undefined ||
process.env.HARDHAT_CREATE_TYPESCRIPT_VIEM_PROJECT_WITH_DEFAULTS !==
undefined;
if (useDefaultPromptResponses) {
responses = {
projectRoot: process.cwd(),
shouldAddGitIgnore: true,
};
} else {
try {
responses = await confirmProjectCreation();
} catch (e) {
if (e === "") {
return;
}
// eslint-disable-next-line @nomicfoundation/hardhat-internal-rules/only-hardhat-error
throw e;
}
}
const { projectRoot, shouldAddGitIgnore } = responses;
if (shouldAddGitIgnore) {
await addGitIgnore(projectRoot);
}
if (
process.env.HARDHAT_DISABLE_TELEMETRY_PROMPT !== "true" &&
!isRunningOnCiServer() &&
hasConsentedTelemetry() === undefined
) {
await requestTelemetryConsent();
}
await copySampleProject(projectRoot, action, isEsm);
let shouldShowInstallationInstructions = true;
if (await canInstallRecommendedDeps()) {
const dependencies = await getDependencies(action);
const recommendedDeps = Object.keys(dependencies);
const dependenciesToInstall = fromEntries(
Object.entries(dependencies).filter(([name]) => !isInstalled(name))
);
const installedRecommendedDeps = recommendedDeps.filter(isInstalled);
const installedExceptHardhat = installedRecommendedDeps.filter(
(name) => name !== HARDHAT_PACKAGE_NAME
);
if (installedRecommendedDeps.length === recommendedDeps.length) {
shouldShowInstallationInstructions = false;
} else if (installedExceptHardhat.length === 0) {
const shouldInstall =
useDefaultPromptResponses ||
(await confirmRecommendedDepsInstallation(
dependenciesToInstall,
await getProjectPackageManager()
));
if (shouldInstall) {
const installed = await installRecommendedDependencies(
dependenciesToInstall
);
if (!installed) {
console.warn(
chalk.red("Failed to install the sample project's dependencies")
);
}
shouldShowInstallationInstructions = !installed;
}
}
}
if (shouldShowInstallationInstructions) {
console.log(``);
await printRecommendedDepsInstallationInstructions(action);
}
console.log(
`\n${emoji("✨ ")}${chalk.cyan("Project created")}${emoji(" ✨")}`
);
console.log();
console.log("See the README.md file for some example tasks you can run");
console.log();
showStarOnGitHubMessage();
showSoliditySurveyMessage();
}
async function canInstallRecommendedDeps() {
return fsExtra.pathExists("package.json");
}
function isInstalled(dep: string) {
const packageJson = fsExtra.readJSONSync("package.json");
const allDependencies = {
...packageJson.dependencies,
...packageJson.devDependencies,
...packageJson.optionalDependencies,
};
return dep in allDependencies;
}
async function isYarnProject() {
return fsExtra.pathExists("yarn.lock");
}
async function isPnpmProject() {
return fsExtra.pathExists("pnpm-lock.yaml");
}
async function getProjectPackageManager(): Promise<PackageManager> {
if (await isYarnProject()) return "yarn";
if (await isPnpmProject()) return "pnpm";
return "npm";
}
async function doesNpmAutoInstallPeerDependencies() {
const { execSync } = require("child_process");
try {
const version: string = execSync("npm --version").toString();
return parseInt(version.split(".")[0], 10) >= 7;
} catch (_) {
return false;
}
}
async function installRecommendedDependencies(dependencies: Dependencies) {
console.log("");
const installCmd = await getRecommendedDependenciesInstallationCommand(
dependencies
);
return installDependencies(installCmd[0], installCmd.slice(1));
}
async function installDependencies(
packageManager: string,
args: string[]
): Promise<boolean> {
const { spawn } = await import("child_process");
console.log(`${packageManager} ${args.join(" ")}`);
const childProcess = spawn(packageManager, args, {
stdio: "inherit",
shell: true,
});
return new Promise((resolve, reject) => {
childProcess.once("close", (status) => {
childProcess.removeAllListeners("error");
if (status === 0) {
resolve(true);
return;
}
reject(false);
});
childProcess.once("error", (_status) => {
childProcess.removeAllListeners("close");
reject(false);
});
});
}
async function getRecommendedDependenciesInstallationCommand(
dependencies: Dependencies
): Promise<string[]> {
const deps = Object.entries(dependencies).map(
([name, version]) => `"${name}@${version}"`
);
if (await isYarnProject()) {
return ["yarn", "add", "--dev", ...deps];
}
if (await isPnpmProject()) {
return ["pnpm", "add", "-D", ...deps];
}
return ["npm", "install", "--save-dev", ...deps];
}
async function getDependencies(
projectType: SampleProjectTypeCreationAction
): Promise<Dependencies> {
const shouldInstallPeerDependencies =
(await isYarnProject()) ||
(await isPnpmProject()) ||
!(await doesNpmAutoInstallPeerDependencies());
const shouldInstallTypescriptDependencies =
projectType === Action.CREATE_TYPESCRIPT_PROJECT_ACTION ||
projectType === Action.CREATE_TYPESCRIPT_VIEM_PROJECT_ACTION;
const shouldInstallTypescriptPeerDependencies =
shouldInstallTypescriptDependencies && shouldInstallPeerDependencies;
const commonDependencies: Dependencies = {
[HARDHAT_PACKAGE_NAME]: `^${(await getPackageJson()).version}`,
...PROJECT_DEPENDENCIES,
...(shouldInstallPeerDependencies ? PEER_DEPENDENCIES : {}),
...(shouldInstallTypescriptDependencies ? TYPESCRIPT_DEPENDENCIES : {}),
...(shouldInstallTypescriptPeerDependencies
? TYPESCRIPT_PEER_DEPENDENCIES
: {}),
};
// At the moment, the default toolbox is the ethers based toolbox
const shouldInstallDefaultToolbox =
projectType !== Action.CREATE_TYPESCRIPT_VIEM_PROJECT_ACTION;
const ethersToolboxDependencies: Dependencies = {
...ETHERS_PROJECT_DEPENDENCIES,
...(shouldInstallPeerDependencies ? ETHERS_PEER_DEPENDENCIES : {}),
...(shouldInstallTypescriptPeerDependencies
? TYPESCRIPT_ETHERS_PEER_DEPENDENCIES
: {}),
};
const viemToolboxDependencies: Dependencies = {
...VIEM_PROJECT_DEPENDENCIES,
...(shouldInstallPeerDependencies ? VIEM_PEER_DEPENDENCIES : {}),
...(shouldInstallTypescriptPeerDependencies
? TYPESCRIPT_VIEM_PEER_DEPENDENCIES
: {}),
};
const toolboxDependencies: Dependencies = shouldInstallDefaultToolbox
? ethersToolboxDependencies
: viemToolboxDependencies;
return {
...commonDependencies,
...toolboxDependencies,
};
}