Skip to content

讨论和概述(几乎所有)当前的互操作性问题 + 如何继续前进的想法

commonjs 插件(以及扩展中的 rollup)尽可能与自身生成的输出和生态系统中的工具无缝配合。

有很多互操作性问题,请参见下面的详细信息。这是关于此插件实际运作方式的概述,以及对所有互操作情况和当前行为的更或完整描述。想法是将其用作讨论基础,从中努力解决其引起的子问题。我很乐意负责 rollup 核心的所有更改,但如果能得到支持改进插件就会非常高兴。这个列表非常长,我会尝试根据建议进行更新。我还将尽快添加一个编译好的总结在最后。由于我坚信测试胜过相信,所以几乎为每一项都添加了实际输出代码示例。

rollup 配置信息如下:

js
// "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)中,会转移为两种导入:

  1. 一个是对 importee(dep.js) 的直接导入(空导入)

    js
    import './dep.js';

    importee 文件(dep.js)进行直接导入(空导入)的原因是为了触发模块的加载和转换,以便在构建代理文件时知道原始模块是 CJS 还是 ESM。

  2. 一个是对 importee(dep.js?commonjs-proxy) 的代理导入

    js
    import dep from './dep.js?commonjs-proxy';

    importer 文件(main.js)中的实际依赖的变量属性是从代理模块(dep.js?commonjs-proxy)中导入的。

原始模块(dep.js)转译后包含如下两种输出方式:

  1. 分配给 module.exports 的内容作为默认值

    js
    // "dep.js"
    var dep = 'foo';
    
    export default dep;
  2. moduleExports 导出

    js
    // "dep.js"
    var dep = 'foo';
    
    export { dep as __moduleExports };

代理模块(dep.js?commonjs-proxy)则会再次将 moduleExports 作为默认值导出(用于情况稍有不同的代理操作,请查看从 CJS 导入 ESM 的部分)。

js
// "\u0000dep.js?commonjs-proxy"
import { __moduleExports } from '/Users/lukastaegert/Github/rollup-playground/dep.js';
export default __moduleExports;

例子:

Input:

js
// "main.js"
const dep = require('./dep.js');
console.log(dep);

// "dep.js"
module.exports = 'foo';

Transformed:

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

js
// "main.js"
var dep = 'foo';

console.log(dep);

赋值给 exports 或 module.exports 的属性

在这种情况下,rollup 会手动创建一个 module.exports 对象,并将通过 内联方式 创建的所有属性赋值给 module.exports 对象。

js
// 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 为别名的 命名导出。另外,所有已分配的属性(除了 default 属性)也作为命名导出进行导出。

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

js
// "main.js"
const dep = require('./dep.js');
console.log(dep);

// "dep.js"
module.exports.foo = 'foo';
exports.bar = 'bar';
exports.default = 'baz';

Transformed:

js
// "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,
  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:

js
// "bundle.js"
var foo = 'foo';
var bar = 'bar';
var _default = 'baz';

var dep = {
  foo,
  bar,
  default: _default
};

console.log(dep);

🐞 Bug 1:对同一属性进行两次赋值将生成两个同名导出,会导致 rollup 抛出 "重复导出" 错误。

处理不支持的 module.exports 或 exports 用法

有很多情况下插件会进行反优化,例如读取属性而不是赋值。在这种情况下,使用 createCommonjsModule 辅助函数来创建一个包装器,来或多或少的像 Node 那样执行模块,跑运行时而不检测任何 命名导出

完整例子:

Input:

js
// "main.js"
const dep = require('./dep.js');
console.log(dep);

// "dep.js"
if (true) {
  exports.foo = 'foo';
}

Transformed:

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

js
// "bundle.js"
function createCommonjsModule(fn, basedir, module) {
  return (
    (module = {
      path: basedir,
      exports: {},
      require(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 调用

目前,插件无法维护精确的执行顺序。相反,即使是嵌套和有条件执行的 require 语句(除非它们以特定方式通过 if 语句编写)总是会被提升到顶部。这种情况可以通过一些重大改变来改善,比如将模块包装在函数封闭中,并在首次使用时调用它们。然而,这将对生成代码的效率产生负面影响,因此只有在真正必要时才应该这样做。不幸的是,在整个模块图构建完成之前,无法判断模块是否以非标准方式被 require,所以在最坏的情况下,所有模块可能都需要被包装。rollup 在这里稍微优化了一下,通过实现一些内联算法,但这很大程度上是未来需要讨论的(比如说,最多一年后),而且很可能只适用于模块恰好在一个地方使用的情况。其他方法可能包括插件分析实际执行顺序,看看是否可以确保第一次使用不需要包装,这样后面的动态 require 就无关紧要了,但这感觉很复杂且容易出错。无论如何,这里主要列出是为了完整性,因为它并不真正涉及互操作性的主题,但值得单独讨论。这里有一个示例来说明:

完整例子:

Input:

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

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

js
// "bundle.js"
console.log('second');

console.log('not executed');

console.log('first');

console.log('third');

现在让我们来谈谈实际的互操作模式:

从 ESM 导入 CJS

NodeJS 中的执行情况

Node 中,CJS 模块仅暴露默认导出,默认导出的值和 module.exports 的值相同,这种模式始终有效。

对于 commonjs 插件来说,主要区别是不使用代理而是直接导入实际模块(dep.js)。这里只举一个例子来说明,在一般情况下,所有操作都类似于 CJSCJS 的情况。

完整例子:

Input:

js
// "main.js"
import foo from './dep.js';
console.log(foo);

// "dep.js"
module.exports = 'foo';

Transformed:

js
// "main.js"
import foo from './dep.js';
console.log(foo);

// "dep.js"
var dep = 'foo';

export default dep;
export { dep as __moduleExports };

Output:

js
// "bundle.js"
var dep = 'foo';

console.log(dep);

使用具名导入的 NodeJS 中的执行情况

这是由 Webpack 支持的,但(之前部分地但现在完全)也由这个插件支持。除了默认导入解析为 module.exports 外,命名导入将解析为 module.exports 上的属性。以前,这只对插件可以自动检测到的命名导出起作用(仅当模块未被优化为使用 createCommonjsModule 时),或者用户列出的命名导出。rollup 当前形式中对 syntheticNamedExports 属性的使用现在使得任意命名导入能够无需额外配置而被解析,同时保持动态绑定。

关于 Webpack 中的 .mjs 语义和更好的 NodeJS 互操作性的说明:

Webpack 目前在使用 .mjs 扩展名的模块时,要么不允许这种模式,要么发出警告。

🚧 TODO: 确认一下会很好

这里的意图是指示我们想进入某种严格的 NodeJS 互操作模式。我们可以做类似的事情,但我希望在 @rollup/plugin-node-resolve 中有 ESM/CJS 检测,并建立一个通信渠道从该插件获取此信息。然后我们可能会向 @rollup/plugin-commonjs 添加一个开关来使用“严格的 NodeJS 互操作”。

  • 不会自动检测模块类型,而是使用 NodeJS 语义(extension + package.type),这甚至可能提供轻微速度提升
  • 不允许从 CJS 文件中导入非默认导出 这可能成为我们解决当前更紧迫问题之后添加的高级功能。

这可能是我们解决当前更紧迫问题后添加的高级功能。

使用静态检测到的具名导出示例

完整例子:

Input:

js
// "main.js"
import { foo } from './dep.js';
console.log(foo);

// "dep.js"
exports.foo = 'foo';

Transformed:

js
// "main.js"
import { foo } from './dep.js';
console.log(foo);

// "dep.js"
var foo = 'foo';

var dep = {
  foo
};

export default dep;
export { dep as __moduleExports };
export { foo };

Output:

js
// "bundle.js"
var foo = 'foo';

console.log(foo);

依赖合成命名导出的示例

在这里,我们只是将一个对象分配给 module.exports。请注意如何通过使用属性访问来保留动态绑定:如果稍后会改变 module.exports 中的对象,则访问我们命名的变量始终会提供当前值。

完整例子:

Input:

js
// "main.js"
import { foo } from './dep.js';
console.log(foo);

// "dep.js"
module.exports = { foo: 'foo' };

Transformed:

js
// "main.js"
import { foo } from './dep.js';
console.log(foo);

// "dep.js"
var dep = { foo: 'foo' };

export default dep;
export { dep as __moduleExports };

Output:

js
// "bundle.js"
var dep = { foo: 'foo' };

console.log(dep.foo);

由 ESM 模块转译为 CJS 模块

这是一个棘手的问题。在原始 ES 模块和 CJS 模块之间保持同构行为是个挑战。在 “带命名导入的 NodeJS” 模式中,大多数情况下可以正确处理命名导出(exports 缺失除外),但默认导出应该是 module.exports.default 而不是 module.exports

这与先前列出的互操作模式不兼容。目前,大多数 bundlers 通过向 module.exports 添加 __esModule 属性来实现运行时检测模式,若模块中使用 __esModule 且值为真,则表示这是一个经过由 ES 模块转译而来的 CJS 模块。然后获取默认导入时的算法如下:

  1. 如果存在此属性且值为真,则将 module.exports.default 的值作为默认导出。
  2. 否则使用 module.exports 的值作为默认导出。

在默认导出存在时导入命名导出的示例

🐞 Bug 2: 它试图返回 default export 的属性而非命名导出。原因在于,在这种情况下,unwrapExports 中的互操作模式正确地提取了 default export 并将其作为默认导出,但 syntheticNamedExports 中不应该使用此方法来提取命名导出。

完整例子:

Input:

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

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

js
// "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(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 以允许附加值来指示默认导出也被选为实际默认导出的默认属性。然而,这意味着自动检测互操作性变得缓慢和困难,并且破坏了非ESM情况下所有可能的实时绑定,因为我们需要构建一个新对象,即。

js
export default moduleExports && moduleExports.__esModule
  ? moduleExports
  : { ...moduleExports, default: moduleExports };

📈 改进1:我想到的一个更好的主意是允许将任意字符串指定为 syntheticNamedExports 的值,例如 syntheticNamedExports: "__moduleExports"。这意味着缺少的命名(甚至默认)导出不会从默认导出中获取,而是从给定名称的命名导出中获取。然后互操作性就可以直接进行

js
export { __moduleExports };
export default __moduleExports.__esModule
  ? __moduleExports.default
  : __moduleExports;

这是相当高效的,尽管如果我们想节省一些字节,仍然可以将其放入一个名为 getDefault 的互操作函数中。当然,在转译后的 ESM 情况下,默认导出不会获得实时绑定,但即使这个问题也是可以解决的,如果在第二步中我们实现对 __esModule 的静态检测:

📈 改进 2:如果我们在模块的顶层遇到 Object.defineProperty(exports, "__esModule", { value: true }) 这行代码(或者代码压缩后使用 !0 替代 true),那么我们可以将此模块标记为已转译,并且甚至可以在转换器中去掉这行代码,使得代码更高效并消除了任何互操作的需要,即上面我们不会在这种情况下添加 export default。如果忽略此属性定义,则也不再需要将代码包装在 createCommonjsModule 中。

例如导入一个不存在的默认导出

🐞 Bug 3:这种情况无法正常工作并不奇怪,因为实际上应该在构建时或运行时抛出错误。否则至少默认导出应该是未定义的,而这里实际上是命名空间。

完整例子:

Input:

js
// "main.js"
import foo from './dep.js';
console.log(foo);

// "dep.js"
Object.defineProperty(exports, '__esModule', { value: true });
exports.foo = 'foo';

Transformed:

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

js
// "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(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 模块

  1. NodeJS:目前 NodeJS 还不支持直接在 CJS 模块中 require ES 模块,但未来可能会支持,届时 require 一个 ES 模块可能会返回命名空间。
  2. Webpack 和 TypeScript:Webpack 支持 require ES 模块并返回命名空间。TypeScript 在转译 ES 模块时,会添加 __esModule 属性,并将默认导出作为一个属性,这意味着在使用 CJS 作为输出目标时,require 一个 ES 模块会得到命名空间。Babel 的行为类似。
  3. Rollup:Rollup 主要用于创建库,默认使用“auto”模式生成 CJS 输出:如果有多个导出,module.exports 包含命名空间。如果只有一个默认导出,则直接赋值给 module.exports。
  4. 改进建议:
    1. 改进3:默认情况下,require 时总是返回命名空间。
    2. 改进4a:添加一个标志,允许用户选择所有模块或部分模块(通过模式匹配)使用类似 Rollup 的“auto”模式,以便于处理混合 ESM 和 CJS 的代码库。
    3. 改进5:Rollup 在使用“auto”模式时,如果没有明确指定,将显示警告,解释可能遇到的问题以及如何更改接口。

仅使用具名导出 require ESM 模块

这个按预期运行。

完整例子:

Input:

js
// "main.js"
const foo = require('./dep.js');
console.log(foo);

// "dep.js"
export const foo = 'foo';

Transformed:

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

js
// "bundle.js"
const foo = 'foo';

var dep = /*#__PURE__*/ Object.freeze({
  __proto__: null,
  foo
});

console.log(dep);

仅使用默认导出 require ESM 模块

这个在自动模式下工作正常,但根据上述参数,可能需要进行更改,请参见改进3和改进4。

完整例子:

Input:

js
// "main.js"
const foo = require('./dep.js');
console.log(foo);

// "dep.js"
export default 'default';

Transformed:

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

js
// "bundle.js"
var foo = 'default';

console.log(foo);

使用具名导出和默认导出 require ESM 模块

🐞 Bug 4: 异常行为,正常情况应该需要返回一个命名空间对象,但实际上返回了默认导出。

完整例子:

Input:

js
// "main.js"
const foo = require('./dep.js');
console.log(foo);

// "dep.js"
export const foo = 'foo';
export default 'default';

Transformed:

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

js
// "bundle.js"
var foo = 'default';

console.log(foo);

外部模块导入

要查看完整的互操作故事,必须同时查看此插件生成的内容以及Rollup生成的CJS输出。

CommonJS插件中的外部导入

🐞 Bug 5: 对于外部导入,commonjs 插件将始终 require 默认导入。

📈 改进 6: 根据上述论点,实际上应该默认为命名空间(从 'external' 导入为 external),因为这在技术上等同于要求一个 ES 模块。

📈 改进 4b: 再次,我们应该添加一个选项来指定当外部 require 只返回默认导出时的情况。甚至可以是相同的选项。所以这里有一些讨论的余地。

完整例子:

Input:

js
// "main.js"
const foo = require('external');
console.log(foo);

Transformed:

js
// "main.js"
import 'external';
import foo from 'external?commonjs-proxy';

console.log(foo);

// "\u0000external?commonjs-external"
import external from 'external';
export default external;

Output:

js
// "bundle.js"
import external from 'external';

console.log(external);

Rollup 的 CJS 输出中的外部导入

导入一个 namespace 对象

理想情况下,这应该转换为一个简单的 require 语句,因为

  • Transpiled ESM 模块在 require 时会返回命名空间。
  • 这意味着根据前一节中的参数,一个简单的 require 将再次变成一个简单的 require。而事实上确实如此:

完整例子:

Input:

js
// "main.js"
import * as foo from 'external';
console.log(foo);

Output:

js
// "bundle.js"
'use strict';

var foo = require('external');

console.log(foo);

导入命名绑定

这些应该只是转换为 require 返回的属性,因为那应该等同于一个命名空间。而且确实如此:

完整例子:

Input:

js
// "main.js"
import { bar, foo } from 'external';
console.log(foo, bar);

Output:

js
// "bundle.js"
'use strict';

var external = require('external');

console.log(external.foo, external.bar);

导入 default export

🐞 Bug 6: 这里有几个问题:

  • interop 函数只检查默认属性的存在,而不是检查 __esModule 属性。我一直以为这背后有某个旧问题中隐藏的原因,但回顾历史发现似乎一直都是这样。这应该改变,因为它会产生各种负面影响:如果文件不是 ES 模块但给 exports.default 赋值,则会被误认为是默认导出;如果文件是 ES 模块但没有默认导出,则错误地返回命名空间。
  • 默认导入的动态绑定将不会被保留。如果外部模块之间存在循环依赖,可能会引发问题。

完整例子:

Input:

js
// "main.js"
import foo from 'external';
console.log(foo);

Output:

js
// "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 类似的做法

Entry points

分配给 module.exports 的入口点

这将被转换为默认导出,这可能是我们所能期望的最好结果。

完整例子:

Input:

js
// "main.js"
module.exports = 'foo';

Transformed:

js
// "main.js"
var main = 'foo';

export default main;

Output:

js
// "bundle.js"
var main = 'foo';

export default main;

将属性添加到导出的入口点

这似乎是合理运行的。

完整例子:

Input:

js
// "main.js"
module.exports.foo = 'foo';

Transformed:

js
// "main.js"
var foo = 'foo';

var main = {
  foo
};

export default main;
export { foo };

Output:

js
// "bundle.js"
var foo = 'foo';

var main = {
  foo
};

export default main;
export { foo };

分配给 module.exports 但我 require 的入口点

🐞 Bug 7:基本上它寻找一个不存在的 __moduleExports 导出,而不是提供命名空间。我在改进1中关于如何重新设计 syntheticNamedExports 的建议也应该从 rollup 内部解决这个问题。当另一个模块导入我们的入口点或者当模块只是向导出添加属性时,类似的问题也会出现。

完整例子:

Input:

js
// "main.js"
const x = require('./main.js');
console.log(x);

module.exports = 'foo';

Transformed:

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

js
// "bundle.js"
console.log(main.__moduleExports);

var main = 'foo';

export default main;

分配给 exports.default 的入口点

🐞 Bug 8:在这里没有生成任何输出,而应该默认导出命名空间,除非存在 __esModule 属性。

完整例子:

Input:

js
// "main.js"
module.exports.default = 'foo';

Transformed:

js
// "main.js"
var _default = 'foo';

var main = {
  default: _default
};

Output:

js
// "bundle.js"

入口点是一个转译后的 ES 模块

理想情况下,输出应该与原始输入具有相同的导出。目前,由于 Object.defineProperty 调用总是会导致使用 createCommonjsModule 包装器,并且不会检测到命名导出。有几种方法可以改进这一点:

  • 删除 __esModule 属性定义,并不将其视为优化失败的原因,请参见改进 2
  • 📈 改进 8:添加一个类似于现在已移除的 namedExports 的新选项,专门列出条目中公开的导出。我们还可以使用此选项来激活或停用默认导出,并决定默认导出应该是分配给 exports.default 还是 module.exports 的内容。这可以通过简单地创建一个重新导出这些输出的包装文件来处理。如果我们以这种方式做,Rollup tree-shaking 可能甚至会删除一些未使用的导出代码。

完整例子:

Input:

js
// "main.js"
Object.defineProperty(exports, '__esModule', { value: true });
exports.default = 'foo';
exports.bar = 'bar';

Transformed:

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

js
// "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(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;

Contributors

Changelog

Discuss

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