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

Handling external JavaScript-based libraries #1041

Open
evmar opened this issue Jul 7, 2019 · 0 comments
Open

Handling external JavaScript-based libraries #1041

evmar opened this issue Jul 7, 2019 · 0 comments

Comments

@evmar
Copy link
Contributor

evmar commented Jul 7, 2019

In #1039 @theseanl is asking about module augmentation and externs generation, which requires a better understanding of how externs work at all, so I thought I'd write down a summary here.

When file A imports file B, tsickle rewrites them both to use goog.module/goog.require so Closure compiler can understand them. When you want to use an external library like react or jquery that is not written as a goog.module, what should tsickle do?

External libraries in Closure

The answer is that you first need to understand what Closure can do, and after you understand that, convince tsickle to emit whatever Closure wants.

There are two fundamental options:

  1. You pass in the library as part of compilation so that the compiler includes it in the output.
  2. You leave the library out of compilation, and handle it yourself.

For option 1 to work, you need the library to successfully pass compilation, and convince the compiler to put them in the output in the right order (perhaps by adding goog.module to the library source manually). In our experience this is possible for tiny libraries that you're willing to modify (like say "a uuid() function") and not possible for large libraries (like react).

So instead we generally recommend option 2. Here, your project has to bring in the library itself, e.g. via a separate <script> tag or by manually concatenating the library in front of the Closure compiled output, and then you need to convince Closure to produce a compiled bundle that references that script.

(If you're within Google you can read our massive doc go/tpl-js that tours the different ways different apps have tried to solve this, which are all minor variants of the above. It also has our proposal to fix it, more on this below. It's not too important here, it's just the above two paragraphs in more detail.)

External libraries as scripts

If you go with option 2, now you need to figure out how your code can refer to the external library. The simplest thing is when your library just produces some globals. E.g. after the <script> tag, imagine your library adds a function to window. This is relatively easy to make work in Closure, via externs, where you tell the compiler which global variables exist outside of your program.

In tsickle we take any script d.ts (and 'declare' statement within .ts) and generate externs from it. This ~mostly works. (It actually is still wrong: if a.ts does declare var x: string and b.ts does declare var x: number, TypeScript is happy to accept it but you get broken externs. This is an example of how subtle this all is, there are no good answers.)

External libraries as modules

More common these days is for an external library to be written as a module: in your TypeScript code you write an import statement. Closure basically has no real model for making this work -- if you import a library, it expects that library to be part of compilation.

So when tsickle sees a statement like import * as X from 'mylibrary', what should it do? All options are bad.

Our current design

Our current answer, which is not great but I am writing it down just so you can understand it, is to treat those imports the same as any other import. This means we let TypeScript resolve it to whatever file it thinks that import actually resolves to (which often means following node module resolution) and then we generate an import statement like goog.require('some.dotted.path.to.some.file');. That path often ends up under node_modules somewhere because that is usually where these libraries are defined.

But nothing defines the corresponding goog.module to satisfy that import, so this fails compilation. Within Google we sometimes write such a file by hand that attempts to glue things back together, see next section.

Finally, what happens to typings? The only reason the above import statement was even allowed by TypeScript is because there is a typings file somewhere that defines the library. This typings file is a module (contains an export statement), which we cannot translate into externs, because externs only let you define globals. As another weird hack, what we currently do is put all the types definitions into a hidden namespace with a name like tsickle_hidden_foo.your_library_here. This at least allows code that really wants to refer to those types to find them somehow, but is not otherwise linked into the rest of this system.

Gluing it back together

If there is a file (written by hand) that does something like

goog.module('the.name.under.node_modules');
/** @type {tsickle_hidden_foo.your_library} */
exports = the_actual_library;  // TODO: you figure this out

This glues all the systems back together. L1 makes the Closure compiler believe the import statement is satisfied, L2 shoves the TypeScript types into it, and in L3 you might be able to figure out what actual value should be used at runtime. In theory tsickle could autogenerate something like this maybe, but this whole area is really fragile already.

What should users do

It's all bad, I am sorry. As I wrote above we have made some proposals to Closure about how to make this better (which we haven't been able to convince the Closure team about yet -- briefly, my opinion is that we should make option 1 actually work) but they are very busy. Also there is some handling of node_modules within Closure itself (process_common_js_modules) that I have never taken the time to understand.

theseanl added a commit to theseanl/tscc that referenced this issue Sep 18, 2019
Changed the way we support external modules to what is described in angular/tsickle/issues/1041.

Previously, what we did is described in a previous readme: https://github.com/theseanl/tscc/tree/b0f656e773bc4b43dba6876aa68340f2b5d71dd8#detailed-description-of-external-modules-handling. We replaced names that references the export of an external module. In addition to that, we used some wild hacks that required patching tsickle in order to prevent generation of `goog.requireType()` for external modules.

The way described in the above linked issue requires is simpler and make us free of such hacks. I also vaguely think that this will provide a more correct behavior in case of accessing an external module's global name having side effects.
theseanl added a commit to theseanl/tscc that referenced this issue Sep 18, 2019
…ngular/tsickle/issues/1041.

Previously, what we did is described in a previous readme: https://github.com/theseanl/tscc/tree/b0f656e773bc4b43dba6876aa68340f2b5d71dd8#detailed-description-of-external-modules-handling. We replaced names that references the export of an external module. In addition to that, we used some wild hacks that required patching tsickle in order to prevent generation of `goog.requireType()` for external modules.

The way described in the above linked issue requires is simpler and make us free of such hacks. I also vaguely think that this will provide a more correct behavior in case of accessing an external module's global name having side effects.
theseanl added a commit to theseanl/tscc that referenced this issue Sep 18, 2019
Changed the way we support external modules to what is described in angular/tsickle/issues/1041.

Previously, what we did is described in a previous readme: https://github.com/theseanl/tscc/tree/b0f656e773bc4b43dba6876aa68340f2b5d71dd8#detailed-description-of-external-modules-handling. We replaced names that references the export of an external module. In addition to that, we used some wild hacks that required patching tsickle in order to prevent generation of `goog.requireType()` for external modules.

The way described in the above linked issue requires is simpler and make us free of such hacks. I also vaguely think that this will provide a more correct behavior in case of accessing an external module's global name having side effects.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant