Switching from CJS to ES Modules

I’ve written previously on my attempts at switching from CJS to ES Modules. tldr; it wasn’t as easy as I thought. But that had more to do with the configuration and setup of a particular project than it did Node’s implementation of ES modules.

I am happy to report that, once Node official announced core support for ES modules, I switched a few other projects over without much hassle.

Here’s a quick run-down of what it took.

Update package.json

Node will treat files with the extension .mjs as ES modules, but who wants to rename all their files?

As an alternative, Node will also treat .js files as ES modules when the nearest package.json contains a "type": "module" value in it. So I went with that.

Screenshot of a git diff for a package.json file.

You may have noticed that I also added an engines value too. As noted in the npm docs, this is a way to say “hey, you should probably be running x version(s) of Node with this project.” For me, the sole reason I specify 13.2.0 or higher is because that’s the moment when core ES module support shipped in Node! So that’s my new baseline.

Oh, and because one of the projects was building/deploying on top of netlify, I had to tell the build to make sure it was using that version of Node or higher as well.

Screenshot of a git diff for a netlify.toml file.

Change Module Statements from CJS to ESM

This next step might seem self-evident, but it’s worth noting anyway: I had to change all my require statements to import statements and my module.export statements to export statements.

Screenshot of git diff when changing import and export statements from CJS to ESM

Replace Any CJS-Specific Variables

Some CJS globals (like __dirname) are not available with ES modules. You can still use them as values, but you have to set them yourself. I was only using __dirname in one file, so I changed it in place.

import path from "path";
const __dirname = path.dirname(
  new URL(import.meta.url).pathname

There are actually a few different ways I’ve seen this done. Node’s docs have an example, so it’s probably worth following their code samples and not mine.

Note: if you’re going to need to access CJS variables like __dirname quite frequently across modules, you might want to abstract the retrieval of their values into helper functions as I’ve written about in a prior post.


That’s it. Here’s the PR where I did this for my blog’s codebase. The PR has a bit more code in it because I had to also change an aspect of my build/development tooling to get things working properly, but you can filter those pieces out and see the gist of what it took to transform that specific project from CJS to ESM.