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();

考虑一下例子,通过 esbuild 将下例中的 commonjs 模块打包为 esm 模块。

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;

esbuild 打包后的 esm 产物如下:

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();

经多次测试可以发现不管 commonjs 是以何种方式导出 exports 对象,esbuild 打包 commonjs 模块为 esm 模块时,均以 default 做为导出方式。

为什么 esbuild 会这么做?

本质上的原因是 esbuildcommonjs 作为一等公民。esbuild 转译 commonjsesm 的时候是采用与 nodejs 类似的 运行时加载方式,并没有对 commonjs 模块进行静态分析。换句话说 esbuild 并不知道 commonjs 模块具体导出了哪些引用,也就是无法采用具名导出的方式交付给 引用方(importer)

例子

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;
}
for (const key in obj) {
  // bad case
  export key = obj[key];
}

你不可能在 非运行时 阶段确切了解到 obj 对象具体包含了哪些引用,那么就不可能通过 具名导出 的方式处理 obj 对象中的每一个属性,也就是说上述的 export key = obj[key] 语句是错误的,是运行时的方案,不符合 export 语句静态的特性。

ESM 中,export 语句必须是静态的。这意味着所有的导出(无论是 命名导出 还是 默认导出)都必须在模块 非运行时 阶段就可以明确知道哪些引用需要导出。像上述例子中的 obj 属性具有不确定性,但 obj 引用对象是确定的,因此可以像下述例子中一样通过导出具有确定性的引用(上述中即为 obj)对象。至于 obj 对象中的属性,那么在 运行时 阶段可以得知,这样既确保了 export 语句的 静态特性 同时又维护了 commonjs 模块的 高度动态性

js
const obj = {};

// ... do obj something in runtime
obj.a = 1;
obj.b = 2;

export { obj };

export default obj;

至此已经介绍了针对运行时的属性在静态分析需要如何处理导出,即通过引用对象的方式导出(具名导出或默认导出)。由于 commonjs 自身并没有所谓的具名导出,因此 esbuild 会统一将 commonjs 模块的导出处理为 默认导出。这也就是我们所看到的 esbuildcommonjs 的转译产物(esm)中的最底部均会使用 export default 语句。

js
var require_demo = __commonJS({
  'node_modules/demo/index.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;
    }
    module.exports = obj;
  }
});
export default require_demo();

默认导出的 引用 是确定的,属性是运行时加载确认的。解释完了 esbuild 为什么在转译 commonjs 模块为 esm 模块时,均使用 default 导出。那么上述提到的一个观点。

esbuild 转译 commonjsesm 的时候是采用与 Node 类似的 运行时加载 方式。

可能熟悉 rollup 的处理 commonjs 模块的方案可以知道,rollupesm 作为一等公民。也就是说遇到任何非 esm 模块时,rollup 都会要求提供插件来转译模块为 esm 模块。

针对 commonjs 模块的转译,rollup 会通过 @rollup/plugin-commonjs 插件来处理 commonjs 模块,将 commonjs 模块转译为 esm 模块。

那么问题来了:

@rollup/plugin-commonjs 转译 commonjs 模块的行为与 esbuild 转译 commonjs 模块的行为是否会有所不同呢?

default strict requires to true 文章中可以知道。@rollup/plugin-commonjs 在先前的版本中(strictRequires = false | auto)中会采用静态分析的方式尽可能将 commonjs 转译为具有静态性质的 esm 模块。但是由于 commonjs 模块具有高度动态的特性,将其转化为高度静态化的 esm 模块势必会存在一些边界问题。

文章 最后提出将 strictRequires = true 作为默认配置项。也就是说 @rollup/plugin-commonjs 会将所有的 commonjs 模块都包装在函数中,执行类似 nodejs 加载 commonjs 模块的方式,在运行时确认 commonjs 模块具体导出了哪些引用。

因此现版本的 @rollup/plugin-commonjsstrictRequires = true 的配置下,与 esbuild 在转译 commonjs 模块为 esm 模块的行为类似。

Contributors

Changelog

Discuss

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