hardhat icon indicating copy to clipboard operation
hardhat copied to clipboard

Support for ESM in TypeScript projects

Open fvictorio opened this issue 2 years ago • 49 comments

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

fvictorio avatar Nov 30 '22 13:11 fvictorio

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

github-actions[bot] avatar Nov 30 '22 13:11 github-actions[bot]

@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

matthewlilley avatar Dec 06 '22 16:12 matthewlilley

I'm using https://github.com/sindresorhus/execa, which is ESM-only.

DenisGorbachev avatar Dec 07 '22 13:12 DenisGorbachev

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: image

anteqkois avatar Jan 06 '23 08:01 anteqkois

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.

talentlessguy avatar Mar 04 '23 08:03 talentlessguy

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 requires and is cool.

paulmillr avatar Mar 15 '23 16:03 paulmillr

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 !

alcuadrado avatar Mar 15 '23 19:03 alcuadrado

Upvote; we are using ESM in our TypeScript project because of a dependency requirement.

alex-mccreary avatar Mar 23 '23 17:03 alex-mccreary

pure esm package is coming.

pynixwang avatar Apr 13 '23 16:04 pynixwang

I am looking to integrate Playwright with Hardhat + Wagmi in order to automate frontend testing that involves connecting to a hardhat node :)

thevolcanomanishere avatar Apr 14 '23 09:04 thevolcanomanishere

Upvote; we are using ESM in our TypeScript project

kostysh avatar Apr 14 '23 16:04 kostysh

pure esm package is coming.

do u know when? 🧐

highskore avatar Apr 17 '23 01:04 highskore

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.

robwilkes avatar May 04 '23 07:05 robwilkes

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.

image

https://github.com/lukso-network/lsp-smart-contracts/pull/601/files

The file in the folder looks like this:

image

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;

CJ42 avatar Jun 07 '23 10:06 CJ42

Following this issue. Would help a lot to get rid of some workarounds for ESM/CJS interop.

vinerz avatar Jul 09 '23 15:07 vinerz

yes pls really need esm compatibility with typescript, make it happen guys

fraVlaca avatar Jul 28 '23 16:07 fraVlaca

Having to choose dependencies basing on "does it still support CommonJS" criterion is annoying. Following this issue.

paulperegud avatar Aug 02 '23 13:08 paulperegud

Another upvote for this feature. Aztec 's ZK framework, Noir, needs ESM for the generation of proofs.

https://noir-lang.org/typescript

DefiCake avatar Aug 11 '23 16:08 DefiCake

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.

fvictorio avatar Aug 11 '23 18:08 fvictorio

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.

timbrinded avatar Aug 31 '23 16:08 timbrinded

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
}

developerasun avatar Sep 23 '23 08:09 developerasun

I have found the following solution:

  1. Import the real code via dynamic imports:
    const { load } = await import("lp-staking-research/load");
    
  2. 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.

MOZGIII avatar Sep 27 '23 20:09 MOZGIII

I need that sweet pure ESM

niconiahi avatar Oct 04 '23 16:10 niconiahi

Tried a barebones bun setup for a new Hardhat project and ran into an error with ESM.

rhlsthrm avatar Oct 11 '23 10:10 rhlsthrm

@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.

damianobarbati avatar Oct 23 '23 09:10 damianobarbati

+1 -- Need access to this please :)

nftchance avatar Oct 25 '23 17:10 nftchance

hoping to support bun which uses ESM in typescript

Confucian-e avatar Oct 29 '23 13:10 Confucian-e

The Hyperlane team is hoping for this fix too! We compile our TS libs to ESM for better tree shaking.

jmrossy avatar Dec 04 '23 21:12 jmrossy

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

  1. Modify package.json
    • Add "type": "module" to your package.json. This indicates that the project uses ESM syntax.
  2. Rename Hardhat configuration file
    • Change hardhat.config.ts to hardhat.config.cts. The .cts extension is necessary as the Hardhat config can only be CommonJS.

TypeScript Configuration

  1. 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 the ts-node configuration. This setting enables file extension inclusion in CommonJS, and can help suppressing some IDE errors.
    • If you’re using filesinclude or exclude in your config, you may need to add files: true the ts-node configuration to avoid certain type checking errors.
{
  "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:

  1. Update relative imports
    • Include the file extension in all your relative import statements. This is a requirement in ESM to correctly resolve modules.
  2. 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.
  3. 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;

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 imports 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 imports 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 your NODE_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"
}

schaable avatar Dec 05 '23 18:12 schaable

Thanks @schaable this works perfectly! Finally dumping CJS in Aztec's Noir stuff!

signorecello avatar Dec 06 '23 11:12 signorecello