讨论和概述几乎所有当前的互操作性问题 + 如何继续前进的想法
commonjs 插件(以及扩展中的 rollup)尽可能与自身生成的输出和生态系统中的工具无缝配合。
有很多互操作性问题,请参见下面的详细信息。这是关于此插件实际运作方式的概述,以及对所有互操作情况和当前行为的更或完整描述。想法是将其用作讨论基础,从中努力解决其引起的子问题。我很乐意负责 rollup 核心的所有更改,但如果能得到支持改进插件就会非常高兴。这个列表非常长,我会尝试根据建议进行更新。我还将尽快添加一个编译好的总结在最后。由于我坚信测试胜过相信,所以几乎为每一项都添加了实际输出代码示例。
rollup 配置信息如下:
// "rollup.config.js"
import path from 'path';
import cjs from '@rollup/plugin-commonjs';
const inputFiles = Object.create(null);
const transformedFiles = Object.create(null);
const formatFiles = files =>
Object.keys(files)
.map(id => `// ${JSON.stringify(id)}\n${files[id].code}\n`)
.join('\n');
const formatId = id => {
const [, prefix, modulePath] = /(\0)?(.*)/.exec(id);
return `${prefix || ''}${path.relative('.', modulePath)}`;
};
export default {
input: 'main',
plugins: [
{
name: 'collect-input-files',
transform(code, id) {
if (id[0] !== '\0') {
inputFiles[formatId(id)] = { code: code.trim() };
}
}
},
cjs(),
{
name: 'collect-output',
transform(code, id) {
// Never display the helpers file
if (id !== '\0commonjsHelpers.js') {
transformedFiles[formatId(id)] = { code: code.trim() };
}
},
generateBundle(options, bundle) {
console.log(`<details>
<summary>Input</summary>
\`\`\`js
${formatFiles(inputFiles)}
\`\`\`
</details>
<details>
<summary>Transformed</summary>
\`\`\`js
${formatFiles(transformedFiles)}
\`\`\`
</details>
<details>
<summary>Output</summary>
\`\`\`js
${formatFiles(bundle)}
\`\`\`
</details>`);
}
}
],
output: {
format: 'es',
file: 'bundle.js'
}
};从 CJS 导入 CJS
插件需要确保这一切能够无缝运行,通过使用所加载模块的 module.exports 来解析 require 语句。这并不是一个互操作性问题,本节的目标更多是突出插件的实际工作原理以及它如何处理某些场景以及可以在哪些方面进行改进。
赋值给 module.exports
在 importer 文件(main.js)中,会转移为两种导入:
一个是对
importee(dep.js) 的直接导入(空导入)jsimport './dep.js';对
importee文件(dep.js)进行直接导入(空导入)的原因是为了触发模块的加载和转换,以便在构建代理文件时知道原始模块是 CJS 还是 ESM。一个是对
importee(dep.js?commonjs-proxy) 的代理导入jsimport dep from './dep.js?commonjs-proxy';在
importer文件(main.js)中的实际依赖的变量属性是从代理模块(dep.js?commonjs-proxy)中导入的。代理模块的作用本质上是对于转译后的dep.js模块的导出属性的重写。
原始模块(
dep.js)转译后包含如下两种输出方式:
分配给
module.exports的内容作为默认值js// "dep.js" var dep = 'foo'; export default dep;moduleExports 导出
js// "dep.js" var dep = 'foo'; export { dep as __moduleExports };
代理模块(
dep.js?commonjs-proxy)则会再次将moduleExports作为默认值导出(用于情况稍有不同的代理操作,请查看从 CJS 导入 ESM 的部分)。
// "\u0000dep.js?commonjs-proxy"
import { __moduleExports } from '/Users/lukastaegert/Github/rollup-playground/dep.js';
export default __moduleExports;例子:
Input:
// "main.js"
const dep = require('./dep.js');
console.log(dep);
// "dep.js"
module.exports = 'foo';Transformed:
// "main.js"
import "./dep.js";
import dep from "./dep.js?commonjs-proxy";
console.log(dep);
// "dep.js"
var dep = "foo";
export default dep;
export { dep as __moduleExports };
// "\u0000dep.js?commonjs-proxy"
import { __moduleExports } from "/Users/lukastaegert/Github/rollup-playground/dep.js";
export default __moduleExports;Output:
// "main.js"
var dep = 'foo';
console.log(dep);赋值给 exports 或 module.exports 的属性
在这种情况下,rollup 会手动创建一个 module.exports 对象,并将通过 内联方式 创建的所有属性赋值给 module.exports 对象。
// input "dep.js"
module.exports.foo = 'foo';
exports.bar = 'bar';
exports.default = 'baz';
// Transformed "dep.js"
var foo = 'foo';
var bar = 'bar';
var _default = 'baz';
var dep = {
foo: foo,
bar: bar,
default: _default
};与逐个为对象分配属性不同的是这种方式非常高效,因为运行时引擎可以立即优化此类对象以便快速访问。然后将 module.exports 对象作为 默认导出 和以 __moduleExports 为别名的 命名导出。另外,所有已分配的属性也作为命名导出进行导出。
// Transformed "dep.js"
var foo = 'foo';
var bar = 'bar';
var _default = 'baz';
var dep = {
foo: foo,
bar: bar,
default: _default
};
export default dep;
export { dep as __moduleExports };
export { foo };
export { bar };完整例子:
Input:
// "main.js"
const dep = require('./dep.js');
console.log(dep);
// "dep.js"
module.exports.foo = 'foo';
exports.bar = 'bar';
exports.default = 'baz';Transformed:
// "main.js"
import "./dep.js";
import dep from "./dep.js?commonjs-proxy";
console.log(dep);
// "dep.js"
var foo = "foo";
var bar = "bar";
var _default = "baz";
var dep = {
foo: foo,
bar: bar,
default: _default,
};
export default dep;
export { dep as __moduleExports };
export { foo };
export { bar };
// "\u0000dep.js?commonjs-proxy"
import { __moduleExports } from "/Users/lukastaegert/Github/rollup-playground/dep.js";
export default __moduleExports;Output:
// "bundle.js"
var foo = 'foo';
var bar = 'bar';
var _default = 'baz';
var dep = {
foo: foo,
bar: bar,
default: _default
};
console.log(dep);🐞 Bug 1:对同一属性进行两次赋值将生成两个同名导出,会导致 rollup 抛出 "重复导出" 错误。
处理不支持的 module.exports 或 exports 用法
有很多情况下插件会进行反优化,例如读取属性而不是赋值。在这种情况下,使用 createCommonjsModule 辅助函数来创建一个包装器,来或多或少的像 Node 那样执行模块,直接跑运行时而不需要检测 命名导出。
完整例子:
Input:
// "main.js"
const dep = require('./dep.js');
console.log(dep);
// "dep.js"
if (true) {
exports.foo = 'foo';
}Transformed:
// "main.js"
import "./dep.js";
import dep from "./dep.js?commonjs-proxy";
console.log(dep);
// "dep.js"
import * as commonjsHelpers from "commonjsHelpers.js";
var dep = commonjsHelpers.createCommonjsModule(function (module, exports) {
if (true) {
exports.foo = "foo";
}
});
export default dep;
export { dep as __moduleExports };
// "\u0000dep.js?commonjs-proxy"
import { __moduleExports } from "/Users/lukastaegert/Github/rollup-playground/dep.js";
export default __moduleExports;Output:
// "bundle.js"
function createCommonjsModule(fn, basedir, module) {
return (
(module = {
path: basedir,
exports: {},
require: function (path, base) {
return commonjsRequire(
path,
base === undefined || base === null ? module.path : base
);
}
}),
fn(module, module.exports),
module.exports
);
}
function commonjsRequire() {
throw new Error(
'Dynamic requires are not currently supported by @rollup/plugin-commonjs'
);
}
var dep = createCommonjsModule(function (module, exports) {
{
exports.foo = 'foo';
}
});
console.log(dep);内联 require 调用
目前,插件无法维护精确的执行顺序。由于 commonjs 语法的动态性质和 esm 语法的静态性质,同时 rollup 会以 esm 作为 第一语法,一切模块均为 esm 模块的原因。即使是嵌套和有条件使用 require 语句(除非它们以特定方式通过 if 语句编写)总是会被 提升到顶部。
// "main.js"
console.log('first');
require('./dep.js');
// transformed "main.js"
import './dep.js';
import './dep.js?commonjs-proxy';
console.log('first');那么就会导致由 commonjs 转译后的 esm 模块在语义上存在不一致问题。
这种情况可以通过一些重大改变来改善,比如类似 node 执行行为,将模块包装在函数封闭中,并在首次使用时调用它们,整个流程跑在运行时中。然而,这将对 生成代码的效率产生负面影响,因此只有在真正必要时才应该这样做。不幸的是,在整个模块图构建完成之前,无法判断模块是否以非标准方式被 require
非标准方式 require
动态
requirejsconst moduleName = getModuleName(); const mod = require(moduleName);条件
requirejsif (condition) { const mod = require('some-module'); }这种方式在运行时有效。
通过字符串拼接使用
requirejsconst mod = require('./module-' + 'name');这种方式类似于动态
require,它通过拼接字符串生成模块路径,而非直接的字符串常量。静态分析工具无法解析拼接的路径,也因此不能识别模块的依赖关系。非标准的
require参数jsconst mod = require(someNonStringValue);标准的
require语法要求传入的参数必须是一个字符串常量,用于指定模块路径。如果require的参数不是字符串常量,工具链可能无法正确解析模块。对
require的重写或自定义某些开发者可能会在代码中重写 require 函数或者使用某种方式自定义模块加载逻辑。这种情况也会干扰打包工具的正常模块解析和依赖分析,因为工具链默认认为 require 是全局且固定的函数。
所以在最坏的情况下,所有模块可能都需要被包装。
rollup 在这里稍微优化了一下,通过实现一些内联算法,但这很大程度上是未来需要讨论的(比如说,最多一年后),而且很可能只适用于模块恰好在一个地方使用的情况。其他方法可能包括插件分析实际执行顺序,看看是否可以确保第一次使用不需要包装,这样后面的动态 require 就无关紧要了,但这感觉很复杂且容易出错。无论如何,这里主要列出是为了完整性,因为它并不真正涉及互操作性的主题,但值得单独讨论。这里有一个示例来说明:
完整例子:
Input:
// "main.js"
console.log('first');
require('./dep.js');
console.log('third');
false && require('./broken-conditional.js');
// There is special logic to handle this exact case, which is why
// "working-conditional.js" is not in the module graph, but it is not easily
// generalized.
if (false) {
require('./working-conditional.js');
}
// "dep.js"
console.log('second');
// "broken-conditional.js"
console.log('not executed');Transformed:
// "main.js"
import "./dep.js";
import "./broken-conditional.js";
import "./dep.js?commonjs-proxy";
import require$$1 from "./broken-conditional.js?commonjs-proxy";
console.log("first");
console.log("third");
false && require$$1;
// There is special logic to handle this exact case, which is why
// "working-conditional.js" is not in the module graph, but it is not easily
// generalized.
if (false) {
require("./working-conditional.js");
}
// "dep.js"
console.log("second");
// "\u0000dep.js?commonjs-proxy"
import * as dep from "/Users/lukastaegert/Github/rollup-playground/dep.js";
export default dep;
// "broken-conditional.js"
console.log("not executed");
// "\u0000broken-conditional.js?commonjs-proxy"
import * as brokenConditional from "/Users/lukastaegert/Github/rollup-playground/broken-conditional.js";
export default brokenConditional;Output:
// "bundle.js"
console.log('second');
console.log('not executed');
console.log('first');
console.log('third');现在让我们来谈谈实际的互操作模式:
从 ESM 导入非 ESM 转译的 CJS 模块
在 NodeJS 中的执行情况
在 Node 中,CJS 模块中仅暴露默认导出,默认导出的值和 module.exports 的值相同,这种重要模式应该始终有效。
对于 commonjs 插件来说,与 CJS 导入 CJS 的主要区别是不需要使用代理模块而是直接导入实际模块(dep.js)。这里只举一个例子来说明,在一般情况下,所有操作都类似于 CJS 到 CJS 的情况。
完整例子:
Input:
// "main.js"
import foo from './dep.js';
console.log(foo);
// "dep.js"
module.exports = 'foo';Transformed:
// "main.js"
import foo from './dep.js';
console.log(foo);
// "dep.js"
var dep = 'foo';
export default dep;
export { dep as __moduleExports };Output:
// "bundle.js"
var dep = 'foo';
console.log(dep);在 NodeJS 中使用具名导入的执行情况
Webpack 是支持这种行为的,但(之前部分功能现在完全功能)也得到这个插件的支持。除了默认导入解析为 module.exports 外,命名导入将解析为 module.exports 的属性。以前,这只适用于插件可以自动检测的命名导出(并且仅当模块未被反优化以使用 createCommonjsModule 时),或由用户列出的命名导出。现在使用 rollup 的 syntheticNamedExports 属性可以启用任意命名导入的解析,同时保持动态绑定。
关于 Webpack 中的 .mjs 语义和更好的 NodeJS 互操作性的说明
请注意,当从扩展名为 .mjs 的模块使用时,Webpack 目前要么不允许要么警告这种模式。
🚧 TODO:最好确认一下这一点
这里的意图是这个扩展名(.mjs)表示我们想进入某种严格的 NodeJS 互操作模式。rollup 也可以做类似的事情,如果要实现的话我会乐意在 @rollup/plugin-node-resolve 插件中进行 ESM / CJS 检测,并建立一个通信渠道从这个插件中获取检测信息。之后可以在 @rollup/plugin-commonjs 中添加一个 开关 来启用某种严格的 NodeJS 互操作模式,这将:
- 不自动检测模块类型,而是使用
NodeJS语义(类似.mjs声明的扩展名 +package.json中的type = 'module'字段),这甚至可能带来轻微的速度提升。 - 不允许从
CJS文件中导入非默认导出,这可能成为我们解决当前更紧迫问题之后添加的高级功能。
静态检测到命名导出的示例
完整例子:
Input:
// "main.js"
import { foo } from './dep.js';
console.log(foo);
// "dep.js"
exports.foo = 'foo';Transformed:
// "main.js"
import { foo } from './dep.js';
console.log(foo);
// "dep.js"
var foo = 'foo';
var dep = {
foo: foo
};
export default dep;
export { dep as __moduleExports };
export { foo };Output:
// "bundle.js"
var foo = 'foo';
console.log(foo);依赖合成命名导出的示例
这里我们只是将一个对象赋值给 module.exports。但注意需要考量的点是当访问属性时如何确保动态绑定,也就是说若后续 module.exports 上的对象被修改,ESM 访问 CJS 的命名变量值将始终保持为当前值。
// "main.js"
import { foo } from './dep.js';
console.log(foo);
setTimeout(() => {
console.log(foo);
}, 3000);
// "dep.js"
module.exports = { foo: 'foo' };
setTimeout(() => {
// case1: importer named import `foo` has not been changed
module.exports = { foo: 'foo-changed1' };
// case2: importer named import `foo` has been changed
module.exports.foo = 'foo-changed2';
}, 1000);从 ESM 导入由 ESM 模块转译后的 CJS 模块
这是棘手的一个。问题在于原始 ESM 模块和转译后的 CJS 模块之间需要保持同构行为。当使用 "带命名导入的 NodeJS" 模式时,命名导出 大多处理正确(除了我们不会对缺失的导出抛出异常),但 默认导出 会存在分歧,不应该是使用 module.exports 作为 默认导出,而应该使用 module.exports.default 作为 默认导出。
目前,大多数工具(例如 babel, webpack, typescript 等) 通过向 module.exports 添加 __esModule 属性来实现运行时检测模式,若模块中使用了 __esModule 且值为真,则表示这是一个经过由 ESM 模块转译而来的 CJS 模块。然后获取 默认导入 时的算法如下:
- 如果存在
__esModule属性且值为真,则将module.exports.default的值作为 默认导出。 - 否则使用
module.exports的值作为 默认导出。
当存在默认导出时导入命名导出的示例
🐞 Bug 2: 它试图返回默认导出的属性而非命名导出。原因在于,在这种情况下,unwrapExports 中的互操作模式某种程度上正确地提取了默认导出并将其作为默认导出,但在 syntheticNamedExports 中不应该使用默认导出来继续提取命名导出。
完整例子:
Input:
// "main.js"
import { foo } from './dep.js';
console.log(foo);
// "dep.js"
Object.defineProperty(exports, '__esModule', { value: true });
exports.foo = 'foo';
exports.default = 'default';Transformed:
// "main.js"
import { foo } from './dep.js';
console.log(foo);
// "dep.js"
import * as commonjsHelpers from 'commonjsHelpers.js';
var dep = commonjsHelpers.createCommonjsModule(function (module, exports) {
Object.defineProperty(exports, '__esModule', { value: true });
exports.foo = 'foo';
exports.default = 'default';
});
export default /*@__PURE__*/ commonjsHelpers.unwrapExports(dep);
export { dep as __moduleExports };Output:
// "bundle.js"
function unwrapExports(x) {
return x &&
x.__esModule &&
Object.prototype.hasOwnProperty.call(x, 'default')
? x['default']
: x;
}
function createCommonjsModule(fn, basedir, module) {
return (
(module = {
path: basedir,
exports: {},
require: function (path, base) {
return commonjsRequire(
path,
base === undefined || base === null ? module.path : base
);
}
}),
fn(module, module.exports),
module.exports
);
}
function commonjsRequire() {
throw new Error(
'Dynamic requires are not currently supported by @rollup/plugin-commonjs'
);
}
var dep = createCommonjsModule(function (module, exports) {
Object.defineProperty(exports, '__esModule', { value: true });
exports.foo = 'foo';
exports.default = 'default';
});
var dep$1 = /*@__PURE__*/ unwrapExports(dep);
console.log(dep$1.foo);这很难修复,特别是如果我们想为命名导出保持动态绑定。我的第一个想法是扩展 syntheticNamedExports 以允许一个额外的值,表示默认导出也作为实际默认导出的默认属性被选取(可以看作以 Namespace 对象的形式)。然而,这意味着自动检测互操作会变得缓慢和困难,并且可能破坏非 ESM 情况下的所有动态绑定,因为我们需要构建一个新对象,即:
// { ...moduleExports, default: moduleExports?.__esModule ? moduleExports.default : moduleExports }
export default moduleExports && moduleExports.__esModule
? moduleExports
: Object.assign({}, moduleExports, { default: moduleExports });也就是说情况如下的转译情况:
// dep.js
// case1:
module.exports = {
foo: 'foo',
default: 'default foo',
__esModule: true
};
// tranform case1 ==>
export default {
foo: 'foo',
default: 'default foo',
__esModule: true
}
// case2:
module.exports = {
foo: 'foo',
default: 'default foo',
};
// tranform case2 ==>
export default {
foo: 'foo',
default: {
foo: 'foo',
default: 'default foo',
}
};导出 Namespace 对象,之后可以在 Namespace 对象中获取默认导出
import dep from './dep.js';
console.log(dep.default);📈 改进1:我有一个更好的想法是允许将任意字符串指定为 syntheticNamedExports 的值,例如 syntheticNamedExports: "__moduleExports"。其含义是缺失的命名(甚至默认)导出不是从默认导出中获取,而是从给定名称的命名导出中获取。然后互操作就只需要:
export { __moduleExports };
export default __moduleExports.__esModule
? __moduleExports.default
: __moduleExports;这是相当高效的,尽管需要优化代码的话,可以将逻辑放入一个名为 getDefault 的互操作函数中。不过,转译后的 ESM 情况还存在问题,默认导出并不是动态绑定的。但这个问题也是可以解决的,如果在第二步中我们实现对 __esModule 的静态检测:
📈 改进 2:如果我们在模块的顶层遇到 Object.defineProperty(exports, "__esModule", { value: true }) 这行代码(或者在最小化的情况下使用 !0 而不是 true),那么我们可以将此模块标记为被转译的,并且甚至可以在转换器中去掉这行代码,使得代码更高效并消除了任何互操作的需要,也就是说上述的
export default __moduleExports.__esModule
? __moduleExports.default
: __moduleExports;一块逻辑就不需要包括了,同时也不再需要将代码包装在 createCommonjsModule 中。
导入不存在的默认导出的示例
🐞 Bug 3:这种情况不能正确工作并不令人惊讶,因为它实际上应该在构建时或运行时抛出错误。否则,至少默认导出应该是undefined,而这里它实际上是命名空间。
完整例子:
Input:
// "main.js"
import foo from './dep.js';
console.log(foo);
// "dep.js"
Object.defineProperty(exports, '__esModule', { value: true });
exports.foo = 'foo';Transformed:
// "main.js"
import foo from './dep.js';
console.log(foo);
// "dep.js"
import * as commonjsHelpers from 'commonjsHelpers.js';
var dep = commonjsHelpers.createCommonjsModule(function (module, exports) {
Object.defineProperty(exports, '__esModule', { value: true });
exports.foo = 'foo';
});
export default /*@__PURE__*/ commonjsHelpers.unwrapExports(dep);
export { dep as __moduleExports };Output:
// "bundle.js"
function unwrapExports(x) {
return x &&
x.__esModule &&
Object.prototype.hasOwnProperty.call(x, 'default')
? x['default']
: x;
}
function createCommonjsModule(fn, basedir, module) {
return (
(module = {
path: basedir,
exports: {},
require: function (path, base) {
return commonjsRequire(
path,
base === undefined || base === null ? module.path : base
);
}
}),
fn(module, module.exports),
module.exports
);
}
function commonjsRequire() {
throw new Error(
'Dynamic requires are not currently supported by @rollup/plugin-commonjs'
);
}
var dep = createCommonjsModule(function (module, exports) {
Object.defineProperty(exports, '__esModule', { value: true });
exports.foo = 'foo';
});
var foo = /*@__PURE__*/ unwrapExports(dep);
console.log(foo);要修复这个问题,我想将互操作模式缩减为只检查 __esModule 的存在,而不检查其他任何内容。这将被建议的改进2所涵盖。
从 CJS 模块中导入 ESM 模块
nodejs:目前nodejs还不支持直接在CJS模块中requireESM模块,但未来可能会支持,届时require一个ESM模块可能会返回 命名空间。webpack和typescript:webpack支持requireES模块并返回命名空间。typescript在转译ESM模块时,会添加__esModule属性,并将默认导出作为一个属性,这意味着在使用CJS作为输出目标时,require一个ESM模块会得到 命名空间。babel的行为类似。rollup:rollup主要用于创建库,默认使用auto模式来生成CJS输出:如果有多个导出,module.exports包含命名空间。如果只有一个默认导出,则直接赋值给module.exports。
📈 改进3:默认情况下,require 时总是返回命名空间。
📈 改进4a:我们添加一个标志来切换所有模块或一些模块(通过 glob / include / exclude 模式或者可能类似于 rollup 中 external 选项的工作方式)以像上面概述的 auto 模式那样工作,以使现有的混合 ESM CJS 代码库能够工作。我认为我们需要这个的原因是 require 的模块本身可能是第三方依赖,因此不在你的直接控制之下。
📈 改进5:rollup 在使用 auto 模式时,如果没有明确指定,将显示警告,解释可能遇到的问题以及如何更改接口。
只有命名导出的 ESM 模块的 require
这种情况下按预期运行。
完整例子:
Input:
// "main.js"
const foo = require('./dep.js');
console.log(foo);
// "dep.js"
export const foo = 'foo';Transformed:
// "main.js"
import './dep.js';
import foo from './dep.js?commonjs-proxy';
console.log(foo);
// "dep.js"
export const foo = 'foo';
// "\u0000dep.js?commonjs-proxy"
import * as dep from '/Users/lukastaegert/Github/rollup-playground/dep.js';
export default dep;Output:
// "bundle.js"
const foo = 'foo';
var dep = /*#__PURE__*/ Object.freeze({
__proto__: null,
foo: foo
});
console.log(dep);只有默认导出的 ESM 模块的 require
这对于 auto 模式是正确工作的,但根据上面的论点,它可能应该被改变,参见 改进3 和 改进4。
完整例子:
Input:
// "main.js"
const foo = require('./dep.js');
console.log(foo);
// "dep.js"
export default 'default';Transformed:
// "main.js"
import "./dep.js";
import foo from "./dep.js?commonjs-proxy";
console.log(foo);
// "dep.js"
export default "default";
// "\u0000dep.js?commonjs-proxy"
export { default } from "/Users/lukastaegert/Github/rollup-playground/dep.js";Output:
// "bundle.js"
var foo = 'default';
console.log(foo);混合导出(命名导出和默认导出)的 ESM 模块的 require
🐞 Bug 4: 异常行为,正常情况应该需要返回一个命名空间对象,但实际上返回了默认导出。
完整例子:
Input:
// "main.js"
const foo = require('./dep.js');
console.log(foo);
// "dep.js"
export const foo = 'foo';
export default 'default';Transformed:
// "main.js"
import "./dep.js";
import foo from "./dep.js?commonjs-proxy";
console.log(foo);
// "dep.js"
export const foo = "foo";
export default "default";
// "\u0000dep.js?commonjs-proxy"
export { default } from "/Users/lukastaegert/Github/rollup-playground/dep.js";Output:
// "bundle.js"
var foo = 'default';
console.log(foo);外部模块导入
要查看完整的互操作执行流程,就必须同时查看 commonjs 插件生成的内容以及 rollup 生成的 CJS 输出。
commonjs 插件中的外部导入
🐞 Bug 5: 对于外部导入,commonjs 插件将始终 require 默认导入。
📈 改进 6: 根据上述论点,正确的是应该把命名空间(import * as external from 'external')做为 默认导入,因为这在技术上等同于 require 一个 ES 模块。
📈 改进 4b: 再次,我们应该添加一个选项来指定何时外部 require 应该只返回默认导出。它甚至可能是相同的选项。所以这里有一些讨论的空间。
完整例子:
Input:
// "main.js"
const foo = require('external');
console.log(foo);Transformed:
// "main.js"
import 'external';
import foo from 'external?commonjs-proxy';
console.log(foo);
// "\u0000external?commonjs-external"
import external from 'external';
export default external;Output:
// "bundle.js"
import external from 'external';
console.log(external);rollup 的 CJS 输出中的外部导入
导入一个 namespace 对象
理想情况下,这应该被转换为一个简单的 require 语句,因为
- 转译的
ESM模块在被require时将返回命名空间 - 这将意味着根据前一节中的论点,一个简单的
require将再次成为一个简单的require。
而这确实是这种情况:
完整例子:
Input:
// "main.js"
import * as foo from 'external';
console.log(foo);Output:
// "bundle.js"
'use strict';
var foo = require('external');
console.log(foo);导入命名绑定
这些应该只是转换为 require 返回的属性,因为那应该等同于一个命名空间。而且确实如此:
完整例子:
Input:
// "main.js"
import { foo, bar } from 'external';
console.log(foo, bar);Output:
// "bundle.js"
'use strict';
var external = require('external');
console.log(external.foo, external.bar);导入默认导出
🐞 Bug 6:这里有几个问题:
- 互操作函数只检查
default属性的存在,而不是检查__esModule属性。我一直以为有一个原因埋藏在某个旧问题中,但回顾历史,似乎它一直都是这样。这应该改变,因为它有各种不利影响:如果模块不是ESM模块但将某些内容分配给exports.default,它会被误认为是默认导出;如果它是ESM模块但没有默认导出,这将错误地返回命名空间而不是默认导出。 - 默认导入的动态绑定将不会被保留。如果有外部模块的循环依赖,这可能会导致问题。
完整例子:
Input:
// "main.js"
import foo from 'external';
console.log(foo);Output:
// "bundle.js"
'use strict';
function _interopDefault(ex) {
return ex && typeof ex === 'object' && 'default' in ex
? ex['default']
: ex;
}
var foo = _interopDefault(require('external'));
console.log(foo);📈 改进 7:为了解决这个问题,我认为我会尝试像 Babel 类似的做法。
入口模块
赋值给 module.exports 的入口模块
这将被转换为默认导出,这可能是我们所能期望的最好结果。
完整例子:
Input:
// "main.js"
module.exports = 'foo';Transformed:
// "main.js"
var main = 'foo';
export default main;Output:
// "bundle.js"
var main = 'foo';
export default main;赋值给 exports 的入口模块
这似乎工作得很好。
完整例子:
Input:
// "main.js"
module.exports.foo = 'foo';Transformed:
// "main.js"
var foo = 'foo';
var main = {
foo: foo
};
export default main;
export { foo };Output:
// "bundle.js"
var foo = 'foo';
var main = {
foo: foo
};
export default main;
export { foo };赋值给 module.exports 同时自我 require 的入口模块
🐞 Bug 7:基本上它寻找一个不存在的 __moduleExports 导出,而不是提供命名空间。我在 改进1 中关于如何重新设计 syntheticNamedExports 的建议也应该从 rollup 内部解决这个问题。当另一个模块导入项目的入口模块或者当模块只是想导出添加属性时,类似的问题也会出现。
完整例子:
Input:
// "main.js"
const x = require('./main.js');
console.log(x);
module.exports = 'foo';Transformed:
// "main.js"
import "./main.js";
import x from "./main.js?commonjs-proxy";
console.log(x);
var main = "foo";
export default main;
// "\u0000main.js?commonjs-proxy"
import { __moduleExports } from "/Users/lukastaegert/Github/rollup-playground/main.js";
export default __moduleExports;Output:
// "bundle.js"
console.log(main.__moduleExports);
var main = 'foo';
export default main;赋值给 exports.default 的入口模块
🐞 Bug 8:在这里没有生成任何输出,而应该默认导出命名空间,除非存在 __esModule 属性。
完整例子:
Input:
// "main.js"
module.exports.default = 'foo';Transformed:
// "main.js"
var _default = 'foo';
var main = {
default: _default
};Output:
// "bundle.js"入口模块是一个转译后的 ES 模块
理想情况下,输出应该与原始输入具有相同的导出。目前,由于 Object.defineProperty 调用总是会导致使用 createCommonjsModule 包装器,并且不会检测到命名导出。有几种方法可以改进这一点:
- 删除
__esModule属性定义,并不将其视为优化失败的原因,请参见 改进2 - 📈 改进 8:添加一个类似于现在已移除的
namedExports的新选项,专门列出条目中公开的导出。我们还可以使用此选项来激活或停用默认导出,并决定默认导出应该是分配给exports.default还是module.exports的内容。这可以通过简单地创建一个重新导出这些输出的包装文件来处理。如果我们以这种方式做,Rollup tree-shaking 可能甚至会删除一些未使用的导出代码。
完整例子:
Input:
// "main.js"
Object.defineProperty(exports, '__esModule', { value: true });
exports.default = 'foo';
exports.bar = 'bar';Transformed:
// "main.js"
import * as commonjsHelpers from 'commonjsHelpers.js';
var main = commonjsHelpers.createCommonjsModule(function (module, exports) {
Object.defineProperty(exports, '__esModule', { value: true });
exports.default = 'foo';
exports.bar = 'bar';
});
export default /*@__PURE__*/ commonjsHelpers.unwrapExports(main);Output:
// "bundle.js"
function unwrapExports(x) {
return x &&
x.__esModule &&
Object.prototype.hasOwnProperty.call(x, 'default')
? x['default']
: x;
}
function createCommonjsModule(fn, basedir, module) {
return (
(module = {
path: basedir,
exports: {},
require: function (path, base) {
return commonjsRequire(
path,
base === undefined || base === null ? module.path : base
);
}
}),
fn(module, module.exports),
module.exports
);
}
function commonjsRequire() {
throw new Error(
'Dynamic requires are not currently supported by @rollup/plugin-commonjs'
);
}
var main = createCommonjsModule(function (module, exports) {
Object.defineProperty(exports, '__esModule', { value: true });
exports.default = 'foo';
exports.bar = 'bar';
});
var main$1 = /*@__PURE__*/ unwrapExports(main);
export default main$1;