Skip to content

Commit aa64672

Browse files
authored
feat: experimental ES Modules support (#9772)
1 parent 2a92e7f commit aa64672

File tree

18 files changed

+457
-31
lines changed

18 files changed

+457
-31
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- `[expect]` Support `async function`s in `toThrow` ([#9817](https://github.com/facebook/jest/pull/9817))
66
- `[jest-console]` Add code frame to `console.error` and `console.warn` ([#9741](https://github.com/facebook/jest/pull/9741))
77
- `[@jest/globals]` New package so Jest's globals can be explicitly imported ([#9801](https://github.com/facebook/jest/pull/9801))
8+
- `[jest-runtime, jest-jasmine2, jest-circus]` Experimental, limited ECMAScript Modules support ([#9772](https://github.com/facebook/jest/pull/9772))
89

910
### Fixes
1011

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 runs test with native ESM 1`] = `
4+
Test Suites: 1 passed, 1 total
5+
Tests: 4 passed, 4 total
6+
Snapshots: 0 total
7+
Time: <<REPLACED>>
8+
Ran all test suites.
9+
`;

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,6 @@ FAIL __tests__/test.js
3737
| ^
3838
9 |
3939
40-
at Resolver.resolveModule (../../packages/jest-resolve/build/index.js:296:11)
40+
at Resolver.resolveModule (../../packages/jest-resolve/build/index.js:299:11)
4141
at Object.require (index.js:8:18)
4242
`;

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', () => {
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,46 @@
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 {readFileSync} from 'fs';
9+
import {dirname, resolve} from 'path';
10+
import {fileURLToPath} from 'url';
11+
import {double} from '../index';
12+
13+
test('should have correct import.meta', () => {
14+
expect(typeof require).toBe('undefined');
15+
expect(typeof jest).toBe('undefined');
16+
expect(import.meta).toEqual({
17+
url: expect.any(String),
18+
});
19+
expect(
20+
import.meta.url.endsWith('/e2e/native-esm/__tests__/native-esm.test.js')
21+
).toBe(true);
22+
});
23+
24+
test('should double stuff', () => {
25+
expect(double(1)).toBe(2);
26+
});
27+
28+
test('should support importing node core modules', () => {
29+
const dir = dirname(fileURLToPath(import.meta.url));
30+
const packageJsonPath = resolve(dir, '../package.json');
31+
32+
expect(JSON.parse(readFileSync(packageJsonPath, 'utf8'))).toEqual({
33+
jest: {
34+
testEnvironment: 'node',
35+
transform: {},
36+
},
37+
type: 'module',
38+
});
39+
});
40+
41+
test('dynamic import should work', async () => {
42+
const {double: importedDouble} = await import('../index');
43+
44+
expect(importedDouble).toBe(double);
45+
expect(importedDouble(1)).toBe(2);
46+
});

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
@@ -76,9 +76,24 @@ const jestAdapter = async (
7676
}
7777
});
7878

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

81-
runtime.requireModule(testPath);
8297
const results = await runAndTransformResultsToJestFormat({
8398
config,
8499
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
"slash": "^3.0.0"

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,78 @@
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 cachedFileLookups = new Map<string, boolean>();
17+
const cachedDirLookups = new Map<string, boolean>();
18+
19+
export function clearCachedLookups(): void {
20+
cachedFileLookups.clear();
21+
cachedDirLookups.clear();
22+
}
23+
24+
export default function cachedShouldLoadAsEsm(path: Config.Path): boolean {
25+
let cachedLookup = cachedFileLookups.get(path);
26+
27+
if (cachedLookup === undefined) {
28+
cachedLookup = shouldLoadAsEsm(path);
29+
cachedFileLookups.set(path, cachedLookup);
30+
}
31+
32+
return cachedLookup;
33+
}
34+
35+
// this is a bad version of what https://github.com/nodejs/modules/issues/393 would provide
36+
function shouldLoadAsEsm(path: Config.Path): boolean {
37+
if (!runtimeSupportsVmModules) {
38+
return false;
39+
}
40+
41+
const extension = extname(path);
42+
43+
if (extension === '.mjs') {
44+
return true;
45+
}
46+
47+
if (extension === '.cjs') {
48+
return false;
49+
}
50+
51+
// this isn't correct - we might wanna load any file as a module (using synthetic module)
52+
// do we need an option to Jest so people can opt in to ESM for non-js?
53+
if (extension !== '.js') {
54+
return false;
55+
}
56+
57+
const cwd = dirname(path);
58+
59+
let cachedLookup = cachedDirLookups.get(cwd);
60+
61+
if (cachedLookup === undefined) {
62+
cachedLookup = cachedPkgCheck(cwd);
63+
cachedFileLookups.set(cwd, cachedLookup);
64+
}
65+
66+
return cachedLookup;
67+
}
68+
69+
function cachedPkgCheck(cwd: Config.Path): boolean {
70+
// TODO: can we cache lookups somehow?
71+
const pkg = readPkgUp.sync({cwd, normalize: false});
72+
73+
if (!pkg) {
74+
return false;
75+
}
76+
77+
return pkg.packageJson.type === 'module';
78+
}

packages/jest-runner/src/runTest.ts

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

157157
const start = Date.now();
158158

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

161169
const sourcemapOptions: sourcemapSupport.Options = {
162170
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)