Skip to content

Commit

Permalink
feat: resolve reexported CJS modules as named ESM exports (#10988)
Browse files Browse the repository at this point in the history
  • Loading branch information
SimenB committed Dec 30, 2020
1 parent 4c4162b commit 596ebff
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -73,6 +73,7 @@
- `[jest-resolve, jest-runtime]` [**BREAKING**] Use `Map`s instead of objects for all cached resources ([#10968](https://github.com/facebook/jest/pull/10968))
- `[jest-runner]` [**BREAKING**] Migrate to ESM ([#10900](https://github.com/facebook/jest/pull/10900))
- `[jest-runtime]` [**BREAKING**] Remove deprecated and unnused `getSourceMapInfo` from Runtime ([#9969](https://github.com/facebook/jest/pull/9969))
- `[jest-runtime]` Detect reexports from CJS as named exports in ESM ([#10988](https://github.com/facebook/jest/pull/10988))
- `[jest-util]` No longer checking `enumerable` when adding `process.domain` ([#10862](https://github.com/facebook/jest/pull/10862))
- `[jest-validate]` [**BREAKING**] Remove `recursiveBlacklist ` option in favor of previously introduced `recursiveDenylist` ([#10650](https://github.com/facebook/jest/pull/10650))

Expand Down
9 changes: 7 additions & 2 deletions e2e/native-esm/__tests__/native-esm.test.js
Expand Up @@ -14,7 +14,7 @@ import {fileURLToPath} from 'url';
import {jest as jestObject} from '@jest/globals';
import staticImportedStatefulFromCjs from '../fromCjs.mjs';
import {double} from '../index';
import defaultFromCjs, {namedFunction} from '../namedExport.cjs';
import defaultFromCjs, {half, namedFunction} from '../namedExport.cjs';
// eslint-disable-next-line import/named
import {bag} from '../namespaceExport.js';
import staticImportedStateful from '../stateful.mjs';
Expand Down Expand Up @@ -139,10 +139,15 @@ test('varies module cache by query', () => {
});

test('supports named imports from CJS', () => {
expect(half(4)).toBe(2);
expect(namedFunction()).toBe('hello from a named CJS function!');
expect(defaultFromCjs.default()).toBe('"default" export');

expect(Object.keys(defaultFromCjs)).toEqual(['namedFunction', 'default']);
expect(Object.keys(defaultFromCjs)).toEqual([
'half',
'namedFunction',
'default',
]);
});

test('supports file urls as imports', async () => {
Expand Down
8 changes: 8 additions & 0 deletions e2e/native-esm/commonjsNamed.cjs
@@ -0,0 +1,8 @@
/**
* 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.half = require('./commonjs.cjs');
1 change: 1 addition & 0 deletions e2e/native-esm/namedExport.cjs
Expand Up @@ -5,5 +5,6 @@
* LICENSE file in the root directory of this source tree.
*/

module.exports = require('./commonjsNamed.cjs');
module.exports.namedFunction = () => 'hello from a named CJS function!';
module.exports.default = () => '"default" export';
54 changes: 39 additions & 15 deletions packages/jest-runtime/src/index.ts
Expand Up @@ -165,7 +165,8 @@ export default class Runtime {
private readonly _moduleMocker: ModuleMocker;
private _isolatedModuleRegistry: ModuleRegistry | null;
private _moduleRegistry: ModuleRegistry;
private readonly _esmoduleRegistry: Map<string, EsmModuleCache>;
private readonly _esmoduleRegistry: Map<Config.Path, EsmModuleCache>;
private readonly _cjsNamedExports: Map<Config.Path, Set<string>>;
private readonly _testPath: Config.Path;
private readonly _resolver: Resolver;
private _shouldAutoMock: boolean;
Expand Down Expand Up @@ -214,6 +215,7 @@ export default class Runtime {
this._isolatedMockRegistry = null;
this._moduleRegistry = new Map();
this._esmoduleRegistry = new Map();
this._cjsNamedExports = new Map();
this._testPath = testPath;
this._resolver = resolver;
this._scriptTransformer = new ScriptTransformer(config, this._cacheFS);
Expand Down Expand Up @@ -524,21 +526,15 @@ export default 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);
const parsedExports = this.getExportsOfCjs(modulePath);

let cjsExports: ReadonlyArray<string> = [];

if (transformedCode) {
const {exports} = parseCjs(transformedCode.code);

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 cjsExports = [...parsedExports].filter(exportName => {
// we don't wanna respect any exports _named_ default as a named export
if (exportName === 'default') {
return false;
}
return Object.hasOwnProperty.call(cjs, exportName);
});

const module = new SyntheticModule(
[...cjsExports, 'default'],
Expand All @@ -556,6 +552,33 @@ export default class Runtime {
return evaluateSyntheticModule(module);
}

private getExportsOfCjs(modulePath: Config.Path) {
const cachedNamedExports = this._cjsNamedExports.get(modulePath);

if (cachedNamedExports) {
return cachedNamedExports;
}

const transformedCode =
this._fileTransforms.get(modulePath)?.code ?? this.readFile(modulePath);

const {exports, reexports} = parseCjs(transformedCode);

const namedExports = new Set(exports);

reexports.forEach(reexport => {
const resolved = this._resolveModule(modulePath, reexport);

const exports = this.getExportsOfCjs(resolved);

exports.forEach(namedExports.add, namedExports);
});

this._cjsNamedExports.set(modulePath, namedExports);

return namedExports;
}

requireModule<T = unknown>(
from: Config.Path,
moduleName?: string,
Expand Down Expand Up @@ -845,6 +868,7 @@ export default class Runtime {
this._mockRegistry.clear();
this._moduleRegistry.clear();
this._esmoduleRegistry.clear();
this._cjsNamedExports.clear();

if (this._environment) {
if (this._environment.global) {
Expand Down

0 comments on commit 596ebff

Please sign in to comment.