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(jest-transform): support transformers written in ESM #11163

Merged
merged 10 commits into from Mar 7, 2021
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -21,6 +21,8 @@
- `[jest-reporters]` Add static filepath property to all reporters ([#11015](https://github.com/facebook/jest/pull/11015))
- `[jest-snapshot]` [**BREAKING**] Make prettier optional for inline snapshots - fall back to string replacement ([#7792](https://github.com/facebook/jest/pull/7792))
- `[jest-transform]` Pass config options defined in Jest's config to transformer's `process` and `getCacheKey` functions ([#10926](https://github.com/facebook/jest/pull/10926))
- `[jest-transform]` Add support for transformers written in ESM ([#11163](https://github.com/facebook/jest/pull/11163))
- `[jest-transform]` [**BREAKING**] Do not export `ScriptTransformer` class, instead export the async function `createScriptTransformer` ([#11163](https://github.com/facebook/jest/pull/11163))
- `[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
18 changes: 2 additions & 16 deletions babel.config.js
Expand Up @@ -35,22 +35,6 @@ module.exports = {
],
test: /\.tsx?$/,
},
// we want this file to keep `import()`, so exclude the transform for it
{
plugins: ['@babel/plugin-syntax-dynamic-import'],
presets: [
'@babel/preset-typescript',
[
'@babel/preset-env',
{
exclude: ['@babel/plugin-proposal-dynamic-import'],
shippedProposals: true,
targets: {node: supportedNodeVersion},
},
],
],
test: 'packages/jest-config/src/readConfigFileAndSetRootDir.ts',
},
],
plugins: [
['@babel/plugin-transform-modules-commonjs', {allowTopLevelThis: true}],
Expand All @@ -63,6 +47,8 @@ module.exports = {
'@babel/preset-env',
{
bugfixes: true,
// a runtime error is preferable, and we need a real `import`
exclude: ['@babel/plugin-proposal-dynamic-import'],
shippedProposals: true,
targets: {node: supportedNodeVersion},
},
Expand Down
2 changes: 2 additions & 0 deletions docs/Configuration.md
Expand Up @@ -1295,6 +1295,8 @@ _Note: a transformer is only run once per file unless the file has changed. Duri

_Note: when adding additional code transformers, this will overwrite the default config and `babel-jest` is no longer automatically loaded. If you want to use it to compile JavaScript or Typescript, it has to be explicitly defined by adding `{"\\.[jt]sx?$": "babel-jest"}` to the transform property. See [babel-jest plugin](https://github.com/facebook/jest/tree/master/packages/babel-jest#setup)_

A transformer must be an object with at least a `process` function, and it's also recommended to include a `getCacheKey` function. If your transformer is written in ESM you should have a default export with that object.

### `transformIgnorePatterns` [array\<string>]

Default: `["/node_modules/", "\\.pnp\\.[^\\\/]+$"]`
Expand Down
14 changes: 14 additions & 0 deletions e2e/__tests__/transform.test.ts
Expand Up @@ -8,6 +8,7 @@
import {tmpdir} from 'os';
import * as path from 'path';
import {wrap} from 'jest-snapshot-serializer-raw';
import {onNodeVersions} from '@jest/test-utils';
import {
cleanup,
copyDir,
Expand Down Expand Up @@ -238,3 +239,16 @@ describe('transform-testrunner', () => {
expect(json.numPassedTests).toBe(1);
});
});

onNodeVersions('^12.17.0 || >=13.2.0', () => {
describe('esm-transformer', () => {
const dir = path.resolve(__dirname, '../transform/esm-transformer');

it('should transform with transformer written in ESM', () => {
const {json, stderr} = runWithJson(dir, ['--no-cache']);
expect(stderr).toMatch(/PASS/);
expect(json.success).toBe(true);
expect(json.numPassedTests).toBe(1);
});
});
});
12 changes: 12 additions & 0 deletions e2e/transform/esm-transformer/__tests__/test.js
@@ -0,0 +1,12 @@
/**
* 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.
*/

const m = require('../module');

test('ESM transformer intercepts', () => {
expect(m).toEqual(42);
});
8 changes: 8 additions & 0 deletions e2e/transform/esm-transformer/module.js
@@ -0,0 +1,8 @@
/**
* 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.
*/

module.exports = 'It was not transformed!!';
22 changes: 22 additions & 0 deletions e2e/transform/esm-transformer/my-transform.mjs
@@ -0,0 +1,22 @@
/**
* 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 {createRequire} from 'module';

const require = createRequire(import.meta.url);

const fileToTransform = require.resolve('./module');

export default {
process(src, filepath) {
if (filepath === fileToTransform) {
return 'module.exports = 42;';
}

return src;
},
};
8 changes: 8 additions & 0 deletions e2e/transform/esm-transformer/package.json
@@ -0,0 +1,8 @@
{
"jest": {
"testEnvironment": "node",
"transform": {
"\\.js$": "<rootDir>/my-transform.mjs"
}
}
}
2 changes: 0 additions & 2 deletions package.json
Expand Up @@ -5,8 +5,6 @@
"devDependencies": {
"@babel/core": "^7.3.4",
"@babel/plugin-proposal-class-properties": "^7.3.4",
"@babel/plugin-proposal-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-modules-commonjs": "^7.1.0",
"@babel/plugin-transform-strict-mode": "^7.0.0",
"@babel/preset-env": "^7.1.0",
Expand Down
36 changes: 19 additions & 17 deletions packages/jest-core/src/TestScheduler.ts
Expand Up @@ -25,7 +25,7 @@ import {
buildFailureTestResult,
makeEmptyAggregatedTestResult,
} from '@jest/test-result';
import {ScriptTransformer} from '@jest/transform';
import {createScriptTransformer} from '@jest/transform';
import type {Config} from '@jest/types';
import {formatExecError} from 'jest-message-util';
import TestRunner, {Test} from 'jest-runner';
Expand Down Expand Up @@ -188,22 +188,24 @@ export default class TestScheduler {

const testRunners: {[key: string]: TestRunner} = Object.create(null);
const contextsByTestRunner = new WeakMap<TestRunner, Context>();
contexts.forEach(context => {
const {config} = context;
if (!testRunners[config.runner]) {
const transformer = new ScriptTransformer(config);
const Runner: typeof TestRunner = interopRequireDefault(
transformer.requireAndTranspileModule(config.runner),
).default;
const runner = new Runner(this._globalConfig, {
changedFiles: this._context?.changedFiles,
sourcesRelatedToTestsInChangedFiles: this._context
?.sourcesRelatedToTestsInChangedFiles,
});
testRunners[config.runner] = runner;
contextsByTestRunner.set(runner, context);
}
});
await Promise.all(
Array.from(contexts).map(async context => {
const {config} = context;
if (!testRunners[config.runner]) {
const transformer = await createScriptTransformer(config);
const Runner: typeof TestRunner = interopRequireDefault(
transformer.requireAndTranspileModule(config.runner),
).default;
const runner = new Runner(this._globalConfig, {
changedFiles: this._context?.changedFiles,
sourcesRelatedToTestsInChangedFiles: this._context
?.sourcesRelatedToTestsInChangedFiles,
});
testRunners[config.runner] = runner;
contextsByTestRunner.set(runner, context);
}
}),
);

const testsByRunner = this._partitionTests(testRunners, tests);

Expand Down
5 changes: 3 additions & 2 deletions packages/jest-core/src/__tests__/watchFileChanges.test.ts
Expand Up @@ -14,6 +14,7 @@ import type {AggregatedResult} from '@jest/test-result';
import {normalize} from 'jest-config';
import type HasteMap from 'jest-haste-map';
import Runtime from 'jest-runtime';
import {interopRequireDefault} from 'jest-util';
import {JestHook} from 'jest-watcher';

describe('Watch mode flows with changed files', () => {
Expand All @@ -31,8 +32,8 @@ describe('Watch mode flows with changed files', () => {
const cacheDirectory = path.resolve(tmpdir(), `tmp${Math.random()}`);
let hasteMapInstance: HasteMap;

beforeEach(async () => {
watch = (await import('../watch')).default;
beforeEach(() => {
watch = interopRequireDefault(require('../watch')).default;
pipe = {write: jest.fn()} as unknown;
stdin = new MockStdin();
rimraf.sync(cacheDirectory);
Expand Down
4 changes: 2 additions & 2 deletions packages/jest-core/src/runGlobalHook.ts
Expand Up @@ -7,7 +7,7 @@

import * as util from 'util';
import pEachSeries = require('p-each-series');
import {ScriptTransformer} from '@jest/transform';
import {createScriptTransformer} from '@jest/transform';
import type {Config} from '@jest/types';
import type {Test} from 'jest-runner';
import {interopRequireDefault} from 'jest-util';
Expand Down Expand Up @@ -45,7 +45,7 @@ export default async ({
: // Fallback to first config
allTests[0].context.config;

const transformer = new ScriptTransformer(projectConfig);
const transformer = await createScriptTransformer(projectConfig);

try {
await transformer.requireAndTranspileModule(modulePath, async m => {
Expand Down
3 changes: 2 additions & 1 deletion packages/jest-create-cache-key-function/package.json
Expand Up @@ -10,7 +10,8 @@
"@jest/types": "^27.0.0-next.3"
},
"devDependencies": {
"@types/node": "*"
"@types/node": "*",
"jest-util": "^27.0.0-next.3"
},
"engines": {
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
Expand Down
Expand Up @@ -5,6 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/

import {interopRequireDefault} from 'jest-util';

let NODE_ENV: string;
let BABEL_ENV: string;

Expand All @@ -20,8 +22,9 @@ afterEach(() => {
process.env.BABEL_ENV = BABEL_ENV;
});

test('creation of a cache key', async () => {
const createCacheKeyFunction = (await import('../index')).default;
test('creation of a cache key', () => {
const createCacheKeyFunction = interopRequireDefault(require('../index'))
.default;
const createCacheKey = createCacheKeyFunction([], ['value']);
const hashA = createCacheKey('test', 'test.js', null, {
config: {},
Expand Down
4 changes: 1 addition & 3 deletions packages/jest-create-cache-key-function/tsconfig.json
Expand Up @@ -4,7 +4,5 @@
"rootDir": "src",
"outDir": "build"
},
"references": [
{"path": "../jest-types"}
]
"references": [{"path": "../jest-types"}, {"path": "../jest-util"}]
}
5 changes: 3 additions & 2 deletions packages/jest-repl/src/cli/runtime-cli.ts
Expand Up @@ -11,7 +11,7 @@ import chalk = require('chalk');
import yargs = require('yargs');
import {CustomConsole} from '@jest/console';
import type {JestEnvironment} from '@jest/environment';
import {ScriptTransformer} from '@jest/transform';
import {createScriptTransformer} from '@jest/transform';
import type {Config} from '@jest/types';
import {deprecationEntries, readConfig} from 'jest-config';
import Runtime from 'jest-runtime';
Expand Down Expand Up @@ -74,7 +74,7 @@ export async function run(
watchman: globalConfig.watchman,
});

const transformer = new ScriptTransformer(config);
const transformer = await createScriptTransformer(config);
const Environment: typeof JestEnvironment = interopRequireDefault(
transformer.requireAndTranspileModule(config.testEnvironment),
).default;
Expand All @@ -91,6 +91,7 @@ export async function run(
config,
environment,
hasteMap.resolver,
transformer,
new Map(),
{
changedFiles: undefined,
Expand Down
2 changes: 1 addition & 1 deletion packages/jest-reporters/src/CoverageWorker.ts
Expand Up @@ -33,7 +33,7 @@ export function worker({
globalConfig,
path,
options,
}: CoverageWorkerData): CoverageWorkerResult | null {
}: CoverageWorkerData): Promise<CoverageWorkerResult | null> {
return generateEmptyCoverage(
fs.readFileSync(path, 'utf8'),
path,
Expand Down
Expand Up @@ -24,7 +24,7 @@ describe('generateEmptyCoverage', () => {
const rootDir = __dirname;
const filepath = path.join(rootDir, './sum.js');

it('generates an empty coverage object for a file without running it', () => {
it('generates an empty coverage object for a file without running it', async () => {
const src = `
throw new Error('this should not be thrown');

Expand All @@ -42,7 +42,7 @@ describe('generateEmptyCoverage', () => {

shouldInstrument.mockReturnValueOnce(true);

const emptyCoverage = generateEmptyCoverage(
const emptyCoverage = await generateEmptyCoverage(
src,
filepath,
makeGlobalConfig(),
Expand Down Expand Up @@ -71,7 +71,7 @@ describe('generateEmptyCoverage', () => {
});
});

it('generates a null coverage result when using /* istanbul ignore file */', () => {
it('generates a null coverage result when using /* istanbul ignore file */', async () => {
const src = `
/* istanbul ignore file */
const a = (b, c) => {
Expand All @@ -86,7 +86,7 @@ describe('generateEmptyCoverage', () => {

shouldInstrument.mockReturnValueOnce(true);

const nullCoverage = generateEmptyCoverage(
const nullCoverage = await generateEmptyCoverage(
src,
filepath,
makeGlobalConfig(),
Expand All @@ -101,7 +101,7 @@ describe('generateEmptyCoverage', () => {
expect(nullCoverage).toBeNull();
});

it('generates a null coverage result when collectCoverage global config is false', () => {
it('generates a null coverage result when collectCoverage global config is false', async () => {
const src = `
const a = (b, c) => {
if (b) {
Expand All @@ -115,7 +115,7 @@ describe('generateEmptyCoverage', () => {

shouldInstrument.mockReturnValueOnce(false);

const nullCoverage = generateEmptyCoverage(
const nullCoverage = await generateEmptyCoverage(
src,
filepath,
makeGlobalConfig(),
Expand Down