ClojureScript

JavaScript Modules (Alpha)

This guide requires ClojureScript 1.10.238 or later and assumes familiarity with the Quick Start.

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.

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

Create a build.edn file with the following contents:

{:output-to    "main.js"
 :output-dir   "out"
 :main         hello-es6.core
 :target       :nodejs
 :foreign-libs [{:file "src"
                 :module-type :es6}]
 :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:

cljs -m cljs.main -co build.edn -w -c

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.

Start the REPL:

clj -M -m cljs.main -co build.edn -r

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 deps.edn file to the following:

{:deps {org.clojure/clojurescript {:mvn/version "1.9.854"}
        cljsjs/react {:mvn/version "15.4.2-0"}
        cljsjs/react-dom {:mvn/version "15.4.2-0"}
        cljsjs/react-dom-server {:mvn/version "15.4.2-0"}
        cljsjs/babel-standalone {:mvn/version "6.18.1-3"}}}

Change your build.edn to look like the following:

{: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 build.edn to look like the following and rebuild:

{: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})

Original author: David Nolen