I have a monorepo utilizing TypeScript, WebPack, and ts-jest. It builds correctly but unit testing fails on one of the sub-projects ./demo
due to:
Cannot find module '@mlhaufe/graphics/adapters' or its corresponding type declarations.
<root>/tsconfig.json
{
"compilerOptions": {
"baseUrl": "./packages",
"paths": {
"@mlhaufe/graphics/*": [
"lib/src/*"
]
}
},
"references": [
{
"path": "./packages/lib"
},
{
"path": "./packages/demo"
}
],
"files": [],
"exclude": [
"node_modules"
]
}
<root>/packages/demo/tsconfig.json
{
...
"references": [
{
"path": "../lib"
}
]
<root>/jest.config.mjs
import fs from 'fs';
import path from 'path';
import url from 'url';
import { pathsToModuleNameMapper } from 'ts-jest';
import tsconfig from './tsconfig.json' assert { type: 'json' };
const { compilerOptions } = tsconfig,
__filename = url.fileURLToPath(import.meta.url),
__dirname = path.dirname(__filename),
packageNames = fs.readdirSync(path.resolve(__dirname, './packages'));
/** @type {import('jest').Config} */
export default {
rootDir: compilerOptions.baseUrl,
verbose: true,
testPathIgnorePatterns: [
'<rootDir>/node_modules/',
],
reporters: [
'default',
['jest-junit', { outputDirectory: './coverage' }]
],
// <https://jestjs.io/docs/next/configuration#projects-arraystring--projectconfig>
projects: packageNames.map((name) => ({
displayName: name,
transform: {
'^.+\\.mts$': ['ts-jest', { useESM: true }]
},
moduleFileExtensions: ['js', 'mjs', 'mts'],
roots: [`<rootDir>`],
modulePaths: [compilerOptions.baseUrl],
// required due to ts-jest limitation
// <https://kulshekhar.github.io/ts-jest/docs/guides/esm-support/#support-mts-extension>
resolver: '<rootDir>/mjs-resolver.ts',
// Used the path aliases in tsconfig.json
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
prefix: '<rootDir>',
useESM: true
}),
// moduleNameMapper: {
// '@mlhaufe/graphics/(.+)': '<rootDir>/packages/lib/src',
// '^(\\.\\.?/.*)\\.mjs$': ['$1.mts', '$0']
// },
testMatch: [`<rootDir>/packages/${name}/**/*.test.mts`],
testPathIgnorePatterns: [`<rootDir>/packages/${name}/dist/`]
}))
};
<root>/package.json
{
...
"workspaces": [
"packages/*"
],
"engines": {
"node": ">=16.0.0"
},
"scripts": {
"build": "npm run build --workspaces",
"build:lib": "npm run build --workspace=packages/lib",
"build:demo": "npm run build --workspace=packages/demo",
"test": "jest --coverage",
"test:lib": "jest --selectProjects=lib --coverage",
"test:demo": "jest --selectProjects=demo --coverage",
"serve:demo": "npm run serve --workspace=packages/demo"
},
...
}
I can't figure out why ts-jest is unable to find the module when webpack+typescript have no trouble. I've also yet to figure out the relationship between settings outside of the projects
property and those within. My assumption was that those would be global and apply to each project, but I suspect that is not true.
Any feedback would be appreciated. I've yet to see a consistent (modern) article on how this is accomplished.
So as far as I understood the problem: jest is resolving the local modules in a monorepo without transpiling them before. I treat the solution in this answer more like a workaround than like a final thing.
The solution that I've come up with is basically translating the import path to be not treated as an external package, but rather as a path import.
My jest.config.ts
file:
const config: Config = {
preset: 'ts-jest/presets/js-with-ts-esm',
resolver: '<rootDir>/jest.resolve.cjs',
// the rest of the config is omitted, the above two lines are the important part
}
export default config
The jest.resolve.cjs
file:
const fs = require('node:fs')
const path = require('node:path')
const resolver = require('ts-jest-resolver')
const localModules = new Map(
fs.readdirSync(path.join(__dirname, 'packages'), { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => {
const directory = path.join(__dirname, 'packages', dirent.name)
const pkg = require(path.join(directory, 'package.json'))
const main = path.join(directory, pkg.main)
return [pkg.name, main]
}),
)
/**
* @param {string} path
* @param {import('jest-resolve').ResolverOptions} options
* @returns {string}
*/
module.exports = function resolve (path, options) {
return resolver(localModules.get(path) ?? path, options)
}
As you may have figured it out already, all the local modules in my case are stored inside of the packages
directory - so you may need to adjust it accordingly if it's different for you.
And dear reader: if you have encountered this problem I can safely assume you are advanced enough to understand the code so I'll omit the explanation of it :)