shadow-cljs 2025-06-15

I'm switching into using shadow-cljs from my dev/user.clj with the api (trying to get rid of node). I got most of my hints from this article: https://code.thheller.com/blog/shadow-cljs/2024/10/18/fullstack-cljs-workflow-with-shadow-cljs.html At some point it is recommended to add {:shadow/requires-server true} to the start fn that calls the watch, but the user guide says Commands that do long-running things implicitly start a server instance (eg. watch) but it is often advisable to have a dedicated server process running. I noticed that even without the requires-server hint the watch works, what's the catch? My simple setup is a repl started with clj -M:dev:test:repl/headless:cljs where the :cljs alias is:

:cljs {:extra-paths ["src/cljs"]
         :extra-deps {thheller/shadow-cljs {:mvn/version "3.1.6"}}
         :main-opts ["-m" "shadow.cljs.devtools.cli" "clj-repl"]}
After that I just run in my dev/user.clj a simple (shadow.cljs.devtools.api/watch :app) manually by hand

this is where I'm currently at with this whole setup

so you run clj -M:dev:start

that then starts the shadow-cljs server

dunno what you are doing but {:shadow/requires-server true} would be required if you are never starting a server someplace else

but only if actually going through shadow.cljs.devtools.cli, which my stuff doesn't do (anymore)

many ways to do this, kinda depends on personal preference

Hmm I suppose I could get rid of the :main-opts ["-m" "shadow.cljs.devtools.cli" "clj-repl"] and get a normal repl, that then drives the dev processes using the shadow.cljs.devtools.api

It seems I was trying to do the same thing as the examples you posted above, but with extra steps 🙂

I suppose your article above gives a little bit outdated advice 😛

it still all applies just fine

What is your preferred way of managing other long running subprocesses along the shadow-cljs watch?

E.g. If I wanted to run the tailwindcss watch process alongside the shadow-cljs watch process, how would you do it?

same way that does 😛

this is great

I can't thank you enough

With shadow-cljs api, replicant and a good skeleton I might actually arrive at a point that having a single repo full-stack clj/cljs app doesn't feel heavily glued with lots of external tools

nothing wrong with external tools built to glue stuff together if you ask me 😛

In the past I did use npm run-p to manage shadow-cljs and tailwindcss processes

but recovering from the tailwindcss crashes was a pain

Also I prefer a lot to manage my whole codebase just with clojure

I do too, but usually that means more work 😛

One more question: does the :dev-http server start with the watch command or with the server command?

(server/start!) as :dev-http is not tied to any particular build and always runs regardless of watch

thanks again for the clarifications

Sorry again, I have one more question about your mpv example: what does requiring [shadow.user] namespace do in the repl ns?

for this starts a REPL for the process you are starting itself

not sure I have actually yet used that for anything, but the intent was to have a REPL for the command line process

and I wanted that to switch to the shadow.user namespace

you can adjust that however you like, it doesn't do anything "required". if you don't feel like using that just remove it

but the in-ns there requires the namespace to be already loaded

In my case that I use the repl headless (I just connect to it from my editor (nvim with conjure)) I suppose its kinda redundant

then you can skip that entire thing yeah

there have been times where I wanted to interact with the process without opening my editor first

but its rare 😛

should I also require cider/nrepl middleware to avoid :missing-nrepl-middleware?

if you intend to use that middleware it needs to be on the classpath, as in added to your dependencies

well do you actually call it over nrepl?

I'm guessing blindly, so this is kinda pointless

that is not what I asked 😛

you are calling nrepl-select somewhere

is that call actually triggered over an nrepl connection

if its called as part of start or whatever your setup has, then it isn't invoked over nrepl, thus will fail

ok so I start my repl using clj -M:dev:test:repl/headless:cljs where these aliases are the one you would expect:

{:dev {:extra-paths ["dev/clj"]}
  :test {:extra-paths ["test/clj"]
         :extra-deps {org.clojure/test.check {:mvn/version "1.1.1"}
                      io.github.cognitect-labs/test-runner {:git/tag "v0.5.1" :git/sha "dfb30dd"}}}
  :cljs {:extra-paths ["src/cljs"]
         :extra-deps {thheller/shadow-cljs {:mvn/version "3.1.6"}}}}}
(+ the :repl/headless alias above) I then connect to the normal clojure repl using neovim with conjure (same as all my projects). I eval the dev/user.clj namespace and call (start) by hand from my editor, that is over the nrepl connection). I was used to call ConjureShadowSelect (that calls shadow.cljs.devtools.api/nrepl-select https://github.com/Olical/conjure/blob/8edc78f7cf2cd49c0feba3ddd9a29eca1673a8ce/doc/conjure-client-clojure-nrepl.txt#L50) before opening to a cljs file to 'switch' my repl to a cljs one. I thought that starting the shadow-cljs server (and watch) was a clojure/host operation, do I understand something wrong?

you giving me partial information makes that kinda hard to answer 😛

i.e. thats a different :cljs alias than the one you posted above

which one is it?

ah yes, I changed it after your example 🙂

ok, so the shadow-cljs nrepl server will automatically add the cider nrepl middleware if its on the classpath

assuming you are actually connecting to the nrepl server shadow-cljs started then it should just work with no extra configuration

yes thats what I assumed too

also be aware that all code in dev/user.clj will ALWAYS run whenever ANY clojure process is started

so depending on how that file looks it may have unintended effects if you think that you triggering the load over the REPL is the "first" load

there is a reason I outlined this exact start/stop workflow in my post and that it wasn't in the user namespace

so, unless you provide me with more information I really cannot help you further

I am not starting anything automatically, so far my user.clj looks like this:

(ns user
  (:require
   [shadow.cljs.devtools.api :as shadow]
   [shadow.cljs.devtools.server :as shadow-server])
  (:import
   [java.lang ProcessBuilder ProcessBuilder$Redirect]))

(defn shadow-watch []
  (shadow-server/start!)
  (shadow/watch :app))

(defn css-watch []
  (-> ["npx" "tailwindcss" "-i" "src/css/site.css" "-o" "target/classes/public/css/main.css" "--watch"]
      (ProcessBuilder.)
      (.redirectError ProcessBuilder$Redirect/INHERIT)
      (.redirectOutput ProcessBuilder$Redirect/INHERIT)
      (.start)))

(defn watch []
  (shadow-watch))
  ;(css-watch))

(comment
  (watch))

I strongly recommend not using user, but otherwise that looks ok

After your above hint I'll probably move it to a dedicated ns too

where/how/what calls watch

I manually call watch from my editor

(over the nrepl connection)

well ... then something isn't quite right is it?

you are starting the shadow-cljs server in that shadow-watch. so its nrepl server won't be running beforehand? so how are you connected to the repl to call it in the first place? 😛

now I kinda understand

shadow/server starts its OWN nrepl server

which I assumed you are using? but guess not

and I call shadow-server/start from my nrepl connection, spawing another second nrepl connection (shadow-cljs one)

what for though? 😛

so thats the problem, I need to spawn the shadow-cljs server with its nrepl connection first and connect to THAT

or add the shadow-cljs nrepl middleware to the first

so that is why you have a main entrypoint for this

I do not know what you mean by this

so let me type real quick an overview of what I understand for the problem:

(main/repl :init repl-init) if you mean that line then no that has absolutely zero to do with it

if you mean (start) being called in -main then yes, because that starts shadow-cljs and the whole other development setup

I want that to start stuff when the process starts, not when I connect to the REPL

I start a normal clojure nrepl, I connect to it from my editor, and from that I spawn an shadow-cljs server+watch (by calling (watch) in the user ns). When the shadow-cljs server starts, it creates a specific nrepl connection itself that I am NOT connected into (I am connected into the nrepl connection of the process that started the shadow-cljs server through the api), so when I try to do nrepl-select I get the :missing-nrepl-middleware because I try to run it in the wrong nrepl connection

A better way would be instead of manually calling (watch) to create an entrypoint as you do, assign it to an alias, run the (watch) (and thus the shadow-server/start) before connecting with my editor

dunno why you'd make your life complicated like that but ok 😛

and then nrepl-select

shadow.cljs.devtools.server.nrepl/middleware this is the shadow-cljs middleware for nrepl, you need to add that to the first server you are starting, if you intend to switch the REPL in that nrepl connection

or just use one nrepl server 😛

(also, I do not try to make my life complicated, its all a consequence of incorrect understanding of how some of the above things work)

I think that I'll go with the one nrepl server route similar to what you have

Now everything is clear, thank you