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
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e60a56d
Use interop default helper for AMD, IIFE, UMD as well, unify interop …
lukastaegert Jul 10, 2020
39ecb40
Support live-bindings for default imports, deconflict default variables
lukastaegert Jul 12, 2020
332a80a
Only deconflict namespace variables in ESM when preserving modules
lukastaegert Jul 13, 2020
d96e2a8
Do not deconflict default export variables if there is no interop
lukastaegert Jul 13, 2020
ca077d6
Remove unneeded checks, use square brackets for default properties in…
lukastaegert Jul 13, 2020
3b828f5
Fix external dependency variable name deconflicting
lukastaegert Jul 13, 2020
3016d88
Generate proper interop namespaces when losing track of a namespace, …
lukastaegert Jul 15, 2020
0dea6b5
Decouple dynamic import deconflicting per output
lukastaegert Aug 3, 2020
7c15a77
Implement per dependency interop
lukastaegert Aug 5, 2020
b787e21
Support "auto" interop
lukastaegert Aug 6, 2020
9273ac0
Unify export block generation
lukastaegert Aug 6, 2020
5625610
Take default from namespace if possible
lukastaegert Aug 7, 2020
27b4d7b
Use static default interop if externalLiveBindings are false
lukastaegert Aug 7, 2020
61e2f82
Use correct namespace interop for default case
lukastaegert Aug 7, 2020
d08e5a8
Add defaultOnly interop type
lukastaegert Aug 8, 2020
d1c500a
Deprecate boolean interop values
lukastaegert Aug 9, 2020
3ebcc7c
Throw for invalid interop values
lukastaegert Aug 9, 2020
440a66c
Throw when importing named bindings from a defaultOnly dependency, wa…
lukastaegert Aug 10, 2020
6b88f2d
Do not add unused helpers for custom dynamic import wrappers
lukastaegert Aug 10, 2020
6901a57
Add documentation
lukastaegert Aug 11, 2020
4f22c80
Update changelog
lukastaegert Aug 12, 2020
d41a16f
Improve docs
lukastaegert Aug 12, 2020
7a7e15f
Improve coverage
lukastaegert Aug 12, 2020
188ad34
Add dynamic import to examples
lukastaegert Aug 13, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
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