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: requireAndTranspileModule support ESM #11232

Merged
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Expand Up @@ -18,15 +18,18 @@
- `[jest-core]` Add support for `testSequencer` written in ESM ([#11207](https://github.com/facebook/jest/pull/11207))
- `[jest-core]` Add support for `globalSetup` and `globalTeardown` written in ESM ([#11267](https://github.com/facebook/jest/pull/11267))
- `[jest-core]` Add support for `watchPlugins` written in ESM ([#11315](https://github.com/facebook/jest/pull/11315))
- `[jest-core]` Add support for `runner` written in ESM ([#11232](https://github.com/facebook/jest/pull/11232))
- `[jest-environment-node]` Add AbortController to globals ([#11182](https://github.com/facebook/jest/pull/11182))
- `[@jest/fake-timers]` Update to `@sinonjs/fake-timers` to v7 ([#11198](https://github.com/facebook/jest/pull/11198))
- `[jest-haste-map]` Handle injected scm clocks ([#10966](https://github.com/facebook/jest/pull/10966))
- `[jest-haste-map]` Add `enableSymlinks` configuration option to follow symlinks for test files ([#9351](https://github.com/facebook/jest/pull/9351))
- `[jest-repl, jest-runner]` [**BREAKING**] Run transforms over environment ([#8751](https://github.com/facebook/jest/pull/8751))
- `[jest-repl]` Add support for `testEnvironment` written in ESM ([#11232](https://github.com/facebook/jest/pull/11232))
- `[jest-runner]` [**BREAKING**] set exit code to 1 if test logs after teardown ([#10728](https://github.com/facebook/jest/pull/10728))
- `[jest-runner]` [**BREAKING**] Run transforms over `runnner` ([#8823](https://github.com/facebook/jest/pull/8823))
- `[jest-runner]` [**BREAKING**] Run transforms over `testRunnner` ([#8823](https://github.com/facebook/jest/pull/8823))
- `[jest-runner]` Possibility to use ESM for test environment ([11033](https://github.com/facebook/jest/pull/11033))
- `[jest-runner]` Add support for `testRunner` written in ESM ([#11232](https://github.com/facebook/jest/pull/11232))
- `[jest-runtime]` Detect reexports from CJS as named exports in ESM ([#10988](https://github.com/facebook/jest/pull/10988))
- `[jest-runtime]` Support for async code transformations ([#11191](https://github.com/facebook/jest/pull/11191) & [#11220](https://github.com/facebook/jest/pull/11220))
- `[jest-reporters]` Add static filepath property to all reporters ([#11015](https://github.com/facebook/jest/pull/11015))
Expand All @@ -37,6 +40,10 @@
- `[jest-transform]` [**BREAKING**] Do not export `ScriptTransformer` class, instead export the async function `createScriptTransformer` ([#11163](https://github.com/facebook/jest/pull/11163))
- `[jest-transform]` Async code transformations ([#9889](https://github.com/facebook/jest/pull/9889))
- `[jest-transform]` Support transpiled transformers ([#11193](https://github.com/facebook/jest/pull/11193))
- `[jest-transform]` [**BREAKING**] `requireAndTranspileModule` always return a `Promise`, and the third parameter type is changed to `RequireAndTranspileModuleOptions` which accept `applyInteropRequireDefault` option ([#11232](https://github.com/facebook/jest/pull/11232))
- `[jest-transform]` [**BREAKING**] `createTranspilingRequire` return function which return a `Promise` now ([#11232](https://github.com/facebook/jest/pull/11232))
- `[jest-util]` add requireOrImportModule for importing CJS or ESM ([#11199](https://github.com/facebook/jest/pull/11199))
- `[jest-util]` add `applyInteropRequireDefault` option on `requireOrImportModule` ([#11232](https://github.com/facebook/jest/pull/11232))
- `[jest-watcher]` Added support for clearing the line when `<C-u>` is pressed in a watch mode pattern prompt ([#11358](https://github.com/facebook/jest/pull/11358))
- `[jest-worker]` Add support for custom task queues and adds a `PriorityQueue` implementation. ([#10921](https://github.com/facebook/jest/pull/10921))
- `[jest-worker]` Add in-order scheduling policy to jest worker ([10902](https://github.com/facebook/jest/pull/10902))
Expand Down
3 changes: 3 additions & 0 deletions e2e/__tests__/globalSetup.test.ts
Expand Up @@ -27,6 +27,7 @@ const customTransformDIR = path.join(
const nodeModulesDIR = path.join(tmpdir(), 'jest-global-setup-node-modules');
const rejectionDir = path.join(tmpdir(), 'jest-global-setup-rejection');
const e2eDir = path.resolve(__dirname, '../global-setup');
const esmTmpDir = path.join(tmpdir(), 'jest-global-setup-esm');
Copy link
Member

Choose a reason for hiding this comment

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

is this (and the one in teardown) used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, the directory is created in setup file.

Copy link
Member

Choose a reason for hiding this comment

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

sure, but what is that directory used for? In the test it looks like it's created and deleted but never used

Copy link
Contributor Author

Choose a reason for hiding this comment

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

GlobalSetup/Teardown will read this directory and do some assertion.
If we don't clean directory before testing, it will failed in second round.

Copy link
Member

Choose a reason for hiding this comment

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

even after 884c7e0 (#11232)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes.
It fix #11267 globalSetup/globalTeardown e2e test.

Copy link
Member

Choose a reason for hiding this comment

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

Aha, gotcha!


beforeAll(() => {
runYarnInstall(e2eDir);
Expand All @@ -39,6 +40,7 @@ beforeEach(() => {
cleanup(customTransformDIR);
cleanup(nodeModulesDIR);
cleanup(rejectionDir);
cleanup(esmTmpDir);
});

afterAll(() => {
Expand All @@ -48,6 +50,7 @@ afterAll(() => {
cleanup(customTransformDIR);
cleanup(nodeModulesDIR);
cleanup(rejectionDir);
cleanup(esmTmpDir);
});

test('globalSetup is triggered once before all test suites', () => {
Expand Down
3 changes: 3 additions & 0 deletions e2e/__tests__/globalTeardown.test.ts
Expand Up @@ -17,6 +17,7 @@ const DIR = path.join(tmpdir(), 'jest-global-teardown');
const project1DIR = path.join(tmpdir(), 'jest-global-teardown-project-1');
const project2DIR = path.join(tmpdir(), 'jest-global-teardown-project-2');
const e2eDir = path.resolve(__dirname, '../global-teardown');
const esmTmpDir = path.join(tmpdir(), 'jest-global-teardown-esm');

beforeAll(() => {
runYarnInstall(e2eDir);
Expand All @@ -26,11 +27,13 @@ beforeEach(() => {
cleanup(DIR);
cleanup(project1DIR);
cleanup(project2DIR);
cleanup(esmTmpDir);
});
afterAll(() => {
cleanup(DIR);
cleanup(project1DIR);
cleanup(project2DIR);
cleanup(esmTmpDir);
});

test('globalTeardown is triggered once after all test suites', () => {
Expand Down
29 changes: 29 additions & 0 deletions e2e/__tests__/transform.test.ts
Expand Up @@ -315,4 +315,33 @@ onNodeVersions('^12.17.0 || >=13.2.0', () => {
expect(json.numPassedTests).toBe(1);
});
});

describe('transform-esm-runner', () => {
const dir = path.resolve(__dirname, '../transform/transform-esm-runner');
test('runs test with native ESM', () => {
const {json, stderr} = runWithJson(dir, ['--no-cache'], {
nodeOptions: '--experimental-vm-modules',
});

expect(stderr).toMatch(/PASS/);
expect(json.success).toBe(true);
expect(json.numPassedTests).toBe(1);
});
});

describe('transform-esm-testrunner', () => {
const dir = path.resolve(
__dirname,
'../transform/transform-esm-testrunner',
);
test('runs test with native ESM', () => {
const {json, stderr} = runWithJson(dir, ['--no-cache'], {
nodeOptions: '--experimental-vm-modules',
});

expect(stderr).toMatch(/PASS/);
expect(json.success).toBe(true);
expect(json.numPassedTests).toBe(1);
});
});
});
1 change: 0 additions & 1 deletion e2e/test-environment-async/__tests__/custom.test.js
Expand Up @@ -6,7 +6,6 @@
*
*/
'use strict';
/* eslint-env browser*/

test('setup', () => {
expect(global.setup).toBe('setup');
Expand Down
10 changes: 10 additions & 0 deletions e2e/transform/transform-esm-runner/__tests__/add.test.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.
*/

it('should add two numbers', () => {
expect(1 + 1).toBe(2);
});
7 changes: 7 additions & 0 deletions e2e/transform/transform-esm-runner/package.json
@@ -0,0 +1,7 @@
{
"type": "module",
"jest": {
"rootDir": "./",
"runner": "<rootDir>/runner.mjs"
}
}
48 changes: 48 additions & 0 deletions e2e/transform/transform-esm-runner/runner.mjs
@@ -0,0 +1,48 @@
/**
* 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 testResult from '@jest/test-result';

const {createEmptyTestResult} = testResult;
Comment on lines +8 to +10
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
import testResult from '@jest/test-result';
const {createEmptyTestResult} = testResult;
import {createEmptyTestResult} from '@jest/test-result';

does this work?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No.
Because ts will compile to common js modules, and export using Object.defineProperty with getter.
ESM named import from CJS module only support exports.name = value way. esm commonjs namespaces

CI error log

Choose a reason for hiding this comment

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

Tested. It worked.


export default class BaseTestRunner {
constructor(globalConfig, context) {
this._globalConfig = globalConfig;
this._context = context || {};
}

async runTests(tests, watcher, onStart, onResult, onFailure) {
return tests.reduce(
(promise, test) =>
promise
.then(async () => {
await onStart(test);
return {
...createEmptyTestResult(),
numPassingTests: 1,
testFilePath: test.path,
testResults: [
{
ancestorTitles: [],
duration: 2,
failureDetails: [],
failureMessages: [],
fullName: 'sample test',
location: null,
numPassingAsserts: 1,
status: 'passed',
title: 'sample test',
},
],
};
})
.then(result => onResult(test, result))
.catch(err => onFailure(test, err)),
Promise.resolve(),
);
}
}
10 changes: 10 additions & 0 deletions e2e/transform/transform-esm-testrunner/__tests__/add.test.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.
*/

it('should add two numbers', () => {
expect(1 + 1).toBe(2);
});
7 changes: 7 additions & 0 deletions e2e/transform/transform-esm-testrunner/package.json
@@ -0,0 +1,7 @@
{
"type": "module",
"jest": {
"rootDir": "./",
"testRunner": "<rootDir>/test-runner.mjs"
}
}
35 changes: 35 additions & 0 deletions e2e/transform/transform-esm-testrunner/test-runner.mjs
@@ -0,0 +1,35 @@
/**
* 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 testResult from '@jest/test-result';

const {createEmptyTestResult} = testResult;

export default async function testRunner(
globalConfig,
config,
environment,
runtime,
testPath,
) {
return {
...createEmptyTestResult(),
numPassingTests: 1,
testFilePath: testPath,
testResults: [
{
ancestorTitles: [],
duration: 2,
failureMessages: [],
fullName: 'sample test',
location: null,
numPassingAsserts: 1,
status: 'passed',
title: 'sample test',
},
],
};
}
6 changes: 3 additions & 3 deletions packages/jest-core/src/TestScheduler.ts
Expand Up @@ -203,9 +203,9 @@ export default class TestScheduler {
const {config} = context;
if (!testRunners[config.runner]) {
const transformer = await createScriptTransformer(config);
const Runner: typeof TestRunner = interopRequireDefault(
transformer.requireAndTranspileModule(config.runner),
).default;
const Runner: typeof TestRunner = await transformer.requireAndTranspileModule(
config.runner,
);
const runner = new Runner(this._globalConfig, {
changedFiles: this._context?.changedFiles,
sourcesRelatedToTestsInChangedFiles: this._context
Expand Down
70 changes: 23 additions & 47 deletions packages/jest-core/src/runGlobalHook.ts
Expand Up @@ -5,14 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/

import {pathToFileURL} from 'url';
import * as util from 'util';
import pEachSeries = require('p-each-series');
import {createScriptTransformer} from '@jest/transform';
import type {Config} from '@jest/types';
import type {Test} from 'jest-runner';
import {interopRequireDefault} from 'jest-util';
import {format as prettyFormat} from 'pretty-format';
import prettyFormat from 'pretty-format';

export default async ({
allTests,
Expand Down Expand Up @@ -49,53 +47,31 @@ export default async ({
const transformer = await createScriptTransformer(projectConfig);

try {
await transformer.requireAndTranspileModule(modulePath, async m => {
const globalModule = interopRequireDefault(m).default;

if (typeof globalModule !== 'function') {
throw new TypeError(
`${moduleName} file must export a function at ${modulePath}`,
);
}

await globalModule(globalConfig);
});
await transformer.requireAndTranspileModule(
modulePath,
async globalModule => {
if (typeof globalModule !== 'function') {
throw new TypeError(
`${moduleName} file must export a function at ${modulePath}`,
);
}

await globalModule(globalConfig);
},
);
} catch (error) {
if (error && error.code === 'ERR_REQUIRE_ESM') {
const configUrl = pathToFileURL(modulePath);

// node `import()` supports URL, but TypeScript doesn't know that
const importedConfig = await import(configUrl.href);

if (!importedConfig.default) {
throw new Error(
`Jest: Failed to load ESM transformer at ${modulePath} - did you use a default export?`,
);
}

const globalModule = importedConfig.default;
if (util.types.isNativeError(error)) {
error.message = `Jest: Got error running ${moduleName} - ${modulePath}, reason: ${error.message}`;

if (typeof globalModule !== 'function') {
throw new TypeError(
`${moduleName} file must export a function at ${modulePath}`,
);
}

await globalModule(globalConfig);
} else {
if (util.types.isNativeError(error)) {
error.message = `Jest: Got error running ${moduleName} - ${modulePath}, reason: ${error.message}`;

throw error;
}

throw new Error(
`Jest: Got error running ${moduleName} - ${modulePath}, reason: ${prettyFormat(
error,
{maxDepth: 3},
)}`,
);
throw error;
}

throw new Error(
`Jest: Got error running ${moduleName} - ${modulePath}, reason: ${prettyFormat(
error,
{maxDepth: 3},
)}`,
);
}
});
}
Expand Down
9 changes: 5 additions & 4 deletions packages/jest-repl/src/cli/runtime-cli.ts
Expand Up @@ -15,7 +15,7 @@ import {createScriptTransformer} from '@jest/transform';
import type {Config} from '@jest/types';
import {deprecationEntries, readConfig} from 'jest-config';
import Runtime from 'jest-runtime';
import {interopRequireDefault, setGlobal, tryRealpath} from 'jest-util';
import {setGlobal, tryRealpath} from 'jest-util';
import {validateCLIOptions} from 'jest-validate';
import * as args from './args';
import {VERSION} from './version';
Expand Down Expand Up @@ -75,9 +75,10 @@ export async function run(
});

const transformer = await createScriptTransformer(config);
const Environment: typeof JestEnvironment = interopRequireDefault(
transformer.requireAndTranspileModule(config.testEnvironment),
).default;
const Environment: typeof JestEnvironment = await transformer.requireAndTranspileModule(
config.testEnvironment,
);

const environment = new Environment(config);
setGlobal(
environment.global,
Expand Down