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 pjson.importInterop to support __esModule #40902

Closed
wants to merge 1 commit 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
15 changes: 15 additions & 0 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,21 @@ When importing [CommonJS modules](#commonjs-namespaces), the
available, provided by static analysis as a convenience for better ecosystem
compatibility.

You can change the behavior of default imports by adding `importInterop` option
to your `package.json`:

```json
{
"importInterop": true
}
```

All `import` statements in that package will check the value of the imported
module's `module.exports.__esModule` value.
If `__esModule` is true, `module.exports.default` value is used as the default
export instead of `module.exports`.
This is useful when you are importing modules generated by Babel or TypeScript.

### `require`

The CommonJS module `require` always treats the files it references as CommonJS.
Expand Down
9 changes: 8 additions & 1 deletion lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,14 @@ class ESMLoader {
throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat);
}

return FunctionPrototypeCall(translator, this, url, source, isMain);
return FunctionPrototypeCall(
translator,
this,
url,
source,
isMain,
parentURL
);
};

const inspectBrk = (
Expand Down
5 changes: 4 additions & 1 deletion lib/internal/modules/esm/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ function getPackageConfig(path, specifier, base) {
type: 'none',
exports: undefined,
imports: undefined,
cjsImportInterop: undefined,
};
packageJSONCache.set(path, packageConfig);
return packageConfig;
Expand All @@ -197,7 +198,7 @@ function getPackageConfig(path, specifier, base) {
}

let { imports, main, name, type } = packageJSON;
const { exports } = packageJSON;
const { exports, cjsImportInterop } = packageJSON;
if (typeof imports !== 'object' || imports === null) imports = undefined;
if (typeof main !== 'string') main = undefined;
if (typeof name !== 'string') name = undefined;
Expand All @@ -212,6 +213,7 @@ function getPackageConfig(path, specifier, base) {
type,
exports,
imports,
cjsImportInterop,
};
packageJSONCache.set(path, packageConfig);
return packageConfig;
Expand Down Expand Up @@ -247,6 +249,7 @@ function getPackageScopeConfig(resolved) {
type: 'none',
exports: undefined,
imports: undefined,
cjsImportInterop: undefined,
};
packageJSONCache.set(packageJSONPath, packageConfig);
return packageConfig;
Expand Down
21 changes: 19 additions & 2 deletions lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const experimentalImportMetaResolve =
const asyncESM = require('internal/process/esm_loader');
const { emitWarningSync } = require('internal/process/warning');
const { TextDecoder } = require('internal/encoding');
const { getPackageScopeConfig } = require('internal/modules/esm/resolve');

let cjsParse;
async function initCJSParse() {
Expand Down Expand Up @@ -166,9 +167,17 @@ function enrichCJSError(err, content, filename) {
const isWindows = process.platform === 'win32';
const winSepRegEx = /\//g;
translators.set('commonjs', async function commonjsStrategy(url, source,
isMain) {
isMain,
parentURL) {
debug(`Translating CJSModule ${url}`);

let importInterop = false;
if (parentURL) {
const parentPackageConfig = getPackageScopeConfig(parentURL);
importInterop =
parentPackageConfig && parentPackageConfig.cjsImportInterop;
}

let filename = internalURLModule.fileURLToPath(new URL(url));
if (isWindows)
filename = StringPrototypeReplace(filename, winSepRegEx, '\\');
Expand All @@ -194,6 +203,14 @@ translators.set('commonjs', async function commonjsStrategy(url, source,
}
}

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

for (const exportName of exportNames) {
if (!ObjectPrototypeHasOwnProperty(exports, exportName) ||
exportName === 'default')
Expand All @@ -205,7 +222,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
40 changes: 40 additions & 0 deletions test/es-module/test-esm-cjs-import-interop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use strict';

const common = require('../common');
const fixtures = require('../common/fixtures');
const { spawn } = require('child_process');
const assert = require('assert');

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

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

const entryCjs = fixtures.path(
'/es-modules/package-cjs-import-interop/cjs-exports-dynamic.cjs'
);

const childCjs = spawn(process.execPath, [entryCjs]);
childCjs.stderr.setEncoding('utf8');
let stdoutCjs = '';
childCjs.stdout.setEncoding('utf8');
childCjs.stdout.on('data', (data) => {
stdoutCjs += data;
});
childCjs.on('close', common.mustCall((code, signal) => {
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
assert.strictEqual(stdoutCjs, 'ok\n');
}));
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use strict';
const assert = require('assert');

(async () => {
const ns = await import('../exports-cases.js');
assert.deepEqual(Object.keys(ns), ['?invalid', 'default', 'invalid identifier', 'isObject', 'package', 'z', 'π', '\u{d83c}\u{df10}']);
assert.strictEqual(ns.π, 'yes');
assert.strictEqual(typeof ns.default.isObject, 'undefined');
assert.strictEqual(ns.default.π, 'yes');
assert.strictEqual(ns.default.z, 'yes');
assert.strictEqual(ns.default.package, 10);
assert.strictEqual(ns.default['invalid identifier'], 'yes');
assert.strictEqual(ns.default['?invalid'], 'yes');

const ns2 = await import('../exports-cases2.js');
assert.strictEqual(typeof ns2, 'object');
assert.strictEqual(ns2.default, 'the default');
assert.strictEqual(ns2.__esModule, true);
assert.strictEqual(ns2.name, 'name');
assert.deepEqual(Object.keys(ns2), ['__esModule', 'case2', 'default', 'name', 'pi']);

const ns3 = await import('../exports-cases3.js')
assert.deepEqual(Object.keys(ns3), ['__esModule', 'case2', 'default', 'name', 'pi']);
assert.strictEqual(ns3.default, 'the default');
assert.strictEqual(ns3.__esModule, true);
assert.strictEqual(ns3.name, 'name');
assert.strictEqual(ns3.case2, 'case2');

console.log('ok');
})();
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');
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "package-cjs-import-interop",
"main": "index.mjs",
"type": "module",
"cjsImportInterop": true
}