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

Ivy compatibility #409

Closed
wtho opened this issue Jul 9, 2020 · 39 comments
Closed

Ivy compatibility #409

wtho opened this issue Jul 9, 2020 · 39 comments

Comments

@wtho
Copy link
Collaborator

wtho commented Jul 9, 2020

Background

To turn TypeScript into JavaScript, Angular uses the TypeScript compiler, while also taking advantage of its own enhancements and processes. This practice was taken to the next level when the novel Ivy compiler was introduced, which optimizes the build even more and makes Angular a high-level web framework.

If tests running in jest just apply the TS-to-JS-compilation via ts-jest on the Angular code and try to run a tiny Angular App for a test case, many specialized scenarios miss the enhancements the Angular compiler does during Angular development.

The following identified scenarios are possibly such, although there are probably more:

To keep up with Ivy and further Angular enhancements, we want to depend more on the tools Angular provides.

PR #406 shows we are able to add Angular transformers in the preset.

Basic Transformer Analysis

When looking for TransformatorFactory in the Angular codebase (July 2020), we find several transformers, such as

Further objects referenced as transformers are used in program.ts


While we could just try to include all of them, it is a much better path to understand what we are doing and why.

Any help analyzing these and figuring out what these do does help a lot.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Jul 10, 2020

Hi @JiaLiPassion, I know that you work on zone.js but I wonder if you are also familiar with Angular compiler area ? We need some helps from Angular team to tackle this problem. If you can help us or you can help to get someone to help with this, really appreciated !

@ahnpnl
Copy link
Collaborator

ahnpnl commented Jul 11, 2020

Angular compiler design documentation can also be a helpful source https://github.com/angular/angular/blob/master/packages/compiler/design/architecture.md

@JiaLiPassion
Copy link
Contributor

@ahnpnl , sure, I would like to help, and I will try to understand this issue and find out who to contact in the Angular team.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Jul 12, 2020

The best scenario is somehow we can invoke a function which is exposed by Angular compiler. The function will create Angular TypeScript Program and produce compiled output. Then we can deliver the output back to jest.

If this scenario is possible, we can reduce lots of maintenance work and everything will be handled by angular source.

I suspect https://github.com/angular/angular/blob/d1ea1f4c7f3358b730b0d94e65b00bc28cae279c/packages/compiler-cli/src/perform_compile.ts#L237 is something might be useful for this approach.

@wtho
Copy link
Collaborator Author

wtho commented Jul 13, 2020

Yeah, I think using the Angular compiler API is the optimal approach, no fiddling with transformers and such!

But we should take a good look at what Angular is doing different from the general compilation for testing with Karma as a reference.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Aug 14, 2020

I have tried out

import * as path from "path";
import * as ng from '@angular/compiler-cli'
import * as ts from 'typescript'

const basePath = process.cwd()

function compile(
  oldProgram?: ng.Program, _overrideOptions?: ng.CompilerOptions, rootNames?: string[],
  host?: ng.CompilerHost): {program: ng.Program, emitResult: ts.EmitResult} {
  const options = {
    basePath,
    'experimentalDecorators': true,
    'skipLibCheck': true,
    'strict': true,
    'strictPropertyInitialization': false,
    'types': Object.freeze<string>([]) as string[],
    'outDir': path.resolve(basePath, 'built'),
    'rootDir': basePath,
    'baseUrl': basePath,
    'declaration': true,
    'target': ts.ScriptTarget.ES5,
    'newLine': ts.NewLineKind.LineFeed,
    'module': ts.ModuleKind.CommonJS,
    'moduleResolution': ts.ModuleResolutionKind.NodeJs,
    'enableIvy': false,
    'lib': Object.freeze([
      path.resolve(basePath, 'node_modules/typescript/lib/lib.es6.d.ts'),
    ]) as string[],
    // clang-format off
    'paths': Object.freeze({'@angular/*': ['./node_modules/@angular/*']}) as {[index: string]: string[]}
    // clang-format on
  }
  if (!rootNames) {
    rootNames = [path.resolve(basePath, 'e2e/test-app-v9/src/app/app.component.ts')];
  }
  if (!host) {
    host = ng.createCompilerHost({options});
  }
  const program = ng.createProgram({
    rootNames: rootNames,
    options,
    host,
    oldProgram,
  });
  const emitResult = program.emit();
  return {emitResult, program};
}

test('test test', () => {
  console.warn(compile().emitResult)
})

and discover a few nice things:

