Repository: jestjs/jest (TypeScript)

Issue:
Require of a shared config file returns unwanted rootDir field
<!-- Love Jest? Please consider supporting our collective: 👉  https://opencollective.com/jest/donate -->

## To Reproduce

Create the following monorepo structure with configs that reference each other:

_/jest.config.js_
```js
module.exports = {
  projects: [ 'packages/*' ]
}
```

_/packages/build-scripts/jest.config.js_
```js
module.exports = {
  testMatch: [ '<rootDir>/**/test/*.js' ]
}
```

_/packages/foo/jest.config.js_
```js
const commonConfig = require( '../build-scripts/jest.config.js' );
module.exports = {
  ...commonConfig,
  testEnvironment: 'jsdom',
}
```

## Expected behavior

Each of the configs, when `require`d, will have `rootDir` set to the directory where the `jest.config.js` is. Because the configs don't specify them.

## Actual behavior

The first require of `packages/build-scripts/jest.config.js` (when Jest reads the config) will mutate the module object and will set `rootDir` to `/abs/path/to/packages/build-scripts`.

The second require (from the `packages/foo/jest.config.js` file) will return the module object with `rootDir` already set to `/abs/path/to/packages/build-scripts` and Jest won't set it to `/abs/path/to/packages/foo` as expected.

Can be worked around by carefully setting `rootDir: '.'` on every config that extends the common one.


Hints:
Another issue that has the same root cause: export a frozen object from the `jest.config.js`:
```js
module.exports = Object.freeze( {
  ...
} );
```
Then Jest will fail to read it:
```
TypeError: Cannot assign to read only property 'rootDir' of object '#<Object>'
    at _default (.../node_modules/jest-config/build/readConfigFileAndSetRootDir.js:75:28)
    at readConfig (.../node_modules/jest-config/build/index.js:160:59)
```
Here's a reproduction https://github.com/also/jest-project-shared-config-bug.

The way I experienced this issue was sharing a single base config in each project. This meant that all projects got the `rootDir` of the first and that project ran multiple times while the others never ran.
Oh wow, what a stupid bug 😅 Thanks for the great reproduction @also! Would you be able to send a PR? This diff seems to fix it:

```diff
diff --git i/packages/jest-config/src/readConfigFileAndSetRootDir.ts w/packages/jest-config/src/readConfigFileAndSetRootDir.ts
index e4361c8c1f..b30e37fca0 100644
--- i/packages/jest-config/src/readConfigFileAndSetRootDir.ts
+++ w/packages/jest-config/src/readConfigFileAndSetRootDir.ts
@@ -64,14 +64,17 @@ export default async function readConfigFileAndSetRootDir(
     // We don't touch it if it has an absolute path specified
     if (!path.isAbsolute(configObject.rootDir)) {
       // otherwise, we'll resolve it relative to the file's __dirname
-      configObject.rootDir = path.resolve(
-        path.dirname(configPath),
-        configObject.rootDir,
-      );
+      configObject = {
+        ...configObject,
+        rootDir: path.resolve(path.dirname(configPath), configObject.rootDir),
+      };
     }
   } else {
     // If rootDir is not there, we'll set it to this file's __dirname
-    configObject.rootDir = path.dirname(configPath);
+    configObject = {
+      ...configObject,
+      rootDir: path.dirname(configPath),
+    };
   }
 
   return configObject;
```
This issue is stale because it has been open for 1 year with no activity. Remove stale label or comment or this will be closed in 30 days.

Patch:
diff --git a/packages/jest-config/src/readConfigFileAndSetRootDir.ts b/packages/jest-config/src/readConfigFileAndSetRootDir.ts
--- a/packages/jest-config/src/readConfigFileAndSetRootDir.ts
+++ b/packages/jest-config/src/readConfigFileAndSetRootDir.ts
@@ -64,14 +64,17 @@ export default async function readConfigFileAndSetRootDir(
     // We don't touch it if it has an absolute path specified
     if (!path.isAbsolute(configObject.rootDir)) {
       // otherwise, we'll resolve it relative to the file's __dirname
-      configObject.rootDir = path.resolve(
-        path.dirname(configPath),
-        configObject.rootDir,
-      );
+      configObject = {
+        ...configObject,
+        rootDir: path.resolve(path.dirname(configPath), configObject.rootDir),
+      };
     }
   } else {
     // If rootDir is not there, we'll set it to this file's __dirname
-    configObject.rootDir = path.dirname(configPath);
+    configObject = {
+      ...configObject,
+      rootDir: path.dirname(configPath),
+    };
   }
 
   return configObject;


