August 4, 2015

GSoC: Week 9/10 - Support for JavaScript transforms

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.

ClojureScript 1.7.48 comes with a new feature that allows you to add a custom transformation step for JavaScript libraries. This means that you are now able to include JavaScript libraries in your project which are written in a dialect or make use of a syntax extension. In this post I will show you a small example how to use this feature to include an existing React component which uses JSX. The component we want to include creates a small SVG circle that sets the color of the circle from a property.

// Circle.js
var React = require('./react');

var Circle = React.createClass({
  render: function() {
    return(
      <svg width="200px" height="200px" className="center">
        <circle cx="100px" cy="100px" r="100px" fill={this.props.color}>
        </circle>
      </svg>
    );
  }
});

module.exports = Circle;

We will use this component to create a small ClojureScript project that will allow us you to change the color of the circle by specifying it via a text input field. We can include this component in our project by using the following compiler options:

;; build.clj
(require '[cljs.build.api :as b]
         '[clojure.java.io :as io])
(refer 'cljs.closure :only '[js-transforms])
(import 'javax.script.ScriptEngineManager)

(defmethod js-transforms :jsx [ijs opts]
  (let [engine (doto (.getEngineByName (ScriptEngineManager.) "nashorn")
                 (.eval (io/reader (io/file "jstransform-simple.bundle.js")))
                 (.put "originalCode" (:source ijs)))]
    (assoc ijs :source
      (.eval engine (str "simple.transform(originalCode, {react: true}).code")))))

(b/build "src"
  {:main 'circle-color.core
   :asset-path "js/out"
   :output-to "resources/public/js/out/circle_color.js"
   :output-dir "resources/public/js/out"
   :verbose true
   :pretty-print true
   :foreign-libs [{:file "resources/public/js/libs/react.js"
                   :provides ["React"]
                   :module-type :commonjs}
                  {:file "resources/public/js/libs/Circle.js"
                   :provides ["Circle"]
                   :module-type :commonjs
                   :preprocess :jsx}]
   :closure-warnings {:non-standard-jsdoc :off}})

Both, React and the Circle component are included as CommonJS libraries, meaning that they will be converted to Google Closure modules. However, the important thing to notice here is that we are specifying an additional :preprocess option for the Circle component and are adding a new js-transforms method for the :jsx dispatch-value. This is the part where the transformation happens. js-transforms gets and returns an object which satisfies the IJavaScript protocol and can be a plain map or a record with keys like :url, :provides, :requires and :source. We will get the JavaScript code from the :source key, transform it and then return the IJavaScript instance with the transformed code set as :source. To transform the source code, we are using facebook's JSTransform (which we've bundled) and are evaluating it using Nashorn. When we are now building our project, each foreign library will be checked for a :preprocess option and will then be passed to the js-transforms mutlimethod which uses the value of the :preprocess option as a dispatch-value. This happens before module conversion, however, both steps are independent of each other and can be used in disjunction.

Transforming the Circle component to plain JavaScript and then to a Google Closure module, allows us to use it in our ClojurScript project as follows:

;; core.cljs
(ns circle-color.core
  (:require [clojure.browser.repl :as repl]
            [React :refer [createElement createClass render]]
            [Circle :as Circle]))

(def ColorInput
  (createClass
   #js {:render
        (fn []
          (this-as this
            (createElement "div" nil
              (createElement "input" #js {:type "text"
                                          :className "center"
                                          :onChange (.. this -props -onChange)}))))}))

(def Container
  (createClass
   #js {:getInitialState (fn [] #js {:color ""})
        :handleColorChange (fn [event]
                             (this-as this
                               (.setState this #js {:color (.. event -target -value)})))
        :render (fn []
                  (this-as this
                    (createElement "div" nil
                      (createElement ColorInput #js {:onChange (. this -handleColorChange)})
                      (createElement js/Circle #js {:color (.. this -state -color)}))))}))

(render
 (createElement Container)
 (.getElementById js/document "app"))

We are creating two additional components, the ColorInput component, where we will specify the color of the circle, and a Container component which will hold the ColorInput and the Circle component and will pass the color property to the Circle component. And this is our final result:

To check out the code and try this example, have a look at the project on GitHub. This example requires a custom build of the Google Closure compiler with UMD support. This is needed to be able to successfully convert React to a Google Closure module. We already made a pull request for this, but it has not been merged into the Google Closure compiler yet. Also, it is currently not possible to load Babel into Nashorn to transform JavaScript, since it will throw a Method code too large! exception.

Tags: js transforms cljs closure react gsoc