Shadow.lazy - Convenience wrapper for shadow.loader/cljs.loader


#1

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/x)

(def x "x")

(ns demo.thing/y)

(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 [email protected]. Feedback is welcome.


#2

This is super cool! We’ve built a loading solution on top of shadow.loader at work, and have felt some of this pain. This seems like a much more intuitive to use tool.


#3

I still have some confusions after reading the snippets. Despite the confusions, is there a working example to look at?


#4

The use is still pretty dependent on how or where you actually handle loading the code. The main thing this is supposed to solve is getting rid of the module ids in code. So you only use a symbol that refers to a namespace def that may not be loaded yet. I started adding it to the UI code directly but that still probably won’t explain much if you don’t know the rest of the UI code.

I’ll see if can create a simpler proper example. Unfortunately there aren’t many CLJS examples that use :modules, so I’ll have to create that first.