Notes

  • I can see the benefits of using internal Angular compiler codes, that will simplify a lot on our side related to transformers as well as in sync with Angular. This is also similar to the approach of vue-jest.

  • The biggest downside is: we have to write this preset to be a jest transformer instead and we will depend less on ts-jest, probably only the some parts (e.g. hoisting) but not the complete compilation process.

@wtho
Copy link
Collaborator Author

wtho commented Aug 16, 2020

@ahnpnl Awesome! This breakthrough is great!

Ultimately I see it like this: We can either go in the performance optimization by avoiding tsc and ts-jest completely by completely relying on babel and by adding some transformers for the magic Angular adds or we can use Angular's performance optimization by applying their whole compilation process.

The direction taken here is the latter.

To evaluate if this works as we imagine, we have to take some more steps. A roadmap for these next steps would be awesome. Please correct me, if I forgot something here, but it looks like this to me:

  1. Add jest transformer angular-jest-transformer to jest-preset-angular
  2. Make transformer use Angular compiler-cli
  3. Add ts-jest's hoist transformer to compilation (for mocks)
  4. Figure out if/how ng works with watch and caching
  5. Test a lot

@ahnpnl
Copy link
Collaborator

ahnpnl commented Aug 18, 2020

To evaluate if this works as we imagine, we have to take some more steps. A roadmap for these next steps would be awesome. Please correct me, if I forgot something here, but it looks like this to me:

  1. Add jest transformer angular-jest-transformer to jest-preset-angular
  2. Make transformer use Angular compiler-cli
  3. Add ts-jest's hoist transformer to compilation (for mocks)
  4. Figure out if/how ng works with watch and caching
  5. Test a lot

Yes that is my idea as well.

I did a bit investigation into how Angular CLI invokes Angular compiler. Here is the flow:

ng test ---> architect-command.ts (schedule run) --> create Angular Compiler Plugin (angular_compiler_plugin.ts) 

--> prepare a few transformers (Angular CLI transformers) --> creating Program --> compile codes with Program

Flow explanation

Unknown thing

No idea how this constant value is correctly set for Ivy/VE. This is one of the key things to make TestBed resolve the correct test compiler instance. Do you know something about this constant @JiaLiPassion ?

@ahnpnl ahnpnl changed the title Reuse of Angular Transformer in jest-preset-angular Ivy compatibility Sep 1, 2020
@ahnpnl ahnpnl pinned this issue Sep 1, 2020
@ahnpnl
Copy link
Collaborator

ahnpnl commented Sep 2, 2020

So I found how the flag ɵivyEnabled value is set.

  • ngcc will create a folder __ivy_ngcc__ in each Angular package type. This folder will contain the main script of that package together with source map to that script. Let's say we take an example of @angular/core.

MicrosoftTeams-image

In this picture is a part of @angular/core structure after ngcc. The file core.js is the main file of @angular/core.

  • The file core.js which locates in __ivy_ngcc__ is loaded by Angular CLI (probably Angular CLI delegates that task to webpack, not sure) when enableIvy is false in tsconfig. Once this file is loaded, and in test file we do import { ɵivyEnabled as ivyEnabled } from '@angular/core';. The variable ivyEnabled will have the value false

  • The same explanation applying to enableIvy: true, which then core.js in core/fesm2015 is loaded which result in the value of ivyEnabled is true if we import it into test file.

So I think our task should first look into AngularCompilerPlugin to replicate some logic here, including something like downlevel metadata.

A new question comes up is: what does TestBed do besides setting up test environment ? Does it compile the component code and put in some sort of memory ? According to angular/angular#38600 (comment)

This is indeed a known difference between View Engine and Ivy - in View Engine, components are compiled in the context of some environment - either a platform browser instance, or a test.

In Ivy, there is only one compiled version of a component at any time, and this compilation happens independently of the TestBed

So it seems like the component compilation is done separately in Ivy while TestBed only set up testing environment (like jest globalSetup, globalTeardown, setupFiles, setupFilesAfterEnv). If so, we need to find out how and when the component compilation is executing.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Sep 2, 2020

we got an answer from one Angular team member, he suggested that we can open an issue which they can give some insights about how things work internally

@wtho
Copy link
Collaborator Author

wtho commented Sep 2, 2020

Wow, this is great!
I am really looking forward to advance in this!

Thanks so much for all the effort you do in this direction.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Sep 30, 2020

Today I asked Angular CLI team about reusing their transformers. They said those are private APIs so we are on our own to get it working.

