nREPL middleware that enables the bootstrap of a ClojureScript REPL on top of an nREPL session.
Are you just getting started with ClojureScript or using ClojureScript
in a REPL? You should almost certainly be starting with
Austin; it uses Piggieback, but wraps it
up with a bunch of helpful utilities, auto-configuration of your project.clj
,
and other goodies.
The default ClojureScript REPL (as described in the "quick start" tutorial) requires/assumes that it is running in a terminal environment; specifically:
- that code to be evaluated is provided via
*in*
, and that results are - primarily printed to
*out*
that every REPL evaluation is performed in the - same thread as that which started the REPL
nREPL does not provide for such things, and so starting the default
ClojureScript REPL in an nREPL session yields a massive pile of No Context associated with current Thread
errors, and resulting exceptions attempting to
evaluate core.cljs
and co.
Piggieback provides an alternative ClojureScript REPL entry point
(cemerick.piggieback/cljs-repl
) that lifts a ClojureScript REPL on top of any
nREPL session, while accepting all the same options as cljs.repl/repl
.
Piggieback is available in Maven Central. Add this :dependency
to your
Leiningen project.clj
:
[com.cemerick/piggieback "0.1.5"]
Or, add this to your Maven project's pom.xml
:
<dependency>
<groupId>com.cemerick</groupId>
<artifactId>piggieback</artifactId>
<version>0.1.5</version>
</dependency>
Piggieback is compatible with Clojure 1.5.1+, and requires ClojureScript
0.0-2665
or later.
Refer to "Compatibility Notes" to see if your preferred environment has known issues with Piggieback.
Available @ CHANGES.md.
Piggieback is nREPL middleware, so you need to add it to your nREPL server's middleware stack.
If you're using Leniningen v2.0+, you can add this to your project.clj
to
automagically mix the Piggieback middleware into the stack that lein repl
will
use when starting nREPL:
:repl-options {:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}
If you're not starting nREPL through Leiningen (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 '[clojure.tools.nrepl.server :as server]
'[cemerick.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" something that can evaluate code, like
clojure.tools.nrepl.middleware.interruptible-eval/interruptible-eval
. - Piggieback depends upon persistent REPL sessions, like those provided by
clojure.tools.nrepl.middleware.session/session
.)
With Piggieback added to your nREPL middleware stack, you can start a ClojureScript REPL from any nREPL-capable client (e.g. Leiningen, REPL-y, Counterclockwise, nrepl.el, and so on).
So, with Leiningen, and using the Rhino ClojureScript environment (the default
that you get when you e.g. run the script/repljs
script in the ClojureScript
source tree, or follow the 'Quick Start' ClojureScript
tutorial):
la-mer:piggieback chas$ lein2 repl
nREPL server started on port 56393
REPL-y 0.1.0-beta10
Clojure 1.4.0
Exit: Control+D or (exit) or (quit)
Commands: (user/help)
Docs: (doc function-name-here)
(find-doc "part-of-name-here")
Source: (source function-name-here)
(user/sourcery function-name-here)
Javadoc: (javadoc java-object-or-class-here)
Examples from clojuredocs.org: [clojuredocs or cdoc]
(user/clojuredocs name-here)
(user/clojuredocs "ns-here" "name-here")
user=> (cemerick.piggieback/cljs-repl)
Type `:cljs/quit` to stop the ClojureScript REPL
nil
cljs.user=> (+ 1 1)
2
cljs.user=> (defn <3 [a b] (str a " <3 " b "!"))
#<
function _LT_3(a, b) {
return [cljs.core.str(a), cljs.core.str(" <3 "), cljs.core.str(b), cljs.core.str("!")].join("");
}
>
nil
cljs.user=> (<3 "nREPL" "ClojureScript")
"nREPL <3 ClojureScript!"
cljs.user=>
Note that the REPL prompt changed after invoking
cemerick.piggieback/cljs-repl
; after that point, all expressions sent to the
REPL are evaluated within the ClojureScript environment. Of course, you can
concurrently take advantage of all of nREPL's other facilities, including
connecting to the server with other clients (so as to easily modify Clojure and
ClojureScript code in 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)
clojure.tools.nrepl.middleware.interruptible_eval$interruptible_eval$fn__374.invoke(interruptible_eval.clj:185)
cemerick.piggieback$wrap_cljs_repl$fn__2535.invoke(piggieback.clj:171)
clojure.tools.nrepl.middleware.pr_values$pr_values$fn__390.invoke(pr_values.clj:17)
clojure.tools.nrepl.middleware.session$add_stdin$fn__451.invoke(session.clj:185)
clojure.tools.nrepl.middleware.session$session$fn__444.invoke(session.clj:164)
clojure.tools.nrepl.server$handle_STAR_.invoke(server.clj:16)
clojure.tools.nrepl.server$handle.invoke(server.clj:25)
clojure.tools.nrepl.server$accept_connection$fn__458.invoke(server.clj:35)
clojure.core$binding_conveyor_fn$fn__3989.invoke(core.clj:1819)
clojure.lang.AFn.call(AFn.java:18)
java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:303)
java.util.concurrent.FutureTask.run(FutureTask.java:138)
java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
java.lang.Thread.run(Thread.java:680)
InterruptedException java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly (AbstractQueuedSynchronizer.java:1199)
cljs.user=> (<3 "nREPL still" "ClojureScript")
"nREPL still <3 ClojureScript!"
(The ugly ThreadDeath
exception will be eliminated eventually.)
A Node.js REPL environment implementation is included in ClojureScript startingn with version 2665. You can use this via nREPL and Piggieback, too:
user=> (require '[cljs.repl :as repl])
nil
user=> (require '[cljs.repl.node :as node])
nil
user=> (cemerick.piggieback/cljs-repl
:repl-env (node/repl-env)
:output-dir ".cljs_node_repl"
:cache-analysis true
:source-map true)
ClojureScript Node.js REPL server listening on 54718
Type `:cljs/quit` to stop the ClojureScript REPL
nil
cljs.user=> (.-version js/process)
"v0.10.25"
Piggieback can be used with other ClojureScript REPL environments as well, such
as browser-repl. For example, start with this index.html
file in your project
(by the way, nearly all of this is cribbed from the browser REPL
tutorial:
<html>
<head>
<meta charset="UTF-8">
<title>Browser-connected REPL through nREPL with Piggieback</title>
</head>
<body>
<div id="content">
<script type="text/javascript" src="out/goog/base.js"></script>
<script type="text/javascript" src="piggieback_browser.js"></script>
<input type="text" name="afield" id="afield"/>
<script type="text/javascript">
goog.require('piggieback_browser');
</script>
</div>
</body>
</html>
Now create piggieback_browser.cljs
:
(ns piggieback-browser
(:require [clojure.browser.repl :as repl]))
(repl/connect "http://localhost:9000/repl")
…and compile it:
$CLOJURESCRIPT/bin/cljsc piggieback_browser.cljs > piggieback_browser.js
Now start your ClojureScript REPL through Piggieback; but, this time, use the browser-repl environment instead of the Rhino default:
(require 'cljs.repl.browser)
(cemerick.piggieback/cljs-repl
:repl-env (cljs.repl.browser/repl-env :port 9000))
Now go to http://localhost:9000; note that you must
access the index.html
page you created earlier through the same server that it
will connect to the browser-repl through due to the JavaScript "same-origin"
policy. Doing anything else (such as opening the index.html
file from disk
directly) will not work, and will probably result in your ClojureScript/nREPL
session being hung permanently with no feedback.
Once the browser-repl is connected, we evaluate ClojureScript expressions in the browser:
=> (set! (.-value (goog.dom/getElement "afield")) "it works!")
"it works!"
=> (.-value (goog.dom/getElement "afield"))
"it works!"
…and so on.
(Huge thanks to Nelson Morris for doing the initial experimentation with Piggieback + browser-repl, and for helping me puzzle out all sorts of ClojureScript difficulty I had along the way.)
Note that Piggieback (and all other ClojureScript REPLs, for that matter)
is incompatible with ac-nrepl.
ac-nrepl implements its code completion backend by evaluating Clojure-specific
expressions to interrogate the runtime. When you have a ClojureScript REPL
hooked up via Piggieback, these expressions end up pelting the ClojureScript
compiler, which doesn't appreciate it (as you might imagine). The result can
be odd "hangs" of the REPL, information-free StackOverflow
exceptions, and
other ills.
Send a message to the
clojure-tools mailing list, or
ping cemerick
on freenode irc or twitter if you
have questions or would like to contribute patches.
Copyright © 2012-2013 Chas Emerick and other contributors.
Distributed under the Eclipse Public License, the same as Clojure.