June 2, 2015

GSoC: Week 1 - Converting JavaScript modules to Google Closure modules

I have been accepted to this years Google's Summer of Code to work on ClojureScript. The goal of my project is to improve the integration of ClojureScript with the existing JavaScript ecosystem. I will post weekly updates about the progress of the project here. For more details about the project have a look at the ClojureScript GitHub wiki page.

Last week was the first week of my Google's Summer of Code project. The first part of the project that I will be working on is adding JavaScript module support to ClojureScript, meaning that CommonJS, AMD and ECMAScript 6 modules can be included into a ClojureScript project. For this, we will be using functionality that is already included in the Google Closure compiler. The Google Closure compiler has the classes ProcessCommonJSModules and ProcessEs6Modules, which each convert either a CommonJS or ECMAScript 6 module into a Google Closure module. To be able to use those two classes directly from within the ClojureScript compiler, we needed to make the classes and its constructors public. Luckily, our pull request to the Google Closure compiler project got accepted and our changes should be included in the next release of the project. With those changes in place, I started off by working on including a CommonJS module into a ClojureScript project. Following, I will try to describe which functionality we will need to add to the ClojurScript compiler so that users can include a CommonJS module without modifying it.

Assume we have the following simple CommonJS module in libs/greeting.js:

exports.hello = function(name) {
    return "Hello, " + name;
};

We can include this module as a foreign library. To specify the module type, we will be adding a new compiler option called module-type, which can have the values :commonjs, :es6 and :amd.

(require 'cljs.build.api)

(cljs.build.api/build "src"
                      {:main 'hello-world.core
                       :output-to "out/main.js"
                       :foreign-libs [{:file "libs/greeting.js"
                                       :provides ["greeting"]
                                       :module-type :commonjs}]})

To be able to use the functionality provided by the module, we need to require it into our namespace with the same name that we used in the provides compiler option for the module.

(ns hello-world.core
  (:require [greeting]))

(enable-console-print!)

(println (.hello js/greeting "World!"))

If the ClojureScript compiler would try to handle the module the same way as any other foreign library, we would get the following errors:

error 1

We are getting the first error, because the Google Closure dependency management system doesn't implement the CommonJS specification. To get rid of the first error, we need to convert the CommonJS module into a form that is understood by the Google Closure dependency management system and can be used in combination with other Google Closure modules. For this task, we can use the ProcessCommonJSModules class. Instead of wrapping everything in a function it adds a filename based prefix to the global variables and adds a goog.provide statement.

goog.provide("module$libs$greeting");
var module$libs$greeting = {};
module$libs$greeting.hello = function(name) {
  return "Hello, " + name;
};

Converting the CommonJS module helps us to get rid of the exports error, but we are still left with the greeting is not defined error. If you have a look at the JavaScript that the compiler emitted, you can see that we are requiring the module as greeting and later use the greeting object to call the hello function on.

// Compiled by ClojureScript 0.0-0000 {}
goog.provide('hello_world.core');
goog.require('cljs.core');
goog.require('greeting');
cljs.core.enable_console_print_BANG_.call(null);
cljs.core.println.call(null,greeting.hello("World!"));

//# sourceMappingURL=core.js.map

Looking at the rewritten module code we can see that we should be using module$libs$greeting to refer to the module instead. This means, in the ClojureScript compiler whenever we see a reference to the module using the "old" name (greeting) while emitting the JavaScript we need to replace it with the "new" name (module$libs$greeting).

// Compiled by ClojureScript 0.0-0000 {}
goog.provide('hello_world.core');
goog.require('cljs.core');
goog.require('module$libs$greeting');
cljs.core.enable_console_print_BANG_.call(null);
cljs.core.println.call(null,module$libs$greeting.hello("World!"));

//# sourceMappingURL=core.js.map

Finally, we get our Hello, World!.

Hello, World!

To see the current progress, have a look at the GitHub branch.

Tags: cljs js modules gsoc