diff --git a/packages/commonjs/README.md b/packages/commonjs/README.md index 53afe5217..9212ed941 100644 --- a/packages/commonjs/README.md +++ b/packages/commonjs/README.md @@ -375,13 +375,18 @@ For these situations, you can change Rollup's behaviour either globally or per m console.log(dep); ``` -- `"auto"`: This is complementary to how [`output.exports`](https://rollupjs.org/guide/en/#outputexports): `"auto"` works in Rollup: If a module has a default export and no named exports, requiring that module returns the default export. In all other cases, the namespace is returned. For external dependencies when using `esmExternals: true`, a corresponding interop helper is added: +- `"auto"`: This is complementary to how [`output.exports`](https://rollupjs.org/guide/en/#outputexports): `"auto"` works in Rollup: If a module has a default export and no named exports, requiring that module returns the default export. In all other cases, the namespace is returned. One exception are namespaces of CommonJS modules imported on Node >= 23: They expose the raw `module.exports` value under a `'module.exports'` key ([nodejs/node#53848](https://github.com/nodejs/node/pull/53848)), and requiring such a module returns that value, just like a real `require()` call would. For external dependencies when using `esmExternals: true`, a corresponding interop helper is added: ```js // output import * as dep$1 from 'dep'; function getDefaultExportFromNamespaceIfNotNamed(n) { + if (n && Object.prototype.hasOwnProperty.call(n, 'module.exports')) { + var d = n['default']; + var m = n['module.exports']; + if (d === m || (d !== d && m !== m)) return m; + } return n && Object.prototype.hasOwnProperty.call(n, 'default') && Object.keys(n).length === 1 ? n['default'] : n; diff --git a/packages/commonjs/src/helpers.js b/packages/commonjs/src/helpers.js index a4c6ca251..aec76cf65 100644 --- a/packages/commonjs/src/helpers.js +++ b/packages/commonjs/src/helpers.js @@ -26,6 +26,11 @@ export const IS_WRAPPED_COMMONJS = 'withRequireFunction'; // Minifiers like uglify will usually transpile it back if compatibility with ES3 is not enabled. // This could be improved by inspecting Rollup's "generatedCode" option +// Since Node 23, the namespace of an imported CommonJS module exposes the raw `module.exports` +// value under a 'module.exports' key whose value is identical to the `default` (https://github.com/nodejs/node/pull/53848). +// `getDefaultExportFromNamespaceIfNotNamed` returns it to match what a real `require()` call would return. +// The same-value check (NaN-safe, hence the self-comparisons) keeps namespaces of ES modulesthat just export a binding named 'module.exports' intact. + const HELPERS = ` export var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; @@ -38,6 +43,11 @@ export function getDefaultExportFromNamespaceIfPresent (n) { } export function getDefaultExportFromNamespaceIfNotNamed (n) { + if (n && Object.prototype.hasOwnProperty.call(n, 'module.exports')) { + var d = n['default']; + var m = n['module.exports']; + if (d === m || (d !== d && m !== m)) return m; + } return n && Object.prototype.hasOwnProperty.call(n, 'default') && Object.keys(n).length === 1 ? n['default'] : n; } diff --git a/packages/commonjs/test/fixtures/function/import-cjs-node23-require-returns-default-auto/_config.js b/packages/commonjs/test/fixtures/function/import-cjs-node23-require-returns-default-auto/_config.js new file mode 100644 index 000000000..b1a9068fb --- /dev/null +++ b/packages/commonjs/test/fixtures/function/import-cjs-node23-require-returns-default-auto/_config.js @@ -0,0 +1,19 @@ +// Since Node 23,the namespace of an imported CommonJS module carries a 'module.exports' key, +// so it never has `default` as its only key and the "auto" unwrapping failed. +// https://github.com/nodejs/node/pull/53848 +module.exports = { + description: + 'returns `module.exports` when requiring an external CommonJS module through a Node >= 23 namespace and requireReturnsDefault is "auto"', + options: { + external: [ + 'external-cjs-node23-constructor', + 'external-cjs-node23-named', + 'external-cjs-node23-nan', + 'external-esm-module-exports-key' + ] + }, + pluginOptions: { + requireReturnsDefault: 'auto', + esmExternals: true + } +}; diff --git a/packages/commonjs/test/fixtures/function/import-cjs-node23-require-returns-default-auto/main.js b/packages/commonjs/test/fixtures/function/import-cjs-node23-require-returns-default-auto/main.js new file mode 100644 index 000000000..97a90c99a --- /dev/null +++ b/packages/commonjs/test/fixtures/function/import-cjs-node23-require-returns-default-auto/main.js @@ -0,0 +1,18 @@ +const ExternalCtor = require('external-cjs-node23-constructor'); +const namedExports = require('external-cjs-node23-named'); +const esmNamespace = require('external-esm-module-exports-key'); +const nanExport = require('external-cjs-node23-nan'); + +t.is(typeof ExternalCtor, 'function', 'unwraps `module.exports` instead of returning the namespace'); +t.is(new ExternalCtor('foo').value, 'foo', 'the required value is constructable'); +t.deepEqual( + namedExports, + { foo: 'foo' }, + 'returns `module.exports` even when named exports were detected' +); +t.deepEqual( + esmNamespace, + { default: 'bar', foo: 'foo', 'module.exports': 'not-the-default' }, + 'keeps the namespace of an ES module that exports a binding named "module.exports"' +); +t.is(nanExport, NaN, 'unwraps `module.exports` when the exported value is NaN'); diff --git a/packages/commonjs/test/node_modules/external-cjs-node23-constructor/index.js b/packages/commonjs/test/node_modules/external-cjs-node23-constructor/index.js new file mode 100644 index 000000000..0d0d3a600 --- /dev/null +++ b/packages/commonjs/test/node_modules/external-cjs-node23-constructor/index.js @@ -0,0 +1,10 @@ +// Fakes the namespace Node >= 23 creates when a CommonJS module is imported: +// the raw `module.exports` value is exposed under a 'module.exports' key: https://github.com/nodejs/node/pull/53848 +Object.defineProperty(exports, '__esModule', { value: true }); + +function ExternalCtor(value) { + this.value = value; +} + +exports.default = ExternalCtor; +exports['module.exports'] = ExternalCtor; diff --git a/packages/commonjs/test/node_modules/external-cjs-node23-named/index.js b/packages/commonjs/test/node_modules/external-cjs-node23-named/index.js new file mode 100644 index 000000000..8553e8201 --- /dev/null +++ b/packages/commonjs/test/node_modules/external-cjs-node23-named/index.js @@ -0,0 +1,9 @@ +// Fakes the Node >= 23 namespace of a CommonJS module whose named exports were detected by cjs-module-lexer: +// named keys next to 'default' and 'module.exports' (https://github.com/nodejs/node/pull/53848). +Object.defineProperty(exports, '__esModule', { value: true }); + +const moduleExports = { foo: 'foo' }; + +exports.default = moduleExports; +exports.foo = 'foo'; +exports['module.exports'] = moduleExports; diff --git a/packages/commonjs/test/node_modules/external-cjs-node23-nan/index.js b/packages/commonjs/test/node_modules/external-cjs-node23-nan/index.js new file mode 100644 index 000000000..557f16396 --- /dev/null +++ b/packages/commonjs/test/node_modules/external-cjs-node23-nan/index.js @@ -0,0 +1,6 @@ +// Fakes the Node >= 23 namespace of a CommonJS module that exports NaN: +// `default` and 'module.exports' hold the same value, but NaN === NaN is false. +Object.defineProperty(exports, '__esModule', { value: true }); + +exports.default = NaN; +exports['module.exports'] = NaN; diff --git a/packages/commonjs/test/node_modules/external-esm-module-exports-key/index.js b/packages/commonjs/test/node_modules/external-esm-module-exports-key/index.js new file mode 100644 index 000000000..72d328378 --- /dev/null +++ b/packages/commonjs/test/node_modules/external-esm-module-exports-key/index.js @@ -0,0 +1,8 @@ +// Fakes a genuine ES module that exports a binding literally named 'module.exports' +// (`export { x as 'module.exports' }`). Unlike a Node >= 23 CommonJS namespace, its value +// differs from `default`, so "auto" must keep returning the namespace. +Object.defineProperty(exports, '__esModule', { value: true }); + +exports.default = 'bar'; +exports.foo = 'foo'; +exports['module.exports'] = 'not-the-default'; diff --git a/packages/commonjs/test/snapshots/function.js.snap b/packages/commonjs/test/snapshots/function.js.snap index 1ecce0bb1..fb34e9e65 100644 --- a/packages/commonjs/test/snapshots/function.js.snap +++ b/packages/commonjs/test/snapshots/function.js.snap @@ -5010,6 +5010,95 @@ module.exports = main; } `; +exports[`import-cjs-node23-require-returns-default-auto 1`] = ` +{ + "main.js": "'use strict'; + +var externalCjsNode23Constructor = require('external-cjs-node23-constructor'); +var externalCjsNode23Named = require('external-cjs-node23-named'); +var externalEsmModuleExportsKey = require('external-esm-module-exports-key'); +var externalCjsNode23Nan = require('external-cjs-node23-nan'); + +function _interopNamespaceCompat(e) { + if (e && typeof e === 'object' && 'default' in e) return 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 externalCjsNode23Constructor__namespace = /*#__PURE__*/_interopNamespaceCompat(externalCjsNode23Constructor); +var externalCjsNode23Named__namespace = /*#__PURE__*/_interopNamespaceCompat(externalCjsNode23Named); +var externalEsmModuleExportsKey__namespace = /*#__PURE__*/_interopNamespaceCompat(externalEsmModuleExportsKey); +var externalCjsNode23Nan__namespace = /*#__PURE__*/_interopNamespaceCompat(externalCjsNode23Nan); + +function getDefaultExportFromCjs (x) { + return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; +} + +function getDefaultExportFromNamespaceIfNotNamed (n) { + if (n && Object.prototype.hasOwnProperty.call(n, 'module.exports')) { + var d = n['default']; + var m = n['module.exports']; + if (d === m || (d !== d && m !== m)) return m; + } + return n && Object.prototype.hasOwnProperty.call(n, 'default') && Object.keys(n).length === 1 ? n['default'] : n; +} + +var main$1 = {}; + +var require$$0 = /*@__PURE__*/getDefaultExportFromNamespaceIfNotNamed(externalCjsNode23Constructor__namespace); + +var require$$1 = /*@__PURE__*/getDefaultExportFromNamespaceIfNotNamed(externalCjsNode23Named__namespace); + +var require$$2 = /*@__PURE__*/getDefaultExportFromNamespaceIfNotNamed(externalEsmModuleExportsKey__namespace); + +var require$$3 = /*@__PURE__*/getDefaultExportFromNamespaceIfNotNamed(externalCjsNode23Nan__namespace); + +var hasRequiredMain; + +function requireMain () { + if (hasRequiredMain) return main$1; + hasRequiredMain = 1; + const ExternalCtor = require$$0; + const namedExports = require$$1; + const esmNamespace = require$$2; + const nanExport = require$$3; + + t.is(typeof ExternalCtor, 'function', 'unwraps \`module.exports\` instead of returning the namespace'); + t.is(new ExternalCtor('foo').value, 'foo', 'the required value is constructable'); + t.deepEqual( + namedExports, + { foo: 'foo' }, + 'returns \`module.exports\` even when named exports were detected' + ); + t.deepEqual( + esmNamespace, + { default: 'bar', foo: 'foo', 'module.exports': 'not-the-default' }, + 'keeps the namespace of an ES module that exports a binding named "module.exports"' + ); + t.is(nanExport, NaN, 'unwraps \`module.exports\` when the exported value is NaN'); + return main$1; +} + +var mainExports = requireMain(); +var main = /*@__PURE__*/getDefaultExportFromCjs(mainExports); + +module.exports = main; +", +} +`; + exports[`import-esm-require-returns-default-auto 1`] = ` { "main.js": "'use strict'; @@ -5045,6 +5134,11 @@ function getDefaultExportFromCjs (x) { } function getDefaultExportFromNamespaceIfNotNamed (n) { + if (n && Object.prototype.hasOwnProperty.call(n, 'module.exports')) { + var d = n['default']; + var m = n['module.exports']; + if (d === m || (d !== d && m !== m)) return m; + } return n && Object.prototype.hasOwnProperty.call(n, 'default') && Object.keys(n).length === 1 ? n['default'] : n; } @@ -5503,6 +5597,11 @@ function getDefaultExportFromCjs (x) { } function getDefaultExportFromNamespaceIfNotNamed (n) { + if (n && Object.prototype.hasOwnProperty.call(n, 'module.exports')) { + var d = n['default']; + var m = n['module.exports']; + if (d === m || (d !== d && m !== m)) return m; + } return n && Object.prototype.hasOwnProperty.call(n, 'default') && Object.keys(n).length === 1 ? n['default'] : n; }