shadow-cljs and Emacs/Cider integration


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.


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

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.


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.)


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 ""
  :license {:name "Eclipse Public License"
            :url ""}
  :dependencies [[camel-snake-kebab "0.4.0"]]
  :exclusions [[org.clojure/clojurescript]
  :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


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.


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) {
    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]

(defn watch-app! []
  (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:

  1. Install npm dependencies:
  1. Start the clj REPL:
M+x cider-jack-in
  1. Start the server, build watcher and get the cljs REPL:
  1. Point your browser to localhost:4040

Similarly you can run watch and run the tests as you change the code by:

  1. Starting the server and the watcher:
yarn shadow-cljs watch ci
  1. 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!

Written on March 14, 2018