When I mentioned this in last week’s “what are you working on” thread I knew it would raise some eyebrows, and since someone asked, I figured I’d elaborate a bit on why we chose to go this way and how the experience has been.
Please no “you should have just” replies. This is presented as a data point, this is how we experienced it. Maybe if we had more boot experts on the team it could have gone differently. Maybe not.
The project is NextJournal. When I started helping them out some six months ago, they had been using boot for a while, and it seemed to be causing them a lot of pain. It’s hard to point at a single thing that caused this, it was a combination of factors, but it all led to team feeling like they weren’t in control. Debugging and trying to get the setup right was tedious and frustrating.
This is a project that includes three different ClojureScript builds: the web frontend, a web worker that evaluates JS and CLJS, and a renderer that runs on Node to generate the initial payload offline. For each of these we need dev, test, and production builds. Then there’s a Clojure based server component.
Further complications arise because of a large number of foreign js dependencies. Cljsjs works well for the browser, but many of the cljsjs packages are not suitable for use on Node, so we had to resort to some exotic hacks to prevent ClojureScript from using the cljsjs packages (even when a dependency asked for it, e.g. reagent -> cljsjs.react), and instead load them from node_modules directly.
I’d say the biggest trouble we ran into came from Boot’s “fileset” approach. Boot tries to turn the filesystem “immutable” by creating temporary directories, and having each build step write to a new tempdir. This is to prevent stale artifacts from messing up your build, but it also means your build can’t reuse any state from a previous build. What this meant in practice was that any change took close to a minute to recompile and show up in the browser. This was simply unacceptable.
There’s boot-figreload, but it’s not figwheel, it only borrows small parts of it, it does not in any way give you that smooth unified experience. We decided we wanted figwheel, and tried to use figwheel.sidecar from inside the app, but boot puts tmpdirs on the classpath Pedestal was serving up assets from there, whereas they should have been coming from the project directory.
Another huge source of frustration was just getting the ClojureScript builds right. I mentioned before the funky foreign-libs setup. The thing is that tools like lein-cljsbuild or figwheel pass there options more or less unchanged on to the ClojureScript compiler, whereas boot-cljs does a lot of “magic” under the covers that ended up getting in our way.
Boot itself was also a source of frustration. Tasks are supposed to compose, until they don’t. e.g. you can’t have a REPL and a watcher task in the same process. I spent a lot of time in the boot and boot-cljs codebases trying to understand what was going on, it did not seem like there was enough documentation for this. In particular I had the file with boot’s built-in taks almost constantly open.
We now just finished the migration to Leiningen, so it’s a bit hard to give any longer term perspective yet, but it feels like we’re in control again, and the development experience has improved a lot, with subsecond recompile times in Figwheel.
Leiningen isn’t as scriptable, but it’s more malleable than people think. We ended up pushing much more stuff into the system itself, or the other way into shell scripts, which also made everything behave more predictably. This is the approach I would generally recommend to people: don’t try to do everything with Leininge, do more inside your app instead.