ClojureScript

JavaScript Modules (Alpha)

This page documents how to mix modern JavaScript source files seamlessly into an existing ClojureScript project. The features documented should be considered of alpha quality and subject to change.

Motivation

When ClojureScript was initially released, compilation to JavaScript was still a novelty and source transformation beyond minification was rare. Since then, source to source compilation of JavaScript has become increasingly popular, whether embedded HTML DSLs as with React JSX, or the new ECMAScript standards that address many of JavaScript’s old weaknesses. But integrating these new kinds of source files into a ClojureScript project required deferring to JavaScript build tools which still lack the more advanced features of the Google Closure compiler like precise dead code elimination and code splitting.

Fortunately, Google Closure has not only kept abreast of many of the various enhancements to the JavaScript language, they’ve also provided transformation from the various popular JavaScript module formats (CommonJS, AMD, ES6) into the Google Closure namespace convention. ClojureScript now exposes all of this functionality and, with the help of Java 8’s Nashorn JavaScript engine, can provide even the most cutting edge JavaScript source transforms with relative ease.

In addition Google Closure now has support for the Node.js resolution algorithm. The ClojureScript compiler can now build projects that want to use dependencies from NPM.

Prerequisites

Like the Quick Start, this guide assumes you have the latest release of JDK 8, Node.js >= 6.9.4 and rlwrap installed. This guide only uses Leiningen for managing dependencies and is easily adapted to Boot or Maven.

JavaScript Modules

First let’s see how JavaScript modules can be a part of your build. We’ll focus on ES6, but the instructions are the same for CommonJS and AMD.

mkdir -p hello-es6
cd hello-es6
touch project.clj

Edit the project.clj file to look like the following:

(defproject hello-es6 "0.1.0-SNAPSHOT"
  :dependencies [[org.clojure/clojure "1.9.0-alpha14"]
                 [org.clojure/clojurescript "1.9.854"]]
  :jvm-opts ^:replace ["-Xmx1g" "-server"])

Create a watch script:

touch watch.clj

Edit it to look like the following:

(require '[cljs.build.api :as b])

(b/watch "src"
  {:output-to    "main.js"
   :output-dir   "out"
   :main         'hello-es6.core
   :target       :nodejs
   :foreign-libs [{:file "src"
                   :module-type :es6}] ;; or :commonjs / :amd
   :verbose      true})

Notice that a :foreign-libs entry may now specify a directory for :file. In this case, the ClojureScript compiler will recursively search this directory for .js files and automatically create the :foreign-libs entries for you with the supplied options. The :provides namespace for each entry will be automatically computed from the directory structure.

Let’s create the main ClojureScript namespace:

mkdir -p src/hello_es6
touch src/hello_es6/core.cljs

Edit this file to look like the following:

(ns hello-es6.core
  (:require [cljs.nodejs :as nodejs]
            [js.hello :as hello]))

(nodejs/enable-util-print!)

(defn -main [& args]
  (hello/sayHello))

(set! *main-cli-fn* -main)

Note that our JavaScript file can be imported like any other Google Closure namespace.

Let’s write the JavaScript:

mkdir -p src/js
touch src/js/hello.js

JavaScript files do not declare namespaces, so the ClojureScript compiler will compute one based on the location of the entry. Since the :foreign-libs entry specified "src", the namespace of this JavaScript file for usage from ClojureScript will be js.hello.

Edit this file to look like the following:

export var sayHello = function() {
    console.log("Hello, world!");
};

Let’s check that our watch script works:

lein trampoline run -m clojure.main watch.clj

You can verify the script works as intended by invoking Node on main.js:

node main.js
Hello world!

Using the REPL

Since JavaScript modules simply get compiled into Google Closure namespaces, all of the generic ClojureScript REPL features just work. For example, if you want automatic hot-loading of your ES6 source files just use Figwheel.

We’ll demonstrate manual hot-loading with the standard Node.js REPL.

Create a repl file:

touch repl.clj

Edit repl.clj to look like the following:

(require '[cljs.repl :as repl])
(require '[cljs.repl.node :as node])

(repl/repl* (node/repl-env)
  {:watch "src"
   :foreign-libs [{:file "src" :module-type :es6}]})

Start the REPL:

rlwrap lein trampoline run -m clojure.main repl.clj

Require the js.hello namespace and try it out:

user> (require '[js.hello :as hello])
true
user> (hello/sayHello)
Hello world!

Without quitting your REPL, edit src/js/hello.js to the following:

export var sayHello = function() {
    console.log("Hello, world!");
};
export var sayThings = function(xs) {
    for(let x of xs) {
        console.log(x);
    }
};

Reload your JavaScript module and try the new functionality:

user> (require '[js.hello :as hello] :reload)
true
user> (hello/sayThings ["ClojureScript", "+", "JavaScript", "Rocks!"])
ClojureScript
+
JavaScript
Rocks!

Since ClojureScript vectors support the ES6 iteration protocol ES6 for…​of just works.

While Google Closure can handle ES6 you may want to use other preprocessors from the JavaScript ecosystem - for example Babel’s JSX transform. In this case we will want to leverage Nashorn.

Babel Transforms

Change your project.clj to the following:

(defproject hello-es6 "0.1.0-SNAPSHOT"
  :dependencies [[org.clojure/clojure "1.9.0-alpha14"]
                 [org.clojure/clojurescript "1.9.854"]
                 [cljsjs/react "15.4.2-0"]
                 [cljsjs/react-dom "15.4.2-0"]
                 [cljsjs/react-dom-server "15.4.2-0"]
                 [cljsjs/babel-standalone "6.18.1-3"]]
  :jvm-opts ^:replace ["-Xmx1g" "-server"])

Change your watch.clj to look like the following:

(require '[cljs.build.api :as b])
(require '[cljsjs.babel-standalone])

(b/watch "src"
  {:output-to    "main.js"
   :output-dir   "out"
   :main         'hello-es6.core
   :target       :nodejs
   :foreign-libs [{:file "src"
                   :module-type :es6
                   :preprocess 'cljsjs.babel-standalone/transform}] ;; CHANGED
   :verbose      true})

Babel-standalone package from Cljsjs provides the necessary JavaScript file and a function that can be used as :preprocess handler. The function uses Nashorn JS engine to run Babel and process foreign libraries. Options to Babel can be provided by adding property :cljsjs.babel-standalone/babel-opts to the foreign library map.

Let’s add a React JSX component to src/js/hello.js:

export var sayHello = function() {
    console.log("Hello, world!");
};
export var sayThings = function(xs) {
    for(let x of xs) {
        console.log(x);
    }
};
export var reactHello = function() {
    return <div>Hello world!</div>
};

Let’s change our ClojureScript:

(ns hello-es6.core
  (:require [cljsjs.react]
            [cljsjs.react.dom]
            [cljsjs.react.dom.server]
            [cljs.nodejs :as nodejs]
            [js.hello :as hello]))

(nodejs/enable-util-print!)

(defn -main [& args]
  (hello/sayHello)
  (println (.renderToString js/ReactDOMServer (hello/reactHello))))

(set! *main-cli-fn* -main)

Run the watch script:

lein trampoline run -m clojure.main watch.clj

When the build finishes run the code

node main.js

You should see output like the following:

Hello, world!
<div data-reactroot="" data-reactid="1" data-react-checksum="1334186935">Hello world!</div>

You may have noticed that our ES6 file does not declare its dependency on React, ReactDOM, or ReactDOMServer via import. Handling this correctly depends on a pending patch to Google Closure to support Node.js module resolution for ES6 source files. When this change lands this guide will updated.

However CommonJS support for Node.js resolution works today. The following section covers this topic and will eventually apply to ES6 files as well.

Custom JavaScript transforms

In previous example the Babel transformation function was provided by a Cljsjs package. If you need to use different transformations you can write your own preprocessing function. The Babel transformation can be implemented like this, without the Cljsjs package:

Remove cljsjs/babel-standalone dependency from your project.clj.

Download babel.min.js into your project directory:

curl -O https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.18.1/babel.min.js

Create a new src/hello_es6/babel.clj file:

(ns hello-es6.babel
  (:require [clojure.java.io :as io]
            [cljs.build.api :as b])
  (:import javax.script.ScriptEngineManager))

(def engine
  (doto (.getEngineByName (ScriptEngineManager.) "nashorn")
    (.eval (io/reader (io/file "babel.min.js")))))

(defn transform-jsx [js-module opts]
  (let [code (str (gensym))]
    (.put engine code (:source js-module))
    (assoc js-module :source
      (.eval engine (str "Babel.transform("code", {presets: ['react', 'es2016']}).code")))))

Change your watch.clj to look like the following:

(require '[clojure.java.io :as io])
(require '[cljs.build.api :as b])

(b/watch "src"
  {:output-to    "main.js"
   :output-dir   "out"
   :main         'hello-es6.core
   :target       :nodejs
   :foreign-libs [{:file "src"
                   :module-type :es6
                   :preprocess 'hello-es6.babel/transform-jsx}] ;; CHANGED
   :verbose      true})

Node Modules

ClojureScript now has support for building dependencies from NPM. Like everything else in this guide this support should be considered experimental and subject to change. Even when consuming dependencies from NPM all the usual caveats around Google Closure Compiler apply. You may in some cases, as we will see, need to supply externs for library internals in order to compile successfully.

We will see how we can successfully compile React and ReactDOM server NPM module packages.

First lets create a new project:

mkdir hello-cjs
cd hello-cjs
touch project.clj
touch package.json

Edit project.clj to look like the following:

(defproject hello-cjs "0.1.0-SNAPSHOT"
  :dependencies [[org.clojure/clojure "1.9.0-alpha14"]
                 [org.clojure/clojurescript "1.9.854"]
                 [cljsjs/react "15.4.2-2"]
                 [cljsjs/react-dom "15.4.2-2"]
                 [cljsjs/react-dom-server "15.4.2-2"]]
  :jvm-opts ^:replace ["-Xmx1g" "-server"])

Notice that we’ve declared a bunch of CLJSJS dependencies. We’re not going to actually use them, we’re only pulling them in to get the externs.

Edit package.json to look like the following:

{
  "devDependencies": {
    "module-deps": "4.0.8",
    "resolve": "1.3.3",
    "browser-resolve": "1.11.2"
  },
  "dependencies": {
    "react": "15.4.2",
    "react-dom": "15.4.2"
  }
}

The ClojureScript compiler needs the module-deps, resolve and browser-resolve development time packages to build the project.

Install the deps:

npm install

Let’s write a simple program. First the ClojureScript:

mkdir -p src/hello_cjs
touch src/hello_cjs/core.cljs

Edit src/hello_cjs/core.cljs to look like the following:

(ns hello-cjs.core
  (:require [libs.npm-stuff :as npm-stuff]))

(enable-console-print!)

(println (npm-stuff/renderSomething))

Now let’s make a CommonJS file that will load deps from node_modules:

mkdir src/libs
touch src/libs/npm_stuff.js

Edit src/libs/npm_stuff.js to look like the following:

var React = require("react");
var ReactDOMServer = require("react-dom/server");

function renderSomething() {
    return ReactDOMServer.renderToString(React.createElement("div", {}, "Hello world!"));
};

module.exports = {
    renderSomething: renderSomething
};

Now let’s make our build file.

touch build.clj

Edit build.clj to look like the following:

(require '[clojure.java.io :as io]
         '[cljs.build.api :as b])

(b/build (b/inputs "src")
  {:main       'hello-cjs.core
   :target     :nodejs
   :output-to  "main.js"
   :output-dir "out"
   :verbose    true
   :externs    ["process.js"]
   :foreign-libs (let [entry {:file (.getAbsolutePath (io/file "src/libs/npm_stuff.js"))
                              :provides ["libs.npm-stuff"]
                              :module-type :commonjs}]
                   (into [entry] (b/node-inputs [entry])))
   :optimizations :advanced
   :closure-warnings {:non-standard-jsdoc :off
                      :global-this :off}})

(System/exit 0)

Notice the new helper, cljs.build.api/node-inputs, which takes a sequence of JavaScript entry points. The ClojureScript compiler will now invoke a Node.js script to figure out all the node_module dependencies needed by these entry points and return a vector of CommonJS foreign lib entries.

Also notice the presence of a extern file. To get process.js:

curl -O https://raw.githubusercontent.com/dcodeIO/node.js-closure-compiler-externs/master/process.js

We need these additional externs because internally React refers to the process module. :pseudo-names true is a good way to figure out cases like this. In the future hopefully these externs will be covered by CLJSJS.

We’re now ready to build our project:

lein trampoline run -m clojure.main build.clj

You might see a couple of warnings about the CommonJS files that can be ignored. Once the compilation process is complete run main.js:

node main.js

You should see some server rendered HTML.

Original author: David Nolen