Lucky part is most of necessary apis in compiler-cli are public.

Started a bit today with replace-resources. The conclusion is: we can’t use it because it requires webpack. So our 2 transformers: inline-files and strip-styles are required and must have.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Oct 8, 2020

Update

#543 introduces the usage of Angular compiler which solves the issue with entryComponents in #353. The compiler is still in very early stage so not all tests in e2e working yet but I think we are going the right direction.

This compiler tries to replicate the behavior of Angular Compiler Plugin. The compiler only follows the path for JIT mode, AOT mode is more complex and requires ESM full support which is not available now in jest. In the future, it is a nice to have.

Apparently, using Angular compiler host via createCompilerHost seems to do the trick after running ngcc. Running ngcc is a must-have step.

Regarding to the usage of transformers:

  • inline_resources.ts A transformer to inline resources (such as templateUrl and styleUrls) : this transformer is only used in AOT mode but it is a very nice transformer which we can learn from it to combine our inline-files + strip-styles into one. @wtho I think this one is interested for you.

  • Constructor Parameters Downlevel Transformer downlevels constructor parameters - for JIT compilation - this transformer is a must-have to support forwardRef

I only see the need of using these 2 transformers from Angular, not sure yet about other transformers.

@wtho
Copy link
Collaborator Author

wtho commented Oct 8, 2020

Yeah, I had a look at these two transformers, I used the downlevel transformer in #406 and took some inspiration from the inline_resources.ts for the inline/strip transformers.

I can take over the integration of these in the AngularJestTransformer 👍

@ahnpnl
Copy link
Collaborator

ahnpnl commented Oct 8, 2020

Yes the benefit of using inline-resource for us is:

  • gaining performance by walking less 1 time on AST tree

  • gaining performance by not asking jest to pass HTML to our jest transformer which saves 1 time transpiling.

A potential future feature is type checking template, but that seems to be only done with AOT because AOT has static analysis.

Anyways, it’s a good transformer for us to follow and apply. I’m not sure if that will affect JIT tests or not, worth to try out.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Oct 19, 2020

According to angular/angular-cli#15044, ngcc should always run before running test. So we should find a way to achieve the similar behavior.

I am thinking about 2 ways to tackle this:

  • Force users to run ngcc on their own before running tests.

  • jest-preset-angular runs ngcc before running tests.

Force users to run ngcc

We can check node_modules to find core-testing.umd.js.__ivy_ngcc_bak. If this file doesn't exist, simply throw an error to indicate that users should run ngcc first.

This approach has the advantage that it helps us to reduce the amount of work on jest-preset-angular and dedicate that to Angular CLI.

The disadvantage is not so convenient. But since ngcc only needs to run in postinstall, I think this approach looks fine.

jest-preset-angular runs ngcc before running tests

To achieve this, we need to create an async function and ask users to use it in jest.config.js via jest config async. For example.

// jest.config.js
module.exports = async () => {
  await runNgccProcess()

  return {
    verbose: true,
  };
};

Because jest.config.js only runs once before jest workers spawn for parallel tests, that is the only place we can run script once. ngcc should not be run inside jest transformer because each jest worker uses one singleton jest transformer instance which we can end up running ngcc for all workers if we run ngcc inside jest-preset-angular transformer.

Besides, if I am not wrong, when using jest projects option, jest will read each project jest.config.js. That means the async function which we expose can only run in root jest.config.js.

Overall, I prefer the approach Force users to run ngcc

What is your opinion @wtho ?

@wtho
Copy link
Collaborator Author

wtho commented Oct 19, 2020

I think the second version will be much more convenient for ci environments. If we manage runNgccProcess() to be fast in non-ngcc-running cases, we should offer it to our users. Also it could be a CLI command coming with jest-preset-angular.

I really like the second version, but this is a comfort-level feature. We have no hurry to implement it. We can open a new issue and maybe find contributors who would want to tackle this.

We could offer something like this:

// jest.config.js
const initNgcc = require('jest-preset-angular/init-ngcc')

module.exports = async () => {
  await initNgcc()

  return {
    verbose: true,
  };
};

@ahnpnl
Copy link
Collaborator

ahnpnl commented Oct 19, 2020

Ye expose an async function is simple. The only problem is it is limited to use only in root jest.config.js. The error handling part will be a bit complicated since it needs to check whether or not this function runs in root config.

I think in the meantime, we can implement 1st approach. 2nd approach is nice to have so it can come later.

