An easier way to try Cljs libraries with shadow-cljs ob-clojure and cider
In my previous blog, I showed how to inject libraries in a Cider session started via ob-clojure. So I thought to provide the same for ClojureScript (Cljs). With Cljs we need to do something similar, but with the additional step of setting up a REPL for Cider to work with.
When working with Cljs you may be targeting different platforms (e.g., Node.js, the browser, or even mobile). I had a great experience using shadow-cljs for compiling Cljs to one of these, so I picked that tool for starting the volatile REPL I need. I say volatile because in my note-taking/min-exploration session I am aiming to avoiding the distraction of setting up a Cljs project from scratch.
The use case looks like this in an Org Mode file:
#+begin_src clojurescript :deps '(("io.github.ruffnext/cljs-http" "0.1.47")) :shadowcljs-type "browser-repl"
(ns example.core
(:require-macros [cljs.core.async.macros :refer [go]])
(:require [cljs-http.client :as http]
[cljs.core.async :refer [<!]]))
(def result (atom {}))
(go (let [response (<! (http/get "https://api.github.com/users"
{:with-credentials? false
:query-params {"since" 135}}))]
(swap! result #(conj %
[:status (:status response)]
[:login (map :login (:body response))]))))
@result
#+end_src
#+RESULTS:
| #'example.core/result |
| #object[cljs.core.async.impl.channels.ManyToManyChannel] |
| {:status 200, :login ("simonjefford" "josh" ...)} |
In the above block you may see that I successfully run some example code for the cljs-http library via an Org Mode source block. (Hurrah!)
May be also useful to see this in action:
If you are an user of Org Babel, you may also recognize that the
:deps and shadowcljs-type headers don't come by default. My hack
uses these to setup the shadow-cljs REPL correctly: in the example, I
target the browser and I declare the dependency I need.
The code for this to work consists in an Emacs advice which runs
around ob-eval:clojure, which is the function Org Mode calls when
you try to run a Cljs code block.
The aim is to start a shadow-cljs REPL and then let ob-clojure start
Cider. I put an if statement to avoid starting the REPL multiple
times: that seemed to work in my case, but is not battle-tested.
All in all this is the hack:
(defun my/cider-run-projectless-cljs-repl (&optional repl-type deps)
"Start a projectless cljs REPL running on REPL-TYPE with DEPS.
DEPS needs to be something like '((\"foo/bar\" \"0.0.1\") (\"foo/baz\" \"0.0.2\"))"
(when (and
;; shadow-cljs is available
(executable-find "shadow-cljs")
;; check if cider has a shadow session running for this dir
(not (--any
(and (s-contains-p "cider-repl" (buffer-name it))
(s-contains-p "cljs" (buffer-name it))
(s-contains-p (file-name-base (directory-file-name
default-directory))
(buffer-name it)))
(buffer-list)))
;; not in a cljs project
(not (--any (or (s-ends-with-p ".edn" it)
(s-ends-with-p ".clj" it))
(directory-files
(or
(ignore-errors (project-root (project-current)))
default-directory)))))
(let* ((repl-type (or
repl-type
(completing-read
"Shadow-cljs REPL type:"
'("browser-repl" "node-repl")
nil
t)))
(deps-as-vectors
(concat "["
(s-join
"\n"
(--map (concat "[" (nth 0 it) " \"" (nth 1 it) "\"]")
deps))
"]")))
(with-temp-file "shadow-cljs.edn"
(insert (concat
"{:dependencies "
deps-as-vectors
"}")))
(async-shell-command
(concat "shadow-cljs " repl-type))
;; in a bit get rid of the temporary shadow-cljs.edn
(run-with-idle-timer
10
nil
(lambda () (shell-command "rm shadow-cljs.edn")))
repl-type)))
(defun my/setup-shadow-cljs-project-if-possible (orig-fun &rest args)
(if-let* ((headers (nth 1 args))
(_ (equal "cljs" (alist-get :target headers)))
(cider-default-cljs-repl 'shadow)
(cider-shadow-default-options
(my/cider-run-projectless-cljs-repl (alist-get :shadowcljs-type headers)
(alist-get :deps headers))))
(progn
(while (not (car-safe (nrepl-extract-ports (cider--file-path "."))))
(message "Waiting for shadow-cljs to connect...")
(sleep-for 1))
(cider-connect-cljs (list
:host "localhost"
:port (car (nrepl-extract-ports (cider--file-path ".")))
:project-dir (concat "~/" (file-relative-name "." "~"))
:cljs-repl-type 'shadow))
(apply orig-fun args))
(apply orig-fun args)))
(advice-add 'ob-clojure-eval-with-cider :around #'my/setup-shadow-cljs-project-if-possible)
Note that this code creates a shadow-cljs.edn configuration file to
declare the dependencies. Starting a REPL comes to shadow-cljs
browser-repl or shadow-cljs node-repl, which are default build ids
that shadow-cljs provides by default. I also added a timer that
deletes the file after 10 seconds of inactivity, because I am pretty
sure I would forget to cleanup things after my note taking.
I put these ob-clojure extensions in this little GitHub project to make it easier to load for people.
Happy literate hacking!