Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tests fail when using TypeScript project references #1648

Open
Tracked by #18
FantasticFiasco opened this issue May 15, 2020 · 38 comments
Open
Tracked by #18

Tests fail when using TypeScript project references #1648

FantasticFiasco opened this issue May 15, 2020 · 38 comments

Comments

@FantasticFiasco
Copy link

🐛 Bug Report

I'm in the process of migrating one of my projects to use Typescript project references, but after migration the tests that validated error types failed.

I've created a repo that has one directory with a very small project where tests succeed, and one directory with a project using typescript projects references where the tests fail. The projects are identical with the exception of project references. I'm not saying that ts-jest is to blame. It could as well be my understanding of project references or jest.

To Reproduce

git clone https://github.com/FantasticFiasco/jest-error-type-issue.git
cd jest-error-type-issue/does-not-work
yarn
yarn test

Expected behavior

I would expect the test to pass, because in the same git repository there is a directory called works with an identical project except the project references that has a test that passes.

Link to repo (highly encouraged)

https://github.com/FantasticFiasco/jest-error-type-issue

Debug log:

{"context":{"allowJs":false,"logLevel":20,"namespace":"jest-preset","package":"ts-jest","version":"25.5.1"},"message":"creating jest presets not handling JavaScript files","sequence":1,"time":"2020-05-15T16:59:54.105Z"}
{"context":{"logLevel":20,"namespace":"Importer","package":"ts-jest","version":"25.5.1"},"message":"creating Importer singleton","sequence":2,"time":"2020-05-15T16:59:55.006Z"}
{"context":{"allowJs":false,"logLevel":20,"namespace":"jest-preset","package":"ts-jest","version":"25.5.1"},"message":"creating jest presets not handling JavaScript files","sequence":3,"time":"2020-05-15T16:59:55.010Z"}
{"context":{"actualVersion":"25.5.4","expectedVersion":">=25 <26","logLevel":20,"namespace":"versions","package":"ts-jest","version":"25.5.1"},"message":"checking version of jest: OK","sequence":4,"time":"2020-05-15T16:59:55.011Z"}
{"context":{"baseOptions":{},"logLevel":20,"namespace":"jest-transformer","package":"ts-jest","transformerId":1,"version":"25.5.1"},"message":"created new transformer","sequence":5,"time":"2020-05-15T16:59:55.011Z"}
{"context":{"fileName":"/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work/test/foo.spec.ts","logLevel":20,"namespace":"jest-transformer","package":"ts-jest","transformOptions":{"config":{"automock":false,"browser":false,"cache":true,"cacheDirectory":"/private/var/folders/cl/yqb0znw56dj17l06y_45rzx40000gn/T/jest_dx","clearMocks":false,"coveragePathIgnorePatterns":["/node_modules/"],"cwd":"/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work","detectLeaks":false,"detectOpenHandles":false,"errorOnDeprecated":false,"extraGlobals":[],"forceCoverageMatch":[],"globals":{},"haste":{"computeSha1":false,"providesModuleNodeModules":[],"throwOnModuleCollision":false},"moduleDirectories":["node_modules"],"moduleFileExtensions":["js","json","jsx","ts","tsx","node"],"moduleNameMapper":[],"modulePathIgnorePatterns":[],"name":"065014f8040ebe756b2da8ebc80e3c0a","prettierPath":"prettier","resetMocks":false,"resetModules":false,"restoreMocks":false,"rootDir":"/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work","roots":["/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work"],"runner":"jest-runner","setupFiles":[],"setupFilesAfterEnv":[],"skipFilter":false,"snapshotSerializers":[],"testEnvironment":"/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work/node_modules/jest-environment-node/build/index.js","testEnvironmentOptions":{},"testLocationInResults":false,"testMatch":["**/__tests__/**/*.[jt]s?(x)","**/?(*.)+(spec|test).[tj]s?(x)"],"testPathIgnorePatterns":["/node_modules/"],"testRegex":[],"testRunner":"/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work/node_modules/jest-jasmine2/build/index.js","testURL":"http://localhost","timers":"real","transform":[["^.+\\.tsx?$","/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work/node_modules/ts-jest/dist/index.js",{}]],"transformIgnorePatterns":["/node_modules/"],"watchPathIgnorePatterns":[]},"instrument":false,"rootDir":"/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work","supportsDynamicImport":false,"supportsStaticESM":false},"transformerId":1,"version":"25.5.1"},"message":"computing cache key for /Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work/test/foo.spec.ts","sequence":6,"time":"2020-05-15T16:59:55.012Z"}
{"context":{"logLevel":30,"namespace":"jest-transformer","package":"ts-jest","transformerId":1,"version":"25.5.1"},"message":"no matching config-set found, creating a new one","sequence":7,"time":"2020-05-15T16:59:55.012Z"}
{"context":{"config":{"automock":false,"browser":false,"cache":true,"cacheDirectory":"/private/var/folders/cl/yqb0znw56dj17l06y_45rzx40000gn/T/jest_dx","clearMocks":false,"coveragePathIgnorePatterns":["/node_modules/"],"cwd":"/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work","detectLeaks":false,"detectOpenHandles":false,"errorOnDeprecated":false,"extraGlobals":[],"forceCoverageMatch":[],"globals":{},"haste":{"computeSha1":false,"providesModuleNodeModules":[],"throwOnModuleCollision":false},"moduleDirectories":["node_modules"],"moduleFileExtensions":["js","json","jsx","ts","tsx","node"],"moduleNameMapper":[],"modulePathIgnorePatterns":[],"name":"065014f8040ebe756b2da8ebc80e3c0a","prettierPath":"prettier","resetMocks":false,"resetModules":false,"restoreMocks":false,"rootDir":"/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work","roots":["/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work"],"runner":"jest-runner","setupFiles":[],"setupFilesAfterEnv":[],"skipFilter":false,"snapshotSerializers":[],"testEnvironment":"/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work/node_modules/jest-environment-node/build/index.js","testEnvironmentOptions":{},"testLocationInResults":false,"testMatch":["**/__tests__/**/*.[jt]s?(x)","**/?(*.)+(spec|test).[tj]s?(x)"],"testPathIgnorePatterns":["/node_modules/"],"testRegex":[],"testRunner":"/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work/node_modules/jest-jasmine2/build/index.js","testURL":"http://localhost","timers":"real","transform":[["^.+\\.tsx?$","/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work/node_modules/ts-jest/dist/index.js",{}]],"transformIgnorePatterns":["/node_modules/"],"watchPathIgnorePatterns":[]},"logLevel":20,"namespace":"backports","package":"ts-jest","transformerId":1,"version":"25.5.1"},"message":"backporting config","sequence":8,"time":"2020-05-15T16:59:55.012Z"}
{"context":{"jestConfig":{"automock":false,"browser":false,"cache":true,"cacheDirectory":"/private/var/folders/cl/yqb0znw56dj17l06y_45rzx40000gn/T/jest_dx","clearMocks":false,"coveragePathIgnorePatterns":["/node_modules/"],"cwd":"/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work","detectLeaks":false,"detectOpenHandles":false,"errorOnDeprecated":false,"extraGlobals":[],"forceCoverageMatch":[],"globals":{"ts-jest":{}},"haste":{"computeSha1":false,"providesModuleNodeModules":[],"throwOnModuleCollision":false},"moduleDirectories":["node_modules"],"moduleFileExtensions":["js","json","jsx","ts","tsx","node"],"moduleNameMapper":[],"modulePathIgnorePatterns":[],"name":"065014f8040ebe756b2da8ebc80e3c0a","prettierPath":"prettier","resetMocks":false,"resetModules":false,"restoreMocks":false,"rootDir":"/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work","roots":["/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work"],"runner":"jest-runner","setupFiles":[],"setupFilesAfterEnv":[],"skipFilter":false,"snapshotSerializers":[],"testEnvironment":"/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work/node_modules/jest-environment-node/build/index.js","testEnvironmentOptions":{},"testLocationInResults":false,"testMatch":["**/__tests__/**/*.[jt]s?(x)","**/?(*.)+(spec|test).[tj]s?(x)"],"testPathIgnorePatterns":["/node_modules/"],"testRegex":[],"testRunner":"/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work/node_modules/jest-jasmine2/build/index.js","testURL":"http://localhost","timers":"real","transform":[["^.+\\.tsx?$","/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work/node_modules/ts-jest/dist/index.js",{}]],"transformIgnorePatterns":["/node_modules/"],"watchPathIgnorePatterns":[]},"logLevel":20,"namespace":"config","package":"ts-jest","transformerId":1,"version":"25.5.1"},"message":"normalized jest config","sequence":9,"time":"2020-05-15T16:59:55.013Z"}
{"context":{"logLevel":20,"namespace":"config","package":"ts-jest","transformerId":1,"tsJestConfig":{"compiler":"typescript","diagnostics":{"ignoreCodes":[6059,18002,18003],"pretty":true,"throws":true},"isolatedModules":false,"packageJson":{"kind":"file"},"transformers":[],"tsConfig":{"kind":"file"}},"version":"25.5.1"},"message":"normalized ts-jest config","sequence":10,"time":"2020-05-15T16:59:55.013Z"}
{"context":{"logLevel":20,"namespace":"config","package":"ts-jest","transformerId":1,"version":"25.5.1"},"message":"babel is disabled","sequence":11,"time":"2020-05-15T16:59:55.015Z"}
{"context":{"logLevel":20,"namespace":"Importer","package":"ts-jest","requireResult":{"exists":true,"given":"typescript","path":"/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work/node_modules/typescript/lib/typescript.js"},"version":"25.5.1"},"message":"loaded module typescript","sequence":12,"time":"2020-05-15T16:59:55.169Z"}
{"context":{"logLevel":20,"namespace":"Importer","package":"ts-jest","version":"25.5.1"},"message":"patching typescript","sequence":13,"time":"2020-05-15T16:59:55.169Z"}
{"context":{"actualVersion":"3.9.2","expectedVersion":">=3.4 <4","logLevel":20,"namespace":"versions","package":"ts-jest","version":"25.5.1"},"message":"checking version of typescript: OK","sequence":14,"time":"2020-05-15T16:59:55.169Z"}
{"context":{"logLevel":20,"namespace":"config","package":"ts-jest","transformerId":1,"tsConfigFileName":"/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work/tsconfig.json","version":"25.5.1"},"message":"readTsConfig(): reading /Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work/tsconfig.json","sequence":15,"time":"2020-05-15T16:59:55.169Z"}
{"context":{"logLevel":20,"namespace":"config","package":"ts-jest","transformerId":1,"tsconfig":{"compileOnSave":false,"configFileSpecs":{"filesSpecs":[],"wildcardDirectories":{}},"errors":[],"fileNames":[],"options":{"configFilePath":"/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work/tsconfig.json","declaration":false,"inlineSourceMap":false,"inlineSources":true,"module":1,"noEmit":false,"removeComments":false,"sourceMap":true,"target":1},"projectReferences":[{"originalPath":"./src","path":"/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work/src"},{"originalPath":"./test","path":"/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work/test"}],"raw":{"compileOnSave":false,"compilerOptions":{},"files":[],"references":[{"path":"./src"},{"path":"./test"}]},"typeAcquisition":{"enable":false,"exclude":[],"include":[]},"wildcardDirectories":{}},"version":"25.5.1"},"message":"normalized typescript config","sequence":16,"time":"2020-05-15T16:59:55.175Z"}
{"context":{"fileName":"/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work/src/foo.ts","logLevel":20,"namespace":"jest-transformer","package":"ts-jest","transformOptions":{"config":{"automock":false,"browser":false,"cache":true,"cacheDirectory":"/private/var/folders/cl/yqb0znw56dj17l06y_45rzx40000gn/T/jest_dx","clearMocks":false,"coveragePathIgnorePatterns":["/node_modules/"],"cwd":"/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work","detectLeaks":false,"detectOpenHandles":false,"errorOnDeprecated":false,"extraGlobals":[],"forceCoverageMatch":[],"globals":{},"haste":{"computeSha1":false,"providesModuleNodeModules":[],"throwOnModuleCollision":false},"moduleDirectories":["node_modules"],"moduleFileExtensions":["js","json","jsx","ts","tsx","node"],"moduleNameMapper":[],"modulePathIgnorePatterns":[],"name":"065014f8040ebe756b2da8ebc80e3c0a","prettierPath":"prettier","resetMocks":false,"resetModules":false,"restoreMocks":false,"rootDir":"/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work","roots":["/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work"],"runner":"jest-runner","setupFiles":[],"setupFilesAfterEnv":[],"skipFilter":false,"snapshotSerializers":[],"testEnvironment":"/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work/node_modules/jest-environment-node/build/index.js","testEnvironmentOptions":{},"testLocationInResults":false,"testMatch":["**/__tests__/**/*.[jt]s?(x)","**/?(*.)+(spec|test).[tj]s?(x)"],"testPathIgnorePatterns":["/node_modules/"],"testRegex":[],"testRunner":"/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work/node_modules/jest-jasmine2/build/index.js","testURL":"http://localhost","timers":"real","transform":[["^.+\\.tsx?$","/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work/node_modules/ts-jest/dist/index.js",{}]],"transformIgnorePatterns":["/node_modules/"],"watchPathIgnorePatterns":[]},"instrument":false,"rootDir":"/Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work","supportsDynamicImport":false,"supportsStaticESM":false},"transformerId":1,"version":"25.5.1"},"message":"computing cache key for /Users/mattiask/code/open-source/temp/jest-error-type-issue/does-not-work/src/foo.ts","sequence":17,"time":"2020-05-15T16:59:55.178Z"}

