Skip to content

Commit

Permalink
feat: experimental ES Modules support (#9772)
Browse files Browse the repository at this point in the history
  • Loading branch information
SimenB authored Apr 16, 2020
1 parent 2a92e7f commit aa64672
Show file tree
Hide file tree
Showing 18 changed files with 457 additions and 31 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- `[expect]` Support `async function`s in `toThrow` ([#9817](https://github.com/facebook/jest/pull/9817))
- `[jest-console]` Add code frame to `console.error` and `console.warn` ([#9741](https://github.com/facebook/jest/pull/9741))
- `[@jest/globals]` New package so Jest's globals can be explicitly imported ([#9801](https://github.com/facebook/jest/pull/9801))
- `[jest-runtime, jest-jasmine2, jest-circus]` Experimental, limited ECMAScript Modules support ([#9772](https://github.com/facebook/jest/pull/9772))

### Fixes

Expand Down
4 changes: 2 additions & 2 deletions e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ FAIL __tests__/index.js
12 | module.exports = () => 'test';
13 |
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:540:17)
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:545:17)
at Object.require (index.js:10:1)
`;

Expand Down Expand Up @@ -65,6 +65,6 @@ FAIL __tests__/index.js
12 | module.exports = () => 'test';
13 |
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:540:17)
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:545:17)
at Object.require (index.js:10:1)
`;
9 changes: 9 additions & 0 deletions e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`on node >=12.16.0 runs test with native ESM 1`] = `
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites.
`;
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,6 @@ FAIL __tests__/test.js
| ^
9 |
at Resolver.resolveModule (../../packages/jest-resolve/build/index.js:296:11)
at Resolver.resolveModule (../../packages/jest-resolve/build/index.js:299:11)
at Object.require (index.js:8:18)
`;
36 changes: 36 additions & 0 deletions e2e/__tests__/nativeEsm.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* 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.
*/

import {resolve} from 'path';
import wrap from 'jest-snapshot-serializer-raw';
import {onNodeVersions} from '@jest/test-utils';
import runJest, {getConfig} from '../runJest';
import {extractSummary} from '../Utils';

const DIR = resolve(__dirname, '../native-esm');

test('test config is without transform', () => {
const {configs} = getConfig(DIR);

expect(configs).toHaveLength(1);
expect(configs[0].transform).toEqual([]);
});

// The versions vm.Module was introduced
onNodeVersions('>=12.16.0', () => {
test('runs test with native ESM', () => {
const {exitCode, stderr, stdout} = runJest(DIR, [], {
nodeOptions: '--experimental-vm-modules',
});

const {summary} = extractSummary(stderr);

expect(wrap(summary)).toMatchSnapshot();
expect(stdout).toBe('');
expect(exitCode).toBe(0);
});
});
46 changes: 46 additions & 0 deletions e2e/native-esm/__tests__/native-esm.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* 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.
*/

import {readFileSync} from 'fs';
import {dirname, resolve} from 'path';
import {fileURLToPath} from 'url';
import {double} from '../index';

test('should have correct import.meta', () => {
expect(typeof require).toBe('undefined');
expect(typeof jest).toBe('undefined');
expect(import.meta).toEqual({
url: expect.any(String),
});
expect(
import.meta.url.endsWith('/e2e/native-esm/__tests__/native-esm.test.js')
).toBe(true);
});

test('should double stuff', () => {
expect(double(1)).toBe(2);
});

test('should support importing node core modules', () => {
const dir = dirname(fileURLToPath(import.meta.url));
const packageJsonPath = resolve(dir, '../package.json');

expect(JSON.parse(readFileSync(packageJsonPath, 'utf8'))).toEqual({
jest: {
testEnvironment: 'node',
transform: {},
},
type: 'module',
});
});

test('dynamic import should work', async () => {
const {double: importedDouble} = await import('../index');

expect(importedDouble).toBe(double);
expect(importedDouble(1)).toBe(2);
});
10 changes: 10 additions & 0 deletions e2e/native-esm/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* 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.
*/

export function double(num) {
return num * 2;
}
7 changes: 7 additions & 0 deletions e2e/native-esm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "module",
"jest": {
"testEnvironment": "node",
"transform": {}
}
}
19 changes: 17 additions & 2 deletions packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,24 @@ const jestAdapter = async (
}
});

config.setupFilesAfterEnv.forEach(path => runtime.requireModule(path));
for (const path of config.setupFilesAfterEnv) {
const esm = runtime.unstable_shouldLoadAsEsm(path);

if (esm) {
await runtime.unstable_importModule(path);
} else {
runtime.requireModule(path);
}
}

const esm = runtime.unstable_shouldLoadAsEsm(testPath);

if (esm) {
await runtime.unstable_importModule(testPath);
} else {
runtime.requireModule(testPath);
}

runtime.requireModule(testPath);
const results = await runAndTransformResultsToJestFormat({
config,
globalConfig,
Expand Down
19 changes: 17 additions & 2 deletions packages/jest-jasmine2/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,15 @@ async function jasmine2(
testPath,
});

config.setupFilesAfterEnv.forEach(path => runtime.requireModule(path));
for (const path of config.setupFilesAfterEnv) {
const esm = runtime.unstable_shouldLoadAsEsm(path);

if (esm) {
await runtime.unstable_importModule(path);
} else {
runtime.requireModule(path);
}
}

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

runtime.requireModule(testPath);
const esm = runtime.unstable_shouldLoadAsEsm(testPath);

if (esm) {
await runtime.unstable_importModule(testPath);
} else {
runtime.requireModule(testPath);
}

await env.execute();

const results = await reporter.getResults();
Expand Down
1 change: 1 addition & 0 deletions packages/jest-resolve/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"browser-resolve": "^1.11.3",
"chalk": "^3.0.0",
"jest-pnp-resolver": "^1.2.1",
"read-pkg-up": "^7.0.1",
"realpath-native": "^2.0.0",
"resolve": "^1.15.1",
"slash": "^3.0.0"
Expand Down
5 changes: 5 additions & 0 deletions packages/jest-resolve/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import isBuiltinModule from './isBuiltinModule';
import defaultResolver, {clearDefaultResolverCache} from './defaultResolver';
import type {ResolverConfig} from './types';
import ModuleNotFoundError from './ModuleNotFoundError';
import shouldLoadAsEsm, {clearCachedLookups} from './shouldLoadAsEsm';

type FindNodeModuleConfig = {
basedir: Config.Path;
Expand Down Expand Up @@ -100,6 +101,7 @@ class Resolver {

static clearDefaultResolverCache(): void {
clearDefaultResolverCache();
clearCachedLookups();
}

static findNodeModule(
Expand Down Expand Up @@ -129,6 +131,9 @@ class Resolver {
return null;
}

// unstable as it should be replaced by https://github.com/nodejs/modules/issues/393, and we don't want people to use it
static unstable_shouldLoadAsEsm = shouldLoadAsEsm;

resolveModuleFromDirIfExists(
dirname: Config.Path,
moduleName: string,
Expand Down
78 changes: 78 additions & 0 deletions packages/jest-resolve/src/shouldLoadAsEsm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* 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.
*/

import {dirname, extname} from 'path';
// @ts-ignore: experimental, not added to the types
import {SourceTextModule} from 'vm';
import type {Config} from '@jest/types';
import readPkgUp = require('read-pkg-up');

const runtimeSupportsVmModules = typeof SourceTextModule === 'function';

const cachedFileLookups = new Map<string, boolean>();
const cachedDirLookups = new Map<string, boolean>();

export function clearCachedLookups(): void {
cachedFileLookups.clear();
cachedDirLookups.clear();
}

export default function cachedShouldLoadAsEsm(path: Config.Path): boolean {
let cachedLookup = cachedFileLookups.get(path);

if (cachedLookup === undefined) {
cachedLookup = shouldLoadAsEsm(path);
cachedFileLookups.set(path, cachedLookup);
}

return cachedLookup;
}

// this is a bad version of what https://github.com/nodejs/modules/issues/393 would provide
function shouldLoadAsEsm(path: Config.Path): boolean {
if (!runtimeSupportsVmModules) {
return false;
}

const extension = extname(path);

if (extension === '.mjs') {
return true;
}

if (extension === '.cjs') {
return false;
}

// this isn't correct - we might wanna load any file as a module (using synthetic module)
// do we need an option to Jest so people can opt in to ESM for non-js?
if (extension !== '.js') {
return false;
}

const cwd = dirname(path);

let cachedLookup = cachedDirLookups.get(cwd);

if (cachedLookup === undefined) {
cachedLookup = cachedPkgCheck(cwd);
cachedFileLookups.set(cwd, cachedLookup);
}

return cachedLookup;
}

function cachedPkgCheck(cwd: Config.Path): boolean {
// TODO: can we cache lookups somehow?
const pkg = readPkgUp.sync({cwd, normalize: false});

if (!pkg) {
return false;
}

return pkg.packageJson.type === 'module';
}
10 changes: 9 additions & 1 deletion packages/jest-runner/src/runTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,15 @@ async function runTestInternal(

const start = Date.now();

config.setupFiles.forEach(path => runtime.requireModule(path));
for (const path of config.setupFiles) {
const esm = runtime.unstable_shouldLoadAsEsm(path);

if (esm) {
await runtime.unstable_importModule(path);
} else {
runtime.requireModule(path);
}
}

const sourcemapOptions: sourcemapSupport.Options = {
environment: 'node',
Expand Down
10 changes: 9 additions & 1 deletion packages/jest-runtime/src/__mocks__/createRuntime.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,15 @@ module.exports = async function createRuntime(filename, config) {
Runtime.createResolver(config, hasteMap.moduleMap),
);

config.setupFiles.forEach(path => runtime.requireModule(path));
for (const path of config.setupFiles) {
const esm = runtime.unstable_shouldLoadAsEsm(path);

if (esm) {
await runtime.unstable_importModule(path);
} else {
runtime.requireModule(path);
}
}

runtime.__mockRootPath = path.join(config.rootDir, 'root.js');
runtime.__mockSubdirPath = path.join(
Expand Down
17 changes: 15 additions & 2 deletions packages/jest-runtime/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,22 @@ export async function run(

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

config.setupFiles.forEach(path => runtime.requireModule(path));
for (const path of config.setupFiles) {
const esm = runtime.unstable_shouldLoadAsEsm(path);

runtime.requireModule(filePath);
if (esm) {
await runtime.unstable_importModule(path);
} else {
runtime.requireModule(path);
}
}
const esm = runtime.unstable_shouldLoadAsEsm(filePath);

if (esm) {
await runtime.unstable_importModule(filePath);
} else {
runtime.requireModule(filePath);
}
} catch (e) {
console.error(chalk.red(e.stack || e));
process.on('exit', () => (process.exitCode = 1));
Expand Down
Loading

0 comments on commit aa64672

Please sign in to comment.