
clojure.browser.repl.cljs Maven / Gradle / Ivy
;; Copyright (c) Rich Hickey. All rights reserved.
;; The use and distribution terms for this software are covered by the
;; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
;; which can be found in the file epl-v10.html at the root of this distribution.
;; By using this software in any fashion, you are agreeing to be bound by
;; the terms of this license.
;; You must not remove this notice, or any other, from this software.
(ns ^{:doc "Receive - Eval - Print - Loop
Receive a block of JS (presumably generated by a ClojureScript compiler)
Evaluate it naively
Print the result of evaluation to a string
Send the resulting string back to the server Loop!"
:author "Bobby Calderwood and Alex Redington"}
clojure.browser.repl
(:require [goog.dom :as gdom]
[goog.object :as gobj]
[goog.array :as garray]
[goog.userAgent.product :as product]
[clojure.browser.net :as net]
[clojure.browser.event :as event]
;; repl-connection callback will receive goog.require('cljs.repl')
;; and monkey-patched require expects to be able to derive it
;; via goog.basePath, so this namespace should be compiled together
;; with clojure.browser.repl:
[cljs.repl]))
(goog-define HOST "localhost")
(goog-define PORT 9000)
(def xpc-connection (atom nil))
(def parent-connected? (atom false))
(def print-queue (array))
(defn flush-print-queue! [conn]
(doseq [str print-queue]
(net/transmit conn :print str))
(garray/clear print-queue))
(defn repl-print [data]
(.push print-queue (pr-str data))
(when @parent-connected?
(flush-print-queue! @xpc-connection)))
(set! *print-newline* true)
(set-print-fn! repl-print)
(set-print-err-fn! repl-print)
(defn get-ua-product []
(cond
product/SAFARI :safari
product/CHROME :chrome
product/FIREFOX :firefox
product/IE :ie))
(defn evaluate-javascript
"Process a single block of JavaScript received from the server"
[conn block]
(let [result
(try
{:status :success
:value (str (js* "eval(~{block})"))}
(catch :default e
{:status :exception
:ua-product (get-ua-product)
:value (str e)
:stacktrace
(if (.hasOwnProperty e "stack")
(.-stack e)
"No stacktrace available.")}))]
(pr-str result)))
(defn send-result [connection url data]
(net/transmit connection url "POST" data nil 0))
(defn send-print
"Send data to be printed in the REPL. If there is an error, try again
up to 10 times."
([url data]
(send-print url data 0))
([url data n]
(let [conn (net/xhr-connection)]
(event/listen conn :error
(fn [_]
(if (< n 10)
(send-print url data (inc n))
(.log js/console (str "Could not send " data " after " n " attempts.")))))
(net/transmit conn url "POST" data nil 0))))
(def order (atom 0))
(defn wrap-message [t data]
(pr-str {:type t :content data :order (swap! order inc)}))
(defn start-evaluator
"Start the REPL server connection."
[url]
(if-let [repl-connection (net/xpc-connection)]
(let [connection (net/xhr-connection)
repl-connected? (atom false)
try-handshake (fn try-handshake []
(when-not @repl-connected?
(net/transmit repl-connection
:start-handshake
nil)
;; In case we miss, try again. Parent will only
;; ack once.
(js/setTimeout try-handshake
10)))]
(net/connect repl-connection
try-handshake)
(net/register-service repl-connection
:ack-handshake
(fn [_]
(when-not @repl-connected?
(reset! repl-connected? true)
;; Now that we're connected to the parent, we can start talking to
;; the server.
(send-result connection
url
(wrap-message :ready "ready")))))
(event/listen connection
:success
(fn [e]
(net/transmit
repl-connection
:evaluate-javascript
(.getResponseText (.-currentTarget e)
()))))
(net/register-service repl-connection
:send-result
(fn [data]
(send-result connection url (wrap-message :result data))))
(net/register-service repl-connection
:print
(fn [data]
(send-print url (wrap-message :print data)))))
(js/alert "No 'xpc' param provided to child iframe.")))
(def load-queue nil)
(defn bootstrap
"Reusable browser REPL bootstrapping. Patches the essential functions
in goog.base to support re-loading of namespaces after page load."
[]
;; Monkey-patch goog.provide if running under optimizations :none - David
(when-not js/COMPILED
(set! (.-require__ js/goog) js/goog.require)
;; suppress useless Google Closure error about duplicate provides
(set! (.-isProvided_ js/goog) (fn [name] false))
;; provide cljs.user
(goog/constructNamespace_ "cljs.user")
(set! (.-writeScriptTag__ js/goog)
(fn [src opt_sourceText]
;; the page is already loaded, we can no longer leverage document.write
;; instead construct script tag elements and append them to the body
;; of the page, to avoid parallel script loading enforce sequential
;; load with a simple load queue
(let [loaded (atom false)
onload (fn []
(when (and load-queue (false? @loaded))
(swap! loaded not)
(if (zero? (alength load-queue))
(set! load-queue nil)
(.apply js/goog.writeScriptTag__ nil (.shift load-queue)))))]
(.appendChild js/document.body
(as-> (.createElement js/document "script") script
(doto script
(gobj/set "type" "text/javascript")
(gobj/set "onload" onload)
(gobj/set "onreadystatechange" onload)) ;; IE
(if (nil? opt_sourceText)
(doto script (gobj/set "src" src))
(doto script (gdom/setTextContext opt_sourceText))))))))
;; queue or load
(set! (.-writeScriptTag_ js/goog)
(fn [src opt_sourceText]
(if load-queue
(.push load-queue #js [src opt_sourceText])
(do
(set! load-queue #js [])
(js/goog.writeScriptTag__ src opt_sourceText)))))
;; we must reuse Closure library dev time dependency management, under namespace
;; reload scenarios we simply delete entries from the correct private locations
(set! (.-require js/goog)
(fn [src reload]
(when (= reload "reload-all")
(set! (.-cljsReloadAll_ js/goog) true))
(let [reload? (or reload (.-cljsReloadAll__ js/goog))]
(when reload?
(let [path (gobj/get js/goog.dependencies_.nameToPath src)]
(gobj/remove js/goog.dependencies_.visited path)
(gobj/remove js/goog.dependencies_.written path)
(gobj/remove js/goog.dependencies_.written
(str js/goog.basePath path))))
(let [ret (.require__ js/goog src)]
(when (= reload "reload-all")
(set! (.-cljsReloadAll_ js/goog) false))
ret))))))
(defn connect
"Connects to a REPL server from an HTML document. After the
connection is made, the REPL will evaluate forms in the context of
the document that called this function."
[repl-server-url]
(let [connected? (atom false)
repl-connection
(net/xpc-connection
{:peer_uri repl-server-url})]
(swap! xpc-connection (constantly repl-connection))
(net/register-service repl-connection
:start-handshake
(fn [_]
;; Child will keep retrying, but we only want
;; to ack once.
(when-not @connected?
(reset! connected? true)
(reset! parent-connected? true)
(net/transmit repl-connection
:ack-handshake
nil)
(flush-print-queue! repl-connection))))
(net/register-service repl-connection
:evaluate-javascript
(fn [js]
(net/transmit
repl-connection
:send-result
(evaluate-javascript repl-connection js))))
(net/connect repl-connection
(constantly nil)
(fn [iframe]
(set! (.-display (.-style iframe))
"none")))
(bootstrap)
repl-connection))
© 2015 - 2025 Weber Informatics LLC | Privacy Policy