Create new rules for managing static vs dynamic imports and apply them with Claude#8088
Create new rules for managing static vs dynamic imports and apply them with Claude#8088alcuadrado merged 21 commits intomainfrom
Conversation
🦋 Changeset detectedLatest commit: 3e61d58 The changes in this PR will be included in the next version bump. This PR includes changesets to release 13 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Pull request overview
This PR updates the repository’s guidance for when to use top-level imports vs await import(...), and applies the new import strategy across multiple packages (Hardhat core, plugins, utilities, and verify tooling) by replacing many dynamic imports with static imports and introducing a few cached/lazy-load patterns for known-slow modules.
Changes:
- Document new import rules (when dynamic imports are allowed/required) in
docs/engineering-guidelines.mdandCLAUDE.md. - Replace numerous dynamic imports with top-level imports across the codebase; in a few cases, introduce cached lazy-loading for slow dependencies (e.g.
semver, HRE import). - Refactor Hardhat Verify’s solc-version helpers from async to sync and update call sites/tests accordingly.
Reviewed changes
Copilot reviewed 37 out of 37 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| v-next/ignition-core/src/internal/execution/future-processor/helpers/future-resolvers.ts | Switch ethers Interface loading from dynamic to static import. |
| v-next/hardhat/src/internal/utils/package.ts | Replace dynamic getRequest import with static import. |
| v-next/hardhat/src/internal/core/user-interruptions.ts | Replace dynamic imports of chalk/readline with top-level imports. |
| v-next/hardhat/src/internal/core/plugins/detect-plugin-npm-dependency-problems.ts | Add cached lazy-load of semver.satisfies with a typed satisfies variable. |
| v-next/hardhat/src/internal/cli/init/prompt.ts | Replace dynamic enquirer import with static import. |
| v-next/hardhat/src/internal/cli/init/init.ts | Replace dynamic BannerManager import with static import. |
| v-next/hardhat/src/internal/cli/help/get-help-string.ts | Replace dynamic chalk import with static import. |
| v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/compiler/downloader.ts | Replace dynamic adm-zip import with static import. |
| v-next/hardhat/src/internal/builtin-plugins/node/artifacts/build-info-watcher.ts | Replace dynamic chokidar.watch import with static import. |
| v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers-array.ts | Replace per-handler dynamic imports with top-level imports for all handlers. |
| v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/network.ts | Replace dynamic createHandlersArray import with static import. |
| v-next/hardhat/src/internal/builtin-plugins/network-manager/accounts/derive-private-keys.ts | Add cached lazy-loading for slow ethereum-cryptography submodules. |
| v-next/hardhat/src/internal/builtin-plugins/gas-analytics/helpers.ts | Cache dynamic import of HRE behind a helper (getHre). |
| v-next/hardhat-verify/test/solc-versions.ts | Update tests for sync solc-version helpers and change assertion helper. |
| v-next/hardhat-verify/src/verify.ts | Replace dynamic import of verification entry with static import. |
| v-next/hardhat-verify/src/internal/verification.ts | Update calls to solc-version helpers after async→sync refactor. |
| v-next/hardhat-verify/src/internal/solc-versions.ts | Convert solc-version utilities from async+dynamic import to sync+static import. |
| v-next/hardhat-verify/src/internal/metadata.ts | Replace dynamic cbor2.decode import with static import. |
| v-next/hardhat-verify/src/internal/hook-handlers/network.ts | Replace dynamic import of Verification with static import. |
| v-next/hardhat-verify/src/internal/constructor-args.ts | Replace dynamic @ethersproject/abi import with static import. |
| v-next/hardhat-utils/src/request.ts | Replace dynamic undici imports with static imports. |
| v-next/hardhat-utils/src/lang.ts | Update deepClone to call getDeepCloneFunction synchronously. |
| v-next/hardhat-utils/src/internal/request.ts | Replace dynamic undici imports with static imports. |
| v-next/hardhat-utils/src/internal/lang.ts | Replace dynamic imports of rfdc / fast-equals with static imports and make helpers sync. |
| v-next/hardhat-utils/src/internal/global-dir.ts | Replace dynamic env-paths import with static import. |
| v-next/hardhat-typechain/src/internal/generate-types.ts | Replace dynamic TypeChain imports with static imports and rename transformer binding. |
| v-next/hardhat-node-test-reporter/src/github-actions.ts | Replace dynamic @actions/core import with static import. |
| v-next/hardhat-mocha/src/task-action.ts | Replace dynamic mocha import with static import; keep side-effect hook as dynamic import. |
| v-next/hardhat-ignition/src/internal/ui/helpers/calculate-list-transactions-display.ts | Replace dynamic json5 import with static import usage. |
| v-next/hardhat-ignition/src/internal/tasks/deploy.ts | Replace dynamic json5 import with static import usage. |
| v-next/hardhat-ignition/src/helpers/read-deployment-parameters.ts | Replace dynamic json5 import with static import usage. |
| v-next/hardhat-ignition-viem/src/internal/viem-ignition-helper.ts | Replace dynamic viem import with static getContract import. |
| v-next/hardhat-ethers/src/internal/signers/derive-private-key.ts | Add cached lazy-loading for slow ethereum-cryptography submodules. |
| v-next/hardhat-ethers/src/internal/initialization.ts | Replace dynamic ethers import with static import. |
| v-next/hardhat-ethers/src/internal/hardhat-helpers/hardhat-helpers.ts | Replace multiple dynamic ethers/signer imports with static imports. |
| docs/engineering-guidelines.md | Update documented rules for top-level vs dynamic imports and plugin index import restrictions. |
| CLAUDE.md | Update repo assistant guidance to match the new import rules and plugin index restrictions. |
| import { | ||
| findClosestPackageRoot, | ||
| findDependencyPackageJson, | ||
| readClosestPackageJson, | ||
| } from "@nomicfoundation/hardhat-utils/package"; | ||
| import { getRequest } from "@nomicfoundation/hardhat-utils/request"; | ||
|
|
There was a problem hiding this comment.
This file is imported during HRE creation (src/internal/core/hre.ts), but getRequest (and therefore @nomicfoundation/hardhat-utils/request/undici) is only needed by getLatestHardhatVersion(). Importing it at top-level forces undici to load on every Hardhat startup. Move the getRequest import back into getLatestHardhatVersion() (optionally with module caching) to keep startup fast.
There was a problem hiding this comment.
unidici is lazy-loaded now
| import { AutomaticSenderHandler } from "./handlers/accounts/automatic-sender-handler.js"; | ||
| import { FixedSenderHandler } from "./handlers/accounts/fixed-sender-handler.js"; | ||
| import { HDWalletHandler } from "./handlers/accounts/hd-wallet-handler.js"; | ||
| import { LocalAccountsHandler } from "./handlers/accounts/local-accounts.js"; | ||
| import { ChainIdValidatorHandler } from "./handlers/chain-id/chain-id-handler.js"; |
There was a problem hiding this comment.
These handlers are now imported eagerly, but at least LocalAccountsHandler/HDWalletHandler are documented as slow to load (they pull in expensive deps like micro-eth-signer) and were previously dynamically imported for that reason. With these top-level imports, those slow deps will load whenever handlers-array.ts is evaluated, even for networks that never use local/HD accounts. Consider restoring dynamic imports for the expensive handlers (and/or caching the imported modules).
| import chalk from "chalk"; | ||
|
|
There was a problem hiding this comment.
getHelpString is imported by the CLI entrypoint (src/internal/cli/main.ts) even when help output isn’t needed, but chalk is only required when building the help string. Importing chalk at top-level makes it part of the always-on startup cost; consider switching back to a dynamic import (optionally cached) inside getHelpString() to align with the startup-performance import guidelines.
There was a problem hiding this comment.
intentional, as it loads quickly anyways
| import { createInterface } from "node:readline"; | ||
|
|
||
| import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors"; | ||
| import { AsyncMutex } from "@nomicfoundation/hardhat-utils/synchronization"; | ||
| import chalk from "chalk"; |
There was a problem hiding this comment.
This module is part of core HRE initialization (src/internal/core/hre.ts), but chalk (and even node:readline) are only needed when an interruption actually occurs. Importing them at top-level makes them part of the baseline startup cost for all Hardhat runs; consider lazily importing (and caching) these dependencies inside the default handlers.
There was a problem hiding this comment.
intentional, as they load quickly anyways
| // semver is slow to load, and this file is loaded by HookManager, so we lazy | ||
| // load it. | ||
| let satisfies: typeof SatisfiesT | undefined; | ||
|
|
There was a problem hiding this comment.
SatisfiesT is imported as a type, but it's then used in a typeof query (let satisfies: typeof SatisfiesT). typeof only works with values, so this won’t typecheck. Consider typing this as typeof import("semver").satisfies (or import the satisfies value type via import type semverT from "semver" and use typeof semverT.satisfies).
| import { AutomaticSenderHandler } from "./handlers/accounts/automatic-sender-handler.js"; | ||
| import { FixedSenderHandler } from "./handlers/accounts/fixed-sender-handler.js"; | ||
| import { HDWalletHandler } from "./handlers/accounts/hd-wallet-handler.js"; | ||
| import { LocalAccountsHandler } from "./handlers/accounts/local-accounts.js"; | ||
| import { ChainIdValidatorHandler } from "./handlers/chain-id/chain-id-handler.js"; | ||
| import { AutomaticGasHandler } from "./handlers/gas/automatic-gas-handler.js"; | ||
| import { AutomaticGasPriceHandler } from "./handlers/gas/automatic-gas-price-handler.js"; | ||
| import { FixedGasHandler } from "./handlers/gas/fixed-gas-handler.js"; | ||
| import { FixedGasPriceHandler } from "./handlers/gas/fixed-gas-price-handler.js"; |
There was a problem hiding this comment.
These handlers are now imported eagerly, which forces expensive dependencies to load even when they’re not used by the current network config. In particular, LocalAccountsHandler pulls in micro-eth-signer (documented in that handler as expensive), and HDWalletHandler depends on it transitively. Consider restoring dynamic imports (and caching) for the expensive handlers (at least LocalAccountsHandler/HDWalletHandler) so they’re only loaded when networkConfig.accounts actually requires them.
There was a problem hiding this comment.
micro-eth-signer is loaded lazily now
| import { runTypeChain } from "typechain"; | ||
| import { outputTransformers as typechainOutputTransformers } from "typechain/dist/codegen/outputTransformers/index.js"; |
There was a problem hiding this comment.
typechain and typechain/dist/.../outputTransformers are now imported at module load time, which means they’ll be loaded even when generateTypes() immediately returns (e.g. --noTypechain or dontOverrideCompile). Since those flags exist specifically to skip type generation, consider moving these imports back to a lazy await import() after the early-return guard (optionally cached) so skipping typechain also skips loading its dependencies.
v-next/hardhat-utils/src/request.ts
Outdated
| import querystring from "node:querystring"; | ||
| import stream from "node:stream/promises"; | ||
|
|
||
| import { MockAgent, request } from "undici"; |
There was a problem hiding this comment.
MockAgent is only used by getTestDispatcher(), but it’s now imported at top-level together with request, which makes the production getRequest/post* paths pay the cost of loading test-only undici machinery whenever this module is imported. Consider lazy-importing MockAgent inside getTestDispatcher() (or splitting test helpers into a separate module) to keep the runtime surface minimal.
| import { MockAgent, request } from "undici"; | |
| import { request } from "undici"; |
| import type * as Bip39T from "ethereum-cryptography/bip39"; | ||
| import type { HDKey as HDKeyT } from "ethereum-cryptography/hdkey"; | ||
|
|
||
| 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; |
There was a problem hiding this comment.
Bip39T / HDKeyT are imported with import type, but the caches are typed using typeof Bip39T.mnemonicToSeedSync and typeof HDKeyT. typeof type queries require value-side symbols, which type-only imports don’t provide. Use a module-namespace type (e.g. typeof import("ethereum-cryptography/bip39").mnemonicToSeedSync) or reference the imported types directly without typeof.
| import { cleanupTestFailError } from "./node-test-error-utils.js"; | ||
|
|
||
| // We don't load github actions core on local runs | ||
| let core: typeof GithubActionsCoreT | undefined; |
There was a problem hiding this comment.
GithubActionsCoreT is imported with import type, but core is declared as typeof GithubActionsCoreT. Since typeof queries require a value-side symbol, this is likely to cause TypeScript errors. Prefer typing the cache as typeof import("@actions/core") | undefined (or otherwise adjust the typing to avoid typeof on a type-only import).
| let core: typeof GithubActionsCoreT | undefined; | |
| let core: typeof import("@actions/core") | undefined; |
|
|
||
| // We don't load undici on startup because this package is transitively imported | ||
| // from too many places and it's too complex to optimize case by case. | ||
| let undici: typeof UndiciT | undefined; |
There was a problem hiding this comment.
UndiciT is imported with import type, but typeof UndiciT is a type query that requires a value-side symbol. This pattern typically fails to typecheck; type the cached module as typeof import("undici") (or avoid typeof and use a proper module-namespace type) so the lazy import cache remains correctly typed.
| let undici: typeof UndiciT | undefined; | |
| let undici: typeof import("undici") | undefined; |
|
|
||
| // We don't load undici on startup because this package is transitively imported | ||
| // from too many places and it's too complex to optimize case by case. | ||
| let undici: typeof UndiciT | undefined; |
There was a problem hiding this comment.
UndiciT is a type-only import, but let undici: typeof UndiciT relies on a value-side symbol for the typeof query. Consider typing this cache as typeof import("undici") | undefined to avoid type errors and keep the lazy-loading implementation type-safe.
| let undici: typeof UndiciT | undefined; | |
| let undici: typeof import("undici") | undefined; |
| @@ -11,7 +12,9 @@ import { | |||
| } from "@nomicfoundation/hardhat-utils/fs"; | |||
| import { hexStringToBytes } from "@nomicfoundation/hardhat-utils/hex"; | |||
| import chalk from "chalk"; | |||
| import { addr } from "micro-eth-signer"; | |||
|
|
|||
| // micro-eth-signer is known to be slow to load, so we lazy load it | |||
| let microEthSigner: typeof MicroEthSignerT | undefined; | |||
There was a problem hiding this comment.
MicroEthSignerT is imported with import type, but the cache is declared as typeof MicroEthSignerT. typeof type queries require a value-side symbol, so this is prone to TypeScript errors. Prefer a module-namespace type like typeof import("micro-eth-signer") for the cached import (or otherwise adjust the typing to avoid typeof on a type-only import).
| @@ -27,40 +29,44 @@ import { | |||
| rpcTransactionRequest, | |||
| validateParams, | |||
| } from "@nomicfoundation/hardhat-zod-utils/rpc"; | |||
| import { addr, Transaction } from "micro-eth-signer"; | |||
| import { signTyped } from "micro-eth-signer/typed-data"; | |||
| import * as typed from "micro-eth-signer/typed-data"; | |||
|
|
|||
| // micro-eth-signer is known to be slow to load, so we lazy load it | |||
| let microEthSigner: typeof MicroEthSignerT | undefined; | |||
| let microEthSignerTypedData: typeof MicroEthSignerTypedDataT | undefined; | |||
There was a problem hiding this comment.
The lazy-loaded module caches are typed as typeof MicroEthSignerT / typeof MicroEthSignerTypedDataT, but both are brought in via import type. typeof type queries require a value-side symbol, so this can break typechecking. Use a module-namespace type like typeof import("micro-eth-signer") / typeof import("micro-eth-signer/typed-data") for these caches (or remove the typeof and reference the imported types directly where appropriate).
| @@ -13,7 +14,9 @@ import { | |||
| } from "@nomicfoundation/edr"; | |||
| import { AsyncMutex } from "@nomicfoundation/hardhat-utils/synchronization"; | |||
| import { hexToBytes } from "ethereum-cryptography/utils"; | |||
| import { addr } from "micro-eth-signer/address"; | |||
|
|
|||
| // micro-eth-signer is known to be slow to load, so we lazy load it | |||
| let microEthSignerAddress: typeof MicroEthSignerAddressT | undefined; | |||
|
|
|||
There was a problem hiding this comment.
MicroEthSignerAddressT is imported with import type, but microEthSignerAddress is typed as typeof MicroEthSignerAddressT. typeof type queries require a value-side symbol, so this can fail to compile. Prefer typeof import("micro-eth-signer/address") | undefined (or another module-namespace typing) for the lazy import cache.
| // 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; |
There was a problem hiding this comment.
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.
| // 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; |
| import { addr, Transaction } from "micro-eth-signer"; | ||
|
|
||
| // micro-eth-signer is known to be slow to load, so we lazy load it | ||
| let microEthSigner: typeof MicroEthSignerT | undefined; |
There was a problem hiding this comment.
microEthSigner is declared as typeof MicroEthSignerT, but MicroEthSignerT is a type-only import. Using typeof here typically causes a TS error because there’s no value-side symbol. Consider typing the cache as typeof import("micro-eth-signer") | undefined to keep the lazy import correctly typed.
| let microEthSigner: typeof MicroEthSignerT | undefined; | |
| let microEthSigner: typeof import("micro-eth-signer") | undefined; |
| let microEthSigner: typeof MicroEthSignerT | undefined; | ||
| let microEthSignerTypedData: typeof MicroEthSignerTypedDataT | undefined; | ||
| let microEthSignerUtils: typeof MicroEthSignerUtilsT | undefined; |
There was a problem hiding this comment.
The cached lazy imports are typed as typeof MicroEthSignerT / typeof MicroEthSignerTypedDataT / typeof MicroEthSignerUtilsT, but these are imported with import type. typeof queries require value-side symbols, so this is likely to fail typechecking. Use module-namespace types like typeof import("micro-eth-signer") (and the corresponding subpaths) for the caches, or otherwise adjust to avoid typeof on type-only imports.
| let microEthSigner: typeof MicroEthSignerT | undefined; | |
| let microEthSignerTypedData: typeof MicroEthSignerTypedDataT | undefined; | |
| let microEthSignerUtils: typeof MicroEthSignerUtilsT | undefined; | |
| let microEthSigner: typeof import("micro-eth-signer") | undefined; | |
| let microEthSignerTypedData: typeof import("micro-eth-signer/typed-data") | undefined; | |
| let microEthSignerUtils: typeof import("micro-eth-signer/utils") | undefined; |
…tallation problems
3dba86f to
3e61d58
Compare
|
All the comments about |
schaable
left a comment
There was a problem hiding this comment.
LGTM. Some functions became sync after removing the dynamic imports, but they're still being treated as async. We've agreed to fix that in a follow-up PR.
This PR updates the rules about how to handle static vs dynamic imports and uses claude to fix it. It took some tweaking to get Claude to get it right, but it now works well.