shadow-cljs and Emacs/Cider integration
Intro
In this post I will cover shadow-cljs, a ClojureScript compiler with focus on simplicity and ease of use. Grossly simplifying, you can think of it as a cljsbuild and Figwheel replacement.
The main selling point for me was the npm integration, which works out-of-the-box, but there are also other strong advantages. You can find other selling points in the introduction to the official UserGuide.
NOTE
We will assume you are using a GNU/Emacs or a similar editor flavour with Cider installed. If you have not yet you can quickly install it with:
M+x package-install
cider
We will also assume yarn is used as npm dependency management tool, but npm
would work just as fine.
Integrating shadow-cljs and emacs-cider
There are multiple workflows when using shadow-cljs. You could use the command-line tools only, however to get the full interactive development experience use an editor and integrated REPL. This is where cider enters. Below we will specify the config files needed to get the Emacs-cider and shadow-cljs working together.
shadow-cljs.edn
Start by creating a shadow-cljs.edn
, a config file which specifies build targets. In this example we will have two builds there, an :app
build and :ci
(a test build):
{:lein true
:builds {:app {:target :browser
:output-dir "public/js"
:asset-path "/js"
:modules {:main {:entries [app.core]}}
:devtools {:http-root "public"
:http-port 4040
:before-load app.core/stop
:after-load app.core/start}}
:ci {:target :karma
:output-to "out/ci.js"
:ns-regexp "-test$"}}}
:lein true
means the dependencies and sources will be managed by leiningen.
The :app
build is targeting the browser (other option is :node-script
if we’re targeting node):
- Compiled JS files will go to the public/js directory
- The entry point is in the
app.core
namespace. - Everything that is in the public directory is served on local port 4040.
- There are two (optional) functions to be run when code is hot-reloaded.
The :ci
build is for the karma test runner:
- compiled JS files will be in out/ directory.
- runners will be created for all source files in the classpath which end with -test (another option is to specify the test runner namespace see here.)
project.clj
This config file is well known to every CLojure developer.
We need to add all the dependencies and specify all the source-paths for the :app
and :ci
builds created in shadow-cljs.edn:
(defproject shadow-cljs-demo "1.0.0-SNAPSHOT"
:description "demo app"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[camel-snake-kebab "0.4.0"]]
:exclusions [[org.clojure/clojurescript]
[org.clojure/clojure]]
:source-paths ["src" "test"]
:clean-targets ^{:protect false} ["target"]
:profiles {:dev {:source-paths ["dev"]
:dependencies [[thheller/shadow-cljs "2.2.8"]
[org.clojure/clojure "1.9.0"]
[org.clojure/clojurescript "1.10.145"]]
:plugins [[cider/cider-nrepl "0.16.0"]]
:repl-options {:init-ns ^:skip-aot user
:nrepl-middleware [shadow.cljs.devtools.server.nrepl/cljs-load-file
shadow.cljs.devtools.server.nrepl/cljs-eval
shadow.cljs.devtools.server.nrepl/cljs-select]}}})
package.json
Since in this project we will include packages from npm repository, those dependencies need to be in the package.json
config file.
If you run yarn init
you will get an interactive prompt to help you specify the config.
NOTE
I have not tried it, but it’s quite possible that the lein-npm Leiningen plugin can be used to specify project’s npm dependencies, eliminating the need for maintaining a package.json config file. Which might be nice.
This is what we need in the end:
{
"name": "shadow-cljs-demo",
"version": "1.0.0-SNAPSHOT",
"description": "",
"author": "me",
"license": "MIT",
"main": "main.js",
"scripts": {
"compile": "shadow-cljs compile dev",
"release": "shadow-cljs release dev",
"delete": "rm -r public/js/* out/*",
},
"devDependencies": {
"karma": "^2.0.0",
"karma-chrome-launcher": "^2.2.0",
"karma-cljs-test": "^0.1.0",
"shadow-cljs": "^2.1.21"
},
"dependencies": {
"left-pad": "^1.2.0"
}
}
- The
shadow-cljs
devDependency is needed for running the scripts commands. karma
is needed for tests.- No project is complete withoud a
left-pad
as a dependency.
The karma config
This is a typical config file for the karma test runner:
module.exports = function (config) {
config.set({
browsers: ['Chrome'],
// The directory where the output file lives
basePath: 'out',
// The file itself
files: ['ci.js'],
frameworks: ['cljs-test'],
plugins: ['karma-cljs-test', 'karma-chrome-launcher'],
colors: true,
logLevel: config.LOG_INFO,
client: {
args: ["shadow.test.karma.init"],
singleRun: true
}
})
};
- We will run the test in the Chrome browser.
- Compiled JS files (ci.js) for the runner are in the out directory.
The user namespace
We will use this namespace as an utility to start the shadow-cljs server, get the watcher for hot-reloading and finally give us the cljs REPL:
(ns user
(:require [shadow.cljs.devtools.api]
[shadow.cljs.devtools.server]))
(defn watch-app! []
(shadow.cljs.devtools.server/start!)
(shadow.cljs.devtools.api/watch :app)
(shadow.cljs.devtools.api/nrepl-select :app))
Give me the REPL!
With all that in place we can have a REPL running in a four simple steps:
- Install npm dependencies:
yarn
- Start the clj REPL:
M+x cider-jack-in
- Start the server, build watcher and get the cljs REPL:
(watch-app!)
- Point your browser to
localhost:4040
Similarly you can run watch and run the tests as you change the code by:
- Starting the server and the watcher:
yarn shadow-cljs watch ci
- Starting the karma runner:
yarn karma start
Show me the code!
A complete application is availiable as a GitHub repository. It shows a couple of things not covered in this post, i.e.
- How to include macros in a namespace compiled with shadow-cljs.
- How to include npm dependencies.
Thank you for reading!