ESBuild 编译 Commonjs 的行为 
现象 
模块只包含 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();模块只包含 ES6 模块 
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 };模块包含 ES6 模块和 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();考虑一下例子,通过 esbuild 将下例中的 commonjs 模块打包为 esm 模块。
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 产物如下:
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 要这么做? 
本质上的原因是 esbuild 将 commonjs 作为一等公民。esbuild 转译 commonjs 为 esm 的时候是采用与 nodejs 类似的 运行时加载方式,并没有对 commonjs 模块进行静态分析。换句话说 esbuild 并不知道 commonjs 模块具体导出了哪些引用,也就是无法采用具名导出的方式交付给 引用方(importer)。
例子
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 模块的 高度动态性。
const obj = {};
// ... do obj something in runtime
obj.a = 1;
obj.b = 2;
export { obj };
export default obj;至此已经介绍了针对运行时的属性在静态分析需要如何处理导出,即通过引用对象的方式导出(具名导出或默认导出)。由于 commonjs 自身并没有所谓的具名导出,因此 esbuild 会统一将 commonjs 模块的导出处理为 默认导出。这也就是我们所看到的 esbuild 在 commonjs 的转译产物(esm)中的最底部均会使用 export default 语句。
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转译commonjs为esm的时候是采用与Node类似的运行时加载方式。
可能熟悉 rollup 的处理 commonjs 模块的方案可以知道,rollup 把 esm 作为一等公民。也就是说遇到任何非 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-commonjs 在 strictRequires = true 的配置下,与 esbuild 在转译 commonjs 模块为 esm 模块的行为类似。
 XiSenao
 XiSenao