diff --git a/app/malli/instrument_app.cljs b/app/malli/instrument_app.cljs
index e6253d9c0..8e34bd0bd 100644
--- a/app/malli/instrument_app.cljs
+++ b/app/malli/instrument_app.cljs
@@ -5,13 +5,15 @@
[malli.dev.cljs :as dev]
[malli.dev.pretty :as pretty]
[malli.generator :as mg]
+ [malli.dev.cljs :as md]
[malli.instrument.cljs :as mi]))
(defn init []
(js/console.log "INIT!"))
(defn refresh {:dev/after-load true} []
- (.log js/console "hot reload"))
+ ;(.log js/console "hot reload")
+ )
(defn x+y
{:malli/schema [:=> [:cat float? float?] :double]}
@@ -163,6 +165,16 @@
(defn plusX [x] (inc x))
(m/=> plusX [:=> [:cat :int] MyInt])
+(defn try-it []
+ (plus "a"))
+
+(defn ^:dev/after-load x []
+ (println "AFTER LOAD - malli.dev.cljs/start!")
+ (md/start!)
+ (js/setTimeout try-it 200)
+ )
+
+
(comment
((->minus) 5)
(mi/check)
diff --git a/deps.edn b/deps.edn
index 9c378016d..6245587e3 100644
--- a/deps.edn
+++ b/deps.edn
@@ -29,7 +29,8 @@
jmh-clojure/task {:mvn/version "0.1.1"}}
:main-opts ["-m" "jmh.main"]}
:shadow {:extra-paths ["app"]
- :extra-deps {thheller/shadow-cljs {:mvn/version "2.17.8"}}}
+ :extra-deps {thheller/shadow-cljs {:mvn/version "2.17.8"}
+ binaryage/devtools {:mvn/version "1.0.5"}}}
:slow {:extra-deps {io.dominic/slow-namespace-clj
{:git/url "https://git.sr.ht/~severeoverfl0w/slow-namespace-clj"
:sha "f68d66d99d95f4d2bfd61f001e28a8ad7c4d3a12"}}
diff --git a/docs/cljs-instrument-development.md b/docs/cljs-instrument-development.md
new file mode 100644
index 000000000..8c57cb7b6
--- /dev/null
+++ b/docs/cljs-instrument-development.md
@@ -0,0 +1,19 @@
+To work on the function instrumentation for ClojureScript clone the malli repository and:
+
+```bash
+npm i
+./node_modules/.bin/shadow-cljs watch instrument
+```
+
+Open an nREPL connection from your favorite editor to the port located in `.shadow-cljs/nrepl.port`
+
+Open a browser to `http://localhost:8000`
+
+the port is set in the `shadow-cljs.edn` file should you wish to change it.
+
+
+In your editor evaluate:
+
+`(shadow/repl :instrument)`
+
+The dev-time code is located in the file: `app/malli/instrument_app.cljs`.
diff --git a/docs/clojurescript-function-instrumentation.md b/docs/clojurescript-function-instrumentation.md
new file mode 100644
index 000000000..57dafcf25
--- /dev/null
+++ b/docs/clojurescript-function-instrumentation.md
@@ -0,0 +1,94 @@
+# ClojureScript Function Instrumentation
+
+Function instrumentation is also supported when developing ClojureScript browser applications.
+
+Things work differently from the Clojure version of instrumentation because there are no runtime Vars in ClojureScript and thus the
+instrumentation must happen at compile-time using macros.
+The macro will emit code that `set!`s the function at runtime with a version that validates the function's inputs and outputs
+against its declare malli schema.
+
+# Dev Setup
+
+For the best developer experience make sure you install the latest version of binaryage/devtools and use a chromium based browser:
+
+https://clojars.org/binaryage/devtools
+
+if you are using shadow-cljs just ensure this library is on the classpath.
+
+For an application that uses React.js such as Reagent you will typically declare an entry namespace and init function in your `shadow-cljs.edn` config like so:
+
+```clj
+{...
+:modules {:app {:entries [your-app.entry-ns]
+:init-fn your-app.entry-ns/init}}
+...}
+```
+
+In your init namespace require `malli.dev.cljs`:
+
+```clj
+(require [malli.dev.cljs :as md])
+```
+
+and invoke `start!` in your init function before rendering your application:
+
+```clj
+(defn ^:export init []
+ (md/start!)
+ (my-app/mount!)
+```
+
+When you save source code files during development and new code is hot-reloaded the non-instrumented versions will now
+overwrite any instrumented versions.
+
+To instrument the newly loaded code with shadow-cljs we can use the [lifecylce hook](https://shadow-cljs.github.io/docs/UsersGuide.html#_lifecycle_hooks)
+`:after-load` by adding metadata to a function and invoking `malli.dev.cljs/start!` again:
+
+```clj
+(defn ^:dev/after-load reload []
+ (md/start!)
+ (my-app/mount!))
+```
+
+It is useful to understand what is happening when you invoke `(malli.dev.cljs/start!)`
+
+The line where `start!` lives in your code will be replaced by a block of code that looks something like:
+
+```clj
+(set! your-app-ns/a-function
+ (fn [& args]
+ :; validate the args against the input schema
+ ;; invoke the function your-app-ns/a-function and validate the output against the output schema
+ ;; return the output
+ )
+;; assuming an implementation in your-app-ns like:
+(defn a-function
+ {:malli/schema [:=> [:cat :int] :string]}
+ [x]
+ (str x))
+```
+
+(you can see what is actually output here: https://github.com/metosin/malli/blob/400dc0c79805028a6d85413086d4d6d627231940/src/malli/instrument/cljs.clj#L69)
+
+And this is why the order of loaded code will affect the instrumented functions. If the code for `your-app-ns/a-function`
+is hot-reloaded and the `start!` call is never invoked again, the function will no longer be instrumented.
+
+## Errors in the browser console
+
+When you get a schema validation error and instrumentation is on you will see an exception in the browser devtools.
+
+A validation error looks like this:
+
+
+
+If you click the arrow that is highlighted in the above image you will see the error message:
+
+
+
+and if you click the arrow highlighted in this above image you will see the stracktrace:
+
+
+
+the instrumented function is the one with the red rectangle around it in the image above.
+
+If you click the filename (`instrument_app.cljs` in this example) the browser devtools will open a file viewer at the problematic call-site.
diff --git a/docs/img/cljs-instrument/cljs-instrument-error-collapsed.png b/docs/img/cljs-instrument/cljs-instrument-error-collapsed.png
new file mode 100644
index 000000000..b5a62ce2d
Binary files /dev/null and b/docs/img/cljs-instrument/cljs-instrument-error-collapsed.png differ
diff --git a/docs/img/cljs-instrument/cljs-instrument-error-expanded.png b/docs/img/cljs-instrument/cljs-instrument-error-expanded.png
new file mode 100644
index 000000000..ec6542df6
Binary files /dev/null and b/docs/img/cljs-instrument/cljs-instrument-error-expanded.png differ
diff --git a/docs/img/cljs-instrument/cljs-instrument-stacktrace-expanded.png b/docs/img/cljs-instrument/cljs-instrument-stacktrace-expanded.png
new file mode 100644
index 000000000..3ff8e5a83
Binary files /dev/null and b/docs/img/cljs-instrument/cljs-instrument-stacktrace-expanded.png differ
diff --git a/src/malli/dev/cljs.cljc b/src/malli/dev/cljs.cljc
index 34d9db258..dbe358a79 100644
--- a/src/malli/dev/cljs.cljc
+++ b/src/malli/dev/cljs.cljc
@@ -1,6 +1,7 @@
(ns malli.dev.cljs
#?(:cljs (:require-macros [malli.dev.cljs]))
- #?(:cljs (:require [malli.instrument.cljs]))
+ #?(:cljs (:require [malli.instrument.cljs]
+ [malli.dev.pretty :as pretty]))
#?(:clj (:require [malli.clj-kondo :as clj-kondo]
[malli.dev.pretty :as pretty]
[malli.instrument.cljs :as mi])))
@@ -24,11 +25,11 @@
#?(:clj (defmacro start!
"Collects defn schemas from all loaded namespaces and starts instrumentation for
a filtered set of function Vars (e.g. `defn`s). See [[malli.core/-instrument]] for possible options.
- Differences from Clojure malli.dev/start:
+ Differences from Clojure `malli.dev/start!`:
- - Does not emit clj-kondo type annotations.
+ - Does not emit clj-kondo type annotations. See `malli.clj-kondo/print-cljs!` to print clj-kondo config.
- Does not re-instrument functions if the function schemas change - use hot reloading to get a similar effect."
- ([] (start!* &env {:report `(pretty/reporter)}))
+ ([] (start!* &env {:report `(pretty/thrower)}))
([options] (start!* &env options))))
#?(:clj (defmacro collect-all! [] (mi/-collect-all-ns)))
diff --git a/src/malli/dev/pretty.cljc b/src/malli/dev/pretty.cljc
index 70a889dec..13747c793 100644
--- a/src/malli/dev/pretty.cljc
+++ b/src/malli/dev/pretty.cljc
@@ -39,27 +39,30 @@
(-block "Schema:" (v/-visit schema printer) printer) :break :break
(-block "More information:" (-link "https://cljdoc.org/d/metosin/malli/CURRENT" printer) printer)]}))
-(defmethod v/-format ::m/invalid-input [_ _ {:keys [args input]} printer]
+(defmethod v/-format ::m/invalid-input [_ _ {:keys [args input fn-name]} printer]
{:body
[:group
(-block "Invalid function arguments:" (v/-visit args printer) printer) :break :break
+ #?(:cljs (-block "Function Var:" (v/-visit fn-name printer) printer)) :break :break
(-block "Input Schema:" (v/-visit input printer) printer) :break :break
(-block "Errors:" (-explain input args printer) printer) :break :break
(-block "More information:" (-link "https://cljdoc.org/d/metosin/malli/CURRENT/doc/function-schemas" printer) printer)]})
-(defmethod v/-format ::m/invalid-output [_ _ {:keys [value output]} printer]
+(defmethod v/-format ::m/invalid-output [_ _ {:keys [value output fn-name] :as args} printer]
{:body
[:group
(-block "Invalid function return value:" (v/-visit value printer) printer) :break :break
+ #?(:cljs (-block "Function Var:" (v/-visit fn-name printer) printer)) :break :break
(-block "Output Schema:" (v/-visit output printer) printer) :break :break
(-block "Errors:" (-explain output value printer) printer) :break :break
(-block "More information:" (-link "https://cljdoc.org/d/metosin/malli/CURRENT/doc/function-schemas" printer) printer)]})
-(defmethod v/-format ::m/invalid-arity [_ _ {:keys [args arity schema]} printer]
+(defmethod v/-format ::m/invalid-arity [_ _ {:keys [args arity schema fn-name]} printer]
{:body
[:group
(-block (str "Invalid function arity (" arity "):") (v/-visit args printer) printer) :break :break
(-block "Function Schema:" (v/-visit schema printer) printer) :break :break
+ #?(:cljs (-block "Function Var:" (v/-visit fn-name printer) printer)) :break :break
(-block "More information:" (-link "https://cljdoc.org/d/metosin/malli/CURRENT/doc/function-schemas" printer) printer)]})
;;
@@ -72,7 +75,8 @@
(fn [type data]
(-> (ex-info (str type) {:type type :data data})
(v/-exception-doc printer)
- (v/-print-doc printer)))))
+ (v/-print-doc printer)
+ #?(:cljs (-> with-out-str println))))))
(defn thrower
([] (thrower (-printer)))
diff --git a/src/malli/dev/virhe.cljc b/src/malli/dev/virhe.cljc
index 4b485b73c..b8a7a592a 100644
--- a/src/malli/dev/virhe.cljc
+++ b/src/malli/dev/virhe.cljc
@@ -24,7 +24,8 @@
(defn -color [color body printer]
(let [colors (:colors printer -dark-colors)
color (get colors color (:error colors))]
- [:span [:pass (str "\033[38;5;" color "m")] body [:pass "\u001B[0m"]]))
+ #?(:cljs [:span body]
+ :clj [:span [:pass (str "\033[38;5;" color "m")] body [:pass "\u001B[0m"]])))
;;
;; EDN
diff --git a/src/malli/instrument/cljs.clj b/src/malli/instrument/cljs.clj
index 7c3ad3a38..6b03ef5eb 100644
--- a/src/malli/instrument/cljs.clj
+++ b/src/malli/instrument/cljs.clj
@@ -52,17 +52,21 @@
;; instrument
;;
-(defn -emit-instrument-fn [env {:keys [gen filters] :as instrument-opts} {:keys [schema] :as schema-map} ns-sym fn-sym]
+(defn -emit-instrument-fn [env {:keys [gen filters report] :as instrument-opts}
+ {:keys [schema] :as schema-map} ns-sym fn-sym]
;; gen is a function
(let [schema-map (-> schema-map
(select-keys [:gen :scope :report])
;; The schema passed in may contain cljs vars that have to be resolved at runtime in cljs.
- (assoc :schema `(m/function-schema ~schema)))
+ (assoc :schema `(m/function-schema ~schema))
+ (cond-> report
+ (assoc :report `(cljs.core/fn [type# data#] (~report type# (assoc data# :fn-name '~fn-sym))))))
schema-map-with-gen
- (as-> (merge (select-keys instrument-opts [:scope :report :gen]) schema-map) $
- ;; use the passed in gen fn to generate a value
- (cond (and gen (true? (:gen schema-map))) (assoc $ :gen gen)
- :else (dissoc $ :gen)))
+ (as-> (merge (select-keys instrument-opts [:scope :report :gen]) schema-map) $
+ ;; use the passed in gen fn to generate a value
+ (if (and gen (true? (:gen schema-map)))
+ (assoc $ :gen gen)
+ (dissoc $ :gen)))
replace-var-code (when (ana-api/resolve env fn-sym)
`(do
(swap! instrumented-vars #(assoc % '~fn-sym ~fn-sym))