envinfo

System:
    OS: macOS Catalina v10.15.4

Npm packages:
    jest: v25.5.4
    ts-jest: v25.5.1
    typescript: v3.9.2
@FantasticFiasco FantasticFiasco added Bug Report Needs Repo Need a minimium repository to reproduce the problem Needs Triage labels May 15, 2020
@ahnpnl
Copy link
Collaborator

ahnpnl commented May 15, 2020

This one might be related to your case microsoft/TypeScript#37239

In general, ts-jest uses typescript api to read tsconfig and resolves the config to fetch to typescript compiler. In your case, it cannot find enough configs to make tests pass.

Your case is similar to #766

@FantasticFiasco
Copy link
Author

Thanks, that did the trick!

@ryami333
Copy link

ryami333 commented Jul 21, 2020

@FantasticFiasco what did the trick? I couldn't see any actionables in the two links that @ahnpnl posted. I'm trying to leverage the new "Solution Style" config as introduced in microsoft/TypeScript#37239, so I have a tsconfig.json file that looks like this:

{
  "files": [],
  "references": [
    { "path": "./tsconfig.foo.json" },
    { "path": "./tsconfig.bar.json" }
  ]
}

But when I run my tests they all fail because ts-jest doesn't seem to recognize that the compiler options for each test should be governed by one of tsconfig.foo.json or tsconfig.bar.json.

@FantasticFiasco
Copy link
Author

@ryami333 As I recall the problem was with tsconfig.json, I thought that all projects would pick up the configuration from the file in repo root, but that was not the case. Please see https://github.com/FantasticFiasco/axis-maintenance-js for a project that is using TypeScript and project references successfully.

@ryami333
Copy link

ryami333 commented Jul 21, 2020

@FantasticFiasco that repo uses a workaround in jest.config.js to get around the fact that ts-jest does not automatically inherit the correct TS config for any given file:

  globals: {
    "ts-jest": {
      tsConfig: "tsconfig-base.json",
    },
  },

But that is a workaround which indicates an underlying bug, and not one that can be applied to my setup either. It's frustrating to see this and other similar issues (such as #766) be closed when they're still reproducible, simply because some people have found a workaround which works for them.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Jul 21, 2020

Note that if you are using TypeScript 3.9 solution style, you might run into issue. By default when no tsconfig specified for ts-jest, root tsconfig.json will be picked up.

It is recommended to specify which tsconfig to be picked up via option tsConfig in jest configuration.

@ryami333
Copy link

ryami333 commented Jul 21, 2020

No that's my point exactly: the root tsconfig.json file is a valid config target. That's basically one of the motivations of "Solution Style" configs (per microsoft/TypeScript#37239) in the first place, so that tools like this (and IDE's linter plugins etc) which derive their entire project-config from a single root tsconfig.json file can have different settings for different files/directories.

Now, for example, I can have different configs for my foo and bar directories, and my IDE is informed of whether to use the config in tsconfig.foo.json or tsconfig.bar.json, not because I explicitly told it so, but because it can derive that information from a single root tsconfig.json file.

Otherwise I have to run jest + ts-jest once for my foo directory (with foo.config.json) and once more for my bar directory (with bar.config.json). Despite the fact that running tsc -b --project=tsconfig.json compiles correctly etc.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Jul 21, 2020

I was thinking about a new feature that is similar to angular/angular#38003 which will show a warning message when encountering solution style, then automatically fallback to either tsconfig.test.json or tsconfig.spec.json (the most 2 common ways of naming tsconfig for test). If cannot find these 2, fallback to the current logic.

Running jest is a bit different from tsc so some behaviors of tsc are difficult to implement when combining with jest.

@FantasticFiasco
Copy link
Author

It's frustrating to see this and other similar issues (such as #766) be closed when they're still reproducible, simply because some people have found a workaround which works for them.

I'll try not to frustrate you next time 😉

You have a collaborator in the thread, I'm sure that given the correct argumentation the issue can be re-opened. Don't you think?

@ahnpnl
Copy link
Collaborator

ahnpnl commented Jul 21, 2020

I think ts 3.9 has an API for solution style and project references. We can reopen and work on this

@ryami333
Copy link

ryami333 commented Jul 21, 2020

@ahnpnl you're focussing on the use-case where someone has different config for their tests as they do for their source files, which is subtly different from the classic "Solution Style" case where the project has 2-or-more sub-projects (such as my foo and bar example above) with entirely different configuration requirements (and the tests may or may not have different config still). So if the plan is simply to publish a warning message when users have Solution configs, then does that mean that you simply do not support this valid Typescript setup pattern? In the case of Angular, a framework with an opinionated structure, I can see why this might be appropriate, but less so here.

EDIT: I posted this right before seeing your message about the new API in 3.9 🥳


Sorry @FantasticFiasco, on reflection I can see that came across as abrasive.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Jul 21, 2020

Appreciate some ideas to tackle this. I took a quick look at the PR microsoft/TypeScript#37239 and saw some discussions about a function but not quite sure.

@ahnpnl ahnpnl reopened this Jul 21, 2020
@ahnpnl ahnpnl added 🐛 Bug Confirmed Bug is confirmed and removed Bug Report Needs Repo Need a minimium repository to reproduce the problem Needs Triage labels Jul 21, 2020
@ryami333
Copy link

I can help with a repro if nothing else, I'll look into that now.

@ryami333
Copy link

@ahnpnl
Copy link
Collaborator

ahnpnl commented Jul 21, 2020

Correct me if I’m wrong. The expectation is test files should be run according to the project config of the project they belong to. For example:

  • Project A has tsconfig-a.json
  • Project B has tsconfig-b.json

When running tests from root (which runs all tests for all projects), all tests in project A should be run with tsconfig-a.json and all tests in project B should be run with tsconfig-b.json ?

Quite struggling to find the right approach here .

Hi @orta can you help us a bit about this ?

@ryami333
Copy link

If my understanding is correct, then yes. It seems that the tsc includes and files keys in tsconfig-a.json and tsconfig-b.json determine which configs apply to which files, which make sense. There are edge-cases though of course, such as files which match includes (or files) in multiple configs, and I suppose ts-jest should just try and match tsc's behaviour in these cases as much as possible.

I happen to know that the Sublime Text Editor's Typescript plugin already seems to be able to apply the correct config for any given file in a Solutions config context. I don't yet know whether this is a feature which they actively developed into the plugin, or whether they just defer to the typescript server's internal getConfigFileNameForFile method.

@JakeGinnivan
Copy link

JakeGinnivan commented Aug 31, 2020

I am trying to figure out the best approach for this as well. I have two main strategies

  1. Single jest project with a tsconfig.test.json which includes source files for all projects and module mappings so it can resolve the project references. This side steps project references entirely and I am having better luck with this approach
  2. Jest projects with module mappings, with module paths it seems to work if I run tsc -b before to ensure the referenced projects are up to date. ie modulePaths: ['<rootdir>/../src/shared']

Both strategies are valid I think but expect quite different behaviors from ts-jest. I think the mapper is an ok solution for option 1 because the intention is that jest references the source files across multiple TypeScript projects.

I think the experience here could be greatly improved if ts-jest resolves a solution tsconfig then assume approach 1 and just use a default tsconfig with sensible defaults for a new project created by TS 3.9 (if you are using a solution tsconfig it's a reasonably up to date project), this tsconfig should automatically have module mappings to be able to resolve the source files for each TypeScript project referenced by the solution tsconfig

Number 2 is where it gets hard.

Lets say there are 2 jest projects referenced by the root jest config and a shared library, when jest goes to run the tests it needs to ensure the shared library is compiled by typescript (does the TypeScript program handle this sort of concurrency?), once it is compiled then each jest project should be able to resolve the shared module using the main entry in package.json

I think solutions to both can be shipped in ts-jest, but these two approaches should be separated from a solution point of view.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Aug 31, 2020

@JakeGinnivan thanks a lot for the brainstorming help. I'd like to separate the 2 terms here: jest projects vs TypeScript project references to deal with them separately. I agree that in the end, ts-jest should support both.

Overall, these 2 work almost similar. However, the way ts-jest works based on how jest executes tests. Therefore, actually point 1 should be a bit more details besides your explanation above:

  1. Single jest project with a tsconfig.test.json which includes source files for all projects and module mappings so it can resolve the project references. This side steps project references entirely and I am having better luck with this approach
    Both strategies are valid I think but expect quite different behaviors from ts-jest. I think the mapper is an ok solution for option 1 because the intention is that jest references the source files across multiple TypeScript projects.

I think the experience here could be greatly improved if ts-jest resolves a solution tsconfig then assume approach 1 and just use a default tsconfig with sensible defaults for a new project created by TS 3.9 (if you are using a solution tsconfig it's a reasonably up to date project), this tsconfig should automatically have module mappings to be able to resolve the source files for each TypeScript project referenced by the solution tsconfig

ts-jest still needs to behave like tsc -b:

  • It needs to gather compiler options from each individual tsconfig.json locates in each referenced project (defined in projectReferences).

  • Detect the current processing file belongs to which referenced projects, then compile using the correct referenced project's compiler options.

  • moduleNameMapper is a sort of partially related to this topic. Once the compiling with correct referenced project's compiler options is solved, moduleNameMapper will be easier to solve.

Number 2 is where it gets hard.

Lets say there are 2 jest projects referenced by the root jest config and a shared library, when jest goes to run the tests it needs to ensure the shared library is compiled by typescript (does the TypeScript program handle this sort of concurrency?), once it is compiled then each jest project should be able to resolve the shared module using the main entry in package.json

I think solutions to both can be shipped in ts-jest, but these two approaches should be separated from a solution point of view.

In this scenario, jest will execute tests for each project using that project's jest config associated with ts-jest config.

I did a small test for this scenario. The project I have contains project-1, project-2 and share. project-1 and project-2 are jest projects and don't have projectReferences in each tsconfig.json. share is just a normal directory to contain shared codes. Here are how things execute:

  • Jest run tests for project-1. ts-jest compiles all the files needed in project-1 for test run, include file in share as well. The tsconfig.json of project-1 is used.

  • Jest run tests for project-2. ts-jest compiles all the files needed in project-2 for test run, include file in share as well. The tsconfig.json of project-2 is also used.

This means when using jest config projects, jest will automatically create different instance of the ts-jest. So regardless using projects or not using projects, jest guarantee that tests in each project run in an sandbox environment. Therefore we only need to take care of resolving correct tsconfig.json.

If tsconfig.json from project-1/project-2 contains projectReferences to somewhere, ts-jest also needs to do the same like scenario 1, which also looks for tsconfig.json of those referenced projects and compile files with the correct compiler options.

Currently, jest transformer doesn't support async, neither TypeScript Program, everything is processed on demand and sequentially.

@wmaca
Copy link

wmaca commented Nov 23, 2020

I am going through exactly what @ahnpnl described below. I am posting my use case here in case it is helpful.

ts-jest still needs to behave like tsc -b:

It needs to gather compiler options from each individual tsconfig.json locates in each referenced project (defined in projectReferences).

Detect the current processing file belongs to which referenced projects, then compile using the correct referenced project's compiler options.

moduleNameMapper is a sort of partially related to this topic. Once the compiling with correct referenced project's compiler options is solved, moduleNameMapper will be easier to solve.

I have one monorepo with a package called common and one called ui. The common package uses absolute imports inside of itself. The ui uses the common package, and has some tests inside that depends on common.

When I try to run the tests from ui package, it is unable to understand the absolute imports that the common package uses inside itself. If common's tsconfig was properly read, I guess it would work.

@ktutnik
Copy link

ktutnik commented Dec 30, 2020

Same issue here..

This workaround https://github.com/FantasticFiasco/axis-maintenance-js doesn't work if you have namespace/interface augmentation..

Test passes but failed when collecting coverage report Failed to collect coverage

@rhyek
Copy link

rhyek commented Jan 25, 2021

Not sure if this is the same use case as you all, but in my ts monorepo example in these test scripts you can see how I have to do for example:

"test": "ttsc -b tsconfig.test.json && jest",

instead of just "test": "jest"

This is because this project (webapi) depends on lib via ts project references, but whatever ts-jest is doing under the hood it does not seem to be behaving like the tsc -b counterpart thus it is not implicitly building the dependencies. Therefore, I must build these dependencies myself which is not ideal.

@tommysullivan
Copy link

@rhyek same.

if i run yarn workspace subproject1 build, and then in the subproject1 folder's package.json i define build to do a tsc -b tsconfig.subproject1.json, where this tsconfig contains references to subproject2, which has its own package.json and tsconfig etc, then, everything compiles, all the dist folders are there, and my jest tests, located in subproject1, work just fine.

if i do not build myself, then it complains Cannot find module 'subproject1' or its corresponding type declarations., regardless of whether or not I attempt to use moduleNameMapper in subproject1's jest config.

So, bottom line is that it seems that ts-jest is not doing what tsc -b would do, so when in watch mode, i really suffer.

@StarpTech
Copy link

I have the exact same issue as @tommysullivan described. ts-jest should build like tsc- b

@ahnpnl
Copy link
Collaborator

ahnpnl commented Feb 18, 2021

I want to make clear something: ts-jest cannot behave like TypeScript tsc -b. The reason ists-jest is a Jest transformer. We only transpile ts to js based on what jest-runtime passes in. This processing way is different from tsc -b where tsc controls the whole project.

We can still support project references but maybe only partially and definitely won't be exact the same like tsc -b does. In the meantime, you can try

I hope this makes clear to everyone the difference between jest + ts-jest vs TypeScript tsc and your expectations.

@StarpTech
Copy link

Hi @ahnpnl thanks for the clarification. The DX isn't great in that common scenario.

@Goldziher
Copy link

Goldziher commented Apr 16, 2021

Hi,

So after spending several hours on this - I was able to make this work in my monorepo. This is my jest.config.ts, hopefully it helps others here:

import { defaults as tsjPreset } from 'ts-jest/presets'
import type { Config } from '@jest/types'
import type { InitialOptionsTsJest } from 'ts-jest/dist/types'

// there are some typing issues in the jest/types library we fix here
type JestConfig = Omit<InitialOptionsTsJest, 'projects'> & {
    projects?: Partial<
        | (Omit<Omit<Config.ProjectConfig, 'transfrom'>, 'moduleNameMapper'> & {
              transform: any
              moduleNameMapper: Record<any, any>
          })
        | Config.Glob
    >[]
}

const config: JestConfig = {
    preset: 'ts-jest',
    collectCoverageFrom: ['packages/**/src/**/*.(ts|tsx)'],
    coverageReporters: ['lcov', 'text'],
    cacheDirectory: '.jest/cache',
    projects: [
        {
            testMatch: ['<rootDir>/packages/frontend/tests/**/*.(spec|test).*'],
            displayName: { name: 'Frontend', color: 'cyan' },
            testEnvironment: 'jsdom',
            transform: tsjPreset.transform,
            globals: {
                'ts-jest': {
                    tsconfig: {
                        noPropertyAccessFromIndexSignature: false,
                    },
                },
            },
            moduleNameMapper: {
                '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
            },
        },
        {
            testMatch: ['<rootDir>/packages/backend/tests/**/*.(spec|test).*'],
            displayName: { name: 'Backend', color: 'blue' },
            testEnvironment: 'node',
            transform: tsjPreset.transform,
        },
    ],
}

export default config

Note - what did the trcik as specifying transform for each project, and not having a top level testRegex etc. Also, although I have tsconfig.json files in each sub-project that extend the baseline one at the monorepo's root, there doesn't appear to be a way to force ts-jest to actually pick these up correctly. Hence the need to override the globals.

@johnciprian
Copy link

I'm also running into this problem. All my tests are failing after migrating to TypeScript project references. It would be nice to have a fix for this.

@taozi0818
Copy link

taozi0818 commented Dec 23, 2021

You can use extends in tsconfig.json in your tests directory to resolve it, like this:

  • /tests/tsconfig.json:
{
  "extends":  "../tsconfig.json"
}

@dnalborczyk
Copy link

dnalborczyk commented Apr 26, 2022

depending on what underlying typescript api ts-jest is using, some flag is probably needed to be specified in the jest config, similar to what the tsc --build flag essentially enforces.

e.g. ts-loader for webpack provides a config setting for this: projectReferences: true

issue: TypeStrong/ts-loader#1005

PR: TypeStrong/ts-loader#935

@alexbruno
Copy link

I know this is an old issue, but if anyone else is running into similar problems...
I just made this on Jest config and it worked for me:

  globals: {
    'ts-jest': {
      isolatedModules: true,
    },
  }

@divmgl
Copy link

divmgl commented Nov 27, 2022

#1648 (comment)

This, hilariously, just worked for me. What doesn't make sense too is that if I put isolatedModules in the transform itself it doesn't work. It only works as a global.

@adrian-gierakowski
Copy link

@alexbruno @divmgl this is not a solution, is a workaround with a drawback: you loose type checking

@lmihalkovic
Copy link

the more I spend time with this thing, the more I touch its absurdity... nothing seems to be reliably reproducible, and the quality of the reporting/logging is .... .. baffling at best.

@unional
Copy link

unional commented Dec 11, 2022

FYI, just open an issue explaining that isolatedModules is not the same as removing type checking

#3930

@ChoSeoHwan
Copy link

I know this is an old issue, but if anyone else is running into similar problems... I just made this on Jest config and it worked for me:

  globals: {
    'ts-jest': {
      isolatedModules: true,
    },
  }

This doesn't solve my problem.

When an error occurs in the common module, the correction is not reflected when you correct it and run jest again.
This is because it just "ignores" the error on type , not rebuilding the common module.

@joostlubach
Copy link

joostlubach commented Aug 20, 2023

I was dealing with incorrect path resolution in a monorepo, which I have also been able to solve.

My specific problem was: I have multiple packages in my monorepo, and they all map ~/* to <packageDir>/src/* in their tsconfig.json's paths property. However, as I was running jest, even with multiple projects, if package A depends on package B, it would use package A's tsconfig.json and therefore path resolution. I.e. if I had a reference in package B to ~/formatting, and I would test package A which imports something from package B, the module ~/formatting would be mapped to packages/package-a/src/formatting, instead of package B's.

To solve this, I had to create my own resolver, mimicking what TypeScript does, to find the closest tsconfig.json for each requesting file. I then use tsconfig-paths to do the actual resolving.

Disclaimer: I'm copying this stuff by hand from my code, and removing some stuff that's outside of the scope of this example, so it might not be a working example, but I hope someone is helped by this!

Another disclaimer: this solves for module resolution. If your tsconfig.jsons are different otherwise between packages, this solution may not prove to be effective.

What I've done is the following (assume that my monorepo uses a scope @my):

  1. First I added the isolatedModules option as described above, which appeases the TypeScript compiler, but not the Jest resolver.
  2. I've created a package in my monorepo, called @my/testing, which I have added as a dependency to all packages with tests. This allows me to refer to the packages themselves with <rootDir> and to any file in this shared package as @my/test.
  3. The package @my/testing has some different stuff to help me out, but mostly it has a resolve.js, which is included from the configuration, which is my custom resolver.

The following is specific to my situation, but if anyone is still struggling with this, I hope you can use it as an inspiration:

/packages/testing/package.json

{
  "name": "@my/testing",
  "version": "0.0.1",
  "exports": {
    ".": "./index.js",
    "./resolve": "./resolve.js"
  },
  "dependencies": {
    "fs-extra": "^11.1.1",
    "ts-jest": "^29.1.1",
    "tsconfig-paths": "^4.2.0"
  }
}

/packages/testing/index.js

const configTemplate = require('./jest.config.template')

function packageConfig(displayName) {
  return {
    displayName,
    ...configTemplate
  }
}

module.exports = {
  packageConfig
}

/packages/testing/jest.config.template.js

module.exports = {
  preset: 'ts-jest',
  transform: {
    '^.+\\.tsx?$': [
      'ts-jest',
      {
        isolatedModules: true
      }
    ]
  },
  testMatch: ['<rootDir>/src/**/*.test.ts'],
  resolver: '@my/testing/resolve',    // <-- this is where I refer to my resolve script
  testEnvironment: 'node'
}

/packages/testing/resolve.js

const Path = require('path')
const FS = require('fs')
const tscp = require('tsconfig-paths')

const rootDir = Path.join(__dirname, '../..')

module.exports = function resolve(path, options) {
  const { defaultResolver, basedir } = options
  if (!path.startsWith('~')) {  // <- note, I make a quick check here because I only use the prefix '~' for package-local path resolution
    return defaultResolver(path, options)
  }

  const matchPath = matchPathForSourceFile(basedir)
  const matched = matchPath(path, undefined, undefined, options.extensions)
  if (matched) {
    return defaultResolver(matched, options)
  } else {
    return defaultResolver(path, options)
  }
}

function matchPathForSourceFile(source) {
  const dir = closestTSconfigDir(source)
  const tsconfig = tscp.loadConfig(dir)
  return tscp.createMatchPath(
    tsconfig.absoluteBaseUrl,
    tsconfig.paths,
    tsconfig.mainFields,
    tsconfig.addMatchAll
  )
}

function closestTSconfigDir(path) {
  let dir = Path.dirname(path)
  while (dir !== '/') {
    if (dir === rootDir) { break }

    const tsconfigPath = Path.join(dir, 'tsconfig.json')
    if (FS.existsSync(tsconfigPath)) {
      return dir
    }
    dir = Path.dirname(dir)
  }

  return null
}

Now the only thing left to do is adding this @my/testing package to each of the other packages in my monorepo:

packages/package-a$ pnpm add --workspace @my/testing

Then, add a jest.config.js in those packages like so:

packages/package-a/jest.config.js

const { packageConfig } = require('@my/testing')
module.exports = packageConfig('@my/package-a')

@daniel-nagy
Copy link

oof, just tried updating from TypeScript v5.1.6 to v5.4.5 and ran into this issue. It's odd that project references appear to be working fine with TypeScript v5.1.6 and the following jest.config.ts:

{
  transform: {
    "^.+\\.tsx?$": ["ts-jest", { tsconfig: "tsconfig.test.json" }]
  },
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests