thymikee / jest-preset-angular

Jest configuration preset for Angular projects.

Home Page:https://thymikee.github.io/jest-preset-angular/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

After upgrade Angular to v8: Can't resolve all parameters for Component: (?).

jfcere opened this issue · comments

Hi folks,

I recently upgraded my application to Angular 8 and couldn't get the tests to run successfully with Jest-Preset-Angular resulting in the error below telling me it cannot resolve the constructor dependencies although it works fine using Karma and was working with Angular 7.

Can't resolve all parameters for AppComponent: (?).

I've created a newly generated Angular 8 application to see if it has something to do with my migration and I ended up having the same issue.

Here is the reproduction repository (Jest configuration is in package.json):
https://github.com/jfcere/jest-preset-angular-issue

Thanks in advance!

commented

TL;DR

tsconfig.spec.json

   "compilerOptions": {
+    "emitDecoratorMetadata": true,
     "outDir": "./out-tsc/spec",

Issue

The Angular CLI takes over some of the TS -> JS transpilation using transformers, especially when it's about the decorators and reflection. We were not aware of this change, but it appearently came in in Angular v8.1.

Here the full PR: angular/angular-cli#14473

We can try to reuse their transformer (https://github.com/angular/angular-cli/blob/master/packages/ngtools/webpack/src/transformers/ctor-parameters.ts), as @angular/cli is anyway a dependency in every Angular project.

I will try to experiment with the transformer, we can track the progress in this issue till then.

Workaround

To make ts-jest not lose the parameter information upon compilation without the AST transformation, you can set "emitDecoratorMetadata": true in your tsconfig.spec.json for now. The PR mentions issues with AOT compilation, which is usually not used in testing, so this setting should not create any problems. If you use karma as well and this leads to issues, try to use a separate tsconfig.spec.json.

@wtho thanks for the quick answer, really appreciated!

I've downgraded the reproduction repository to use Angular 8.0.3 and the error is still happening.

But the workaround for tsconfig.spec.ts you've provided works! 😄

commented

Ok interesting, I created a project with 8.0.0 once and there emitDecoratorMetadata was still set, I thought it would not have come in in a patch version update, but for Angular it is just an internal detail.

It's difficult to spot, as I could not find the PR referenced in the angular-cli releases list anywhere.

I created a project with 8.0.0 once and there emitDecoratorMetadata was still set

Oh alright, that could be it, I've only downgraded the angular dependencies manually in package.json, I didn't regenerate a new project with @angular/cli v8.0.x ... sorry!

commented

So I was able to wrap the transformer, but I am not sure if this is the way to with ts-jest.

Why?

This example leads to a runtime error in JIT-compiled Angular projects <v7 using emitDecoratorMetadata, but in Angular projects v8 it works:

@Injectable()
export class BService {
  constructor(@Inject(forwardRef(() => AService)) public as: AService) {}
}

@Injectable()
export class AService {
}

Runtime Error with emitDecoratorMetadata: ReferenceError: Cannot access 'AService' before initialization.

More to read about it: angular/angular#30106 and microsoft/TypeScript#27519

Angular AOT always handled this, but JIT only since recently (since this PR angular/angular-cli#14473). Basically a test with this scenario would pass in Angular or karma, but not with our preset. This is achieved using the DecoratorDownlevelTransformer.

What does the transformer do

The transformer stores the constructor parameter types as JS values on the object inside a ctorParameters-property:

export class SomeClass { constructor(v: ClassInject) {} }

  becomes

export class SomeClass { constructor(v) { } } SomeClass.ctorParameters = () => [ { type: ClassInject } ]

Discussion

To make the existing angular-cli transformer work I had to import typescript on my own and call ts.createProgram, which seems something ts-jest tries to avoid, although I am not sure why, i just found this note, where it says that it is slow:

[...] If we don't return undefined it results in undefined === "undefined" and run createProgram again (which is very slow).

On the other hand ts-jest without isolatedModules became slower in bigger projects since v25.10, so maybe createProgram scales better? I am not a TypeScript internals expert, this is just a baseless guess...

ts.createProgram is required to gain access to the type checker, which is extensively used in the transformer, but is not available in the lightweight compiler ts-jest offers. Rewriting this transformer without the type checker is a task that is quite complicated, if not impossible (the type checker keeps track of class declarations in the project and can then re-reference them from other files).

I also used @angular/compiler-cli to parse the project configuration and the transformer itself, which is located inside @ngtools/webpack.

The wrapper would look something like this:

const { readConfiguration } = require('@angular/compiler-cli')
const { downlevelConstructorParameters } = require('@ngtools/webpack/src/transformers/ctor-parameters.js')
const ts = require('typescript')

function factory(cs) {
  const config = readConfiguration(cs.tsJest.tsConfig.value)
  const program = ts.createProgram(config.rootNames, config.options || {}, config.compilerHost)

  return downlevelConstructorParameters(() => program.getTypeChecker())
}

I would require more feedback from you guys, @thymikee and especially @ahnpnl or someone else from ts-jest, who is familiar with the TypeScript internals going on in ts-jest.


Update

I figured out we can create the program with the ts compiler module inside configSet, which removes the direct dependency to typescript and @angular/compiler-cli:

const { downlevelConstructorParameters } = require('@ngtools/webpack/src/transformers/ctor-parameters.js')

function factory(cs) {
  const program = cs.compilerModule.createProgram(cs.typescript.fileNames, cs.typescript.options, undefined)
  return downlevelConstructorParameters(() => program.getTypeChecker())
}

If the TS Program and Type Checker would be instantiated by ts-jest and available to the transformer, we could just include the original Angular transformer. See kulshekhar/ts-jest#1146 for the this request.

FWIW - I've updated the Briebug schematic to support Angular 8. It includes some of the changes mentioned above in version 2.0.0 - https://github.com/briebug/jest-schematic

solved my problem,nice

The only thing that worked for me, after trying all the other options here,was to enable the following in the compilerOptions in the tsconfig.json file:
"emitDecoratorMetadata": true,
This option should be enabled if the "experimentalDecorators": true.
I had to enable both in order to make my app work.
Hope that helps someone.

commented

Update

I figured out we can create the program with the ts compiler module inside configSet, which removes the direct dependency to typescript and @angular/compiler-cli:

const { downlevelConstructorParameters } = require('@ngtools/webpack/src/transformers/ctor-parameters.js')

function factory(cs) {
  const program = cs.compilerModule.createProgram(cs.typescript.fileNames, cs.typescript.options, undefined)
  return downlevelConstructorParameters(() => program.getTypeChecker())
}

If the TS Program and Type Checker would be instantiated by ts-jest and available to the transformer, we could just include the original Angular transformer. See kulshekhar/ts-jest#1146 for the this request.

@who I think we can start working on this right ?, based on these codes from you + the exposed Program from ts-jest.

Currently isolatedModules mode doesn’t expose Program so I don’t know if it is a must to have or just use compileModule.createProgram as fallback for isolatedModules mode like you did above ?

commented

@ahnpnl yes, I have a working implementation for isolatedModules: false using the original Angular Transformer. I am not sure what the better alternative would be:

  • recommending the workaround for whoever wants to use isolatedModules: true
  • provide a possibly bad performing program in isolatedModules: true
  • discourage from using isolatedModules: true

Finally the best solution for everybody would be a performant ts-jest without isolated modules, but it is difficult ot estimate how difficult it is to achieve this, or if it can be achieved at all.

commented

Hmm actually I gonna create a PR to expose Program also for isolatedModules: true because the need of support projectReferences which also requires Program. So no matter what kind of settings for isolatedModules, there will be always a Program instance.

I noticed that the impact to performance is usually noticeable when using Program.emit, otherwise creating a Program instance doesn't do anything. I hope I'm right. Do you have similar observation ?

LanguageService in ts-jest has been improved quite a lot but not sure what else it can be done. There will be room for improvement but will need some times to figure out and helps from others.

Exposing Program for isolatedModules: true can be found here kulshekhar/ts-jest#766 (comment) If you can help to test projectReferences as well, it would be very nice :)

Update 1: I have tested the tgz file from that ts-jest issue and I don't see any downgrade in performance for isolatedModules: true. Seem like my theory is correct that only when using .emit() will impact the performance

Update 2: Exposing Program for isolatedModules: true will be available after this PR kulshekhar/ts-jest#1527

commented

Ok, awesome! This simplifies things for this issue.

Will prepare the PR soon!

The error
This constructor is not compatible with Angular Dependency Injection because its dependency at index 0 of the parameter list is invalid.

is fixed using this emitDecoratorMetadata: true

commented

@norato this workaround was mentioned several times before in this thread, but is just a workaround and not more, not a fix for the preset.

If you wonder why this is not closed: The fix in this preset would not require any changes in the tsconfig, as this preset in a perfect world would already include all required settings.

@wtho I did not found those references mentioned when I wrote this comment. This was just to help the ones that need to "fix" this error until we don't have the solution.

This is a weird one. I'm also getting this error, however, I do have emitDecoratorMetadata: true in my tsconfig.spec.json. But the problem only seems to appear with a specific provider that doesn't want to work unless I reference it in the file outside of type definitions.

Edit: seems like adding isolatedModules: true to ts-jest config makes everything work. 🤷‍♂️

commented

@villelahdenvuo isolatedModules switches off typechecking and just compiles TS to JS.

Do you have an own file for each injectable class?

@wtho Yeah, but I actually re-export the class through a another file, that might be why it's failing.

@wtho I can confirm that moving the failing class to the file being imported made the provider work again even without isolatedModules.

image

I have created component with name 'test' and after simple coding i am facing this type of error. I have also make this true "emitDecoratorMetadata": true,. still i am getting this error.
I am using webstorm framework and recently joined GitHub. kindly help me out.

image
this is how my test.component.ts page looks like. In test.component.html i have created simple form by using some input fields only. what is the error in this code. your response will highly be appreciated.

commented

Hey @Murkhassan86,
you definitely have a different issue, you seem to not have worked much with TypeScript syntax yet.

Try this instead:

constructor(private fb: FormBuilder) {}

Cheers!

Dear wtho,
thank you for the correction.

Why I'm getting this error? "emitDecoratorMetadata": true, didn't work.

import { Component, OnInit, Input, ChangeDetectorRef, Attribute, Output, EventEmitter } from '@angular/core';
import { Store } from '@ngrx/store';

@Component({
  selector: 'keyword-list',
  templateUrl: './keyword-list.component.html',
  styleUrls: ['./keyword-list.component.css']
})
export class KeywordListComponent {
  constructor(
    private masterDataStore: any,
    private changeDetectorRef: ChangeDetectorRef,
    @Attribute('isSkills') public isSkills: boolean = false
  ) {}
}

There is something wrong with constructor here.

@senseihimanshu Most likely due to that you're declaring masterDataStore as any. You need to provide a proper (and registered) type in order for Angular's DI system to know what instance to inject

TL;DR

tsconfig.spec.json

   "compilerOptions": {
+    "emitDecoratorMetadata": true,
     "outDir": "./out-tsc/spec",

Issue

The Angular CLI takes over some of the TS -> JS transpilation using transformers, especially when it's about the decorators and reflection. We were not aware of this change, but it appearently came in in Angular v8.1.

Here the full PR: angular/angular-cli#14473

We can try to reuse their transformer (https://github.com/angular/angular-cli/blob/master/packages/ngtools/webpack/src/transformers/ctor-parameters.ts), as @angular/cli is anyway a dependency in every Angular project.

I will try to experiment with the transformer, we can track the progress in this issue till then.

Workaround

To make ts-jest not lose the parameter information upon compilation without the AST transformation, you can set "emitDecoratorMetadata": true in your tsconfig.spec.json for now. The PR mentions issues with AOT compilation, which is usually not used in testing, so this setting should not create any problems. If you use karma as well and this leads to issues, try to use a separate tsconfig.spec.json.

You are awesome. It solved my issue. Iwas struggling for days and hours.

Hey there! Any news on this bug?
[Edit] Seems to be closed by an old pr, nice!