Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework interop handling #3710

Merged
merged 24 commits into from Aug 13, 2020
Merged

Rework interop handling #3710

merged 24 commits into from Aug 13, 2020

Conversation

lukastaegert
Copy link
Member

@lukastaegert lukastaegert commented Aug 10, 2020

This PR contains:

  • bugfix
  • feature
  • refactor
  • documentation
  • other

Are tests included?

  • yes (bugfixes and features will not be merged without tests)
  • no

Breaking Changes?

  • yes (breaking changes will not be merged unless absolutely necessary)
  • no

List any relevant issue numbers:
Resolves #3288
Resolves #2554

Description

This completely reimplements how interop is handled. I.e. in formats cjs, amd, iife and umd, this reimplements how default and namespace imports and reexports are handled, as well as what is returned by dynamic imports from external dependencies.

Besides the existing values, there are now four new interop values: "auto", "esModule", "default" and "defaultOnly". Moreover, a function can be supplied for output.interop to configure the correct interop per dependency.

From the new documentation:

output.interop

Type: "auto" | "esModule" | "default" | "defaultOnly" | boolean | ((id: string) => "auto" | "esModule" | "default" | "defaultOnly" | boolean)

CLI: --interop <value>

Default: true

Controls how Rollup handles default, namespace and dynamic imports from external dependencies in formats like CommonJS that do not natively support these concepts. Note that even though true is the current default value, this value is deprecated and will be replaced by "auto" in the next major version of Rollup. In the examples, we will be using the CommonJS format, but the interop similarly applies to AMD, IIFE and UMD targets as well.

To understand the different values, assume we are bundling the following code for a cjs target:

import ext_default, * as external from 'external1';
console.log(ext_default, external.bar, external);
import('external2').then(console.log);

