mkdir -p hello-es6 cd hello-es6 touch project.clj
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.
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.
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.
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!
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.
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.
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