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

Experiment: Try out TypeScript Program in compiler #1146

Closed
mtebenev opened this issue Jul 4, 2019 · 10 comments · Fixed by #1433
Closed

Experiment: Try out TypeScript Program in compiler #1146

mtebenev opened this issue Jul 4, 2019 · 10 comments · Fixed by #1433

Comments

@mtebenev
Copy link

mtebenev commented Jul 4, 2019

Issue :

Hi guys! Is there a way to get access to the TS Program/TypeChecker instance from inside of a custom AST transformer?
There was a similar question #303 At that time it wasn't possible but now ts-jest uses TS language service I guess it is possible? Would passing the languageService to ConfigSet introduce some issues?

@wtho
Copy link
Contributor

wtho commented Jul 5, 2019

Hey!

I looked a bit into ts-jest recently to look for the TS Program (and the TypeChecker), but it seems to me that ts-jest does avoid to use it. I found this comment:

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

ts-jest exclusively uses transpileModule to transpile all tests independently, see https://github.com/kulshekhar/ts-jest/blob/master/src/compiler.ts#L97-L102.
Here ts is the actual resolved typescript module, but createProgram is never called.

Maybe we could learn e. g. from Angular/webpack, how it compiles incrementally using the TS language service with TS Program and TypeChecker (this is the place where Angular's webpack plugin creates the program etc). Obvoiusly tansformers without TypeChecker functionality are way less powerful and restricted. The language service itself gets created in createCompiler (compiler.ts) when isolatedModules is not set to true.

As an ugly workaround you could create your own program instance for the transformer using configSet.compilerModule.createProgram(configSet.typescript.fileNames, configSet.typescript.options, undefined) and then call program.getTypeChecker to access the TypeChecker.

I'm interested in your findings as well, cheers!

@mtebenev
Copy link
Author

mtebenev commented Jul 5, 2019

As an ugly workaround you could create your own program instance for the transformer using configSet.compilerModule.createProgram(configSet.typescript.fileNames, configSet.typescript.options, undefined) and then call program.getTypeChecker to access the TypeChecker.

@wtho I'm already doing this ugly workaround in my PoC project

https://github.com/mtebenev/typefixture/blob/5ccf238858b9f07e7d314ceb14a66edc30438f44/packages/core/src/kernel/instrumentation/fixture-reference-finder.ts#L23-L28

And I've found it extremely slow (adding roughle 1.5seconds per file). So if such PR would be accepted I can prepare modifications providing the language service for transformers.

@wtho
Copy link
Contributor

wtho commented Jul 6, 2019

I am not a ts-jest-member, just stumbled upon the same issue around the same time.

But I personally would be interested in performance results of an implementation with a single Program instance for all compilations done in ts-jest. Maybe calling transpileModule is actually slower when called for each file individually, e. g. if a big dependency tree has to be loaded every single time than doing it just once using the TS Program, and the comment in the code is a wrong assumption for big projects.

I am not in the position to suggest a PR, but I think it is valuable for ts-jest to learn about the performance implications of this implementation.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Jan 10, 2020

According to @huafu , he said once about transformer should be only initialized once for all tests. Does it mean about Program instance ?

@wtho
Copy link
Contributor

wtho commented Jan 11, 2020

No, it is not really related to the transformer lifecycle.

Basically, there are two ways to compile TS code to JS calling the tsc compiler programmatically:

  • transpileModule, which is a simpler, more naive approach of transpiling a simple module (usually a single file), it cannot verify import statements in depth
  • createProgram and program.emit creates a compiler host that gathers lots of context and diagnostics during the compilation, if program is used and supplied to transformers, they can access the TypeChecker, which knows about references of other modules, e. g. which class is referenced by a class reference. This additional context information can be super useful.

huafu once stated (see above) that createProgram is slow, and I totally agree it is slow when using it in unit tests for tiny applications. But in a big actual application, where the compiler host and program is created only once for all tests, it can be very helpful.

See https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API for further information.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Jan 11, 2020

Not dive into createProgram yet but creating instance once for all tests already sounds logical. I’d go for it.

How do we approach that solution ?

@wtho
Copy link
Contributor

wtho commented Jan 11, 2020

This might be too costly, as huafu stated before, but we should give it a shot.
I also have not worked much with the createProgram of tsc.
Probably it is best to create the program instance on the top level of createCompiler inside compiler.ts, or maybe even a language server instance, although I am also not too sure about the implications of it.

There is not much literature about the TS compiler API. It is probably best to have a look at how other compilers (e. g. Angular-cli, Vue-cli, webpack-ts-loader, webpack-awesome-typescript-loader) do this and learn from them. I looked a bit into Angular Compilation, but it is quite complicated, it might be better to have a look at the others (and check if they use createProgram).

@ahnpnl
Copy link
Collaborator

ahnpnl commented Jan 11, 2020

Thanks for the info 🙏, I’m curious how the examples do with that pattern

Updated: I have an idea for this experiment.

  • Before processing is invoked, we can create Program which will compile all ts files based on fileNames from ParsedCommandLine object.
  • In the emit method in Program, we can pass in transformers. This allows custom transformers specifying in ts-jest config.
  • update cache with emit result from Program
  • When 1st time processing is invoked, simply get emit result from Program.
  • When test is run again, jest will simply get result from cache.
  • When isolatedModules: true, use transpileModuleto compile.

I'd say the main changes will be:

  • Use Program instead of language service to compile when isolatedModules: false.
  • Compile before processing is invoked (this might be tricky).
  • Everything about cache, isolatedModules: true etc remains the same.
    Not sure how it will affect to the performance though.

What do you think about the approach @wtho @mtebenev ?
cc @kulshekhar

@ahnpnl
Copy link
Collaborator

ahnpnl commented Mar 19, 2020

@mtebenev , @wtho Program is available now for transformers. You can test against master

@wtho
Copy link
Contributor

wtho commented Mar 19, 2020

Yeah! Thanks so much!

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

Successfully merging a pull request may close this issue.

3 participants