diff --git a/README.md b/README.md
index 764fed7..c2b2917 100644
--- a/README.md
+++ b/README.md
@@ -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:
+
+```
+
+
+```
+
+Also include the CLJS file that you want to evaluate with nREPL:
+
+```
+
+```
+
+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.
diff --git a/bb.edn b/bb.edn
index 1eef13f..9a89e71 100644
--- a/bb.edn
+++ b/bb.edn
@@ -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"}}
diff --git a/deps.edn b/deps.edn
index fbc1235..f9461c6 100644
--- a/deps.edn
+++ b/deps.edn
@@ -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
diff --git a/resources/public/cljs/nrepl_playground.cljs b/resources/public/cljs/nrepl_playground.cljs
new file mode 100644
index 0000000..2c71307
--- /dev/null
+++ b/resources/public/cljs/nrepl_playground.cljs
@@ -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? :)")
diff --git a/resources/public/index.html b/resources/public/index.html
index 82ea23f..c2720e5 100644
--- a/resources/public/index.html
+++ b/resources/public/index.html
@@ -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)))
@@ -60,7 +60,7 @@
-
+
Scittle
What is this?
@@ -73,7 +73,7 @@
To embed scittle in your website, it is recommended to use the links
published to
the releases
- page .
+ page.
Include scittle.js and write a script tag
where type is set
@@ -90,7 +90,7 @@
When you have a file on your server, say cljs/script.cljs , you can load it using the src attribute:
-<script src="cljs/script.cljs" type="application/x-scittle"></script>
+ <script src="cljs/script.cljs" type="application/x-scittle"></script>
@@ -118,6 +118,12 @@
Click me!
+
+
+
+ To connect to a REPL with Scittle,
+ see README.md
+
diff --git a/resources/public/nrepl.html b/resources/public/nrepl.html
new file mode 100644
index 0000000..b6fcb05
--- /dev/null
+++ b/resources/public/nrepl.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+ Scittle
+ What is this?
+
+
diff --git a/shadow-cljs.edn b/shadow-cljs.edn
index ff3946c..fe96550 100644
--- a/shadow-cljs.edn
+++ b/shadow-cljs.edn
@@ -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]
diff --git a/src/scittle/core.cljs b/src/scittle/core.cljs
index feff374..12c3afa 100644
--- a/src/scittle/core.cljs
+++ b/src/scittle/core.cljs
@@ -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*))
+
diff --git a/src/scittle/nrepl.cljs b/src/scittle/nrepl.cljs
new file mode 100644
index 0000000..e7fcad2
--- /dev/null
+++ b/src/scittle/nrepl.cljs
@@ -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))))
diff --git a/src/scittle/reagent.cljs b/src/scittle/reagent.cljs
index 57448d2..6354ef7 100644
--- a/src/scittle/reagent.cljs
+++ b/src/scittle/reagent.cljs
@@ -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))