Skip to content

Commit

Permalink
feat(jest-transform): support transformers written in ESM (#11163)
Browse files Browse the repository at this point in the history
  • Loading branch information
SimenB committed Mar 7, 2021
1 parent 3c679f1 commit ab014c1
Show file tree
Hide file tree
Showing 27 changed files with 321 additions and 238 deletions.
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

0 comments on commit ab014c1

Please sign in to comment.