I read the documentation of bun two days ago. I am having a hard time understanding this:
I googled it and found out many people also mentioned this, but no one explained it. Here’s some reference:
- How to dynamically load ESM in CJS
- CommonJS (cjs) and Modules (esm): Import compatibility
- Interoperability with CommonJS
From my point, the loading of ES modules are synchronous with the static import. For example:
// exporter.mjs
console.log("exporter");
// importer.mjs
import "exporter.mjs"
console.log("importer");
Using this command node importer.mjs
. Well, if the loading of exporter.js
is asynchronous, the output would be "importer" printing first and "exporter" following. While the result is "exporter" printing first and "importer" following.
enter image description here
This proved that the loading of ES modules are synchronous with the static import, in which case we could import ES modules in CommonJS modules. While the article I mentioned above all say "we can’t import ES modules in CommonJS modules, we have to use the dynamic import function" , so did the documentation of nodejs. However, we could import ES modules in CommonJS modules in Bun. This is an example:
// exporter.mjs
export let number = 10;
// importer.cjs
const { number } = require("./exporter.mjs");
console.log(number);
Using this command bun importer.cjs
. We can get the output like this.
enter image description here
I am really confused.
2
Answers
import * as foo from './foo'
being asynchronous means it’s a complete equivalent ofconst foo = await import('./foo')
which only makes a difference if ‘./foo’ has a top level await
As the doc says,
The idea that "CommonJS modules are synchronous and ES Modules are asynchronous" by itself might be a bit of an over-simplification which causes this confusion. Let’s add some context.
How do ES Modules work?
ES Modules go through the following steps:
Load: Asynchronously loads dependencies.
From the spec:
Link: Connects module exports with imports in memory.
From the spec:
Evaluate (and execute): Run the code.
From the spec:
As you can see from the quotes from the JavaScript/ECMAScript spec, various steps are performed asynchronously however steps depend on each other. In practice, this means dependencies will be evaluated and executed before the dependent, which gives the impression that ES modules are synchronous.
How do CommonJS modules work?
CommonJS is different because it uses a function (
require
) to import modules. This means, dependencies are only known at runtime (when the code is executed). In other words, code is executed first then dependencies are loaded; as opposed to ES modules which loads dependencies first then executes the code. The synchronous nature ofrequire
comes in because when arequire
function is encountered, code after the function is not run until after therequire
function has loaded and executed the code within the dependency.A practical comparison
This difference in the order of loading vs execution becomes apparent in a case like this:
In the CommonJS case,
a.js
requiresb.js
. Asrequire
is synchronous, it blocks the export ofa
. As a result,a
is undefined inb.js
.In the ES Module case, while
a.js
also requiresb.js
, the link between thea
export ina.js
and import inb.js
is set up before code is executed. As module evaluation and execution occurs asynchronously,a
is exported before thesetTimeout
‘s callback execution meaninga
will have its value initialised to"a"
. Why this happens is because Promises are scheduled in the microtask queue while timers are scheduled in the macrotask queue; and microtasks are executed before macrotasks. This itself is a whole topic that already has answers elsewhere on the internet. I’ll include something in the further reading below.Further reading