Native ES Modules - something almost, but not quite entirely unlike CommonJS
Gil Tayar, November 2017
@giltayar
“He had found a Nutri-Matic machine which had provided him with a plastic cup […].
The way it functioned was very interesting. When the Drink button was pressed it made an instant but highly detailed examination of the subject's taste buds, a spectroscopic analysis of the subject's metabolism and then sent tiny experimental signals down the neural pathways to the taste centers of the subject's brain to see what was likely to go down well…
… However, no one knew quite why it did this because it invariably delivered a cupful of liquid that was almost, but not quite, entirely unlike tea.”
— Douglas Adams, The Hitchhiker's Guide to the Galaxy
About Me
ES Modules are coming to NodeJS!
$ node main.js
main.js
const {handle, spout, tea} = � require('./kettle')
console.log(handle)
console.log(spout)
console.log(tea)
$ node --experimental-modules main.mjs
main.mjs
import {handle, spout, tea}
from './kettle'
console.log(handle)
console.log(spout)
console.log(tea)
And they are so similar to CommonJS!
Similarities and Differences
Named exports
kettle.mjs
export const spout = 'the spout'
export const handle = 'the handle'
export const tea = 'hot tea'
main.mjs
import {handle, spout, tea} from './kettle'
console.log(handle) // ==> the handle
console.log(spout) // ==> the spout
console.log(tea) // ==> hot tea
kettle.js
module.exports.spout = 'the spout'
module.exports.handle = 'the handle'
module.exports.tea = 'hot tea'
main.js
const {handle, spout, tea} = require('./kettle')
console.log(handle) // ==> the handle
console.log(spout) // ==> the spout
console.log(tea) // ==> hot tea
kettle.mjs
export const spout = 'the spout'
export const handle = 'the handle'
export const tea = 'hot tea'
main.mjs
import {handle, spout, tea} from './kettle'
console.log(handle) // ==> the handle
console.log(spout) // ==> the spout
console.log(tea) // ==> hot tea
kettle.js
module.exports.spout = 'the spout'
module.exports.handle = 'the handle'
module.exports.tea = 'hot tea'
main.js
const {handle, spout, tea} = require('./kettle')
console.log(handle) // ==> the handle
console.log(spout) // ==> the spout
console.log(tea) // ==> hot tea
kettle.mjs
export const spout = 'the spout'
export const handle = 'the handle'
export const tea = 'hot tea'
main.mjs
import {handle, spout, tea} from './kettle'
console.log(handle) // ==> the handle
console.log(spout) // ==> the spout
console.log(tea) // ==> hot tea
kettle.js
module.exports.spout = 'the spout'
module.exports.handle = 'the handle'
module.exports.tea = 'hot tea'
main.js
const {handle, spout, tea} = require('./kettle')
console.log(handle) // ==> the handle
console.log(spout) // ==> the spout
console.log(tea) // ==> hot tea
kettle.mjs
export const spout = 'the spout'
export const handle = 'the handle'
export const tea = 'hot tea'
main.mjs
import {handle, spout, tea} from './kettle'
console.log(handle) // ==> the handle
console.log(spout) // ==> the spout
console.log(tea) // ==> hot tea
kettle.js
module.exports.spout = 'the spout'
module.exports.handle = 'the handle'
module.exports.tea = 'hot tea'
main.js
const {handle, spout, tea} = require('./kettle')
console.log(handle) // ==> the handle
console.log(spout) // ==> the spout
console.log(tea) // ==> hot tea
Asynchronous vs Synchronous
kettle.mjs
export const spout = 'the spout'
export const handle = 'the handle'
export const tea = 'hot tea'
main.mjs
import {handle, spout, tea} from './kettle'
console.log(handle) // ==> the handle
console.log(spout) // ==> the spout
console.log(tea) // ==> hot tea
kettle.js
module.exports.spout = 'the spout'
module.exports.handle = 'the handle'
module.exports.tea = 'hot tea'
main.js
const {handle, spout, tea} = require('./kettle')
console.log(handle) // ==> the handle
console.log(spout) // ==> the spout
console.log(tea) // ==> hot tea
The Rules of ES Modules
The Rules of ES Modules
Michael Jackson Script!
Binding
kettle.mjs
export const spout = 'the spout'
export const handle = 'the handle'
export let tea = 'hot tea'
export const heatUp = () =>
(tea = 'scalding tea')
main.mjs
import {handle, spout, tea, heatUp}
from './kettle'
console.log(handle) // the handle
console.log(spout) // the spout
heatUp(); console.log(tea) // scalding tea
kettle.js
module.exports.spout = 'the spout'
module.exports.handle = 'the handle'
module.exports.tea = 'hot tea'
module.exports.heatUp = () =>
(module.exports.tea = 'scalding tea')
main.js
const {handle, spout, tea, heatUp} =
require('./kettle')
console.log(handle) // the handle
console.log(spout) // the spout
heatUp(); console.log(tea) // hot tea
kettle.mjs
export const spout = 'the spout'
export const handle = 'the handle'
export let tea = 'hot tea'
export const heatUp = () =>
(tea = 'scalding tea')
main.mjs
import {handle, spout, tea, heatUp}
from './kettle'
console.log(handle) // the handle
console.log(spout) // the spout
heatUp(); console.log(tea) // scalding tea
kettle.js
module.exports.spout = 'the spout'
module.exports.handle = 'the handle'
module.exports.tea = 'hot tea'
module.exports.heatUp = () =>
(module.exports.tea = 'scalding tea')
main.js
const {handle, spout, tea, heatUp} =
require('./kettle')
console.log(handle) // the handle
console.log(spout) // the spout
heatUp(); console.log(tea) // hot tea
kettle.mjs
export const spout = 'the spout'
export const handle = 'the handle'
export let tea = 'hot tea'
export const heatUp = () =>
(tea = 'scalding tea')
main.mjs
import {handle, spout, tea, heatUp}
from './kettle'
console.log(handle) // the handle
console.log(spout) // the spout
heatUp(); console.log(tea) // scalding tea
kettle.js
module.exports.spout = 'the spout'
module.exports.handle = 'the handle'
module.exports.tea = 'hot tea'
module.exports.heatUp = () =>
(module.exports.tea = 'scalding tea')
main.js
const {handle, spout, tea, heatUp} =
require('./kettle')
console.log(handle) // the handle
console.log(spout) // the spout
heatUp(); console.log(tea) // hot tea
Default export
kettle.js
module.exports = scalding => {
return {
handle: 'the handle',
spout: 'the spout',
tea: scalding ? 'scalding tea' : 'hot tea',
}
}
main.js
const kettleMaker = require('./kettle')
const kettle = kettleMaker(false)
console.log(kettle.handle) // the handle
console.log(kettle.spout) // the spout
console.log(kettle.tea) // hot tea
kettle.mjs
export default scalding => {
return {
handle: 'the handle',
spout: 'the spout',
tea: scalding ? 'scalding tea' : 'hot tea',
}
}
main.mjs
import kettleMaker from './kettle'
const kettle = kettleMaker(false)
console.log(kettle.handle) // the handle
console.log(kettle.spout) // the spout
console.log(kettle.tea) // hot tea
kettle.mjs
export default scalding => {
return {
handle: 'the handle',
spout: 'the spout',
tea: scalding ? 'scalding tea' : 'hot tea',
}
}
main.mjs
import kettleMaker from './kettle'
const kettle = kettleMaker(false)
console.log(kettle.handle) // the handle
console.log(kettle.spout) // the spout
console.log(kettle.tea) // hot tea
kettle.js
module.exports = scalding => {
return {
handle: 'the handle',
spout: 'the spout',
tea: scalding ? 'scalding tea' : 'hot tea',
}
}
main.js
const kettleMaker = require('./kettle')
const kettle = kettleMaker(false)
console.log(kettle.handle) // the handle
console.log(kettle.spout) // the spout
console.log(kettle.tea) // hot tea
kettle.mjs
export default scalding => {
return {
handle: 'the handle',
spout: 'the spout',
tea: scalding ? 'scalding tea' : 'hot tea',
}
}
main.mjs
import kettleMaker from './kettle'
const kettle = kettleMaker(false)
console.log(kettle.handle) // the handle
console.log(kettle.spout) // the spout
console.log(kettle.tea) // hot tea
kettle.js
module.exports = scalding => {
return {
handle: 'the handle',
spout: 'the spout',
tea: scalding ? 'scalding tea' : 'hot tea',
}
}
main.js
const kettleMaker = require('./kettle')
const kettle = kettleMaker(false)
console.log(kettle.handle) // the handle
console.log(kettle.spout) // the spout
console.log(kettle.tea) // hot tea
Default + named exports
kettle.js
module.exports = scalding => {
return {
handle: 'the handle',
spout: 'the spout',
tea: scalding ? 'scalding tea' : 'hot tea',
}
}
module.exports.potColor = 'black'
main.js
const kettleMaker = require('./kettle')
const {potColor} = kettleMaker
const kettle = kettleMaker(false)
console.log(kettle.handle) // the handle
// ...
console.log('the pot is', potColor) // the pot is black
kettle.mjs
export default scalding => {
return {
handle: 'the handle',
spout: 'the spout',
tea: scalding ? 'scalding tea' : 'hot tea',
}
}
export const potColor = 'black'
main.mjs
import kettleMaker, {potColor} from './kettle'
const kettle = kettleMaker(false)
console.log(kettle.handle) // the handle
// ...
console.log('the pot is', potColor) // the pot is black
kettle.js
module.exports = scalding => {
return {
handle: 'the handle',
spout: 'the spout',
tea: scalding ? 'scalding tea' : 'hot tea',
}
}
module.exports.potColor = 'black'
main.js
const kettleMaker = require('./kettle')
const {potColor} = kettleMaker
const kettle = kettleMaker(false)
console.log(kettle.handle) // the handle
// ...
console.log('the pot is', potColor) // the pot is black
kettle.mjs
export default scalding => {
return {
handle: 'the handle',
spout: 'the spout',
tea: scalding ? 'scalding tea' : 'hot tea',
}
}
export const potColor = 'black'
main.mjs
import kettleMaker, {potColor} from './kettle'
const kettle = kettleMaker(false)
console.log(kettle.handle) // the handle
// ...
console.log('the pot is', potColor) // the pot is black
kettle.js
module.exports = scalding => {
return {
handle: 'the handle',
spout: 'the spout',
tea: scalding ? 'scalding tea' : 'hot tea',
}
}
module.exports.potColor = 'black'
main.js
const kettleMaker = require('./kettle')
const {potColor} = kettleMaker
const kettle = kettleMaker(false)
console.log(kettle.handle) // the handle
// ...
console.log('the pot is', potColor) // the pot is black
kettle.mjs
export default scalding => {
return {
handle: 'the handle',
spout: 'the spout',
tea: scalding ? 'scalding tea' : 'hot tea',
}
}
export const potColor = 'black'
main.mjs
import kettleMaker, {potColor} from './kettle'
const kettle = kettleMaker(false)
console.log(kettle.handle) // the handle
// ...
console.log('the pot is', potColor) // the pot is black
Renaming exports
kettle.js
module.exports.spout = 'the spout'
module.exports.handle = 'the handle'
module.exports.tea = 'hot tea'
main.js
const {handle: handoru,
spout: sosogiguchi, tea: お茶} =
require('./kettle')
console.log(handoru) // ==> the handle
console.log(sosogiguchi) // ==> the spout
console.log(お茶) // ==> hot tea
kettle.mjs
export const spout = 'the spout'
export const handle = 'the handle'
export const tea = 'hot tea'
main.mjs
import {handle as handoru,
spout as sosogiguchi, tea as お茶}
from './kettle'
console.log(handoru) // ==> the handle
console.log(sosogiguchi) // ==> the spout
console.log(お茶) // ==> hot tea
kettle.js
module.exports.spout = 'the spout'
module.exports.handle = 'the handle'
module.exports.tea = 'hot tea'
main.js
const {handle: handoru,
spout: sosogiguchi, tea: お茶} =
require('./kettle')
console.log(handoru) // ==> the handle
console.log(sosogiguchi) // ==> the spout
console.log(お茶) // ==> hot tea
kettle.mjs
export const spout = 'the spout'
export const handle = 'the handle'
export const tea = 'hot tea'
main.mjs
import {handle as handoru,
spout as sosogiguchi, tea as お茶}
from './kettle'
console.log(handoru) // ==> the handle
console.log(sosogiguchi) // ==> the spout
console.log(お茶) // ==> hot tea
kettle.js
module.exports.spout = 'the spout'
module.exports.handle = 'the handle'
module.exports.tea = 'hot tea'
main.js
const {handle: handoru,
spout: sosogiguchi, tea: お茶} =
require('./kettle')
console.log(handoru) // ==> the handle
console.log(sosogiguchi) // ==> the spout
console.log(お茶) // ==> hot tea
kettle.mjs
export const spout = 'the spout'
export const handle = 'the handle'
export const tea = 'hot tea'
main.mjs
import {handle as handoru,
spout as sosogiguchi, tea as お茶}
from './kettle'
console.log(handoru) // ==> the handle
console.log(sosogiguchi) // ==> the spout
console.log(お茶) // ==> hot tea
Very cool!
File Resolution - index
kettle.mjs
export const spout = 'the spout'
export const handle = 'the handle'
export const tea = 'hot tea'
main.mjs
import {handle, spout, tea} from './kettle'
console.log(handle) // ==> the handle
console.log(spout) // ==> the spout
console.log(tea) // ==> hot tea
kettle.js
module.exports.spout = 'the spout'
module.exports.handle = 'the handle'
module.exports.tea = 'hot tea'
main.js
const {handle, spout, tea} = require('./kettle')
console.log(handle) // ==> the handle
console.log(spout) // ==> the spout
console.log(tea) // ==> hot tea
Remember?
kettle/index.mjs
export const spout = 'the spout'
export const handle = 'the handle'
export const tea = 'hot tea'
main.mjs
import {handle, spout, tea} from './kettle'
console.log(handle) // ==> the handle
console.log(spout) // ==> the spout
console.log(tea) // ==> hot tea
kettle/index.js
module.exports.spout = 'the spout'
module.exports.handle = 'the handle'
module.exports.tea = 'hot tea'
main.js
const {handle, spout, tea} = require('./kettle')
console.log(handle) // ==> the handle
console.log(spout) // ==> the spout
console.log(tea) // ==> hot tea
File Resolution - package.json
kettle/kettle.mjs
export const spout = 'the spout'
export const handle = 'the handle'
export const tea = 'hot tea'
kettle/package.json
{"main": "kettle.mjs"}
main.mjs
import {handle, spout, tea} from './kettle'
console.log(handle) // ==> the handle
console.log(spout) // ==> the spout
console.log(tea) // ==> hot tea
kettle/kettle.js
module.exports.spout = 'the spout'
module.exports.handle = 'the handle'
module.exports.tea = 'hot tea'
kettle/package.json
{"main": "kettle.js"}
main.js
const {handle, spout, tea} = require('./kettle')
console.log(handle) // ==> the handle
console.log(spout) // ==> the spout
console.log(tea) // ==> hot tea
File Resolution - node_modules
node_modules/kettle.mjs
export const spout = 'the spout'
export const handle = 'the handle'
export const tea = 'hot tea'
main.mjs
import {handle, spout, tea} from 'kettle'
console.log(handle) // ==> the handle
console.log(spout) // ==> the spout
console.log(tea) // ==> hot tea
node_modules/kettle.js
module.exports.spout = 'the spout'
module.exports.handle = 'the handle'
module.exports.tea = 'hot tea'
main.js
const {handle, spout, tea} = require('kettle')
console.log(handle) // ==> the handle
console.log(spout) // ==> the spout
console.log(tea) // ==> hot tea
node_modules/kettle.mjs
export const spout = 'the spout'
export const handle = 'the handle'
export const tea = 'hot tea'
main.mjs
import {handle, spout, tea} from 'kettle'
console.log(handle) // ==> the handle
console.log(spout) // ==> the spout
console.log(tea) // ==> hot tea
node_modules/kettle.js
module.exports.spout = 'the spout'
module.exports.handle = 'the handle'
module.exports.tea = 'hot tea'
main.js
const {handle, spout, tea} = require('kettle')
console.log(handle) // ==> the handle
console.log(spout) // ==> the spout
console.log(tea) // ==> hot tea
Dynamic import
kettle.mjs
export const spout = 'the spout'
export const handle = 'the handle'
export const tea = 'hot tea'
main.mjs
async function main() {
const {tea} =
await import('./kettle.mjs')
console.log(tea)
}
main()
kettle.js
module.exports.spout = 'the spout'
module.exports.handle = 'the handle'
module.exports.tea = 'hot tea'
main.js
function main() {
const {tea} =
require('./kettle.js')
console.log(tea)
}
main()
kettle.mjs
export const spout = 'the spout'
export const handle = 'the handle'
export const tea = 'hot tea'
main.mjs
async function main() {
const {tea} =
await import('./kettle.mjs')
console.log(tea)
}
main()
kettle.js
module.exports.spout = 'the spout'
module.exports.handle = 'the handle'
module.exports.tea = 'hot tea'
main.js
function main() {
const {tea} =
require('./kettle.js')
console.log(tea)
}
main()
Module dirname
main.js
const path = require('path')
const fs = require('fs')
console.log(
fs.readFileSync(
path.join(__dirname, 'hello.txt'),
'utf-8'))
main.mjs
import path from 'path'
import fs from 'fs'
import url from 'url'
const __dirname = path.dirname(
new url.URL(import.meta.url).pathname)
console.log(
fs.readFileSync(
path.join(__dirname, 'hello.txt'),
'utf-8'))
main.js
const path = require('path')
const fs = require('fs')
console.log(
fs.readFileSync(
path.join(__dirname, 'hello.txt'),
'utf-8'))
main.mjs
import path from 'path'
import fs from 'fs'
import url from 'url'
const __dirname = path.dirname(
new url.URL(import.meta.url).pathname)
console.log(
fs.readFileSync(
path.join(__dirname, 'hello.txt'),
'utf-8'))
Interoperability
Remember The Rules of ESM?
Two separate worlds
.mjs
.mjs
import
.js
.js
require
The Rules of Interoperability
Bridge over troubled waters
.mjs
.mjs
import
.js
.js
require
default import
dynamic import
The Rules of Interoperability
kettle.js
module.exports = 'short and stout (js)'
main.mjs
import kettle from './kettle'
console.log(kettle) // short and stout (js)
kettle.js
module.exports = 'short and stout (js)'
main.mjs
import kettle from './kettle'
console.log(kettle) // short and stout (js)
The Rules of Interoperability
.mjs
.mjs
import
.js
import
kettle.js
module.exports.kettle = 'short and stout (js)'
main.mjs
import {kettle} from './kettle'
console.log(kettle)
Fails with error!
kettle.js
module.exports.kettle = 'short and stout (js)'
main.mjs
import {kettle} from './kettle'
console.log(kettle)
Fails with error!
(only default import)
kettle.js
module.exports.kettle = 'short and stout (js)'
main.mjs
import kettleModule from './kettle'
console.log(kettleModule.kettle) // short and stout (js)
Works!
The Rules of Interoperability
.mjs
.mjs
import
.js
import
The Rules of Interoperability
.js
.mjs
require
.js
dyamic import
kettle.mjs
export const kettle = 'short and stout (mjs)'
main.js
const {kettle} = require('./kettle')
console.log(kettle)
Fails with error!�(cannot require mjs)
kettle.mjs
export const kettle = 'short and stout (mjs)'
main.js
const {kettle} = require('./kettle')
console.log(kettle)
Fails with error!
Because mjs loading is an async task
kettle.mjs
export const kettle = 'short and stout (mjs)'
main.js
import {kettle} from './kettle'
console.log(kettle)
Fails with error!�(import not allowed in js)
kettle.mjs
export const kettle = 'short and stout (mjs)'
main.js
async function main() {
const {kettle} = await import('./kettle')
console.log(kettle) // short and stout (mjs)
}
main()
Works!
The Rules of Interoperability
.mjs
.mjs
import
.js
.js
require
import
dynamic import
Migrating from CJS
Migrating
Migrating as application developers
Migrating
The Rules of Migration
Dual-Mode Libraries
an-esm-module.mjs
import {handle, spout, tea} from './kettle'
console.log(handle) // ==> the handle
console.log(spout) // ==> the spout
console.log(tea) // ==> hot tea
a-cjs-module.js
const {handle, spout, tea} = require('./kettle')
console.log(handle) // ==> the handle
console.log(spout) // ==> the spout
console.log(tea) // ==> hot tea
main.mjs
export const spout = 'the spout'
export const handle = 'the handle'
export const tea = 'hot tea'
main.js
module.exports.spout = 'the spout'
module.exports.handle = 'the handle'
module.exports.tea = 'hot tea'
I have to write my code twice?!
kettle/entry.mjs
export const spout = 'the spout'
export const handle = 'the handle'
export const tea = 'hot tea'
kettle/package.json
{
"name": "kettle",
"main": "entry",
"scripts": {
"build": "babel *.mjs **/*.mjs --out-dir ."
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
"babel-plugin-dynamic-import-node": "^1.1.0"
}, ...
kettle/entry.mjs
export const spout = 'the spout'
export const handle = 'the handle'
export const tea = 'hot tea'
kettle/package.json
{
"name": "kettle",
"main": "entry",
"scripts": {
"build": "babel *.mjs **/*.mjs --out-dir ."
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
"babel-plugin-dynamic-import-node": "^1.1.0"
}, ...
You write your file mjs-style
kettle/entry.mjs
export const spout = 'the spout'
export const handle = 'the handle'
export const tea = 'hot tea'
kettle/package.json
{
"name": "kettle",
"main": "entry",
"scripts": {
"build": "babel *.mjs **/*.mjs --out-dir ."
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
"babel-plugin-dynamic-import-node": "^1.1.0"
}, ...
kettle/.babelrc
{
"plugins": [
"transform-es2015-modules-commonjs",
"dynamic-import-node"
]
}
You write your file mjs-style
And babelize all mjs to js
kettle/.gitignore
*.js
Don’t forget to .gitignore all js files!
main.mjs
import {handle, spout, tea} from './kettle'
console.log(handle) // ==> the handle
console.log(spout) // ==> the spout
console.log(tea) // ==> hot tea
main.js
const {handle, spout, tea} = require('./kettle')
console.log(handle) // ==> the handle
console.log(spout) // ==> the spout
console.log(tea) // ==> hot tea
Dual-Mode library, bitches!
Summary
ES Modules are coming to NodeJS!
The Rules of ES Modules
The Rules of Interoperability
.mjs
.mjs
import
.js
.js
require
import
dynamic import
The Rules of Migration
Migrating as application developers
kettle/entry.mjs
export const spout = 'the spout'
export const handle = 'the handle'
export const tea = 'hot tea'
kettle/package.json
{
"name": "kettle",
"main": "entry",
"scripts": {
"build": "babel *.mjs **/*.mjs --out-dir ."
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
"babel-plugin-dynamic-import-node": "^1.1.0"
}, ...
kettle/.babelrc
{
"plugins": [
"transform-es2015-modules-commonjs",
"dynamic-import-node"
]
}
You write your file mjs-style
And babelize all mjs to js
Thank You!
Twitter: @giltayar
https://github.com/giltayar/node-esm-tea