In the previous post, I explored how J2CL is used in Bazel. Starting with this post, with this knowledge, I'll try to design a Gradle plugin for J2CL.
Let's start with the low-level building blocks.
All the J2CL (
and Closure compiler (
can easily be called from Java processes
(the Bazel workers actually call different, sometimes internal, APIs),
so they could be called from Gradle workers.
It also shouldn't be a problem to call the J2CL tools incrementally,
only processing changed files;
well, actually, it won't be a problem for the
which processes files one by one in isolation;
J2clTranspile however, just like
would also need to reprocess other Java files referencing the changed files,
so making it incremental would mean knowing about those dependencies;
let's leave this optimization work for later
(FWIW, the way Bazel deals with it is to not do any incremental/partial processing of any kind,
but instead using small sets of files, generally at the Java package level).
That means we'd have to declare 3
configurations (for the 3 tools' dependencies),
and we could create corresponding tasks with proper inputs and outputs.
That would work for project sources though, but not external dependencies.
For those, we could probably use artifact transforms,
but that excludes compiling the source files with
at least if we want to leverage the Gradle standard
(I've been floating the idea of an ASM-based
which would solve the problem then,
as an artifact transform of the binary JAR).
Let's keep external dependencies for later though.
Speaking of the
for J2CL we'd want to configure their
bootstrapClasspath to the Java Runtime Emulation JAR.
-bootclasspath can only be used when compiling for Java 8,
so that rules out using any Java 9+ syntax: private methods in interfaces,
var for type inference, etc.
For those more recent Java versions, we'd want to use
but that's an entirely different format,
and one that's not even supported by Bazel yet, let alone produced by J2CL
(though maybe it's not that hard to create from the Java Runtime Emulation JAR).
Fwiw, Google also faces the same issue for Android for adding Java 9+ syntax support,
as well as J2ObjC,
so we can be sure they'll find a solution
(they're actually already working on it).
For tests, the annotation processor and its
@J2clTestInput have been designed as implementation details,
hidden behind a
j2cl_test rule in Bazel.
Contrary to Bazel where one
j2cl_test rule only runs a single test class (which could actually be a test suite),
gen_j2cl_tests macro to generate them automatically from source files using a naming convention,
in Gradle we'll have a single task for the whole
We could however have a task that generates a JUnit suite, annotated with
and referencing the test classes, using a naming convention,
and then processes it with the annotation processor.
This will generate some Java code to be transpiled with J2CL.
This phase can reuse the
J2clTranspile task from above,
src/test/java and the generate Java code all at once.
test_summary.json file then needs to be processed
and fan out one Closure compilation per JS entrypoint
(one per non-suite test class, with
.testsuite file extension).
This cannot reuse the
ClosureCompile task though:
we want a single task driving multiple Closure compilations.
The result will thus be several JS applications;
we'll put them into separate directories (named after the original Java test),
and generate an additional HTML page to run them.
The task could be made incremental, only recompiling tests that have changed,
but that would need dependency information between files
(that can be extracted using Closure, with some additional work;
or possibly even just analyzing the
BTW, that knowledge could possibly also be used by the
to skip compilation if a file has changed that's not needed by anything.
Finally, we'll need to run those generated tests in browsers. The best way to do that is through Selenium WebDriver. We'll thus want a task taking those directories of compiled tests, that starts an HTTP server to serve them, and loads each of them in a web browser through WebDriver, generating reports (that helps skipping the task if no input has changed).
Putting it all to work
For an application like the HelloWorld sample from the Bazel repository, wiring all those tasks together would lead to a graph like the following:
The code for these tasks and sample project is available on Github.
Now that we have the building blocks and are able to wire them together for a simple example, the next steps will be:
- handling external dependencies (stripping and transpiling them on-the-fly)
- handling project dependencies (a library subproject would expose its transpiled sources to an application subproject)
- defining conventions to make things as simple as applying a Gradle plugin (a J2CL application would be the easiest, as libraries could target J2CL, GWT, the JVM, J2ObjC, etc.)