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

esm: add option to interpret __esModule like Babel #40892

Closed
wants to merge 3 commits into from
Closed
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
9 changes: 9 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ If this flag is passed, the behavior can still be set to not abort through
[`process.setUncaughtExceptionCaptureCallback()`][] (and through usage of the
`domain` module that uses it).

### `--cjs-import-interop`

<!-- YAML
added: REPLACEME
-->

Enables the CommonJS default export recognition.

### `--completion-bash`

<!-- YAML
Expand Down Expand Up @@ -1545,6 +1553,7 @@ Node.js options that are allowed are:

<!-- node-options-node start -->

* `--cjs-import-interop`
* `--conditions`, `-C`
* `--diagnostic-dir`
* `--disable-proto`
Expand Down
5 changes: 5 additions & 0 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,11 @@ When importing [CommonJS modules](#commonjs-namespaces), the
available, provided by static analysis as a convenience for better ecosystem
compatibility.

If `--cjs-import-interop` is provided and the imported CommonJS module has
`__esModule` exports as a truthy value, then the CommonJS module is treated as
derived from an ES module. In this case, the `module.exports.default` value is
used as the default export instead of `module.exports`.

### `require`

The CommonJS module `require` always treats the files it references as CommonJS.
Expand Down
12 changes: 11 additions & 1 deletion lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
const moduleWrap = internalBinding('module_wrap');
const { ModuleWrap } = moduleWrap;
const { getOptionValue } = require('internal/options');
const cjsImportInterop =
getOptionValue('--cjs-import-interop');
const experimentalImportMetaResolve =
getOptionValue('--experimental-import-meta-resolve');
const asyncESM = require('internal/process/esm_loader');
Expand Down Expand Up @@ -194,6 +196,14 @@ translators.set('commonjs', async function commonjsStrategy(url, source,
}
}

// We might trigger a getter -> dont fail.
let esModule = false;
if (cjsImportInterop) {
try {
esModule = !!exports.__esModule;
} catch {}
}

for (const exportName of exportNames) {
if (!ObjectPrototypeHasOwnProperty(exports, exportName) ||
exportName === 'default')
Expand All @@ -205,7 +215,7 @@ translators.set('commonjs', async function commonjsStrategy(url, source,
} catch {}
this.setExport(exportName, value);
}
this.setExport('default', exports);
this.setExport('default', esModule ? exports.default : exports);
});
});

Expand Down
4 changes: 4 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,10 @@ DebugOptionsParser::DebugOptionsParser() {
}

EnvironmentOptionsParser::EnvironmentOptionsParser() {
AddOption("--cjs-import-interop",
"Supports interop for modules with __esModule",
&EnvironmentOptions::cjs_import_interop,
kAllowedInEnvironment);
AddOption("--conditions",
"additional user conditions for conditional exports and imports",
&EnvironmentOptions::conditions,
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ class EnvironmentOptions : public Options {
std::vector<std::string> conditions;
std::string dns_result_order;
bool enable_source_maps = false;
bool cjs_import_interop = false;
bool experimental_json_modules = false;
bool experimental_modules = false;
std::string experimental_specifier_resolution;
Expand Down
15 changes: 15 additions & 0 deletions test/es-module/test-esm-cjs-exports.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,21 @@ child.on('close', common.mustCall((code, signal) => {
assert.strictEqual(stdout, 'ok\n');
}));

const entryInterop = fixtures.path('/es-modules/cjs-exports-interop.mjs');

child = spawn(process.execPath, ['--cjs-import-interop', entryInterop]);
child.stderr.setEncoding('utf8');
let stdout2 = '';
child.stdout.setEncoding('utf8');
child.stdout.on('data', (data) => {
stdout2 += data;
});
child.on('close', common.mustCall((code, signal) => {
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
assert.strictEqual(stdout2, 'ok\n');
}));

const entryInvalid = fixtures.path('/es-modules/cjs-exports-invalid.mjs');
child = spawn(process.execPath, [entryInvalid]);
let stderr = '';
Expand Down
37 changes: 37 additions & 0 deletions test/fixtures/es-modules/cjs-exports-interop.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { strictEqual, deepEqual } from 'assert';

import m, { π } from './exports-cases.js';
import * as ns from './exports-cases.js';

deepEqual(Object.keys(ns), ['?invalid', 'default', 'invalid identifier', 'isObject', 'package', 'z', 'π', '\u{d83c}\u{df10}']);
strictEqual(π, 'yes');
strictEqual(typeof m.isObject, 'undefined');
strictEqual(m.π, 'yes');
strictEqual(m.z, 'yes');
strictEqual(m.package, 10);
strictEqual(m['invalid identifier'], 'yes');
strictEqual(m['?invalid'], 'yes');

import m2, { __esModule as __esModule2, name as name2 } from './exports-cases2.js';
import * as ns2 from './exports-cases2.js';

strictEqual(__esModule2, true);
strictEqual(name2, 'name');
strictEqual(typeof ns2, 'object');
strictEqual(m2, 'the default');
strictEqual(ns2.__esModule, true);
strictEqual(ns2.name, 'name');
deepEqual(Object.keys(ns2), ['__esModule', 'case2', 'default', 'name', 'pi']);

import m3, { __esModule as __esModule3, name as name3 } from './exports-cases3.js';
import * as ns3 from './exports-cases3.js';

strictEqual(__esModule3, true);
strictEqual(name3, 'name');
deepEqual(Object.keys(ns3), ['__esModule', 'case2', 'default', 'name', 'pi']);
strictEqual(m3, 'the default');
strictEqual(ns3.__esModule, true);
strictEqual(ns3.name, 'name');
strictEqual(ns3.case2, 'case2');

console.log('ok');