One of the best things in Clojure (and ClojureScript) is that you can design your code connected in a live environment – so, your auto-complete abilities reflect exactly what’s running right now. Then, you can evaluate code with real data, to catch bugs or just test things. Then comes Atom, an editor that, in my opinion, is one of the easiest editors to create plugins (packages), using technologies we already know – mostly, HTML, CSS, and JavaScript. To program with Clojure, you can use proto-repl – an awesome package that, combined with ink, allows us to run clojure code and display right on the editor, Light Table style.
But then I became greedy and wanted more. I created clojure-plus, a package that extends proto-repl to be able to work with multiple projects, specially when these projects are not configured to be “refresh-friendly” or something. Most of the things I have in clojure-plus are simple helpers that I found missing in proto-repl, at least in the beginning.
But, after that, I began to work professionally with Clojure. And then, most of the projects had some kind of “strangeness”, mostly because everyone was using InteliJ with Cursive – a lot of people I knew didn’t even run the code, with exception of midje tests. So, I changed my package to work around these “strangeness”, and after a while, I saw that I was creating a big mess of code. Then, came ClojureScript support, and things became even more complicated… so, came the idea to port my package to ClojureScript, and after trying several things (Figwheel, Ajom, and other packages) I discovered that they could not solve my problems. The only one that worked, with restrictions, was Weasel, but then with some hacks things worked fine. So, here are the steps to make things work:
You’ll need weasel, piggieback, and clojure-plus (yes, I’m using clojure-plus to develop clojure-plus). Then, configure your project.clj
to add weasel and piggieback dependency (including :repl-options
), and on clojure-plus
settings, change Cljs Command
to (use 'weasel.repl.websocket) (cemerick.piggieback/cljs-repl (weasel.repl.websocket/repl-env :ip "0.0.0.0" :port 9001))
(probably in the future Clojure Plus will have a better way to configure these commands per project).
The above configurations will not work yet. For now, we’re just configuring Atom to, when it needs to, load a ClojureScript REPL and connect on port 9001. There’s a limitation with this approach – the port will be kept open even if we disconnect from REPL, for any reason, but for now this will do.
The next step is to create a ClojureScript code to connect into this REPL. The problem is that Weasel uses JavaScript’s “eval” to evaluate code, and this doesn’t work with Atom. So, we need to change it a little bit. As process-message
is a multimethod, we can rewrite it – but first, we need a way to run code. In Atom, we can var vm = require('vm')
and then run code from a function context: (function(code) { return vm.runInThisContext(code); })();
. In Clojure, this translates to ((fn [src] (.runInThisContext vm src)) code))
.
(ns your.namespace.here (:require [cljs.nodejs :as nodejs] [weasel.repl :as repl])) (def vm (js/require "vm")) (defmethod repl/process-message :eval-js [message] (let [code (:code message)] {:op :result :value (try {:status :success, :value (str ((fn [src] (.runInThisContext vm src)) code))} (catch js/Error e {:status :exception :value (pr-str e) :stacktrace (if (.hasOwnProperty e "stack") (.-stack e) "No stacktrace available.")}) (catch :default e {:status :exception :value (pr-str e) :stacktrace "No stacktrace available."}))}))
After this “monkey patch”, we can ask Atom to connect on REPL (on the same file). Please notice that we’ll try to connect every 3 seconds – this is necessary because we just open the websocket when we first try to run ClojureScript code, and we don’t know when it’ll happen.
(js/setInterval #(when-not (repl/alive?) (repl/connect "ws://localhost:9001")) 3000)
Now, all we need to do is to compile our ClojureScript code to be loaded in Atom. Problem is that, by default, ClojureScript will expose google closure compiler globally, and this causes all sorts of problems. To solve it, we need to optimize our code a little, and use a flag that says that it’ll wrap our code with (function() { ... })();
. So, our project.clj
will be updated with the following data:
:cljsbuild {:builds [{:id "dev" :source-paths ["src" "test"] :compiler { :main clojure-plus.helpers-test :output-to "lib/js/main.js" :target :nodejs :optimizations :simple :output-wrapper true :pretty-print true}}]})
The key parameters here are: :target :nodejs
, to not generate javascript for the browser, :optimizations :simple
, because otherwise we’ll have multiple files and then some kind of “global variable” (in this case, goog
) will be required, and last, :output-wrapper true
will surround our compiled JS code into (function() { ... })();
, so we’ll not have any globals being defined.
Then, we compile our code: lein cljsbuild once dev
, and we are ready to go – just import js/main.js
in some file, and it’ll log on console that we’re not connected to a websocket (yet!). But then, opening a REPL, connecting from Atom, and trying to run some ClojureScript code will fire the weasel websocket, our ClojureScript will connect to it, and then a REPL will be connected… most of the time. Unfortunately, sometimes it simply doesn’t connect and I had to reload Atom.
There are still many problems – mostly, certain requires don’t work (like cljs.test
most test libraries, some namespaces that have macros on it) and there’s no easy way of reloading code, but still, being able to live-test some plugins code is great.
I’ll keep testing other approaches and keep this blog updated. There’s probably a lot of better ways to work on it, but even with all the problems, it’s still awesome!
1 Comment
The History of Chlorine – Maurício Szabo · 2020-04-20 at 23:48
[…] I did do was try to port some of the CoffeeScript files to CLJS, with mixed results. Lots of things were missing: sometimes the REPL didn’t connect, sometimes things stopped […]
Comments are closed.