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 type="module"
in <script>
elements.
Let’s start with approaches that would enable us to keep .js
.
.js
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.
.mjs
Using a different filename extension is a simple way of attaching metadata to files.
There are many precedents for putting slightly different JavaScript into files with different filename extensions. For example:
*.jsx
for React code with JSX*.ts
for TypeScript files*.es6
for files to be compiled via Babel (initially popular, now getting out of fashion).js
desirable? Whatever filename extension for modules we’ll end up with, it will be the dominant extension for JavaScript in the future. Thus, moving away from .js
is visually undesirable.
Another argument in favor of keeping .js
is that tools need to be adapted in order to recognize .mjs
files as containing JavaScript. However, I’d argue that most tools need to be changed significantly to meaningfully handle ESM, anyway.
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';
The string '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.
The following 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 ./
or ../
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.
.mjs
in browsers will help with keeping JavaScript code cross-platform compatible.'./foo.mjs'
may (among others) refer to: foo.mjs
, foo.mjs/index.mjs
, foo.mjs/index.js
Referring to ES modules in node_modules
:
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_modules
paths to actual paths via a configuration file (similar to how AMD does it). Another option is to link into node_modules
directories.It’s unclear if node_modules
specifiers will eventually have filename extensions, too.
.mjs
in tools Adding support for .mjs
to tools is ongoing. For example:
.mjs
: https://github.com/babel/babili/blob/2b1b16ac05596e65ec77c56a1e3e1b7882991341/packages/babili/src/fs.js#L6For Visual Studio Code, I added this to my workspace settings:
"files.associations": {
"*.mjs": "javascript"
}
Acknowledgement: Thanks to Bradley Farias for answering my module-specifier-related questions.