Skip to content

Instantly share code, notes, and snippets.

@jkuester
Last active January 26, 2025 15:24
Show Gist options
  • Save jkuester/26339ac292d036134148d9153e6b6f47 to your computer and use it in GitHub Desktop.
Save jkuester/26339ac292d036134148d9153e6b6f47 to your computer and use it in GitHub Desktop.
Converting Project with CommonJS Modules to ES Modules

Converting Project with CommonJS Modules to ES Modules

This is a record of my experiences uplifiting CHToolbox from CommonJS to ES Modules. Some things are specific to TypeScript.

  1. Updage esLint configuration. While on CJS, I was stuck on esLint 8.x. So, I was still using the old .eslintrc.js file format. To prepare for being able to upgrade to esLint 9.x (where support has been removed for the old config format), I preemptively updated my esLint config to the new flat format (in an eslint.config.mjs file).

    • This was a headache of its own since the migration path between config formats was not super clear (and the automated tools did not work for me).
    • The new flat config works with both esLint 8.x and 9.x.
  2. Update project configuration to target ESM:

    • Set "type": "module", in package.json

    • Update compilerOptions in tsconfig.json to have:

      "module": "nodenext",
      "moduleResolution": "nodenext",
      
  3. Convert all imports to new format (automated using ts2esm)

    • This process turned out to be super easy. No drama.
  4. Fix unit tests. This, by far, was the worst part of the process.

    • With ESM, you can no longer use sinon to directly modify the file dependencies. This article had the best breakdown I could find on the situation and the avaiable options.
    • Unfortunatly, as can been seen from reviewing the optinos in the article, basically all the options for doing stubbing in ESM unit tests are bad.
    • Unless you are content doing dependency injection at the platform level (e.g. Angular or Effect), basically your only realistic option for stubbing imports is to hijack the the loading of the module you want to test (simliar vibes to how rewire could be used in CJS tests).
    • I settled on using esmock in my test files to load the module being tested (with my sinon stubs injected). This allowed me to keep most of my test code and assertions the same (since I was still using sinon stubs). However, I had to re-write pretty much all of my test setup code to get the mocks injected where they needed to be.
  5. Deal with all the broken tooling.

    • Come to find out, nyc (istanbul code coverage) does not play very nicely with ESM. I ended up switching to c8 which worked with ESM out of the box (and was drop-in compatible with my nyc config).
    • Also had a bunch of troubled with ts-node (used for my mocha tests and direclty running the TS code locally). I ended up switching to tsx which also just worked out of the box with ESM

In the end, the only real "benefit" I have found so far with ESM (besides just remaining "compatible" with the trajectory of the JS ecosystem) is the support for top-level await statements (without needing to be wrapped in an async IIFE). Even this is probably bad practice in production code (performance concerns...), but it can be super handy in test files!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment