Blog of Bastien Jansen

Ceylon in the browser (again)

As you might (or might not) know, Ceylon is more than a JVM language. It has been possible to compile Ceylon code to JavaScript for a long time, but other platforms such as Dart or LLVM are around the corner.

Having a JS backend means that you can actually write Ceylon code that can be run in a web browser, giving the opportunity to share code between the server and the client. The web IDE is a very good example of this. Up until now, using Ceylon in a browser wasn't really straightforward though. The good news is, Ceylon 1.2.1 brought two major features that overcome this problem:

Let's see how these fit together.

Creating a new project

First things first, we need an empty project that will hold two modules:

  • com.acme.client is a native("js") module that imports ceylon.interop.browser:

    native("js") module com.acme.client "1.0.0" { import ceylon.interop.browser "1.2.1-1"; }

  • com.acme.server is a native("jvm") module that imports ceylon.net:

    native("jvm") module com.acme.server "1.0.0" { import ceylon.net "1.2.1"; }

Serving Ceylon modules

In order to run com.acme.client in a browser, we have to import it from an HTML file. The recommended way is to use RequireJS:

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Hello from Ceylon!</title>
</head>
<body>
    <div id="container">
    </div>
    <script src="//requirejs.org/docs/release/2.1.22/minified/require.js"></script>
    <script type="text/javascript">
    require.config({
        baseUrl : 'modules'
    });
    require(
            [ 'com/acme/client/1.0.0/com.acme.client-1.0.0' ],
            function(app) {
                app.run();
            }
    );
    </script>
</body>
</html>

Here, we tell RequireJS to use the prefix modules when downloading artifacts from the server, which means we need something on a server that will listen on /modules, parse module names and serve the correct artifact.

Option 1: using a Ceylon server

Ceylon SDK 1.2.1 introduced a new endpoint named RepositoryEndpoint, that uses a RepositoryManager to look up module artifacts in one or more Ceylon repositories, like the compiler or Ceylon IDE do:

import ceylon.net.http.server.endpoints {
    RepositoryEndpoint
}
import ceylon.net.http.server {
    newServer
}

"Run the module `com.acme.server`."
shared void run() {
    value modulesEp = RepositoryEndpoint("/modules");
    value server = newServer { modulesEp };

    server.start();
}

By default, this endpoint will look for artifacts in your local Ceylon repository, but also in the compiler's output directory. This greatly simplifies our development workflow, because each time we modify files in com.acme.client, Ceylon IDE will rebuild the JS artifacts, which can then be immediately refreshed in the browser.

Finally, to serve static files (HTML, CSS, images etc), we need a second endpoint that uses serveStaticFile to look up files in the www folder, and serve index.html by default:

function mapper(Request req) 
        => req.path == "/" then "/index.html" else req.path;

value staticEp = AsynchronousEndpoint(
    startsWith("/"), 
    serveStaticFile("www", mapper),
    {get}
);

value server = newServer { modulesEp, staticEp };

If we start the server and open http://localhost:8080/, we can see in the web inspector that the modules are correctly loaded:

Option 2: using static HTTP servers

Option 1 is interesting if you already have a backend written in Ceylon. Otherwise, it might be a little too heavy because you're basically starting a Ceylon server just to serve static files. Luckily, there's a way to create a standard Ceylon repository containing a module and all its dependencies: ceylon copy.

ceylon copy --with-dependencies com.acme.client

This command will copy the module com.acme.client and all its dependencies to a given folder (by default ./modules), preserving a repository layout like the one RequireJs expects. This means we can start httpd or nginx and bind them directly on the project folder. Modules will be loaded from ./modules, we just have to configure the server to look for other files in the www directory.

Attention though, each time we modify dependencies of com.acme.client, we will have to run ceylon copy again to update the local repository.

Option 2 is clearly the way to go for client apps that don't require a backend. Like option 1, it doesn't force you to publish artifacts in ~/.ceylon/repo.

Of course, if you are running a local Ceylon JS application, and your browser allows you to include files directly from the filesystem, you can also avoid the HTTP server and load everything for the filesystem.

Using browser APIs

Now that we have bootstrapped a Ceylon application running in a browser, it's time to do actual things that leverage browser APIs. To do this, we'll use the brand new ceylon.interop.browser which was introduced in the Ceylon SDK 1.2.1 a few days ago. Basically, it's a set of dynamic interfaces that allow wrapping native JS objects returned by the browser in nice typed Ceylon instances. For example, this interface represents the browser's Document:

shared dynamic Document satisfies Node & GlobalEventHandlers {
    shared formal String \iURL;
    shared formal String documentURI;
    ...
    shared formal HTMLCollection getElementsByTagName(String localName);
    shared formal HTMLCollection getElementsByClassName(String classNames);
    ...
}

An instance of Document can be retrieved via the toplevel object document, just like in JavaScript:

shared Document document => window.document;

Note that window is also a toplevel instance of the dynamic interface Window.

ceylon.interop.browser contains lots of interfaces related to:

Making an AJAX call, retrieving the result and adding it to a <div> is now super easy in Ceylon:

import ceylon.interop.browser.dom {
    document,
    Event
}
import ceylon.interop.browser {
    newXMLHttpRequest
}

shared void run() {
    value req = newXMLHttpRequest();

    req.onload = void (Event evt) {
        if (exists container = document.getElementById("container")) {
            value title = document.createElement("h1");
            title.textContent = "Hello from Ceylon";
            container.appendChild(title);

            value content = document.createElement("p");
            content.innerHTML = req.responseText;
            container.appendChild(content);
        }
    };

    req.open("GET", "/msg.txt");
    req.send();
}

Going further

Dynamic interfaces are really nice when it comes to using JavaScript objects in Ceylon. They are somewhat similar to TypeScript's type definitions, which means in theory, it is possible to use any JavaScript framework directly from Ceylon, provided that someone writes dynamic interfaces for its API.

The Ceylon team is currently looking for ways to load TypeScript definitions and make them available to Ceylon modules, which would greatly simplify the process of adding support for a new framework/API.

The complete source code for this article is available on GitHub.

A live example is available on the Web IDE.