Skip to content

Commit

Permalink
Rework interop handling (#3710)
Browse files Browse the repository at this point in the history
* Use interop default helper for AMD, IIFE, UMD as well, unify interop handling

* Support live-bindings for default imports, deconflict default variables

* Only deconflict namespace variables in ESM when preserving modules

* Do not deconflict default export variables if there is no interop

* Remove unneeded checks, use square brackets for default properties in more cases

* Fix external dependency variable name deconflicting

* Generate proper interop namespaces when losing track of a namespace, freeze interop namespaces, mark interop helpers as pure

* Decouple dynamic import deconflicting per output

* Implement per dependency interop

* Support "auto" interop

* Unify export block generation

* Take default from namespace if possible

* Use static default interop if externalLiveBindings are false

* Use correct namespace interop for default case

* Add defaultOnly interop type

* Deprecate boolean interop values

* Throw for invalid interop values

* Throw when importing named bindings from a defaultOnly dependency, warn when reexporting the namespace

* Do not add unused helpers for custom dynamic import wrappers

* Add documentation

* Update changelog

* Improve docs

* Improve coverage

* Add dynamic import to examples
  • Loading branch information
lukastaegert committed Aug 13, 2020
1 parent 1cd3094 commit 4b41f4e
Show file tree
Hide file tree
Showing 455 changed files with 6,775 additions and 1,528 deletions.
27 changes: 26 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
# rollup changelog

## 2.24.0
*unreleased*

### Features
* Allow defining interop per dependency via a function (#3710)
* Support interop "auto" as a more compatible version of "true" (#3710)
* Support interop "default" and "esModule" to avoid unnecessary interop helpers (#3710)
* Support interop "defaultOnly" for simplified helpers and Node ESM interop compatible output (#3710)
* Respect interop option for external dynamic imports (#3710)
* Support live-bindings for external default imports in non-ES formats unless "externalLiveBindings" is "false" (#3710)
* Use shared default interop helpers for AMD, UMD and IIFE formats (#3710)
* Avoid unnecessarily deconflicted module variables in non-ES formats (#3710)
* Freeze generated interop namespace objects (#3710)
* Always mark interop helpers as pure (#3710)
* Avoid default export interop if there is already an interop namespace object (#3710)
* Sort all `require` statements to the top in CommonJS output for easier back-transpilation to ES modules by other tools (#3710)

### Bug Fixes
* Deconflict the names of helper variables introduced for interop (#3710)
* Generate proper namespace objects for static namespace imports in non-ES formats (#3710)
* Do not add unused interop helpers when using the renderDynamicImport hook (#3710)

### Pull Requests
* [#3710](https://github.com/rollup/rollup/pull/3710): Rework interop handling (@lukastaegert)

## 2.23.1
*2020-08-07*

Expand Down Expand Up @@ -40,7 +65,7 @@
*2020-07-18*

### Features
* Allow resolving snythetic named exports via an arbitrary export name (#3657)
* Allow resolving synthetic named exports via an arbitrary export name (#3657)
* Display a warning when the user does not explicitly select an export mode and would generate a chunk with default export mode when targeting CommonJS (#3657)

### Pull Requests
Expand Down
21 changes: 9 additions & 12 deletions cli/run/batchWarnings.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { bold, gray, yellow } from 'colorette';
import { RollupWarning } from '../../src/rollup/types';
import { getOrCreate } from '../../src/utils/getOrCreate';
import relativeId from '../../src/utils/relativeId';
import { stderr } from '../logging';

Expand All @@ -22,8 +23,7 @@ export default function batchWarnings() {
count += 1;

if (warning.code! in deferredHandlers) {
if (!deferredWarnings.has(warning.code!)) deferredWarnings.set(warning.code!, []);
deferredWarnings.get(warning.code!)!.push(warning);
getOrCreate(deferredWarnings, warning.code!, () => []).push(warning);
} else if (warning.code! in immediateHandlers) {
immediateHandlers[warning.code!](warning);
} else {
Expand Down Expand Up @@ -223,8 +223,7 @@ const deferredHandlers: {

const dependencies = new Map();
for (const warning of warnings) {
if (!dependencies.has(warning.source)) dependencies.set(warning.source, []);
dependencies.get(warning.source).push(warning.importer);
getOrCreate(dependencies, warning.source, () => []).push(warning.importer);
}

for (const dependency of dependencies.keys()) {
Expand Down Expand Up @@ -255,16 +254,14 @@ function nest<T>(array: T[], prop: string) {

for (const item of array) {
const key = (item as any)[prop];
if (!lookup.has(key)) {
lookup.set(key, {
getOrCreate(lookup, key, () => {
const items = {
items: [],
key
});

nested.push(lookup.get(key)!);
}

lookup.get(key)!.items.push(item);
};
nested.push(items);
return items;
}).items.push(item);
}

return nested;
Expand Down
2 changes: 2 additions & 0 deletions docs/05-plugin-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,8 @@ const plugin = {
};
```
Note that when this hook rewrites dynamic imports in non-ES formats, no interop code to make sure that e.g. the default export is available as `.default` is generated. It is the responsibility of the plugin to make sure the rewritten dynamic import returns a Promise that resolves to a proper namespace object.
#### `renderError`
Type: `(error: Error) => void`<br>
Kind: `async, parallel`<br>
Expand Down
167 changes: 163 additions & 4 deletions docs/999-big-list-of-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -452,11 +452,170 @@ Default: `false`
This will inline dynamic imports instead of creating new chunks to create a single bundle. Only possible if a single input is provided. Note that this will change the execution order: A module that is only imported dynamically will be executed immediately if the dynamic import is inlined.

#### output.interop
Type: `boolean`<br>
CLI: `--interop`/`--no-interop`<br>
Type: `"auto" | "esModule" | "default" | "defaultOnly" | boolean | ((id: string) => "auto" | "esModule" | "default" | "defaultOnly" | boolean)`<br>
CLI: `--interop <value>`<br>
Default: `true`

Whether to add an 'interop block'. By default (`interop: true`), for safety's sake, Rollup will assign any external dependencies' `default` exports to a separate variable if it is necessary to distinguish between default and named exports. This generally only applies if your external dependencies were transpiled (for example with Babel) – if you are sure you do not need it, you can save a few bytes with `interop: false`.
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:

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

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

```js
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](guide/en/#outputesmodule). 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:

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

```js
// 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.

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

```js
// 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`](guide/en/#outputexternallivebindings) to `false` will generate simplified namespace helpers as well as simplified code for extracted default imports.
- Setting [`output.freeze`](guide/en/#outputfreeze) to `false` will prevent generated interop namespace objects from being frozen.

#### output.intro/output.outro
Type: `string | (() => string | Promise<string>)`<br>
Expand Down Expand Up @@ -915,7 +1074,7 @@ Type: `boolean`<br>
CLI: `--esModule`/`--no-esModule`<br>
Default: `true`

Whether to add a `__esModule: true` property when generating exports for non-ES formats.
Whether to add a `__esModule: true` property when generating exports for non-ES formats. This property signifies that the exported value is the namespace of an ES module and that the default export of this module corresponds to the `.default` property of the exported object. By default, Rollup adds this property when using [named exports mode](guide/en/#outputexports) for a chunk. See also [`output.interop`](https://rollupjs.org/guide/en/#outputinterop).

#### output.exports
Type: `string`<br>
Expand Down
7 changes: 6 additions & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,12 @@ export default command => {
externalLiveBindings: false,
format: 'cjs',
freeze: false,
interop: false,
interop: id => {
if (id === 'fsevents') {
return 'defaultOnly';
}
return 'default';
},
manualChunks: { rollup: ['src/node-entry.ts'] },
sourcemap: true
}
Expand Down

0 comments on commit 4b41f4e

Please sign in to comment.