Skip to content

Instantly share code, notes, and snippets.

@mhuebert
Last active September 9, 2020 11:59
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mhuebert/18a1ef480d1bb0b0a270 to your computer and use it in GitHub Desktop.
Save mhuebert/18a1ef480d1bb0b0a270 to your computer and use it in GitHub Desktop.
Using existing namespaces from cljs.js

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

  1. Decide which namespaces you need the compiler to be aware of
  2. 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.)
  3. 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 simple GET function
  • David Nolen's example with cljs.core uses transit to encode the edn file. I imagine this is better; for development it's handy to read edn 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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment