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 See [releases](https://github.com/babashka/scittle/releases) for links to
[JSDelivr](https://www.jsdelivr.com) to get versioned artifacts. [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 ## Tasks
Run `bb tasks` to see all available 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 ## License
Copyright © 2021 Michiel Borkent Copyright © 2021 - 2022 Michiel Borkent
Distributed under the EPL License. See LICENSE. 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] {:requires ([babashka.fs :as fs]
[cheshire.core :as json] [cheshire.core :as json]
[babashka.process :as p :refer [process]]) [babashka.process :as p :refer [process]])
@ -8,9 +14,24 @@
(fs/delete-tree ".cpcache") (fs/delete-tree ".cpcache")
(fs/delete-tree ".shadow-cljs"))} (fs/delete-tree ".shadow-cljs"))}
dev {:doc "Development build. Starts webserver and watches for changes." shadow:watch {:doc "Development build. Starts webserver and watches for changes."
:task (clojure "-M:dev -m shadow.cljs.devtools.cli watch main")} :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 (do (run '-dev {:parallel true})
(deref (promise)))}
prod {:doc "Builds production artifacts." prod {:doc "Builds production artifacts."
:task (clojure {:extra-env {"SCI_ELIDE_VARS" "true"}} :task (clojure {:extra-env {"SCI_ELIDE_VARS" "true"}}
"-M:dev -m shadow.cljs.devtools.cli release main")} "-M:dev -m shadow.cljs.devtools.cli release main")}

View file

@ -2,12 +2,18 @@
:deps :deps
{org.clojure/clojure {:mvn/version "1.10.3"} {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"} reagent/reagent {:mvn/version "1.1.0"}
cljsjs/react {:mvn/version "17.0.2-0"} cljsjs/react {:mvn/version "17.0.2-0"}
cljsjs/react-dom {: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"} 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 :aliases
{:dev {: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

@ -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: 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"> <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> </code></pre>
<script type="text/javascript">hljs.highlightElement(document.getElementById("scittle-tag-example"));</script> <script type="text/javascript">hljs.highlightElement(document.getElementById("scittle-tag-example"));</script>
@ -118,6 +118,12 @@
Click me! Click me!
</button> </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> <a name="examples"></a>
<h2><a href="#examples">Examples</a></h2> <h2><a href="#examples">Examples</a></h2>
<ul> <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"}}} :global "ReactDOM"}}}
:modules :modules
{:scittle {:entries [scittle.core]} {:scittle {:entries [scittle.core]}
:scittle.nrepl {:entries [scittle.nrepl]
:depends-on #{:scittle}}
:scittle.reagent {:entries [scittle.reagent] :scittle.reagent {:entries [scittle.reagent]
:depends-on #{:scittle}} :depends-on #{:scittle}}
:scittle.cljs-ajax {:entries [scittle.cljs-ajax] :scittle.cljs-ajax {:entries [scittle.cljs-ajax]

View file

@ -29,15 +29,29 @@
'goog.object {'set gobject/set 'goog.object {'set gobject/set
'get gobject/get}}) 'get gobject/get}})
(def ctx (atom (sci/init {:namespaces namespaces (def !sci-ctx (atom (sci/init {:namespaces namespaces
:classes {'js js/window :classes {'js js/window
:allow :all} :allow :all}
:disable-arity-checks true}))) :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] (defn ^:export eval-string [s]
(try (sci/eval-string* @ctx s) (try (-eval-string s)
(catch :default e (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)] (let [sci-error? (isa? (:type (ex-data e)) :sci/error)]
(throw (if sci-error? (throw (if sci-error?
(or (ex-cause e) e) (or (ex-cause e) e)
@ -45,14 +59,14 @@
(defn register-plugin! [plug-in-name sci-opts] (defn register-plugin! [plug-in-name sci-opts]
plug-in-name ;; unused for now 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] (defn- eval-script-tags* [script-tags]
(when-let [tag (first script-tags)] (when-let [tag (first script-tags)]
(if-let [text (not-empty (gobject/get tag "textContent"))] (if-let [text (not-empty (gobject/get tag "textContent"))]
(let [scittle-id (str (gensym "scittle-tag-"))] (let [scittle-id (str (gensym "scittle-tag-"))]
(gobject/set tag "scittle_id" scittle-id) (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] (sci/binding [sci/file scittle-id]
(eval-string text)) (eval-string text))
(eval-script-tags* (rest script-tags))) (eval-script-tags* (rest script-tags)))
@ -64,7 +78,7 @@
(let [response (gobject/get this "response")] (let [response (gobject/get this "response")]
(gobject/set tag "scittle_id" src) (gobject/set tag "scittle_id" src)
;; save source for error messages ;; 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] (sci/binding [sci/file src]
(eval-string response))) (eval-string response)))
(eval-script-tags* (rest script-tags)))))] (eval-script-tags* (rest script-tags)))))]
@ -89,3 +103,4 @@
(enable-console-print!) (enable-console-print!)
(sci/alter-var-root sci/print-fn (constantly *print-fn*)) (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,107 +1,11 @@
(ns scittle.reagent (ns scittle.reagent
(:require [reagent.core :as r] (:require
[reagent.debug :as d :refer-macros [dev?]]
[reagent.dom :as rdom] [reagent.dom :as rdom]
[reagent.ratom :as ratom] [sci.configs.reagent.reagent :refer [reagent-debug-namespace
reagent-namespace reagent-ratom-namespace]]
[sci.core :as sci] [sci.core :as sci]
[scittle.core :as scittle])) [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)})
(def rdns (sci/create-ns 'reagent.dom nil)) (def rdns (sci/create-ns 'reagent.dom nil))
(def reagent-dom-namespace (def reagent-dom-namespace