Skip to content

Commit 26037fb

Browse files
committed
feat: experimental ES Modules support
1 parent 4caa7b5 commit 26037fb

File tree

17 files changed

+387
-28
lines changed

17 files changed

+387
-28
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
### Features
44

5+
- `[jest-runtime, jest-jasmine2, jest-circus]` Experimental, limited ECMAScript Modules support ([#9772](https://github.com/facebook/jest/pull/9772))
6+
57
### Fixes
68

79
### Chore & Maintenance

e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ FAIL __tests__/index.js
3636
12 | module.exports = () => 'test';
3737
13 |
3838
39-
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:540:17)
39+
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:545:17)
4040
at Object.require (index.js:10:1)
4141
`;
4242

@@ -65,6 +65,6 @@ FAIL __tests__/index.js
6565
12 | module.exports = () => 'test';
6666
13 |
6767
68-
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:540:17)
68+
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:545:17)
6969
at Object.require (index.js:10:1)
7070
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`on node ^12.16.0 || >=13.0.0 runs test with native ESM 1`] = `
4+
Test Suites: 1 passed, 1 total
5+
Tests: 2 passed, 2 total
6+
Snapshots: 0 total
7+
Time: <<REPLACED>>
8+
Ran all test suites.
9+
`;

e2e/__tests__/nativeEsm.test.ts

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import {resolve} from 'path';
9+
import wrap from 'jest-snapshot-serializer-raw';
10+
import {onNodeVersions} from '@jest/test-utils';
11+
import runJest, {getConfig} from '../runJest';
12+
import {extractSummary} from '../Utils';
13+
14+
const DIR = resolve(__dirname, '../native-esm');
15+
16+
test('test config is without transform', () => {
17+
const {configs} = getConfig(DIR);
18+
19+
expect(configs).toHaveLength(1);
20+
expect(configs[0].transform).toEqual([]);
21+
});
22+
23+
// The versions vm.Module was introduced
24+
onNodeVersions('^12.16.0 || >=13.0.0', () => {
25+
test('runs test with native ESM', () => {
26+
const {exitCode, stderr, stdout} = runJest(DIR, [], {
27+
nodeOptions: '--experimental-vm-modules',
28+
});
29+
30+
const {summary} = extractSummary(stderr);
31+
32+
expect(wrap(summary)).toMatchSnapshot();
33+
expect(stdout).toBe('');
34+
expect(exitCode).toBe(0);
35+
});
36+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import {double} from '../index';
9+
10+
test('should have correct import.meta', () => {
11+
expect(typeof jest).toBe('undefined');
12+
expect(import.meta).toEqual({
13+
jest: expect.anything(),
14+
url: expect.any(String),
15+
});
16+
expect(
17+
import.meta.url.endsWith('/e2e/native-esm/__tests__/native-esm.test.js')
18+
).toBe(true);
19+
});
20+
21+
test('should double stuff', () => {
22+
expect(double(1)).toBe(2);
23+
});

e2e/native-esm/index.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
export function double(num) {
9+
return num * 2;
10+
}

e2e/native-esm/package.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "module",
3+
"jest": {
4+
"testEnvironment": "node",
5+
"transform": {}
6+
}
7+
}

packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts

+17-2
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,24 @@ const jestAdapter = async (
7777
}
7878
});
7979

80-
config.setupFilesAfterEnv.forEach(path => runtime.requireModule(path));
80+
for (const path of config.setupFilesAfterEnv) {
81+
const esm = runtime.unstable_shouldLoadAsEsm(path);
82+
83+
if (esm) {
84+
await runtime.unstable_importModule(path);
85+
} else {
86+
runtime.requireModule(path);
87+
}
88+
}
89+
90+
const esm = runtime.unstable_shouldLoadAsEsm(testPath);
91+
92+
if (esm) {
93+
await runtime.unstable_importModule(testPath);
94+
} else {
95+
runtime.requireModule(testPath);
96+
}
8197

82-
runtime.requireModule(testPath);
8398
const results = await runAndTransformResultsToJestFormat({
8499
config,
85100
globalConfig,

packages/jest-jasmine2/src/index.ts

+17-2
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,15 @@ async function jasmine2(
155155
testPath,
156156
});
157157

158-
config.setupFilesAfterEnv.forEach(path => runtime.requireModule(path));
158+
for (const path of config.setupFilesAfterEnv) {
159+
const esm = runtime.unstable_shouldLoadAsEsm(path);
160+
161+
if (esm) {
162+
await runtime.unstable_importModule(path);
163+
} else {
164+
runtime.requireModule(path);
165+
}
166+
}
159167

160168
if (globalConfig.enabledTestsMap) {
161169
env.specFilter = (spec: Spec) => {
@@ -169,7 +177,14 @@ async function jasmine2(
169177
env.specFilter = (spec: Spec) => testNameRegex.test(spec.getFullName());
170178
}
171179

172-
runtime.requireModule(testPath);
180+
const esm = runtime.unstable_shouldLoadAsEsm(testPath);
181+
182+
if (esm) {
183+
await runtime.unstable_importModule(testPath);
184+
} else {
185+
runtime.requireModule(testPath);
186+
}
187+
173188
await env.execute();
174189

175190
const results = await reporter.getResults();

packages/jest-resolve/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"browser-resolve": "^1.11.3",
2222
"chalk": "^3.0.0",
2323
"jest-pnp-resolver": "^1.2.1",
24+
"read-pkg-up": "^7.0.1",
2425
"realpath-native": "^2.0.0",
2526
"resolve": "^1.15.1"
2627
},

packages/jest-resolve/src/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import isBuiltinModule from './isBuiltinModule';
1515
import defaultResolver, {clearDefaultResolverCache} from './defaultResolver';
1616
import type {ResolverConfig} from './types';
1717
import ModuleNotFoundError from './ModuleNotFoundError';
18+
import shouldLoadAsEsm, {clearCachedLookups} from './shouldLoadAsEsm';
1819

1920
type FindNodeModuleConfig = {
2021
basedir: Config.Path;
@@ -100,6 +101,7 @@ class Resolver {
100101

101102
static clearDefaultResolverCache(): void {
102103
clearDefaultResolverCache();
104+
clearCachedLookups();
103105
}
104106

105107
static findNodeModule(
@@ -129,6 +131,9 @@ class Resolver {
129131
return null;
130132
}
131133

134+
// unstable as it should be replaced by https://github.com/nodejs/modules/issues/393, and we don't want people to use it
135+
static unstable_shouldLoadAsEsm = shouldLoadAsEsm;
136+
132137
resolveModuleFromDirIfExists(
133138
dirname: Config.Path,
134139
moduleName: string,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import {dirname, extname} from 'path';
9+
// @ts-ignore: experimental, not added to the types
10+
import {SourceTextModule} from 'vm';
11+
import type {Config} from '@jest/types';
12+
import readPkgUp = require('read-pkg-up');
13+
14+
const runtimeSupportsVmModules = typeof SourceTextModule === 'function';
15+
16+
const cachedLookups = new Map<string, boolean>();
17+
18+
export function clearCachedLookups(): void {
19+
cachedLookups.clear();
20+
}
21+
22+
export default function cachedShouldLoadAsEsm(path: Config.Path): boolean {
23+
let cachedLookup = cachedLookups.get(path);
24+
25+
if (cachedLookup === undefined) {
26+
cachedLookup = shouldLoadAsEsm(path);
27+
cachedLookups.set(path, cachedLookup);
28+
}
29+
30+
return cachedLookup;
31+
}
32+
33+
// this is a bad version of what https://github.com/nodejs/modules/issues/393 would provide
34+
function shouldLoadAsEsm(path: Config.Path): boolean {
35+
if (!runtimeSupportsVmModules) {
36+
return false;
37+
}
38+
39+
const extension = extname(path);
40+
41+
if (extension === '.mjs') {
42+
return true;
43+
}
44+
45+
if (extension === '.cjs') {
46+
return false;
47+
}
48+
49+
// this isn't correct - we might wanna load any file as a module (using synthetic module)
50+
// do we need an option to Jest so people can opt in to ESM for non-js?
51+
if (extension !== '.js') {
52+
return false;
53+
}
54+
55+
const cwd = dirname(path);
56+
57+
// TODO: can we cache lookups somehow?
58+
const pkg = readPkgUp.sync({cwd, normalize: false});
59+
60+
if (!pkg) {
61+
return false;
62+
}
63+
64+
return pkg.packageJson.type === 'module';
65+
}

packages/jest-runner/src/runTest.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,15 @@ async function runTestInternal(
163163

164164
const start = Date.now();
165165

166-
config.setupFiles.forEach(path => runtime!.requireModule(path));
166+
for (const path of config.setupFiles) {
167+
const esm = runtime.unstable_shouldLoadAsEsm(path);
168+
169+
if (esm) {
170+
await runtime.unstable_importModule(path);
171+
} else {
172+
runtime.requireModule(path);
173+
}
174+
}
167175

168176
const sourcemapOptions: sourcemapSupport.Options = {
169177
environment: 'node',

packages/jest-runtime/src/__mocks__/createRuntime.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,15 @@ module.exports = async function createRuntime(filename, config) {
4949
Runtime.createResolver(config, hasteMap.moduleMap),
5050
);
5151

52-
config.setupFiles.forEach(path => runtime.requireModule(path));
52+
for (const path of config.setupFiles) {
53+
const esm = runtime.unstable_shouldLoadAsEsm(path);
54+
55+
if (esm) {
56+
await runtime.unstable_importModule(path);
57+
} else {
58+
runtime.requireModule(path);
59+
}
60+
}
5361

5462
runtime.__mockRootPath = path.join(config.rootDir, 'root.js');
5563
runtime.__mockSubdirPath = path.join(

packages/jest-runtime/src/cli/index.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,22 @@ export async function run(
9393

9494
const runtime = new Runtime(config, environment, hasteMap.resolver);
9595

96-
config.setupFiles.forEach(path => runtime.requireModule(path));
96+
for (const path of config.setupFiles) {
97+
const esm = runtime.unstable_shouldLoadAsEsm(path);
9798

98-
runtime.requireModule(filePath);
99+
if (esm) {
100+
await runtime.unstable_importModule(path);
101+
} else {
102+
runtime.requireModule(path);
103+
}
104+
}
105+
const esm = runtime.unstable_shouldLoadAsEsm(filePath);
106+
107+
if (esm) {
108+
await runtime.unstable_importModule(filePath);
109+
} else {
110+
runtime.requireModule(filePath);
111+
}
99112
} catch (e) {
100113
console.error(chalk.red(e.stack || e));
101114
process.on('exit', () => (process.exitCode = 1));

0 commit comments

Comments
 (0)