Dec 20, 2017 • Arnout Roemers
With the release of Clojure 1.9, Cognitect has added command line tools for Clojure. Using those tools it becomes quite easy to run a Clojure script/application or start a REPL. Next to this, it can also be used to fetch project dependencies, form a classpath with those dependencies and start a script using this classpath.
Because of the latter feature, the new CLI tools seem suitable for building ClojureScript projects, instead of using leiningen or boot. All you need is your own build script, started using the CLI tools. For a recently started ClojureScript on NodeJS project, we set out to find a workable setup for building and developing it using such a build script. A workable setup for us would at least include being able to compile and test the project, having a Figwheel REPL available and being able to connect to it using Emacs CIDER.
This blog post shows our minimal but workable setup.
To declare the dependencies of the project, a deps.edn
file is required by the CLI tools.
A minimal file would have the following contents:
The :deps
map declares the projects dependencies required to build it.
Next to this, there is also the :repl
alias, which declares the dependencies required to start a CIDER-compatible Figwheel ClojureScript REPL.
If your project does not need CIDER compatibility, you could leave out the cider/cider-nrepl
, org.clojure/tools.nrepl
and refactor-nrepl
dependencies.
When you use the clojure
(or clj
) CLI tool, it will use the dependencies in the :deps
map.
If you want the REPL dependencies mixed in as well, you need to supply the :repl
alias using the -R
option, like so:
Next is the build script.
The goal is to have the script perform a certain task, such as compile
, test
or figwheel
, and thus use the CLI tools like this:
For this to work, the following “framework” is created in the build.clj
script:
Now we can define a task
method for each of the tasks we want, dispatched on the the first
argument to the build script.
Let’s add the first task for compiling the project. We need to require the ClojureScript compiler API namespace and use it to call the ClojureScript compiler:
Easy and effective.
Executing clojure build.clj compile
will compile the project for us.
As our project is a NodeJS project, we would now be able to run it by executing node target/my-app/main.js
.
Next, testing our project.
For testing a ClojureScript project, one generally uses a different entrypoint into the application - the test runner.
The same is true for our NodeJS project.
We simply execute the NodeJS project from within our build script using the clojure.java.shell
namespace:
Next to a one-off “test” task, it is very convenient to have the project tested after every change in the source code.
This workflow can be had by extending the “test” task a bit, using the :watch-fn
option of the ClojureScript compiler.
On every change, the tests will be re-run, without a new JVM spinning up.
The “test” task can be extended as follows:
Now you can execute clojure build.clj test watch
to start this continuously testing workflow.
The one-off test can still be executed by leaving out the watch
argument, or using the once
argument.
Now let’s setup a Figwheel REPL task. Luckily for us, the Figwheel project is nicely split up between leiningen-specific stuff and the actual compilation, code reloading and REPL mechanics. The latter is what we need, and is available in the sidecar library. This library compiles the ClojureScript project with Figwheel client related code inserted and starts the Figwheel server. The Figwheel server re-compiles the project on every source code change and signals the client to reload some files after compilation is complete.
The server can start a ClojureScript REPL for you, hooking into the running application. It can also start an nREPL server for you, such that you can connect to it using CIDER, where you can start the ClojureScript REPL yourself. In our setup, we want to be able to choose either one of these “modes”, by optionally supplying an nREPL server port. The “figwheel” task definition then looks as follows:
If we launch our Figwheel setup as follows, we get a simple REPL in our terminal:
And if we launch as follows, we get an nREPL server, which we can connect to in Emacs by using the cider-connect
command.
In this CIDER repl, you can call (cljs-repl)
yourself, to open the ClojureScript REPL.
Very nice.
But, if you try to run our former tasks, such as “compile” or “test”, you will see that those won’t work if you don’t add the -R:repl
option.
It will complain about not being able to load the figwheel-sidecar.repl-api
namespace.
We don’t want to add the repl
alias for all our tasks, for various reasons.
Therefore we have to make our task definitions check whether the required namespaces are available.
For this we introduce a small macro:
Using this macro, we can update our “figwheel” task a bit:
We can now remove the global require of figwheel-sidecar.repl-api
namespace.
If we then try to execute the “figwheel” task without the :repl
alias, it will show a message that the required namespaces are missing.
Other tasks now work again as expected.
Building, testing and developing a ClojureScript application using the new Clojure tools is certainly possible. If anything, it was a nice excercise in how to work with the new tools. Creating your own build script makes it very explicit as to how the project is build and tested, instead of “just add these plugins, and it will probably work”. In other words, in may create insight in how it all works and fits together.
That said, the custom build script implements concepts like tasks, which tools like leiningen and boot have implemented and standardized for you.
Simply being able to execute cider-jack-in-clojurescript
from within Emacs is easier and works because of those standardized build tools.
This is fine, as the new Clojure CLI tools and tools like leiningen and boot can complement each other.
Because of this, we will probably see more support for the Clojure tools, and the underlying tools.deps
library, in those build tools.
Initial examples of such integration are the boot-tools-deps task and lein-tools-deps plugin.
A full build script can be found on github.
As always, have fun!