If you are running JavaScript on the command line using Node.js, and you want to load a file using the JavaScript language feature import, you must do one of two things:

  1. use the .mjs extension for your JavaScript file to declare that it's a module, or
  2. add "type": "module" to your package.json, which tells Node.js to consider all .js files to be modules by default.

If you are running JavaScript on the command line using Node.js, and you want to load a file using the require() function that Node.js has supported for years and which is much more broadly compatible than the JavaScript language feature import, the file you load must not be a module. This is because TC39, the committee that designs new JavaScript features, chose violence.

This means that if you want to be able to load the same file either via import or via require(), you must either:

  1. use the .cjs extension to declare that it's not a module, or
  2. avoid adding "type": "module" to your package.json.

Okay, fine. Weird, but fine. But what if you want to load files from a web browser as well? Here's a thing about web browsers: they will not load JavaScript files unless they're served with the MIME type text/javascript. And most servers will only attach this MIME type to files with extension .js, not .cjs or .mjs. And browsers only support import, not require().

So.

If you want to make a file that's loadable via import and require(), on Node.js and the browser, it has to have extension .js, you cannot use "type": "module", and thus if your application contains files that use import you need to have two different names for each of those files, one with extension .js for the browser and one with extension .mjs for Node.js.


You must log in to comment.

in reply to @nex3's post:

What do you even want require for anyway? It's more limiting (synchronous, not possible to find reliably with static analysis, fundamentally can't work in browsers) and most of the ecosystem (node-fetch, all of sindresorhus's stuff, etc.) is moving that way. It's not even conceptually possible to require an es module from a commonjs module in a way that preserves the semantics of both systems because es modules support top level await and require is synchronous.

And require is definitely not more broadly compatible, import in node can import anything require can + any es module. And if you really really need commonjs you can just use dynamic import.

require() is strictly more compatible for downstream users because they can use it whether they're using require() or import. If you distribute an ESM-only package, you are forbidding anyone from using it unless they also are ESM-only. This is not a great policy if you want people to continue using the latest versions of your package.

Sindre's packages are a good example. You can look at the versions of @sindresorhus/is and see that the last version with CJS support has been downloaded 5.6 million times despite only being the most recent version for five months, while all the versions that are ESM-exclusive have only been downloaded a cumulative 1.5 million times.

I remember having issues with all the different ways of modules, in the end I think that the most painless way is to adapt to import/export and mjs everywhere you can help it. Now, if only most of Node's libraries didn't break when using this method...

TC39 chose violence by deciding to make import inherently asynchronous everywhere no matter what, and then Node.js also chose violence by deciding that that meant you could not load ESM modules with require() because require() is synchronous.