shadow-cljs (since about 2.0.15) supports compiling builds that make use of the self-hosted ClojureScript Compiler. Previously this was not supported since the build needs to be modified in a few places to ensure that all the required files are available. shadow-cljs itself continues to use the JVM compiler.

This feature is split into two parts. First you need a “host” build which will be your “app” (currently limited to :browser builds). This build will host the compiler and then use the support files generated by the second :bootstrap build. These support files include the CLJS analyzer cache, macro JS files, .cljs files, etc.

The :bootstrap build itself will generate an “index” with useful information for the compiler. When the “host” build starts up it calls the provided shadow.cljs.bootstrap.browser/init function to load the index and fetch all resources necessary to start using the compiler (which usually involves the analyzer data for cljs.core and the cljs.core$macros JS file).

Once init completes the compiler can be used. The provided shadow.cljs.bootstrap.browser/load function takes care of properly loading dependencies. The CLJS analyzer will call this function whenever a dependency is required and it will load the analyzer data, macro and JS files. Only namespaces pre-compiled by the :bootstrap build will be available.

The “host” can look something like this:

(ns demo.selfhost.simple
  (:require [cljs.js :as cljs]
            [cljs.env :as env]
            [shadow.cljs.bootstrap.browser :as boot]))

(defn print-result [{:keys [error value] :as result}]
  (js/console.log "result" result)
  (set! (.-innerHTML (js/document.getElementById "dump")) value))

(def code
  "
(ns simpleexample.core
  (:require [clojure.string :as str]
            [reagent.core :as r]))
(defonce timer (r/atom (js/Date.)))
(defonce time-color (r/atom \"#f34\"))
(defonce time-updater (js/setInterval
                       #(reset! timer (js/Date.)) 1000))
(defn greeting [message]
  [:h1 message])
(defn clock []
  (let [time-str (-> @timer .toTimeString (str/split \" \") first)]
    [:div.example-clock
     {:style {:color @time-color}}
     time-str]))
(defn color-input []
  [:div.color-input
   \"Time color: \"
   [:input {:type \"text\"
            :value @time-color
            :on-change #(reset! time-color (-> % .-target .-value))}]])
(defn simple-example []
  [:div
   [greeting \"Hello world, it is now\"]
   [clock]
   [color-input]])
(r/render [simple-example] (js/document.getElementById \"app\"))" )

(defonce compile-state-ref (env/default-compiler-env))

(defn compile-it []
  (cljs/eval-str
    compile-state-ref
    code
    "[test]"
    {:eval cljs/js-eval
     :load (partial boot/load compile-state-ref)}
    print-result))

(defn start []
  (boot/init compile-state-ref
    {:path "/bootstrap"}
    compile-it))

(defn stop [])

The shadow-cljs.edn config

{:dependencies
 [[reagent "0.8.0-alpha1" :exclusions [cljsjs/create-react-class]]

 :source-paths
 ["src"]
 
 :builds
 {:bootstrap-host
  {:target :browser

   :output-dir "out/demo-selfhost/public/simple/js"
   :asset-path "/simple/js"

   :compiler-options
   {:optimizations :simple}

   :modules
   {:base
    {:entries [demo.selfhost.simple]}}

   :devtools
   {:http-root "out/demo-selfhost/public"
    :http-port 8700
    :before-load demo.selfhost.simple/stop
    :after-load demo.selfhost.simple/start}}
 
  :bootstrap-support
  {:target :bootstrap
   :output-dir "out/demo-selfhost/public/bootstrap"
   :exclude #{cljs.js}
   :entries [cljs.js demo.macro reagent.core]
   :macros []}}}

The config option for the :bootstrap build are

  • :entries a sequence of namespaces you want to have available for the self-hosted compiler
  • :exclude for macro namespaces that are not self-host compatible as they would otherwise break the build. This would include things like cljs.core.async.macros, cljs.js, etc.
  • :macros will usually be optional since all macros used by the :entries will already be included.

Everything is written to :output-dir. The path where those files are available must be passed to the boot/init function.

@mhuebert created a standalone example and the shadow-cljs repo itself contains the example above.

You can try it by running

npm install -g shadow-cljs
git clone https://github.com/thheller/shadow-cljs.git
cd shadow-cljs
shadow-cljs watch bootstrap-host bootstrap-support
open http://localhost:8700

Usually shadow-cljs does not require lein but it is required in this case since I’m using lein to build the shadow-cljs project. The standalone example is probably better suited for testing.

I didn’t cover a whole lot of technical details. Come by #shadow-cljs if you have questions. Please report issues here.