The Many Ways to use shadow-cljs
This post on ClojureVerse prompted a whole discussion about boot
vs lein
again and all I can think is: Why does it matter?
Are we going to add the tools.deps
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
The .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)
(shadow.cljs.devtools.api/compile :app)
Want to rsync
your 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?
Convenience
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.
Optimization
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.
This concept is not new. grench
and drip
come to mind or any Clojure REPL.
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
AOT | Server | Cache | Time |
---|---|---|---|
0m25.730s | |||
✓ | 0m14.575s | ||
✓ | ✓ | 0m7.815s | |
- | ✓ | 0m7.706s | |
- | ✓ | ✓ | 0m0.673s |
touch
to force a recompile when using incremental compilation- Server means
shadow-cljs server
is 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 shadow-cljs
with 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 shadow-cljs
).
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.
Conclusion
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.
IMHO, YMMV.