hardhat
hardhat copied to clipboard
Support for ESM in TypeScript projects
Please comment and/or upvote this issue if you need support for ESM in TypeScript projects. If possible, tell us more about your use case and why you need this and cannot just transpile to cjs.
UPDATE: We have an experimental version working
See this comment to learn how to use an experimental version of this: https://github.com/NomicFoundation/hardhat/issues/3385#issuecomment-1841380253
This issue is also being tracked on Linear.
We use Linear to manage our development process, but we keep the conversations on Github.
LINEAR-ID: b31f8c05-52a3-44ca-b22e-10ec21278d4c
@fvictorio we use wagmi/core for some scripts which only supports ESM. Had to convert all tests to .js and extract chunks of scripts into another package for now to deal with this.
https://github.com/sushiswap/sushiswap/commit/ccd68f906a16287ab3cf1f2ffcf07bdd36c8d67d
I'm using https://github.com/sindresorhus/execa, which is ESM-only.
I can't run task when use new TypeScript:
"module": "NodeNext",
"moduleResolution": "NodeNext",
Obviously when i delete "type": "module"
from package.json it's start work, but i must use this setting.
Error from terimnal:
Suggestion for TS ESM implementation:
ts-node has a ts-node/esm
loader hook. Also mts
extension could be used but I'm not sure if that's needed.
Copying @alcuadrado comments from monorepo:
Nop, unfortunately Hardhat's core depends on CJS pretty heavily.
The reason for that is that our plugin system is dependent on the order of require() that users make to plugins. In ESM the order of imports is not guaranteed (and does change in practice).
What about await import
? You can do this one after another.
We need to rework our plugin system for it to fully support ESM.
I'm starting to think that maybe we should move to esm-only when it's properly supported by ts-node or some similar project. But note that even after that, the migration would be a pretty large project, as every plugin will need to be adapted.
If you keep supporting two module systems, ESM modules would be able to import common.js code - which uses old sequential require
s and is cool.
What about await import? You can do this one after another.
Oh, that may be a great idea actually! I think now ESM in node.js means/implies that you have top-level-await. This could work! Thanks !
Upvote; we are using ESM in our TypeScript project because of a dependency requirement.
pure esm package is coming.
I am looking to integrate Playwright with Hardhat + Wagmi in order to automate frontend testing that involves connecting to a hardhat node :)
Upvote; we are using ESM in our TypeScript project
pure esm package is coming.
do u know when? 🧐
upvote, many packages are ESM only now, I have to install older versions for compatibility. all my projects and code is ESM now, except for hardhat.
Upvoting as well. We have constants
values defined in a constants.ts
/ constants.js](https://github.com/lukso-network/lsp-smart-contracts/blob/develop/constants.ts) file in the LUKSO lsp-smart-contracts. These are part of the [
@lukso/lsp-smart-contracts` npm package and are important values to be consumed by projects that use our smart contracts package.
https://github.com/lukso-network/lsp-smart-contracts
In our case, this is problematic as any Hardhat Typescript project that has the @lukso/lsp-smart-contracts
as a dependency cannot import these necessary constant values.
The only workaround we found was in this PR by @richtera: https://github.com/lukso-network/lsp-smart-contracts/pull/601.
https://github.com/lukso-network/lsp-smart-contracts/pull/601/files
The file in the folder looks like this:
ES Lint raises a warning, but we are still able to import as shown below.
import { INTERFACE_IDS } from "@lukso/lsp-smart-contracts/dist/constants.cjs.js";
const test = INTERFACE_IDS.LSP0ERC725Account;
Following this issue. Would help a lot to get rid of some workarounds for ESM/CJS interop.
yes pls really need esm compatibility with typescript, make it happen guys
Having to choose dependencies basing on "does it still support CommonJS" criterion is annoying. Following this issue.
Another upvote for this feature. Aztec 's ZK framework, Noir, needs ESM for the generation of proofs.
https://noir-lang.org/typescript
Thanks everyone. We'll try to prioritize this. I don't think we'll be able to do it properly before the next major, but we'll try to at least provide a useful workaround, even if it has some friction.
ts-node can be axed entirely with a drop-in replacement for tsx.
This should unblock you entirely from allowing ESM TS support for HardHat
[!NOTE]
I have gone through this process with other repos, and it's been a surprisingly easy experience.
Upvoted. I'm using hardhat-ts in workplace for Optimism L2 smart contract, dealing with batch transactions.
And fee estimate package provided by optimism is based on ts ESM and hardhat is not yet support it.
so I had to dynamic import it like below.
async function feeEstimator() {
const sdk = await import("@eth-optimism/fee-estimation")
const { what-you-need } = sdk
}
I have found the following solution:
- Import the real code via dynamic imports:
const { load } = await import("lp-staking-research/load");
- Import types via the type imports with import annotation like this:
import type { TestCase } from "lp-staking-research/schema/test-case" assert { "resolution-mode": "require" };
These will also require using "module": "NodeNext"
and "moduleResolution": "NodeNext"
at tsconfig.json
, and, for now, nightly TypeScript (yarn install --dev typescript@next
).
This is not ideal, especially since in some contexts you can't use async
calls, but is solves the issue for me.
A hint: to use dynamic import for test case declaration in mocha
you can use the --delay
mode.
I need that sweet pure ESM
Tried a barebones bun
setup for a new Hardhat project and ran into an error with ESM.
@schaable can hardhat
just migrate to https://www.npmjs.com/package/tsx package? It runs without any problems with ESM, it was built to be ESM first.
+1 -- Need access to this please :)
hoping to support bun which uses ESM in typescript
The Hyperlane team is hoping for this fix too! We compile our TS libs to ESM for better tree shaking.
Hi everyone, we just released a new version of Hardhat which has experimental support for TypeScript + ESM projects. To try it out, upgrade your version of Hardhat and follow these steps.
Initial Setup
- Modify
package.json
- Add
"type": "module"
to yourpackage.json
. This indicates that the project uses ESM syntax.
- Add
- Rename Hardhat configuration file
- Change
hardhat.config.ts
tohardhat.config.cts
. The.cts
extension is necessary as the Hardhat config can only be CommonJS.
- Change
TypeScript Configuration
- Update
tsconfig.json
- Under
compilerOptions
, set"module": "node16"
,"target": "es2022"
, and"esModuleInterop": true
. These options ensure compatibility with Node.js v16 and above. - Add
experimentalResolver: true
under thets-node
configuration. This setting enables file extension inclusion in CommonJS, and can help suppressing some IDE errors. - If you’re using
files
,include
orexclude
in your config, you may need to addfiles: true
thets-node
configuration to avoid certain type checking errors.
- Under
{
"compilerOptions": {
"module": "node16",
"target": "es2022",
"esModuleInterop": true
// ...
},
"ts-node": {
"experimentalResolver": true
"files": true // only if you have "files", "include" or "exclude" in your config
}
}
Code Adjustments
To make your Hardhat project compatible with ESM, you'll need to make the following adjustments to your code:
- Update relative imports
- Include the file extension in all your relative import statements. This is a requirement in ESM to correctly resolve modules.
- Specify full path for Directory Modules
- Replace any directory module imports with the full file path. For instance, instead of importing from a directory like
./utils
, specify the exact file:./utils/index.js
.
- Replace any directory module imports with the full file path. For instance, instead of importing from a directory like
- Handling Hardhat imports
- As Hardhat is still based on CommonJS, you cannot use named imports directly due to the module system difference. Instead:
- Import the entire module as a default import:
import hre from 'hardhat';
- Use destructuring to access specific properties:
const { ethers, network } = hre;
- Import the entire module as a default import:
- As Hardhat is still based on CommonJS, you cannot use named imports directly due to the module system difference. Instead:
Plugins and types
Plugins such as hardhat-ethers
and hardhat-viem
automatically generate TypeScript typings for contracts during compilation. These files, despite having .ts
extensions, should be treated as CommonJS modules. However, due to the root package.json
specifying "type": "module"
, they are incorrectly interpreted as ESM modules. This leads to issues in IDEs with type checking and autocomplete features.
To resolve this, add a package.json
file with "type": "commonjs"
in the folder where these files are generated (./typechain-types
for hardhat-ethers
and ./artifacts
for hardhat-viem
). To streamline this process, we provide snippets with overrides of the compilation task which you can include in your hardhat.config.cts
. These tasks automatically create the necessary package.json
file during the compilation process.
hardhat-viem
If you’re using hardhat-viem
, add the following snippet after the import
s in your hardhat-config.cts
import { join } from "path";
import { writeFile } from "fs/promises";
import { subtask } from "hardhat/config";
import { TASK_COMPILE_SOLIDITY } from "hardhat/builtin-tasks/task-names";
subtask(TASK_COMPILE_SOLIDITY).setAction(async (_, { config }, runSuper) => {
const superRes = await runSuper();
try {
await writeFile(
join(config.paths.artifacts, "package.json"),
'{ "type": "commonjs" }'
);
} catch (error) {
console.error("Error writing package.json: ", error);
}
return superRes;
});
hardhat-ethers
If you’re using hardhat-ethers, add the following snippet after the import
s in your hardhat-config.cts
import { join } from "path";
import { writeFile } from "fs/promises";
import { subtask } from "hardhat/config";
import { TASK_COMPILE_SOLIDITY } from "hardhat/builtin-tasks/task-names";
subtask(TASK_COMPILE_SOLIDITY).setAction(async (_, { config }, runSuper) => {
const superRes = await runSuper();
try {
await writeFile(
join(config.paths.root, "typechain-types", "package.json"),
'{ "type": "commonjs" }'
);
} catch (error) {
console.error("Error writing package.json: ", error);
}
return superRes;
});
Running Scripts and Tests
- Run scripts with:
NODE_OPTIONS="--experimental-loader ts-node/esm/transpile-only" hh run scripts/deploy.ts
- Execute tests using:
NODE_OPTIONS="--experimental-loader ts-node/esm/transpile-only" hh test
- To avoid experimental feature warnings, append
--no-warnings=ExperimentalWarning
to yourNODE_OPTIONS
- You can integrate these commands into your project's
package.json
for easy execution:
"scripts": {
"test": "NODE_OPTIONS='--experimental-loader ts-node/esm/transpile-only --no-warnings=ExperimentalWarning' hardhat test",
"script": "NODE_OPTIONS='--experimental-loader ts-node/esm/transpile-only --no-warnings=ExperimentalWarning' hardhat run"
}
Thanks @schaable this works perfectly! Finally dumping CJS in Aztec's Noir stuff!