Creating JavaScript modules
With TeaVM you can not only compile Java application to JavaScript or WebAssembly, but also create JavaScript and WebAssembly libraries with Java. You are required to put additional annotations on your code and make some extra setup in the build system. These topics are uncovered below.
Setting up module system
This section is only relevant for JS backend. Currently, WebAssembly can't compile to JS modules.
By default, TeaVM generates UMD wrapper, which is compatible both with AMD and CommonJS module systems, or puts Java entry points into global namespace, if none available. You can change this behaviour, e.g. to produce ES2015 modules.
If you are using Gradle, you need following configuration:
teavm.js {
moduleType = JSModuleType.ES2015
}
where available module types are following:
COMMON_JS– CommonJS module, e.g. for compilation with Node.js.UMD– (default) UMD wrapper for compatibility with AMD, Node.js, if available.NONE– make TeaVM entry points available as global declarations (for example, to use in web).ES2015– produce ES2015 modules.
another way is to specify teavm.js.moduleType gradle property or js.moduleType TeaVM property
(please, refer to Gradle documentation).
In Maven you need following in plugin configuration:
<jsModuleType>ES2015</jsModuleType>
Generating a module
By default, you use Java convention with public static main method of the main
class specified in configuration.
A module will be generated that exports one method called main.
If you need to convert a library, don't follow this convention.
Instead, create static methods in your main class and annotate them with @JSExport annotation,
as follows:
public class MyModuleExample {
@JSExport
public static void foo() {
System.out.println("foo called");
}
@JSExport
public static String bar(int a, int b) {
return "bar: " + (a + b);
}
}
TeaVM will produce module that exports foo and bar functions.
In case you set up ES2015 module type in TeaVM, you can use this module from JS side like follows:
import { foo, bar } from './myModuleExample.js'
foo();
bar(2, 3);
Exporting properties from module
To export properties from modules, use Java beans naming convention and put @JSProperty
annotation on methods:
public class ModuleWithExportedProperties {
private static int bar = 23;
@JSProperty
public static String getFoo() {
return "foo value";
}
@JSProperty
public static int getBar() {
return bar;
}
@JSProperty
public static void setBar(int value) {
bar = value;
}
}
TeaVM will produce module that exports foo readonly property and bar read-write property.
Usage example:
import * as java from './myModuleExample.js'
console.log(java.foo);
console.log(java.bar);
java.bar = 42;
console.log(java.bar);
Returning non-primitive values from module
If you need a function that produces an object, you can do with either of two ways:
- Return a simple Java class that exports its declarations to JavaScript with
@JSExport. - Return a Java class that implements one of sub-interfaces of JSObject interface (see Interacting with JavaScript).
For example:
public class Point {
private int x;
private int y;
@JSExport
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@JSExport
@JSProperty
public int getX() {
return x;
}
@JSExport
@JSProperty
public int getY() {
return y;
}
@JSExport
public static Point getZero() {
return new Point(0, 0);
}
}
public interface Color extends JSObject {
@JSProperty
int getRed();
@JSProperty
int getGreen();
@JSProperty
int getBlue();
}
public class ColorImpl implements Color {
private int red;
private int green;
private int blue;
public ColorImpl(int red, int green, int blue) {
this.red = red;
this.green = green;
this.blue = blue;
}
@Override
public int getRed() {
return red;
}
@Override
public int getGreen() {
return green;
}
@Override
public int getBlue() {
return blue;
}
}
public class MyModule {
@JSExport
public static Point createPoint(int x, int y) {
return new Point(x, y);
}
@JSExport
public static Color createColor(int red, int green, int blue) {
return new ColorImpl(red, green, blue);
}
}
There's some differences between these approaches.
The first approach exports entire class, so that you can consume it and then, for example,
use it in instanceof expression, or call its static methods.
The second approach returns just anonymous object with certain methods and properties.
From JS side:
import { createPoint, createColor, Point } from 'myModule.js';
let pt = createPoint(2, 3);
console.log(pt.x, pt.y);
let color = createColor(255, 255, 0);
console.log(color.red, color.green, color.blue);
console.log(pt instanceof Point); // prints 'true'
console.log(color instanceof Point); // prints 'false'
Note that if you want to specify exported class name explicitly, you can use @JSClass annotation:
@JSExportClasses(Foo.class)
public class MyModule {
}
// This class will be seen as 'Bar' in JS
@JSClass(name = "Bar")
public class Foo {
@JSExport
public Foo() {}
}
Or if the class is already referenced from exported method signatures, @JSExportClasses is not
needed — TeaVM will export it automatically. The @JSClass(name = "Bar") annotation on Foo
controls the name under which it appears in the generated module.
Taking non-primitive parameters
To take non-primitive parameters to exported methods, you can use the same two approaches, that you use to return values, i.e.:
public class MyModule {
@JSExport
public static Point createPoint(int x, int y) {
return new Point(x, y);
}
@JSExport
public static double length(Point pt) {
return Math.sqrt(pt.getX() * pt.getX() + pt.getY() * pt.getY());
}
@JSExport
public static void logColor(Color color) {
System.out.println("rgb: " + color.getRed() + ", " + color.getGreen()
+ ", " + color.getBlue());
}
}
The difference is however, a bit more sensitive.
In the first case you should pass exactly instance of Point class that you got from Java module.
In the second case you are only required to pass object with corresponding properties.
For example, this is the right usage of length method:
import { createPoint, length } from './myModule.js';
let pt = createPoint(2, 3);
console.log(length(pt));
And this is invalid:
import { length } from './myModule.js';
console.log(length({ x: 2, y: 3 }));
However, this is valid:
import { logColor } from './myModule.js';
logColor({ red: 255, green: 255, blue: 255 });
Exporting classes explicitly
When a Java class mentioned explicitly somewhere in signature of module methods,
they are exported automatically.
However, sometimes you need to export some class that does not present in signatures of other methods.
In this scenario you just put @JSExportClasses annotation to the module entry point class,
or alternatively to other exported classes.
The behaviour is following: as soon as a class with @JSExportClasses annotation is exported to JavaScript
for some reason, all classes enumerated in the annotation will also be exported. For example:
@JSExportClasses({ Point.class })
public class MyModule {
// This class is empty, it does not export any methods to JavaScript
// However, Point class is exported
}
Exporting constructors
@JSExport can be placed on individual constructors to control which constructors are visible on
the JavaScript side. If a class has any constructor annotated with @JSExport, only those
constructors are exported; non-annotated constructors remain private to Java:
public class Point {
private int x;
private int y;
@JSExport
public Point(int x, int y) {
this.x = x;
this.y = y;
}
// This constructor is only used from Java, not exported to JS
Point(String encoded) {
// ...
}
}
On the JavaScript side, only the two-argument constructor is accessible via new Point(x, y).
Exporting instance members
@JSExport can be placed on instance methods and properties of a plain Java class, not just on
static methods. The class itself does not need any class-level annotation; TeaVM detects the
exported members automatically when the class appears in an exported method's signature.
public class Counter {
private int value;
@JSExport
public Counter(int initial) {
this.value = initial;
}
@JSExport
@JSProperty
public int getValue() {
return value;
}
@JSExport
public void increment() {
value++;
}
@JSExport
public static int baz() {
return 99;
}
@JSExport
@JSProperty
public static String staticProp() {
return "I'm static";
}
}
public class MyModule {
@JSExport
public static Counter createCounter(int initial) {
return new Counter(initial);
}
}
From JavaScript:
import { createCounter } from './myModule.js';
const c = createCounter(5);
console.log(c.value); // 5
c.increment();
console.log(c.value); // 6
console.log(Counter.baz()); // 99
console.log(Counter.staticProp); // "I'm static"
Exporting class hierarchies
When exported classes form an inheritance hierarchy, TeaVM preserves the prototype chain so that
instanceof and inherited members work correctly on the JS side:
@JSExportClasses({
BaseClass.class,
Subclass.class
})
public class MyModule {}
public class BaseClass {
@JSExport
public BaseClass() {}
@JSExport
public String foo() {
return "Base.foo";
}
}
public class Subclass extends BaseClass {
@JSExport
public Subclass() {}
@Override
public String foo() {
return "Sub.foo";
}
@JSExport
public String bar() {
return "Sub.bar";
}
}
From JavaScript:
import { BaseClass, Subclass } from './myModule.js';
const base = new BaseClass();
const sub = new Subclass();
console.log(base.foo()); // "Base.foo"
console.log(sub.foo()); // "Sub.foo"
console.log(sub.bar()); // "Sub.bar"
console.log(sub instanceof BaseClass); // true
Only methods annotated with @JSExport (or inherited from an exported superclass) are visible to
JavaScript; un-annotated overrides are still called polymorphically from the Java side.
Varargs in exported methods
Exported methods may use Java varargs. TeaVM translates them to JavaScript rest parameters, so callers can pass any number of arguments:
public class MyModule {
@JSExport
public static String greet(String prefix, String... names) {
return prefix + ": " + String.join(", ", names);
}
}
import { greet } from './myModule.js';
console.log(greet("Hello", "Alice", "Bob", "Carol"));
// "Hello: Alice, Bob, Carol"