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

"Can't resolve all parameters" when importing ngc-compiled library with *.metadata.json #322

Closed
AmarildoK opened this issue Oct 8, 2019 · 13 comments · Fixed by #562
Closed
Labels

Comments

@AmarildoK
Copy link

AmarildoK commented Oct 8, 2019

Hi,
I recently changed to jest. Still trying to learn and optimize the testing process for our app. But switching to jest broke some tests. These tests keep failing and I can't figure why they fail. Maybe I'm doing something wrong or this is a bug.

I have already looked at similar issues but I think they are different from mine. #154 #288
I also used the https://github.com/briebug/jest-schematic shematic to do all the config setup. Which automatically added the emitDecoratorMetadata so I am excluding that as a possible issue.

The component I'm trying to test
This component just has one dependency GoogleChartComponent which is a part of the Ng2GoogleChartsModule.

import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { IColumnRole } from '@influo/modules/chart/chart.helpers';
import { GoogleChartInterface } from 'ng2-google-charts/google-charts-interfaces';
import { BehaviorSubject } from 'rxjs';

@Component({
  selector: 'influo-geo-chart',
  template: `
      <google-chart
              *ngIf="googleChartConfig$ | async as googleChartConfig; else loader"
              [data]="googleChartConfig">
      </google-chart>
      <ng-template #loader>
          <mat-spinner color="accent" diameter="80" strokeWidth="8"></mat-spinner>
      </ng-template>
  `,
  styleUrls: [ './geo-chart.component.scss' ]
})
export class GeoChartComponent implements OnInit, OnChanges {
  @Input() columns: Array<IColumnRole | string>;
  @Input() config: GoogleChartInterface;
  @Input() data: Array<Array<string | number>>;

  private defaultConfig: GoogleChartInterface = {
    chartType: 'GeoChart',
    dataTable: [],
    options: {
      legend: false,
      region: 155,
      enableRegionInteractivity: true,
      displayMode: 'region',
      colors: [ '#e6e6e6', '#1672AD' ],
      datalessRegionColor: '#e6e6e6',
    },
  };

  constructor() {
  }

  _googleChartConfig = new BehaviorSubject<GoogleChartInterface | null>(null);

  set googleChartConfig(config: GoogleChartInterface) {
    const value = this._googleChartConfig.getValue() || {};

    this._googleChartConfig.next(Object.assign({}, value, config));
  }

  get googleChartConfig$() {
    return this._googleChartConfig.asObservable();
  }

  ngOnInit() {
  }

  ngOnChanges(changes: SimpleChanges): void {
    if (this.columns && this.data) {
      this.googleChartConfig = Object.assign({}, this.defaultConfig, this.config, {
        dataTable: [
          this.columns,
          ...this.data,
        ],
      });
    }
  }

}

A very basic test. I am encapsulating GoogleChartComponent.

import { CommonModule } from '@angular/common';
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { UiKitModule } from '@influo/modules/ui-kit/ui-kit.module';
import { Ng2GoogleChartsModule } from 'ng2-google-charts';

import { GeoChartComponent } from './geo-chart.component';

