I’m trying out the new clojurescript instrumentation in malli 0.8.0 and am getting into issues with hot-reload and shadow-cljs. I’ve battled back-and-forth with this for a while so I’m not quite sure I’ve understood the problem correctly.
Aim: To be able to call malli.instrument.cljs/instrument! from another namespace than the ones where m/=> is called.
Problem: Changed instrumentations do not take effect, not even after quitting and restarting shadow-cljs watch app. I have to delete the .shadow-cljs directory and then restart the watch.
Hypothesized reason: Since the malli.instrument.cljs/instrument! is a macro, it generates the instrumentations at compile time. Since the namespace where it is called is not recompiled when I change the instrumentations (in other namespaces), the changed instrumentations are not included when the macro is expanded. Restarting shadow-cljs watch app doesn’t help since the build is cached.
Potential solution: Can I force shadow-cljs to recompile the namespace where malli.instrument.cljs/instrument! is called whenever any other namespace changes? Or should I go about this some other way?
Instrumentation is kinda tricky since it can affect all namespaces. The best way to go about this is via :preloads with a custom ns created for it.
(ns my.app.preload
{:dev/always true}
(:require
[my.app] ;; must require all namespace here that potentially get instrumented
[malli.instrument.cljs :as mi]))
(mi/instrument!)
The in your build config :devtools {:preloads [my.app.preload]}.
The {:dev/always true} always metadata on the ns ensures that this namespace is always recompiled. Although note that this can make your build a lot slower. It isn’t strictly necessary if the macro doesn’t emit changing code but since there is no reliable way to know what the macro does (from the shadow-cljs side) it might be best to always run it.
The my.app require ensures that the preload is actually compiled after all your app namespaces have been compiled. I know “preload” isn’t the best name here but you must do this to ensure that other namespaces aren’t still compiling when the macro runs.
Works perfectly in my dev environment (I think). But when I try the exact same setup for my tests (pre-load which requires my core test namespace), I get all kinds of strange errors. Sometimes just some functions are instrumented, and other times mi/instrument! results in a js error claiming that an instrumented function is undefined, although tests against work fine.
Can this be related to how test namespaces are loaded? There is no {:modules {:main {:entries [...]}}] in the test configuration, and the user’s guide suggests that there shouldn’t be one.
EDIT: Tried to enter the core test namespace as main module in :test but that led to a compile error.
Thanks @dvingo. Yes, I guess that the order of evaluation in tests is not predictable as it is when you are running a “regular” Shadow-CLJS app. I’m using Shadow-CLJS’ test runner, so compiled and run through the browser (not REPL). I’ve given up on getting instrumentation to work there - I don’t think it is as important though since I’m not running generative tests. It serves a purpose in development though so I get feedback if my functions don’t work in the semi-real world.