Keep in mind that for Rollup, import * as ext_namespace from 'external'; console.log(ext_namespace.bar); is completely equivalent to import {bar} from 'external'; console.log(bar); and will produce the same code. In the example above however, the namespace object itself is passed to a global function as well, which means we need it as a properly formed object.

  • "esModule" assumes that required modules are transpiled ES modules where the required value corresponds to the module namespace and the default export is the .default property of the exported object:

    var external = require('external1');
    console.log(external['default'], external.bar, external);
    Promise.resolve().then(function () {
      return require('external2');
    }).then(console.log);

    When esModule is used, Rollup adds no additional interop helpers and also supports live-bindings for default exports.

  • "default" assumes that the required value should be treated as the default export of the imported module, just like when importing CommonJS from an ES module context in Node. In contrast to Node, though, named imports are supported as well which are treated as properties of the default import. To create the namespace object, Rollup injects helpers:

    var external = require('external1');
    
    function _interopNamespaceDefault(e) {
      var n = Object.create(null);
      if (e) {
        Object.keys(e).forEach(function (k) {
          if (k !== 'default') {
            var d = Object.getOwnPropertyDescriptor(e, k);
            Object.defineProperty(n, k, d.get ? d : {
              enumerable: true,
              get: function () {
                return e[k];
              }
            });
          }
        });
      }
      n['default'] = e;
      return Object.freeze(n);
    }
    
    var external__namespace = /*#__PURE__*/_interopNamespaceDefault(external);
    console.log(external, external.bar, external__namespace);
    Promise.resolve().then(function () {
      return /*#__PURE__*/_interopNamespaceDefault(require('external2'));
    }).then(console.log);
  • "auto" combines both "esModule" and "default" by injecting helpers that contain code that detects at runtime if the required value contains the __esModule property. Adding this property is a standard implemented by Rollup, Babel and many other tools to signify that the required value is the namespace of a transpiled ES module:

    var external = require('external1');
    
    function _interopNamespace(e) {
      if (e && e.__esModule) { return e; } else {
        var n = Object.create(null);
        if (e) {
          Object.keys(e).forEach(function (k) {
            if (k !== 'default') {
              var d = Object.getOwnPropertyDescriptor(e, k);
              Object.defineProperty(n, k, d.get ? d : {
                enumerable: true,
                get: function () {
                  return e[k];
                }
              });
            }
          });
        }
        n['default'] = e;
        return Object.freeze(n);
      }
    }
    
    var external__namespace = /*#__PURE__*/_interopNamespace(external);
    console.log(external__namespace['default'], external.bar, external__namespace);
    Promise.resolve().then(function () {
      return /*#__PURE__*/_interopNamespace(require('external2'));
    }).then(console.log);

    Note how Rollup is reusing the created namespace object to get the default export. If the namespace object is not needed, Rollup will use a simpler helper:

    // input
    import ext_default from 'external';
    console.log(ext_default);
    
    // output
    var ext_default = require('external');
    
    function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
    
    var ext_default__default = /*#__PURE__*/_interopDefault(ext_default);
    console.log(ext_default__default['default']);
  • "defaultOnly" is similar to "default" except for the following:

    • Named imports are forbidden. If such an import is encountered, Rollup throws an error even in es and system formats. That way it is ensure that the es version of the code is able to import non-builtin CommonJS modules in Node correctly.
    • While namespace reexports export * from 'external'; are not prohibited, they are ignored and will cause Rollup to display a warning because they would not have an effect if there are no named exports.
    • When a namespace object is generated, Rollup uses a much simpler helper.

    Here is what Rollup will create from the example code. Note that we removed external.bar from the code as otherwise, Rollup would have thrown an error because, as stated before, this is equivalent to a named import.

    var ext_default = require('external1');
    
    function _interopNamespaceDefaultOnly(e) {
      return Object.freeze({__proto__: null, 'default': e});
    }
    
    var ext_default__namespace = /*#__PURE__*/_interopNamespaceDefaultOnly(ext_default);
    console.log(ext_default, ext_default__namespace);
    Promise.resolve().then(function () {
      return /*#__PURE__*/_interopNamespaceDefaultOnly(require('external2'));
    }).then(console.log);
  • When a function is supplied, Rollup will pass each external id to this function once to control the interop type per dependency.

    As an example if all dependencies are CommonJs, the following config will ensure that named imports are only permitted from Node builtins:

    // rollup.config.js
    import builtins from 'builtins';
    const nodeBuiltins = new Set(builtins());
    
    export default {
      // ...
      output: {
        // ...
        interop(id) {
          if (nodeBuiltins.has(id)) {
            return 'default';
          }
          return 'defaultOnly';
        }
      }
    };
  • true is equivalent to "auto" except that it uses a slightly different helper for the default export that checks for the presence of a default property instead of the __esModule property.

    ☢️ This value is deprecated and will be removed in a future Rollup version.

  • false is equivalent to using default when importing a default export and esModule when importing a namespace.

    ☢️ This value is deprecated and will be removed in a future Rollup version.

There are some additional options that have an effect on the generated interop code:

  • Setting output.exernalLiveBindings to false will generate simplified namespace helpers as well as simplified code for extracted default imports.
  • Setting output.freeze to false will prevent generated interop namespace objects from being frozen.

Included fixes and improvements

Features

Bug Fixes

@rollup-bot
Copy link
Collaborator

rollup-bot commented Aug 10, 2020

Thank you for your contribution! ❤️

You can try out this pull request locally by installing Rollup via

npm install rollup/rollup#better-default-interop

or load it into the REPL:
https://rollupjs.org/repl/?circleci=12506

@codecov
Copy link

codecov bot commented Aug 10, 2020

Codecov Report

Merging #3710 into master will increase coverage by 0.16%.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #3710      +/-   ##
==========================================
+ Coverage   96.80%   96.96%   +0.16%     
==========================================
  Files         183      184       +1     
  Lines        6320     6402      +82     
  Branches     1843     1854      +11     
==========================================
+ Hits         6118     6208      +90     
+ Misses        105      103       -2     
+ Partials       97       91       -6     
Impacted Files Coverage Δ
src/utils/variableNames.ts 100.00% <ø> (ø)
cli/run/batchWarnings.ts 98.40% <100.00%> (-0.03%) ⬇️
src/Chunk.ts 100.00% <100.00%> (ø)
src/ExternalModule.ts 100.00% <100.00%> (+2.04%) ⬆️
src/ast/nodes/ImportExpression.ts 100.00% <100.00%> (ø)
src/ast/nodes/MetaProperty.ts 100.00% <100.00%> (ø)
src/ast/nodes/ObjectExpression.ts 92.80% <100.00%> (+0.91%) ⬆️
src/ast/scopes/ChildScope.ts 100.00% <100.00%> (ø)
src/ast/scopes/ModuleScope.ts 100.00% <100.00%> (+11.76%) ⬆️
src/ast/utils/PathTracker.ts 100.00% <100.00%> (ø)
... and 19 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 1cd3094...188ad34. Read the comment docs.

Copy link
Contributor

@guybedford guybedford left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do ping when you've got the docs up.

@@ -2,7 +2,7 @@ define(['exports', 'external'], function (exports, external) { 'use strict';



exports.p = external.default;
exports.p = external;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a bug fix!?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a matter of fact yes, here, reexports behaved differently from regular imports where the .default was not added.

@lukastaegert
Copy link
Member Author

@guybedford I added the documentation and also pasted it into the PR description. By tomorrow, I will try to compile what other changes are included as some hidden bugs were fixed as well.

@lukastaegert
Copy link
Member Author

I also compiled the changelog now, this is a list of the included fixes and improvements:

Features

Bug Fixes

@lukastaegert
Copy link
Member Author

This is now complete and ready for review
cc @guybedford

@dgoldstein0
Copy link
Contributor

very impressive list of changes! Definitely looking forward to this.

The description mentions that dynamic imports are impacted too, but doesn't give any examples of the codegen changes for those. could you add more details around that? I expect it'll be the same as the import * changes, but would like some confirmation.

@lukastaegert
Copy link
Member Author

Yes, exactly. Maybe I extend the examples to also contain a dynamic import as it would of course use the same helper.

Copy link
Contributor

@guybedford guybedford left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is some really great work and a solid model!

One extension of auto in future that I always imagined would be interesting for __esModule would be to check ns[Symbol.toStringTag] === 'Module' as equivalent to the __esModule branding check. This way it could be possible to have a autoModern or something mode which does "the right thing" for externals in both Node.js and browsers without needing the per-module function to support that.

@guybedford
Copy link
Contributor

Also, the modern module brand check can be done with Object.prototype.toString.call(ns) === '[object Module]' to support legacy environments properly.

@lukastaegert
Copy link
Member Author

@guybedford That is definitely an interesting idea. If I understand correctly, though, it would involve Rollup's cjs/amd/iife/umd output to replace or augment

Object.defineProperty(exports, '__esModule', { value: true });

with

exports[Symbol.toStringTag] = 'Module';

or alternatively

Object.defineProperty(exports, 'toString', { value: function() { return  '[object Module]'; } });

for a legacy version to work correctly?

@dgoldstein0 I extended the examples to also contain a dynamic import as well, I hope this answers your question.

@guybedford
Copy link
Contributor

@lukastaegert actually thinking about this more I'm not sure it solves anything... sorry for false suggestion. I don't think we should ever change the __esModule brand because that is a reliable differentiator. Rather this was about environment inference, but in an ES module environment we just want to be treating everything as a namespace anyway so perhaps best not to meddle.

@dgoldstein0
Copy link
Contributor

@dgoldstein0 I extended the examples to also contain a dynamic import as well, I hope this answers your question.

yes it does, thanks a ton!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants