Fork me on GitHub
#shadow-cljs
<
2024-06-14
>
Vincent Cantin04:06:29

I would like to generate some rendering functions in CLJS based on some templates defined in CLJC files, and would like to know what would be the best approach to do this using shadow-cljs. I first tried to do it just using macros, it works when compiling the CLJC file in a CLJ env, but doesn't work in a CLJS env because it doesn't have the equivalent of clojure.core/resolve which I am using to get a template's value from its var symbol.

Vincent Cantin05:06:03

Since my macros work when the CLJC are compiled in a CLJ env, I am wondering if there was a way to use that to generate some files from a shadow-cljs hook and have those files fed into the compiled CLJS program. Would it work? Is there an existing project which does that and from which I could learn?

thheller06:06:22

CLJS does have the equivalent of resolve. It is cljs.analyzer.api/resolve

thheller06:06:45

and no, hooks should not generate files. if you already are in a CLJC file then just use CLJC

Vincent Cantin07:06:10

clojure.core/resolve gives me the value of the symbol via (-> my-symbol resolve deref) , cljs.analyzer.api/resolve doesn't seem to do that.

thheller07:06:06

yes, it returns a map instead of a var. what is it that you are trying to do exactly? usually you'd just want to get the resolved symbol no?

Vincent Cantin07:06:25

