cri.dev about posts rss

Did an ES module migration, and it was okay

Published on

Just recently I moved minimal-analytics, a Node.js and Preact project to ES modules.

And it was quite pleasant and straightforward.

It didn’t require much work to migrate the project to ES modules 🤭.

Why?

  • wanted to learn more about ES modules
  • it’s the (foreseeable) future/standard of module resolution in Node.js and the browser
  • more and more people and dependencies are using ES modules
  • perfect excuse to revisit some parts of the project
  • why not?

How did it go?

I honestly had some troubles with two things:

Namely:

  • path.resolve occurrences with __dirname caused me some headaches
  • I had to dig deeper to simulate the require.main === module check

Ah, and before I forget, use node 16.16.0 (lts) for the best experience.

And also, set "type": "module" in your project’s package.json to make it work.

Check out the commit for the migration if you want to see the code.

path.resolve with __dirname

I had some trouble with this, since in a few places I needed to resolve for relative filepaths.

The CommonJS variables __dirname and __filename are not available in ES modules.

But the usage can be replicated with import.meta.url, the native URL module and pathname property.

E.g.

new URL(import.meta.url).pathname is essentially the same as __filename in the CommonJS environment.

And similarly, new URL('.', import.meta.url).pathname can be compared to __dirname in CommonJS.

Keep in mind, this is not the only way to simulate __dirname and __filename

Under an experimental flag, you can resolve modules relatively with import.meta.resolve (as far as I understood)

See this for more information.

require.main === module

This is oftentimes used to determine if the main script was run directly from the shell, or required as a module from another script.

I am using this technique to start the server when the script was invoked from the shell.

e.g.

#!/usr/bin/env node

...

if (require.main === module) {
  startServer()
}

...

To replicate this behaviour, I found multiple ways to do it:

# option 1
if (import.meta.url === `file://${process.argv[1]}`) { ... }

# option 2 
if (new URL(import.meta.url).pathname === process.argv[1]) { ... }

# option 3
import url from 'url'
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { ... }

# option 4
// TC39 proposal for import.meta.main

# option 5
# See https://github.com/tschaub/es-main
import esMain from 'es-main';

if (esMain(import.meta)) { ... }

Well, as always, in JavaScript, there are multiple ways to achieve the same thing. Some cleaner, some hackier.

Conclusion

Apart from the usual ramblings about JavaScript, Node.js, standards and headaches, it was a fun experience.

Learned a ton about ES modules, and how to use them in Node.js.

More reading about ESM

Here some of the best resources I found on this topic, I highly suggest to read them

Here, have a slice of pizza 🍕