Skip to content

Esbuild Transpiles Commonjs Behavior

Phenomenon

The Module Only Contains CommonJS

js
const next = require('./a.cjs');
exports.main = next;
js
module.exports.a = 3;
exports.b = 2;
module.exports = {
  c: 4
};
js
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

js
import defaultValue, { a, b } from './a.mjs';
export { a, b };
export default defaultValue;
js
export const a = 3;
export const b = 2;
export default {
  c: 4
};
js
// 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

js
import defaultValue, { a, b } from './a.mjs';
exports.main = defaultValue.c + a + b;
js
export const a = 3;
export const b = 2;
export default {
  c: 4
};
js
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.

js
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:

Test Demo

js
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

java
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:

js
module.exports = {
  a: 1,
  b: 2
};

rollup will convert it to:

js
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 treats commonjs as a first-class citizen, using runtime loading methods similar to nodejs.
  • rollup prioritizes static analysis and treats esm as a first-class citizen, using plugins to convert commonjs to esm format.

These differences have important implications for how you structure your code and what build tools you choose for your project.

Contributors

Changelog

Discuss

Released under the CC BY-SA 4.0 License. (dbcbf17)