describe('GeoChartComponent', () => {
  let component: GeoChartComponent;
  let fixture: ComponentFixture<GeoChartComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
        imports: [
          CommonModule,
          Ng2GoogleChartsModule
        ],
        providers: [],
        declarations: [ GeoChartComponent ]
      })
      .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(GeoChartComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

Error
When I look at the constructor of the GoogleChartComponent I find ElementRef, GoogleChartsLoaderService. GoogleChartsLoaderService is provided by the module itself. So it's not possible that it's not provided.

When I use this module outside of a testing environment it works perfectly. So my hunch is that it has to do something with the use of jsdom but this seems wrong to assume that.
Which confuses me more why this error is even occurring.

Error: Can't resolve all parameters for GoogleChartComponent: (?, ?). 


    at syntaxError (/Users/other/Documents/projects/influo/src/main/packages/compiler/src/util.ts:100:17)
    at CompileMetadataResolver._getDependenciesMetadata (/Users/other/Documents/projects/influo/src/main/packages/compiler/src/metadata_resolver.ts:957:27)
...

Thanks in advance!

@wtho
Copy link
Collaborator

wtho commented Oct 8, 2019

This is mostly a jest configuration issue. Can you share your jest.config.js and your setupJest.js?

Jest has to be configured in a specific way to work with Angular, as Angular does a lot under the hood with its own Compiler and Webpack Configuration.

@AmarildoK
Copy link
Author

Thank you for the quick response. Here are the config files. The only diff is that I put my jest.conf in package.json.

setup-jest.js

import 'hammerjs';
import createGoogleMapsMock from 'jest-google-maps-mock';
import 'jest-preset-angular';

/* global mocks for jsdom */
const mock = () => {
  let storage: { [key: string]: string } = {};
  return {
    getItem: (key: string) => (key in storage ? storage[key] : null),
    setItem: (key: string, value: string) => (storage[key] = value || ''),
    removeItem: (key: string) => delete storage[key],
    clear: () => (storage = {})
  };
};

Object.defineProperty(window, 'localStorage', { value: mock() });
Object.defineProperty(window, 'sessionStorage', { value: mock() });
Object.defineProperty(window, 'getComputedStyle', {
  value: () => [ '-webkit-appearance' ],
});
const basicGoogleMock = createGoogleMapsMock('places');
(window as any).google = (window as any).google || {
  maps: Object.assign(
    {},
    ...basicGoogleMock,
    {
      places: {...basicGoogleMock.places, Autocomplete: () => {}},
      event: {
        addListener: () => {}
      }
    }

  )
};

Object.defineProperty(document.body.style, 'transform', {
  value: () => {
    return {
      enumerable: true,
      configurable: true,
    };
  },
});

/* output shorter and more meaningful Zone error stack traces */
// Error.stackTraceLimit = 2;

jest.config.js

{
"preset": "jest-preset-angular",
    "roots": [
      "src"
    ],
    "transform": {
      "^.+\\.(ts|js|html)$": "ts-jest"
    },
    "setupFilesAfterEnv": [
      "<rootDir>/src/setup-jest.ts"
    ],
    "moduleNameMapper": {
      "@influo/(.*)": "<rootDir>/src/app/$1",
      "@assets/(.*)": "<rootDir>/src/assets/$1",
      "@testing/(.*)": "<rootDir>/src/testing/$1",
      "@core/(.*)": "<rootDir>/src/app/core/$1",
      "@env": "<rootDir>/src/environments/environment",
      "@src/(.*)": "<rootDir>/src/src/$1",
      "@state/(.*)": "<rootDir>/src/app/state/$1"
    },
    "globals": {
      "ts-jest": {
        "tsConfig": "<rootDir>/src/tsconfig.spec.json",
        "stringifyContentPathRegex": "\\.html$",
        "astTransformers": [
          "jest-preset-angular/InlineHtmlStripStylesTransformer.js"
        ]
      }
    }
}

@wtho
Copy link
Collaborator

wtho commented Oct 8, 2019

You mentioned you checked #288, but just to make sure everything is configured properly: did you set emitDecoratorMetadata: true either in tsconfig.json or tsconfig.spec.json?

@AmarildoK
Copy link
Author

Yes already checked that. And it's set.

Here is the full tsconfig.spec.json file.

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "../out-tsc/spec",
    "emitDecoratorMetadata": true,
    "types": [
      "jasmine",
      "node",
      "googlemaps"
    ],
    "module": "commonjs",
    "allowJs": true
  },
  "files": [
    "polyfills.ts"
  ],
  "include": [
    "**/*.spec.ts",
    "**/*.d.ts"
  ]
}

@wtho
Copy link
Collaborator

wtho commented Oct 10, 2019

Ok great, looks like you did everything alright!

Checking node_modules/ng2-google-charts/bundles/ng2-google-charts.umd.js, it looks like the library was compiled with outputting the metadata in metadata.json, so the information on which class has to be injected is not in the component itself, but only in this file. Somehow this has not occurred before and we have no processing of this file yet. I found some traces of a metadatareader e. g. in metadata_reader.ts, but we have to investigate a bit more.

If you like you can try to do some research on how it is processed, read and used, this would help a ton! I think I can have a look at it next week.

Cheers!

@AmarildoK
Copy link
Author

Hi, sorry for the late response. This is not really my area of expertise but I'm willing to probe around and maybe learn new things. Will post any progress I make here.

Sorry again for the late response.

@wtho
Copy link
Collaborator

wtho commented Oct 15, 2019

No problem! This is Open Source work, we all invest our free time.

I might find some time during the weekend to look into it.

@wtho
Copy link
Collaborator

