fix(jest-runtime): handle function module.exports for CJS-from-ESM named exports (#16150)

This commit is contained in:
Simen Bekkhus
2026-05-09 02:26:29 +02:00
committed by GitHub
parent b0e0b44c63
commit d652e19b7a
9 changed files with 93 additions and 60 deletions
+6
View File
@@ -1,5 +1,11 @@
## main
## 30.4.2
### Fixes
- `[jest-runtime]` Fix named imports from CJS modules whose `module.exports` is a function with own-property exports ([#16150](https://github.com/jestjs/jest/pull/16150))
## 30.4.1
### Features
@@ -2,7 +2,7 @@
exports[`runs ESM/CJS interop fixture (__esModule unwrapping, named exports, singleton caching) 1`] = `
"Test Suites: 1 passed, 1 total
Tests: 6 passed, 6 total
Tests: 8 passed, 8 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites matching cjs-esm-interop."
@@ -7,6 +7,10 @@
import greet, {helper} from '../babel-style-default.cjs';
import plainDefault, {multiply, value} from '../plain-cjs.cjs';
import fnDefault, {
helper as fnHelper,
splitCookiesString,
} from '../fn-exports.cjs';
import {getCount as countA, increment as incA} from '../importer-a.mjs';
import {getCount as countB, increment as incB} from '../importer-b.mjs';
@@ -38,6 +42,18 @@ test('named imports from plain CJS work', () => {
expect(multiply(6, 7)).toBe(42);
});
// ── CJS with function module.exports ─────────────────────────────────────────
test('default import of function-export CJS is the function itself', () => {
expect(typeof fnDefault).toBe('function');
expect(fnDefault('x')).toEqual({input: 'x'});
});
test('named imports from function-export CJS work', () => {
expect(splitCookiesString('a,b')).toEqual(['a', 'b']);
expect(fnHelper(21)).toBe(42);
});
// ── CJS-as-ESM caching (singleton) ───────────────────────────────────────────
test('two ESM importers of the same CJS module share one singleton instance', () => {
+23
View File
@@ -0,0 +1,23 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
// CJS pattern where module.exports is a function with own-property named exports.
'use strict';
function parse(input) {
return {input};
}
parse.splitCookiesString = function splitCookiesString(str) {
return str.split(',');
};
parse.helper = function helper(x) {
return x * 2;
};
module.exports = parse;
+8 -25
View File
@@ -673,13 +673,10 @@ __metadata:
languageName: node
linkType: hard
"ip-address@npm:^9.0.5":
version: 9.0.5
resolution: "ip-address@npm:9.0.5"
dependencies:
jsbn: "npm:1.1.0"
sprintf-js: "npm:^1.1.3"
checksum: 10/1ed81e06721af012306329b31f532b5e24e00cb537be18ddc905a84f19fe8f83a09a1699862bf3a1ec4b9dea93c55a3fa5faf8b5ea380431469df540f38b092c
"ip-address@npm:^10.1.1":
version: 10.2.0
resolution: "ip-address@npm:10.2.0"
checksum: 10/12fec399e1af5753ac322e47a6d81a50d3a528b3abb17c09525b2a2edcaedcca628c40520706f7037bc4d8e951b0296c47e7b86d0a8e6e2335c8f0ba4afcfac1
languageName: node
linkType: hard
@@ -734,13 +731,6 @@ __metadata:
languageName: node
linkType: hard
"jsbn@npm:1.1.0":
version: 1.1.0
resolution: "jsbn@npm:1.1.0"
checksum: 10/bebe7ae829bbd586ce8cbe83501dd8cb8c282c8902a8aeeed0a073a89dc37e8103b1244f3c6acd60278bcbfe12d93a3f83c9ac396868a3b3bbc3c5e5e3b648ef
languageName: node
linkType: hard
"lodash.snakecase@npm:^4.1.1":
version: 4.1.1
resolution: "lodash.snakecase@npm:4.1.1"
@@ -1217,19 +1207,12 @@ __metadata:
linkType: hard
"socks@npm:^2.8.3":
version: 2.8.4
resolution: "socks@npm:2.8.4"
version: 2.8.9
resolution: "socks@npm:2.8.9"
dependencies:
ip-address: "npm:^9.0.5"
ip-address: "npm:^10.1.1"
smart-buffer: "npm:^4.2.0"
checksum: 10/ab3af97aeb162f32c80e176c717ccf16a11a6ebb4656a62b94c0f96495ea2a1f4a8206c04b54438558485d83d0c5f61920c07a1a5d3963892a589b40cc6107dd
languageName: node
linkType: hard
"sprintf-js@npm:^1.1.3":
version: 1.1.3
resolution: "sprintf-js@npm:1.1.3"
checksum: 10/e7587128c423f7e43cc625fe2f87e6affdf5ca51c1cc468e910d8aaca46bb44a7fbcfa552f787b1d3987f7043aeb4527d1b99559e6621e01b42b3f45e5a24cbb
checksum: 10/8675e6b023faeaa5c2511664b106fd21f5d5ecb403584412e0efae58a76e0c0661c0c18ca340988f6cb398232308dae85fb710f847c9c6e0a6af33b07e4cd33a
languageName: node
linkType: hard
@@ -130,6 +130,27 @@ describe('syntheticBuilders', () => {
},
);
testWithVmEsm(
'exposes own-property named exports when module.exports is a function',
async () => {
// Pattern: `module.exports = function parse() {}; parse.helper = ...`
function parse() {}
(parse as any).helper = () => 'h';
(parse as any).splitCookiesString = () => ['a', 'b'];
const m = buildCjsAsEsmSyntheticModule(
'/from.js',
'/mod.cjs',
context(),
() => parse,
makeCache(['helper', 'splitCookiesString']),
);
const ns = await evaluate(m);
expect(ns.default).toBe(parse);
expect(ns.helper).toBe((parse as any).helper);
expect(ns.splitCookiesString).toBe((parse as any).splitCookiesString);
},
);
testWithVmEsm('handles non-object module.exports', async () => {
const m = buildCjsAsEsmSyntheticModule(
'/from.js',
@@ -114,9 +114,10 @@ export function buildCjsAsEsmSyntheticModule(
const parsedExports = cjsExportsCache.getExportsOf(from, modulePath);
// CJS modules can legally set `module.exports` to `null` or a primitive.
// Functions are also valid (e.g. `module.exports = fn; fn.helper = ...`).
const cjsRecord =
typeof cjs === 'object' && cjs !== null
? (cjs as Record<string, unknown>)
cjs !== null && (typeof cjs === 'object' || typeof cjs === 'function')
? cjs
: null;
const allCandidates = new Set([
@@ -126,19 +127,19 @@ export function buildCjsAsEsmSyntheticModule(
const cjsExports = [...allCandidates].filter(exportName => {
// `default` is handled separately below as the whole module.exports.
if (exportName === 'default') {
if (exportName === 'default' || cjsRecord == null) {
return false;
}
return cjsRecord
? Object.hasOwnProperty.call(cjsRecord, exportName)
: false;
return Object.hasOwn(cjsRecord, exportName);
});
return new SyntheticModule(
[...cjsExports, 'default'],
function () {
for (const exportName of cjsExports) {
this.setExport(exportName, (cjs as any)[exportName]);
if (cjsRecord != null) {
for (const exportName of cjsExports) {
this.setExport(exportName, Reflect.get(cjsRecord, exportName));
}
}
// module.exports is the ESM default, matching Node's CJS-from-ESM behavior.
// __esModule is not honored — see Node docs on named exports from CJS.
+1 -1
View File
@@ -2,7 +2,7 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
// needed for WebAssembly, see https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/826
"lib": ["dom"],
"lib": ["es2023", "dom"],
"rootDir": "src",
"outDir": "build"
},
+8 -25
View File
@@ -13622,13 +13622,10 @@ __metadata:
languageName: node
linkType: hard
"ip-address@npm:^9.0.5":
version: 9.0.5
resolution: "ip-address@npm:9.0.5"
dependencies:
jsbn: "npm:1.1.0"
sprintf-js: "npm:^1.1.3"
checksum: 10/1ed81e06721af012306329b31f532b5e24e00cb537be18ddc905a84f19fe8f83a09a1699862bf3a1ec4b9dea93c55a3fa5faf8b5ea380431469df540f38b092c
"ip-address@npm:^10.1.1":
version: 10.2.0
resolution: "ip-address@npm:10.2.0"
checksum: 10/12fec399e1af5753ac322e47a6d81a50d3a528b3abb17c09525b2a2edcaedcca628c40520706f7037bc4d8e951b0296c47e7b86d0a8e6e2335c8f0ba4afcfac1
languageName: node
linkType: hard
@@ -15100,13 +15097,6 @@ __metadata:
languageName: node
linkType: hard
"jsbn@npm:1.1.0":
version: 1.1.0
resolution: "jsbn@npm:1.1.0"
checksum: 10/bebe7ae829bbd586ce8cbe83501dd8cb8c282c8902a8aeeed0a073a89dc37e8103b1244f3c6acd60278bcbfe12d93a3f83c9ac396868a3b3bbc3c5e5e3b648ef
languageName: node
linkType: hard
"jsc-safe-url@npm:^0.2.2":
version: 0.2.4
resolution: "jsc-safe-url@npm:0.2.4"
@@ -21119,12 +21109,12 @@ __metadata:
linkType: hard
"socks@npm:^2.8.3":
version: 2.8.4
resolution: "socks@npm:2.8.4"
version: 2.8.9
resolution: "socks@npm:2.8.9"
dependencies:
ip-address: "npm:^9.0.5"
ip-address: "npm:^10.1.1"
smart-buffer: "npm:^4.2.0"
checksum: 10/ab3af97aeb162f32c80e176c717ccf16a11a6ebb4656a62b94c0f96495ea2a1f4a8206c04b54438558485d83d0c5f61920c07a1a5d3963892a589b40cc6107dd
checksum: 10/8675e6b023faeaa5c2511664b106fd21f5d5ecb403584412e0efae58a76e0c0661c0c18ca340988f6cb398232308dae85fb710f847c9c6e0a6af33b07e4cd33a
languageName: node
linkType: hard
@@ -21279,13 +21269,6 @@ __metadata:
languageName: node
linkType: hard
"sprintf-js@npm:^1.1.3":
version: 1.1.3
resolution: "sprintf-js@npm:1.1.3"
checksum: 10/e7587128c423f7e43cc625fe2f87e6affdf5ca51c1cc468e910d8aaca46bb44a7fbcfa552f787b1d3987f7043aeb4527d1b99559e6621e01b42b3f45e5a24cbb
languageName: node
linkType: hard
"sprintf-js@npm:~1.0.2":
version: 1.0.3
resolution: "sprintf-js@npm:1.0.3"