ludus/out/clojure/browser/repl.cljs

279 lines
10 KiB
Plaintext
Raw Normal View History

2023-11-16 18:22:15 +00:00
;; 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.json :as json]
[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 ^:dynamic *repl* nil)
;; these two defs are top-level so we can use them for printing
(def xpc-connection (atom nil))
(def parent-connected? (atom false))
;; captures any printing that occurs *before* we actually have a connection
(def print-queue (array))
(defn flush-print-queue! [conn]
(doseq [str print-queue]
(net/transmit conn :print
(json/serialize
#js {"repl" *repl*
"str" 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
:value (cljs.repl/error->str e)}))]
(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 [repl t data]
(pr-str
{:repl repl
:type t
:content data
:order (swap! order inc)}))
(defn start-evaluator
"Start the REPL server connection process. This process runs inside the
embedded iframe."
[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)))]
(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 nil :ready "ready")))))
(event/listen connection
:error
(fn [e]
(reset! repl-connected? false)
(net/transmit repl-connection :reconnect nil)
(js/setTimeout try-handshake 1000)))
(event/listen connection
:success
(fn [e]
(net/transmit
repl-connection
:evaluate-javascript
(.getResponseText (.-currentTarget e) ()))))
(net/register-service repl-connection
:send-result
(fn [json]
(let [obj (json/parse json)
repl (gobj/get obj "repl")
result (gobj/get obj "result")]
(send-result connection url
(wrap-message repl :result result)))))
(net/register-service repl-connection
:print
(fn [json]
(let [obj (json/parse json)
repl (gobj/get obj "repl")
str (gobj/get obj "str")]
(send-print url (wrap-message repl :print str))))))
(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/setTextContent 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)))))
;; In the latest Closure library implementation, there is no goog.writeScriptTag_,
;; to monkey-patch. The behavior of interest is instead in goog.Dependency.prototype.load,
;; which first checks and uses CLOSURE_IMPORT_SCRIPT if defined. So we hook our desired
;; behavior here.
(when goog/debugLoader_
(set! js/CLOSURE_IMPORT_SCRIPT (.-writeScriptTag_ js/goog)))
;; 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?
(if (some? goog/debugLoader_)
(let [path (.getPathFromDeps_ goog/debugLoader_ src)]
(gobj/remove (.-written_ goog/debugLoader_) path)
(gobj/remove (.-written_ goog/debugLoader_)
(str js/goog.basePath path)))
(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))
;; handle requires from Closure Library goog.modules
(if (js/goog.isInModuleLoader_)
(js/goog.module.getInternal_ src)
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
:reconnect
(fn [_]
(reset! connected? false)
(reset! parent-connected? false)))
(net/register-service repl-connection
:evaluate-javascript
(fn [json]
(let [obj (json/parse json)
repl (gobj/get obj "repl")
form (gobj/get obj "form")]
(net/transmit
repl-connection
:send-result
(json/serialize
#js {"repl" repl
"result"
(binding [*repl* repl]
(evaluate-javascript repl-connection form))})))))
(net/connect repl-connection
(constantly nil)
(fn [iframe]
(set! (.-display (.-style iframe))
"none")))
(bootstrap)
repl-connection))