Awesome
Piggieback
nREPL middleware that enables the use of a ClojureScript REPL on top of an nREPL session.
Why?
Two reasons:
-
The default ClojureScript REPL (as described in the "quick start" tutorial) assumes that it is running in a teletype environment. This works fine with nREPL tools in that environment (e.g.
lein repl
inTerminal.app
orgnome-terminal
, etc), but isn't suitable for development environments that have richer interaction models (including editors like vim (vim-fireplace) and Emacs (CIDER), and IDEs like Intellij (Cursive) and Eclipse (Counterclockwise)). -
Most of the more advanced tool support for Clojure and ClojureScript (code completion, introspection and inspector utilities, refactoring tools, etc) is packaged and delivered as nREPL extensions (e.g. cider-nrepl and refactor-nrepl).
Piggieback provides an alternative ClojureScript REPL entry point
(cider.piggieback/cljs-repl
) that changes an nREPL session into a
ClojureScript REPL for eval
and load-file
operations, while accepting all
the same options as cljs.repl/repl
. When the ClojureScript REPL is terminated
(by sending :cljs/quit
for evaluation), the nREPL session is restored to it
original state.
Installation
Piggieback is compatible with Clojure 1.8.0+, and requires ClojureScript
1.9
or later and nREPL 0.6.0
or later.
To use the default Node.js REPL (cljs.repl.node
) you'll also need to install a recent version of Node.js.
Leiningen
These instructions are for Leiningen. Translating them for use in Boot should be straightforward.
Modify your project.clj
to include the following :dependencies
and
:repl-options
:
:profiles {:dev {:dependencies [[cider/piggieback "0.5.3"]]
:repl-options {:nrepl-middleware [cider.piggieback/wrap-cljs-repl]}}}
The :repl-options
bit causes lein repl
to automagically mix the Piggieback
nREPL middleware into its default stack.
If you're using Leiningen directly, or as the basis for the REPLs in your local development environment (e.g. CIDER, fireplace, counterclockwise, etc), you're done. Skip to starting a ClojureScript REPL.
Boot
Contributions welcome!
Clojure CLI (aka tools.deps
)
The instructions below require nREPL 0.6.0 or newer
Add this alias to ~/.clojure/deps.edn
:
{
;; ...
:aliases {:nrepl
{:extra-deps
{nrepl/nrepl {:mvn/version "0.8.3"}
cider/piggieback {:mvn/version "0.5.3"}}}}
}
Then you can simply run a ClojureScript-capable nREPL server like this:
clj -R:nrepl -m nrepl.cmdline --middleware "[cider.piggieback/wrap-cljs-repl]"
When you connect to the running server with your favourite nREPL client (e.g. CIDER), you will be greeted by a Clojure REPL. Within this Clojure REPL, you can now start a ClojureScript REPL.
Embedded
If you're not starting nREPL through a build tool (e.g. maybe you're starting up
an nREPL server from within an application), you can achieve the same thing by
specifying that the wrap-cljs-repl
middleware be mixed into nREPL's default
handler:
(require '[nrepl.server :as server]
'[cider.piggieback :as pback])
(server/start-server
:handler (server/default-handler #'pback/wrap-cljs-repl)
; ...additional `start-server` options as desired
)
Alternatively, you can add wrap-cljs-repl
to your application's hand-tweaked
nREPL handler. Keep two things in mind when doing so:
- Piggieback needs to be "above" nREPL's
nrepl.middleware.interruptible-eval/interruptible-eval
; it doesn't useinterruptible-eval
's evaluation machinery, but it does reuse its execution queue and thus inherits its interrupt capability. - Piggieback depends upon persistent REPL sessions, like those provided by
nrepl.middleware.session/session
.)
Usage
Before you run the following, you must have gone through the setup
steps. Instead of using lein repl
, you might also connect to a
headless nREPL using your development environment.
$ lein repl
....
user=> (require 'cljs.repl.node)
nil
user=> (cider.piggieback/cljs-repl (cljs.repl.node/repl-env))
To quit, type: :cljs/quit
nil
cljs.user=> (defn <3 [a b] (str a " <3 " b "!"))
#<
function cljs$user$_LT_3(a, b) {
return [cljs.core.str(a), cljs.core.str(" <3 "), cljs.core.str(b), cljs.core.str("!")].join("");
}
>
cljs.user=> (<3 "nREPL" "ClojureScript")
"nREPL <3 ClojureScript!"
See how the REPL prompt changed after invoking
cider.piggieback/cljs-repl
? After that point, all expressions sent to the
REPL are evaluated within the ClojureScript environment.
cider.piggieback/cljs-repl
's passes along all of its options to
cljs.repl/repl
, so all of the tutorials and documentation related to it hold.
Important Notes
- When using Piggieback to enable a browser REPL: the ClojureScript compiler
defaults to putting compilation output in
out
, which is probably not where your ring app is serving resources from (resources
,target/classes/public
, etc). Either configure your ring app to serve resources fromout
, or pass acljs-repl
:output-dir
option so that a reasonable correspondence is established. - The
load-file
nREPL operation will only load the state of files from disk. This is in contrast to "regular" Clojure nREPL operation, where the current state of a file's buffer is loaded without regard to its saved state on disk.
Of course, you can concurrently take advantage of all of nREPL's other facilities, including connecting to the same nREPL server with other clients (so as to easily modify Clojure and ClojureScript code via the same JVM), and interrupting hung ClojureScript invocations:
cljs.user=> (iterate inc 0)
^C
cljs.user=> "Error evaluating:" (iterate inc 0) :as "cljs.core.iterate.call(null,cljs.core.inc,0);\n"
java.lang.ThreadDeath
java.lang.Thread.stop(Thread.java:776)
....
cljs.user=> (<3 "nREPL still" "ClojureScript")
"nREPL still <3 ClojureScript!"
(The ugly ThreadDeath
exception will be eliminated eventually.)
Piggieback works well with all known ClojureScript REPL environments, including Node and browser REPLs.
Support for Rhino was dropped in version 0.3, and Nashorn support was dropped from ClojureScript in 1.10.741.
Design
This section documents some of the main design decisions in Piggieback and the differences between similar functionality in nREPL and Piggieback.
Perhaps the most important thing to remember is that Piggieback is written in
Clojure and runs on Clojure. It drives ClojureScript evaluation by using
ClojureScript's Clojure API (cljs.repl/IJavaScriptEnv
). This allows you to
host both Clojure and ClojureScript evaluation sessions on the same nREPL
server, which is pretty cool. On the other hand it also means that you can't use
Piggieback with self-hosted ClojureScript REPLs (e.g. Lumo).
Note: For self-hosted ClojureScript you'll need an nREPL implementation that can run natively on it (e.g. nrepl-cljs).
No hard dependency on ClojureScript
Piggieback doesn't have a hard dependency on ClojureScript, as users are
expected to provide the necessary ClojureScript dependency themselves. If
ClojureScript is not present, Piggieback simply won't do anything (see
piggieback_shim.clj
for details).
This allows tools to safely load Piggieback without having to consider whether something would blow up.
Session type based dispatch
Clients don't have to specify explicitly whether they are doing a ClojureScript eval
operation (e.g. by passing some :env :cljs
request params). As Piggieback operates
at the nREPL session level all clients need to do is to pass a Piggieback session
to ops like eval
and that would trigger the Piggieback version of those ops.
Evaluation
As noted above Piggieback provides alternative versions of the standard nREPL
ops eval
and load-file
for ClojureScript evaluation. Due to some differences
between Clojure and ClojureScript they don't behave exactly the same.
Most notably - for performance reasons we don't spin separate instances of cljs.repl
for each evaluation, as nREPL does for Clojure. In practice this means that if you try
to evaluate multiple forms together only the first of them would be evaluated:
;; standard ClojureScript REPL behaviour
cljs.user>
(declare is-odd?)
(defn is-even? [n] (if (= n 0) true (is-odd? (dec n))))
(defn is-odd? [n] (if (= n 0) false (is-even? (dec n))))
#'cljs.user/is-odd?
#'cljs.user/is-even?
#'cljs.user/is-odd?
cljs.user> (is-even? 4)
true
Let's compare this to a REPL powered by Piggieback:
cljs.user>
(declare is-odd?)
(defn is-even? [n] (if (= n 0) true (is-odd? (dec n))))
(defn is-odd? [n] (if (= n 0) false (is-even? (dec n))))
#'cljs.user/is-odd?
cljs.user> (is-even? 4)
Compile Warning <cljs repl> line:1 column:2
Use of undeclared Var cljs.user/is-even?
1 (is-even? 4)
^---
#object[TypeError TypeError: Cannot read property 'call' of undefined]
(<NO_SOURCE_FILE>)
cljs.user>
Normally that's not a big deal in practice, as you'd rarely want to evaluate multiple expressions together, but it's something to be kept in mind.
Note: Check out this discussion for more details on the subject.
Pretty-printing
Note: Piggieback introduced support for nREPL's pretty-printing interface in version 0.5.
Support for pretty printing ClojureScript evaluation results is not
entirely straightforward. This is because Piggieback mostly relies on
the underlying nREPL server implementation to support the features of
the nREPL protocol and on the cljs.repl/IJavaScriptEnv
interface for
ClojureScript evaluation.
nREPL 0.6 introduced nrepl.middleware.print
to facilitate printing
evaluation results in a configurable way. Since nREPL is implemented
in Clojure and runs on the JVM, the middleware relies on receiving
Clojure values for printing them. Conversely when evaluating a
ClojureScript expression in a JavaScript environment, the resulting
Clojure value of the evaluation is always a string. If this value
would simply be passed on as is to the middleware, only the string
itself could be printed by it instead of the evaluation result within
the string.
There are multiple approaches for working around this issue with various trade-offs. The current implementation has the following main considerations:
-
nrepl.middleware.print
is used to print ClojureScript evaluation results whenever possible, so that the same nREPL (pretty) printing configuration is applied to both Clojure and ClojureScript. -
For cases where the above is not possible (see below), there is a fallback to support basic pretty printing.
In order to support nrepl.middleware.print
for ClojureScript
evaluation results, they first need to be read. The resulting
Clojure values can then be normally printed by the middleware. However
there are various cases where ClojureScript evaluation results can not
be read by the default Clojure reader. Some examples:
- Functions:
#object[Function]
- Objects:
#object[cljs.user.Cheese]
,#object[Window [object Window]]
#js
literals:#js {:foo 1, :bar 2}
#queue
literals:#queue [1 2 3]
- Custom tagged literals:
#user/cheese "Pálpusztai"
- Types implementing
IPrintWithWriter
in a way that is incompatible with the Clojure reader
To work around some of these cases Piggieback provides its own
UnknownTaggedLiteral
type. It is used as the default tag reader when
reading ClojureScript evaluation results. It doesn't parse the
contents of the literal and has print-method
defined to simply print
the original.
Note: When a pretty-printer which doesn't rely on print-method
to
serialize values (such as fipp, puget, etc.) is used,
UnknownTaggedLiteral
will be serialized in the output instead of the
original literal.
There are still cases left which can prevent the Clojure reader from
successfully reading ClojureScript evaluation results (mostly custom
IPrintWithWriter
implementations). In order to support pretty
printing these results as well, the ClojureScript expression to be
evaluated is always wrapped with cljs.pprint/pprint
(unless
:nrepl.middleware.print/print
is set to nrepl.util.print/pr
or cider.nrepl.pprint/pr
, in
which case cljs.core/pr
is used instead). This means that whenever
the Clojure reader fails to read the value for any reason, we can
safely fall back on an already (pretty) printed string, albeit
disabling nrepl.middleware.print
and hence effectively ignoring the
nrepl.middleware.print
configuration. Special care is taken that
output written to *out*
during evaluation is not affected by the
wrapping.
For the cases where the (pretty) printing configuration is not being applied, the reader probably failed to read the evaluation results and the above fallbacks are being used instead.
Note: See this pull request for more background and discussion on the current solution.
FAQ
Why "piggieback" instead of "piggyback"?
That's one of life's greatest mysteries. Only Chas can answer that one.
Why is the artifact group id "cider" instead of "nrepl"?
Bozhidar took over the maintenance of Piggieback before taking over the maintenance of nREPL. That's why for a period of time Piggieback lived under CIDER's GitHub org and back then it made sense to use CIDER's group id. Eventually, it got reunited with nREPL, but we've opted to preserve the CIDER group id to avoid further breakages.
For the same reason the main namespace is cider.piggieback
instead of
nrepl.piggieback
.
Does Piggieback work with self-hosted ClojureScript REPLs (e.g. Lumo)?
No, it doesn't. Piggieback is implemented in Clojure and relies on Clojure's ClojureScript evaluation
API (cljs.repl/IJavaScriptEnv
).
For self-hosted ClojureScript you'll need a native ClojureScript nREPL implementation like nrepl-cljs.
Does shadow-cljs use Piggieback?
No, it doesn't use it. It's most recommended for shadow-cljs users to avoid including the cider.piggieback/wrap-cljs-repl
middleware.
Unlike figwheel
, which relies on Piggieback, shadow-cljs
provides
its own nREPL middleware. That's why some features of Piggieback (e.g. pretty-printing)
might not be available with shadow-cljs
.
You can find shadow-cljs
's middleware here.
Need Help?
Feel free to create a Github issue or ask on #cider
on Clojurians Slack if you
have questions or would like to contribute patches.
Acknowledgements
Nelson Morris was instrumental in the initial development of piggieback.
License
Copyright © 2012-2023 Chas Emerick, Bruce Hauman, Bozhidar Batsov and other contributors.
Distributed under the Eclipse Public License, the same as Clojure.