Browser repl (#24)

This commit is contained in:
Michiel Borkent 2022-05-17 21:07:22 +02:00 committed by GitHub
parent 0d204c7ae6
commit 2a93334a43
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 208 additions and 129 deletions

View file

@ -13,6 +13,57 @@ for a minimal full stack web application.
See [releases](https://github.com/babashka/scittle/releases) for links to
[JSDelivr](https://www.jsdelivr.com) to get versioned artifacts.
## Serving assets
To serve assets you can use the
[babashka.http-server](https://github.com/babashka/http-server) dependency (with
babashka or Clojure JVM):
``` clojure
(require '[babashka.http-server :as http])
(http/serve {:port 1341 :dir "resoures/public"}
@(promise) ;; wait until process is killed
```
### REPL
To connect to a Scittle REPL from your editor, scittle provides an nREPL
implementation. To run the nREPL server you need to follow these steps:
In babashka or Clojure JVM, use the
[sci.nrepl](https://github.com/babashka/sci.nrepl) dependency and run:
```
(require 'sci.nrepl.browser-server :as nrepl)
(nrepl/start! {:nrepl-port 1339 :websocket-port 1340})
```
This will run an nREPL server on port 1339 and a websocket server on port 1340.
Your editor's nREPL client will connect to port 1339 and your browser, running
scittle, will connect to port 1340. The nREPL server forwards messages to the
browser via the websocket connection.
In your scittle website, you will need to include the following, in addition to
the normal routine:
```
<script>var SCITTLE_NREPL_WEBSOCKET_PORT = 1340;</script>
<script src="https://cdn.jsdelivr.net/npm/scittle@0.2.0/dist/scittle.nrepl.js" type="application/javascript"></script>
```
Also include the CLJS file that you want to evaluate with nREPL:
```
<script src="cljs/script.cljs" type="application/x-scittle"></script>
```
Then visit `cljs/script.cljs` in your editor and connect to the nREPL server,
and start evaluating!
See the `resources/public/nrepl.html` file for an example. When you run `bb dev`
in this repository, and then open `http://localhost:1341/nrepl.html` you should
be able evaluate expressions in `resources/public/cljs/nrepl_playground.cljs`.
## Tasks
Run `bb tasks` to see all available tasks:
@ -33,6 +84,6 @@ Idea by Arne Brasseur a.k.a [plexus](https://github.com/plexus).
## License
Copyright © 2021 Michiel Borkent
Copyright © 2021 - 2022 Michiel Borkent
Distributed under the EPL License. See LICENSE.

25
bb.edn
View file

@ -1,4 +1,10 @@
{:tasks
{:deps {io.github.babashka/sci.nrepl
#_{:local/root "../sci.nrepl"}
{:git/sha "c14b5b4ef4390ff206cdb71f763f327799f5e853"}
io.github.babashka/http-server
{:git/sha "b38c1f16ad2c618adae2c3b102a5520c261a7dd3"}}
:tasks
{:requires ([babashka.fs :as fs]
[cheshire.core :as json]
[babashka.process :as p :refer [process]])
@ -8,8 +14,23 @@
(fs/delete-tree ".cpcache")
(fs/delete-tree ".shadow-cljs"))}
shadow:watch {:doc "Development build. Starts webserver and watches for changes."
:task (clojure "-M:dev -m shadow.cljs.devtools.cli watch main")}
http-server {:doc "Starts http server for serving static files"
:requires ([babashka.http-server :as http])
:task (do (http/serve {:port 1341 :dir "resources/public"})
(println "Serving static assets at http://localhost:1341"))}
browser-nrepl {:doc "Start browser nREPL"
:requires ([sci.nrepl.browser-server :as bp])
:task (bp/start! {})}
-dev {:depends [shadow:watch browser-nrepl http-server]}
dev {:doc "Development build. Starts webserver and watches for changes."
:task (clojure "-M:dev -m shadow.cljs.devtools.cli watch main")}
:task (do (run '-dev {:parallel true})
(deref (promise)))}
prod {:doc "Builds production artifacts."
:task (clojure {:extra-env {"SCI_ELIDE_VARS" "true"}}

View file

@ -2,12 +2,18 @@
:deps
{org.clojure/clojure {:mvn/version "1.10.3"}
org.babashka/sci {:mvn/version "0.3.1"}
org.babashka/sci {:mvn/version "0.3.5"}
reagent/reagent {:mvn/version "1.1.0"}
cljsjs/react {:mvn/version "17.0.2-0"}
cljsjs/react-dom {:mvn/version "17.0.2-0"}
cljsjs/react-dom-server {:mvn/version "17.0.2-0"}
cljs-ajax/cljs-ajax {:mvn/version "0.8.3"}}
cljs-ajax/cljs-ajax {:mvn/version "0.8.3"}
io.github.babashka/sci.nrepl
#_{:local/root "../sci.nrepl"}
{:git/sha "e83421ce9349c36df56a2eb936196dbb65b0de63"}
io.github.babashka/sci.configs
{:git/sha "fcd367c6a6115c5c4e41f3a08ee5a8d5b3387a18"}}
:aliases
{:dev

View file

@ -0,0 +1,15 @@
(ns nrepl-playground)
(+ 1 2 3)
(->
(js/document.getElementsByTagName "body")
first
(.append
(doto (js/document.createElement "p")
(.append
(js/document.createTextNode "there")))))
(defn foo [])
(js/alert "Isn't this cool? :)")

View file

@ -19,10 +19,10 @@
(def state (r/atom {:clicks 0}))
(defn my-component []
[:div
[:p "Clicks: " (:clicks @state)]
[:p [:button {:on-click #(swap! state update :clicks inc)}
"Click me!"]]])
[:div
[:p "Clicks: " (:clicks @state)]
[:p [:button {:on-click #(swap! state update :clicks inc)}
"Click me!"]]])
(rdom/render [my-component] (.getElementById js/document "app"))
@ -41,15 +41,15 @@
(require '[goog.object :as gobject])
(doseq [code code-tags]
(let [src (.getAttribute code "data-src")
req (js/XMLHttpRequest.)]
(.open req "GET" src true)
(set! (.-onload req)
(fn []
(let [response (gobject/get req "response")]
(set! (.-innerText code) response)
(.highlightElement js/hljs code))))
(.send req)))
(let [src (.getAttribute code "data-src")
req (js/XMLHttpRequest.)]
(.open req "GET" src true)
(set! (.-onload req)
(fn []
(let [response (gobject/get req "response")]
(set! (.-innerText code) response)
(.highlightElement js/hljs code))))
(.send req)))
</script>
@ -60,7 +60,7 @@
</head>
<body>
<div style="float: right;">
<a href="https://gitHub.com/babashka/scittle"><img src="https://img.shields.io/github/stars/babashka/scittle.svg?style=social&label=Star"></a></div>
<a href="https://gitHub.com/babashka/scittle"><img src="https://img.shields.io/github/stars/babashka/scittle.svg?style=social&label=Star"></a></div>
<h1>Scittle</h1>
<h2>What is this?</h2>
@ -73,7 +73,7 @@
To embed scittle in your website, it is recommended to use the links
published to
the <a href="https://github.com/babashka/scittle/releases/tag/v0.1.2">releases
page</a>.
page</a>.
Include <tt>scittle.js</tt> and write a <tt>script</tt> tag
where <tt>type</tt> is set
@ -90,7 +90,7 @@
When you have a file on your server, say <tt>cljs/script.cljs</tt>, you can load it using the <tt>src</tt> attribute:
<pre><code id="scittle-tag-example" class="html">
&lt;script src=&quot;cljs/script.cljs&quot; type=&quot;application/x-scittle&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;cljs/script.cljs&quot; type=&quot;application/x-scittle&quot;&gt;&lt;/script&gt;
</code></pre>
<script type="text/javascript">hljs.highlightElement(document.getElementById("scittle-tag-example"));</script>
@ -118,6 +118,12 @@
Click me!
</button>
<a name="repl"></a>
<h2><a href="#nrepl">REPL</a></h2>
To connect to a REPL with Scittle,
see <a href="https://github.com/babashka/scittle/blob/main/README.md#repl">README.md</a>
<a name="examples"></a>
<h2><a href="#examples">Examples</a></h2>
<ul>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="css/style.css">
<script src="js/scittle.js" type="application/javascript"></script>
<script>var SCITTLE_BROWSER_REPL_PROXY_PORT = 1340;</script>
<script src="js/scittle.nrepl.js" type="application/javascript"></script>
<script type="application/x-scittle" src="cljs/nrepl_playground.cljs"></script>
</head>
<body>
<h1>Scittle</h1>
<h2>What is this?</h2>
</body>
</html>

View file

@ -14,6 +14,8 @@
:global "ReactDOM"}}}
:modules
{:scittle {:entries [scittle.core]}
:scittle.nrepl {:entries [scittle.nrepl]
:depends-on #{:scittle}}
:scittle.reagent {:entries [scittle.reagent]
:depends-on #{:scittle}}
:scittle.cljs-ajax {:entries [scittle.cljs-ajax]

View file

@ -29,15 +29,29 @@
'goog.object {'set gobject/set
'get gobject/get}})
(def ctx (atom (sci/init {:namespaces namespaces
(def !sci-ctx (atom (sci/init {:namespaces namespaces
:classes {'js js/window
:allow :all}
:disable-arity-checks true})))
(def !last-ns (volatile! @sci/ns))
(defn- -eval-string [s]
(sci/binding [sci/ns @!last-ns]
(let [rdr (sci/reader s)]
(loop [res nil]
(let [form (sci/parse-next @!sci-ctx rdr)]
(if (= :sci.core/eof form)
(do
(vreset! !last-ns @sci/ns)
res)
(recur (sci/eval-form @!sci-ctx form))))))))
(defn ^:export eval-string [s]
(try (sci/eval-string* @ctx s)
(try (-eval-string s)
(catch :default e
(error/error-handler e (:src @ctx))
(error/error-handler e (:src @!sci-ctx))
(let [sci-error? (isa? (:type (ex-data e)) :sci/error)]
(throw (if sci-error?
(or (ex-cause e) e)
@ -45,14 +59,14 @@
(defn register-plugin! [plug-in-name sci-opts]
plug-in-name ;; unused for now
(swap! ctx sci/merge-opts sci-opts))
(swap! !sci-ctx sci/merge-opts sci-opts))
(defn- eval-script-tags* [script-tags]
(when-let [tag (first script-tags)]
(if-let [text (not-empty (gobject/get tag "textContent"))]
(let [scittle-id (str (gensym "scittle-tag-"))]
(gobject/set tag "scittle_id" scittle-id)
(swap! ctx assoc-in [:src scittle-id] text)
(swap! !sci-ctx assoc-in [:src scittle-id] text)
(sci/binding [sci/file scittle-id]
(eval-string text))
(eval-script-tags* (rest script-tags)))
@ -64,7 +78,7 @@
(let [response (gobject/get this "response")]
(gobject/set tag "scittle_id" src)
;; save source for error messages
(swap! ctx assoc-in [:src src] response)
(swap! !sci-ctx assoc-in [:src src] response)
(sci/binding [sci/file src]
(eval-string response)))
(eval-script-tags* (rest script-tags)))))]
@ -89,3 +103,4 @@
(enable-console-print!)
(sci/alter-var-root sci/print-fn (constantly *print-fn*))

45
src/scittle/nrepl.cljs Normal file
View file

@ -0,0 +1,45 @@
(ns scittle.nrepl
(:require
[clojure.edn :as edn]
[sci.nrepl.completions :refer [completions]]
[scittle.core :refer [!last-ns eval-string !sci-ctx]]))
(defn nrepl-websocket []
(.-ws_nrepl js/window))
(defn nrepl-reply [{:keys [id session]} payload]
(.send (nrepl-websocket)
(str (assoc payload :id id :session session :ns (str @!last-ns)))))
(defn handle-nrepl-eval [{:keys [code] :as msg}]
(let [[kind val] (try [::success (eval-string code)]
(catch :default e
[::error (str e)]))]
(case kind
::success
(do (nrepl-reply msg {:value (pr-str val)})
(nrepl-reply msg {:status ["done"]}))
::error
(do
(nrepl-reply msg {:err (pr-str val)})
(nrepl-reply msg {:ex (pr-str val)
:status ["error" "done"]})))))
(defn handle-nrepl-message [msg]
(case (:op msg)
:eval (handle-nrepl-eval msg)
:complete (let [completions (completions (assoc msg :ctx @!sci-ctx))]
(nrepl-reply msg completions))))
(when (.-SCITTLE_NREPL_WEBSOCKET_PORT js/window)
(set! (.-ws_nrepl js/window)
(new js/WebSocket "ws://localhost:1340/_nrepl")))
(when-let [ws (nrepl-websocket)]
(prn :ws ws)
(set! (.-onmessage ws)
(fn [event]
(handle-nrepl-message (edn/read-string (.-data event)))))
(set! (.-onerror ws)
(fn [event]
(js/console.log event))))

View file

@ -1,106 +1,10 @@
(ns scittle.reagent
(:require [reagent.core :as r]
[reagent.debug :as d :refer-macros [dev?]]
[reagent.dom :as rdom]
[reagent.ratom :as ratom]
[sci.core :as sci]
[scittle.core :as scittle]))
;; The with-let macro from reagent.core. The only change is that the
;; interop/unchecked-aget+set were replaced by aget and aset.
(defn ^:macro with-let [_ _ bindings & body]
(assert (vector? bindings)
(str "with-let bindings must be a vector, not "
(pr-str bindings)))
(let [v (gensym "with-let")
k (keyword v)
init (gensym "init")
;; V is a reaction, which holds a JS array.
;; If the array is empty, initialize values and store to the
;; array, using binding index % 2 to access the array.
;; After init, the bindings are just bound to the values in the array.
bs (into [init `(zero? (alength ~v))]
(map-indexed (fn [i x]
(if (even? i)
x
(let [j (quot i 2)]
;; Issue 525
;; If binding value is not yet set,
;; try setting it again. This should
;; also throw errors for each render
;; and prevent the body being called
;; if bindings throw errors.
`(if (or ~init
(not (.hasOwnProperty ~v ~j)))
(aset ~v ~j ~x)
(aget ~v ~j)))))
bindings))
[forms destroy] (let [fin (last body)]
(if (and (list? fin)
(= 'finally (first fin)))
[(butlast body) `(fn [] ~@(rest fin))]
[body nil]))
add-destroy (when destroy
(list
`(let [destroy# ~destroy]
(if (reagent.ratom/reactive?)
(when (nil? (.-destroy ~v))
(set! (.-destroy ~v) destroy#))
(destroy#)))))
asserting (dev?) #_(if *assert* true false)
res (gensym "res")]
`(let [~v (reagent.ratom/with-let-values ~k)]
~(when asserting
`(when-some [c# (reagent.ratom/-ratom-context)]
(when (== (.-generation ~v) (.-ratomGeneration c#))
(d/error "Warning: The same with-let is being used more "
"than once in the same reactive context."))
(set! (.-generation ~v) (.-ratomGeneration c#))))
(let ~(into bs [res `(do ~@forms)])
~@add-destroy
~res))))
(def rns (sci/create-ns 'reagent.core nil))
(def reagent-namespace
{'atom (sci/copy-var r/atom rns)
'as-element (sci/copy-var r/as-element rns)
'with-let (sci/copy-var with-let rns)
'cursor (sci/copy-var r/cursor rns)
'create-class (sci/copy-var r/create-class rns)
'create-compiler (sci/copy-var r/create-compiler rns)})
(def rtmns (sci/create-ns 'reagent.ratom nil))
(defn -ratom-context
"Read-only access to the ratom context."
[]
ratom/*ratom-context*)
(def reagent-ratom-namespace
{'with-let-values (sci/copy-var ratom/with-let-values rtmns)
'reactive? (sci/copy-var ratom/reactive? rtmns)
'-ratom-context (sci/copy-var -ratom-context rtmns)})
(def rdbgns (sci/create-ns 'reagent.debug nil))
(defn -tracking? []
reagent.debug/tracking)
(defn ^:macro error
"Print with console.error."
[_ _ & forms]
(when *assert*
`(when (some? js/console)
(.error (if (reagent.debug/-tracking?)
reagent.debug/track-console
js/console)
(str ~@forms)))))
(def reagent-debug-namespace
{'error (sci/copy-var error rdbgns)
'-tracking? (sci/copy-var -tracking? rdbgns)
'track-console (sci/copy-var d/track-console rdbgns)})
(:require
[reagent.dom :as rdom]
[sci.configs.reagent.reagent :refer [reagent-debug-namespace
reagent-namespace reagent-ratom-namespace]]
[sci.core :as sci]
[scittle.core :as scittle]))
(def rdns (sci/create-ns 'reagent.dom nil))