diff --git a/.eslintrc.js b/.eslintrc.js index 556de75aba05..a20fd9d0b276 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -77,7 +77,7 @@ module.exports = { 'packages/jest-jasmine2/src/jasmine/Suite.ts', 'packages/jest-jasmine2/src/jasmine/jasmineLight.ts', 'packages/jest-jasmine2/src/jestExpect.ts', - 'packages/jest-resolve/src/index.ts', + 'packages/jest-resolve/src/resolver.ts', ], rules: { 'local/prefer-spread-eventually': 'warn', diff --git a/CHANGELOG.md b/CHANGELOG.md index 7890a1d31096..36888f1eb302 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,13 @@ ### Features - `[jest-reporters]` Expose the `getResultHeader` util ([#11460](https://github.com/facebook/jest/pull/11460)) +- `[jest-resolver]` Export `resolve*` utils for different Jest modules ([#11466](https://github.com/facebook/jest/pull/11466)) +- `[@jest/test-result]` Export `Test`, `TestEvents` and `TestFileEvent` ([#11466](https://github.com/facebook/jest/pull/11466)) ### Fixes +- `[jest-circus, @jest/test-sequencer]` Remove dependency on `jest-runner` ([#11466](https://github.com/facebook/jest/pull/11466)) +- `[jest-runner]` Remove dependency on `jest-config` ([#11466](https://github.com/facebook/jest/pull/11466)) - `[jest-worker]` Loosen engine requirement to `>= 10.13.0` ([#11451](https://github.com/facebook/jest/pull/11451)) ### Chore & Maintenance diff --git a/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap b/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap index d742b2ce2943..7b7539f60631 100644 --- a/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap +++ b/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap @@ -41,7 +41,7 @@ FAIL __tests__/index.js 12 | module.exports = () => 'test'; 13 | - at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:558:17) + at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:558:17) at Object.require (index.js:10:1) `; @@ -70,6 +70,6 @@ FAIL __tests__/index.js 12 | module.exports = () => 'test'; 13 | - at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:558:17) + at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:558:17) at Object.require (index.js:10:1) `; diff --git a/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap b/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap index a5d9190d9b19..d5e865161f13 100644 --- a/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap +++ b/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap @@ -37,6 +37,6 @@ FAIL __tests__/test.js | ^ 9 | - at Resolver.resolveModule (../../packages/jest-resolve/build/index.js:311:11) + at Resolver.resolveModule (../../packages/jest-resolve/build/resolver.js:311:11) at Object.require (index.js:8:18) `; diff --git a/e2e/__tests__/stackTrace.test.ts b/e2e/__tests__/stackTrace.test.ts index a82a02b72217..b2377c1b601c 100644 --- a/e2e/__tests__/stackTrace.test.ts +++ b/e2e/__tests__/stackTrace.test.ts @@ -82,7 +82,7 @@ describe('Stack Trace', () => { ); expect(stderr).toMatch( - /\s+at\s(?:.+?)\s\((?:.+?)jest-resolve\/build\/index\.js/, + /\s+at\s(?:.+?)\s\((?:.+?)jest-resolve\/build\/resolver\.js/, ); }); diff --git a/e2e/async-regenerator/package.json b/e2e/async-regenerator/package.json index db1f1a7efbe8..45e5c439e991 100644 --- a/e2e/async-regenerator/package.json +++ b/e2e/async-regenerator/package.json @@ -9,7 +9,8 @@ "testEnvironment": "node", "transformIgnorePatterns": [ "jest-circus", - "jest-jasmine2" + "jest-jasmine2", + "jest-runner" ] } } diff --git a/e2e/babel-plugin-jest-hoist/package.json b/e2e/babel-plugin-jest-hoist/package.json index eac0243cf4d4..f8c92b090b70 100644 --- a/e2e/babel-plugin-jest-hoist/package.json +++ b/e2e/babel-plugin-jest-hoist/package.json @@ -11,7 +11,8 @@ "transformIgnorePatterns": [ "jest-circus", "jest-environment-node", - "jest-jasmine2" + "jest-jasmine2", + "jest-runner" ] } } diff --git a/e2e/coverage-transform-instrumented/package.json b/e2e/coverage-transform-instrumented/package.json index c571de5fed97..4ba9983f99de 100644 --- a/e2e/coverage-transform-instrumented/package.json +++ b/e2e/coverage-transform-instrumented/package.json @@ -9,8 +9,9 @@ "transformIgnorePatterns": [ "jest-circus", "jest-each", - "jest-environment-node/", - "jest-jasmine2" + "jest-environment-node", + "jest-jasmine2", + "jest-runner" ], "moduleFileExtensions": [ "js" diff --git a/e2e/transform-linked-modules/package.json b/e2e/transform-linked-modules/package.json index aef81a535be6..27eb5796d585 100644 --- a/e2e/transform-linked-modules/package.json +++ b/e2e/transform-linked-modules/package.json @@ -8,7 +8,8 @@ "jest-circus", "jest-each", "jest-environment-node", - "jest-jasmine2" + "jest-jasmine2", + "jest-runner" ], "transform": { "\\.js$": "/preprocessor.js" diff --git a/e2e/transform/babel-jest-ignored/package.json b/e2e/transform/babel-jest-ignored/package.json index 148788b25446..6716c4a59301 100644 --- a/e2e/transform/babel-jest-ignored/package.json +++ b/e2e/transform/babel-jest-ignored/package.json @@ -1,5 +1,9 @@ { "jest": { - "testEnvironment": "node" + "testEnvironment": "node", + "transformIgnorePatterns": [ + "node_modules", + "jest-runner" + ] } } diff --git a/e2e/transform/multiple-transformers/package.json b/e2e/transform/multiple-transformers/package.json index e3760dfeae93..fb6fec0c6fff 100644 --- a/e2e/transform/multiple-transformers/package.json +++ b/e2e/transform/multiple-transformers/package.json @@ -9,7 +9,8 @@ "transformIgnorePatterns": [ "jest-circus", "jest-environment-node", - "jest-jasmine2" + "jest-jasmine2", + "jest-runner" ] }, "dependencies": { diff --git a/e2e/transform/transform-runner/runner.ts b/e2e/transform/transform-runner/runner.ts index 1a1f7b432a7c..fb45dbf4be69 100644 --- a/e2e/transform/transform-runner/runner.ts +++ b/e2e/transform/transform-runner/runner.ts @@ -6,13 +6,12 @@ */ import throat from 'throat'; -import {TestResult, createEmptyTestResult} from '@jest/test-result'; +import {Test, TestResult, createEmptyTestResult} from '@jest/test-result'; import type {Config} from '@jest/types'; -import { +import type { OnTestFailure, OnTestStart, OnTestSuccess, - Test, TestRunnerContext, TestWatcher, } from 'jest-runner'; diff --git a/packages/jest-circus/package.json b/packages/jest-circus/package.json index 405a01807abd..328e023f1b2f 100644 --- a/packages/jest-circus/package.json +++ b/packages/jest-circus/package.json @@ -27,7 +27,6 @@ "jest-each": "^27.0.1", "jest-matcher-utils": "^27.0.1", "jest-message-util": "^27.0.1", - "jest-runner": "^27.0.1", "jest-runtime": "^27.0.1", "jest-snapshot": "^27.0.1", "jest-util": "^27.0.1", diff --git a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts index 1b6908fa113d..3bd1f1554800 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts @@ -6,9 +6,8 @@ */ import type {JestEnvironment} from '@jest/environment'; -import type {TestResult} from '@jest/test-result'; +import type {TestFileEvent, TestResult} from '@jest/test-result'; import type {Config} from '@jest/types'; -import type {TestFileEvent} from 'jest-runner'; import type Runtime from 'jest-runtime'; import type {SnapshotStateType} from 'jest-snapshot'; import {deepCyclicCopy} from 'jest-util'; diff --git a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts index 46d21818a596..ed5f39923b11 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts @@ -10,6 +10,7 @@ import type {JestEnvironment} from '@jest/environment'; import { AssertionResult, Status, + TestFileEvent, TestResult, createEmptyTestResult, } from '@jest/test-result'; @@ -17,7 +18,6 @@ import type {Circus, Config, Global} from '@jest/types'; import {extractExpectedAssertionsErrors, getState, setState} from 'expect'; import {bind} from 'jest-each'; import {formatExecError, formatResultsErrors} from 'jest-message-util'; -import type {TestFileEvent} from 'jest-runner'; import { SnapshotState, SnapshotStateType, diff --git a/packages/jest-circus/src/testCaseReportHandler.ts b/packages/jest-circus/src/testCaseReportHandler.ts index 86001187fe50..4a9bac2cb9b1 100644 --- a/packages/jest-circus/src/testCaseReportHandler.ts +++ b/packages/jest-circus/src/testCaseReportHandler.ts @@ -5,8 +5,8 @@ * LICENSE file in the root directory of this source tree. */ +import type {TestFileEvent} from '@jest/test-result'; import type {Circus} from '@jest/types'; -import type {TestFileEvent} from 'jest-runner'; import {makeSingleTestResult, parseSingleTestResult} from './utils'; const testCaseReportHandler = diff --git a/packages/jest-circus/tsconfig.json b/packages/jest-circus/tsconfig.json index 630f21878c72..423a421a0cb0 100644 --- a/packages/jest-circus/tsconfig.json +++ b/packages/jest-circus/tsconfig.json @@ -10,7 +10,6 @@ {"path": "../jest-environment"}, {"path": "../jest-matcher-utils"}, {"path": "../jest-message-util"}, - {"path": "../jest-runner"}, {"path": "../jest-runtime"}, {"path": "../jest-snapshot"}, {"path": "../jest-test-result"}, diff --git a/packages/jest-config/src/index.ts b/packages/jest-config/src/index.ts index 35bc4f524e85..dfe12a305c01 100644 --- a/packages/jest-config/src/index.ts +++ b/packages/jest-config/src/index.ts @@ -15,7 +15,11 @@ import normalize from './normalize'; import readConfigFileAndSetRootDir from './readConfigFileAndSetRootDir'; import resolveConfigPath from './resolveConfigPath'; import {isJSONString, replaceRootDirInPath} from './utils'; -export {getTestEnvironment, isJSONString} from './utils'; + +// TODO: remove export in Jest 28 +export {resolveTestEnvironment as getTestEnvironment} from 'jest-resolve'; + +export {isJSONString} from './utils'; export {default as normalize} from './normalize'; export {default as deprecationEntries} from './Deprecated'; export {replaceRootDirInPath} from './utils'; diff --git a/packages/jest-config/src/normalize.ts b/packages/jest-config/src/normalize.ts index 08ea6d7ba270..a703f7f7a179 100644 --- a/packages/jest-config/src/normalize.ts +++ b/packages/jest-config/src/normalize.ts @@ -14,7 +14,12 @@ import {statSync} from 'graceful-fs'; import micromatch = require('micromatch'); import type {Config} from '@jest/types'; import {replacePathSepForRegex} from 'jest-regex-util'; -import Resolver from 'jest-resolve'; +import Resolver, { + resolveRunner, + resolveSequencer, + resolveTestEnvironment, + resolveWatchPlugin, +} from 'jest-resolve'; import { clearLine, replacePathSepForGlob, @@ -35,14 +40,11 @@ import { DOCUMENTATION_NOTE, _replaceRootDirTags, escapeGlobCharacters, - getRunner, - getSequencer, - getTestEnvironment, - getWatchPlugin, replaceRootDirInPath, resolve, } from './utils'; import validatePattern from './validatePattern'; + const ERROR = `${BULLET}Validation Error`; const PRESET_EXTENSIONS = ['.json', '.js', '.cjs', '.mjs']; const PRESET_NAME = 'jest-preset'; @@ -604,7 +606,7 @@ export default async function normalize( options.setupFilesAfterEnv.push(options.setupTestFrameworkScriptFile); } - options.testEnvironment = getTestEnvironment({ + options.testEnvironment = resolveTestEnvironment({ rootDir: options.rootDir, testEnvironment: options.testEnvironment || DEFAULT_CONFIG.testEnvironment, }); @@ -739,7 +741,7 @@ export default async function normalize( const option = oldOptions[key]; value = option && - getRunner(newOptions.resolver, { + resolveRunner(newOptions.resolver, { filePath: option, rootDir: options.rootDir, }); @@ -1010,7 +1012,7 @@ export default async function normalize( if (typeof watchPlugin === 'string') { return { config: {}, - path: getWatchPlugin(newOptions.resolver, { + path: resolveWatchPlugin(newOptions.resolver, { filePath: watchPlugin, rootDir: options.rootDir, }), @@ -1018,7 +1020,7 @@ export default async function normalize( } else { return { config: watchPlugin[1] || {}, - path: getWatchPlugin(newOptions.resolver, { + path: resolveWatchPlugin(newOptions.resolver, { filePath: watchPlugin[0], rootDir: options.rootDir, }), @@ -1051,7 +1053,7 @@ export default async function normalize( // ignored } - newOptions.testSequencer = getSequencer(newOptions.resolver, { + newOptions.testSequencer = resolveSequencer(newOptions.resolver, { filePath: options.testSequencer || DEFAULT_CONFIG.testSequencer, rootDir: options.rootDir, }); diff --git a/packages/jest-config/src/utils.ts b/packages/jest-config/src/utils.ts index 9b7e17ccd609..869e60897d7e 100644 --- a/packages/jest-config/src/utils.ts +++ b/packages/jest-config/src/utils.ts @@ -117,134 +117,9 @@ export const _replaceRootDirTags = ( return config; }; -export const resolveWithPrefix = ( - resolver: string | undefined | null, - { - filePath, - humanOptionName, - optionName, - prefix, - rootDir, - }: { - filePath: string; - humanOptionName: string; - optionName: string; - prefix: string; - rootDir: Config.Path; - }, -): string => { - const fileName = replaceRootDirInPath(rootDir, filePath); - let module = Resolver.findNodeModule(`${prefix}${fileName}`, { - basedir: rootDir, - resolver: resolver || undefined, - }); - if (module) { - return module; - } - - try { - return require.resolve(`${prefix}${fileName}`); - } catch {} - - module = Resolver.findNodeModule(fileName, { - basedir: rootDir, - resolver: resolver || undefined, - }); - if (module) { - return module; - } - - try { - return require.resolve(fileName); - } catch {} - - throw createValidationError( - ` ${humanOptionName} ${chalk.bold( - fileName, - )} cannot be found. Make sure the ${chalk.bold( - optionName, - )} configuration option points to an existing node module.`, - ); -}; - -/** - * Finds the test environment to use: - * - * 1. looks for jest-environment- relative to project. - * 1. looks for jest-environment- relative to Jest. - * 1. looks for relative to project. - * 1. looks for relative to Jest. - */ -export const getTestEnvironment = ({ - rootDir, - testEnvironment: filePath, -}: { - rootDir: Config.Path; - testEnvironment: string; -}): string => - resolveWithPrefix(undefined, { - filePath, - humanOptionName: 'Test environment', - optionName: 'testEnvironment', - prefix: 'jest-environment-', - rootDir, - }); - -/** - * Finds the watch plugins to use: - * - * 1. looks for jest-watch- relative to project. - * 1. looks for jest-watch- relative to Jest. - * 1. looks for relative to project. - * 1. looks for relative to Jest. - */ -export const getWatchPlugin = ( - resolver: string | undefined | null, - {filePath, rootDir}: {filePath: string; rootDir: Config.Path}, -): string => - resolveWithPrefix(resolver, { - filePath, - humanOptionName: 'Watch plugin', - optionName: 'watchPlugins', - prefix: 'jest-watch-', - rootDir, - }); - -/** - * Finds the runner to use: - * - * 1. looks for jest-runner- relative to project. - * 1. looks for jest-runner- relative to Jest. - * 1. looks for relative to project. - * 1. looks for relative to Jest. - */ -export const getRunner = ( - resolver: string | undefined | null, - {filePath, rootDir}: {filePath: string; rootDir: Config.Path}, -): string => - resolveWithPrefix(resolver, { - filePath, - humanOptionName: 'Jest Runner', - optionName: 'runner', - prefix: 'jest-runner-', - rootDir, - }); - type JSONString = string & {readonly $$type: never}; // newtype export const isJSONString = (text?: JSONString | string): text is JSONString => text != null && typeof text === 'string' && text.startsWith('{') && text.endsWith('}'); - -export const getSequencer = ( - resolver: string | undefined | null, - {filePath, rootDir}: {filePath: string; rootDir: Config.Path}, -): string => - resolveWithPrefix(resolver, { - filePath, - humanOptionName: 'Jest Sequencer', - optionName: 'testSequencer', - prefix: 'jest-sequencer-', - rootDir, - }); diff --git a/packages/jest-core/src/FailedTestsCache.ts b/packages/jest-core/src/FailedTestsCache.ts index 777f29ba9ca1..de125583e0c4 100644 --- a/packages/jest-core/src/FailedTestsCache.ts +++ b/packages/jest-core/src/FailedTestsCache.ts @@ -5,8 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import type {TestResult} from '@jest/test-result'; -import type {Test} from 'jest-runner'; +import type {Test, TestResult} from '@jest/test-result'; type TestMap = Record>; diff --git a/packages/jest-core/src/ReporterDispatcher.ts b/packages/jest-core/src/ReporterDispatcher.ts index e6f5aa71c06d..992925cdd65b 100644 --- a/packages/jest-core/src/ReporterDispatcher.ts +++ b/packages/jest-core/src/ReporterDispatcher.ts @@ -10,10 +10,10 @@ import type {Reporter, ReporterOnStartOptions} from '@jest/reporters'; import type { AggregatedResult, + Test, TestCaseResult, TestResult, } from '@jest/test-result'; -import type {Test} from 'jest-runner'; import type {Context} from 'jest-runtime'; export default class ReporterDispatcher { diff --git a/packages/jest-core/src/SearchSource.ts b/packages/jest-core/src/SearchSource.ts index 5cf6de74c55b..8b10cf5e0323 100644 --- a/packages/jest-core/src/SearchSource.ts +++ b/packages/jest-core/src/SearchSource.ts @@ -8,12 +8,12 @@ import * as os from 'os'; import * as path from 'path'; import micromatch = require('micromatch'); +import type {Test} from '@jest/test-result'; import type {Config} from '@jest/types'; import type {ChangedFiles} from 'jest-changed-files'; import {replaceRootDirInPath} from 'jest-config'; import {escapePathForRegex} from 'jest-regex-util'; import {DependencyResolver} from 'jest-resolve-dependencies'; -import type {Test} from 'jest-runner'; import type {Context} from 'jest-runtime'; import {buildSnapshotResolver} from 'jest-snapshot'; import {globsToMatcher, testPathPatternToRegExp} from 'jest-util'; diff --git a/packages/jest-core/src/TestPathPatternPrompt.ts b/packages/jest-core/src/TestPathPatternPrompt.ts index 19ff54161101..45d76fb2e5a8 100644 --- a/packages/jest-core/src/TestPathPatternPrompt.ts +++ b/packages/jest-core/src/TestPathPatternPrompt.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import type {Test} from 'jest-runner'; +import type {Test} from '@jest/test-result'; import type {Context} from 'jest-runtime'; import { PatternPrompt, diff --git a/packages/jest-core/src/TestScheduler.ts b/packages/jest-core/src/TestScheduler.ts index b20759cf34d8..848c611d2022 100644 --- a/packages/jest-core/src/TestScheduler.ts +++ b/packages/jest-core/src/TestScheduler.ts @@ -20,6 +20,7 @@ import { import { AggregatedResult, SerializableError, + Test, TestResult, addResult, buildFailureTestResult, @@ -28,7 +29,7 @@ import { import {createScriptTransformer} from '@jest/transform'; import type {Config} from '@jest/types'; import {formatExecError} from 'jest-message-util'; -import TestRunner, {Test} from 'jest-runner'; +import type TestRunner from 'jest-runner'; import type {Context} from 'jest-runtime'; import snapshot = require('jest-snapshot'); import {requireOrImportModule} from 'jest-util'; @@ -36,10 +37,6 @@ import ReporterDispatcher from './ReporterDispatcher'; import type TestWatcher from './TestWatcher'; import {shouldRunInBand} from './testSchedulerHelper'; -// The default jest-runner is required because it is the default test runner -// and required implicitly through the `runner` ProjectConfig option. -TestRunner; - export type TestSchedulerOptions = { startRun: (globalConfig: Config.GlobalConfig) => void; }; diff --git a/packages/jest-core/src/__tests__/SearchSource.test.ts b/packages/jest-core/src/__tests__/SearchSource.test.ts index 968907ea16d5..763d8c58018b 100644 --- a/packages/jest-core/src/__tests__/SearchSource.test.ts +++ b/packages/jest-core/src/__tests__/SearchSource.test.ts @@ -7,9 +7,9 @@ */ import * as path from 'path'; +import type {Test} from '@jest/test-result'; import type {Config} from '@jest/types'; import {normalize} from 'jest-config'; -import type {Test} from 'jest-runner'; import Runtime from 'jest-runtime'; import SearchSource, {SearchResult} from '../SearchSource'; diff --git a/packages/jest-core/src/runGlobalHook.ts b/packages/jest-core/src/runGlobalHook.ts index 1a0e0a463d3b..ca0a357d62dd 100644 --- a/packages/jest-core/src/runGlobalHook.ts +++ b/packages/jest-core/src/runGlobalHook.ts @@ -7,9 +7,9 @@ import * as util from 'util'; import pEachSeries = require('p-each-series'); +import type {Test} from '@jest/test-result'; import {createScriptTransformer} from '@jest/transform'; import type {Config} from '@jest/types'; -import type {Test} from 'jest-runner'; import prettyFormat from 'pretty-format'; export default async ({ diff --git a/packages/jest-core/src/runJest.ts b/packages/jest-core/src/runJest.ts index b6088007235a..40d2970b22c9 100644 --- a/packages/jest-core/src/runJest.ts +++ b/packages/jest-core/src/runJest.ts @@ -12,13 +12,13 @@ import * as fs from 'graceful-fs'; import {CustomConsole} from '@jest/console'; import { AggregatedResult, + Test, formatTestResults, makeEmptyAggregatedTestResult, } from '@jest/test-result'; import type TestSequencer from '@jest/test-sequencer'; import type {Config} from '@jest/types'; import type {ChangedFiles, ChangedFilesPromise} from 'jest-changed-files'; -import type {Test} from 'jest-runner'; import type {Context} from 'jest-runtime'; import {requireOrImportModule, tryRealpath} from 'jest-util'; import {JestHook, JestHookEmitter} from 'jest-watcher'; diff --git a/packages/jest-core/src/testSchedulerHelper.ts b/packages/jest-core/src/testSchedulerHelper.ts index a75b6a4cb8cf..a913b3958c94 100644 --- a/packages/jest-core/src/testSchedulerHelper.ts +++ b/packages/jest-core/src/testSchedulerHelper.ts @@ -5,8 +5,8 @@ * LICENSE file in the root directory of this source tree. */ +import type {Test} from '@jest/test-result'; import type {Config} from '@jest/types'; -import type {Test} from 'jest-runner'; const SLOW_TEST_TIME = 1000; diff --git a/packages/jest-core/src/types.ts b/packages/jest-core/src/types.ts index 144ade08f1d6..b51174cafad9 100644 --- a/packages/jest-core/src/types.ts +++ b/packages/jest-core/src/types.ts @@ -5,8 +5,8 @@ * LICENSE file in the root directory of this source tree. */ +import type {Test} from '@jest/test-result'; import type {Config} from '@jest/types'; -import type {Test} from 'jest-runner'; import type {Context} from 'jest-runtime'; export type Stats = { diff --git a/packages/jest-resolve/package.json b/packages/jest-resolve/package.json index 7d786f335169..07b229c23ae3 100644 --- a/packages/jest-resolve/package.json +++ b/packages/jest-resolve/package.json @@ -20,6 +20,7 @@ "graceful-fs": "^4.2.4", "jest-pnp-resolver": "^1.2.2", "jest-util": "^27.0.1", + "jest-validate": "^27.0.1", "resolve": "^1.20.0", "slash": "^3.0.0" }, diff --git a/packages/jest-resolve/src/__tests__/resolve.test.ts b/packages/jest-resolve/src/__tests__/resolve.test.ts index 0a7584df4f70..2f2f56501429 100644 --- a/packages/jest-resolve/src/__tests__/resolve.test.ts +++ b/packages/jest-resolve/src/__tests__/resolve.test.ts @@ -10,10 +10,10 @@ import * as path from 'path'; import * as fs from 'graceful-fs'; import {sync as resolveSync} from 'resolve'; import {ModuleMap} from 'jest-haste-map'; -import Resolver from '../'; import userResolver from '../__mocks__/userResolver'; import defaultResolver from '../defaultResolver'; import nodeModulesPaths from '../nodeModulesPaths'; +import Resolver from '../resolver'; import type {ResolverConfig} from '../types'; jest.mock('../__mocks__/userResolver'); diff --git a/packages/jest-resolve/src/index.ts b/packages/jest-resolve/src/index.ts index a492cf198978..34600936314f 100644 --- a/packages/jest-resolve/src/index.ts +++ b/packages/jest-resolve/src/index.ts @@ -5,493 +5,9 @@ * LICENSE file in the root directory of this source tree. */ -/* eslint-disable local/prefer-spread-eventually */ +import Resolver from './resolver'; -import * as path from 'path'; -import chalk = require('chalk'); -import slash = require('slash'); -import type {Config} from '@jest/types'; -import type {IModuleMap} from 'jest-haste-map'; -import {tryRealpath} from 'jest-util'; -import ModuleNotFoundError from './ModuleNotFoundError'; -import defaultResolver, {clearDefaultResolverCache} from './defaultResolver'; -import isBuiltinModule from './isBuiltinModule'; -import nodeModulesPaths from './nodeModulesPaths'; -import shouldLoadAsEsm, {clearCachedLookups} from './shouldLoadAsEsm'; -import type {ResolverConfig} from './types'; +export type {ResolveModuleConfig} from './resolver'; +export * from './utils'; -type FindNodeModuleConfig = { - basedir: Config.Path; - browser?: boolean; - extensions?: Array; - moduleDirectory?: Array; - paths?: Array; - resolver?: Config.Path | null; - rootDir?: Config.Path; - throwIfNotFound?: boolean; -}; - -export type ResolveModuleConfig = { - skipNodeResolution?: boolean; - paths?: Array; -}; - -const NATIVE_PLATFORM = 'native'; - -// We might be inside a symlink. -const resolvedCwd = tryRealpath(process.cwd()); -const {NODE_PATH} = process.env; -const nodePaths = NODE_PATH - ? NODE_PATH.split(path.delimiter) - .filter(Boolean) - // The resolver expects absolute paths. - .map(p => path.resolve(resolvedCwd, p)) - : undefined; - -export default class Resolver { - private readonly _options: ResolverConfig; - private readonly _moduleMap: IModuleMap; - private readonly _moduleIDCache: Map; - private readonly _moduleNameCache: Map; - private readonly _modulePathCache: Map>; - private readonly _supportsNativePlatform: boolean; - - constructor(moduleMap: IModuleMap, options: ResolverConfig) { - this._options = { - defaultPlatform: options.defaultPlatform, - extensions: options.extensions, - hasCoreModules: - options.hasCoreModules === undefined ? true : options.hasCoreModules, - moduleDirectories: options.moduleDirectories || ['node_modules'], - moduleNameMapper: options.moduleNameMapper, - modulePaths: options.modulePaths, - platforms: options.platforms, - resolver: options.resolver, - rootDir: options.rootDir, - }; - this._supportsNativePlatform = options.platforms - ? options.platforms.includes(NATIVE_PLATFORM) - : false; - this._moduleMap = moduleMap; - this._moduleIDCache = new Map(); - this._moduleNameCache = new Map(); - this._modulePathCache = new Map(); - } - - static ModuleNotFoundError = ModuleNotFoundError; - - static tryCastModuleNotFoundError( - error: unknown, - ): ModuleNotFoundError | null { - if (error instanceof ModuleNotFoundError) { - return error as ModuleNotFoundError; - } - - const casted = error as ModuleNotFoundError; - if (casted.code === 'MODULE_NOT_FOUND') { - return ModuleNotFoundError.duckType(casted); - } - - return null; - } - - static clearDefaultResolverCache(): void { - clearDefaultResolverCache(); - clearCachedLookups(); - } - - static findNodeModule( - path: Config.Path, - options: FindNodeModuleConfig, - ): Config.Path | null { - const resolver: typeof defaultResolver = options.resolver - ? require(options.resolver) - : defaultResolver; - const paths = options.paths; - - try { - return resolver(path, { - basedir: options.basedir, - browser: options.browser, - defaultResolver, - extensions: options.extensions, - moduleDirectory: options.moduleDirectory, - paths: paths ? (nodePaths || []).concat(paths) : nodePaths, - rootDir: options.rootDir, - }); - } catch (e) { - if (options.throwIfNotFound) { - throw e; - } - } - return null; - } - - // unstable as it should be replaced by https://github.com/nodejs/modules/issues/393, and we don't want people to use it - static unstable_shouldLoadAsEsm = shouldLoadAsEsm; - - resolveModuleFromDirIfExists( - dirname: Config.Path, - moduleName: string, - options?: ResolveModuleConfig, - ): Config.Path | null { - const paths = (options && options.paths) || this._options.modulePaths; - const moduleDirectory = this._options.moduleDirectories; - const key = dirname + path.delimiter + moduleName; - const defaultPlatform = this._options.defaultPlatform; - const extensions = this._options.extensions.slice(); - let module; - - if (this._supportsNativePlatform) { - extensions.unshift( - ...this._options.extensions.map(ext => '.' + NATIVE_PLATFORM + ext), - ); - } - if (defaultPlatform) { - extensions.unshift( - ...this._options.extensions.map(ext => '.' + defaultPlatform + ext), - ); - } - - // 1. If we have already resolved this module for this directory name, - // return a value from the cache. - const cacheResult = this._moduleNameCache.get(key); - if (cacheResult) { - return cacheResult; - } - - // 2. Check if the module is a haste module. - module = this.getModule(moduleName); - if (module) { - this._moduleNameCache.set(key, module); - return module; - } - - // 3. Check if the module is a node module and resolve it based on - // the node module resolution algorithm. If skipNodeResolution is given we - // ignore all modules that look like node modules (ie. are not relative - // requires). This enables us to speed up resolution when we build a - // dependency graph because we don't have to look at modules that may not - // exist and aren't mocked. - const skipResolution = - options && options.skipNodeResolution && !moduleName.includes(path.sep); - - const resolveNodeModule = (name: Config.Path, throwIfNotFound = false) => - Resolver.findNodeModule(name, { - basedir: dirname, - extensions, - moduleDirectory, - paths, - resolver: this._options.resolver, - rootDir: this._options.rootDir, - throwIfNotFound, - }); - - if (!skipResolution) { - module = resolveNodeModule(moduleName, Boolean(process.versions.pnp)); - - if (module) { - this._moduleNameCache.set(key, module); - return module; - } - } - - // 4. Resolve "haste packages" which are `package.json` files outside of - // `node_modules` folders anywhere in the file system. - const parts = moduleName.split('/'); - const hastePackage = this.getPackage(parts.shift()!); - if (hastePackage) { - try { - const module = path.join.apply( - path, - [path.dirname(hastePackage)].concat(parts), - ); - // try resolving with custom resolver first to support extensions, - // then fallback to require.resolve - const resolvedModule = - resolveNodeModule(module) || require.resolve(module); - this._moduleNameCache.set(key, resolvedModule); - return resolvedModule; - } catch {} - } - - return null; - } - - resolveModule( - from: Config.Path, - moduleName: string, - options?: ResolveModuleConfig, - ): Config.Path { - const dirname = path.dirname(from); - const module = - this.resolveStubModuleName(from, moduleName) || - this.resolveModuleFromDirIfExists(dirname, moduleName, options); - if (module) return module; - - // 5. Throw an error if the module could not be found. `resolve.sync` only - // produces an error based on the dirname but we have the actual current - // module name available. - const relativePath = - slash(path.relative(this._options.rootDir, from)) || '.'; - - throw new ModuleNotFoundError( - `Cannot find module '${moduleName}' from '${relativePath}'`, - moduleName, - ); - } - - private _isAliasModule(moduleName: string): boolean { - const moduleNameMapper = this._options.moduleNameMapper; - if (!moduleNameMapper) { - return false; - } - - return moduleNameMapper.some(({regex}) => regex.test(moduleName)); - } - - isCoreModule(moduleName: string): boolean { - return ( - this._options.hasCoreModules && - isBuiltinModule(moduleName) && - !this._isAliasModule(moduleName) - ); - } - - getModule(name: string): Config.Path | null { - return this._moduleMap.getModule( - name, - this._options.defaultPlatform, - this._supportsNativePlatform, - ); - } - - getModulePath(from: Config.Path, moduleName: string): Config.Path { - if (moduleName[0] !== '.' || path.isAbsolute(moduleName)) { - return moduleName; - } - return path.normalize(path.dirname(from) + '/' + moduleName); - } - - getPackage(name: string): Config.Path | null { - return this._moduleMap.getPackage( - name, - this._options.defaultPlatform, - this._supportsNativePlatform, - ); - } - - getMockModule(from: Config.Path, name: string): Config.Path | null { - const mock = this._moduleMap.getMockModule(name); - if (mock) { - return mock; - } else { - const moduleName = this.resolveStubModuleName(from, name); - if (moduleName) { - return this.getModule(moduleName) || moduleName; - } - } - return null; - } - - getModulePaths(from: Config.Path): Array { - const cachedModule = this._modulePathCache.get(from); - if (cachedModule) { - return cachedModule; - } - - const moduleDirectory = this._options.moduleDirectories; - const paths = nodeModulesPaths(from, {moduleDirectory}); - if (paths[paths.length - 1] === undefined) { - // circumvent node-resolve bug that adds `undefined` as last item. - paths.pop(); - } - this._modulePathCache.set(from, paths); - return paths; - } - - getModuleID( - virtualMocks: Map, - from: Config.Path, - _moduleName?: string, - ): string { - const moduleName = _moduleName || ''; - - const key = from + path.delimiter + moduleName; - const cachedModuleID = this._moduleIDCache.get(key); - if (cachedModuleID) { - return cachedModuleID; - } - - const moduleType = this._getModuleType(moduleName); - const absolutePath = this._getAbsolutePath(virtualMocks, from, moduleName); - const mockPath = this._getMockPath(from, moduleName); - - const sep = path.delimiter; - const id = - moduleType + - sep + - (absolutePath ? absolutePath + sep : '') + - (mockPath ? mockPath + sep : ''); - - this._moduleIDCache.set(key, id); - return id; - } - - private _getModuleType(moduleName: string): 'node' | 'user' { - return this.isCoreModule(moduleName) ? 'node' : 'user'; - } - - private _getAbsolutePath( - virtualMocks: Map, - from: Config.Path, - moduleName: string, - ): Config.Path | null { - if (this.isCoreModule(moduleName)) { - return moduleName; - } - return this._isModuleResolved(from, moduleName) - ? this.getModule(moduleName) - : this._getVirtualMockPath(virtualMocks, from, moduleName); - } - - private _getMockPath( - from: Config.Path, - moduleName: string, - ): Config.Path | null { - return !this.isCoreModule(moduleName) - ? this.getMockModule(from, moduleName) - : null; - } - - private _getVirtualMockPath( - virtualMocks: Map, - from: Config.Path, - moduleName: string, - ): Config.Path { - const virtualMockPath = this.getModulePath(from, moduleName); - return virtualMocks.get(virtualMockPath) - ? virtualMockPath - : moduleName - ? this.resolveModule(from, moduleName) - : from; - } - - private _isModuleResolved(from: Config.Path, moduleName: string): boolean { - return !!( - this.getModule(moduleName) || this.getMockModule(from, moduleName) - ); - } - - resolveStubModuleName( - from: Config.Path, - moduleName: string, - ): Config.Path | null { - const dirname = path.dirname(from); - const paths = this._options.modulePaths; - const extensions = this._options.extensions.slice(); - const moduleDirectory = this._options.moduleDirectories; - const moduleNameMapper = this._options.moduleNameMapper; - const resolver = this._options.resolver; - const defaultPlatform = this._options.defaultPlatform; - - if (this._supportsNativePlatform) { - extensions.unshift( - ...this._options.extensions.map(ext => '.' + NATIVE_PLATFORM + ext), - ); - } - - if (defaultPlatform) { - extensions.unshift( - ...this._options.extensions.map(ext => '.' + defaultPlatform + ext), - ); - } - - if (moduleNameMapper) { - for (const {moduleName: mappedModuleName, regex} of moduleNameMapper) { - if (regex.test(moduleName)) { - // Note: once a moduleNameMapper matches the name, it must result - // in a module, or else an error is thrown. - const matches = moduleName.match(regex); - const mapModuleName = matches - ? (moduleName: string) => - moduleName.replace( - /\$([0-9]+)/g, - (_, index) => matches[parseInt(index, 10)], - ) - : (moduleName: string) => moduleName; - - const possibleModuleNames = Array.isArray(mappedModuleName) - ? mappedModuleName - : [mappedModuleName]; - let module: string | null = null; - for (const possibleModuleName of possibleModuleNames) { - const updatedName = mapModuleName(possibleModuleName); - - module = - this.getModule(updatedName) || - Resolver.findNodeModule(updatedName, { - basedir: dirname, - extensions, - moduleDirectory, - paths, - resolver, - rootDir: this._options.rootDir, - }); - - if (module) { - break; - } - } - - if (!module) { - throw createNoMappedModuleFoundError( - moduleName, - mapModuleName, - mappedModuleName, - regex, - resolver, - ); - } - return module; - } - } - } - return null; - } -} - -const createNoMappedModuleFoundError = ( - moduleName: string, - mapModuleName: (moduleName: string) => string, - mappedModuleName: string | Array, - regex: RegExp, - resolver?: ((...args: Array) => unknown) | string | null, -) => { - const mappedAs = Array.isArray(mappedModuleName) - ? JSON.stringify(mappedModuleName.map(mapModuleName), null, 2) - : mappedModuleName; - const original = Array.isArray(mappedModuleName) - ? JSON.stringify(mappedModuleName, null, 6) // using 6 because of misalignment when nested below - .slice(0, -1) + ' ]' /// align last bracket correctly as well - : mappedModuleName; - - const error = new Error( - chalk.red(`${chalk.bold('Configuration error')}: - -Could not locate module ${chalk.bold(moduleName)} mapped as: -${chalk.bold(mappedAs)}. - -Please check your configuration for these entries: -{ - "moduleNameMapper": { - "${regex.toString()}": "${chalk.bold(original)}" - }, - "resolver": ${chalk.bold(String(resolver))} -}`), - ); - - error.name = ''; - - return error; -}; +export default Resolver; diff --git a/packages/jest-resolve/src/resolver.ts b/packages/jest-resolve/src/resolver.ts new file mode 100644 index 000000000000..a492cf198978 --- /dev/null +++ b/packages/jest-resolve/src/resolver.ts @@ -0,0 +1,497 @@ +/** + * 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. + */ + +/* eslint-disable local/prefer-spread-eventually */ + +import * as path from 'path'; +import chalk = require('chalk'); +import slash = require('slash'); +import type {Config} from '@jest/types'; +import type {IModuleMap} from 'jest-haste-map'; +import {tryRealpath} from 'jest-util'; +import ModuleNotFoundError from './ModuleNotFoundError'; +import defaultResolver, {clearDefaultResolverCache} from './defaultResolver'; +import isBuiltinModule from './isBuiltinModule'; +import nodeModulesPaths from './nodeModulesPaths'; +import shouldLoadAsEsm, {clearCachedLookups} from './shouldLoadAsEsm'; +import type {ResolverConfig} from './types'; + +type FindNodeModuleConfig = { + basedir: Config.Path; + browser?: boolean; + extensions?: Array; + moduleDirectory?: Array; + paths?: Array; + resolver?: Config.Path | null; + rootDir?: Config.Path; + throwIfNotFound?: boolean; +}; + +export type ResolveModuleConfig = { + skipNodeResolution?: boolean; + paths?: Array; +}; + +const NATIVE_PLATFORM = 'native'; + +// We might be inside a symlink. +const resolvedCwd = tryRealpath(process.cwd()); +const {NODE_PATH} = process.env; +const nodePaths = NODE_PATH + ? NODE_PATH.split(path.delimiter) + .filter(Boolean) + // The resolver expects absolute paths. + .map(p => path.resolve(resolvedCwd, p)) + : undefined; + +export default class Resolver { + private readonly _options: ResolverConfig; + private readonly _moduleMap: IModuleMap; + private readonly _moduleIDCache: Map; + private readonly _moduleNameCache: Map; + private readonly _modulePathCache: Map>; + private readonly _supportsNativePlatform: boolean; + + constructor(moduleMap: IModuleMap, options: ResolverConfig) { + this._options = { + defaultPlatform: options.defaultPlatform, + extensions: options.extensions, + hasCoreModules: + options.hasCoreModules === undefined ? true : options.hasCoreModules, + moduleDirectories: options.moduleDirectories || ['node_modules'], + moduleNameMapper: options.moduleNameMapper, + modulePaths: options.modulePaths, + platforms: options.platforms, + resolver: options.resolver, + rootDir: options.rootDir, + }; + this._supportsNativePlatform = options.platforms + ? options.platforms.includes(NATIVE_PLATFORM) + : false; + this._moduleMap = moduleMap; + this._moduleIDCache = new Map(); + this._moduleNameCache = new Map(); + this._modulePathCache = new Map(); + } + + static ModuleNotFoundError = ModuleNotFoundError; + + static tryCastModuleNotFoundError( + error: unknown, + ): ModuleNotFoundError | null { + if (error instanceof ModuleNotFoundError) { + return error as ModuleNotFoundError; + } + + const casted = error as ModuleNotFoundError; + if (casted.code === 'MODULE_NOT_FOUND') { + return ModuleNotFoundError.duckType(casted); + } + + return null; + } + + static clearDefaultResolverCache(): void { + clearDefaultResolverCache(); + clearCachedLookups(); + } + + static findNodeModule( + path: Config.Path, + options: FindNodeModuleConfig, + ): Config.Path | null { + const resolver: typeof defaultResolver = options.resolver + ? require(options.resolver) + : defaultResolver; + const paths = options.paths; + + try { + return resolver(path, { + basedir: options.basedir, + browser: options.browser, + defaultResolver, + extensions: options.extensions, + moduleDirectory: options.moduleDirectory, + paths: paths ? (nodePaths || []).concat(paths) : nodePaths, + rootDir: options.rootDir, + }); + } catch (e) { + if (options.throwIfNotFound) { + throw e; + } + } + return null; + } + + // unstable as it should be replaced by https://github.com/nodejs/modules/issues/393, and we don't want people to use it + static unstable_shouldLoadAsEsm = shouldLoadAsEsm; + + resolveModuleFromDirIfExists( + dirname: Config.Path, + moduleName: string, + options?: ResolveModuleConfig, + ): Config.Path | null { + const paths = (options && options.paths) || this._options.modulePaths; + const moduleDirectory = this._options.moduleDirectories; + const key = dirname + path.delimiter + moduleName; + const defaultPlatform = this._options.defaultPlatform; + const extensions = this._options.extensions.slice(); + let module; + + if (this._supportsNativePlatform) { + extensions.unshift( + ...this._options.extensions.map(ext => '.' + NATIVE_PLATFORM + ext), + ); + } + if (defaultPlatform) { + extensions.unshift( + ...this._options.extensions.map(ext => '.' + defaultPlatform + ext), + ); + } + + // 1. If we have already resolved this module for this directory name, + // return a value from the cache. + const cacheResult = this._moduleNameCache.get(key); + if (cacheResult) { + return cacheResult; + } + + // 2. Check if the module is a haste module. + module = this.getModule(moduleName); + if (module) { + this._moduleNameCache.set(key, module); + return module; + } + + // 3. Check if the module is a node module and resolve it based on + // the node module resolution algorithm. If skipNodeResolution is given we + // ignore all modules that look like node modules (ie. are not relative + // requires). This enables us to speed up resolution when we build a + // dependency graph because we don't have to look at modules that may not + // exist and aren't mocked. + const skipResolution = + options && options.skipNodeResolution && !moduleName.includes(path.sep); + + const resolveNodeModule = (name: Config.Path, throwIfNotFound = false) => + Resolver.findNodeModule(name, { + basedir: dirname, + extensions, + moduleDirectory, + paths, + resolver: this._options.resolver, + rootDir: this._options.rootDir, + throwIfNotFound, + }); + + if (!skipResolution) { + module = resolveNodeModule(moduleName, Boolean(process.versions.pnp)); + + if (module) { + this._moduleNameCache.set(key, module); + return module; + } + } + + // 4. Resolve "haste packages" which are `package.json` files outside of + // `node_modules` folders anywhere in the file system. + const parts = moduleName.split('/'); + const hastePackage = this.getPackage(parts.shift()!); + if (hastePackage) { + try { + const module = path.join.apply( + path, + [path.dirname(hastePackage)].concat(parts), + ); + // try resolving with custom resolver first to support extensions, + // then fallback to require.resolve + const resolvedModule = + resolveNodeModule(module) || require.resolve(module); + this._moduleNameCache.set(key, resolvedModule); + return resolvedModule; + } catch {} + } + + return null; + } + + resolveModule( + from: Config.Path, + moduleName: string, + options?: ResolveModuleConfig, + ): Config.Path { + const dirname = path.dirname(from); + const module = + this.resolveStubModuleName(from, moduleName) || + this.resolveModuleFromDirIfExists(dirname, moduleName, options); + if (module) return module; + + // 5. Throw an error if the module could not be found. `resolve.sync` only + // produces an error based on the dirname but we have the actual current + // module name available. + const relativePath = + slash(path.relative(this._options.rootDir, from)) || '.'; + + throw new ModuleNotFoundError( + `Cannot find module '${moduleName}' from '${relativePath}'`, + moduleName, + ); + } + + private _isAliasModule(moduleName: string): boolean { + const moduleNameMapper = this._options.moduleNameMapper; + if (!moduleNameMapper) { + return false; + } + + return moduleNameMapper.some(({regex}) => regex.test(moduleName)); + } + + isCoreModule(moduleName: string): boolean { + return ( + this._options.hasCoreModules && + isBuiltinModule(moduleName) && + !this._isAliasModule(moduleName) + ); + } + + getModule(name: string): Config.Path | null { + return this._moduleMap.getModule( + name, + this._options.defaultPlatform, + this._supportsNativePlatform, + ); + } + + getModulePath(from: Config.Path, moduleName: string): Config.Path { + if (moduleName[0] !== '.' || path.isAbsolute(moduleName)) { + return moduleName; + } + return path.normalize(path.dirname(from) + '/' + moduleName); + } + + getPackage(name: string): Config.Path | null { + return this._moduleMap.getPackage( + name, + this._options.defaultPlatform, + this._supportsNativePlatform, + ); + } + + getMockModule(from: Config.Path, name: string): Config.Path | null { + const mock = this._moduleMap.getMockModule(name); + if (mock) { + return mock; + } else { + const moduleName = this.resolveStubModuleName(from, name); + if (moduleName) { + return this.getModule(moduleName) || moduleName; + } + } + return null; + } + + getModulePaths(from: Config.Path): Array { + const cachedModule = this._modulePathCache.get(from); + if (cachedModule) { + return cachedModule; + } + + const moduleDirectory = this._options.moduleDirectories; + const paths = nodeModulesPaths(from, {moduleDirectory}); + if (paths[paths.length - 1] === undefined) { + // circumvent node-resolve bug that adds `undefined` as last item. + paths.pop(); + } + this._modulePathCache.set(from, paths); + return paths; + } + + getModuleID( + virtualMocks: Map, + from: Config.Path, + _moduleName?: string, + ): string { + const moduleName = _moduleName || ''; + + const key = from + path.delimiter + moduleName; + const cachedModuleID = this._moduleIDCache.get(key); + if (cachedModuleID) { + return cachedModuleID; + } + + const moduleType = this._getModuleType(moduleName); + const absolutePath = this._getAbsolutePath(virtualMocks, from, moduleName); + const mockPath = this._getMockPath(from, moduleName); + + const sep = path.delimiter; + const id = + moduleType + + sep + + (absolutePath ? absolutePath + sep : '') + + (mockPath ? mockPath + sep : ''); + + this._moduleIDCache.set(key, id); + return id; + } + + private _getModuleType(moduleName: string): 'node' | 'user' { + return this.isCoreModule(moduleName) ? 'node' : 'user'; + } + + private _getAbsolutePath( + virtualMocks: Map, + from: Config.Path, + moduleName: string, + ): Config.Path | null { + if (this.isCoreModule(moduleName)) { + return moduleName; + } + return this._isModuleResolved(from, moduleName) + ? this.getModule(moduleName) + : this._getVirtualMockPath(virtualMocks, from, moduleName); + } + + private _getMockPath( + from: Config.Path, + moduleName: string, + ): Config.Path | null { + return !this.isCoreModule(moduleName) + ? this.getMockModule(from, moduleName) + : null; + } + + private _getVirtualMockPath( + virtualMocks: Map, + from: Config.Path, + moduleName: string, + ): Config.Path { + const virtualMockPath = this.getModulePath(from, moduleName); + return virtualMocks.get(virtualMockPath) + ? virtualMockPath + : moduleName + ? this.resolveModule(from, moduleName) + : from; + } + + private _isModuleResolved(from: Config.Path, moduleName: string): boolean { + return !!( + this.getModule(moduleName) || this.getMockModule(from, moduleName) + ); + } + + resolveStubModuleName( + from: Config.Path, + moduleName: string, + ): Config.Path | null { + const dirname = path.dirname(from); + const paths = this._options.modulePaths; + const extensions = this._options.extensions.slice(); + const moduleDirectory = this._options.moduleDirectories; + const moduleNameMapper = this._options.moduleNameMapper; + const resolver = this._options.resolver; + const defaultPlatform = this._options.defaultPlatform; + + if (this._supportsNativePlatform) { + extensions.unshift( + ...this._options.extensions.map(ext => '.' + NATIVE_PLATFORM + ext), + ); + } + + if (defaultPlatform) { + extensions.unshift( + ...this._options.extensions.map(ext => '.' + defaultPlatform + ext), + ); + } + + if (moduleNameMapper) { + for (const {moduleName: mappedModuleName, regex} of moduleNameMapper) { + if (regex.test(moduleName)) { + // Note: once a moduleNameMapper matches the name, it must result + // in a module, or else an error is thrown. + const matches = moduleName.match(regex); + const mapModuleName = matches + ? (moduleName: string) => + moduleName.replace( + /\$([0-9]+)/g, + (_, index) => matches[parseInt(index, 10)], + ) + : (moduleName: string) => moduleName; + + const possibleModuleNames = Array.isArray(mappedModuleName) + ? mappedModuleName + : [mappedModuleName]; + let module: string | null = null; + for (const possibleModuleName of possibleModuleNames) { + const updatedName = mapModuleName(possibleModuleName); + + module = + this.getModule(updatedName) || + Resolver.findNodeModule(updatedName, { + basedir: dirname, + extensions, + moduleDirectory, + paths, + resolver, + rootDir: this._options.rootDir, + }); + + if (module) { + break; + } + } + + if (!module) { + throw createNoMappedModuleFoundError( + moduleName, + mapModuleName, + mappedModuleName, + regex, + resolver, + ); + } + return module; + } + } + } + return null; + } +} + +const createNoMappedModuleFoundError = ( + moduleName: string, + mapModuleName: (moduleName: string) => string, + mappedModuleName: string | Array, + regex: RegExp, + resolver?: ((...args: Array) => unknown) | string | null, +) => { + const mappedAs = Array.isArray(mappedModuleName) + ? JSON.stringify(mappedModuleName.map(mapModuleName), null, 2) + : mappedModuleName; + const original = Array.isArray(mappedModuleName) + ? JSON.stringify(mappedModuleName, null, 6) // using 6 because of misalignment when nested below + .slice(0, -1) + ' ]' /// align last bracket correctly as well + : mappedModuleName; + + const error = new Error( + chalk.red(`${chalk.bold('Configuration error')}: + +Could not locate module ${chalk.bold(moduleName)} mapped as: +${chalk.bold(mappedAs)}. + +Please check your configuration for these entries: +{ + "moduleNameMapper": { + "${regex.toString()}": "${chalk.bold(original)}" + }, + "resolver": ${chalk.bold(String(resolver))} +}`), + ); + + error.name = ''; + + return error; +}; diff --git a/packages/jest-resolve/src/utils.ts b/packages/jest-resolve/src/utils.ts new file mode 100644 index 000000000000..45d63a8777a2 --- /dev/null +++ b/packages/jest-resolve/src/utils.ts @@ -0,0 +1,159 @@ +/** + * 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 * as path from 'path'; +import chalk = require('chalk'); +import type {Config} from '@jest/types'; +import {ValidationError} from 'jest-validate'; +import Resolver from './resolver'; + +const BULLET: string = chalk.bold('\u25cf '); +const DOCUMENTATION_NOTE = ` ${chalk.bold('Configuration Documentation:')} + https://jestjs.io/docs/configuration +`; + +const createValidationError = (message: string) => + new ValidationError(`${BULLET}Validation Error`, message, DOCUMENTATION_NOTE); + +const replaceRootDirInPath = ( + rootDir: Config.Path, + filePath: Config.Path, +): string => { + if (!/^/.test(filePath)) { + return filePath; + } + + return path.resolve( + rootDir, + path.normalize('./' + filePath.substr(''.length)), + ); +}; + +const resolveWithPrefix = ( + resolver: string | undefined | null, + { + filePath, + humanOptionName, + optionName, + prefix, + rootDir, + }: { + filePath: string; + humanOptionName: string; + optionName: string; + prefix: string; + rootDir: Config.Path; + }, +): string => { + const fileName = replaceRootDirInPath(rootDir, filePath); + let module = Resolver.findNodeModule(`${prefix}${fileName}`, { + basedir: rootDir, + resolver: resolver || undefined, + }); + if (module) { + return module; + } + + try { + return require.resolve(`${prefix}${fileName}`); + } catch {} + + module = Resolver.findNodeModule(fileName, { + basedir: rootDir, + resolver: resolver || undefined, + }); + if (module) { + return module; + } + + try { + return require.resolve(fileName); + } catch {} + + throw createValidationError( + ` ${humanOptionName} ${chalk.bold( + fileName, + )} cannot be found. Make sure the ${chalk.bold( + optionName, + )} configuration option points to an existing node module.`, + ); +}; + +/** + * Finds the test environment to use: + * + * 1. looks for jest-environment- relative to project. + * 1. looks for jest-environment- relative to Jest. + * 1. looks for relative to project. + * 1. looks for relative to Jest. + */ +export const resolveTestEnvironment = ({ + rootDir, + testEnvironment: filePath, +}: { + rootDir: Config.Path; + testEnvironment: string; +}): string => + resolveWithPrefix(undefined, { + filePath, + humanOptionName: 'Test environment', + optionName: 'testEnvironment', + prefix: 'jest-environment-', + rootDir, + }); + +/** + * Finds the watch plugins to use: + * + * 1. looks for jest-watch- relative to project. + * 1. looks for jest-watch- relative to Jest. + * 1. looks for relative to project. + * 1. looks for relative to Jest. + */ +export const resolveWatchPlugin = ( + resolver: string | undefined | null, + {filePath, rootDir}: {filePath: string; rootDir: Config.Path}, +): string => + resolveWithPrefix(resolver, { + filePath, + humanOptionName: 'Watch plugin', + optionName: 'watchPlugins', + prefix: 'jest-watch-', + rootDir, + }); + +/** + * Finds the runner to use: + * + * 1. looks for jest-runner- relative to project. + * 1. looks for jest-runner- relative to Jest. + * 1. looks for relative to project. + * 1. looks for relative to Jest. + */ +export const resolveRunner = ( + resolver: string | undefined | null, + {filePath, rootDir}: {filePath: string; rootDir: Config.Path}, +): string => + resolveWithPrefix(resolver, { + filePath, + humanOptionName: 'Jest Runner', + optionName: 'runner', + prefix: 'jest-runner-', + rootDir, + }); + +export const resolveSequencer = ( + resolver: string | undefined | null, + {filePath, rootDir}: {filePath: string; rootDir: Config.Path}, +): string => + resolveWithPrefix(resolver, { + filePath, + humanOptionName: 'Jest Sequencer', + optionName: 'testSequencer', + prefix: 'jest-sequencer-', + rootDir, + }); diff --git a/packages/jest-resolve/tsconfig.json b/packages/jest-resolve/tsconfig.json index c134ea4e4c28..7ffe2b2a1c93 100644 --- a/packages/jest-resolve/tsconfig.json +++ b/packages/jest-resolve/tsconfig.json @@ -7,6 +7,7 @@ "references": [ {"path": "../jest-haste-map"}, {"path": "../jest-types"}, - {"path": "../jest-util"} + {"path": "../jest-util"}, + {"path": "../jest-validate"} ] } diff --git a/packages/jest-runner/package.json b/packages/jest-runner/package.json index bee44a69ae61..7efd6588a5f3 100644 --- a/packages/jest-runner/package.json +++ b/packages/jest-runner/package.json @@ -24,7 +24,6 @@ "emittery": "^0.8.1", "exit": "^0.1.2", "graceful-fs": "^4.2.4", - "jest-config": "^27.0.1", "jest-docblock": "^27.0.1", "jest-haste-map": "^27.0.1", "jest-leak-detector": "^27.0.1", diff --git a/packages/jest-runner/src/index.ts b/packages/jest-runner/src/index.ts index b58d881416f4..cd297c3696c5 100644 --- a/packages/jest-runner/src/index.ts +++ b/packages/jest-runner/src/index.ts @@ -9,7 +9,13 @@ import chalk = require('chalk'); import Emittery = require('emittery'); import exit = require('exit'); import throat from 'throat'; -import type {SerializableError, TestResult} from '@jest/test-result'; +import type { + SerializableError, + Test, + TestEvents, + TestFileEvent, + TestResult, +} from '@jest/test-result'; import type {Config} from '@jest/types'; import {deepCyclicCopy} from 'jest-util'; import {PromiseWithCustomMessage, Worker} from 'jest-worker'; @@ -19,14 +25,14 @@ import type { OnTestFailure, OnTestStart, OnTestSuccess, - Test, - TestEvents, - TestFileEvent, TestRunnerContext, TestRunnerOptions, TestWatcher, } from './types'; +// TODO: remove re-export in Jest 28 +export type {Test, TestFileEvent, TestEvents} from '@jest/test-result'; + const TEST_WORKER_PATH = require.resolve('./testWorker'); interface WorkerInterface extends Worker { @@ -34,14 +40,12 @@ interface WorkerInterface extends Worker { } export type { - Test, OnTestFailure, OnTestStart, OnTestSuccess, TestWatcher, TestRunnerContext, TestRunnerOptions, - TestFileEvent, } from './types'; export default class TestRunner { diff --git a/packages/jest-runner/src/runTest.ts b/packages/jest-runner/src/runTest.ts index a4c3677ba6b7..b661248b94ee 100644 --- a/packages/jest-runner/src/runTest.ts +++ b/packages/jest-runner/src/runTest.ts @@ -18,17 +18,16 @@ import { getConsoleOutput, } from '@jest/console'; import type {JestEnvironment} from '@jest/environment'; -import type {TestResult} from '@jest/test-result'; +import type {TestFileEvent, TestResult} from '@jest/test-result'; import {createScriptTransformer} from '@jest/transform'; import type {Config} from '@jest/types'; -import {getTestEnvironment} from 'jest-config'; import * as docblock from 'jest-docblock'; import LeakDetector from 'jest-leak-detector'; import {formatExecError} from 'jest-message-util'; -import type Resolver from 'jest-resolve'; +import Resolver, {resolveTestEnvironment} from 'jest-resolve'; import type RuntimeClass from 'jest-runtime'; import {ErrorWithStack, interopRequireDefault, setGlobal} from 'jest-util'; -import type {TestFileEvent, TestFramework, TestRunnerContext} from './types'; +import type {TestFramework, TestRunnerContext} from './types'; type RunTestInternalResult = { leakDetector: LeakDetector | null; @@ -97,7 +96,7 @@ async function runTestInternal( )}"`, ); } - testEnvironment = getTestEnvironment({ + testEnvironment = resolveTestEnvironment({ ...config, testEnvironment: customEnvironment, }); diff --git a/packages/jest-runner/src/testWorker.ts b/packages/jest-runner/src/testWorker.ts index 37a0a6881644..9f57576189b1 100644 --- a/packages/jest-runner/src/testWorker.ts +++ b/packages/jest-runner/src/testWorker.ts @@ -7,7 +7,11 @@ */ import exit = require('exit'); -import type {SerializableError, TestResult} from '@jest/test-result'; +import type { + SerializableError, + TestFileEvent, + TestResult, +} from '@jest/test-result'; import type {Config} from '@jest/types'; import HasteMap, {SerializableModuleMap} from 'jest-haste-map'; import {separateMessageFromStack} from 'jest-message-util'; @@ -15,11 +19,7 @@ import type Resolver from 'jest-resolve'; import Runtime from 'jest-runtime'; import {messageParent} from 'jest-worker'; import runTest from './runTest'; -import type { - ErrorWithCode, - TestFileEvent, - TestRunnerSerializedContext, -} from './types'; +import type {ErrorWithCode, TestRunnerSerializedContext} from './types'; export type SerializableResolver = { config: Config.ProjectConfig; diff --git a/packages/jest-runner/src/types.ts b/packages/jest-runner/src/types.ts index 4e1f7be2187b..65d0f7fdbd46 100644 --- a/packages/jest-runner/src/types.ts +++ b/packages/jest-runner/src/types.ts @@ -8,28 +8,15 @@ import Emittery = require('emittery'); import type {JestEnvironment} from '@jest/environment'; import type { - AssertionResult, SerializableError, + Test, + TestFileEvent, TestResult, } from '@jest/test-result'; import type {Config} from '@jest/types'; -import type {FS as HasteFS, ModuleMap} from 'jest-haste-map'; -import type Resolver from 'jest-resolve'; import type RuntimeType 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: Resolver; -}; export type OnTestStart = (test: Test) => Promise; export type OnTestFailure = ( @@ -41,19 +28,6 @@ export type OnTestSuccess = ( testResult: TestResult, ) => Promise; -// Typings for `sendMessageToJest` events -export type TestEvents = { - 'test-file-start': [Test]; - 'test-file-success': [Test, TestResult]; - 'test-file-failure': [Test, SerializableError]; - 'test-case-result': [Config.Path, AssertionResult]; -}; - -export type TestFileEvent = ( - eventName: T, - args: TestEvents[T], -) => unknown; - export type TestFramework = ( globalConfig: Config.GlobalConfig, config: Config.ProjectConfig, diff --git a/packages/jest-runner/tsconfig.json b/packages/jest-runner/tsconfig.json index 7cb8d3a00c88..a4ae465f33db 100644 --- a/packages/jest-runner/tsconfig.json +++ b/packages/jest-runner/tsconfig.json @@ -5,7 +5,6 @@ "outDir": "build" }, "references": [ - {"path": "../jest-config"}, {"path": "../jest-console"}, {"path": "../jest-docblock"}, {"path": "../jest-environment"}, diff --git a/packages/jest-test-result/src/index.ts b/packages/jest-test-result/src/index.ts index c149c10ee4b1..97c2def09908 100644 --- a/packages/jest-test-result/src/index.ts +++ b/packages/jest-test-result/src/index.ts @@ -24,6 +24,9 @@ export type { SnapshotSummary, Status, Suite, + Test, + TestEvents, + TestFileEvent, TestResult, TestCaseResult, V8CoverageResult, diff --git a/packages/jest-test-result/src/types.ts b/packages/jest-test-result/src/types.ts index b6410c5f9750..94c23cf72cd7 100644 --- a/packages/jest-test-result/src/types.ts +++ b/packages/jest-test-result/src/types.ts @@ -9,6 +9,8 @@ import type {V8Coverage} from 'collect-v8-coverage'; import type {CoverageMap, CoverageMapData} from 'istanbul-lib-coverage'; import type {ConsoleBuffer} from '@jest/console'; import type {Config, TestResult, TransformTypes} from '@jest/types'; +import type {FS as HasteFS, ModuleMap} from 'jest-haste-map'; +import type Resolver from 'jest-resolve'; export interface RuntimeTransformResult extends TransformTypes.TransformResult { wrapperLength: number; @@ -175,3 +177,29 @@ export type SnapshotSummary = { unmatched: number; updated: number; }; + +export type Test = { + context: Context; + duration?: number; + path: Config.Path; +}; + +type Context = { + config: Config.ProjectConfig; + hasteFS: HasteFS; + moduleMap: ModuleMap; + resolver: Resolver; +}; + +// Typings for `sendMessageToJest` events +export type TestEvents = { + 'test-file-start': [Test]; + 'test-file-success': [Test, TestResult]; + 'test-file-failure': [Test, SerializableError]; + 'test-case-result': [Config.Path, AssertionResult]; +}; + +export type TestFileEvent = ( + eventName: T, + args: TestEvents[T], +) => unknown; diff --git a/packages/jest-test-sequencer/package.json b/packages/jest-test-sequencer/package.json index c1e6ace6c674..a16fa4ef9292 100644 --- a/packages/jest-test-sequencer/package.json +++ b/packages/jest-test-sequencer/package.json @@ -17,7 +17,6 @@ "@jest/test-result": "^27.0.1", "graceful-fs": "^4.2.4", "jest-haste-map": "^27.0.1", - "jest-runner": "^27.0.1", "jest-runtime": "^27.0.1" }, "devDependencies": { diff --git a/packages/jest-test-sequencer/src/index.ts b/packages/jest-test-sequencer/src/index.ts index 8038771a3047..2637498f36d5 100644 --- a/packages/jest-test-sequencer/src/index.ts +++ b/packages/jest-test-sequencer/src/index.ts @@ -6,9 +6,8 @@ */ import * as fs from 'graceful-fs'; -import type {AggregatedResult} from '@jest/test-result'; +import type {AggregatedResult, Test} from '@jest/test-result'; import HasteMap from 'jest-haste-map'; -import type {Test} from 'jest-runner'; import type {Context} from 'jest-runtime'; const FAIL = 0; diff --git a/packages/jest-test-sequencer/tsconfig.json b/packages/jest-test-sequencer/tsconfig.json index fd37f0a8849a..8f8417fc4e78 100644 --- a/packages/jest-test-sequencer/tsconfig.json +++ b/packages/jest-test-sequencer/tsconfig.json @@ -6,7 +6,6 @@ }, "references": [ {"path": "../jest-haste-map"}, - {"path": "../jest-runner"}, {"path": "../jest-runtime"}, {"path": "../jest-test-result"} ] diff --git a/packages/jest-transform/src/ScriptTransformer.ts b/packages/jest-transform/src/ScriptTransformer.ts index 8fb433899e59..081ca1d8b272 100644 --- a/packages/jest-transform/src/ScriptTransformer.ts +++ b/packages/jest-transform/src/ScriptTransformer.ts @@ -773,18 +773,18 @@ class ScriptTransformer { }, }, ); - const module: ModuleType = await requireOrImportModule( - moduleName, - applyInteropRequireDefault, - ); + try { + const module: ModuleType = await requireOrImportModule( + moduleName, + applyInteropRequireDefault, + ); - if (!callback) { - revertHook(); + if (!callback) { + revertHook(); - return module; - } + return module; + } - try { const cbResult = callback(module); if (isPromise(cbResult)) { @@ -792,11 +792,11 @@ class ScriptTransformer { () => module, ); } + + return module; } finally { revertHook(); } - - return module; } shouldTransform(filename: Config.Path): boolean { diff --git a/yarn.lock b/yarn.lock index 3a10d67d66db..09304724cd7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2761,7 +2761,6 @@ __metadata: "@types/graceful-fs": ^4.1.3 graceful-fs: ^4.2.4 jest-haste-map: ^27.0.1 - jest-runner: ^27.0.1 jest-runtime: ^27.0.1 languageName: unknown linkType: soft @@ -13262,7 +13261,6 @@ fsevents@^1.2.7: jest-each: ^27.0.1 jest-matcher-utils: ^27.0.1 jest-message-util: ^27.0.1 - jest-runner: ^27.0.1 jest-runtime: ^27.0.1 jest-snapshot: ^27.0.1 jest-snapshot-serializer-raw: ^1.1.0 @@ -13698,6 +13696,7 @@ fsevents@^1.2.7: jest-haste-map: ^27.0.1 jest-pnp-resolver: ^1.2.2 jest-util: ^27.0.1 + jest-validate: ^27.0.1 resolve: ^1.20.0 slash: ^3.0.0 languageName: unknown @@ -13731,7 +13730,6 @@ fsevents@^1.2.7: emittery: ^0.8.1 exit: ^0.1.2 graceful-fs: ^4.2.4 - jest-config: ^27.0.1 jest-docblock: ^27.0.1 jest-haste-map: ^27.0.1 jest-jasmine2: ^27.0.1