diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e5947320ebe..094fd2929697 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - `[jest-runtime]` Fix issue where user cannot utilize dynamic import despite specifying `--experimental-vm-modules` Node option ([#15842](https://github.com/jestjs/jest/pull/15842)) - `[jest-test-sequencer]` Fix issue where failed tests due to compilation errors not getting re-executed even with `--onlyFailures` CLI option ([#15851](https://github.com/jestjs/jest/pull/15851)) +- `[jest-config]` Fix issue where Jest test deps `@jest/test-sequencer` and `jest-environment-node` cannot be resolved correctly when used in `pnpm`-based monorepo ([#15877](https://github.com/jestjs/jest/pull/15877)) ### Chore & Maintenance diff --git a/packages/jest-config/src/__tests__/normalize.test.ts b/packages/jest-config/src/__tests__/normalize.test.ts index 3939302c890a..ba7c6c091b5d 100644 --- a/packages/jest-config/src/__tests__/normalize.test.ts +++ b/packages/jest-config/src/__tests__/normalize.test.ts @@ -15,6 +15,7 @@ import Defaults from '../Defaults'; import {DEFAULT_JS_PATTERN} from '../constants'; import normalize, {type AllOptions} from '../normalize'; +const env = {...process.env}; const DEFAULT_CSS_PATTERN = '\\.(css)$'; jest @@ -85,6 +86,7 @@ beforeEach(() => { afterEach(() => { jest.mocked(console.warn).mockRestore(); + process.env = env; }); it('picks an id based on the rootDir', async () => { @@ -814,6 +816,21 @@ describe('testEnvironment', () => { ); }); + it('resolves to node environment if given arg matches with default value', async () => { + process.env.npm_config_user_agent = 'pnpm'; + const {options} = await normalize( + { + rootDir: '/root', + testEnvironment: 'node', + }, + {} as Config.Argv, + ); + + expect(options.testEnvironment).toEqual( + require.resolve('jest-environment-node'), + ); + }); + it('throws on invalid environment names', async () => { await expect( normalize( @@ -2204,3 +2221,20 @@ describe('runInBand', () => { expect(options.runInBand).toBe(true); }); }); + +describe('testSequencer', () => { + it('resolves to @jest/test-sequencer if given arg matches with default value and pnpm is used', async () => { + process.env.npm_config_user_agent = 'pnpm'; + const {options} = await normalize( + { + rootDir: '/root/path/foo', + testSequencer: '@jest/test-sequencer', + }, + {} as Config.Argv, + ); + + expect(options.testSequencer).toMatch( + require.resolve('@jest/test-sequencer'), + ); + }); +}); diff --git a/packages/jest-config/src/__tests__/utils.test.ts b/packages/jest-config/src/__tests__/utils.test.ts new file mode 100644 index 000000000000..b8a2784f57ef --- /dev/null +++ b/packages/jest-config/src/__tests__/utils.test.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import {tmpdir} from 'os'; +import {useSpecificPackageManager} from '../utils'; +import path from 'path'; +import {cleanup, writeFiles} from '../../../../e2e/Utils'; + +const DIR = path.resolve(tmpdir(), 'jest_config_utils_test'); +const env = {...process.env}; + +beforeEach(() => { + cleanup(DIR); +}); + +afterEach(() => { + process.env = env; + cleanup(DIR); +}); + +describe('useSpecificPackageManager', () => { + it('returns true when package manager matches with arg is used', () => { + writeFiles(DIR, { + 'pnpm-lock.yaml': "lockfileVersion: '9.0'", + }); + process.env.npm_config_user_agent = 'pnpm'; + expect(useSpecificPackageManager('pnpm', DIR)).toBe(true); + }); + + it('returns true when package manager is not used but signature lockfile can be found', () => { + writeFiles(DIR, { + 'pnpm-lock.yaml': "lockfileVersion: '9.0'", + }); + process.env.npm_config_user_agent = 'node'; + expect(useSpecificPackageManager('pnpm', DIR)).toBe(true); + }); + + it('returns false when package manager different from arg is used', () => { + process.env.npm_config_user_agent = 'something_else'; + expect(useSpecificPackageManager('npm', DIR)).toBe(false); + }); +}); diff --git a/packages/jest-config/src/normalize.ts b/packages/jest-config/src/normalize.ts index e2ed840cf020..89e3a57ade3f 100644 --- a/packages/jest-config/src/normalize.ts +++ b/packages/jest-config/src/normalize.ts @@ -49,6 +49,7 @@ import { escapeGlobCharacters, replaceRootDirInPath, resolve, + useSpecificPackageManager, } from './utils'; const ERROR = `${BULLET}Validation Error`; @@ -169,7 +170,9 @@ const setupPreset = async ( ); } throw createConfigError( - ` Preset ${chalk.bold(presetPath)} not found relative to rootDir ${chalk.bold(options.rootDir)}.`, + ` Preset ${chalk.bold( + presetPath, + )} not found relative to rootDir ${chalk.bold(options.rootDir)}.`, ); } throw createConfigError( @@ -518,12 +521,20 @@ export default async function normalize( options.setupFilesAfterEnv = []; } + // For default Jest test env, let's use native resolution mechanism + // of JS runtime instead of resorting to third-party ones. + let testEnvironment = options.testEnvironment; + if ( + `jest-environment-${testEnvironment}` === DEFAULT_CONFIG.testEnvironment && + useSpecificPackageManager('pnpm', options.rootDir) + ) { + testEnvironment = require.resolve(DEFAULT_CONFIG.testEnvironment); + } options.testEnvironment = resolveTestEnvironment({ requireResolveFunction: requireResolve, rootDir: options.rootDir, testEnvironment: - options.testEnvironment || - require.resolve(DEFAULT_CONFIG.testEnvironment), + testEnvironment || require.resolve(DEFAULT_CONFIG.testEnvironment), }); if (!options.roots) { @@ -997,9 +1008,17 @@ export default async function normalize( // ignored } + // For default Jest test sequencer, let's use native resolution mechanism + // of JS runtime instead of resorting to third-party ones. + let testSequencer = options.testSequencer; + if ( + options.testSequencer === DEFAULT_CONFIG.testSequencer && + useSpecificPackageManager('pnpm', options.rootDir) + ) { + testSequencer = require.resolve(DEFAULT_CONFIG.testSequencer); + } newOptions.testSequencer = resolveSequencer(newOptions.resolver, { - filePath: - options.testSequencer || require.resolve(DEFAULT_CONFIG.testSequencer), + filePath: testSequencer || require.resolve(DEFAULT_CONFIG.testSequencer), requireResolveFunction: requireResolve, rootDir: options.rootDir, }); diff --git a/packages/jest-config/src/utils.ts b/packages/jest-config/src/utils.ts index 0ce93cf316bc..b50b7cd66992 100644 --- a/packages/jest-config/src/utils.ts +++ b/packages/jest-config/src/utils.ts @@ -7,6 +7,7 @@ import * as path from 'path'; import chalk from 'chalk'; +import {existsSync} from 'graceful-fs'; import Resolver from 'jest-resolve'; import {ValidationError} from 'jest-validate'; @@ -119,3 +120,16 @@ export const isJSONString = (text?: JSONString | string): text is JSONString => typeof text === 'string' && text.startsWith('{') && text.endsWith('}'); + +export const useSpecificPackageManager = ( + identifier: string, + rootDir: string, +): boolean => { + let checkLockFile = false; + if (identifier === 'pnpm') { + checkLockFile = existsSync(path.join(rootDir, 'pnpm-lock.yaml')); + } + return ( + checkLockFile || !!process.env.npm_config_user_agent?.includes(identifier) + ); +};