This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-01-27
Channels
- # announcements (4)
- # asami (6)
- # aws-lambda (1)
- # babashka (38)
- # babashka-sci-dev (20)
- # beginners (87)
- # calva (67)
- # cider (19)
- # clerk (7)
- # clojure (102)
- # clojure-europe (52)
- # clojure-filipino (1)
- # clojure-hungary (4)
- # clojure-nl (1)
- # clojure-norway (6)
- # clojure-sweden (3)
- # clojure-uk (1)
- # cursive (13)
- # data-science (7)
- # datomic (8)
- # deps-new (1)
- # emacs (3)
- # fulcro (16)
- # graphql (3)
- # humbleui (3)
- # kaocha (3)
- # leiningen (3)
- # malli (3)
- # off-topic (14)
- # pathom (34)
- # polylith (4)
- # rdf (12)
- # reitit (3)
- # releases (1)
- # remote-jobs (7)
- # rum (2)
- # sci (22)
- # shadow-cljs (115)
- # tools-deps (26)
- # tree-sitter (29)
Hi, I have a bit of an out there request/idea.. So malli supports emitting clj-kondo configs for function schemas (https://github.com/metosin/malli#clj-kondo) which will write the config to the proper kondo location on the filesystem. I would like to get this to work for clojurescript function schemas as well. The issue is that the schemas can include Vars which must be resolved at runtime by a JS vm, which also means we don't (directly) have access to the filesystem (this is for browser targets).
Right now the not so good solution is to just give the user a console.log
of the kondo config and ask them to paste the contents into the kondo config file. So... I'm wondering if there is a way to use the shadow.cljs.devtools.client
API (or another, I'm not sure) in the browser combined with a custom websocket event handler that will pass the kondo config, which is just data, to the shadow clojure process which can then write it to the filesystem. The goal would be to include this as an optional addon for malli users so they can get kondo static analysis in clojurescript files. Thanks!
so you could create an external tool that also connects to shadow.remote and then gets the data
but every remote interaction shadow-cljs does is done over shadow.remote and its all accessible to anyone, this includes REPL, hot-reload, etc
very nice, thanks for the info. I'll try to figure out how to go about it based on the shadow codebase.
I added an example client in CLJ https://github.com/thheller/shadow-cljs/blob/master/src/dev/shadow/remote_example_ws.clj
just like :clj-eval
there is :cljs-eval
, just need to find the correct :to
via :request-clients
https://github.com/thheller/shadow-cljs/blob/master/src/dev/shadow/remote_example_ws.clj#L155
or implement some client side code, but given that it'll be mostly macro driven I guess eval is required either way
amazing, thanks for the example - I was going down the route of trying to use runtime.shared/add-extension
with a new op like :write-malli-config
. I don't think there's any macros involved. After your suggestion to do instrumentation at runtime I refactored it to do so https://github.com/metosin/malli/blob/master/src/malli/instrument.cljs .
so at runtime all the schema data is available and a simple data transform is run on it and then printed:
https://github.com/metosin/malli/blob/master/src/malli/clj_kondo.cljc#L208
So my hope was I could do something like
(shadow.remote.runtime.shared/call @runtime-ref
{:op :write-malli-config :data kondo-config})
and then have a listener on the clj side to simply write to fileok so I have it working with the method I was thinking, I just guessed at a few pieces, but the communication is happening clj side, loaded by including this ns in a shadow build hook:
(defmethod worker/do-relay-msg ::write-kondo-config
[worker-state msg]
(println "Got kondo config" msg)
worker-state)
cljs side:
(shared/call
(deref client-shared/runtime-ref)
{:op ::write-kondo-config
:to env/worker-client-id
:data (get-kondo-config)}
{})
(shadow.cljs.devtools.api/cljs-eval :the-build "(whatever.ns/get-kondo-config)")
and then write the result to disk?
oh man, that's so cool, ha - yea that's perfect! I can put that in a compile finish hook and I think that's it! Thanks Thomas
it's working fairly well, but there is an off-by-one problem/non-determinism at play with hot-reload. The hook I have runs on :flush
and then invokes cljs-eval
but sometimes the data returned contains the old values from the previous compilation. So I think the cljs-eval
is happening before the latest code is loaded. Ideally I wouldn't have to update the cljs side of things and could do this all from the build hook, so was wondering if there is a way to determine if the latest code was loaded in the client and not only written to disk.
well the server side implementation does not synchronize with the client side. it just sends messages
got it, I just used a simple counter on the client side and busy loop until it gets increment on the jvm side to know when the code is loaded. Thanks again for the help!
oh haha, yea this is what I got working: https://github.com/metosin/malli/pull/829
Malli has a feature that takes a function schema and converts it to equivalent clj-kondo config and writes that to the kondo config directory. We want the same capability in cljs but the constraints are: • we need to evaluate the cljs code because the schemas may contains Vars which can only be resolved at runtime • once we have the output of the schemas-> clj-kondo config we need to persist it to the filesystem so given those constraints I realized that shadow-cljs is already doing this communication and can we utilize that somehow? and that's how I landed on this solution Hope that helps clarify what the goal is.
in CLJ: • all schemas are collected and then transformed to kondo config and then saved to disk that happens here: https://github.com/metosin/malli/blob/cfbc8eaa05bedb1e7e7b132d52bbf2e61b143f92/src/malli/dev.clj#L35
and then again via that watch on the https://github.com/metosin/malli/blob/cfbc8eaa05bedb1e7e7b132d52bbf2e61b143f92/src/malli/dev.clj#L32 so as you dev and change any function schemas the new kondo config is output
and in the preload ns (defn ^:dev/after-load kondo-transfer! [] (whatever-to-send-message-to-relay))
could be yeah. in the preload ns you could ensure its loaded via :require-macros [malli.shadow-helper]
I see - I think that would work! from the user's perspective they would then only need to add the preload. And users not using shadow, would be unaffected by this
since you could just make the preload part of mall, without needing to add all the requires
yesss! that was my next question - how to make this a library only concern and not need the user to deal with it
if (defn ^:dev/after-load kondo-transfer! [] (whatever-to-send-message-to-relay))
this lives in the library in a ns that cannot include the user's code, will it always run after the user's code?
from a user's point of view they're using
in an after-load
hook and that is where the latest schemas are picked up after hot reload. So this kondo preload would have to run after that, which I'm not sure about the order of multiple after-load
hooks
the order is somewhat unreliable, it should be in dependency order but isn't guaranteed to be
the idea is that a user will call
however they like - could be in a preload but doesn't have to be - it also takes an options map which we want to make visible to the user
I assume the goal should be to keep malli.dev.cljs
out of the build entirely for release
(ns com.my-org.client.preload
{:dev/always true}
(:require
[] ;; must require all namespace here that potentially get instrumented
[my.app.util]
[malli.dev.cljs :as md]
[malli.dev.shadow-hooks]
[malli.instrument :as mi]))
(mi/instrument!)
(md/start!)
yea agreed there - I think we're saying there is a preload ns the library provides which will deal with the kondo transmission, but the user will have to provide their own separate preload to include the namespaces in their app to ensure the preload runs last
just need the start:
(ns com.my-org.client.preload
{:dev/always true}
(:require
[] ;; must require all namespace here that potentially get instrumented
[my.app.util]
[malli.dev.cljs :as md]
[malli.dev.shadow-hooks]
[malli.instrument :as mi]))
(defn {:dev/after-load} dev []
(md/start!))
(ns malli.dev.shadow-instrument
{:shadow.build/late-cljs true
:dev/always true}
(:require
[malli.dev.cljs :as md]
[malli.dev.shadow-hooks]
[malli.instrument :as mi]))
(mi/instrument!)
(md/start!)
but looking at this I think another option is to tell the user to just invoke this call:
(ns com.my-org.client.preload
{:dev/always true}
(:require
[] ;; must require all namespace here that potentially get instrumented
[my.app.util]
[malli.dev.cljs :as md]
[malli.dev.shadow-hooks]
[malli.instrument :as mi]))
(defn {:dev/after-load} dev []
(md/start!)
(malli.shadow-kondo/emit!))
emits something like:
(malli/collect [long list of all namespaces])
(malli/instrument!)
(ns user.app.shadow-instrument-preload
{:dev/always true}
(:require
[]
[malli.dev.cljs :as md]
[malli.dev.shadow-hooks]
))
(md/start!)
if thats the :init-fn
ns it will ensure that the preload is actually compiled after those
preload is a bad name, doesn't mean its actually preloaded just injected into the build
got it - ok yea that should work then. and then the malli side can be a preload like you suggested
{:main
{:target :browser
:output-dir "resources/public/js/main"
:asset-path "js/main"
:dev {:modules {:main {:init-fn com.my-org.client.entry/init}}}
:release {:modules {:main {:init-fn com.my-org.client.release-entry/init}}
:build-options {:ns-aliases
{malli.dev.cljs malli.dev.cljs-noop
com.my-org.client.malli-registry com.my-org.client.malli-registry-release}}}
:closure-defines {malli.registry/type "custom"}
,,,
https://shadow-cljs.github.io/docs/UsersGuide.html#_release_specific_vs_development_configuration
this is really helpful. I'll try out the setup you suggested using the multi-method and clean up those docs. I really appreciate the help on this.
is there a way to trigger an npm install for cljs dependencies from shadow.cljs.devtools.api/watch
? or maybe from (shadow.cljs.devtools.server/start!)
?
(npm-deps/main {} {})
got the job done!