diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c58ed0e1247..df4718fa31dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Features +- `[jest-runtime]` Support named exports from CommonJS as named ES Module imports ([#10673](https://github.com/facebook/jest/pull/10673)) - `[jest-validate]` Add support for `recursiveDenylist` option as an alternative to `recursiveBlacklist` ([#10236](https://github.com/facebook/jest/pull/10236)) ### Fixes diff --git a/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap b/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap index 84c3778f0791..422a1b0e4868 100644 --- a/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap +++ b/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap @@ -2,7 +2,7 @@ exports[`on node ^12.16.0 || >=13.7.0 runs test with native ESM 1`] = ` Test Suites: 1 passed, 1 total -Tests: 14 passed, 14 total +Tests: 15 passed, 15 total Snapshots: 0 total Time: <> Ran all test suites. diff --git a/e2e/native-esm/__tests__/native-esm.test.js b/e2e/native-esm/__tests__/native-esm.test.js index d9c834fb0d7c..6ab61f2a21bf 100644 --- a/e2e/native-esm/__tests__/native-esm.test.js +++ b/e2e/native-esm/__tests__/native-esm.test.js @@ -20,6 +20,7 @@ import staticImportedStatefulWithQuery from '../stateful.mjs?query=1'; import staticImportedStatefulWithAnotherQuery from '../stateful.mjs?query=2'; /* eslint-enable */ import {double} from '../index'; +import defaultFromCjs, {namedFunction} from '../namedExport.cjs'; test('should have correct import.meta', () => { expect(typeof require).toBe('undefined'); @@ -137,3 +138,10 @@ test('varies module cache by query', () => { expect(staticImportedStatefulWithAnotherQuery()).toBe(2); expect(staticImportedStatefulWithAnotherQuery()).toBe(3); }); + +test('supports named imports from CJS', () => { + expect(namedFunction()).toBe('hello from a named CJS function!'); + expect(defaultFromCjs.default()).toBe('"default" export'); + + expect(Object.keys(defaultFromCjs)).toEqual(['namedFunction', 'default']); +}); diff --git a/e2e/native-esm/namedExport.cjs b/e2e/native-esm/namedExport.cjs new file mode 100644 index 000000000000..afd9b3edd813 --- /dev/null +++ b/e2e/native-esm/namedExport.cjs @@ -0,0 +1,9 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +module.exports.namedFunction = () => 'hello from a named CJS function!'; +module.exports.default = () => '"default" export'; diff --git a/packages/jest-runtime/package.json b/packages/jest-runtime/package.json index b4c126c4f14d..04d815eaddcd 100644 --- a/packages/jest-runtime/package.json +++ b/packages/jest-runtime/package.json @@ -20,6 +20,7 @@ "@jest/types": "^26.6.0", "@types/yargs": "^15.0.0", "chalk": "^4.0.0", + "cjs-module-lexer": "^0.4.2", "collect-v8-coverage": "^1.0.0", "exit": "^0.1.2", "glob": "^7.1.3", diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index b19b0dbf1cce..224fe024d25c 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -18,6 +18,8 @@ import { Module as VMModule, } from 'vm'; import * as nativeModule from 'module'; +// @ts-expect-error +import parseCjs = require('cjs-module-lexer'); import type {Config, Global} from '@jest/types'; import type { Jest, @@ -474,9 +476,30 @@ class Runtime { // CJS loaded via `import` should share cache with other CJS: https://github.com/nodejs/modules/issues/503 const cjs = this.requireModuleOrMock(from, modulePath); + const transformedCode = this._fileTransforms.get(modulePath); + + let cjsExports: ReadonlyArray = []; + + if (transformedCode) { + const {exports} = parseCjs(transformedCode.code); + + // @ts-expect-error + cjsExports = exports.filter(exportName => { + // we don't wanna respect any exports _names_ default as a named export + if (exportName === 'default') { + return false; + } + return Object.hasOwnProperty.call(cjs, exportName); + }); + } + const module = new SyntheticModule( - ['default'], + [...cjsExports, 'default'], function () { + cjsExports.forEach(exportName => { + // @ts-expect-error + this.setExport(exportName, cjs[exportName]); + }); // @ts-expect-error: TS doesn't know what `this` is this.setExport('default', cjs); }, diff --git a/yarn.lock b/yarn.lock index 1b9df50b4d17..0c1798af9ea4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5770,6 +5770,13 @@ __metadata: languageName: node linkType: hard +"cjs-module-lexer@npm:^0.4.2": + version: 0.4.2 + resolution: "cjs-module-lexer@npm:0.4.2" + checksum: 2b06d73648a4c1e468f235457205984d0a7f62daaf750fa163c4d6d0965e50998d609bf2b9693aad8b55498aad460a9c2ec758f73eb60b7f988faf0eb54f53b8 + languageName: node + linkType: hard + "class-utils@npm:^0.3.5": version: 0.3.6 resolution: "class-utils@npm:0.3.6" @@ -11850,6 +11857,7 @@ fsevents@^1.2.7: "@types/node": ^14.0.27 "@types/yargs": ^15.0.0 chalk: ^4.0.0 + cjs-module-lexer: ^0.4.2 collect-v8-coverage: ^1.0.0 execa: ^4.0.0 exit: ^0.1.2