Let's say you want to use cljs.js
to eval
code using functions declared in your project.
Expressions that only use core functions are simple to evaluate:
(ns foo.try-eval
(:require [cljs.js :as cljs]))
(def compiler-state (cljs/empty-state))
(cljs/eval compiler-state
`(+ 1 1)`
{:eval cljs/js-eval
:context :expr}
(fn [result] (prn result)))
- We first create a new, empty compiler environment with
(cljs/empty-state)
. This comes pre-loaded with'cljs.core
, so you can run all the normal functions you'd expect, but it does not know anything about the rest of your project. - By default, code is evaluated in the namespace
cljs.user
.
Now let's eval:
(GET " https://hacker-news.firebaseio.com/v0/item/8863.json?print=pretty)
..with the GET
function referred in the namespace:
(ns 'eval-helpers
(:require [foo.http :refer [GET]]
...))
The first step is to include :ns 'foo.eval-helpers
in the compiler options, so that we evaluate from the 'foo.eval-helpers namespace. But that won't work by itself, because the compiler state doesn't know anything about the rest of your project — it has no data associated with the name 'foo.eval-helpers.
Solving this problem will involve learning a bit about a relatively new feature in the ClojureScript. When the experimental :cache-analysis
compiler option is true
(which is the default for REPLs and :optimizations :none
), building a ClojureScript project writes a slew of *.cache.edn files to your output folder, alongside source and compiled js files:
out └── cljs ├── analyzer.cljc ├── analyzer.cljc.cache.edn ├── analyzer.cljc.js ├── analyzer.cljc.js.map ├── compiler.cljc ├── compiler.cljc.cache.edn ├── compiler.js └── ... └── foo ├── core.cljs ├── core.cljs.cache.edn ├── ... ├── eval_helpers.cljs ├── eval_helpers.cljs.cache.edn └── ...
These *.cache.edn files contain the output of running the ClojureScript analyzer over a source file, and are saved to speed up re-compile times as you work on a project. They populate the compiler state to enable validation, optimization, and static access to var info - they tell the compiler what a pre-compiled JS file "means". This is exactly what we're missing when we're trying to eval code with cljs.js
- we already have the compiled js from our project on hand, but the compiler state can't make sense of it.
Fix this by loading the appropriate cache files & feeding them to our compiler state.
- Decide which namespaces you need the compiler to be aware of
- Copy the *.cache.edn files for those namespaces to a public location (while developing, you can access them from your output folder, but they are wiped out when you run
lein clean
and are obviously not created when using:optimizations :simple
. I put mine in resources/public/js/caches, and mirror the same internal directory structure as my output folder.) - From your app, fetch the *.cache.edn file and load it into your compiler state using
cljs.js/load-analysis-cache!
A simple load-cache
function might look like this:
(defn load-cache [cstate s]
(go
(let [path (str "js/caches/" (clojure.string/replace (name s) "." "/") ".cljs.cache.edn")
cache-edn (<! (GET path))
cache (read-string cache-edn)]
(cljs.js/load-analysis-cache! cstate s cache)
cache)))
(load-cache compiler-state 'foo.eval-helpers)
cljs.js/load-analysis-cache!
puts the cache into the compiler state, which is an atom that you can inspect if you're curious what's inside.
A couple of notes:
- in my ns I require
[cljs.reader :refer [read-string]]
, and I am using a very simpleGET
function - David Nolen's example with
cljs.core
usestransit
to encode theedn
file. I imagine this is better; for development it's handy to readedn
directly. - I'm sure there are improvements that can be made to this process, & would welcome feedback.
Much of my understanding of cljs.js
came from helpful conversations with @mfikes and @dnolen on Slack - thanks!