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"