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.