diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a098e0daece..144909b5c314 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ - `[expect]`: Migrate to TypeScript ([#7919](https://github.com/facebook/jest/pull/7919)) - `[jest-circus]`: Migrate to TypeScript ([#7916](https://github.com/facebook/jest/pull/7916)) - `[jest-phabricator]`: Migrate to TypeScript ([#7965](https://github.com/facebook/jest/pull/7965)) +- `[jest-runner]`: Migrate to TypeScript ([#7968](https://github.com/facebook/jest/pull/7968)) ### Performance diff --git a/packages/jest-runner/package.json b/packages/jest-runner/package.json index 98ecf80ca1e7..67cd2369cb63 100644 --- a/packages/jest-runner/package.json +++ b/packages/jest-runner/package.json @@ -8,7 +8,9 @@ }, "license": "MIT", "main": "build/index.js", + "types": "build/index.d.ts", "dependencies": { + "@jest/types": "^24.1.0", "chalk": "^2.4.2", "exit": "^0.1.2", "graceful-fs": "^4.1.15", @@ -18,6 +20,7 @@ "jest-jasmine2": "^24.1.0", "jest-leak-detector": "^24.0.0", "jest-message-util": "^24.0.0", + "jest-resolve": "^24.1.0", "jest-runtime": "^24.1.0", "jest-util": "^24.0.0", "jest-worker": "^24.0.0", diff --git a/packages/jest-runner/src/__tests__/testRunner.test.js b/packages/jest-runner/src/__tests__/testRunner.test.js index 9601969f012d..ca604b10c0ee 100644 --- a/packages/jest-runner/src/__tests__/testRunner.test.js +++ b/packages/jest-runner/src/__tests__/testRunner.test.js @@ -7,7 +7,6 @@ */ import {TestWatcher} from '@jest/core'; -// eslint-disable-next-line import/default import TestRunner from '../index'; let mockWorkerFarm; diff --git a/packages/jest-runner/src/index.js b/packages/jest-runner/src/index.ts similarity index 83% rename from packages/jest-runner/src/index.js rename to packages/jest-runner/src/index.ts index 538978903fd8..e50e94fcf8e3 100644 --- a/packages/jest-runner/src/index.js +++ b/packages/jest-runner/src/index.ts @@ -3,12 +3,15 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - * - * @flow */ -import type {GlobalConfig} from 'types/Config'; -import type { +import {Config, TestResult} from '@jest/types'; +import exit from 'exit'; +import throat from 'throat'; +import Worker from 'jest-worker'; +import runTest from './runTest'; +import {worker} from './testWorker'; +import { OnTestFailure, OnTestStart, OnTestSuccess, @@ -16,24 +19,20 @@ import type { TestRunnerContext, TestRunnerOptions, TestWatcher, -} from 'types/TestRunner'; - -import typeof {worker} from './testWorker'; - -import exit from 'exit'; -import runTest from './runTest'; -import throat from 'throat'; -import Worker from 'jest-worker'; + WatcherState, +} from './types'; const TEST_WORKER_PATH = require.resolve('./testWorker'); -type WorkerInterface = Worker & {worker: worker}; +interface WorkerInterface extends Worker { + worker: typeof worker; +} class TestRunner { - _globalConfig: GlobalConfig; - _context: TestRunnerContext; + private _globalConfig: Config.GlobalConfig; + private _context: TestRunnerContext; - constructor(globalConfig: GlobalConfig, context?: TestRunnerContext) { + constructor(globalConfig: Config.GlobalConfig, context?: TestRunnerContext) { this._globalConfig = globalConfig; this._context = context || {}; } @@ -57,7 +56,7 @@ class TestRunner { )); } - async _createInBandTestRun( + private async _createInBandTestRun( tests: Array, watcher: TestWatcher, onStart: OnTestStart, @@ -91,19 +90,19 @@ class TestRunner { ); } - async _createParallelTestRun( + private async _createParallelTestRun( tests: Array, watcher: TestWatcher, onStart: OnTestStart, onResult: OnTestSuccess, onFailure: OnTestFailure, ) { - const worker: WorkerInterface = new Worker(TEST_WORKER_PATH, { + const worker = new Worker(TEST_WORKER_PATH, { exposedMethods: ['worker'], forkOptions: {stdio: 'pipe'}, maxRetries: 3, numWorkers: this._globalConfig.maxWorkers, - }); + }) as WorkerInterface; if (worker.getStdout()) worker.getStdout().pipe(process.stdout); if (worker.getStderr()) worker.getStderr().pipe(process.stderr); @@ -112,7 +111,7 @@ class TestRunner { // Send test suites to workers continuously instead of all at once to track // the start time of individual tests. - const runTestInWorker = test => + const runTestInWorker = (test: Test) => mutex(async () => { if (watcher.isInterrupted()) { return Promise.reject(); @@ -131,7 +130,7 @@ class TestRunner { }); }); - const onError = async (err, test) => { + const onError = async (err: TestResult.SerializableError, test: Test) => { await onFailure(test, err); if (err.type === 'ProcessTerminatedError') { console.error( @@ -143,7 +142,7 @@ class TestRunner { }; const onInterrupt = new Promise((_, reject) => { - watcher.on('change', state => { + watcher.on('change', (state: WatcherState) => { if (state.interrupted) { reject(new CancelRun()); } @@ -164,10 +163,10 @@ class TestRunner { } class CancelRun extends Error { - constructor(message: ?string) { + constructor(message?: string) { super(message); this.name = 'CancelRun'; } } -module.exports = TestRunner; +export = TestRunner; diff --git a/packages/jest-runner/src/runTest.js b/packages/jest-runner/src/runTest.ts similarity index 78% rename from packages/jest-runner/src/runTest.js rename to packages/jest-runner/src/runTest.ts index 88fe237666cb..7b581863bb2a 100644 --- a/packages/jest-runner/src/runTest.js +++ b/packages/jest-runner/src/runTest.ts @@ -4,16 +4,16 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @flow */ -import type {EnvironmentClass} from 'types/Environment'; -import type {GlobalConfig, Path, ProjectConfig} from 'types/Config'; -import type {Resolver} from 'types/Resolve'; -import type {TestFramework, TestRunnerContext} from 'types/TestRunner'; -import type {TestResult} from 'types/TestResult'; -import type RuntimeClass from 'jest-runtime'; - +import { + Environment, + Config, + TestResult, + Console as ConsoleType, +} from '@jest/types'; +// @ts-ignore: not migrated to TS +import RuntimeClass from 'jest-runtime'; import fs from 'graceful-fs'; import { BufferedConsole, @@ -24,22 +24,31 @@ import { setGlobal, } from 'jest-util'; import LeakDetector from 'jest-leak-detector'; +import Resolver from 'jest-resolve'; +// @ts-ignore: not migrated to TS import {getTestEnvironment} from 'jest-config'; import * as docblock from 'jest-docblock'; import {formatExecError} from 'jest-message-util'; -import sourcemapSupport from 'source-map-support'; +import sourcemapSupport, { + Options as SourceMapOptions, +} from 'source-map-support'; import chalk from 'chalk'; +import {TestFramework, TestRunnerContext} from './types'; type RunTestInternalResult = { - leakDetector: ?LeakDetector, - result: TestResult, + leakDetector: LeakDetector | null; + result: TestResult.TestResult; }; function freezeConsole( + // @ts-ignore: Correct types when `jest-util` is ESM testConsole: BufferedConsole | Console | NullConsole, - config: ProjectConfig, + config: Config.ProjectConfig, ) { - testConsole._log = function fakeConsolePush(_type, message) { + testConsole._log = function fakeConsolePush( + _type: ConsoleType.LogType, + message: ConsoleType.LogMessage, + ) { const error = new ErrorWithStack( `${chalk.red( `${chalk.bold( @@ -73,11 +82,11 @@ function freezeConsole( // references to verify if there is a leak, which is not maintainable and error // prone. That's why "runTestInternal" CANNOT be inlined inside "runTest". async function runTestInternal( - path: Path, - globalConfig: GlobalConfig, - config: ProjectConfig, + path: Config.Path, + globalConfig: Config.GlobalConfig, + config: Config.ProjectConfig, resolver: Resolver, - context: ?TestRunnerContext, + context?: TestRunnerContext, ): Promise { const testSource = fs.readFileSync(path, 'utf8'); const parsedDocblock = docblock.parse(docblock.extract(testSource)); @@ -92,21 +101,22 @@ async function runTestInternal( }); } - /* $FlowFixMe */ - const TestEnvironment = (require(testEnvironment): EnvironmentClass); - const testFramework = ((process.env.JEST_CIRCUS === '1' - ? require('jest-circus/runner') // eslint-disable-line import/no-extraneous-dependencies - : /* $FlowFixMe */ - require(config.testRunner)): TestFramework); - const Runtime = ((config.moduleLoader - ? /* $FlowFixMe */ - require(config.moduleLoader) - : require('jest-runtime')): Class); + const TestEnvironment: Environment.EnvironmentClass = require(testEnvironment); + const testFramework: TestFramework = + process.env.JEST_CIRCUS === '1' + ? require('jest-circus/runner') // eslint-disable-line import/no-extraneous-dependencies + : require(config.testRunner); + const Runtime: RuntimeClass = config.moduleLoader + ? require(config.moduleLoader) + : require('jest-runtime'); - let runtime = undefined; + let runtime: RuntimeClass = undefined; const consoleOut = globalConfig.useStderr ? process.stderr : process.stdout; - const consoleFormatter = (type, message) => + const consoleFormatter = ( + type: ConsoleType.LogType, + message: ConsoleType.LogMessage, + ) => getConsoleOutput( config.cwd, !!globalConfig.verbose, @@ -150,7 +160,7 @@ async function runTestInternal( const start = Date.now(); - const sourcemapOptions = { + const sourcemapOptions: SourceMapOptions = { environment: 'node', handleUncaughtExceptions: false, retrieveSourceMap: source => { @@ -160,7 +170,7 @@ async function runTestInternal( if (sourceMapSource) { try { return { - map: JSON.parse(fs.readFileSync(sourceMapSource)), + map: JSON.parse(fs.readFileSync(sourceMapSource, 'utf8')), url: source, }; } catch (e) {} @@ -187,7 +197,7 @@ async function runTestInternal( ) { const realExit = environment.global.process.exit; - environment.global.process.exit = function exit(...args) { + environment.global.process.exit = function exit(...args: Array) { const error = new ErrorWithStack( `process.exit called with "${args.join(', ')}"`, exit, @@ -210,7 +220,7 @@ async function runTestInternal( try { await environment.setup(); - let result: TestResult; + let result: TestResult.TestResult; try { result = await testFramework( @@ -259,17 +269,18 @@ async function runTestInternal( } finally { await environment.teardown(); + // @ts-ignore: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/33351 sourcemapSupport.resetRetrieveHandlers(); } } export default async function runTest( - path: Path, - globalConfig: GlobalConfig, - config: ProjectConfig, + path: Config.Path, + globalConfig: Config.GlobalConfig, + config: Config.ProjectConfig, resolver: Resolver, - context: ?TestRunnerContext, -): Promise { + context?: TestRunnerContext, +): Promise { const {leakDetector, result} = await runTestInternal( path, globalConfig, diff --git a/packages/jest-runner/src/testWorker.js b/packages/jest-runner/src/testWorker.ts similarity index 73% rename from packages/jest-runner/src/testWorker.js rename to packages/jest-runner/src/testWorker.ts index 1651803861e6..51c8fbc3dd1c 100644 --- a/packages/jest-runner/src/testWorker.js +++ b/packages/jest-runner/src/testWorker.ts @@ -4,28 +4,24 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @flow */ -import type {GlobalConfig, Path, ProjectConfig} from 'types/Config'; -import type {SerializableError, TestResult} from 'types/TestResult'; -import type {SerializableModuleMap} from 'types/HasteMap'; -import type {ErrorWithCode} from 'types/Errors'; -import type {TestRunnerContext} from 'types/TestRunner'; - +import {Config, TestResult} from '@jest/types'; +import HasteMap, {SerializableModuleMap, ModuleMap} from 'jest-haste-map'; import exit from 'exit'; -import HasteMap from 'jest-haste-map'; import {separateMessageFromStack} from 'jest-message-util'; +// @ts-ignore: Not migrated to TS import Runtime from 'jest-runtime'; +import {ErrorWithCode, TestRunnerContext} from './types'; import runTest from './runTest'; -export type WorkerData = {| - config: ProjectConfig, - globalConfig: GlobalConfig, - path: Path, - serializableModuleMap: ?SerializableModuleMap, - context?: TestRunnerContext, -|}; +type WorkerData = { + config: Config.ProjectConfig; + globalConfig: Config.GlobalConfig; + path: Config.Path; + serializableModuleMap: SerializableModuleMap | null; + context?: TestRunnerContext; +}; // Make sure uncaught errors are logged before we exit. process.on('uncaughtException', err => { @@ -33,7 +29,9 @@ process.on('uncaughtException', err => { exit(1); }); -const formatError = (error: string | ErrorWithCode): SerializableError => { +const formatError = ( + error: string | ErrorWithCode, +): TestResult.SerializableError => { if (typeof error === 'string') { const {message, stack} = separateMessageFromStack(error); return { @@ -52,7 +50,10 @@ const formatError = (error: string | ErrorWithCode): SerializableError => { }; const resolvers = Object.create(null); -const getResolver = (config, moduleMap) => { +const getResolver = ( + config: Config.ProjectConfig, + moduleMap: ModuleMap | null, +) => { // In watch mode, the raw module map with all haste modules is passed from // the test runner to the watch command. This is because jest-haste-map's // watch mode does not persist the haste map on disk after every file change. @@ -77,7 +78,7 @@ export async function worker({ path, serializableModuleMap, context, -}: WorkerData): Promise { +}: WorkerData): Promise { try { const moduleMap = serializableModuleMap ? HasteMap.ModuleMap.fromJSON(serializableModuleMap) diff --git a/packages/jest-runner/src/types.ts b/packages/jest-runner/src/types.ts new file mode 100644 index 000000000000..5f1aeb7c557a --- /dev/null +++ b/packages/jest-runner/src/types.ts @@ -0,0 +1,94 @@ +/** + * 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 {EventEmitter} from 'events'; +import {Environment, Config, TestResult} from '@jest/types'; +import {ModuleMap, FS as HasteFS} from 'jest-haste-map'; +import HasteResolver from 'jest-resolve'; +// @ts-ignore: not migrated to TS +import Runtime from 'jest-runtime'; + +export type ErrorWithCode = Error & {code?: string}; +export type Test = { + context: Context; + duration?: number; + path: Config.Path; +}; + +export type Context = { + config: Config.ProjectConfig; + hasteFS: HasteFS; + moduleMap: ModuleMap; + resolver: HasteResolver; +}; + +// TODO: Obtain this from @jest/reporters once its been migrated +type ReporterOnStartOptions = { + estimatedTime: number; + showStatus: boolean; +}; + +export type OnTestStart = (test: Test) => Promise; +export type OnTestFailure = ( + test: Test, + serializableError: TestResult.SerializableError, +) => Promise; +export type OnTestSuccess = ( + test: Test, + testResult: TestResult.TestResult, +) => Promise; + +export type Reporter = { + onTestResult: ( + test: Test, + testResult: TestResult.TestResult, + aggregatedResult: TestResult.AggregatedResult, + ) => Promise; + onRunStart: ( + results: TestResult.AggregatedResult, + options: ReporterOnStartOptions, + ) => Promise; + onTestStart: (test: Test) => Promise; + onRunComplete: ( + contexts: Set, + results: TestResult.AggregatedResult, + ) => Promise; + getLastError: () => Error; +}; + +export type TestFramework = ( + globalConfig: Config.GlobalConfig, + config: Config.ProjectConfig, + environment: Environment.Environment, + runtime: Runtime, + testPath: string, +) => Promise; + +export type TestRunnerOptions = { + serial: boolean; +}; + +export type TestRunnerContext = { + changedFiles?: Set; +}; + +export type TestRunData = Array<{ + context: Context; + matches: {allTests: number; tests: Array; total: number}; +}>; + +// TODO: Should live in `@jest/core` or `jest-watcher` +export type WatcherState = { + interrupted: boolean; +}; +export interface TestWatcher extends EventEmitter { + state: WatcherState; + new ({isWatchMode}: {isWatchMode: boolean}): TestWatcher; + setState(state: WatcherState): void; + isInterrupted(): boolean; + isWatchMode(): boolean; +} diff --git a/packages/jest-runner/tsconfig.json b/packages/jest-runner/tsconfig.json new file mode 100644 index 000000000000..59665ff7c74e --- /dev/null +++ b/packages/jest-runner/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build" + }, + "references": [ + {"path": "../jest-docblock"}, + {"path": "../jest-haste-map"}, + {"path": "../jest-leak-detector"}, + {"path": "../jest-message-util"}, + {"path": "../jest-resolve"}, + {"path": "../jest-types"}, + {"path": "../jest-worker"}, + {"path": "../jest-util"} + ] +} diff --git a/packages/jest-types/src/Global.ts b/packages/jest-types/src/Global.ts index ed7739615e14..1a782eaaa386 100644 --- a/packages/jest-types/src/Global.ts +++ b/packages/jest-types/src/Global.ts @@ -48,7 +48,7 @@ export interface Describe extends DescribeBase { skip: ItBase; } -export interface Global { +export interface Global extends NodeJS.Global { it: It; test: ItConcurrent; fit: ItBase; diff --git a/packages/jest-worker/src/types.ts b/packages/jest-worker/src/types.ts index f0f4d86aecae..ce25219f525c 100644 --- a/packages/jest-worker/src/types.ts +++ b/packages/jest-worker/src/types.ts @@ -5,6 +5,9 @@ * LICENSE file in the root directory of this source tree. */ +import {EventEmitter} from 'events'; +import {ForkOptions} from 'child_process'; + // Because of the dynamic nature of a worker communication process, all messages // coming from any of the other processes cannot be typed. Thus, many types // include "unknown" as a TS type, which is (unfortunately) correct here. @@ -23,18 +26,7 @@ export type PARENT_MESSAGE_ERROR = // Option objects. -const EventEmitter = require('events'); - -export type ForkOptions = { - cwd?: string; - env?: NodeJS.ProcessEnv; - execPath?: string; - execArgv?: Array; - silent?: boolean; - stdio?: Array; - uid?: number; - gid?: number; -}; +export {ForkOptions}; export interface WorkerPoolInterface { getStderr(): NodeJS.ReadableStream;