Search code examples
typescriptjestjses6-modulests-jest

Use jest with Typescript, ESM and aliases


I developed a module with Typescipt and ESM ("type": "module" in package.json).

I also use some path aliases, this is the tsconfig.json

{
    "compilerOptions": {
        "moduleResolution": "Node16",
        "module": "Node16",
        "target": "ES2015",
        "lib": [
            "ES2022"
        ],
        "resolveJsonModule": true,
        "strictNullChecks": true,
        "sourceMap": true,
        "declaration": false,
        "downlevelIteration": true,
        "esModuleInterop": true,
        "baseUrl": ".",
        "paths": {
            "@/*": [
                "./source/*"
            ],
            "@": [
                "./source"
            ],
            "@src/*": [
                "./source/*"
            ],
            "@src": [
                "./source"
            ],
            "@test/*": [
                "./test/*"
            ],
            "@test": [
                "./test"
            ]
        },
        "outDir": "./dist"
    },
    "include": [
        "source",
        "test"
    ]
}

And this is the jest.config.ts

import type { Config } from '@jest/types';
import { pathsToModuleNameMapper } from 'ts-jest';
import tsconfigJson from './tsconfig.json';

const config: Config.InitialOptions = {
    preset: 'ts-jest/presets/default-esm',
    testEnvironment: 'node',
    verbose: true,
    globals: {
        'ts-jest': {
            tsconfig: './tsconfig.json',
            useESM: true
        }
    },
    moduleNameMapper: pathsToModuleNameMapper(tsconfigJson.compilerOptions.paths, { prefix: '<rootDir>/' }),
    transformIgnorePatterns: ['<rootDir>/node_modules/']
};
export default config;

The problem is that, because of ESM, the modules do not work with the aliases:

 FAIL  test/suites/modules/mangleTypes.test.ts
  ● Test suite failed to run

    Configuration error:
    
    Could not locate module @src/modules/mangleTypes.js mapped as:
    /home/euber/Github/lifeware-java-mangler/source/$1.
    
    Please check your configuration for these entries:
    {
      "moduleNameMapper": {
        "/^@src\/(.*)$/": "/home/euber/Github/lifeware-java-mangler/source/$1"
      },
      "resolver": undefined
    }

    > 1 | import { mangleType, PrimitiveType } from '@src/modules/mangleTypes.js';
        | ^
      2 |
      3 | describe('Test @/modules/mangleTypes', function () {
      4 |     describe('Primitive types', function () {

      at createNoMappedModuleFoundError (node_modules/jest-resolve/build/resolver.js:900:17)
      at Object.<anonymous> (test/suites/modules/mangleTypes.test.ts:1:1)

I referenced this as a solution, but it does not work.

UPDATE I tried also like this, but it does not work


Solution

  • Eventually I managed to make it work by mixing the two solutions:

    import type { Config } from '@jest/types';
    import { pathsToModuleNameMapper } from 'ts-jest';
    import tsconfigJson from './tsconfig.json';
    
    function manageKey(key: string): string {
       return key.includes('(.*)') ? key.slice(0, -1) + '\\.js$' : key;
    }
    function manageMapper(mapper: Record<string, string>): Record<string, string> {
       const newMapper: Record<string, string> = {};
       for (const key in mapper) {
          newMapper[manageKey(key)] = mapper[key];
       }
       newMapper['^\.\/(.*)\\.js$'] = './$1';
       return newMapper;
    }
    
    const config: Config.InitialOptions = {
        preset: 'ts-jest/presets/default-esm',
        testEnvironment: 'node',
        verbose: true,
        globals: {
            'ts-jest': {
                tsconfig: './tsconfig.json',
                useESM: true
            }
        },
        moduleNameMapper: manageMapper(pathsToModuleNameMapper(tsconfigJson.compilerOptions.paths, { prefix: '<rootDir>/' }) as Record<string, string>),
        transformIgnorePatterns: ['<rootDir>/node_modules/']
    };
    export default config;
    

    UPDATE:

    newMapper['^\.\/(.*)\\.js$'] = './$1'; does not work in cases like ../utils/index.js. To replace the .js with everything, the regex should be changed with something like newMapper['^(.*).js$'] = '$1';.

    The total code would be:

    import type { Config } from '@jest/types';
    import { pathsToModuleNameMapper } from 'ts-jest';
    import tsconfigJson from './tsconfig.json';
    
    function manageKey(key: string): string {
       return key.includes('(.*)') ? key.slice(0, -1) + '\\.js$' : key;
    }
    function manageMapper(mapper: Record<string, string>): Record<string, string> {
       const newMapper: Record<string, string> = {};
       for (const key in mapper) {
          newMapper[manageKey(key)] = mapper[key];
       }
       newMapper['^(.*).js$'] = '$1';
       return newMapper;
    }
    
    const config: Config.InitialOptions = {
        preset: 'ts-jest/presets/default-esm',
        testEnvironment: 'node',
        verbose: true,
        globals: {
            'ts-jest': {
                tsconfig: './tsconfig.json',
                useESM: true
            }
        },
        coverageProvider: 'v8',
        moduleNameMapper: manageMapper(pathsToModuleNameMapper(tsconfigJson.compilerOptions.paths, { prefix: '<rootDir>/' }) as Record<string, string>),
        transformIgnorePatterns: ['<rootDir>/node_modules/']
    };
    export default config;