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();
考虑一下例子,通过 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
模块的行为类似。