wtho commented Oct 18, 2019

Some findings from my side:

When compiling using ng serve, Angular does tell webpack to use angular_compiler_plugin in the beforeCompile hook.
Somewhere in the plugin (relatively unimportant where) it calls ngModuleResolver.resolve, which loads the metadata via this.host.getMetadataFor(module)
using the metadata_reader.ts (note the replace to the file-ending .metadata.json some lines above).

The loaded metadata information from [...].metadata.json is not being used immediately by Angular, but put in the metadata cache as seen here. Probably this data is accessed later again, but I have not seen where.

Todos

To make this work with jest as well, we have to find an appropriate moment to look for this metadata of loaded classes. Angular does this traversing the root module and all of it's imported modules and their declared components/decorators.

Then we again have to add the data to components before they get loaded by Angular. I have not found out how and when the compilers do this yet, but it must be before instantiating the components on app startup.

Debugging

To debug I used this command and vscode setting:

node --inspect-brk=9206 node_modules/@angular/cli/bin/ng serve

( to run a debuggable instance of angular)

// in launch.json
      {
      "type": "node",
      "request": "attach",
      "name": "Attach to Node.js (9206)",
      "port": 9206,
    },

@ahnpnl
Copy link
Collaborator

ahnpnl commented Oct 21, 2019

Something similar happened to running app too

@wtho
Copy link
Collaborator

wtho commented Oct 21, 2019

@ahnpnl this issue looks different, I think you mean #288, which is a reference issue of several classes that inject each other.

This one is about using a library that was precompiled with ngc and produced a component.metadata.json that contains the reflection metadata (instead of the compiled function).

They might be related though.


This is also the difficulty with jest-preset-angular and ncg compiled projects, we need to

  • get all paths of included modules/components/decorators/services
  • Have to look for possible <filename>.metadata.jsoncompanion and parse it
  • Patch the class/constructor before it gets instantiated

@wtho wtho changed the title Can't resolve All parameters "Can't resolve all parameters" when importing ngc-compiled library with *.metadata.json Oct 21, 2019
@ahnpnl
Copy link
Collaborator

ahnpnl commented Jul 11, 2020

@ahnpnl this issue looks different, I think you mean #288, which is a reference issue of several classes that inject each other.

This one is about using a library that was precompiled with ngc and produced a component.metadata.json that contains the reflection metadata (instead of the compiled function).

They might be related though.

This is also the difficulty with jest-preset-angular and ncg compiled projects, we need to

  • get all paths of included modules/components/decorators/services
  • Have to look for possible <filename>.metadata.jsoncompanion and parse it
  • Patch the class/constructor before it gets instantiated

Do you mean we need to do some logic in the step of module resolution + AST transformation ?

@wtho
Copy link
Collaborator

wtho commented Jul 11, 2020

I think Angular wrote their own TS compiler host in addition to transformers to handle these scenarios (here might be the code of my guess), but I am not very knowledgeable in this area.
I do not know how we can intercept the module resolution step in a scenario with jest and ts-jest.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Jul 11, 2020

it is possible to intercept module resolution of ts-jest because the next version of ts-jest introduces the usage of module resolution for isolatedModules: false . However, for isolatedModules: true, it is not possible because this mode uses transpiler API , only LanguageService or Program allows to customize module resolution.

Module resolution with jest can be achieved via resolve like nrwl nx does https://github.com/nrwl/nx/blob/master/packages/jest/plugins/resolver.ts However, jest module resolution is different from TypeScript module resolution that it is executed whenever jest receives the transformed output from ts-jest.

I will check in the link you mentioned to see, probably I can get something out of it.

ahnpnl added a commit that referenced this issue Dec 12, 2020
Closes #108
Closes #288
Closes #322
Closes #353
Closes #622

BREAKING CHANGE:
With the new jest transformer, `jest-preset-angular` now switches to default to use this new transformer and no longer uses `ts-jest` to transform codes.

Users who are currently doing in jest config
```
// jest.config.js
module.exports = {
    // [...]
    transform: {
      '^.+\\.(ts|js|html)$': 'ts-jest',
    },
}
```

should change to
```
// jest.config.js
module.exports = {
    // [...]
    transform: {
      '^.+\\.(ts|js|html)$': 'jest-preset-angular',
    },
}
```

`isolatedModule: true` will still use `ts-jest` to compile `ts` to `js` but you won't get full compatibility with Ivy.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants