Do ever wonder how ECMAScript evolves and it's available almost everywhere? We gonna do a walkthrough of how TC39 (the technical committee responsible for the ECMAScript) works, how to participate, contribute, and how an idea can make it through to the language, all step-by-step. Together with a sneak pick and debate about some recent and interesting proposals in different stages.
(c) Fest.js Porto 2023
April 20, 2023
Porto, Portugal
https://fest.dev/events/js/porto-2023/
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);
}
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
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);
}
{
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)
);
}
}
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.
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
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"
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
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 ✓
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