Boot Clo­jure­Script tool­ing up­dates and what's up next

04 October 2015

One of the fea­tures miss­ing from Boot Clo­jure­Script tool­ing in com­par­ison to Figwheel was heads-up dis­play (HUD). Thanks to Martin Klepsch this is now im­ple­men­ted in Boot-cljs and Boot-re­load. I've made a screen cast about the new fea­ture so check that to see how it works. Read on for some de­tails about the im­ple­ment­a­tion and to see what's up next for Boot Clo­jure­Script tool­ing.

Demon­stra­tion

Im­ple­ment­a­tion

As Boot Clo­jure­Script tool­ing con­sists of mul­tiple sep­ar­ate tasks, im­ple­ment­a­tion of HUD re­quired changes to two tasks:

Boot-cljs

Im­ple­ment­a­tion of HUD re­quires that in­form­a­tion about Clo­jure­Script warn­ings and ex­cep­tions is avail­able for send­ing to the cli­ent.

To catch the in­form­a­tion about Cljs warn­ings we'll set up cus­tom warn­ing-hand­ler. The hand­ler with both (1) print the warn­ing to con­sole and (2) store the warn­ings in an atom. The reason why we over­write de­fault warn­ing hand­ler which would also print the warn­ings to con­sole, is that we want to pro­cess file-path of the warn­ing be­fore print­ing. Be­cause Clo­jure­Script sees to source files at Boot tem­por­ary-dir­ect­or­ies the file paths in­clude the tem­por­ary dir­ect­ory path. To make the warn­ings cleaner we (3) re­trieve path for the ori­ginal file. Data about warn­ings is at­tached to .cljs.edn file metadata on the file­set.

(fn [warning-type env extra]
  (when (warning-enabled? warning-type)
    (when-let [s (ana/error-message warning-type extra)]
                 ;; 3
      (let [path (util/find-original-path source-paths dirs ana/*cljs-file*)]
        ;; 1
        (butil/warn "WARNING: %s %s\n" s (when (:line env)
                                           (str "at line " (:line env) " " path)))
        ;; 2
        (swap! warnings conj {:message s
                              :file path
                              :line (:line env)
                              :type warning-type})))))

Hand­ling ex­cep­tions is a bit more trick­ier be­cause Clo­jure­Script com­piler is run­ning in­side a pod. Usu­ally the com­mu­nic­a­tion between pods hap­pens us­ing pr-str and read-string, very sim­il­arly to how Lein­in­gen com­mu­nic­ates between mul­tiple JVMs. But this does­n't hap­pen with ex­cep­tions, they are in­stead dir­ectly thrown. The prob­lem here is that for some reason when ex­cep­tions are thrown from one class­loader to an­other, they lose their ex-info metadata. For Clo­jure­Script ex­cep­tions the metadata con­tain all the in­ter­est­ing data: file-path, line num­ber and column num­ber.

A workaround I found for this is to manu­ally seri­al­ize and deseri­al­ize ex­cep­tions, in­clud­ing metadata, stack-trace and cause stack. This way it's pos­sible to throw ex­cep­tion with cor­rect metadata from Boot-cljs to Boot-re­load.

As with warn­ings, file paths in ex­cep­tions are changed to con­tain path to the ori­ginal file in­stead of to a file in tem­por­ary dir­ect­ory.

Boot-re­load

Boot-re­load will either read warn­ings from .cljs.edn file metadata on the file­set or catch the ex­cep­tions thrown by Boot-cljs. Be­cause Boot tasks are im­ple­men­ted us­ing mid­dle­ware pat­tern it's simple as just call­ing next-task in­side try-catch. Boot-cljs will tag the ex­cep­tions so that we can dis­play only the (1) in­ter­est­ing ex­cep­tions on the browser. All ex­cep­tions are rethrown so that other tasks can ac­cess the ex­cep­tion data and to tell that the build failed.

(try
  (next-task fileset)
  (catch Exception e
        ;; 1
    (if (= :boot-cljs (:from (ex-data e)))
      (send-visual! @pod {:exception (merge {:message (.getMessage e)}
                                            (ex-data e))}))
    (throw e)))

The heads-up dis­play user in­ter­face is im­ple­men­ted purely us­ing Google Clos­ure lib­rary. This keeps the build sim­pler as we don't need any ad­di­tional de­pend­en­cies. Even though we are not us­ing soph­ist­ic­ated frame­work like Re­act, the user in­ter­face im­ple­ment­a­tion is only about one hun­dred lines, in­clud­ing CSS defin­i­tions. UI is even im­ple­men­ted us­ing im­me­di­ate mode ren­der­ing: Whenever new :visual mes­sage is re­ceived from the server, old DOM con­tainer is re­moved and a new one is cre­ated.

Next up

Boot-re­load fixes

Cur­rent file-re­load­ing im­ple­ment­a­tion has some prob­lems when one has mul­tiple Clo­jure­Script builds in one pro­ject. Boot-re­load tries to load all changes JS files in browser but it's pos­sible that the files don't be­long to the open ap­plic­a­tion and cause prob­lems.

Google Clos­ure lib­rary defines a (private) de­pend­ency graph of namespaces and it should be pos­sible to use that to de­term­ine if changed file is re­quired by any loaded namespace, if it's not, we don't need to load the changed file.

The same de­pend­ency graph can be used to sort the changed files in de­pend­ency or­der. Cur­rently Boot-cljs is cal­cu­lat­ing this de­pend­ency or­der and passing it to Boot-re­load, this is ad­di­tional work as Clo­jure­Script com­piler has already done this and passed the data to Clos­ure.

Fig­wheel is already us­ing Clos­ure de­pend­ency data so I'll be look­ing on it's im­ple­ment­a­tion and copy­ing rel­ev­ant parts to Boot-re­load.

Boot-cljs-repl fixes and im­prove­ments

I have now work­ing Clo­jure­Script REPL setup with Vim-fireplace so I'll be fix­ing prob­lems as I en­counter them.

Boot-cljs per­form­ance (file­set per­form­ance)

There have been mul­tiple re­ports of Boot-cljs be­ing slower than Lein­in­gen Cljs­build or Fig­wheel. On most cases the Clo­jure­Script com­piler works just as fast, but Boot file­sets cause some over­head which shows es­pe­cially with in­cre­mental re­com­piles. Per­form­ance pro­fil­ing should help to find the bot­tle­necks. Im­prov­ing this should help all Boot tasks.