SlideShare a Scribd company logo
1 of 103
Download to read offline
@NicoloRibaudo
Migrating Babel from
CommonJS to ESM
NICOLÒ RIBAUDO
@nicolo-ribaudo
@NicoloRibaudo hello@nicr.dev
@NicoloRibaudo 2
@NicoloRibaudo
Once upon a time …
3
@NicoloRibaudo
… there was a little JavaScript library, written using the shiny new
import / export syntax and published to npm compiled to CommonJS.
Once upon a time …
4
import { parse } from "babylon";
export function transform(code) {
let ast = parse(code);
return es6to5(ast);
}
"use strict";
exports.__esModule = true;
exports.transform = transform;
var _babylon = require("babylon");
function transform(code) {
let ast = _babylon.parse(code);
return es6to5(ast);
}
@NicoloRibaudo
Until one day, suddenly …
5
@NicoloRibaudo
Until one day, suddenly …
6
@NicoloRibaudo
Until one day, suddenly …
7
@NicoloRibaudo
Our adventure begins here.
8
@NicoloRibaudo
Chapter 1:
Introduction
9
@NicoloRibaudo
What is Babel?
10
@NicoloRibaudo
What is Babel?
11
Babel is a build-time
compiler
JavaScript
@NicoloRibaudo
What is Babel?
12
Babel is a build-time
compiler
JavaScript
@NicoloRibaudo
What is Babel?
13
Babel is a build-time
devtool
@NicoloRibaudo
What is Babel?
14
Babel is a build-time
devtool
configurable
@NicoloRibaudo 15
// babel.config.js
module.exports = function () {
return {
targets: [
"last 3 versions",
"not ie"
],
ignore: ["**/*.test.js"]
};
};
$ npx babel src --out-dir lib
"Generate code that is compatible
with these browsers"
"Don't compile test files"
@NicoloRibaudo
What is Babel?
16
Babel is a build-time
devtool
pluggable
@NicoloRibaudo 17
// babel-plugin-hello.js
const { types: t } = require("@babel/core");
module.exports = () => ({
visitor: {
StringLiteral(path) {
path.replaceWith(
t.stringLiteral("Hello World!")
);
},
},
});
$ npx babel src --out-dir lib
Apply some transformation
to the parsed code,
potentially relying on
Babel-provided AST utilities
@NicoloRibaudo
What is Babel?
18
Babel is a build-time
devtool
n embeddable
@NicoloRibaudo 19
// webpack.config.js
module.exports = {
entry: "./src/main.js",
output: "./out/bundle.js",
module: {
rules: [
{
test: /.js|.ts|.jsx/,
use: "babel-loader",
},
],
},
};
// rollup.config.js
import {
babel
} from "@rollup/plugin-babel";
export default {
input: "src/index.js",
output: {
dir: "output",
format: "es",
},
plugins: [ babel() ],
};
● Webpack
● Rollup
● Parcel
● Vite
● ESLint
● Prettier
● …
@NicoloRibaudo
What is Babel?
20
Babel is a build-time
library
JavaScript
@NicoloRibaudo 21
const babel = require("@babel/core");
const fs = require("fs/promises");
babel
.transformFileAsync("./main.js")
.then(({ code }) =>
fs.writeFile("./main.out.js", code)
).catch(err =>
console.error("Cannot compile!", err)
);
const parser = require("@babl/parser");
const generator = require("@babl/generator");
const ast = parser.parse("const a = 1;");
ast.program.body[0].declarations[0]
.id.name = "b";
const code = generator.default(ast);
console.log(code); // "const b = 1;"
@NicoloRibaudo
ESM challenges
22
@NicoloRibaudo
ESM challenges
23
#1 — It cannot be synchronously imported from CommonJS in Node.js
// SyntaxError in CommonJS
import "./module.mjs";
// Error [ERR_REQUIRE_ESM]: require() of ES Module not supported
require("./module.mjs");
// This works, but it's asynchronous
import("./module.mjs"); // Promise
@NicoloRibaudo
ESM challenges
24
#2 — It cannot be synchronously imported dynamically or lazily
// CommonJS
function loadIt() {
require("./module.cjs");
}
// ES Modules - SyntaxError // ES Modules - async
function loadIt() { async function loadIt() {
import "./module.cjs"; await import("./module.cjs");
} }
@NicoloRibaudo
ESM challenges
25
#3 — ESM-compiled-to-CJS has a different interface from native ESM
import obj from "./esm-compiled-to-cjs.js";
console.log(obj);
// { __esModule: true, default: [Function: A], namedExport: "foo" }
@NicoloRibaudo
ESM challenges
26
#4 — It doesn't integrate with tools that virtualize require and CJS loading
● mocking
● on-the-fly transpilation
● other bad things
🧙
@NicoloRibaudo
Internal vs External
code
27
@NicoloRibaudo
Internal vs External
28
Babel's source
● Written by Babel contributors
● Used by all Babel users
Babel's tests
● Written by Babel contributors
● Used by Babel contributors
Configuration files
● Written by (almost) all Babel users
Plugins
● Written by a limited number of
developers
@NicoloRibaudo
Internal vs External
29
@NicoloRibaudo
Internal vs External
30
@NicoloRibaudo
Chapter 2:
Making Babel
asynchronous
31
@NicoloRibaudo
The async/await virus
32
function transform(code, opts) {
const config = loadFullConfig(opts);
// ...
function loadFullConfig(inputOpts) {
const result = loadPrivatePartialConfig(inputOpts);
// ...
function loadPrivatePartialConfig(inputOpts) {
// ...
const configChain = buildRootChain(args, context);
// ...
function buildRootChain(opts, context) {
// ...
configFile = loadOneConfig(ROOT_FILENAMES, dirname);
// ...
function loadOneConfig(filenames, dirname) {
// ...
const config = readConfigCode(filepath);
// ...
function readConfigCode(filepath) {
// ...
options = loadCodeDefault(filepath);
// ...
function loadCodeDefault(filepath) {
const module = require(filepath);
// ...
@NicoloRibaudo
The async/await virus
33
function transform(code, opts) {
const config = loadFullConfig(opts);
// ...
function loadFullConfig(inputOpts) {
const result = loadPrivatePartialConfig(inputOpts);
// ...
function loadPrivatePartialConfig(inputOpts) {
// ...
const configChain = buildRootChain(args, context);
// ...
function buildRootChain(opts, context) {
// ...
configFile = loadOneConfig(ROOT_FILENAMES, dirname);
// ...
function loadOneConfig(filenames, dirname) {
// ...
const config = readConfigCode(filepath);
// ...
function readConfigCode(filepath) {
// ...
options = loadCodeDefault(filepath);
// ...
function loadCodeDefault(filepath) {
const module = require(filepath);
const module = await import(filepath);
// ...
@NicoloRibaudo
The async/await virus
34
function transform(code, opts) {
const config = loadFullConfig(opts);
// ...
function loadFullConfig(inputOpts) {
const result = loadPrivatePartialConfig(inputOpts);
// ...
function loadPrivatePartialConfig(inputOpts) {
// ...
const configChain = buildRootChain(args, context);
// ...
function buildRootChain(opts, context) {
// ...
configFile = loadOneConfig(ROOT_FILENAMES, dirname);
// ...
function loadOneConfig(filenames, dirname) {
// ...
const config = readConfigCode(filepath);
// ...
function readConfigCode(filepath) {
// ...
options = loadCodeDefault(filepath);
// ...
async function loadCodeDefault(filepath) {
const module = require(filepath);
const module = await import(filepath);
// ...
@NicoloRibaudo
The async/await virus
35
function transform(code, opts) {
const config = loadFullConfig(opts);
// ...
function loadFullConfig(inputOpts) {
const result = loadPrivatePartialConfig(inputOpts);
// ...
function loadPrivatePartialConfig(inputOpts) {
// ...
const configChain = buildRootChain(args, context);
// ...
function buildRootChain(opts, context) {
// ...
configFile = loadOneConfig(ROOT_FILENAMES, dirname);
// ...
function loadOneConfig(filenames, dirname) {
// ...
const config = readConfigCode(filepath);
// ...
function readConfigCode(filepath) {
// ...
options = await loadCodeDefault(filepath);
// ...
async function loadCodeDefault(filepath) {
const module = require(filepath);
const module = await import(filepath);
// ...
@NicoloRibaudo
The async/await virus
36
async function transform(code, opts) {
const config = await loadFullConfig(opts);
// ...
async function loadFullConfig(inputOpts) {
const result = await loadPrivatePartialConfig(inputOpts);
// ...
async function loadPrivatePartialConfig(inputOpts) {
// ...
const configChain = await buildRootChain(args, context);
// ...
async function buildRootChain(opts, context) {
// ...
configFile = await loadOneConfig(ROOT_FILENAMES, dirname);
// ...
async function loadOneConfig(filenames, dirname) {
// ...
const config = await readConfigCode(filepath);
// ...
async function readConfigCode(filepath) {
// ...
options = await loadCodeDefault(filepath);
// ...
async function loadCodeDefault(filepath) {
const module = require(filepath);
const module = await import(filepath);
// ...
@NicoloRibaudo
Preserving Babel's sync API
37
● Preserves backward compatibility
● The asynchronous API is only necessary when loading ESM files
function transform(code, opts) async function transformAsync(code, opts)
@NicoloRibaudo
Preserving Babel's sync API
38
function transform(code, opts)
function loadFullConfig(inputOpts) {
function loadPrivatePartialConfig(inputOpts) {
function buildRootChain(opts, context) {
function loadOneConfig(filenames, dirname) {
function readConfigCode(filepath) {
function loadCodeDefault(filepath) {
if (isCommonJS(filepath)) {
module = require(filepath);
} else {
throw new Error("Unsupported ESM config!");
}
async function transformAsync(code, opts)
async function loadFullConfig(inputOpts) {
async function loadPrivatePartialConfig(inputOpts)
async function buildRootChain(opts, context) {
async function loadOneConfig(filenames, dirname) {
async function readConfigCode(filepath) {
async function loadCodeDefault(filepath) {
if (isCommonJS(filepath)) {
module = require(filepath);
} else {
module = await import(filepath);
}
{
@NicoloRibaudo
Preserving Babel's sync API
39
Can we have a single implementation, capable of running both synchronously
and asynchronously?
@NicoloRibaudo
Preserving Babel's sync API
40
Can we have a single implementation, capable of running both synchronously
and asynchronously?
Callbacks?
function loadCodeDefault(filepath, callback) {
if (isCommonJS(filepath)) {
try { callback(null, require(filepath)); }
catch (err) { callback(err); }
} else {
import(filepath).then(
module => callback(null, module),
err => callback(err)
);
}
}
@NicoloRibaudo
gensync: abstracting the
41
gensync: abstracting the ???
Me trying to
prepare these
slides
Me spending too
much time looking
for a word
@NicoloRibaudo
gensync: abstracting the ???
42
https://github.com/loganfsmyth/gensync
@NicoloRibaudo
gensync: abstracting the ???
43
const readJSON = async function (filepath) {
const contents = await readFile(filepath, "utf8");
return JSON.parse(contents);
};
@NicoloRibaudo
gensync: abstracting the ???
44
const readJSON = async function (filepath) {
const contents = await readFile(filepath, "utf8");
return JSON.parse(contents);
};
const readJSON = gensync(function* (filepath) {
const contents = yield* readFile(filepath, "utf8");
return JSON.parse(contents);
});
@NicoloRibaudo
gensync: abstracting the ???
45
const readJSON = async function (filepath) {
const contents = await readFile(filepath, "utf8");
return JSON.parse(contents);
};
const readJSON = gensync(function* (filepath) {
const contents = yield* readFile(filepath, "utf8");
return JSON.parse(contents);
});
const json = readJSON.sync("package.json");
// or
const json = await readJSON.async("package.json");
@NicoloRibaudo
gensync: abstracting the ???
46
const readJSON = async function (filepath) {
const contents = await readFile(filepath, "utf8");
return JSON.parse(contents);
};
const readJSON = gensync(function* (filepath) {
const contents = yield* readFile(filepath, "utf8");
return JSON.parse(contents);
});
const json = readJSON.sync("package.json");
// or
const json = await readJSON.async("package.json");
const readFile = gensync({
sync: fs.readFileSync,
errback: fs.readFile,
});
@NicoloRibaudo
gensync: abstracting the ???
47
const readJSON = async function (filepath) {
const contents = await readFile(filepath, "utf8");
return JSON.parse(contents);
};
const readJSON = gensync(function* (filepath) {
const contents = yield* readFile(filepath, "utf8");
return JSON.parse(contents);
});
const json = readJSON.sync("package.json");
// or
const json = await readJSON.async("package.json");
const readFile = gensync({
sync: fs.readFileSync,
errback: fs.readFile,
});
You can define any utility that
branches on the execution model:
const isAsync = gensync({
sync: () => false,
async: async () => true,
});
@NicoloRibaudo 48
const transform = gensync(function* (code, opts) {
const config = yield* loadFullConfig(opts);
// ...
function* loadFullConfig(inputOpts) {
const result = yield* loadPrivatePartialConfig(inputOpts);
// ...
function* loadPrivatePartialConfig(opts) {
// ...
const configChain = yield* buildRootChain(args, context);
// ...
function* buildRootChain(opts, context) {
// ...
configFile = yield* loadOneConfig(ROOT_FILENAMES, dirname);
// ...
function* loadOneConfig(filenames, dirname) {
// ...
const config = yield* readConfigCode(filepath);
// ...
}
function* readConfigCode(filepath) {
// ...
options = yield* loadCodeDefault(filepath);
// ...
}
gensync: abstracting the ???
@NicoloRibaudo 49
const transform = gensync(function* (code, opts) {
const config = yield* loadFullConfig(opts);
// ...
export const transform = transform.errback;
export const transformSync = transform.sync;
export const transformAsync = transform.async;
gensync: abstracting the ???
@NicoloRibaudo 50
gensync: abstracting the ???
function loadCodeDefault(filepath) {
if (isCommonJS(filepath)) {
module = require(filepath);
} else {
throw new Error("Unsupported ESM!");
}
async function loadCodeDefault(filepath) {
if (isCommonJS(filepath)) {
module = require(filepath);
} else {
module = await import(filepath);
}
@NicoloRibaudo 51
gensync: abstracting the ???
function loadCodeDefault(filepath) {
if (isCommonJS(filepath)) {
module = require(filepath);
} else {
throw new Error("Unsupported ESM!");
}
async function loadCodeDefault(filepath) {
if (isCommonJS(filepath)) {
module = require(filepath);
} else {
module = await import(filepath);
}
function* loadCodeDefault(filepath) {
if (isCommonJS(filepath)) {
module = require(filepath);
} else if (yield* isAsync()) {
module = yield* wait(import(filepath));
} else {
throw new Error("Unsupported ESM!");
}
// ...
}
const wait = gensync({
sync: x => x,
async: x => x,
});
@NicoloRibaudo 52
gensync: abstracting the ???
@NicoloRibaudo
.mjs config files and plugins :)
53
@NicoloRibaudo
Chapter 3:
Making Babel
synchronous again
54
@NicoloRibaudo
Making Babel synchronous again
55
Sometimes you can force😇 asynchronous APIs on your
users — sometimes you can't.
@NicoloRibaudo
Making Babel synchronous again
56
Sometimes you can force😇 asynchronous APIs on your
users — sometimes you can't.
@babel/eslint-parser is an ESLint parser to support experimental syntax;
@babel/register hooks into Node.js' require() to compile files on-the-fly.
@NicoloRibaudo
Worker and Atomics to the rescue
57
@NicoloRibaudo
Worker and Atomics to the rescue
Main thread
function doSomethingSync() {
const signal = new Int32Array(new SharedArrayBuffer(4));
const { port1, port2 } = new MessageChannel();
// sleep
Atomics.wait(signal, 0, 0);
const { result } = receiveMessageOnPort(port2);
return result;
}
58
Worker thread
addListener("message", () => {
let result =
await doSomethingAsync();
port1.postMessage({ result });
Atomics.store(signal, 0, 1);
Atomics.notify(signal, 0);
});
signal, port1
{ payload: /* ... */ }
{ result: /* ... */ }
@NicoloRibaudo
Worker and Atomics to the rescue
59
More details:
https://giuseppegurgone.com/synchronizing-async-functions
@NicoloRibaudo
Worker and Atomics to the rescue
Main thread
function doSomethingSync() {
● Wait (sleep) until SAB's
contents change
● Read the received result
return result;
}
60
Worker thread
addListener("message", () => {
let result =
await doSomethingAsync();
● Change SAB's contents
● Wake up the main thread
});
SharedArrayBuffer[ 0x00 0x00 0x00 0x00 ]
{ payload: /* ... */ }
{ result: /* ... */ }
@NicoloRibaudo
Worker and Atomics to the rescue
Main thread
1. Create the worker
const { Worker, SHARE_ENV } = require("worker_threads");
const worker = new Worker("./path/to/worker.js", {
env: SHARE_ENV,
});
61
@NicoloRibaudo
Worker and Atomics to the rescue
Main thread, doSomethingSync
2. Delegate the task to the worker
const { MessageChannel } = require("worker_threads");
const signal = new Int32Array(new SharedArrayBuffer(4));
const { port1, port2 } = new MessageChannel();
worker.postMessage({ signal, port: port1, payload }, [port1]);
Atomics.wait(signal, 0, 0);
62
@NicoloRibaudo
Worker and Atomics to the rescue
Worker thread, "message" listener
3. Perform the task and send the result to the main thread
const result = await doSomethingAsync();
port.postMessage({ result });
port.close();
Atomics.store(signal, 0, 1);
Atomics.notify(signal, 0);
63
@NicoloRibaudo
Worker and Atomics to the rescue
Main thread, doSomethingSync
4. After waking up, read the result and return it
const { receiveMessageOnPort } = require("worker_threads");
... const { port1, port2 } = new MessageChannel(); ...
... Atomics.wait(signal, 0, 0); ...
const { result } = receiveMessageOnPort(port2);
return result;
64
@NicoloRibaudo
Production code? Error handling
65
In case of an error, we
must manually report it
to the main thread.
@NicoloRibaudo
Chapter 4:
When reality hits hard
— running Babel's tests as native ESM
66
@NicoloRibaudo
Running Babel's tests as native ESM
Problem #1
Problem #2
67
ESM compiled to CommonJS behaves differently
from ESM that runs natively in Node.js
Our test runner, Jest, didn't properly
support running ESM
@NicoloRibaudo
The __esModule convention
68
// src/main.js
import circ from "./math.js";
circ(2); // 12.56
// src/math.js
export const PI = 3.14;
export default function (r) {
return 2 * PI * r;
}
// dist/main.js
var _math = require("./math.js");
_math.default(2); // 12.56
// dist/math.js
const PI = 3.14; exports.PI = PI;
function _default(r) {
return 2 * PI * r;
}
exports.default = _default;
@NicoloRibaudo
The __esModule convention
69
// src/main.js
import circ from "../libs/math.cjs";
circ(2); // 12.56
// src/math.js
export const PI = 3.14;
export default function (r) {
return 2 * PI * r;
}
// dist/main.js
var _math = require("../libs/math.cjs");
_math(2); // 12.56
// dist/math.js
const PI = 3.14; exports.PI = PI;
function _default(r) {
return 2 * PI * r;
}
exports.default = _default;
// libs/math.cjs
module.exports = function (r) {
return 2 * PI * r;
};
No more .default!
@NicoloRibaudo
The __esModule convention
70
// src/main.js
import circ from "../libs/math.cjs";
circ(2); // 12.56
// src/math.js
export const PI = 3.14;
export default function (r) {
return 2 * PI * r;
}
// dist/main.js
var _math = {
default: require("../libs/math.cjs"),
};
_math.default(2); // 12.56
// dist/math.js
const PI = 3.14; exports.PI = PI;
function _default(r) {
return 2 * PI * r;
}
exports.default = _default;
// libs/math.cjs
module.exports = function (r) {
return 2 * PI * r;
};
@NicoloRibaudo
The __esModule convention
71
// src/main.js
import circ from "./math.js";
circ(2); // 12.56
// dist/main.js
var _math = _interopRequireDefault(
require("./math.js")
);
_math.default(2); // 12.56
function _interopRequireDefault(obj) {
return obj is ESM compiled to CommonJS ? obj : { default: obj };
}
@NicoloRibaudo
The __esModule convention
72
// src/math.js
export const PI = 3.14;
export default function (r) {
return 2 * PI * r;
}
// dist/math.js
exports.__esModule = true;
const PI = 3.14; exports.PI = PI;
function _default(r) {
return 2 * PI * r;
}
exports.default = _default;
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
"This was ESM, compiled to CJS"
@NicoloRibaudo
The __esModule convention
73
@NicoloRibaudo
The __esModule convention
74
● Babel
● Traceur
● tsc (TypeScript)
● SWC
● …
● Webpack
● Rollup (@rollup/plugin-commonjs
)
● Parcel
● Vite
● ESBuild
● …
It's supported by every JavaScript transpiler and bundler:
@NicoloRibaudo
The __esModule convention
75
It's supported by every JavaScript transpiler and bundler.
It's not supported by Node.js:
// main.js
import circ from "./math.cjs";
console.log(circ);
// math.cjs
exports.__esModule = true;
const PI = 3.14; exports.PI = PI;
function _default(r) {
return 2 * PI * r;
}
exports.default = _default;
Always logs
{ __esModule: true,
PI: 3.14,
default: [function] }
@NicoloRibaudo
Converting some files to ESM
76
// math.js
export default function (r) {
return 2 * PI * r;
}
// test.js
import circ from "./math.js";
assert(circ(2) === 12.56);
// math.js
exports.__esModule = true;
exports.default = function (r) {
return 2 * PI * r; };
// test.js
var _math = _interopRequireDefault(
require("./math.js") );
assert(_math.default(2) === 12.56);
✅
✅
@NicoloRibaudo
// test.js
import circ from "./math.js";
assert(circ(2) === 12.56);
Converting some files to ESM
77
// math.js
exports.__esModule = true;
exports.default = function (r) {
return 2 * PI * r; }; // test.js
var _math = _interopRequireDefault(
require("./math.js") );
assert(_math.default(2) === 12.56);
❌
✅
@NicoloRibaudo
// test.js
import circ from "./math.js";
assert(circ.default(2) === 12.56);
Converting some files to ESM
78
// math.js
exports.__esModule = true;
exports.default = function (r) {
return 2 * PI * r; }; // test.js
var _math = _interopRequireDefault(
require("./math.js") );
assert(_math.default.default(2) === 12.56);
✅
❌
@NicoloRibaudo
// test.js
import circ from "./math.js";
assert(circ.default(2) === 12.56);
Converting some files to ESM
79
// math.js
exports.__esModule = true;
exports.default = function (r) {
return 2 * PI * r; }; // test.js
var _math = {
default: require("./math.js") };
assert(_math.default.default(2) === 12.56);
✅
✅
@NicoloRibaudo
importInterop: "node"
"This import should be compiled to match Node.js' behavior,
without checking the __esModule flag."
80
@NicoloRibaudo
importInterop: "node"
"This import should be compiled to match Node.js' behavior,
without checking the __esModule flag."
81
// test/index.js
// Standard __esModule interop
import helper from "./helper.js";
// importInterop: "node"
import dep from "dep";
["@babel/transform-modules-commonjs", {
importInterop(source) {
if (source.startsWith(".")) {
return "babel";
}
return "node";
}
}]
babel-core
|- test
| |- index.js
| - helper.js
- node_modules
- dep
- index.js
@NicoloRibaudo
importInterop: "node"
82
@NicoloRibaudo
Running Babel's tests as native ESM
Problem #1
Problem #2
83
ESM compiled to CommonJS behaves differently
from ESM that runs natively in Node.js
Our test runner, Jest, didn't properly
support running ESM
Solved ✓
@NicoloRibaudo
Jest support for native ESM
84
@NicoloRibaudo
Jest support for native ESM
Jest runs every test in a virtualized context, using Node.js' vm module, to:
● isolate every test file, so that failing or misbehaving tests don't affect
other tests
● abstract and control the linking process between modules, to intercept all
requires/imports and:
○ allow mocking dependencies
○ transpile modules on-the-fly
85
@NicoloRibaudo
Jest support for native ESM
Node.js support for ESM in virtual vm contexts is still… rough.
86
@NicoloRibaudo
jest-light-runner
Jest supports implementing custom test runners,
to define how to execute a test.
87
https://github.com/nicolo-ribaudo/jest-light-runner
@NicoloRibaudo
jest-light-runner
88
Blazing fast!
@NicoloRibaudo
Running Babel's tests as native ESM
Problem #1
Problem #2
89
ESM compiled to CommonJS behaves differently
from ESM that runs natively in Node.js
Our test runner, Jest, didn't properly
support running ESM
Solved ✓
Solved ✓
@NicoloRibaudo
Chapter 5:
Getting ready for an
ESM release
90
@NicoloRibaudo
Dual packages
91
Node.js supports packages with multiple implementations, that are
conditionally required/imported.
// package.json
{
"name": "your-package",
"exports": {
"import": "./esm-dist/index.mjs",
"require": "./cjs-dist/index.js"
}
}
@NicoloRibaudo
Dual packages
Converting to a "dual package" while preserving compatibility with all the
Node.js versions and various tools is incredibly complex.
92
@NicoloRibaudo
Without breaking changes
Dual packages
93
@NicoloRibaudo
Dual packages
Very high risk of breaking changes: the complete migration
is deferred to the next major release (Babel 8)
94
@NicoloRibaudo
Dual packages development
● During development, wether it's ESM or ESM-compiled-to-CJS should just
be a compilation detail
● Our codebase should always be valid in both modes
make use-cjs make use-esm
● We test both ESM and ESM-compiled-to-CJS on CI
● We are always ready to publish an ESM release, by simply flipping a flag
95
@NicoloRibaudo
Maximizing backwards compatibility
"ESM cannot be synchronously imported from CommonJS in Node.js"
~ me, many slides ago
96
const babel = require("@babel/core");
const fs = require("fs/promises");
babel.transformAsync(inputCode)
.then(({ code }) =>
fs.writeFile("./src/output.js", code)
);
const { types: t } = require("@babel/core");
module.exports = function myPlugin() {
return {
visitor: {
NumericLiteral(path) {
path.replaceWith(
t.stringLiteral("foo"),
);
} } };
};
How do we preserve compatibility with existing CommonJS Babel usages?
@NicoloRibaudo
Babel consumers
1. require() Babel
2. Call one of the Babel API entry points,
such as transformSync,
transformAsync, parseAsync, etc.
97
Babel plugins
1. Babel is loaded by someone else
2. Babel loads the plugin
3. The plugin require()s Babel and uses
its utilities
const {
types: t,
template,
} = require("@babel/core");
Maximizing backwards compatibility
@NicoloRibaudo
Instead of duplicating the implementation in CommonJS and ESM files,
CommonJS can act as a "proxy" over the ESM implementation.
There must still be an asynchronous step somewhere, but for libraries that
already offered an async API this should be good enough.
98
CommonJS proxies
@NicoloRibaudo
Babel consumers
1. require() Babel
2. Call one of the Babel API entry points,
such as transformSync,
transformAsync, parseAsync, etc.
99
CommonJS proxies
// @babel/core/index.mjs
export async function transformAsync() {
/* ... */
}
export function transformSync() {
/* ... */
}
// @babel/core/index.cjs
let babel;
exports.transformAsync = async function () {
babel ??= await import("@babel/core");
return babel.transformAsync();
};
exports.transformSync = function () {
if (!babel) throw new Error("Not loaded yet");
return babel.transformSync();
};
@NicoloRibaudo
Babel plugins
1. Babel is loaded by someone else
2. Babel loads the plugin
3. The plugin require()s Babel and uses
its utilities
const {
types: t,
template,
} = require("@babel/core");
100
CommonJS proxies
// @babel/core/index.mjs
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const cjsProxy = require("./index.cjs");
import * as thisFile from "./index.mjs";
cjsProxy.__initialize(thisFile);
// @babel/core/index.cjs
let babel;
exports.transformAsync = function () { /*..*/ };
exports.__initialize = function (b) {
babel = b;
exports.types = b.types;
exports.template = b.template;
/* ... */
};
@NicoloRibaudo
The end
101
@NicoloRibaudo
One more thing!
102
@NicoloRibaudo 103
@nicolo-ribaudo @liuxingbaoyu
@JLHwung
Babel's development is entirely funded by donations.
If you rely on Babel at work, talk to your company to get them to
sponsor the project!
One more thing!
https://opencollective.com/babel
Need help talking to your company? team@babeljs.io

More Related Content

Similar to Migrating Babel from CommonJS to ESM

Construire une application JavaFX 8 avec gradle
Construire une application JavaFX 8 avec gradleConstruire une application JavaFX 8 avec gradle
Construire une application JavaFX 8 avec gradleThierry Wasylczenko
 
How Does Kubernetes Build OpenAPI Specifications?
How Does Kubernetes Build OpenAPI Specifications?How Does Kubernetes Build OpenAPI Specifications?
How Does Kubernetes Build OpenAPI Specifications?reallavalamp
 
What's New in ES6 for Web Devs
What's New in ES6 for Web DevsWhat's New in ES6 for Web Devs
What's New in ES6 for Web DevsRami Sayar
 
Coroutines for Kotlin Multiplatform in Practise
Coroutines for Kotlin Multiplatform in PractiseCoroutines for Kotlin Multiplatform in Practise
Coroutines for Kotlin Multiplatform in PractiseChristian Melchior
 
Firefox OS learnings & visions, WebAPIs - budapest.mobile
Firefox OS learnings & visions, WebAPIs - budapest.mobileFirefox OS learnings & visions, WebAPIs - budapest.mobile
Firefox OS learnings & visions, WebAPIs - budapest.mobileRobert Nyman
 
Blocks & GCD
Blocks & GCDBlocks & GCD
Blocks & GCDrsebbe
 
ES6 in Production [JSConfUY2015]
ES6 in Production [JSConfUY2015]ES6 in Production [JSConfUY2015]
ES6 in Production [JSConfUY2015]Guillermo Paz
 
05 pig user defined functions (udfs)
05 pig user defined functions (udfs)05 pig user defined functions (udfs)
05 pig user defined functions (udfs)Subhas Kumar Ghosh
 
Greach 2019 - Creating Micronaut Configurations
Greach 2019 - Creating Micronaut ConfigurationsGreach 2019 - Creating Micronaut Configurations
Greach 2019 - Creating Micronaut ConfigurationsIván López Martín
 
HelsinkiJS meet-up. Dmitry Soshnikov - ECMAScript 6
HelsinkiJS meet-up. Dmitry Soshnikov - ECMAScript 6HelsinkiJS meet-up. Dmitry Soshnikov - ECMAScript 6
HelsinkiJS meet-up. Dmitry Soshnikov - ECMAScript 6Dmitry Soshnikov
 
Serializing EMF models with Xtext
Serializing EMF models with XtextSerializing EMF models with Xtext
Serializing EMF models with Xtextmeysholdt
 
¿Cómo de sexy puede hacer Backbone mi código?
¿Cómo de sexy puede hacer Backbone mi código?¿Cómo de sexy puede hacer Backbone mi código?
¿Cómo de sexy puede hacer Backbone mi código?jaespinmora
 
JavaScript - Agora nervoso
JavaScript - Agora nervosoJavaScript - Agora nervoso
JavaScript - Agora nervosoLuis Vendrame
 

Similar to Migrating Babel from CommonJS to ESM (20)

JavaScript Core
JavaScript CoreJavaScript Core
JavaScript Core
 
Construire une application JavaFX 8 avec gradle
Construire une application JavaFX 8 avec gradleConstruire une application JavaFX 8 avec gradle
Construire une application JavaFX 8 avec gradle
 
How Does Kubernetes Build OpenAPI Specifications?
How Does Kubernetes Build OpenAPI Specifications?How Does Kubernetes Build OpenAPI Specifications?
How Does Kubernetes Build OpenAPI Specifications?
 
Sprockets
SprocketsSprockets
Sprockets
 
What's New in ES6 for Web Devs
What's New in ES6 for Web DevsWhat's New in ES6 for Web Devs
What's New in ES6 for Web Devs
 
The Beauty of Java Script
The Beauty of Java ScriptThe Beauty of Java Script
The Beauty of Java Script
 
Coroutines for Kotlin Multiplatform in Practise
Coroutines for Kotlin Multiplatform in PractiseCoroutines for Kotlin Multiplatform in Practise
Coroutines for Kotlin Multiplatform in Practise
 
Firefox OS learnings & visions, WebAPIs - budapest.mobile
Firefox OS learnings & visions, WebAPIs - budapest.mobileFirefox OS learnings & visions, WebAPIs - budapest.mobile
Firefox OS learnings & visions, WebAPIs - budapest.mobile
 
Blocks & GCD
Blocks & GCDBlocks & GCD
Blocks & GCD
 
Node intro
Node introNode intro
Node intro
 
ES6 in Production [JSConfUY2015]
ES6 in Production [JSConfUY2015]ES6 in Production [JSConfUY2015]
ES6 in Production [JSConfUY2015]
 
05 pig user defined functions (udfs)
05 pig user defined functions (udfs)05 pig user defined functions (udfs)
05 pig user defined functions (udfs)
 
Txjs
TxjsTxjs
Txjs
 
Day 1
Day 1Day 1
Day 1
 
Greach 2019 - Creating Micronaut Configurations
Greach 2019 - Creating Micronaut ConfigurationsGreach 2019 - Creating Micronaut Configurations
Greach 2019 - Creating Micronaut Configurations
 
Future of NodeJS
Future of NodeJSFuture of NodeJS
Future of NodeJS
 
HelsinkiJS meet-up. Dmitry Soshnikov - ECMAScript 6
HelsinkiJS meet-up. Dmitry Soshnikov - ECMAScript 6HelsinkiJS meet-up. Dmitry Soshnikov - ECMAScript 6
HelsinkiJS meet-up. Dmitry Soshnikov - ECMAScript 6
 
Serializing EMF models with Xtext
Serializing EMF models with XtextSerializing EMF models with Xtext
Serializing EMF models with Xtext
 
¿Cómo de sexy puede hacer Backbone mi código?
¿Cómo de sexy puede hacer Backbone mi código?¿Cómo de sexy puede hacer Backbone mi código?
¿Cómo de sexy puede hacer Backbone mi código?
 
JavaScript - Agora nervoso
JavaScript - Agora nervosoJavaScript - Agora nervoso
JavaScript - Agora nervoso
 

More from Igalia

Running JS via WASM faster with JIT
Running JS via WASM      faster with JITRunning JS via WASM      faster with JIT
Running JS via WASM faster with JITIgalia
 
To crash or not to crash: if you do, at least recover fast!
To crash or not to crash: if you do, at least recover fast!To crash or not to crash: if you do, at least recover fast!
To crash or not to crash: if you do, at least recover fast!Igalia
 
Implementing a Vulkan Video Encoder From Mesa to GStreamer
Implementing a Vulkan Video Encoder From Mesa to GStreamerImplementing a Vulkan Video Encoder From Mesa to GStreamer
Implementing a Vulkan Video Encoder From Mesa to GStreamerIgalia
 
8 Years of Open Drivers, including the State of Vulkan in Mesa
8 Years of Open Drivers, including the State of Vulkan in Mesa8 Years of Open Drivers, including the State of Vulkan in Mesa
8 Years of Open Drivers, including the State of Vulkan in MesaIgalia
 
Introducción a Mesa. Caso específico dos dispositivos Raspberry Pi por Igalia
Introducción a Mesa. Caso específico dos dispositivos Raspberry Pi por IgaliaIntroducción a Mesa. Caso específico dos dispositivos Raspberry Pi por Igalia
Introducción a Mesa. Caso específico dos dispositivos Raspberry Pi por IgaliaIgalia
 
2023 in Chimera Linux
2023 in Chimera                    Linux2023 in Chimera                    Linux
2023 in Chimera LinuxIgalia
 
Building a Linux distro with LLVM
Building a Linux distro        with LLVMBuilding a Linux distro        with LLVM
Building a Linux distro with LLVMIgalia
 
turnip: Update on Open Source Vulkan Driver for Adreno GPUs
turnip: Update on Open Source Vulkan Driver for Adreno GPUsturnip: Update on Open Source Vulkan Driver for Adreno GPUs
turnip: Update on Open Source Vulkan Driver for Adreno GPUsIgalia
 
Graphics stack updates for Raspberry Pi devices
Graphics stack updates for Raspberry Pi devicesGraphics stack updates for Raspberry Pi devices
Graphics stack updates for Raspberry Pi devicesIgalia
 
Delegated Compositing - Utilizing Wayland Protocols for Chromium on ChromeOS
Delegated Compositing - Utilizing Wayland Protocols for Chromium on ChromeOSDelegated Compositing - Utilizing Wayland Protocols for Chromium on ChromeOS
Delegated Compositing - Utilizing Wayland Protocols for Chromium on ChromeOSIgalia
 
MessageFormat: The future of i18n on the web
MessageFormat: The future of i18n on the webMessageFormat: The future of i18n on the web
MessageFormat: The future of i18n on the webIgalia
 
Replacing the geometry pipeline with mesh shaders
Replacing the geometry pipeline with mesh shadersReplacing the geometry pipeline with mesh shaders
Replacing the geometry pipeline with mesh shadersIgalia
 
I'm not an AMD expert, but...
I'm not an AMD expert, but...I'm not an AMD expert, but...
I'm not an AMD expert, but...Igalia
 
Status of Vulkan on Raspberry
Status of Vulkan on RaspberryStatus of Vulkan on Raspberry
Status of Vulkan on RaspberryIgalia
 
Enable hardware acceleration for GL applications without glamor on Xorg modes...
Enable hardware acceleration for GL applications without glamor on Xorg modes...Enable hardware acceleration for GL applications without glamor on Xorg modes...
Enable hardware acceleration for GL applications without glamor on Xorg modes...Igalia
 
Async page flip in DRM atomic API
Async page flip in DRM  atomic APIAsync page flip in DRM  atomic API
Async page flip in DRM atomic APIIgalia
 
From the proposal to ECMAScript – Step by Step
From the proposal to ECMAScript – Step by StepFrom the proposal to ECMAScript – Step by Step
From the proposal to ECMAScript – Step by StepIgalia
 
The rainbow treasure map: Advanced color management on Linux with AMD/Steam D...
The rainbow treasure map: Advanced color management on Linux with AMD/Steam D...The rainbow treasure map: Advanced color management on Linux with AMD/Steam D...
The rainbow treasure map: Advanced color management on Linux with AMD/Steam D...Igalia
 
Freedreno on Android – XDC 2023
Freedreno on Android          – XDC 2023Freedreno on Android          – XDC 2023
Freedreno on Android – XDC 2023Igalia
 
On-going challenges in the Raspberry Pi driver stack – XDC 2023
On-going challenges in the Raspberry Pi driver stack – XDC 2023On-going challenges in the Raspberry Pi driver stack – XDC 2023
On-going challenges in the Raspberry Pi driver stack – XDC 2023Igalia
 

More from Igalia (20)

Running JS via WASM faster with JIT
Running JS via WASM      faster with JITRunning JS via WASM      faster with JIT
Running JS via WASM faster with JIT
 
To crash or not to crash: if you do, at least recover fast!
To crash or not to crash: if you do, at least recover fast!To crash or not to crash: if you do, at least recover fast!
To crash or not to crash: if you do, at least recover fast!
 
Implementing a Vulkan Video Encoder From Mesa to GStreamer
Implementing a Vulkan Video Encoder From Mesa to GStreamerImplementing a Vulkan Video Encoder From Mesa to GStreamer
Implementing a Vulkan Video Encoder From Mesa to GStreamer
 
8 Years of Open Drivers, including the State of Vulkan in Mesa
8 Years of Open Drivers, including the State of Vulkan in Mesa8 Years of Open Drivers, including the State of Vulkan in Mesa
8 Years of Open Drivers, including the State of Vulkan in Mesa
 
Introducción a Mesa. Caso específico dos dispositivos Raspberry Pi por Igalia
Introducción a Mesa. Caso específico dos dispositivos Raspberry Pi por IgaliaIntroducción a Mesa. Caso específico dos dispositivos Raspberry Pi por Igalia
Introducción a Mesa. Caso específico dos dispositivos Raspberry Pi por Igalia
 
2023 in Chimera Linux
2023 in Chimera                    Linux2023 in Chimera                    Linux
2023 in Chimera Linux
 
Building a Linux distro with LLVM
Building a Linux distro        with LLVMBuilding a Linux distro        with LLVM
Building a Linux distro with LLVM
 
turnip: Update on Open Source Vulkan Driver for Adreno GPUs
turnip: Update on Open Source Vulkan Driver for Adreno GPUsturnip: Update on Open Source Vulkan Driver for Adreno GPUs
turnip: Update on Open Source Vulkan Driver for Adreno GPUs
 
Graphics stack updates for Raspberry Pi devices
Graphics stack updates for Raspberry Pi devicesGraphics stack updates for Raspberry Pi devices
Graphics stack updates for Raspberry Pi devices
 
Delegated Compositing - Utilizing Wayland Protocols for Chromium on ChromeOS
Delegated Compositing - Utilizing Wayland Protocols for Chromium on ChromeOSDelegated Compositing - Utilizing Wayland Protocols for Chromium on ChromeOS
Delegated Compositing - Utilizing Wayland Protocols for Chromium on ChromeOS
 
MessageFormat: The future of i18n on the web
MessageFormat: The future of i18n on the webMessageFormat: The future of i18n on the web
MessageFormat: The future of i18n on the web
 
Replacing the geometry pipeline with mesh shaders
Replacing the geometry pipeline with mesh shadersReplacing the geometry pipeline with mesh shaders
Replacing the geometry pipeline with mesh shaders
 
I'm not an AMD expert, but...
I'm not an AMD expert, but...I'm not an AMD expert, but...
I'm not an AMD expert, but...
 
Status of Vulkan on Raspberry
Status of Vulkan on RaspberryStatus of Vulkan on Raspberry
Status of Vulkan on Raspberry
 
Enable hardware acceleration for GL applications without glamor on Xorg modes...
Enable hardware acceleration for GL applications without glamor on Xorg modes...Enable hardware acceleration for GL applications without glamor on Xorg modes...
Enable hardware acceleration for GL applications without glamor on Xorg modes...
 
Async page flip in DRM atomic API
Async page flip in DRM  atomic APIAsync page flip in DRM  atomic API
Async page flip in DRM atomic API
 
From the proposal to ECMAScript – Step by Step
From the proposal to ECMAScript – Step by StepFrom the proposal to ECMAScript – Step by Step
From the proposal to ECMAScript – Step by Step
 
The rainbow treasure map: Advanced color management on Linux with AMD/Steam D...
The rainbow treasure map: Advanced color management on Linux with AMD/Steam D...The rainbow treasure map: Advanced color management on Linux with AMD/Steam D...
The rainbow treasure map: Advanced color management on Linux with AMD/Steam D...
 
Freedreno on Android – XDC 2023
Freedreno on Android          – XDC 2023Freedreno on Android          – XDC 2023
Freedreno on Android – XDC 2023
 
On-going challenges in the Raspberry Pi driver stack – XDC 2023
On-going challenges in the Raspberry Pi driver stack – XDC 2023On-going challenges in the Raspberry Pi driver stack – XDC 2023
On-going challenges in the Raspberry Pi driver stack – XDC 2023
 

Recently uploaded

The Future of Software Development - Devin AI Innovative Approach.pdf
The Future of Software Development - Devin AI Innovative Approach.pdfThe Future of Software Development - Devin AI Innovative Approach.pdf
The Future of Software Development - Devin AI Innovative Approach.pdfSeasiaInfotech2
 
Artificial intelligence in cctv survelliance.pptx
Artificial intelligence in cctv survelliance.pptxArtificial intelligence in cctv survelliance.pptx
Artificial intelligence in cctv survelliance.pptxhariprasad279825
 
New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024BookNet Canada
 
AI as an Interface for Commercial Buildings
AI as an Interface for Commercial BuildingsAI as an Interface for Commercial Buildings
AI as an Interface for Commercial BuildingsMemoori
 
WordPress Websites for Engineers: Elevate Your Brand
WordPress Websites for Engineers: Elevate Your BrandWordPress Websites for Engineers: Elevate Your Brand
WordPress Websites for Engineers: Elevate Your Brandgvaughan
 
Scanning the Internet for External Cloud Exposures via SSL Certs
Scanning the Internet for External Cloud Exposures via SSL CertsScanning the Internet for External Cloud Exposures via SSL Certs
Scanning the Internet for External Cloud Exposures via SSL CertsRizwan Syed
 
SIP trunking in Janus @ Kamailio World 2024
SIP trunking in Janus @ Kamailio World 2024SIP trunking in Janus @ Kamailio World 2024
SIP trunking in Janus @ Kamailio World 2024Lorenzo Miniero
 
Powerpoint exploring the locations used in television show Time Clash
Powerpoint exploring the locations used in television show Time ClashPowerpoint exploring the locations used in television show Time Clash
Powerpoint exploring the locations used in television show Time Clashcharlottematthew16
 
Gen AI in Business - Global Trends Report 2024.pdf
Gen AI in Business - Global Trends Report 2024.pdfGen AI in Business - Global Trends Report 2024.pdf
Gen AI in Business - Global Trends Report 2024.pdfAddepto
 
Install Stable Diffusion in windows machine
Install Stable Diffusion in windows machineInstall Stable Diffusion in windows machine
Install Stable Diffusion in windows machinePadma Pradeep
 
Connect Wave/ connectwave Pitch Deck Presentation
Connect Wave/ connectwave Pitch Deck PresentationConnect Wave/ connectwave Pitch Deck Presentation
Connect Wave/ connectwave Pitch Deck PresentationSlibray Presentation
 
"Subclassing and Composition – A Pythonic Tour of Trade-Offs", Hynek Schlawack
"Subclassing and Composition – A Pythonic Tour of Trade-Offs", Hynek Schlawack"Subclassing and Composition – A Pythonic Tour of Trade-Offs", Hynek Schlawack
"Subclassing and Composition – A Pythonic Tour of Trade-Offs", Hynek SchlawackFwdays
 
Story boards and shot lists for my a level piece
Story boards and shot lists for my a level pieceStory boards and shot lists for my a level piece
Story boards and shot lists for my a level piececharlottematthew16
 
Kotlin Multiplatform & Compose Multiplatform - Starter kit for pragmatics
Kotlin Multiplatform & Compose Multiplatform - Starter kit for pragmaticsKotlin Multiplatform & Compose Multiplatform - Starter kit for pragmatics
Kotlin Multiplatform & Compose Multiplatform - Starter kit for pragmaticscarlostorres15106
 
Developer Data Modeling Mistakes: From Postgres to NoSQL
Developer Data Modeling Mistakes: From Postgres to NoSQLDeveloper Data Modeling Mistakes: From Postgres to NoSQL
Developer Data Modeling Mistakes: From Postgres to NoSQLScyllaDB
 
Dev Dives: Streamline document processing with UiPath Studio Web
Dev Dives: Streamline document processing with UiPath Studio WebDev Dives: Streamline document processing with UiPath Studio Web
Dev Dives: Streamline document processing with UiPath Studio WebUiPathCommunity
 
Vector Databases 101 - An introduction to the world of Vector Databases
Vector Databases 101 - An introduction to the world of Vector DatabasesVector Databases 101 - An introduction to the world of Vector Databases
Vector Databases 101 - An introduction to the world of Vector DatabasesZilliz
 
Transcript: New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
Transcript: New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024Transcript: New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
Transcript: New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024BookNet Canada
 
My Hashitalk Indonesia April 2024 Presentation
My Hashitalk Indonesia April 2024 PresentationMy Hashitalk Indonesia April 2024 Presentation
My Hashitalk Indonesia April 2024 PresentationRidwan Fadjar
 
Human Factors of XR: Using Human Factors to Design XR Systems
Human Factors of XR: Using Human Factors to Design XR SystemsHuman Factors of XR: Using Human Factors to Design XR Systems
Human Factors of XR: Using Human Factors to Design XR SystemsMark Billinghurst
 

Recently uploaded (20)

The Future of Software Development - Devin AI Innovative Approach.pdf
The Future of Software Development - Devin AI Innovative Approach.pdfThe Future of Software Development - Devin AI Innovative Approach.pdf
The Future of Software Development - Devin AI Innovative Approach.pdf
 
Artificial intelligence in cctv survelliance.pptx
Artificial intelligence in cctv survelliance.pptxArtificial intelligence in cctv survelliance.pptx
Artificial intelligence in cctv survelliance.pptx
 
New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
 
AI as an Interface for Commercial Buildings
AI as an Interface for Commercial BuildingsAI as an Interface for Commercial Buildings
AI as an Interface for Commercial Buildings
 
WordPress Websites for Engineers: Elevate Your Brand
WordPress Websites for Engineers: Elevate Your BrandWordPress Websites for Engineers: Elevate Your Brand
WordPress Websites for Engineers: Elevate Your Brand
 
Scanning the Internet for External Cloud Exposures via SSL Certs
Scanning the Internet for External Cloud Exposures via SSL CertsScanning the Internet for External Cloud Exposures via SSL Certs
Scanning the Internet for External Cloud Exposures via SSL Certs
 
SIP trunking in Janus @ Kamailio World 2024
SIP trunking in Janus @ Kamailio World 2024SIP trunking in Janus @ Kamailio World 2024
SIP trunking in Janus @ Kamailio World 2024
 
Powerpoint exploring the locations used in television show Time Clash
Powerpoint exploring the locations used in television show Time ClashPowerpoint exploring the locations used in television show Time Clash
Powerpoint exploring the locations used in television show Time Clash
 
Gen AI in Business - Global Trends Report 2024.pdf
Gen AI in Business - Global Trends Report 2024.pdfGen AI in Business - Global Trends Report 2024.pdf
Gen AI in Business - Global Trends Report 2024.pdf
 
Install Stable Diffusion in windows machine
Install Stable Diffusion in windows machineInstall Stable Diffusion in windows machine
Install Stable Diffusion in windows machine
 
Connect Wave/ connectwave Pitch Deck Presentation
Connect Wave/ connectwave Pitch Deck PresentationConnect Wave/ connectwave Pitch Deck Presentation
Connect Wave/ connectwave Pitch Deck Presentation
 
"Subclassing and Composition – A Pythonic Tour of Trade-Offs", Hynek Schlawack
"Subclassing and Composition – A Pythonic Tour of Trade-Offs", Hynek Schlawack"Subclassing and Composition – A Pythonic Tour of Trade-Offs", Hynek Schlawack
"Subclassing and Composition – A Pythonic Tour of Trade-Offs", Hynek Schlawack
 
Story boards and shot lists for my a level piece
Story boards and shot lists for my a level pieceStory boards and shot lists for my a level piece
Story boards and shot lists for my a level piece
 
Kotlin Multiplatform & Compose Multiplatform - Starter kit for pragmatics
Kotlin Multiplatform & Compose Multiplatform - Starter kit for pragmaticsKotlin Multiplatform & Compose Multiplatform - Starter kit for pragmatics
Kotlin Multiplatform & Compose Multiplatform - Starter kit for pragmatics
 
Developer Data Modeling Mistakes: From Postgres to NoSQL
Developer Data Modeling Mistakes: From Postgres to NoSQLDeveloper Data Modeling Mistakes: From Postgres to NoSQL
Developer Data Modeling Mistakes: From Postgres to NoSQL
 
Dev Dives: Streamline document processing with UiPath Studio Web
Dev Dives: Streamline document processing with UiPath Studio WebDev Dives: Streamline document processing with UiPath Studio Web
Dev Dives: Streamline document processing with UiPath Studio Web
 
Vector Databases 101 - An introduction to the world of Vector Databases
Vector Databases 101 - An introduction to the world of Vector DatabasesVector Databases 101 - An introduction to the world of Vector Databases
Vector Databases 101 - An introduction to the world of Vector Databases
 
Transcript: New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
Transcript: New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024Transcript: New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
Transcript: New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
 
My Hashitalk Indonesia April 2024 Presentation
My Hashitalk Indonesia April 2024 PresentationMy Hashitalk Indonesia April 2024 Presentation
My Hashitalk Indonesia April 2024 Presentation
 
Human Factors of XR: Using Human Factors to Design XR Systems
Human Factors of XR: Using Human Factors to Design XR SystemsHuman Factors of XR: Using Human Factors to Design XR Systems
Human Factors of XR: Using Human Factors to Design XR Systems
 

Migrating Babel from CommonJS to ESM

  • 1. @NicoloRibaudo Migrating Babel from CommonJS to ESM NICOLÒ RIBAUDO @nicolo-ribaudo @NicoloRibaudo hello@nicr.dev
  • 4. @NicoloRibaudo … there was a little JavaScript library, written using the shiny new import / export syntax and published to npm compiled to CommonJS. Once upon a time … 4 import { parse } from "babylon"; export function transform(code) { let ast = parse(code); return es6to5(ast); } "use strict"; exports.__esModule = true; exports.transform = transform; var _babylon = require("babylon"); function transform(code) { let ast = _babylon.parse(code); return es6to5(ast); }
  • 11. @NicoloRibaudo What is Babel? 11 Babel is a build-time compiler JavaScript
  • 12. @NicoloRibaudo What is Babel? 12 Babel is a build-time compiler JavaScript
  • 13. @NicoloRibaudo What is Babel? 13 Babel is a build-time devtool
  • 14. @NicoloRibaudo What is Babel? 14 Babel is a build-time devtool configurable
  • 15. @NicoloRibaudo 15 // babel.config.js module.exports = function () { return { targets: [ "last 3 versions", "not ie" ], ignore: ["**/*.test.js"] }; }; $ npx babel src --out-dir lib "Generate code that is compatible with these browsers" "Don't compile test files"
  • 16. @NicoloRibaudo What is Babel? 16 Babel is a build-time devtool pluggable
  • 17. @NicoloRibaudo 17 // babel-plugin-hello.js const { types: t } = require("@babel/core"); module.exports = () => ({ visitor: { StringLiteral(path) { path.replaceWith( t.stringLiteral("Hello World!") ); }, }, }); $ npx babel src --out-dir lib Apply some transformation to the parsed code, potentially relying on Babel-provided AST utilities
  • 18. @NicoloRibaudo What is Babel? 18 Babel is a build-time devtool n embeddable
  • 19. @NicoloRibaudo 19 // webpack.config.js module.exports = { entry: "./src/main.js", output: "./out/bundle.js", module: { rules: [ { test: /.js|.ts|.jsx/, use: "babel-loader", }, ], }, }; // rollup.config.js import { babel } from "@rollup/plugin-babel"; export default { input: "src/index.js", output: { dir: "output", format: "es", }, plugins: [ babel() ], }; ● Webpack ● Rollup ● Parcel ● Vite ● ESLint ● Prettier ● …
  • 20. @NicoloRibaudo What is Babel? 20 Babel is a build-time library JavaScript
  • 21. @NicoloRibaudo 21 const babel = require("@babel/core"); const fs = require("fs/promises"); babel .transformFileAsync("./main.js") .then(({ code }) => fs.writeFile("./main.out.js", code) ).catch(err => console.error("Cannot compile!", err) ); const parser = require("@babl/parser"); const generator = require("@babl/generator"); const ast = parser.parse("const a = 1;"); ast.program.body[0].declarations[0] .id.name = "b"; const code = generator.default(ast); console.log(code); // "const b = 1;"
  • 23. @NicoloRibaudo ESM challenges 23 #1 — It cannot be synchronously imported from CommonJS in Node.js // SyntaxError in CommonJS import "./module.mjs"; // Error [ERR_REQUIRE_ESM]: require() of ES Module not supported require("./module.mjs"); // This works, but it's asynchronous import("./module.mjs"); // Promise
  • 24. @NicoloRibaudo ESM challenges 24 #2 — It cannot be synchronously imported dynamically or lazily // CommonJS function loadIt() { require("./module.cjs"); } // ES Modules - SyntaxError // ES Modules - async function loadIt() { async function loadIt() { import "./module.cjs"; await import("./module.cjs"); } }
  • 25. @NicoloRibaudo ESM challenges 25 #3 — ESM-compiled-to-CJS has a different interface from native ESM import obj from "./esm-compiled-to-cjs.js"; console.log(obj); // { __esModule: true, default: [Function: A], namedExport: "foo" }
  • 26. @NicoloRibaudo ESM challenges 26 #4 — It doesn't integrate with tools that virtualize require and CJS loading ● mocking ● on-the-fly transpilation ● other bad things 🧙
  • 28. @NicoloRibaudo Internal vs External 28 Babel's source ● Written by Babel contributors ● Used by all Babel users Babel's tests ● Written by Babel contributors ● Used by Babel contributors Configuration files ● Written by (almost) all Babel users Plugins ● Written by a limited number of developers
  • 32. @NicoloRibaudo The async/await virus 32 function transform(code, opts) { const config = loadFullConfig(opts); // ... function loadFullConfig(inputOpts) { const result = loadPrivatePartialConfig(inputOpts); // ... function loadPrivatePartialConfig(inputOpts) { // ... const configChain = buildRootChain(args, context); // ... function buildRootChain(opts, context) { // ... configFile = loadOneConfig(ROOT_FILENAMES, dirname); // ... function loadOneConfig(filenames, dirname) { // ... const config = readConfigCode(filepath); // ... function readConfigCode(filepath) { // ... options = loadCodeDefault(filepath); // ... function loadCodeDefault(filepath) { const module = require(filepath); // ...
  • 33. @NicoloRibaudo The async/await virus 33 function transform(code, opts) { const config = loadFullConfig(opts); // ... function loadFullConfig(inputOpts) { const result = loadPrivatePartialConfig(inputOpts); // ... function loadPrivatePartialConfig(inputOpts) { // ... const configChain = buildRootChain(args, context); // ... function buildRootChain(opts, context) { // ... configFile = loadOneConfig(ROOT_FILENAMES, dirname); // ... function loadOneConfig(filenames, dirname) { // ... const config = readConfigCode(filepath); // ... function readConfigCode(filepath) { // ... options = loadCodeDefault(filepath); // ... function loadCodeDefault(filepath) { const module = require(filepath); const module = await import(filepath); // ...
  • 34. @NicoloRibaudo The async/await virus 34 function transform(code, opts) { const config = loadFullConfig(opts); // ... function loadFullConfig(inputOpts) { const result = loadPrivatePartialConfig(inputOpts); // ... function loadPrivatePartialConfig(inputOpts) { // ... const configChain = buildRootChain(args, context); // ... function buildRootChain(opts, context) { // ... configFile = loadOneConfig(ROOT_FILENAMES, dirname); // ... function loadOneConfig(filenames, dirname) { // ... const config = readConfigCode(filepath); // ... function readConfigCode(filepath) { // ... options = loadCodeDefault(filepath); // ... async function loadCodeDefault(filepath) { const module = require(filepath); const module = await import(filepath); // ...
  • 35. @NicoloRibaudo The async/await virus 35 function transform(code, opts) { const config = loadFullConfig(opts); // ... function loadFullConfig(inputOpts) { const result = loadPrivatePartialConfig(inputOpts); // ... function loadPrivatePartialConfig(inputOpts) { // ... const configChain = buildRootChain(args, context); // ... function buildRootChain(opts, context) { // ... configFile = loadOneConfig(ROOT_FILENAMES, dirname); // ... function loadOneConfig(filenames, dirname) { // ... const config = readConfigCode(filepath); // ... function readConfigCode(filepath) { // ... options = await loadCodeDefault(filepath); // ... async function loadCodeDefault(filepath) { const module = require(filepath); const module = await import(filepath); // ...
  • 36. @NicoloRibaudo The async/await virus 36 async function transform(code, opts) { const config = await loadFullConfig(opts); // ... async function loadFullConfig(inputOpts) { const result = await loadPrivatePartialConfig(inputOpts); // ... async function loadPrivatePartialConfig(inputOpts) { // ... const configChain = await buildRootChain(args, context); // ... async function buildRootChain(opts, context) { // ... configFile = await loadOneConfig(ROOT_FILENAMES, dirname); // ... async function loadOneConfig(filenames, dirname) { // ... const config = await readConfigCode(filepath); // ... async function readConfigCode(filepath) { // ... options = await loadCodeDefault(filepath); // ... async function loadCodeDefault(filepath) { const module = require(filepath); const module = await import(filepath); // ...
  • 37. @NicoloRibaudo Preserving Babel's sync API 37 ● Preserves backward compatibility ● The asynchronous API is only necessary when loading ESM files function transform(code, opts) async function transformAsync(code, opts)
  • 38. @NicoloRibaudo Preserving Babel's sync API 38 function transform(code, opts) function loadFullConfig(inputOpts) { function loadPrivatePartialConfig(inputOpts) { function buildRootChain(opts, context) { function loadOneConfig(filenames, dirname) { function readConfigCode(filepath) { function loadCodeDefault(filepath) { if (isCommonJS(filepath)) { module = require(filepath); } else { throw new Error("Unsupported ESM config!"); } async function transformAsync(code, opts) async function loadFullConfig(inputOpts) { async function loadPrivatePartialConfig(inputOpts) async function buildRootChain(opts, context) { async function loadOneConfig(filenames, dirname) { async function readConfigCode(filepath) { async function loadCodeDefault(filepath) { if (isCommonJS(filepath)) { module = require(filepath); } else { module = await import(filepath); } {
  • 39. @NicoloRibaudo Preserving Babel's sync API 39 Can we have a single implementation, capable of running both synchronously and asynchronously?
  • 40. @NicoloRibaudo Preserving Babel's sync API 40 Can we have a single implementation, capable of running both synchronously and asynchronously? Callbacks? function loadCodeDefault(filepath, callback) { if (isCommonJS(filepath)) { try { callback(null, require(filepath)); } catch (err) { callback(err); } } else { import(filepath).then( module => callback(null, module), err => callback(err) ); } }
  • 41. @NicoloRibaudo gensync: abstracting the 41 gensync: abstracting the ??? Me trying to prepare these slides Me spending too much time looking for a word
  • 42. @NicoloRibaudo gensync: abstracting the ??? 42 https://github.com/loganfsmyth/gensync
  • 43. @NicoloRibaudo gensync: abstracting the ??? 43 const readJSON = async function (filepath) { const contents = await readFile(filepath, "utf8"); return JSON.parse(contents); };
  • 44. @NicoloRibaudo gensync: abstracting the ??? 44 const readJSON = async function (filepath) { const contents = await readFile(filepath, "utf8"); return JSON.parse(contents); }; const readJSON = gensync(function* (filepath) { const contents = yield* readFile(filepath, "utf8"); return JSON.parse(contents); });
  • 45. @NicoloRibaudo gensync: abstracting the ??? 45 const readJSON = async function (filepath) { const contents = await readFile(filepath, "utf8"); return JSON.parse(contents); }; const readJSON = gensync(function* (filepath) { const contents = yield* readFile(filepath, "utf8"); return JSON.parse(contents); }); const json = readJSON.sync("package.json"); // or const json = await readJSON.async("package.json");
  • 46. @NicoloRibaudo gensync: abstracting the ??? 46 const readJSON = async function (filepath) { const contents = await readFile(filepath, "utf8"); return JSON.parse(contents); }; const readJSON = gensync(function* (filepath) { const contents = yield* readFile(filepath, "utf8"); return JSON.parse(contents); }); const json = readJSON.sync("package.json"); // or const json = await readJSON.async("package.json"); const readFile = gensync({ sync: fs.readFileSync, errback: fs.readFile, });
  • 47. @NicoloRibaudo gensync: abstracting the ??? 47 const readJSON = async function (filepath) { const contents = await readFile(filepath, "utf8"); return JSON.parse(contents); }; const readJSON = gensync(function* (filepath) { const contents = yield* readFile(filepath, "utf8"); return JSON.parse(contents); }); const json = readJSON.sync("package.json"); // or const json = await readJSON.async("package.json"); const readFile = gensync({ sync: fs.readFileSync, errback: fs.readFile, }); You can define any utility that branches on the execution model: const isAsync = gensync({ sync: () => false, async: async () => true, });
  • 48. @NicoloRibaudo 48 const transform = gensync(function* (code, opts) { const config = yield* loadFullConfig(opts); // ... function* loadFullConfig(inputOpts) { const result = yield* loadPrivatePartialConfig(inputOpts); // ... function* loadPrivatePartialConfig(opts) { // ... const configChain = yield* buildRootChain(args, context); // ... function* buildRootChain(opts, context) { // ... configFile = yield* loadOneConfig(ROOT_FILENAMES, dirname); // ... function* loadOneConfig(filenames, dirname) { // ... const config = yield* readConfigCode(filepath); // ... } function* readConfigCode(filepath) { // ... options = yield* loadCodeDefault(filepath); // ... } gensync: abstracting the ???
  • 49. @NicoloRibaudo 49 const transform = gensync(function* (code, opts) { const config = yield* loadFullConfig(opts); // ... export const transform = transform.errback; export const transformSync = transform.sync; export const transformAsync = transform.async; gensync: abstracting the ???
  • 50. @NicoloRibaudo 50 gensync: abstracting the ??? function loadCodeDefault(filepath) { if (isCommonJS(filepath)) { module = require(filepath); } else { throw new Error("Unsupported ESM!"); } async function loadCodeDefault(filepath) { if (isCommonJS(filepath)) { module = require(filepath); } else { module = await import(filepath); }
  • 51. @NicoloRibaudo 51 gensync: abstracting the ??? function loadCodeDefault(filepath) { if (isCommonJS(filepath)) { module = require(filepath); } else { throw new Error("Unsupported ESM!"); } async function loadCodeDefault(filepath) { if (isCommonJS(filepath)) { module = require(filepath); } else { module = await import(filepath); } function* loadCodeDefault(filepath) { if (isCommonJS(filepath)) { module = require(filepath); } else if (yield* isAsync()) { module = yield* wait(import(filepath)); } else { throw new Error("Unsupported ESM!"); } // ... } const wait = gensync({ sync: x => x, async: x => x, });
  • 55. @NicoloRibaudo Making Babel synchronous again 55 Sometimes you can force😇 asynchronous APIs on your users — sometimes you can't.
  • 56. @NicoloRibaudo Making Babel synchronous again 56 Sometimes you can force😇 asynchronous APIs on your users — sometimes you can't. @babel/eslint-parser is an ESLint parser to support experimental syntax; @babel/register hooks into Node.js' require() to compile files on-the-fly.
  • 58. @NicoloRibaudo Worker and Atomics to the rescue Main thread function doSomethingSync() { const signal = new Int32Array(new SharedArrayBuffer(4)); const { port1, port2 } = new MessageChannel(); // sleep Atomics.wait(signal, 0, 0); const { result } = receiveMessageOnPort(port2); return result; } 58 Worker thread addListener("message", () => { let result = await doSomethingAsync(); port1.postMessage({ result }); Atomics.store(signal, 0, 1); Atomics.notify(signal, 0); }); signal, port1 { payload: /* ... */ } { result: /* ... */ }
  • 59. @NicoloRibaudo Worker and Atomics to the rescue 59 More details: https://giuseppegurgone.com/synchronizing-async-functions
  • 60. @NicoloRibaudo Worker and Atomics to the rescue Main thread function doSomethingSync() { ● Wait (sleep) until SAB's contents change ● Read the received result return result; } 60 Worker thread addListener("message", () => { let result = await doSomethingAsync(); ● Change SAB's contents ● Wake up the main thread }); SharedArrayBuffer[ 0x00 0x00 0x00 0x00 ] { payload: /* ... */ } { result: /* ... */ }
  • 61. @NicoloRibaudo Worker and Atomics to the rescue Main thread 1. Create the worker const { Worker, SHARE_ENV } = require("worker_threads"); const worker = new Worker("./path/to/worker.js", { env: SHARE_ENV, }); 61
  • 62. @NicoloRibaudo Worker and Atomics to the rescue Main thread, doSomethingSync 2. Delegate the task to the worker const { MessageChannel } = require("worker_threads"); const signal = new Int32Array(new SharedArrayBuffer(4)); const { port1, port2 } = new MessageChannel(); worker.postMessage({ signal, port: port1, payload }, [port1]); Atomics.wait(signal, 0, 0); 62
  • 63. @NicoloRibaudo Worker and Atomics to the rescue Worker thread, "message" listener 3. Perform the task and send the result to the main thread const result = await doSomethingAsync(); port.postMessage({ result }); port.close(); Atomics.store(signal, 0, 1); Atomics.notify(signal, 0); 63
  • 64. @NicoloRibaudo Worker and Atomics to the rescue Main thread, doSomethingSync 4. After waking up, read the result and return it const { receiveMessageOnPort } = require("worker_threads"); ... const { port1, port2 } = new MessageChannel(); ... ... Atomics.wait(signal, 0, 0); ... const { result } = receiveMessageOnPort(port2); return result; 64
  • 65. @NicoloRibaudo Production code? Error handling 65 In case of an error, we must manually report it to the main thread.
  • 66. @NicoloRibaudo Chapter 4: When reality hits hard — running Babel's tests as native ESM 66
  • 67. @NicoloRibaudo Running Babel's tests as native ESM Problem #1 Problem #2 67 ESM compiled to CommonJS behaves differently from ESM that runs natively in Node.js Our test runner, Jest, didn't properly support running ESM
  • 68. @NicoloRibaudo The __esModule convention 68 // src/main.js import circ from "./math.js"; circ(2); // 12.56 // src/math.js export const PI = 3.14; export default function (r) { return 2 * PI * r; } // dist/main.js var _math = require("./math.js"); _math.default(2); // 12.56 // dist/math.js const PI = 3.14; exports.PI = PI; function _default(r) { return 2 * PI * r; } exports.default = _default;
  • 69. @NicoloRibaudo The __esModule convention 69 // src/main.js import circ from "../libs/math.cjs"; circ(2); // 12.56 // src/math.js export const PI = 3.14; export default function (r) { return 2 * PI * r; } // dist/main.js var _math = require("../libs/math.cjs"); _math(2); // 12.56 // dist/math.js const PI = 3.14; exports.PI = PI; function _default(r) { return 2 * PI * r; } exports.default = _default; // libs/math.cjs module.exports = function (r) { return 2 * PI * r; }; No more .default!
  • 70. @NicoloRibaudo The __esModule convention 70 // src/main.js import circ from "../libs/math.cjs"; circ(2); // 12.56 // src/math.js export const PI = 3.14; export default function (r) { return 2 * PI * r; } // dist/main.js var _math = { default: require("../libs/math.cjs"), }; _math.default(2); // 12.56 // dist/math.js const PI = 3.14; exports.PI = PI; function _default(r) { return 2 * PI * r; } exports.default = _default; // libs/math.cjs module.exports = function (r) { return 2 * PI * r; };
  • 71. @NicoloRibaudo The __esModule convention 71 // src/main.js import circ from "./math.js"; circ(2); // 12.56 // dist/main.js var _math = _interopRequireDefault( require("./math.js") ); _math.default(2); // 12.56 function _interopRequireDefault(obj) { return obj is ESM compiled to CommonJS ? obj : { default: obj }; }
  • 72. @NicoloRibaudo The __esModule convention 72 // src/math.js export const PI = 3.14; export default function (r) { return 2 * PI * r; } // dist/math.js exports.__esModule = true; const PI = 3.14; exports.PI = PI; function _default(r) { return 2 * PI * r; } exports.default = _default; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } "This was ESM, compiled to CJS"
  • 74. @NicoloRibaudo The __esModule convention 74 ● Babel ● Traceur ● tsc (TypeScript) ● SWC ● … ● Webpack ● Rollup (@rollup/plugin-commonjs ) ● Parcel ● Vite ● ESBuild ● … It's supported by every JavaScript transpiler and bundler:
  • 75. @NicoloRibaudo The __esModule convention 75 It's supported by every JavaScript transpiler and bundler. It's not supported by Node.js: // main.js import circ from "./math.cjs"; console.log(circ); // math.cjs exports.__esModule = true; const PI = 3.14; exports.PI = PI; function _default(r) { return 2 * PI * r; } exports.default = _default; Always logs { __esModule: true, PI: 3.14, default: [function] }
  • 76. @NicoloRibaudo Converting some files to ESM 76 // math.js export default function (r) { return 2 * PI * r; } // test.js import circ from "./math.js"; assert(circ(2) === 12.56); // math.js exports.__esModule = true; exports.default = function (r) { return 2 * PI * r; }; // test.js var _math = _interopRequireDefault( require("./math.js") ); assert(_math.default(2) === 12.56); ✅ ✅
  • 77. @NicoloRibaudo // test.js import circ from "./math.js"; assert(circ(2) === 12.56); Converting some files to ESM 77 // math.js exports.__esModule = true; exports.default = function (r) { return 2 * PI * r; }; // test.js var _math = _interopRequireDefault( require("./math.js") ); assert(_math.default(2) === 12.56); ❌ ✅
  • 78. @NicoloRibaudo // test.js import circ from "./math.js"; assert(circ.default(2) === 12.56); Converting some files to ESM 78 // math.js exports.__esModule = true; exports.default = function (r) { return 2 * PI * r; }; // test.js var _math = _interopRequireDefault( require("./math.js") ); assert(_math.default.default(2) === 12.56); ✅ ❌
  • 79. @NicoloRibaudo // test.js import circ from "./math.js"; assert(circ.default(2) === 12.56); Converting some files to ESM 79 // math.js exports.__esModule = true; exports.default = function (r) { return 2 * PI * r; }; // test.js var _math = { default: require("./math.js") }; assert(_math.default.default(2) === 12.56); ✅ ✅
  • 80. @NicoloRibaudo importInterop: "node" "This import should be compiled to match Node.js' behavior, without checking the __esModule flag." 80
  • 81. @NicoloRibaudo importInterop: "node" "This import should be compiled to match Node.js' behavior, without checking the __esModule flag." 81 // test/index.js // Standard __esModule interop import helper from "./helper.js"; // importInterop: "node" import dep from "dep"; ["@babel/transform-modules-commonjs", { importInterop(source) { if (source.startsWith(".")) { return "babel"; } return "node"; } }] babel-core |- test | |- index.js | - helper.js - node_modules - dep - index.js
  • 83. @NicoloRibaudo Running Babel's tests as native ESM Problem #1 Problem #2 83 ESM compiled to CommonJS behaves differently from ESM that runs natively in Node.js Our test runner, Jest, didn't properly support running ESM Solved ✓
  • 85. @NicoloRibaudo Jest support for native ESM Jest runs every test in a virtualized context, using Node.js' vm module, to: ● isolate every test file, so that failing or misbehaving tests don't affect other tests ● abstract and control the linking process between modules, to intercept all requires/imports and: ○ allow mocking dependencies ○ transpile modules on-the-fly 85
  • 86. @NicoloRibaudo Jest support for native ESM Node.js support for ESM in virtual vm contexts is still… rough. 86
  • 87. @NicoloRibaudo jest-light-runner Jest supports implementing custom test runners, to define how to execute a test. 87 https://github.com/nicolo-ribaudo/jest-light-runner
  • 89. @NicoloRibaudo Running Babel's tests as native ESM Problem #1 Problem #2 89 ESM compiled to CommonJS behaves differently from ESM that runs natively in Node.js Our test runner, Jest, didn't properly support running ESM Solved ✓ Solved ✓
  • 91. @NicoloRibaudo Dual packages 91 Node.js supports packages with multiple implementations, that are conditionally required/imported. // package.json { "name": "your-package", "exports": { "import": "./esm-dist/index.mjs", "require": "./cjs-dist/index.js" } }
  • 92. @NicoloRibaudo Dual packages Converting to a "dual package" while preserving compatibility with all the Node.js versions and various tools is incredibly complex. 92
  • 94. @NicoloRibaudo Dual packages Very high risk of breaking changes: the complete migration is deferred to the next major release (Babel 8) 94
  • 95. @NicoloRibaudo Dual packages development ● During development, wether it's ESM or ESM-compiled-to-CJS should just be a compilation detail ● Our codebase should always be valid in both modes make use-cjs make use-esm ● We test both ESM and ESM-compiled-to-CJS on CI ● We are always ready to publish an ESM release, by simply flipping a flag 95
  • 96. @NicoloRibaudo Maximizing backwards compatibility "ESM cannot be synchronously imported from CommonJS in Node.js" ~ me, many slides ago 96 const babel = require("@babel/core"); const fs = require("fs/promises"); babel.transformAsync(inputCode) .then(({ code }) => fs.writeFile("./src/output.js", code) ); const { types: t } = require("@babel/core"); module.exports = function myPlugin() { return { visitor: { NumericLiteral(path) { path.replaceWith( t.stringLiteral("foo"), ); } } }; }; How do we preserve compatibility with existing CommonJS Babel usages?
  • 97. @NicoloRibaudo Babel consumers 1. require() Babel 2. Call one of the Babel API entry points, such as transformSync, transformAsync, parseAsync, etc. 97 Babel plugins 1. Babel is loaded by someone else 2. Babel loads the plugin 3. The plugin require()s Babel and uses its utilities const { types: t, template, } = require("@babel/core"); Maximizing backwards compatibility
  • 98. @NicoloRibaudo Instead of duplicating the implementation in CommonJS and ESM files, CommonJS can act as a "proxy" over the ESM implementation. There must still be an asynchronous step somewhere, but for libraries that already offered an async API this should be good enough. 98 CommonJS proxies
  • 99. @NicoloRibaudo Babel consumers 1. require() Babel 2. Call one of the Babel API entry points, such as transformSync, transformAsync, parseAsync, etc. 99 CommonJS proxies // @babel/core/index.mjs export async function transformAsync() { /* ... */ } export function transformSync() { /* ... */ } // @babel/core/index.cjs let babel; exports.transformAsync = async function () { babel ??= await import("@babel/core"); return babel.transformAsync(); }; exports.transformSync = function () { if (!babel) throw new Error("Not loaded yet"); return babel.transformSync(); };
  • 100. @NicoloRibaudo Babel plugins 1. Babel is loaded by someone else 2. Babel loads the plugin 3. The plugin require()s Babel and uses its utilities const { types: t, template, } = require("@babel/core"); 100 CommonJS proxies // @babel/core/index.mjs import { createRequire } from "module"; const require = createRequire(import.meta.url); const cjsProxy = require("./index.cjs"); import * as thisFile from "./index.mjs"; cjsProxy.__initialize(thisFile); // @babel/core/index.cjs let babel; exports.transformAsync = function () { /*..*/ }; exports.__initialize = function (b) { babel = b; exports.types = b.types; exports.template = b.template; /* ... */ };
  • 103. @NicoloRibaudo 103 @nicolo-ribaudo @liuxingbaoyu @JLHwung Babel's development is entirely funded by donations. If you rely on Babel at work, talk to your company to get them to sponsor the project! One more thing! https://opencollective.com/babel Need help talking to your company? team@babeljs.io