Seem like angular already had something for us, see example in https://github.com/angular/angular/blob/master/packages/compiler-cli/ngcc/main-ngcc.ts

@ahnpnl
Copy link
Collaborator

ahnpnl commented Oct 19, 2020

More information on ngcc

Operations done by ngcc

  • Scan the node_modules for packages that are in APF Angular package format by checking .metadata.json files. (It just checks for their existence to determine whether a package is an Angular package or not.).
    Produce Ivy compatible versions for each of these packages

  • Produce Ivy compatible versions for each of these packages

Reference: https://indepth.dev/angular-compatability-compiler/

So that means ngcc only cares about stuffs in node_modules.
The option about this preset runs ngcc is the most needed in watch mode. In non-watch mode, it is recommended to run in postinstall hook (applicable for CI too).

@ahnpnl
Copy link
Collaborator

ahnpnl commented Oct 29, 2020

v9.0.0-next.1 is out under next tag.

With this release, it is now possible to use jest-preset-angular with Ivy, see PR #562 . One note is to make sure everything run correctly, please run ngcc in postinstall hook.

We, the maintainers, highly appreciate the help from community with testing. Feel free to open any issues related to this next version.

@bbarry
Copy link

bbarry commented Nov 7, 2020

What is stopping this next version from going live? It seems to work for me at least (not hitting #347 or #353 anymore with various workarounds removed). Having followed along for a few months now I appreciate the complexity of the changes.

Are there any specific aspects you would like us to pay more attention to?

@ahnpnl
Copy link
Collaborator

ahnpnl commented Nov 7, 2020

Currently we are checking about performance. I myself saw a downgrade in performance with my work's projects so still needs some investigation there. In general, for now there are 2 blockers:

  • Jest 27: it is expected to be released before xmas and it has some breaking changes for jest transformer to adopt to. We want to release v9.0.0 together with jest 27 in 1 major version.

  • Performance with next version.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Nov 8, 2020

9.0.0-next.2 is out. This release improves performance over 9.0.0-next.1. At least from my work’s projects, I can see the performance is more less similar to 8.3.2.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Nov 11, 2020

Updated I've tried to integrate ngcc like Angular CLI did. We can do that, but jest parallel mode will fail. ngcc only works with jest runInBand. We have to find another way to integrate ngcc.

I found a way to run ngcc, see commit 8ee7108

@ahnpnl
Copy link
Collaborator

ahnpnl commented Nov 17, 2020

v9.0.0-next.3 is out. This release provides a util script to run ngcc in jest.config.js and a few bug fixes.

To use the util script, you can just define it in your root jest.config.js

// jest.config.js
require('jest-preset-angular/ngcc-jest-processor');

module.exports = {
     // ...
}

@snortblt
Copy link

Not sure if this is related but we're seeing Error: Error on worker #1: RangeError: Maximum call stack size exceeded. during the ngcc phase after adding the util script. It's thrown when compiling different packages depending on the run, so probably not associated with a particular package.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Nov 18, 2020

Do you have a reproduce repo for it ? The util script tries to invoke ngcc instead of running ngcc by package managers.

@snortblt
Copy link

Looks like that was an issue with and outdated Jest package. With v6 it works. Still getting other errors but I suspect they're unrelated. Thanks.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Dec 18, 2020

9.0.0-next.4 was out with support for Jest 27.

@johncrim
Copy link
Contributor

I'm still seeing really slow start (after ngcc, it appears to hang before any tests complete, but it does finish the first tests after 5-8 minutes, then other tests complete approximately at "normal" speed). Do you have any suggestions for troubleshooting this phase of the test run? I was seeing this in the earlier next versions as well. Would be happy to open a separate issue, but it would be nice to be able to identify the cause.

For example, is there a way to list which files are being compiled?

@ahnpnl
Copy link
Collaborator

ahnpnl commented Dec 28, 2020

Setting the environment variable TS_JEST_LOG=ts-jest.log should produce a log file which contains all loggings.

In that file, there will be some loggings containing file name with the text "getCompiledOutput". That is the logging for file names which are compiled.

Also follow tips https://kulshekhar.github.io/ts-jest/docs/options/isolatedModules#performance will help a bit.

Bootstrap speed depends mainly on the amount of files to read. This includes Jest runtime file set + TypeScript compiler(internal jest-preset-angular) file set.

The optimal file set for TypeScript compiler should only include type definition files so compiler API will spend less time on reading file system.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Jan 6, 2021

v9.0.0-next.5 was out. In this release we use replace resources transformer from @ngtools/webpack for isolatedModules: false. This will bring several benefits:

  • jest-preset-angular now works like 95%+ similar to what Angular does with Karma + Jasmine
  • The replace-resources transformer helps us to stay compatible with both CommonJS and ESM. Since Node 15 has marked ESM support as stable (see https://nodejs.org/api/esm.html#esm_modules_ecmascript_modules) and Jest is working on it, we have a nice preparation for that.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Jan 14, 2021

v9.0.0-next.6 was out. In this release, both isolatedModules: false and isolatedModules: true use the similar set of AST transformers, except that downlevel ctor transformer only works with isolatedModules: false.

Besides, ESM support for both isolatedModules: false and isolatedModules: true was also completed. Now you can try out Jest ESM with Angular.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Feb 12, 2021

9.0.0-next.8 was out with an important change is test speed on Windows. Now Windows test speed should be more less similar to 8.3.2.

Besides, isolatedModules: true now can use downlevel ctor transformer like isolatedModules: false, which means forwardRef will work for both modes.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Mar 12, 2021

I will close this issue as now we have reached the stability from feature point of view with next-10 (work similar to Karma + Jasmine) as well as documentation is ready (choose Next version doc to see).

Now only wait for Jest and some potential fixes from ts-jest to finalize the release.

@ahnpnl ahnpnl closed this as completed Mar 12, 2021
@krokofant
Copy link
Contributor

@ahnpnl What are we waiting for from Jest and ts-jest? I don't know what issues I should track 😄

@ahnpnl
Copy link
Collaborator

ahnpnl commented Apr 1, 2021

we are only waiting for official release of Jest 27 (and ts-jest will follow).

@ahnpnl
Copy link
Collaborator

ahnpnl commented May 27, 2021

v9.0.0 was out.

@ahnpnl ahnpnl unpinned this issue May 27, 2021
@nschimek
Copy link

nschimek commented Nov 4, 2021

I just felt the need to post how eternally grateful I am for this thread. Seriously, you guys are forever in my debt.

Little backstory: I spent the past week or so migrating a fairly low-key enterprise app to Jasmine/Karma to Jest, utilizing of course jest-preset-angular among other libraries. I did my diligence and made sure that the first dozen or so test files I converted ran in our Jenkins environment. They did without issue. And so I powered on, nothing could stop me as I struck down jasmine.createSpyObj after createSpyObj, and brought the righteous glory of Jest to all corners of the codebase. I stopped checking my daily builds in Jenkins because I knew the unconverted tests would continue to fail.

And then, after re-reaching 99% coverage locally, with all 1200+ tests passing, I expected that sweet row of greens in Jenkins...and didn't get it. Tests were blowing up for the strangest things:

  • Missing entryComponents for some components.
  • Anything using SpectatorRouting was exploding with a bizarre demand that the hosting component be included in the testing module. Although this makes sense (guessing this is how SpectatorRouting tests) given the next issue.
  • ​Template parse errors: 'some-component' is not a known element errors causing tests to fail. Now, I actually don't mind this, because it means I need to clean-up some tests. So I set about correcting all of these. But, a bigger issue loomed large.
  • Material Table simply refused to render. Anything within <table mat-table></table> was simply...not there. No matter what I tried and what query approach I took, Jest couldn't see it and all tests would fail.
  • And obviously, the biggest issue here is that the tests were failing in Jenkins but not locally. This is not a good place to be - what will tomorrow's Jenkins-only test failure be?

Troubleshooting this was a bit of a nightmare: it is, after all, the dreaded "works on my machine" scenario. All of my Googling was failing me, and I spent a whole day on this getting absolutely nowhere. Late last night, after mostly focusing on individual symptoms (i.e. what would cause tests to fail for missing components?) I remembered that was something different from JIT to AOT. I started finding old threads about mat-table not rendering correctly in JIT. And then, crucially, I also saw this in the error from the first issue above:

at JitCompiler.Object.<anonymous>.JitCompiler._createCompiledHostTemplate (../packages/compiler/src/jit/compiler.ts:232:13)

JIT Compiler? In my AOT-enabled application? Why? Well, I still can't answer that. I'm so far down the rabbit hole I don't foresee our DevOps guys being able to give me a good answer (although I plan to try anyway). And besides, the following line in jest.config.js solved everything:

require('jest-preset-angular/ngcc-jest-processor');

And so it was, a gloriously green build, and my sanity has returned. Thank you all!

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

8 participants