Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: experimental ES Modules support #9772

Merged
merged 10 commits into from Apr 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -4,6 +4,7 @@

- `[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
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
@@ -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.
`;
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
@@ -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
@@ -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
@@ -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
@@ -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
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
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
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
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
@@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the issue with falling back to pkg.packageJson.type === 'module'; for non-JS as well?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mostly because it's not how node would do it - it expects explicit loaders. This is wrong, but I think less wrong. This whole thing is gonna be implemented in resolve at some point regardless

// 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?
SimenB marked this conversation as resolved.
Show resolved Hide resolved
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
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
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
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