I have a few (defc my-component [,,,] ,,,) in a CLJC file which I transform into (def my-component {:id 'my-ns/my-component, :ast ,,,}) using a macro. I want to build a hashmap containing all the transitive components and their ast {'my-ns/my-component {:id ,,, :ast ,,,} ,,,other-dependent-components} at compile time, that I give to a macro to generate some CLJ/CLJS code.

thheller07:06:06

(defmacro example [the-sym]
  (:name (cljs.analyzer.api/resolve &env the-sym)))

thheller07:06:42

this is an example how you'd use it to get the fully qualified symbol name

thheller07:06:00

so (example Foo) would give you the.namespace.with/Foo

Vincent Cantin07:06:26

I have no problem getting the fully qualified symbol name, I have a problem getting the value associated to the var of that qualified symbol.

thheller07:06:08

ok yes, that is not possible since during compilation the JS code is not actually executed, so runtime values obviously cannot be accessed

Vincent Cantin07:06:28

So my idea would be to extract that information by running the CLJC and macros in a CLJ env, then generate some CLJS files which ... somehow need to be included in the user's program, not sure if there is a way to do that using shadow-cljs.

thheller07:06:14

how would you do it without shadow-cljs? the answer would be to do that 😛

Vincent Cantin07:06:14

yeah .. maybe running a separate program which is listening to changes on those CLJC files then generate the CLJS in the middle of the user's source directory.

thheller07:06:17

it is possible to store analyzer data in a way that can be accessed and survives cache cycles

thheller07:06:24

so you could store it there if you wanted, but that only works if its actual data, that transit can read/write

👍 1
thheller07:06:40

dunno why one component needs the ast of another, but that would be an option

Vincent Cantin07:06:50

It's useful for inlining the AST from different components, and for an holistic code generation.

thheller07:06:18

don't know what that means

thheller07:06:17

anyway, the way you can store data is using cljs.env/*compiler* atom

thheller07:06:44

in it there is s :cljs.analyzer/namespaces map which stores all analyzer data per namespace

thheller07:06:42

so (get-in @cljs.env/*compiler* [:cljs.analyzer/namespaces 'your.ns ::whatever]) can be stored and accessed in macros

thheller07:06:12

there are some rules that go with that so that it survives caching though

thheller07:06:15

so only data works

👍 1
thheller07:06:41

only the namespace that is currently being compiled may write the data as it is persisted after compiling that namespace

👍 1
thheller07:06:56

should something else attempt to modify data of other namespaces those changes may be lost, unless you turn caching off completely

thheller07:06:00

and you should only store data off that namespace, so do not store the full AST (with data from other namespaces)

👍 1
thheller07:06:10

as that will lead to stale data with hot-reload

Vincent Cantin07:06:13

That should work in my case, I will push the AST data in cljs.env/*compiler* from my defc macro and then collect it all from the place where I want to process their aggregation.

thheller07:06:48

yes, but ONLY the :cljs.analyzer/namespaces map is persisted in cache

thheller07:06:57

so anything else you write will not be restored

thheller07:06:26

I'm trying to explain that caching can break very very easily and will lead to very frustrating results debugging 😛

thanks2 1
thheller08:06:12

thats how it looks in practice

thheller08:06:22

try to keep the data small though as it will affect cache times

thheller08:06:42

oh and please use namespaced keywords to not conflict with actual analyzer data from the compiler

Vincent Cantin12:06:34

I did all that you said above and it works 🥳 .. with a small bug, but I understand why it happens.

Vincent Cantin12:06:30

When shadow-cljs is in watch mode, the place where I collect all the data stored in the analyzer's cache is not notified when one of the defc component is changed by the user. It happens because the cache mechanism stops the propagation of re-compilation to the namespaces which depend directly on the changed one, and if themselves don't change compared to before, then the re-compilation does not propagate further. When I collect the data in the core.cljs , I get all the transitive component dependencies starting from the root component that the user provides to my component-collecting macro used in core.clj. But shadow-cljs doesn't know that core.cljs now has a direct dependency to all of those transitive components' namespaces. To fix my bug, I need to declare those dependencies in my component-collecting macro. Do you have an idea how I could do that?

Vincent Cantin13:06:20

Oh .. actually, each defc component will need to list their transitive component dependencies if we want the incremental compilation to work. Maybe listing those dependencies in an unused field will work, let me try ..

thheller13:06:52

I warned you about caching issues 😉

thheller13:06:56

the compiler actually already tracks which vars a namespace used, but I guess in your case only the macro might be using stuff with no actual reference remaining in the CLJS code?

thheller13:06:39

or maybe you are on an older shadow-cljs version that didn't do that? this was changed kinda recently

Vincent Cantin13:06:42

The defc macro only returns data containing quoted symbols, so shadow-cljs has no way to know about the dependency.

thheller13:06:21

yeah it can't see what you are doing in the macro

thheller13:06:57

I also don't quite understand what you are doing

Vincent Cantin13:06:00

is there an API in shadow-cljs for macros to record this type of dependency ?

thheller13:06:08

so giving any kind of advice is tough

thheller13:06:21

API no. you technically do have access to it in build hooks, but I don't recommend you touching that

thheller13:06:31

basically also because it makes everything shadow-cljs only

Vincent Cantin13:06:40

well .. that would be to fix the incremental compilation, which is shadow-cljs only

Vincent Cantin13:06:29

I will try to list the transitive dependencies as vars of qualified symbols. It should work.

fabrao14:06:24

Hello, I'm using this:

["expo-auth-session/providers/google" :as google]
and trying to use it here
(let [[request response promptAsync] (google/useIdTokenAuthRequest
                                        #js {:clientId "***"
                                             :androidClientId "*****"})]
  
but it's returning this ->
Error: Invalid hook call. Hooks can only be called inside of the body of a function component.

fabrao14:06:09

it seems that it's already in to body component, or is there other way to handle hooks?

Ben Lieberman14:06:00

If you're using Reagent, you need to render a function component using [:f> your-component-name] . Examples here: https://github.com/reagent-project/reagent/blob/master/doc/ReactFeatures.md. Probably better to post in #clojurescript or #reagent too.

fabrao14:06:37

Got it, thank you

fabrao14:06:04

@U03QBKTVA0N Thank you, worked with :f>

Apple20:06:26

why does this code crash shadow-cljs? does it happen to you?

user> (re-matches #"." nil)

Execution error (TypeError) at (:1).
re-matches must match against a string.
:repl/exception!

user> (map #(re-matches #"." %) (repeat 1000 nil))
(crashes...)

Apple20:06:16

btw it's emacs/cider env.

INTERNAL REPL ERROR: Got an unexpected reply from relay, check Inspect[:unexpected-from-relay
 {:op :unknown-op,
  :msg
  {:op :obj-as-str,
   :oid "801cf7ae-f552-465b-b307-e434de415534",
   :from 9},
  :from 3}
 {:ns user,
  :runtime-id 3,
  :stage :print,
  :client-id 9,
  :read-result
  {:eof? false,
   :source "(map #(re-matches #\".\" %) (repeat 1000 nil))"},
  :eval-result
  {:op :eval-result-ref,
   :ref-oid "17d4949b-7c8a-4b39-afed-92d76dbdeab8",
   :eval-ms 0,
   :eval-ns user,
   :warnings [],
   :from 3},
  :print-failed true}
 {:system-bus
  {:shadow.cljs.devtools.server.system-bus/service true,
   :bus-mult-chan
   #object[clojure.core.async.impl.channels.ManyToManyChannel 0xcaf4c05 "clojure.core.async.impl.channels.ManyToManyChannel@caf4c05"],
   :bus-mult
   #object[clojure.core.async$mult$reify__17794 0x5c147359 "clojure.core.async$mult$reify__17794@5c147359"],
   :bus-pub-chan
   #object[clojure.core.async.impl.channels.ManyToManyChannel 0x7aa04750 "clojure.core.async.impl.channels.ManyToManyChannel@7aa04750"],
   :bus-pub
   #object[clojure.core.async$pub$reify__18406 0x56ae920c "clojure.core.async$pub$reify__18406@56ae920c"]},
  :proc-id "904845d4-f336-42d0-b449-4fee099d36df",
  :fs-watch
  {:shadow.cljs.devtools.server.fs-watch/service true,
   :control
   #object[clojure.core.async.impl.channels.ManyToManyChannel 0x2ac1b8d "clojure.core.async.impl.channels.ManyToManyChannel@2ac1b8d"],
   :watch-dirs
   [{:dir
     #object[java.io.File 0x136b8bbc "....../resources/public"],
     :watcher
     #object[shadow.util.FileWatcher 0x800ac18 "shadow.util.FileWatcher@800ac18"]}],
   :thread
   #object[clojure.core.async.impl.channels.ManyToManyChannel 0x6ebc52eb "clojure.core.async.impl.channels.ManyToManyChannel@6ebc52eb"]},
  :output-mult
  #object[clojure.core.async$mult$reify__17794 0x4f3cad99 "clojure.core.async$mult$reify__17794@4f3cad99"],
  :shadow.cljs.devtools.server.worker.impl/proc true,
  :status-ref
  #,
  :state-ref
  #

thheller20:06:16

can't tell you what the exact problem is, but the result fails to print. unsure why

Apple20:06:50

you are so fast

Apple20:06:38

no error reported at all. is this expected?

cljs.user> (re-matches #"." nil)
cljs.user> (map #(re-matches #"." %) (repeat 1000 nil))
cljs.user> 

thheller21:06:42

no, just badly handled

thheller21:06:55

I think emacs might be hiding the error from you?

thheller21:06:08

(map #(re-matches #"." %) (repeat 1000 nil))
The result object failed to print. It is available via *1 if you want to interact with it.
The exception was: 
TypeError: re-matches must match against a string.
=> :repl/print-error!

thheller21:06:11

thats what I get

Apple21:06:07

hmm

cljs.user> (/ 1 0)

Apple21:06:43

clj repl still works

shadow.user> (re-matches #"." nil)
Execution error (NullPointerException) at java.util.regex.Matcher/getTextLength (Matcher.java:1806).
Cannot invoke "java.lang.CharSequence.length()" because "this.text" is null
shadow.user> (map #(re-matches #"." %) (repeat 1000 nil))
Error printing return value (NullPointerException) at java.util.regex.Matcher/getTextLength (Matcher.java:1806).
Cannot invoke "java.lang.CharSequence.length()" because "this.text" is null
shadow.user> (/ 1 0)
Execution error (ArithmeticException) at shadow.user/eval68294 (user.clj:48).
Divide by zero
shadow.user> 

thheller21:06:32

(/ 1 0)
=> ##Inf

thheller21:06:45

works fine for me. can't tell you what cider is doing.

Apple22:06:17

not good:

cljs.user> (ns xyz)
cljs.user> 

thheller06:06:57

at this point I don't have the slightest clue what you are doing, so impossible to make any kind of comment

thheller06:06:22

if the REPL is in any kind of broken state it should be restarted. just trying to keep using it leads nowhere

thheller06:06:40

I can maybe help diagnosing the initial error. every error/misbehavior coming after that I always assume was caused by the first

Apple12:06:06

with the patch cljs repl doesn't show any return value, switching ns doesnt change prompt, no exception etc

cljs.user> 4
cljs.user> 5
cljs.user> 6
cljs.user> (+ 1 1)
cljs.user> (ns kkk)
cljs.user> (/ 1 0)
cljs.user> 

Apple12:06:38

perhaps interop issue with emacs/cider since your vscode(?) is working

thheller13:06:37

I use Cursive

thheller13:06:39

this is easy to test. instead of using cider at all run the REPL built into shadow-cljs directly

thheller13:06:46

npx shadow-cljs cljs-repl your-build-id

thheller13:06:55

cljs.user=> (ns foo.bar)
nil
foo.bar=>

thheller13:06:58

works just fine for me

thheller13:06:16

I don't know what setup you are using given that the fix I did is not in any release yet

Apple13:06:03

i save the file to classpath and test

thheller13:06:56

and you are using 2.28.8 to begin with?

Apple13:06:16

npx works

Apple13:06:38

so my shadow-cljs is 2.28.8 plus the patched repl_impl.clj but nothing between 2.28.8 and when repl_impl.clj was patched.

Apple13:06:35

$ find src
src/clj ...
src/cljs ...
src/cljc ...
src/clj/shadow
src/clj/shadow/cljs
src/clj/shadow/cljs/devtools
src/clj/shadow/cljs/devtools/server
src/clj/shadow/cljs/devtools/server/repl_impl.clj
this is how i incorporate the patched to test

thheller13:06:20

sorry can't really help you debug cider issues. I don't have a clue what it might not like

Apple13:06:42

that's totally fine.

Apple13:06:59

just want to mention it. that's all.

Apple13:06:23

perhaps it's isolated emacs/cider config issue on my end

thheller13:06:56

what happens if you remove that file?

Apple13:06:19

then it's back to original behavior

thheller13:06:21

I mean it was already kinda hard running into that problem in the first place

thheller13:06:32

why do you keep evaling stuff that you know is broken? 😛

Apple13:06:01

oh not any more

Apple13:06:34

it took me a while to find the root cause

Apple13:06:51

i'm not running the code like that any more

Apple13:06:59

to avoid crashes

Apple13:06:15

i'm fine to stay at 2.28.8 or 2.28.9 i think

thheller13:06:28

I kinda suspect that you are not actually running the latest shadow-cljs version?

thheller13:06:44

is it possible that you are using deps.edn or project.clj and only updated the shadow-cljs npm version?

Apple13:06:44

how did u mean?

thheller13:06:10

I mean the change was so minimal that it is pretty much impossible for it to affect anything else

thheller13:06:33

so I suspect that you are using an actually older shadow-cljs verison which had a very different repl_impl.clj out of the box

thheller13:06:53

which you then overwrite, which then of course breaks all sorts of stuff

thheller13:06:40

the actual shadow-cljs version being used is printed on startup

Apple13:06:47

$ clj -Stree -M:dev:cider | grep shadow
thheller/shadow-cljs 2.28.8
  . thheller/shadow-util 0.7.0
  . thheller/shadow-client 1.3.3
  . thheller/shadow-undertow 0.3.4
  . thheller/shadow-cljsjs 0.0.22
$ cat user.clj | grep ...
(ns ...
   [shadow.cljs.devtools.api :as shadow]
   [shadow.cljs.devtools.server :as server])
(server/start!)
(shadow/watch :app)
$ cat shadow-cljs.edn
{:nrepl {:port 7002}
 :source-paths ["src/cljs" "src/cljc"]
 :dependencies [[binaryage/devtools "1.0.3"]
                [nrepl              "1.1.2"]
                [cider/cider-nrepl  "0.48.0"]
                [cljs-ajax          "0.8.4"]
                [metosin/reitit     "0.6.0"]
                [reagent            "1.2.0"]]
...

thheller13:06:12

ok that mix of stuff is already extremely suspicious

Apple13:06:13

clj -M:dev:cider
2024-06-15 08:37:08,523 [main] DEBUG org.jboss.logging - Logging Provider: org.jboss.logging.Slf4jLoggerProvider 
2024-06-15 08:37:20,728 [main] INFO  org.xnio - XNIO version 3.8.8.Final 
2024-06-15 08:37:20,829 [main] DEBUG org.xnio - Creating worker:null, pool size:64, max pool size:64, keep alive:60000, io threads:8, stack size:0 
2024-06-15 08:37:20,842 [main] INFO  org.jboss.threads - JBoss Threads version 3.5.0.Final 
shadow-cljs - server version: 2.28.8 running at 

thheller13:06:28

ok, that appears to be fine then

thheller13:06:43

just not sure why you pasted deps.edn clj output, but shadow-cljs.edn not being setup to use it

thheller13:06:25

:source-paths and :dependencies in shadow-cljs.edn do absolutely nothing if you are using deps.edn

Apple13:06:58

it's part of what kit-clj provides and i didnt touch it

Apple14:06:35

well except for the :source-paths

Apple14:06:43

i added cljc dir to it

Apple14:06:57

i launch shadow from user.clj but thought shadow still looks up shadow-cljs.edn for settings. no?

Apple14:06:12

shadow-cljs.edn is not using deps.edn i think

Apple14:06:50

im not entirely sure but the setup works

thheller14:06:03

it looks up shadow-cljs.edn for settings correct. just not :source-paths or :dependencies since deps.edn is in charge of those when running via clj

Apple14:06:25

oh... ok...

Apple14:06:02

git diff f8565dc63468df997e9e41b9ea28834675b45a88..3d81bff7b3fd907b9d38d8e2d7d8b8fe54381363 -- src/main/shadow/cljs/devtools/server/repl_impl.clj
diff --git a/src/main/shadow/cljs/devtools/server/repl_impl.clj b/src/main/shadow/cljs/devtools/server/repl_impl.clj
index a0f7a848..e0053260 100644
--- a/src/main/shadow/cljs/devtools/server/repl_impl.clj
+++ b/src/main/shadow/cljs/devtools/server/repl_impl.clj
@@ -89,7 +89,7 @@
         (loop []
           ;; wait until told to read
           (when (some? (<!! read-lock))
-            (let [{:keys [eof?] :as next} (repl/dummy-read-one input-stream)]
+            (let [{:keys [eof?] :as next} (repl/dummy-read-one input-stream {:read-cond :allow :features #{:cljs}})]
               (if eof?
                 (async/close! stdin)
                 ;; don't recur in case stdin was closed while in blocking read
@@ -210,7 +210,7 @@
                                (recur)))
 
                        (do (>!! to-relay
-                             {:op :obj-as-str
+                             {:op :obj-str
                               :to from
                               :oid ex-oid})

Apple14:06:20

i didnt expect the 89,7 portion

Apple14:06:00

it's a diff between 2.28.8 and the latest

thheller14:06:50

oh yeah I forgot about that change

thheller14:06:59

forgot that 2.28.9 is the latest 😛

thheller14:06:14

guess it fails internally then because it tried to call a function with an invalid arity

thheller14:06:24

kinda concerned why you are not seeing that error anywhere

thheller14:06:34

but cider is known to hide some other relevant stuff

thheller14:06:46

could be hidden in some emacs buffer you didn't look at

Apple14:06:17

i will try 2.28.9 and the patch

thheller14:06:57

just wait for proper release

Apple14:06:51

2.28.9 plus the patch. it's good.

Apple14:06:19

user> (re-matches #"." nil)
Execution error (TypeError) at (:1).
re-matches must match against a string.
:repl/exception!
user> (map #(re-matches #"." %) (repeat 1000 nil))
The result object failed to print. It is available via *1 if you want to interact with it.
The exception was: 
TypeError: re-matches must match against a string.
:repl/print-error!
user> *1
The result object failed to print. It is available via *1 if you want to interact with it.
The exception was: 
TypeError: re-matches must match against a string.
:repl/print-error!
user> 

thheller14:06:28

FWIW the problem is due to the code being lazy

Apple14:06:59

user> (doall (map #(re-matches #"." %) (repeat 1000 nil)))

Execution error (TypeError) at (<cljs repl>:1).
re-matches must match against a string.
:repl/exception!

thheller14:06:01

so first the eval happens, which completes without error. then it attempts to print on a subsequent steps, which then blows up.

thheller14:06:02

I might just do an implicit doall or first for lazy seq results so that the error happens properly during the eval phase

thheller14:06:27

regular REPLs sort of combine eval+print but shadow-cljs keeps them separate

Apple14:06:16

just dont crash 🙂

Apple14:06:47

i launch shadow from clj proj to save one jvm instance

thheller14:06:01

that is fine