Up to Node v8.5.0, publishing a module written in ES6 to NPMJS was a straightforward process: transpile the ES6 code using a tool like Babel, and publish to NPMJS the resulting lib
directory, while your GitHub repo contains the src
files.
With v8.5.0, Node has released experimental support for native modules (export
/import
) via the --experimental-modules
flag. It is now possible to publish purely-ES6 modules to NPMJS, and use them without any transpilation, as long as the files involved have an .mjs extension.
How can I publish an ES6 module (.mjs) so that it can also be used with older Node versions, which don't support ES native modules?
Update:
This is possible with 13.7.0+ using conditional exports (which as of 13.10.0+ are no longer experimental). It's not well documented or obvious how to do this in a completely backwards-compatible way, but here's the trick which I previously researched back when it was experiemental:
node_modules/mod/package.json
{
"main": "./lib.js",
"exports": {
".": [
{
"import": "./lib.mjs",
"require": "./lib.js",
"default": "./lib.js"
},
"./lib.js"
]
}
}
node_modules/mod/lib.js
exports.format = 'cjs';
node_modules/mod/lib.mjs
export const format = 'mjs';
Now it's possible to use both CommonJS:
main.js
const {format} = require('mod');
console.log(format);
$ node main.js
cjs
And ES Modules:
main.mjs
import {format} from 'mod';
console.log(format);
$ node main.mjs
(node:25573) ExperimentalWarning: The ESM module loader is experimental.
mjs
Old answer:
This method is no longer possible in more-recent versions of Node which removed automatic extension resolution for ESM, see Proposal for dual ESM/CommonJS packages for more information and some links to some potential new ways of doing this that may land in the future.
The trick is not to specify an exact path in the package.json main
entry. Instead use an extension-less main
entry, or supply both an index.js
and index.mjs
at the root of the package.
Option 1 - Extension-less main:
If you don't include the extension, Node will dynamically use the .mjs
extension if available and using the ES6 loader, or fallback on .js
.
"main": "lib/entry"
This will resolve to lib/entry.mjs
in ES6 module mode, or lib/entry.js
in CommonJS mode, with the ES6 loader falling back on the CommonJS version if the MJS file is not available.
Option 2 - Use index.mjs
and index.js
instead:
If your package supplies a root index.mjs
and index.js
Node will prefer the index.mjs
when import
-ed, and still use the index.js
when require
-ed (if no index.mjs
is supplied, the ES6 loader will use the index.js
). This means you can supply both an ES6 module version from index.mjs
and a CommonJS transpiled version from index.js
.
Possible Issue:
There is one potential issue that I can think of though, if users of your package mix using both the ES6 and CommonJS modules and expect them to reference the same set of objects. In certain edge-cases, this could be an issue, but multiple packages using the exact same module instance was never a given anyway because different packages can require different versions of the package.
Example:
Example project:
index.mjs
import testmod from 'testmod';
console.log(testmod);
index.js
const testmod = require('testmod');
console.log(testmod);
node_modules/testmod/package.json
{
"name": "testmod",
"version": "1.0.0"
}
In this file, you can optionally use an extension-less main
entry like this:
{
"name": "testmod",
"version": "1.0.0",
"main": "index"
}
node_modules/testmod/index.mjs
export default {
from: 'index.mjs'
};
node_modules/testmod/index.js
module.exports = {
from: 'index.js'
};
Example output (ExperimentalWarning omitted):
$ node --experimental-modules index.mjs
{ from: 'index.mjs' }
$ node --experimental-modules index.js
{ from: 'index.js' }
$ node index.js
{ from: 'index.js' }