Vertispan / j2clmavenplugin

Maven plugin to launch new J2CL compilation
https://vertispan.github.io/j2clmavenplugin/
Apache License 2.0
53 stars 26 forks source link

Support "Service Loader" pattern/api in compiled JS #244

Open niloc132 opened 8 months ago

niloc132 commented 8 months ago

This feature request is mainly in service of an excellent i18n experience, but there will be many other use cases it can support as well. The intended setup allows a project to have a (potentially transitive) dependency on some API that defines an interface for a service/resource, and then dependencies that offer implementations for that interface can be provided. Consumers of the interface need not know about which implementations will be available, and can rely on other features like a runtime lookup or compile-time "define" to let the underlying service loader mechanism select the appropriate one.

For internationalization, this separation and complete compilation is especially important:

Logging APIs might also be interested in using this.

It might also be important to support JVM use cases, as well as have a story for how to use this in GWT2 projects (likely a Generator that checks the value of a property at compile time, and emits an appropriate implementation).

Rough components to this:

Common pattern/implementation for the "service loader" itself

Our initial experiments suggested this would simply be a JsPropertyMap<T> and a static method to let each implementation self-register on startup, plus an api to call with a string to get the desired implementation. A wrapper type around this will append a known string prefix/suffix to these strings to ensure they are matched as part of this feature. Additionally, a runtime check should be performed to validate that the entire key is a valid JS identifier.

Closure-compiler

Better optimized output

Run a modified ConvertToDottedProperties compiler pass earlier than it runs today, only on reading and writing properties by string literal, and only if that string matches the required prefix/suffix. See https://groups.google.com/g/closure-compiler-discuss/c/3Gsd73xdt1U

Multi-stage build

Running the "finalization" stage multiple times suggests that we should cache the output of the earlier stages, so that only the new --define-supplied constants need to be propagated through the program.

Additional support when defining a project

The j2cl-maven-plugin and related tools could provide some "permutation-like" features, computing the matrix of possible outputs.

niloc132 commented 8 months ago

The closure custom compiler pass was fairly easy to implement, but the optimization process in closure is quite different from GWT, and it is taking time to find the right way to incorporate the new pass into the existing set of passes - if the pass runs too early, we can't rely on "constantKeyName" + "$some$key$suffix" already being optimized to "constantKeyName$some$key$suffix" and inlining method calls to direct map access. On the other hand if it runs too late, we can't rely on the unused keys being optimized out. Even if we enforce a rule that all keys (i.e. locale names) must end with some specific key suffix, the earliest pass at inlining method params is still later than the last pass at optimizing out unused functions.

As with the original discussion, running the generated output through the compiler a second time is enough to finish optimizations, so it isn't a question of having unoptimizable code, just the particular order of the passes.

This could either require that we produce more tightly optimized JS/Java to begin with, or make bigger changes to the default pass config. Or, this is a red herring, and some other change to the type system needs to take place, to signal that the "map" actually only has static properties after this new pass is complete.