From 20e66df49cf13e5cba909e9f06739c7c847b02be Mon Sep 17 00:00:00 2001 From: Rafael Rabelo Date: Thu, 30 Nov 2023 05:34:04 -0300 Subject: [PATCH] feat: load referenced config from package.json (#14044) --- CHANGELOG.md | 1 + docs/Configuration.md | 9 +++ .../src/__tests__/resolveConfigPath.test.ts | 66 +++++++++++++++++++ packages/jest-config/src/resolveConfigPath.ts | 47 ++++++++++--- 4 files changed, 114 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 402c9a3d996a..25b3a5e1e402 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - `[jest-circus, jest-cli, jest-config]` Add `waitNextEventLoopTurnForUnhandledRejectionEvents` flag to minimise performance impact of correct detection of unhandled promise rejections introduced in [#14315](https://github.com/jestjs/jest/pull/14315) ([#14681](https://github.com/jestjs/jest/pull/14681)) - `[jest-config]` [**BREAKING**] Add `mts` and `cts` to default `moduleFileExtensions` config ([#14369](https://github.com/facebook/jest/pull/14369)) - `[jest-config]` [**BREAKING**] Update `testMatch` and `testRegex` default option for supporting `mjs`, `cjs`, `mts`, and `cts` ([#14584](https://github.com/jestjs/jest/pull/14584)) +- `[jest-config]` Loads config file from provided path in `package.json` ([#14044](https://github.com/facebook/jest/pull/14044)) - `[@jest/core]` [**BREAKING**] Group together open handles with the same stack trace ([#13417](https://github.com/jestjs/jest/pull/13417), & [#14543](https://github.com/jestjs/jest/pull/14543)) - `[@jest/core]` Add `perfStats` to surface test setup overhead ([#14622](https://github.com/jestjs/jest/pull/14622)) - `[@jest/core]` [**BREAKING**] Changed `--filter` to accept an object with shape `{ filtered: Array }` to match [documentation](https://jestjs.io/docs/cli#--filterfile) ([#13319](https://github.com/jestjs/jest/pull/13319)) diff --git a/docs/Configuration.md b/docs/Configuration.md index fdf9b9585938..208f188d22d0 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -81,6 +81,15 @@ Alternatively Jest's configuration can be defined through the `"jest"` key in th } ``` +Also Jest's configuration json file can be referenced through the `"jest"` key in the `package.json` of your project: + +```json title="package.json" +{ + "name": "my-project", + "jest": "./path/to/config.json" +} +``` + ## Options :::info diff --git a/packages/jest-config/src/__tests__/resolveConfigPath.test.ts b/packages/jest-config/src/__tests__/resolveConfigPath.test.ts index 0b16288ba1fd..a1d9b51a1987 100644 --- a/packages/jest-config/src/__tests__/resolveConfigPath.test.ts +++ b/packages/jest-config/src/__tests__/resolveConfigPath.test.ts @@ -112,6 +112,72 @@ describe.each(JEST_CONFIG_EXT_ORDER.slice(0))( ); }).toThrow(NO_ROOT_DIR_ERROR_PATTERN); }); + + test('file path from "jest" key', () => { + const anyFileName = `anyJestConfigfile${extension}`; + const relativePackageJsonPath = 'a/b/c/package.json'; + const relativeAnyFilePath = `a/b/c/conf/${anyFileName}`; + const absolutePackageJsonPath = path.resolve( + DIR, + relativePackageJsonPath, + ); + const absoluteAnyFilePath = path.resolve(DIR, relativeAnyFilePath); + + writeFiles(DIR, { + 'a/b/c/package.json': `{ "jest": "conf/${anyFileName}" }`, + }); + writeFiles(DIR, {[relativeAnyFilePath]: ''}); + + const result = resolveConfigPath( + path.dirname(absolutePackageJsonPath), + DIR, + ); + + expect(result).toBe(absoluteAnyFilePath); + }); + + test('not a file path from "jest" key', () => { + const anyFileName = `anyJestConfigfile${extension}`; + const relativePackageJsonPath = 'a/b/c/package.json'; + const relativeAnyFilePath = `a/b/c/conf/${anyFileName}`; + const absolutePackageJsonPath = path.resolve( + DIR, + relativePackageJsonPath, + ); + + writeFiles(DIR, { + 'a/b/c/package.json': '{ "jest": {"verbose": true} }', + }); + writeFiles(DIR, {[relativeAnyFilePath]: ''}); + + const result = resolveConfigPath( + path.dirname(absolutePackageJsonPath), + DIR, + ); + + expect(result).toBe(absolutePackageJsonPath); + }); + + test('not a valid file when "jest" key is a path', () => { + const anyFileName = `anyJestConfigfile${extension}`; + const relativePackageJsonPath = 'a/b/c/package.json'; + const relativeAnyFilePath = `a/b/c/conf/${anyFileName}`; + const absolutePackageJsonPath = path.resolve( + DIR, + relativePackageJsonPath, + ); + + writeFiles(DIR, { + 'a/b/c/package.json': '{ "jest": "conf/nonExistentConfigfile.json" }', + }); + writeFiles(DIR, {[relativeAnyFilePath]: ''}); + + expect(() => + resolveConfigPath(path.dirname(absolutePackageJsonPath), DIR), + ).toThrow( + /Jest expects the string configuration to point to a file, but .* not\./, + ); + }); }, ); diff --git a/packages/jest-config/src/resolveConfigPath.ts b/packages/jest-config/src/resolveConfigPath.ts index 4010ff8dc6d0..d6cd21da82d2 100644 --- a/packages/jest-config/src/resolveConfigPath.ts +++ b/packages/jest-config/src/resolveConfigPath.ts @@ -75,8 +75,33 @@ const resolveConfigPathByTraversing = ( ).filter(isFile); const packageJson = findPackageJson(pathToResolve); - if (packageJson && hasPackageJsonJestKey(packageJson)) { - configFiles.push(packageJson); + + if (packageJson) { + const jestKey = getPackageJsonJestKey(packageJson); + + if (jestKey) { + if (typeof jestKey === 'string') { + const absolutePath = path.isAbsolute(jestKey) + ? jestKey + : path.resolve(pathToResolve, jestKey); + + if (!isFile(absolutePath)) { + throw new ValidationError( + `${BULLET}Validation Error`, + ` Configuration in ${chalk.bold(packageJson)} is not valid. ` + + `Jest expects the string configuration to point to a file, but ${absolutePath} is not. ` + + `Please check your Jest configuration in ${chalk.bold( + packageJson, + )}.`, + DOCUMENTATION_NOTE, + ); + } + + configFiles.push(absolutePath); + } else { + configFiles.push(packageJson); + } + } } if (!skipMultipleConfigError && configFiles.length > 1) { @@ -111,14 +136,18 @@ const findPackageJson = (pathToResolve: string) => { return undefined; }; -const hasPackageJsonJestKey = (packagePath: string) => { - const content = fs.readFileSync(packagePath, 'utf8'); +const getPackageJsonJestKey = ( + packagePath: string, +): Record | string | undefined => { try { - return 'jest' in JSON.parse(content); - } catch { - // If package is not a valid JSON - return false; - } + const content = fs.readFileSync(packagePath, 'utf8'); + const parsedContent = JSON.parse(content); + + if ('jest' in parsedContent) { + return parsedContent.jest; + } + } catch {} + return undefined; }; const makeResolutionErrorMessage = (initialPath: string, cwd: string) =>