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 3 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 @@ -20,6 +20,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/7792))
- `[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
11 changes: 11 additions & 0 deletions e2e/__tests__/transform.test.ts
Expand Up @@ -238,3 +238,14 @@ describe('transform-testrunner', () => {
expect(json.numPassedTests).toBe(1);
});
});

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
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
26 changes: 12 additions & 14 deletions packages/jest-reporters/src/generateEmptyCoverage.ts
Expand Up @@ -9,7 +9,7 @@ import type {V8Coverage} from 'collect-v8-coverage';
import * as fs from 'graceful-fs';
import {FileCoverage, createFileCoverage} from 'istanbul-lib-coverage';
import {readInitialCoverage} from 'istanbul-lib-instrument';
import {ScriptTransformer, shouldInstrument} from '@jest/transform';
import {createScriptTransformer, shouldInstrument} from '@jest/transform';
import type {Config} from '@jest/types';

type SingleV8Coverage = V8Coverage[number];
Expand All @@ -24,14 +24,14 @@ export type CoverageWorkerResult =
result: SingleV8Coverage;
};

export default function (
export default async function (
source: string,
filename: Config.Path,
globalConfig: Config.GlobalConfig,
config: Config.ProjectConfig,
changedFiles?: Set<Config.Path>,
sourcesRelatedToTestsInChangedFiles?: Set<Config.Path>,
): CoverageWorkerResult | null {
): Promise<CoverageWorkerResult | null> {
const coverageOptions = {
changedFiles,
collectCoverage: globalConfig.collectCoverage,
Expand Down Expand Up @@ -66,18 +66,16 @@ export default function (
};
}

const scriptTransformer = await createScriptTransformer(config);

// Transform file with instrumentation to make sure initial coverage data is well mapped to original code.
const {code} = new ScriptTransformer(config).transformSource(
filename,
source,
{
instrument: true,
supportsDynamicImport: true,
supportsExportNamespaceFrom: true,
supportsStaticESM: true,
supportsTopLevelAwait: true,
},
);
const {code} = scriptTransformer.transformSource(filename, source, {
instrument: true,
supportsDynamicImport: true,
supportsExportNamespaceFrom: true,
supportsStaticESM: true,
supportsTopLevelAwait: true,
});
// TODO: consider passing AST
const extracted = readInitialCoverage(code);
// Check extracted initial coverage is not null, this can happen when using /* istanbul ignore file */
Expand Down
8 changes: 5 additions & 3 deletions packages/jest-runner/src/runTest.ts
Expand Up @@ -19,7 +19,7 @@ import {
} from '@jest/console';
import type {JestEnvironment} from '@jest/environment';
import type {TestResult} from '@jest/test-result';
import {ScriptTransformer} from '@jest/transform';
import {createScriptTransformer} from '@jest/transform';
import type {Config} from '@jest/types';
import {getTestEnvironment} from 'jest-config';
import * as docblock from 'jest-docblock';
Expand Down Expand Up @@ -103,7 +103,9 @@ async function runTestInternal(
});
}

const transformer = new ScriptTransformer(config);
const cacheFS = new Map([[path, testSource]]);
const transformer = await createScriptTransformer(config, cacheFS);

const TestEnvironment: typeof JestEnvironment = interopRequireDefault(
transformer.requireAndTranspileModule(testEnvironment),
).default;
Expand Down Expand Up @@ -146,13 +148,13 @@ async function runTestInternal(
? new LeakDetector(environment)
: null;

const cacheFS = new Map([[path, testSource]]);
setGlobal(environment.global, 'console', testConsole);

const runtime = new Runtime(
config,
environment,
resolver,
transformer,
cacheFS,
{
changedFiles: context?.changedFiles,
Expand Down