Skip to content

Fix Jest config file imports for Jest v30 ESM compatibility#551

Open
kapral18 wants to merge 3 commits intosorenlouv:mainfrom
kapral18:fix/jest-config-esm-imports
Open

Fix Jest config file imports for Jest v30 ESM compatibility#551
kapral18 wants to merge 3 commits intosorenlouv:mainfrom
kapral18:fix/jest-config-esm-imports

Conversation

@kapral18
Copy link
Copy Markdown
Contributor

@kapral18 kapral18 commented Oct 11, 2025

Summary

Fixes Jest config file imports to work with Jest v30's ESM requirements. Jest v30 switched to using Node's native ESM loader for config files, which requires explicit file extensions in imports.

The Problem

The test-all, test-mutation, and test-private scripts were failing with:

Error [ERR_MODULE_NOT_FOUND]: Cannot find module './jest.config' imported from jest.config.all.ts

Jest v30 config files are now loaded by Node's ESM loader, which requires explicit .ts extensions, but TypeScript doesn't allow .ts extensions in imports by default.

The Solution

How Jest v30 Loads TypeScript Config Files

When Jest v30 encounters a TypeScript config file (e.g., jest.config.private.ts), it uses ts-node internally to compile it. However, ts-node is registered with these default options:

tsLoader.register({
  compilerOptions: {
    module: 'CommonJS'
  },
  moduleTypes: {
    '**': 'cjs'
  }
})

This forces all files to be treated as CommonJS, which doesn't allow .ts extensions in imports without allowImportingTsExtensions enabled.

The Fix: Jest Docblock Pragma

Jest supports passing custom options to ts-node via the @jest-config-loader-options docblock pragma. By adding this to config files that import other .ts files:

/**
 * @jest-config-loader-options {"compilerOptions":{"allowImportingTsExtensions":true}}
 */
import baseConfig from './jest.config.ts';

This tells ts-node to enable allowImportingTsExtensions when compiling these specific config files, allowing the .ts extension imports to work correctly.

Why This Works Across All Node Versions

  • Node < 22.18.0: Relies on ts-node with the docblock pragma configuration
  • Node >= 22.18.0: Can use native TypeScript support, but the docblock pragma is harmless and ensures backward compatibility

Changes

  • ✅ Added @jest-config-loader-options docblock pragma to jest.config.all.ts, jest.config.mutation.ts, and jest.config.private.ts
  • ✅ Updated config files to use explicit .ts extensions in imports
  • ✅ Created tsconfig.jest-config.json for IDE support (TypeScript project references)
  • ✅ Configured ESLint to handle Jest config files separately
  • ✅ Added .tsbuildinfo/ to .gitignore

Benefits

  • ✅ All test scripts work: test-all, test-mutation, test-private
  • ✅ Compatible with all Node 22.x versions (tested with 22.17.1 and 22.20.0)
  • ✅ Full IDE support via TypeScript project references
  • ✅ Proper type checking for all files
  • ✅ Clean, maintainable solution without workarounds

Technical Notes

  • Uses Jest's built-in docblock pragma support for ts-node configuration
  • Only Jest config files need .ts extensions (not test files - those use ts-jest)
  • Test files continue using traditional imports without extensions
  • The tsconfig.jest-config.json provides IDE support but isn't used by Jest at runtime

Testing

# Verify all test scripts can load their configs and find tests
yarn test-all --listTests       # Should find ~65 test files
yarn test-mutation --listTests  # Should find 3 mutation test files  
yarn test-private --listTests   # Should find 28 private test files

# Verify linting and type checking still work
yarn lint                        # Should pass
npx tsc --build                  # Should build successfully

Tested with:

  • ✅ Node 22.17.1 (without native TS support)
  • ✅ Node 22.20.0 (with native TS support)

@sorenlouv
Copy link
Copy Markdown
Owner

When I run: yarn test-private --listTests I get this error:

yarn run v1.22.22
$ jest --config ./jest.config.private.ts --runInBand --listTests
Error: Jest: Failed to parse the TypeScript config file /Users/sorenlouv/dev/backport/jest.config.private.ts
  TSError: ⨯ Unable to compile TypeScript:
jest.config.private.ts:2:24 - error TS5097: An import path can only end with a '.ts' extension when 'allowImportingTsExtensions' is enabled.

2 import baseConfig from './jest.config.ts';
                         ~~~~~~~~~~~~~~~~~~

    at readConfigFileAndSetRootDir (/Users/sorenlouv/dev/backport/node_modules/@jest/core/node_modules/jest-config/build/index.js:2242:13)
    at async readInitialOptions (/Users/sorenlouv/dev/backport/node_modules/@jest/core/node_modules/jest-config/build/index.js:1140:15)
    at async readConfig (/Users/sorenlouv/dev/backport/node_modules/@jest/core/node_modules/jest-config/build/index.js:918:7)
    at async readConfigs (/Users/sorenlouv/dev/backport/node_modules/@jest/core/node_modules/jest-config/build/index.js:1168:26)
    at async runCLI (/Users/sorenlouv/dev/backport/node_modules/@jest/core/build/index.js:1393:7)
    at async Object.run (/Users/sorenlouv/dev/backport/node_modules/jest-cli/build/index.js:656:9)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

