A few words from Agical

A rich poor man's CSS hot reload

Hot reloading CSS can be as easy as (require .. :reload-all) followed by (spit css-file css), at least if you add a file watcher. 😀

I’m using shadow-cljs for almost everything I do. Its hot-reloading of code is so damn good that I have grown addicted to it and I now need moar hot reloading everywhere in my life. shadow-cljs also hot-reloads css files, did you know?

However, unless you hand-code the actual .css files, what remains is something that will rebuild them when their source code changes. A poor, poor man uses SASS or some similar huge dependency for this. But a rich, poor man uses data instead of text. As a Clojurian rich man (but I am repeating myself) I use the Garden CSS library. Garden gives me a way to express CSS in Hiccup-ish data. It also comes with a bunch of nice CSS utilities such as unit conversion and color manipulation, making me miss SASS even less.

Since Garden is a library that uses Clojure data and produces CSS text from it, I can choose to let it generate CSS files, or to inject the CSS in [:style ...] tags in the hiccup making up my views. Or both. I most often chose both. When injecting into style tags, I get shadow-cljs hot reloading for it. But, in most of the projects I am involved in, generating CSS files from Clojure data needs to happen on the JVM/Clojure side of things, and shadow-cljs takes care of ClojureScript reloading, not Clojure.

While a project is young I can put all my styles in the same file and rely on shadow-cljs-hooks/garden. For reasons that are unclear to me the hook does not work when the styles are split up on several files, and the project is a bit abandoned. When splitting the namespace up, I have so far been using a tedious and involved setup of Calva custom REPL commands and my not-so-reliable memory to reload my style namespaces Until I decided I had had enough of it. In theory, garden-watcher should solve it, but I couldn’t figure out how to hold it correctly.

What to do? Here, my rich, poor man’s solution:

The ingredients

The config

In the shadow-cljs.edn build for my app:

:build-hooks [(something.some-ns.style.garden-watcher/hook {:output-to "public/css/main.css"})]

The code

(ns anteo.designer.style.garden-watcher
  (:require
   [anteo.designer.style.style :as style]
   [garden.core :as garden]
   [nextjournal.beholder :as beholder]))

(defonce !watch-ref (atom nil))

(defn- rewrite-style-sheet! [css-path {:keys [path]}]
  (println "Reloading reason:" (str path))
  (require '[anteo.designer.style.style :as style] :reload-all)
  (println "Writing new css to:" css-path)
  (spit css-path (garden/css (style/styles))))

(defn ^:export hook
  {:shadow.build/stage :configure}
  [{:shadow.build/keys [mode] :as build-state} {:keys [output-to]}]
  (let [path "src/anteo/designer/style"]
    (when-not @!watch-ref
      (rewrite-style-sheet! output-to {:path path})
      (when (and (not (System/getenv "CI"))
                 (= :dev mode))
        (println "Installing watcher for path:" path)
        (reset! !watch-ref (beholder/watch (partial rewrite-style-sheet! output-to) path)))))
  build-state)

Basically, this makes shadow-cljs call garden-watcher/hook once, when it starts watching (or just compiling). We then write the initial CSS files from the Garden data. We also start a file watcher which will call our function for rewriting the CSS whenever a file in our style subdirectory changes. In the function for rewriting the .css file we reload the namespaces in the style.style namespace using a simple (require ... :reload-all). This is where I spent most of the time implementing this. I tried quite a few things that didn’t work before realizing how easy and simple (yes, both) it actually is.

This setup forces me to to have my main style namespace in src/something/some_ns/style/style.cljc, while I would rather have it in src/something/some_ns/style.cljc, but that is pure preference, and a small sacrifice, considering the gains. Because now I edit my Garden data, save and see the styles applied in the app I am working with. And if I need something more I know exactly where to add it. Yay!

I guess I could pack this up as a library, but do I really need to? It could be better that people facing the problem find this post, and if they like it, they cook something poor of their own from it, making them richer. Tailor fit, fits the best!


Note: You should also have a look at how the author of shadow-cljs, Thomas Heller, suggests this should be solved: Supercharging the REPL Workflow