This blog post describes how module specifiers (the path-like IDs of modules) change with ECMAScript modules (ESM). There are a few subtle differences, compared to the familiar CommonJS module (CJS) style.
Node.js will support ES modules via the new filename extension
.mjs. Many web developers would have preferred to stick with
.js. Let me make the case why
.mjs is the better approach.
The core thing to keep in mind is: In order to distinguish between CommonJS modules and ES modules, you need some kind of metadata. Browsers face a similar problem, having to distinguish between scripts and modules. They attach the metadata via the attribute
Let’s start with approaches that would enable us to keep
package.json. One interesting aspect of this approach is that its file system layout is different from all other approaches: ESM versions of CJS files are kept in a separate directory whose structure mirrors the structure of the CJS directory.
"use module";). You could combine this approach with detection via parsing and only use a pragma where a file is ambiguous.
One issue with approaches #2 and #3 is that you can’t keep an ESM and a CJS version of a file next to each other (unless you also use approach #1).
It feels wrong to me that either human or machine should have to put that much effort into finding out what kind of file they are dealing with.
Using a different filename extension is a simple way of attaching metadata to files.
*.jsxfor React code with JSX
*.tsfor TypeScript files
*.es6for files to be compiled via Babel (initially popular, now getting out of fashion)
.js is visually undesirable.
Another argument in favor of keeping
.js is that tools need to be adapted in order to recognize
Looking at the pros and cons of all approaches, using
.mjs is not the best solution, it’s the least bad solution.
Module specifiers are path-like identifiers for modules. For example:
import lodash from 'lodash';
'lodash' is a module specifier.
The key change with ES modules is:
All ES module specifiers must be valid URLs
In particular, if the filename of a module has an extension then its specifier must have one, too. That is exactly like non-module
<script> elements work today.
import statements all have legal module specifiers:
import * as foo from 'http://example.com/lib/foo.mjs'; import * as foo from '/lib/foo.mjs'; import * as foo from './foo.mjs'; import * as foo from '../foo.mjs';
For compatibility with CommonJS and in preparation for future features, relative paths that don’t start with
../ are not allowed:
// Not allowed: import * as foo from 'foo.mjs'; import * as foo from 'lib/foo.mjs';
These are two best practices for writing module specifiers:
Referring to ES modules stored in local files: include the filename extensions. This applies to both browsers and Node.js.
'./foo.mjs'may (among others) refer to:
Referring to ES modules in
node_modules(not much will change for code compiled by tools such as webpack, because such tools can resolve paths at compile time). One option is to map
node_modulespaths to actual paths via a configuration file (similar to how AMD does it). Another option is to link into
It’s unclear if
node_modules specifiers will eventually have filename extensions, too.
Adding support for
.mjs to tools is ongoing. For example:
For Visual Studio Code, I added this to my workspace settings:
Acknowledgement: Thanks to Bradley Farias for answering my module-specifier-related questions.