shadow-cljs as the ClojureScript REPL RUNTIME: attaching to the nREPL a
shadow-cljs watch already runs, SELECTING the build whose JS runtime an eval
must land in, and keeping that selection true for every later eval.
Five facts about that server decide everything here, and every one of them was learned from a live shadow-cljs, not from its wire protocol:
describe: it advertises no cljs/shadow op and reports only a :clojure
version, so dialect detection from describe metadata answers :clj for it.
The only honest question is a read-only eval that RESOLVES
shadow.cljs.devtools.api inside the server.(shadow…api/nrepl-select :app) —
and ONE session serves EVERY build of that server, so a selection belongs
to the CONNECTION and never to an attachment: selecting :worker makes
the next eval that believes it targets :app answer from the worker's
runtime, silently and with no error. Every eval therefore re-checks both
the session and the build selected in it, never a boolean.No such namespace: shadow.cljs.devtools.api. :cljs/quit first is what makes selection
total: it returns such a session to CLJ, and in a session that never left
CLJ it is an ordinary keyword evaluating to itself.nrepl-select answers the SAME watch for build not running whether the
build id does not exist or is merely not being watched. The two are told
apart BEFORE selecting — by the server's own build config and
worker-running? — because they need opposite fixes.No available JS runtime. on :err with a done
status. Nothing but a started runtime fixes it, so that answer is turned
into the instruction that starts one.shadow-cljs as the ClojureScript REPL RUNTIME: attaching to the nREPL a `shadow-cljs watch` already runs, SELECTING the build whose JS runtime an eval must land in, and keeping that selection true for every later eval. Five facts about that server decide everything here, and every one of them was learned from a live shadow-cljs, not from its wire protocol: 1. A shadow-cljs nREPL is INDISTINGUISHABLE from a plain JVM one over `describe`: it advertises no cljs/shadow op and reports only a `:clojure` version, so dialect detection from describe metadata answers `:clj` for it. The only honest question is a read-only eval that RESOLVES `shadow.cljs.devtools.api` inside the server. 2. The build is selected PER nREPL SESSION — `(shadow…api/nrepl-select :app)` — and ONE session serves EVERY build of that server, so a selection belongs to the CONNECTION and never to an attachment: selecting `:worker` makes the next eval that believes it targets `:app` answer from the worker's runtime, silently and with no error. Every eval therefore re-checks both the session and the build selected in it, never a boolean. 3. Selecting from a session ALREADY sitting in a build FAILS — the select form is compiled as ClojureScript and dies on `No such namespace: shadow.cljs.devtools.api`. `:cljs/quit` first is what makes selection total: it returns such a session to CLJ, and in a session that never left CLJ it is an ordinary keyword evaluating to itself. 4. `nrepl-select` answers the SAME `watch for build not running` whether the build id does not exist or is merely not being watched. The two are told apart BEFORE selecting — by the server's own build config and `worker-running?` — because they need opposite fixes. 5. A selected build with NO connected JS runtime neither errors nor hangs: every eval answers `No available JS runtime.` on `:err` with a `done` status. Nothing but a started runtime fixes it, so that answer is turned into the instruction that starts one.
(eval! {:keys [host port build target]} eval-opts)Evaluate in build's JS runtime over the shadow-cljs nREPL at host:port.
attachment is {:host :port :build :target}. The build is (re)SELECTED
whenever selected? does not already report it for that connection: a
replaced session (an evicted socket, a restarted watch) would otherwise
silently answer as JVM Clojure, and a SIBLING build selected on the same
server would answer from the wrong runtime without erroring at all. When it
is already selected the hot path stays ONE round trip.
Answers {:selected? true :result <nrepl-client/eval! map>}, plus :message
when the build has no runtime; a build that can no longer be selected answers
{:selected? false :message …} and evaluates NOTHING.
Evaluate in `build`'s JS runtime over the shadow-cljs nREPL at `host:port`.
`attachment` is `{:host :port :build :target}`. The build is (re)SELECTED
whenever `selected?` does not already report it for that connection: a
replaced session (an evicted socket, a restarted watch) would otherwise
silently answer as JVM Clojure, and a SIBLING build selected on the same
server would answer from the wrong runtime without erroring at all. When it
is already selected the hot path stays ONE round trip.
Answers `{:selected? true :result <nrepl-client/eval! map>}`, plus `:message`
when the build has no runtime; a build that can no longer be selected answers
`{:selected? false :message …}` and evaluates NOTHING.The exact stderr shadow-cljs emits for EVERY eval on a build whose runtime is not connected. Its own text, matched as a substring — never re-worded here.
The exact stderr shadow-cljs emits for EVERY eval on a build whose runtime is not connected. Its own text, matched as a substring — never re-worded here.
(no-runtime? result)True when this eval result is shadow-cljs saying the build has no JS runtime
attached — a done reply whose stderr carries shadow's own marker, never an
exception and never a timeout.
True when this eval result is shadow-cljs saying the build has no JS runtime attached — a `done` reply whose stderr carries shadow's own marker, never an exception and never a timeout.
(nrepl-port dir)The nREPL port a shadow-cljs watch running in dir published, or nil.
ONE deterministic file, written by the user's OWN server, under the directory the caller explicitly named — this is not port discovery: nothing is scanned, no other server can answer, and with no watch running there is no file and the caller is told to start one.
The nREPL port a `shadow-cljs watch` running in `dir` published, or nil. ONE deterministic file, written by the user's OWN server, under the directory the caller explicitly named — this is not port discovery: nothing is scanned, no other server can answer, and with no watch running there is no file and the caller is told to start one.
Where shadow-cljs watch publishes its nREPL port, relative to the project.
Where `shadow-cljs watch` publishes its nREPL port, relative to the project.
(probe! {:keys [host port build timeout-ms]})What the nREPL at host:port IS, in one read-only eval:
{:shadow? true :builds ["npm" "app"] :worker? true :target :node-script}
:builds are the ids the SERVER loaded (its own config is authoritative — the
caller's cwd may not be where it runs); :worker? is whether build has a
live watch; :target is that build's shadow target. A plain JVM nREPL
answers :shadow? false, and an unreachable one adds :error.
What the nREPL at `host:port` IS, in one read-only eval:
{:shadow? true :builds ["npm" "app"] :worker? true :target :node-script}
`:builds` are the ids the SERVER loaded (its own config is authoritative — the
caller's `cwd` may not be where it runs); `:worker?` is whether `build` has a
live `watch`; `:target` is that build's shadow target. A plain JVM nREPL
answers `:shadow? false`, and an unreachable one adds `:error`.(runtime-hint build target)What actually starts the missing runtime, phrased for THIS build's target.
The watch stays running either way — the runtime is the second process.
What actually starts the missing runtime, phrased for THIS build's `target`. The watch stays running either way — the runtime is the second process.
(select! {:keys [host port build timeout-ms]})Select build in the nREPL session nrepl-client/eval! reuses for host:port,
by evaluating shadow's own nrepl-select IN that session — so every later eval
through the same session lands in that build's JS runtime.
TOTAL whatever that session currently is: :cljs/quit goes first, returning a
session that already sits in a build to CLJ, and evaluating to itself in one
that never left it. Without it a re-select is compiled AS ClojureScript and
dies on No such namespace: shadow.cljs.devtools.api.
Answers {:selected? true} — recording the selection for selected? — or
{:selected? false :message …} with the server's own reason.
Select `build` in the nREPL session `nrepl-client/eval!` reuses for `host:port`,
by evaluating shadow's own `nrepl-select` IN that session — so every later eval
through the same session lands in that build's JS runtime.
TOTAL whatever that session currently is: `:cljs/quit` goes first, returning a
session that already sits in a build to CLJ, and evaluating to itself in one
that never left it. Without it a re-select is compiled AS ClojureScript and
dies on `No such namespace: shadow.cljs.devtools.api`.
Answers `{:selected? true}` — recording the selection for `selected?` — or
`{:selected? false :message …}` with the server's own reason.(selected? {:keys [host port build]})True when build is the build this process has selected in the session
nrepl-client/eval! will reuse for host:port — the SAME session AND the
SAME build. A live session token alone is not enough: every build on one
shadow-cljs server shares that session, so a sibling build's selection owns
this one's next eval.
True when `build` is the build this process has selected in the session `nrepl-client/eval!` will reuse for `host:port` — the SAME session AND the SAME build. A live session token alone is not enough: every build on one shadow-cljs server shares that session, so a sibling build's selection owns this one's next eval.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |