Esbuild Transpiles Commonjs Behavior
Phenomenon
The Module Only Contains CommonJS
const next = require('./a.cjs');
exports.main = next;
module.exports.a = 3;
exports.b = 2;
module.exports = {
c: 4
};
var __getOwnPropNames = Object.getOwnPropertyNames;
var __commonJS = (cb, mod) =>
function __require() {
return (
mod ||
(0, cb[__getOwnPropNames(cb)[0]])(
(mod = { exports: {} }).exports,
mod
),
mod.exports
);
};
// a.cjs
var require_a = __commonJS({
'a.cjs'(exports, module) {
module.exports.a = 3;
exports.b = 2;
module.exports = {
c: 4
};
}
});
// main.js
var require_main = __commonJS({
'main.js'(exports) {
var next = require_a();
exports.main = next;
}
});
export default require_main();
The Module Only Contains ES6 Module
import defaultValue, { a, b } from './a.mjs';
export { a, b };
export default defaultValue;
export const a = 3;
export const b = 2;
export default {
c: 4
};
// a.mjs
var a = 3;
var b = 2;
var a_default = {
c: 4
};
// main.mjs
var main_default = a_default;
export { a, b, main_default as default };
The Module Contains ES6 Module And CommonJS
import defaultValue, { a, b } from './a.mjs';
exports.main = defaultValue.c + a + b;
export const a = 3;
export const b = 2;
export default {
c: 4
};
var __getOwnPropNames = Object.getOwnPropertyNames;
var __esm = (fn, res) =>
function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])((fn = 0))), res;
};
var __commonJS = (cb, mod) =>
function __require() {
return (
mod ||
(0, cb[__getOwnPropNames(cb)[0]])(
(mod = { exports: {} }).exports,
mod
),
mod.exports
);
};
// a.mjs
var a, b, a_default;
var init_a = __esm({
'a.mjs'() {
a = 3;
b = 2;
a_default = {
c: 4
};
}
});
// main.js
var require_main = __commonJS({
'main.js'(exports) {
init_a();
exports.main = a_default.c + a + b;
}
});
export default require_main();
Consider the following example, where esbuild
bundles a commonjs
module into an esm
module.
const obj = {};
const random = Math.floor(Math.random() * 10) + 1;
for (let i = 0; i <= random; i++) {
obj[i] = Math.floor(Math.random() * 10) + 2;
}
exports.entryFlag = true;
module.exports = obj;
The esm
output after bundling with esbuild
is as follows:
var __getOwnPropNames = Object.getOwnPropertyNames;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
// entry.js
var require_entry = __commonJS({
"entry.js"(exports, module) {
var obj = {};
var random = Math.floor(Math.random() * 10) + 1;
for (let i = 0; i <= random; i++) {
obj[i] = Math.floor(Math.random() * 10) + 2;
}
exports.entryFlag = true;
module.exports = obj;
}
});
export default require_entry();
After multiple tests, it can be found that regardless of how the commonjs
module exports the exports
object, when esbuild
bundles a commonjs
module into an esm
module, it always uses default
as the export method.
Why does esbuild
do this?
The fundamental reason is that esbuild
treats commonjs
as a first-class citizen. When esbuild
transpiles commonjs
to esm
, it uses a runtime loading method similar to nodejs
, without performing static analysis on the commonjs
module. In other words, esbuild
does not know which references the commonjs
module specifically exports, so it cannot use named exports to deliver to the importer.
Example
const obj = {};
const random = Math.floor(Math.random() * 10) + 1;
for (let i = 0; i <= random; i++) {
obj[i] = Math.floor(Math.random() * 10) + 2;
}
exports.entryFlag = true;
module.exports = obj;
In this example, the commonjs
module's exports are dynamically generated at runtime. esbuild
cannot statically analyze what properties the obj
object will have, so it cannot use named exports to deliver to the importer.
Comparison with rollup
In contrast, rollup
treats esm
as a first-class citizen. When rollup
bundles commonjs
modules, it uses the @rollup/plugin-commonjs
plugin to statically analyze the commonjs
module and convert it to esm
format. This means that rollup
can use named exports to deliver to the importer.
For example, if a commonjs
module exports an object with known properties:
module.exports = {
a: 1,
b: 2
};
rollup
will convert it to:
export const a = 1;
export const b = 2;
This is because rollup
can statically analyze the commonjs
module and know exactly what properties are being exported.
Conclusion
The different approaches of esbuild
and rollup
reflect their different design philosophies:
esbuild
prioritizes build speed and treatscommonjs
as a first-class citizen, using runtime loading methods similar tonodejs
.rollup
prioritizes static analysis and treatsesm
as a first-class citizen, using plugins to convertcommonjs
toesm
format.
These differences have important implications for how you structure your code and what build tools you choose for your project.