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

feat(commonjs): inject __esModule marker into ES namespaces #552

Merged
merged 4 commits into from
Sep 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 43 additions & 12 deletions packages/commonjs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,21 +76,21 @@ commonjs({
Type: `string | string[]`<br>
Default: `null`

A [minimatch pattern](https://github.com/isaacs/minimatch), or array of patterns, which specifies the files in the build the plugin should _ignore_. By default non-CommonJS modules are ignored.
A [minimatch pattern](https://github.com/isaacs/minimatch), or array of patterns, which specifies the files in the build the plugin should _ignore_. By default, all files with extensions other than those in `extensions` or `".cjs"` are ignored, but you can exclude additional files. See also the `include` option.

### `include`

Type: `string | string[]`<br>
Default: `null`

A [minimatch pattern](https://github.com/isaacs/minimatch), or array of patterns, which specifies the files in the build the plugin should operate on. By default CommonJS modules are targeted.
A [minimatch pattern](https://github.com/isaacs/minimatch), or array of patterns, which specifies the files in the build the plugin should operate on. By default, all files with extension `".cjs"` or those in `extensions` are included, but you can narrow this list by only including specific files. These files will be analyzed and transpiled if either the analysis does not find ES module specific statements or `transformMixedEsModules` is `true`.

### `extensions`

Type: `string[]`<br>
Default: `['.js']`

Search for extensions other than .js in the order specified.
For extensionless imports, search for extensions other than .js in the order specified. Note that you need to make sure that non-JavaScript files are transpiled by another plugin first.

### `ignoreGlobal`

Expand All @@ -104,28 +104,28 @@ If true, uses of `global` won't be dealt with by this plugin.
Type: `boolean`<br>
Default: `true`

If false, skips source map generation for CommonJS modules.
If false, skips source map generation for CommonJS modules. This will improve performance.

### `transformMixedEsModules`

Type: `boolean`<br>
Default: `false`

Instructs the plugin whether or not to enable mixed module transformations. This is useful in scenarios with mixed ES and CommonJS modules. Set to `true` if it's known that `require` calls should be transformed, or `false` if the code contains env detection and the `require` should survive a transformation.
Instructs the plugin whether to enable mixed module transformations. This is useful in scenarios with modules that contain a mix of ES `import` statements and CommonJS `require` expressions. Set to `true` if `require` calls should be transformed to imports in mixed modules, or `false` if the `require` expressions should survive the transformation. The latter can be important if the code contains environment detection, or you are coding for an environment with special treatment for `require` calls such as [ElectronJS](https://www.electronjs.org/). See also the "ignore" option.

### `ignore`

Type: `string[] | ((id: string) => boolean)`<br>
Default: `[]`

Sometimes you have to leave require statements unconverted. Pass an array containing the IDs or an `id => boolean` function. Only use this option if you know what you're doing!
Sometimes you have to leave require statements unconverted. Pass an array containing the IDs or an `id => boolean` function.

### `esmExternals`

Type: `boolean | string[] || ((id: string) => boolean)`
Type: `boolean | string[] | ((id: string) => boolean)`
Default: `false`

Controls how imports from external dependencies are rendered. By default, all external dependencies are assumed to be CommonJS. This means they are rendered as default imports to be compatible with e.g. NodeJS where ES modules can only import a default export from a CommonJS dependency:
Controls how to render imports from external dependencies. By default, this plugin assumes that all external dependencies are CommonJS. This means they are rendered as default imports to be compatible with e.g. NodeJS where ES modules can only import a default export from a CommonJS dependency:

```js
// input
Expand All @@ -137,16 +137,16 @@ import foo from 'foo';

This is likely not desired for ES module dependencies: Here `require` should usually return the namespace to be compatible with how bundled modules are handled.

If you set `esmExternals` to `true`, all external dependencies are assumed to be ES modules and will adhere to the `requireReturnsDefault` option. If that option is not set, they will be rendered as namespace imports.
If you set `esmExternals` to `true`, this plugins assumes that all external dependencies are ES modules and will adhere to the `requireReturnsDefault` option. If that option is not set, they will be rendered as namespace imports.

You can also supply an array of ids that are to be treated as ES modules, or a function that will be passed each external id to determine if it is an ES module.
You can also supply an array of ids to be treated as ES modules, or a function that will be passed each external id to determine if it is an ES module.

### `requireReturnsDefault`

Type: `boolean | "auto" | "preferred" | ((id: string) => boolean | "auto" | "preferred")`<br>
Default: `false`

Controls what is returned when requiring an ES module or external dependency from a CommonJS file. By default, this plugin will render it as a namespace import, i.e.
Controls what is returned when requiring an ES module from a CommonJS file. When using the `esmExternals` option, this will also apply to external modules. By default, this plugin will render those imports as namespace imports, i.e.

```js
// input
Expand Down Expand Up @@ -182,13 +182,44 @@ This is in line with how other bundlers handle this situation and is also the mo

For these situations, you can change Rollup's behaviour either globally or per module. To change it globally, set the `requireReturnsDefault` option to one of the following values:

- `false`: This is the default, requiring an ES module returns its namespace. For external dependencies when using `esmExternals: true`, no additional interop code is generated:
- `false`: This is the default, requiring an ES module returns its namespace. This is the only option that will also add a marker `__esModule: true` to the namespace to support interop patterns in CommonJS modules that are transpiled ES modules.

```js
// input
const dep = require('dep');
console.log(dep);

// output
import * as dep$1 from 'dep';

function getAugmentedNamespace(n) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps this function could have an explicit opt-out if __esModule is already defined? Would that be a slight perf improvement in some cases?

Copy link
Member Author

Choose a reason for hiding this comment

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

Definitely, added!

var a = Object.defineProperty({}, '__esModule', { value: true });
Object.keys(n).forEach(function (k) {
var d = Object.getOwnPropertyDescriptor(n, k);
Object.defineProperty(
a,
k,
d.get
? d
: {
enumerable: true,
get: function () {
return n[k];
},
}
);
});
return a;
}

var dep = /*@__PURE__*/ getAugmentedNamespace(dep$1);

console.log(dep);
```

- `"namespace"`: Like `false`, requiring an ES module returns its namespace, but the plugin does not add the `__esModule` marker and thus creates more efficient code. For external dependencies when using `esmExternals: true`, no additional interop code is generated.

```js
// output
import * as dep from 'dep';

Expand Down
25 changes: 20 additions & 5 deletions packages/commonjs/src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ export function getDefaultExportFromCjs (x) {

export function createCommonjsModule(fn, basedir, module) {
return module = {
path: basedir,
exports: {},
require: function (path, base) {
return commonjsRequire(path, (base === undefined || base === null) ? module.path : base);
}
path: basedir,
exports: {},
require: function (path, base) {
return commonjsRequire(path, (base === undefined || base === null) ? module.path : base);
}
}, fn(module, module.exports), module.exports;
}

Expand All @@ -52,6 +52,21 @@ export function getDefaultExportFromNamespaceIfPresent (n) {
export function getDefaultExportFromNamespaceIfNotNamed (n) {
return n && Object.prototype.hasOwnProperty.call(n, 'default') && Object.keys(n).length === 1 ? n['default'] : n;
}

export function getAugmentedNamespace(n) {
if (n.__esModule) return n;
var a = Object.defineProperty({}, '__esModule', {value: true});
Object.keys(n).forEach(function (k) {
var d = Object.getOwnPropertyDescriptor(n, k);
Object.defineProperty(a, k, d.get ? d : {
enumerable: true,
get: function () {
return n[k];
}
});
});
return a;
}
`;

const HELPER_NON_DYNAMIC = `
Expand Down
13 changes: 8 additions & 5 deletions packages/commonjs/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,14 @@ export default function commonjs(options = {}) {

transform(code, id) {
const extName = extname(id);
if (extName !== '.cjs' && id !== DYNAMIC_PACKAGES_ID && !id.startsWith(DYNAMIC_JSON_PREFIX)) {
if (!filter(id) || !extensions.includes(extName)) {
setIsCjsPromise(id, null);
return null;
}
if (
extName !== '.cjs' &&
id !== DYNAMIC_PACKAGES_ID &&
!id.startsWith(DYNAMIC_JSON_PREFIX) &&
(!filter(id) || !extensions.includes(extName))
) {
setIsCjsPromise(id, null);
return null;
}

let transformed;
Expand Down
16 changes: 11 additions & 5 deletions packages/commonjs/src/proxies.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ export function getUnknownRequireProxy(id, requireReturnsDefault) {
const name = getName(id);
const exported =
requireReturnsDefault === 'auto'
? `import {getDefaultExportFromNamespaceIfNotNamed} from "${HELPERS_ID}"; export default /*@__PURE__*/getDefaultExportFromNamespaceIfNotNamed(${name})`
? `import {getDefaultExportFromNamespaceIfNotNamed} from "${HELPERS_ID}"; export default /*@__PURE__*/getDefaultExportFromNamespaceIfNotNamed(${name});`
: requireReturnsDefault === 'preferred'
? `import {getDefaultExportFromNamespaceIfPresent} from "${HELPERS_ID}"; export default /*@__PURE__*/getDefaultExportFromNamespaceIfPresent(${name})`
: `export default ${name}`;
? `import {getDefaultExportFromNamespaceIfPresent} from "${HELPERS_ID}"; export default /*@__PURE__*/getDefaultExportFromNamespaceIfPresent(${name});`
: !requireReturnsDefault
? `import {getAugmentedNamespace} from "${HELPERS_ID}"; export default /*@__PURE__*/getAugmentedNamespace(${name});`
: `export default ${name};`;
return `import * as ${name} from ${JSON.stringify(id)}; ${exported}`;
}

Expand Down Expand Up @@ -53,11 +55,15 @@ export async function getStaticRequireProxy(
return `import { __moduleExports } from ${JSON.stringify(id)}; export default __moduleExports;`;
} else if (isCjs === null) {
return getUnknownRequireProxy(id, requireReturnsDefault);
} else if (!requireReturnsDefault) {
return `import {getAugmentedNamespace} from "${HELPERS_ID}"; import * as ${name} from ${JSON.stringify(
id
)}; export default /*@__PURE__*/getAugmentedNamespace(${name});`;
} else if (
requireReturnsDefault !== true &&
(!requireReturnsDefault ||
(requireReturnsDefault === 'namespace' ||
!esModulesWithDefaultExport.has(id) ||
(esModulesWithNamedExports.has(id) && requireReturnsDefault === 'auto'))
(requireReturnsDefault === 'auto' && esModulesWithNamedExports.has(id)))
) {
return `import * as ${name} from ${JSON.stringify(id)}; export default ${name};`;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
module.exports = {
description: 'always uses the default export when esmExternals is not used',
options: {
external: [
'external-cjs-exports',
'external-cjs-module-exports',
'external-esm-named',
'external-esm-mixed',
'external-esm-default'
]
external: ['external-esm-named', 'external-esm-mixed', 'external-esm-default']
},
pluginOptions: {
esmExternals: true
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
const externalExports = require('external-cjs-exports');
const externalModuleExports = require('external-cjs-module-exports');
const externalNamed = require('external-esm-named');
const externalMixed = require('external-esm-mixed');
const externalDefault = require('external-esm-default');

t.deepEqual(externalExports, { foo: 'foo' }, 'external exports');
t.deepEqual(externalModuleExports, 'bar', 'external module exports');
t.deepEqual(externalNamed, { foo: 'foo' }, 'external named');
t.deepEqual(externalMixed, { default: 'bar', foo: 'foo' }, 'external mixed');
t.deepEqual(externalDefault, { default: 'bar' }, 'external default');
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = {
description:
'returns the namespace when requiring an ES module and requireReturnsDefault is "namespace"',
options: {
external: ['external-esm-named', 'external-esm-mixed', 'external-esm-default']
},
pluginOptions: {
requireReturnsDefault: 'namespace',
esmExternals: true
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'bar';
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const externalNamed = require('external-esm-named');
const externalMixed = require('external-esm-mixed');
const externalDefault = require('external-esm-default');

const namedExports = require('./named.js');
const mixedExports = require('./mixed.js');
const defaultExport = require('./default.js');
const noExports = require('./none.js');

t.deepEqual(namedExports, { foo: 'foo' }, 'named exports');
t.deepEqual(mixedExports, { foo: 'foo', default: 'bar' }, 'mixed exports');
t.deepEqual(defaultExport, { default: 'bar' }, 'default export');
t.deepEqual(noExports, {}, 'no exports');
t.deepEqual(externalNamed, { foo: 'foo' }, 'external named');
t.deepEqual(externalMixed, { foo: 'foo', default: 'bar' }, 'external mixed');
t.deepEqual(externalDefault, { default: 'bar' }, 'external default');
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const foo = 'foo';
export default 'bar';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const foo = 'foo';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
description: 'adds the __esModule property when requiring an ES module and support live-bindings'
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* eslint-disable import/no-mutable-exports */
let foo = 'foo';
let bar = 'bar';
export { foo as default, bar };

export function update(newFoo, newBar) {
foo = newFoo;
bar = newBar;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* eslint-disable */
var lib = require('./lib.js');

function _interopDefault(e) {
return e && e.__esModule ? e : { default: e };
}

var lib__default = /*#__PURE__*/_interopDefault(lib);
t.is(lib__default['default'], 'foo')
t.is(lib.bar, 'bar')

lib.update('newFoo', 'newBar');
t.is(lib__default['default'], 'newFoo')
t.is(lib.bar, 'newBar')

Loading