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.