This post on ClojureVerse prompted a whole discussion about
lein again and all I can think is: Why does it matter?
Are we going to add the
clojure tool to this discussion next?
Why not just use Clojure? You can get very far with just doing that and as a bonus you can do everything from the REPL as well. I’m going to use
shadow-cljs as an example here but I think it applies to a whole lot of other “tools” as well.
Build it as a Library first
First of foremost
shadow-cljs is built as a normal Clojure Library. You can add the
thheller/shadow-cljs artifact to any tool that is able to construct a Java Classpath for you and you can start using it.
lein run -m shadow.cljs.devtools.cli compile app boot run -m shadow.cljs.devtools.cli compile app # does this exist yet? clojure -m shadow.cljs.devtools.cli compile app mvn exec:java ... # not exactly sure how this works but it works java -cp ... clojure.main -m shadow.cljs.devtools.cli compile app shadow-cljs compile app
.cli namespace is just a small wrapper to process the strings we get from the command line and turn them into proper Clojure data. In the REPL you can just call the
.api namespace directly (properly
require‘d of course)
release version to a remote server?
(ns app.build (:require [shadow.cljs.devtools.api :as shadow] [some.lib :refer (rsync)])) (defn release  (shadow/release :app) (rsync "some-dir/*" "user@some-server:/some-dir"))
lein run -m app.build/release or
shadow-cljs clj-run app.build/release or
(app.build/release). You get the idea.
Why have a command line tool then?
You have to type less.
shadow-cljs compile app vs
lein run -m shadow.cljs.devtools.cli compile app.
It can also check a lot of things without an actual JVM and can provide faster feedback in those cases.
Starting a JVM+Clojure+Deps takes a while. This can be improved if the Clojure code is AOT compiled but it still won’t be very fast. Fortunately we don’t need to start a new JVM for everything, we can just re-use one we already started.
This is exactly what the
shadow-cljs tool does. It will AOT compile the relevant code on the first startup so subsequent startups are faster. The
shadow-cljs server starts the JVM in
server mode. Every other command will then use that JVM instead of starting a new one.
Here are some numbers to compare the effect this optimization has.
The command used is:
touch src/starter/browser.cljs && time shadow-cljs compile app
touchto force a recompile when using incremental compilation
- Server means
shadow-cljs serveris running, no new JVM is started
As you can see the difference is quite dramatic. Given that I get easily distracted when waiting for things this has a huge impact on my focus during the day.
The non-Server code could be optimized a bit since it always loads all development related code.
compile for example doesn’t need all the REPL/live-reload related code but given the presence of
server this never seemed necessary.
You’ll most likey use
watch during actual development and all tools have an optimized experience for this but it still matters for other commands.
But what about boot?
boot tries to do a lot more than just providing a classpath. The problem with this is that it only works if you want to use the exact abstractions
boot provides. As soon as you want to do something slightly different it just starts getting in the way. I don’t recommend using
boot since it breaks all the caching
shadow-cljs tries to do.
boot-cljs has the same problem as far as I can tell. Restarting the
boot process wipes all cache. You could make an argument here that this is a good thing since it prevents stale cache but that just treats the symptom instead of fixing the root cause (which I did in
If you separated the classpath/pod management from
boot it would make for a very good library I think. I do like some ideas in
boot but its too complected for me.
Write everything as a Clojure Library so it works in any tool and the REPL.
I simplified a great deal here. Things are a lot more complicated in the real world but I am convinced that we can get way better results if less code was written specific to one build tool and instead used Clojure as the common ground.