shadow-cljs - Introduction
shadow-cljs
is my attempt at improving ClojureScript tooling. I started it a couple years ago (then called shadow-build
) since I wanted to do things other tools were not able to do (yet, maybe still can’t).
It does use the default ClojureScript compiler and analyzer but not much beyond that. Everything related to “bundling” is rewritten completely. shadow-cljs
works quite differently compared to other CLJS tools when it comes to deciding which code gets compiled, optimized and then bundled together. The focus has always been on ensuring that the final “release/production” version that gets shipped to your user is as optimized as possible. Code-splitting (aka. :modules
) was the initial motivation to start this tool 3+ years ago but it has grown far beyond that.
Development is important as well so the usual live-reload workflow we all love works out of the box. The CLJS REPL is usually injected automatically so there is no need to configure it most of the time.
Configuration
A couple months ago I started working on a complete rework of the configuration code since I was unhappy with my own build configs. They had a lot of repetition and often were exact copies of each other with one parameter changed (eg. :none
-> :advanced
).
Frequently I wasn’t able to express exactly what I wanted in my configuration which resulted in trying to hack something together in code to emulate the final result. Everyone that ever tried to work with module.exports
for node
code might be able to relate.
So from that the actual shadow-cljs
project was born which threw away pretty much all of the configuration code and started from scratch. If you have used other CLJS tools before you might need to forget some stuff you have learned since they might not apply anymore.
:target and :mode
The basic configuration concept starts with :target
which defines how your code is bundle together based on the platform you plan on running the code in (eg. the Browser or node
). CLJS has a :target
option as well but was not specific enough for my goals.
:mode
is either :dev
during development or :release
for production builds. This means that you have ONE configuration that is used for both modes, if the configuration has development-only parts they will not apply when building a release and vice versa. There is no need to copy the configuration, the tool is smart enough to do the correct thing most of the time. Most of the time you’d probably just change the :optimization
setting anyways, at least I did.
A whole lot of configuration is based on some reasonable defaults so the configuration you actually need to do is reduced to a minimum. You can still change the defaults if you need to though.
:browser - building for the the Browser
The :browser
target is the most complex and deserves its own book probably. Shipping code for the browser has its own set of challenges since you usually want to optimize for size and speed. Depending on the amount of code you want to ship it might be important to split code via :modules
to reduce (or delay) the amount of code the user has to download. Caching can also improve performance so the :browser
target has some hooks to help with that. Using JS dependencies is also especially complicated because you can’t just load them via require
at runtime. Everything must be carefully bundled together and the “best” option is usually very specific to each individual deployment scenario.
:npm-module - building for JS tools or node.js
The goal here is to make it easy to consume ClojureScript from JS. Directly in node.js
or by other tools such as webpack
and higher level “frameworks” like create-react-app
, create-react-native-app
, etc.
Since ClojureScript uses Google Closure to do most of the :advanced
stuff the code generated by the compiler targets the Closure coding conventions. These are very strict and quite different from what node.js
or the other tools expect and understand.
:npm-module
enhances the code so non-closure tools can understand and consume it directly without sacrificing what Closure gives us. The result is that you can use require
directly to consume the compiled (and even optimized) CLJS code.
A CLJS namespace like this
(ns demo.foo)
(defn hello [who]
(str "Hello, " who "!"))
Can be directly consumed in node
via:
> var x = require("shadow-cljs/demo.foo");
undefined
> x.hello("JS")
'Hello, JS!'
You can use :npm-module
to build your own npm
packages as well.
:node-script and :node-library
These are slightly more focused versions of :npm-module
and basically just optimize the last few bits. :node-script
creates a .js
file that you can run directly via node
. :node-library
allows you to compact multiple namespaces down to one map of exports.
Further reading
This post only introduced a few concepts of shadow-cljs
with a bit of background of why it exists and why it is different. Please refer to the README on how to use it.
I will go more in-depth about certain aspects of the tool in future posts, for now I just wanted a post I can refer to which answers the “Why/What”.