Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/hardhat-verify.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nomicfoundation/hardhat-verify": patch
---

Optimize imports.
5 changes: 5 additions & 0 deletions .changeset/imports-hardhat-ethers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nomicfoundation/hardhat-ethers": patch
---

Optimize imports.
5 changes: 5 additions & 0 deletions .changeset/imports-hardhat-utils.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nomicfoundation/hardhat-utils": patch
---

Optimize imports.
5 changes: 5 additions & 0 deletions .changeset/imports-hardhat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hardhat": patch
---

Optimize imports.
7 changes: 7 additions & 0 deletions .changeset/imports-ignition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@nomicfoundation/ignition-core": patch
"@nomicfoundation/hardhat-ignition-viem": patch
"@nomicfoundation/hardhat-ignition": patch
---

Optimize imports.
5 changes: 5 additions & 0 deletions .changeset/imports-ledger.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nomicfoundation/hardhat-ledger": patch
---

Optimize imports.
5 changes: 5 additions & 0 deletions .changeset/imports-mocha.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nomicfoundation/hardhat-mocha": patch
---

Optimize imports.
5 changes: 5 additions & 0 deletions .changeset/imports-node-test-reporter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nomicfoundation/hardhat-node-test-reporter": patch
---

Optimize imports.
5 changes: 5 additions & 0 deletions .changeset/imports-typechain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nomicfoundation/hardhat-typechain": patch
---

Optimize imports.
20 changes: 15 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,25 @@ v-next/hardhat - core logic and cli

**Package structure** — Exported code and types (via `package#exports`) live under `src/`, non-exported internals under `src/internal/`.

**Lazy loading external packages** — Hardhat optimizes startup time. Follow this strictly:

- Top-level imports allowed for: `node:path`, `node:util`, `chalk`, `semver`, `debug` and `import type`
- Everything else: use `await import()` inside the function that needs it

**`hardhat-utils` first** — Before using `node:fs` or writing a utility, check `@nomicfoundation/hardhat-utils`. It covers fs, crypto, hex, error handling, and more.

**Errors** — Only throw `HardhatError`. Never `throw new Error()`. Use `HardhatError.isHardhatError()` (not `instanceof`) and `ensureError()` in catch clauses. `./scripts` is exempt.

### Using imports correctly in Hardhat 3

Use `await import` only if one of these conditions is met:

1. The file with the import is part of the `hardhat` package, is always imported at startup (i.e. imported by `hardhat`'s `src/internal/cli/main.ts` or `src/index.ts`, directly or transitively), and the imported module isn't always used (e.g. `./init/init.js` in `hardhat`'s `main.ts`)
2. The import path is dynamic (e.g. the user config path)
3. The file is dynamically loaded by a wrapper that exports the same interface that loads it on first access (mostly used for HRE extensions, e.g. `src/internal/builtin-plugins/network-manager/hook-handlers/hre.ts` in `hardhat`)
4. The dynamic import is used to avoid a circular dependency (e.g. importing the `HRE` at runtime)
5. The import has to happen at a certain point in time (mostly used for import side-effects, e.g. `await import(...)` without doing anything with the imported module)
6. If there's a comment justifying it, and the imported module is cached (i.e. not running `await import(...)` every time, but instead doing something like `if (cachedModule === undefined) { cachedModule = await import(...) }`). Some code duplication in this case is acceptable if that avoids adding unnecessary async logic (e.g. avoid `const module = await getModule()` to avoid repeating just a few conditionals).

The only accepted imports in the `index.ts` file of plugins (both built-in and external) are their `type-extension`, types and `enums` from `hardhat`, and `hardhat/config`, and potentially a simple file with constants. They can also import files that follow these same rules and restrictions. Everything else should be imported by a callback registered in the plugin object.

Test files are free to use `await import` freely.

## Development workflow

After modifying a package, within the package run:
Expand Down
16 changes: 11 additions & 5 deletions docs/engineering-guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The intention behind the current separation of part of `hardhat` into the `core/

The entry-point of the plugins are meant to only export a description of the plugin, and not implement any functionality. Please don’t import anything in those files.

The only accepted imports are: their `type-extension.ts`, hardhat types, and the `"/config"` module from hardhat.
The only accepted imports in the `index.ts` file of plugins (both built-in and external) are their `type-extension`, types and `enums` from `hardhat`, and `hardhat/config`, and potentially a simple file with constants. They can also import files that follow these same rules and restrictions. Everything else should be imported by a callback registered in the plugin object.

## A3: Always initialize the HRE using the `hre-initialization` module inside of `hardhat`

Expand Down Expand Up @@ -92,12 +92,18 @@ This is also valid for tests, where things can easily become super brittle by us

## GC3: top-level imports vs dynamic imports

Hardhat v3’s plugin system was designed so that plugin hooks and task actions are lazy loaded. This means that within them, we can use top-level imports, and we aren’t restricted to dynamic imports, like we were in v2.
Hardhat 3's plugin system already handles lazy loading — plugins place business logic behind dynamic imports. Dependencies, conditional dependencies, hook handler factories, and task actions are all dynamically imported. This means most imports should be top-level imports, except for a few cases:

Please apply your own criteria when:
Use `await import` only if one of these conditions is met:

- Importing dependencies known to load slowly.
- The import is in a file that’s always loaded (e.g. it gets loaded if you run `pnpm hardhat --help`).
1. The file with the import is part of the `hardhat` package, is always imported at startup (i.e. imported by `hardhat`'s `src/internal/cli/main.ts` or `src/index.ts`, directly or transitively), and the imported module isn't always used (e.g. `./init/init.js` in `hardhat`'s `main.ts`)
2. The import path is dynamic (e.g. the user config path)
3. The file is dynamically loaded by a wrapper that exports the same interface that loads it on first access (mostly used for HRE extensions, e.g. `src/internal/builtin-plugins/network-manager/hook-handlers/hre.ts` in `hardhat`)
4. The dynamic import is used to avoid a circular dependency (e.g. importing the `HRE` at runtime)
5. The import has to happen at a certain point in time (mostly used for import side-effects, e.g. `await import(...)` without doing anything with the imported module)
6. If there's a comment justifying it, and the imported module is cached (i.e. not running `await import(...)` every time, but instead doing something like `if (cachedModule === undefined) { cachedModule = await import(...) }`). Some code duplication in this case is acceptable if that avoids adding unnecessary async logic (e.g. avoid `const module = await getModule()` to avoid repeating just a few conditionals).

Test files are free to use `await import` freely.

# Testing guidelines

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type {
Libraries,
} from "../../types.js";
import type { HardhatEthersProvider } from "../hardhat-ethers-provider/hardhat-ethers-provider.js";
import type { HardhatEthersSigner } from "../signers/signers.js";
import type { ethers as EthersT } from "ethers";
import type {
Abi,
Expand All @@ -18,6 +17,9 @@ import {
assertHardhatInvariant,
HardhatError,
} from "@nomicfoundation/hardhat-errors";
import { Contract, ContractFactory, isAddress, isAddressable } from "ethers";

import { HardhatEthersSigner } from "../signers/signers.js";

interface Link {
sourceName: string;
Expand Down Expand Up @@ -68,11 +70,7 @@ export class HardhatHelpers {
}

public async getSigner(address: string): Promise<HardhatEthersSigner> {
const { HardhatEthersSigner: SignerWithAddressImpl } = await import(
"../signers/signers.js"
);

const signerWithAddress = await SignerWithAddressImpl.create(
const signerWithAddress = await HardhatEthersSigner.create(
this.#provider,
this.#networkName,
this.#networkConfig,
Expand Down Expand Up @@ -179,8 +177,6 @@ export class HardhatHelpers {
return this.getContractAtFromArtifact(artifact, address, signer);
}

const ethers = await import("ethers");

if (signer === undefined) {
const signers = await this.getSigners();
signer = signers[0];
Expand All @@ -192,22 +188,20 @@ export class HardhatHelpers {
signer !== undefined ? signer : this.#provider;

let resolvedAddress;
if (ethers.isAddressable(address)) {
if (isAddressable(address)) {
resolvedAddress = await address.getAddress();
} else {
resolvedAddress = address;
}

return new ethers.Contract(resolvedAddress, nameOrAbi, signerOrProvider);
return new Contract(resolvedAddress, nameOrAbi, signerOrProvider);
}

public async getContractAtFromArtifact(
artifact: Artifact,
address: string | EthersT.Addressable,
signer?: EthersT.Signer,
): Promise<EthersT.Contract> {
const ethers = await import("ethers");

if (!this.#isArtifact(artifact)) {
throw new HardhatError(
HardhatError.ERRORS.HARDHAT_ETHERS.GENERAL.INVALID_ARTIFACT_FOR_FACTORY,
Expand All @@ -220,13 +214,13 @@ export class HardhatHelpers {
}

let resolvedAddress;
if (ethers.isAddressable(address)) {
if (isAddressable(address)) {
resolvedAddress = await address.getAddress();
} else {
resolvedAddress = address;
}

let contract = new ethers.Contract(resolvedAddress, artifact.abi, signer);
let contract = new Contract(resolvedAddress, artifact.abi, signer);

if (contract.runner === null) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- EthersT.Contract overlaps with EthersT.BaseContract
Expand Down Expand Up @@ -315,8 +309,6 @@ export class HardhatHelpers {
}

async #collectLibrariesAndLink(artifact: Artifact, libraries: Libraries) {
const ethers = await import("ethers");

const neededLibraries: Array<{
sourceName: string;
libName: string;
Expand All @@ -334,13 +326,13 @@ export class HardhatHelpers {
libraries,
)) {
let resolvedAddress: string;
if (ethers.isAddressable(linkedLibraryAddress)) {
if (isAddressable(linkedLibraryAddress)) {
resolvedAddress = await linkedLibraryAddress.getAddress();
} else {
resolvedAddress = linkedLibraryAddress;
}

if (!ethers.isAddress(resolvedAddress)) {
if (!isAddress(resolvedAddress)) {
throw new HardhatError(
HardhatError.ERRORS.HARDHAT_ETHERS.GENERAL.INVALID_ADDRESS_TO_LINK_CONTRACT_TO_LIBRARY,
{
Expand Down Expand Up @@ -465,8 +457,6 @@ export class HardhatHelpers {
bytecode: EthersT.BytesLike,
signer?: EthersT.Signer,
): Promise<EthersT.ContractFactory<A, I>> {
const { ContractFactory } = await import("ethers");

if (signer === undefined) {
// const signers = await hre.ethers.getSigners();
const signers = await this.getSigners();
Expand Down
4 changes: 2 additions & 2 deletions v-next/hardhat-ethers/src/internal/initialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { ArtifactManager } from "hardhat/types/artifacts";
import type { NetworkConfig } from "hardhat/types/config";
import type { EthereumProvider } from "hardhat/types/providers";

import * as ethers from "ethers";

import { HardhatEthersProvider } from "./hardhat-ethers-provider/hardhat-ethers-provider.js";
import { HardhatHelpers } from "./hardhat-helpers/hardhat-helpers.js";

Expand All @@ -12,8 +14,6 @@ export async function initializeEthers(
networkConfig: NetworkConfig,
artifactManager: ArtifactManager,
): Promise<HardhatEthers> {
const ethers = await import("ethers");

const provider = new HardhatEthersProvider(
ethereumProvider,
networkName,
Expand Down
27 changes: 24 additions & 3 deletions v-next/hardhat-ethers/src/internal/signers/derive-private-key.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type * as Bip39T from "ethereum-cryptography/bip39";
import type { HDKey as HDKeyT } from "ethereum-cryptography/hdkey";
import type {
EdrNetworkHDAccountsConfig,
HttpNetworkHDAccountsConfig,
Expand All @@ -6,6 +8,12 @@ import type {
import { HardhatError } from "@nomicfoundation/hardhat-errors";
import { bytesToHexString } from "@nomicfoundation/hardhat-utils/bytes";

// ethereum-cryptography/bip39 is known to be slow to load, so we lazy load it
let mnemonicToSeedSync: typeof Bip39T.mnemonicToSeedSync | undefined;

// ethereum-cryptography/hdkey is known to be slow to load, so we lazy load it
let HDKey: typeof HDKeyT | undefined;
Comment on lines +11 to +15
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bip39T / HDKeyT are type-only imports, but the cached function/class references are typed using typeof Bip39T.mnemonicToSeedSync / typeof HDKeyT. This typeof usage typically fails because there’s no value-side symbol. Consider typing these caches via typeof import("ethereum-cryptography/bip39").mnemonicToSeedSync / typeof import("ethereum-cryptography/hdkey").HDKey, or avoid typeof and use the imported types directly.

Suggested change
// ethereum-cryptography/bip39 is known to be slow to load, so we lazy load it
let mnemonicToSeedSync: typeof Bip39T.mnemonicToSeedSync | undefined;
// ethereum-cryptography/hdkey is known to be slow to load, so we lazy load it
let HDKey: typeof HDKeyT | undefined;
type MnemonicToSeedSync = typeof import("ethereum-cryptography/bip39")["mnemonicToSeedSync"];
type HDKeyStatic = typeof import("ethereum-cryptography/hdkey").HDKey;
// ethereum-cryptography/bip39 is known to be slow to load, so we lazy load it
let mnemonicToSeedSync: MnemonicToSeedSync | undefined;
// ethereum-cryptography/hdkey is known to be slow to load, so we lazy load it
let HDKey: HDKeyStatic | undefined;

Copilot uses AI. Check for mistakes.

const HD_PATH_REGEX = /^m(:?\/\d+'?)+\/?$/;

export async function derivePrivateKeys(
Expand Down Expand Up @@ -67,13 +75,26 @@ async function deriveKeyFromMnemonicAndPath(
hdPath: string,
passphrase: string,
): Promise<string | undefined> {
const { mnemonicToSeedSync } = await import("ethereum-cryptography/bip39");
const { HDKey } = await import("ethereum-cryptography/hdkey");

// NOTE: If mnemonic has space or newline at the beginning or end, it will be trimmed.
// This is because mnemonic containing them may generate different private keys.
const trimmedMnemonic = mnemonic.trim();

if (mnemonicToSeedSync === undefined) {
const { mnemonicToSeedSync: importedMnemonicToSeedSync } = await import(
"ethereum-cryptography/bip39"
);

mnemonicToSeedSync = importedMnemonicToSeedSync;
}

if (HDKey === undefined) {
const { HDKey: ImportedHDKey } = await import(
"ethereum-cryptography/hdkey"
);

HDKey = ImportedHDKey;
}

const seed = mnemonicToSeedSync(trimmedMnemonic, passphrase);

const masterKey = HDKey.fromMasterSeed(seed);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
deploy,
isContractFuture,
} from "@nomicfoundation/ignition-core";
import { getContract } from "viem";

export class ViemIgnitionHelperImpl<ChainTypeT extends ChainType | string>
implements ViemIgnitionHelper
Expand Down Expand Up @@ -347,8 +348,7 @@ export class ViemIgnitionHelperImpl<ChainTypeT extends ChainType | string>
);
}

const viem = await import("viem");
const contract = viem.getContract({
const contract = getContract({
address: ViemIgnitionHelperImpl.#ensureAddressFormat(
deployedContract.address,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,17 @@ import type { DeploymentParameters } from "@nomicfoundation/ignition-core";

import { HardhatError } from "@nomicfoundation/hardhat-errors";
import { readUtf8File } from "@nomicfoundation/hardhat-utils/fs";
import json5 from "json5";

import { bigintReviver } from "../internal/utils/bigintReviver.js";

export async function readDeploymentParameters(
filepath: string,
): Promise<DeploymentParameters> {
try {
const {
default: { parse },
} = await import("json5");
const rawFile = await readUtf8File(filepath);

return await parse(rawFile.toString(), bigintReviver);
return await json5.parse(rawFile.toString(), bigintReviver);
} catch (e) {
if (HardhatError.isHardhatError(e)) {
throw e;
Expand Down
7 changes: 2 additions & 5 deletions v-next/hardhat-ignition/src/internal/tasks/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from "@nomicfoundation/hardhat-utils/fs";
import { deploy } from "@nomicfoundation/ignition-core";
import chalk from "chalk";
import json5 from "json5";
import Prompt from "prompts";

import { HardhatArtifactResolver } from "../../helpers/hardhat-artifact-resolver.js";
Expand Down Expand Up @@ -276,11 +277,7 @@ async function resolveParametersString(
paramString: string,
): Promise<DeploymentParameters> {
try {
const {
default: { parse },
} = await import("json5");

return await parse(paramString, bigintReviver);
return await json5.parse(paramString, bigintReviver);
} catch (e) {
if (HardhatError.isHardhatError(e)) {
throw e;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ListTransactionsResult } from "@nomicfoundation/ignition-core";

import { HardhatError } from "@nomicfoundation/hardhat-errors";
import json5 from "json5";

export async function calculateListTransactionsDisplay(
deploymentId: string,
Expand Down Expand Up @@ -34,11 +35,7 @@ export async function calculateListTransactionsDisplay(
}

if (transaction.params !== undefined) {
const {
default: { stringify },
} = await import("json5");

text += ` - Params: ${stringify(
text += ` - Params: ${json5.stringify(
transaction.params,
transactionDisplaySerializeReplacer,
)}\n`;
Expand Down
Loading
Loading