@kapral18 kapral18 force-pushed the fix/jest-config-esm-imports branch from d365b21 to 8836ab4 Compare October 30, 2025 23:38
@kapral18
Copy link
Copy Markdown
Contributor Author

Thanks for testing this! I've identified and fixed the issue.

Root Cause

The error you encountered is due to a difference in Node.js versions:

  • Your environment: Node 22.17.1 or earlier (doesn't have native TypeScript support)
  • My environment: Node 22.18.0+ (has native TypeScript support enabled by default)

Node 22.18.0 introduced native TypeScript support with type stripping, which allows Node to natively handle .ts files and .ts extensions in imports. This is why it worked on my machine but failed on yours.

How Jest v30 Loads TypeScript Config Files

When Jest v30 encounters a TypeScript config file, it uses ts-node internally to compile it. The issue is that ts-node is registered with these default options:

tsLoader.register({
  compilerOptions: {
    module: 'CommonJS'
  },
  moduleTypes: {
    '**': 'cjs'
  }
})

This forces CommonJS mode, which doesn't allow .ts extensions in imports without allowImportingTsExtensions enabled.

The Fix

Jest supports passing custom options to ts-node via the @jest-config-loader-options docblock pragma. I've added this to all config files that import other .ts files:

/**
 * @jest-config-loader-options {"compilerOptions":{"allowImportingTsExtensions":true}}
 */
import baseConfig from './jest.config.ts';

This tells ts-node to enable allowImportingTsExtensions when compiling these config files, allowing the .ts extension imports to work correctly.

Testing

I've verified this fix works with both:

  • Node 22.17.1 (without native TS support) - uses ts-node with docblock pragma
  • Node 22.20.0 (with native TS support) - works natively, docblock pragma is harmless

All test commands now work correctly:

yarn test-all --listTests       # ✅ Works
yarn test-mutation --listTests  # ✅ Works
yarn test-private --listTests   # ✅ Works

The solution ensures compatibility across all Node 22.x versions without requiring a minimum version bump.

Comment thread tsconfig.json
{
"references": [
{ "path": "./tsconfig.jest-config.json" }
],
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why use project references for jest configs? Why not have a separate tsconfig, like the one for eslint https://github.com/sorenlouv/backport/blob/main/tsconfig.eslint.json?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tsconfig.jest-config.json is not a standard path for tsconfig lsps to pickup. So IDEs and editors just don't see it. With eslint config it consumes the custom path of tsconfig directly but with these custom jest-config ts files there is nowhere to link the two together. So reference paths is a natural way to split it while exposing a single entry point for discovery by whatever client.

Jest v30 switched to using Node's native ESM loader for config files,
which requires explicit file extensions in imports. This change adds
support for .ts extensions in Jest config file imports while maintaining
proper TypeScript and ESLint integration.

Changes:
- Created tsconfig.jest-config.json: Separate TypeScript config for Jest
  config files that enables allowImportingTsExtensions
- Updated tsconfig.json: Added project reference to tsconfig.jest-config.json
  and excluded jest.config*.ts files to prevent overlap
- Updated jest.config.*.ts: Changed imports to use explicit .ts extensions
  (e.g., import from './jest.config.ts')
- Updated eslint.config.js: Added specific configuration for Jest config
  files to use the correct tsconfig and ignore build artifacts
- Updated .gitignore: Added .tsbuildinfo/ to ignore TypeScript build output

Benefits:
- IDE support: IDEs automatically use the correct tsconfig via project references
- Clean imports: No @ts-ignore hacks needed
- Full type checking: All files properly type-checked
- All test scripts work: test-all, test-mutation, test-private

Technical details:
- Uses TypeScript composite projects with emitDeclarationOnly to satisfy
  both the composite requirement and allowImportingTsExtensions constraint
- Jest config files loaded by Node ESM require .ts extensions
- Test files continue to use traditional imports (handled by ts-jest)
Add @jest-config-loader-options docblock pragma to Jest config files
that import other .ts files. This ensures ts-node compiles these files
with allowImportingTsExtensions enabled, which is required for .ts
extension imports to work.

The issue occurs because Jest v30 uses ts-node to compile TypeScript
config files, and ts-node registers with module: 'CommonJS' by default,
which doesn't allow .ts extensions in imports. The docblock pragma
passes compiler options to ts-node, enabling the required setting.

This fix ensures compatibility across all Node 22.x versions, including
versions prior to 22.18.0 which don't have native TypeScript support.

Tested with Node 22.17.1 and 22.20.0.
@kapral18 kapral18 force-pushed the fix/jest-config-esm-imports branch from 954c7c8 to f51dbaa Compare November 17, 2025 20:08
@kapral18 kapral18 requested a review from sorenlouv November 18, 2025 20:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants