From d9685775129f5f2cdf84270b10e821e5a7e5a67d Mon Sep 17 00:00:00 2001 From: hainenber Date: Sun, 26 Oct 2025 17:30:48 +0700 Subject: [PATCH 1/4] feat(jest-config): correctly resolve Jest test deps in monorepo that use `pnpm` Signed-off-by: hainenber --- .../src/__tests__/normalize.test.ts | 34 +++++++++++++++++++ .../jest-config/src/__tests__/utils.test.ts | 26 ++++++++++++++ packages/jest-config/src/normalize.ts | 33 ++++++++++++++---- packages/jest-config/src/utils.ts | 3 ++ 4 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 packages/jest-config/src/__tests__/utils.test.ts 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..99b31dc31fda --- /dev/null +++ b/packages/jest-config/src/__tests__/utils.test.ts @@ -0,0 +1,26 @@ +/** + * 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 {useSpecificPackageManager} from '../utils'; + +const env = {...process.env}; + +afterEach(() => { + process.env = env; +}); + +describe('useSpecificPackageManager', () => { + it('returns true when package manager matches with arg is used', () => { + process.env.npm_config_user_agent = 'pnpm'; + expect(useSpecificPackageManager('pnpm')).toBe(true); + }); + it('returns false when package manager different from arg is used', () => { + process.env.npm_config_user_agent = 'something_else'; + expect(useSpecificPackageManager('pnpm')).toBe(false); + }); +}); diff --git a/packages/jest-config/src/normalize.ts b/packages/jest-config/src/normalize.ts index e2ed840cf020..f1835bf7a0f1 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') + ) { + 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') + ) { + 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, }); @@ -1087,8 +1106,8 @@ export default async function normalize( newOptions.ci && !argv.updateSnapshot ? 'none' : argv.updateSnapshot - ? 'all' - : 'new'; + ? 'all' + : 'new'; newOptions.maxConcurrency = Number.parseInt( newOptions.maxConcurrency as unknown as string, diff --git a/packages/jest-config/src/utils.ts b/packages/jest-config/src/utils.ts index 0ce93cf316bc..823106a1adf5 100644 --- a/packages/jest-config/src/utils.ts +++ b/packages/jest-config/src/utils.ts @@ -119,3 +119,6 @@ export const isJSONString = (text?: JSONString | string): text is JSONString => typeof text === 'string' && text.startsWith('{') && text.endsWith('}'); + +export const useSpecificPackageManager = (identifier: string): boolean => + !!process.env.npm_config_user_agent?.includes(identifier); From 157ddc9335e8085d1508e5fa87b784ceeb5f0387 Mon Sep 17 00:00:00 2001 From: hainenber Date: Sun, 26 Oct 2025 17:48:08 +0700 Subject: [PATCH 2/4] chore: add CHANGELOG entry Signed-off-by: hainenber --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 564e7585c84f..c61c34f2d072 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,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 From 7cc5d31f072d43ab3549003e72bd7595da21a756 Mon Sep 17 00:00:00 2001 From: hainenber Date: Sun, 26 Oct 2025 22:50:06 +0700 Subject: [PATCH 3/4] chore: fix eslint issue Signed-off-by: hainenber --- packages/jest-config/src/normalize.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/jest-config/src/normalize.ts b/packages/jest-config/src/normalize.ts index f1835bf7a0f1..035b67364137 100644 --- a/packages/jest-config/src/normalize.ts +++ b/packages/jest-config/src/normalize.ts @@ -1106,8 +1106,8 @@ export default async function normalize( newOptions.ci && !argv.updateSnapshot ? 'none' : argv.updateSnapshot - ? 'all' - : 'new'; + ? 'all' + : 'new'; newOptions.maxConcurrency = Number.parseInt( newOptions.maxConcurrency as unknown as string, From bbb3f03808ede7a43a294526da232c6c57e551e9 Mon Sep 17 00:00:00 2001 From: hainenber Date: Wed, 29 Oct 2025 21:35:14 +0700 Subject: [PATCH 4/4] fix: correctly resolve Jest test deps in pnpm projects by checking signature lockfile Signed-off-by: hainenber --- .../jest-config/src/__tests__/utils.test.ts | 25 +++++++++++++++++-- packages/jest-config/src/normalize.ts | 4 +-- packages/jest-config/src/utils.ts | 15 +++++++++-- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/packages/jest-config/src/__tests__/utils.test.ts b/packages/jest-config/src/__tests__/utils.test.ts index 99b31dc31fda..b8a2784f57ef 100644 --- a/packages/jest-config/src/__tests__/utils.test.ts +++ b/packages/jest-config/src/__tests__/utils.test.ts @@ -6,21 +6,42 @@ * */ +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')).toBe(true); + 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('pnpm')).toBe(false); + expect(useSpecificPackageManager('npm', DIR)).toBe(false); }); }); diff --git a/packages/jest-config/src/normalize.ts b/packages/jest-config/src/normalize.ts index 035b67364137..89e3a57ade3f 100644 --- a/packages/jest-config/src/normalize.ts +++ b/packages/jest-config/src/normalize.ts @@ -526,7 +526,7 @@ export default async function normalize( let testEnvironment = options.testEnvironment; if ( `jest-environment-${testEnvironment}` === DEFAULT_CONFIG.testEnvironment && - useSpecificPackageManager('pnpm') + useSpecificPackageManager('pnpm', options.rootDir) ) { testEnvironment = require.resolve(DEFAULT_CONFIG.testEnvironment); } @@ -1013,7 +1013,7 @@ export default async function normalize( let testSequencer = options.testSequencer; if ( options.testSequencer === DEFAULT_CONFIG.testSequencer && - useSpecificPackageManager('pnpm') + useSpecificPackageManager('pnpm', options.rootDir) ) { testSequencer = require.resolve(DEFAULT_CONFIG.testSequencer); } diff --git a/packages/jest-config/src/utils.ts b/packages/jest-config/src/utils.ts index 823106a1adf5..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'; @@ -120,5 +121,15 @@ export const isJSONString = (text?: JSONString | string): text is JSONString => text.startsWith('{') && text.endsWith('}'); -export const useSpecificPackageManager = (identifier: string): boolean => - !!process.env.npm_config_user_agent?.includes(identifier); +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) + ); +};