Test patch:
diff --git a/packages/jest-config/src/__tests__/readConfigFileAndSetRootDir.test.ts b/packages/jest-config/src/__tests__/readConfigFileAndSetRootDir.test.ts
new file mode 100644
--- /dev/null
+++ b/packages/jest-config/src/__tests__/readConfigFileAndSetRootDir.test.ts
@@ -0,0 +1,169 @@
+/**
+ * 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 path = require('path');
+import * as fs from 'graceful-fs';
+import {requireOrImportModule} from 'jest-util';
+import readConfigFileAndSetRootDir from '../readConfigFileAndSetRootDir';
+
+jest.mock('graceful-fs').mock('jest-util');
+
+describe('readConfigFileAndSetRootDir', () => {
+  describe('JavaScript file', () => {
+    test('reads config and sets `rootDir`', async () => {
+      jest.mocked(requireOrImportModule).mockResolvedValueOnce({notify: true});
+
+      const rootDir = path.resolve('some', 'path', 'to');
+      const config = await readConfigFileAndSetRootDir(
+        path.join(rootDir, 'jest.config.js'),
+      );
+
+      expect(config).toEqual({notify: true, rootDir});
+    });
+
+    test('handles exported function', async () => {
+      jest
+        .mocked(requireOrImportModule)
+        .mockResolvedValueOnce(() => ({bail: 1}));
+
+      const rootDir = path.resolve('some', 'path', 'to');
+      const config = await readConfigFileAndSetRootDir(
+        path.join(rootDir, 'jest.config.js'),
+      );
+
+      expect(config).toEqual({bail: 1, rootDir});
+    });
+
+    test('handles exported async function', async () => {
+      jest
+        .mocked(requireOrImportModule)
+        .mockResolvedValueOnce(async () => ({testTimeout: 10000}));
+
+      const rootDir = path.resolve('some', 'path', 'to');
+      const config = await readConfigFileAndSetRootDir(
+        path.join(rootDir, 'jest.config.js'),
+      );
+
+      expect(config).toEqual({rootDir, testTimeout: 10000});
+    });
+  });
+
+  describe('JSON file', () => {
+    test('reads config and sets `rootDir`', async () => {
+      jest.mocked(fs.readFileSync).mockReturnValueOnce('{ "verbose": true }');
+
+      const rootDir = path.resolve('some', 'path', 'to');
+      const config = await readConfigFileAndSetRootDir(
+        path.join(rootDir, 'jest.config.json'),
+      );
+
+      expect(config).toEqual({rootDir, verbose: true});
+    });
+
+    test('supports comments in JSON', async () => {
+      jest
+        .mocked(fs.readFileSync)
+        .mockReturnValueOnce('{ // test comment\n "bail": true }');
+
+      const rootDir = path.resolve('some', 'path', 'to');
+      const config = await readConfigFileAndSetRootDir(
+        path.join(rootDir, 'jest.config.json'),
+      );
+
+      expect(config).toEqual({bail: true, rootDir});
+    });
+  });
+
+  describe('package.json file', () => {
+    test('reads config from "jest" key and sets `rootDir`', async () => {
+      jest
+        .mocked(fs.readFileSync)
+        .mockReturnValueOnce('{ "jest": { "coverage": true } }');
+
+      const rootDir = path.resolve('some', 'path', 'to');
+      const config = await readConfigFileAndSetRootDir(
+        path.join(rootDir, 'package.json'),
+      );
+
+      expect(config).toEqual({coverage: true, rootDir});
+    });
+
+    test('sets rootDir if "jest" is absent', async () => {
+      jest.mocked(fs.readFileSync).mockReturnValueOnce('{ "name": "test" }');
+
+      const rootDir = path.resolve('some', 'path', 'to');
+      const config = await readConfigFileAndSetRootDir(
+        path.join(rootDir, 'package.json'),
+      );
+
+      expect(config).toEqual({rootDir});
+    });
+  });
+
+  describe('sets `rootDir`', () => {
+    test('handles frozen config object', async () => {
+      jest
+        .mocked(requireOrImportModule)
+        .mockResolvedValueOnce(Object.freeze({preset: 'some-preset'}));
+
+      const rootDir = path.resolve('some', 'path', 'to');
+      const config = await readConfigFileAndSetRootDir(
+        path.join(rootDir, 'jest.config.js'),
+      );
+
+      expect(config).toEqual({preset: 'some-preset', rootDir});
+    });
+
+    test('keeps the path if it is absolute', async () => {
+      const rootDir = path.resolve('some', 'path', 'to');
+      jest.mocked(requireOrImportModule).mockResolvedValueOnce({
+        rootDir,
+        testEnvironment: 'node',
+      });
+
+      const config = await readConfigFileAndSetRootDir(
+        path.join(path.resolve('other', 'path', 'to'), 'jest.config.js'),
+      );
+
+      expect(config).toEqual({rootDir, testEnvironment: 'node'});
+    });
+
+    test('resolves the path relative to dirname of the config file', async () => {
+      jest.mocked(requireOrImportModule).mockResolvedValueOnce({
+        restoreMocks: true,
+        rootDir: path.join('path', 'to'),
+      });
+
+      const config = await readConfigFileAndSetRootDir(
+        path.join(path.resolve('some'), 'jest.config.js'),
+      );
+
+      expect(config).toEqual({
+        restoreMocks: true,
+        rootDir: path.resolve('some', 'path', 'to'),
+      });
+    });
+
+    test('resolves relative path when the read config object if frozen', async () => {
+      jest.mocked(requireOrImportModule).mockResolvedValueOnce(
+        Object.freeze({
+          resetModules: true,
+          rootDir: path.join('path', 'to'),
+        }),
+      );
+
+      const config = await readConfigFileAndSetRootDir(
+        path.join(path.resolve('some'), 'jest.config.js'),
+      );
+
+      expect(config).toEqual({
+        resetModules: true,
+        rootDir: path.resolve('some', 'path', 'to'),
+      });
+    });
+  });
+});