Shadow.lazy - Convenience wrapper for shadow.loader/cljs.loader (original) (raw)

shadow.loader or the cljs.loader variant in CLJS only wrap the underlying goog.module.ModuleManager API in a very minimal fashion. Both are meant to load code-split :modules dynamically at runtime. The API is somewhat clunky to use since you not only have to remember which modules your code ends up in you also have to access it in rather hacky ways. The “official” way to access code from other :modules is resolve which made some odd choices.

Intro

So I created shadow.lazy which solves the issues I saw and made things a bit more convenient (IMHO). A quick example is to first create a shadow.lazy/Loadable instance via the shadow.lazy/loadable macro. It expects one argument which is a qualified symbol, a vector of symbols or a map of keyword to symbol.

(ns demo.app
  (:require [shadow.lazy :as lazy]))

(def x (lazy/loadable demo.thing/x))

(def xy (lazy/loadable [demo.thing/x demo.other/y]))

(def xym (lazy/loadable {:x demo.thing/x
                         :y demo.other/y}))

A Loadable instance only describe what to load. They will not actually load anything until you trigger the load. They can be passed around safely.

Loading them can be done via shadow.lazy/load.

(lazy/load x handle-load)
;; or
(-> (lazy/load x)
    (.then handle-load)

When the async load finishes the function will be called with one argument which will be whatever the loadable referenced by name (basically var lookups).

(ns demo.thing)

(def x "x")

(ns demo.other)

(def y "y")

So the first example would be called with (handle-load "x"), second (handle-load ["x" "y"]) and third (handle-load {:x "x" :y "y"}). defn would simply give you the function you can call.

Once loaded the Loadable instances can also be deref'd. So @x would give you "x". You can check (lazy/ready? x) to see if it has been loaded. A deref before ready? will throw an error, it will not trigger a load since we have to go async to start the load.

Note that none of this involves any module ids at all. The named symbols can be spread into several modules or just one. The compiler will figure out which modules need to be loaded and only trigger once everything is ready. No need to do anything if your module setup changes.

What is wrong with cljs.core/resolve?

I don’t like resolve for 2 reasons: It leaves ugly var metadata in the generated code and resolve before load will always remain nil even after the code was loaded. So the only way to use it properly was after the code was loaded which means you can’t pass it around as a reference.

(def x (resolve 'demo.browser-extra/x)

(cljs.loader/load :the-module (fn [] x)) ;; x still nil. must call resolve inside the fn.

generates

demo.browser.var_x = (((typeof demo !== 'undefined') && (typeof demo.browser_extra !== 'undefined') && (typeof demo.browser_extra.x !== 'undefined'))?(new cljs.core.Var((function (){
return demo.browser_extra.x;
}),cljs.core.with_meta(new cljs.core.Symbol("demo.browser-extra","x","demo.browser-extra/x",1564327616,null),new cljs.core.PersistentArrayMap(null, 1, [new cljs.core.Keyword("cljs.analyzer","no-resolve","cljs.analyzer/no-resolve",-1872351017),true], null)),null)):null);

Notes

Consider this alpha, things may change. Currently this only works in shadow-cljs since it requires support from the compiler to know which module as given var will be in. If this ends up being useful I hope that we can build something to CLJS itself so it works with other build tools as well.

This is available since shadow-cljs@2.8.10. Feedback is welcome.