|
| 1 | +;; Copyright (c) Rich Hickey. All rights reserved. |
| 2 | +;; The use and distribution terms for this software are covered by the |
| 3 | +;; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) |
| 4 | +;; which can be found in the file epl-v10.html at the root of this distribution. |
| 5 | +;; By using this software in any fashion, you are agreeing to be bound by |
| 6 | +;; the terms of this license. |
| 7 | +;; You must not remove this notice, or any other, from this software. |
| 8 | + |
| 9 | +(ns cljs.repl.nashorn |
| 10 | + (:require [clojure.java.io :as io] |
| 11 | + [clojure.string :as string] |
| 12 | + [clojure.stacktrace] |
| 13 | + [clojure.data.json :as json] |
| 14 | + [cljs.analyzer :as ana] |
| 15 | + [cljs.env :as env] |
| 16 | + [cljs.util :as util] |
| 17 | + [cljs.repl :as repl] |
| 18 | + [cljs.cli :as cli] |
| 19 | + [cljs.compiler :as comp] |
| 20 | + [cljs.closure :as closure] |
| 21 | + [cljs.stacktrace :as st]) |
| 22 | + (:import [javax.script ScriptEngine ScriptEngineManager ScriptException ScriptEngineFactory])) |
| 23 | + |
| 24 | +(util/compile-if (Class/forName "jdk.nashorn.api.scripting.NashornException") |
| 25 | + (do |
| 26 | + (import 'jdk.nashorn.api.scripting.NashornException) |
| 27 | + ;; Implementation |
| 28 | + |
| 29 | + (defn create-engine |
| 30 | + ([] (create-engine nil)) |
| 31 | + ([{:keys [code-cache] :or {code-cache true}}] |
| 32 | + (let [args (when code-cache ["-pcc"]) |
| 33 | + factories (.getEngineFactories (ScriptEngineManager.)) |
| 34 | + factory (get (zipmap (map #(.getEngineName %) factories) factories) "Oracle Nashorn")] |
| 35 | + (if-let [engine (if-not (empty? args) |
| 36 | + (.getScriptEngine ^ScriptEngineFactory factory (into-array args)) |
| 37 | + (.getScriptEngine ^ScriptEngineFactory factory))] |
| 38 | + (let [context (.getContext engine)] |
| 39 | + (.setWriter context *out*) |
| 40 | + (.setErrorWriter context *err*) |
| 41 | + engine) |
| 42 | + (throw (IllegalArgumentException. |
| 43 | + "Cannot find the Nashorn script engine, use a JDK version 8 or higher.")))))) |
| 44 | + |
| 45 | + (defn eval-str [^ScriptEngine engine ^String s] |
| 46 | + (.eval engine s)) |
| 47 | + |
| 48 | + (defn eval-resource |
| 49 | + "Evaluate a file on the classpath in the engine." |
| 50 | + [engine path debug] |
| 51 | + (let [r (io/resource path)] |
| 52 | + (eval-str engine (slurp r)) |
| 53 | + (when debug (println "loaded: " path)))) |
| 54 | + |
| 55 | + (defn init-engine [engine {:keys [output-dir] :as opts} debug] |
| 56 | + (eval-str engine (format "var CLJS_DEBUG = %s;" (boolean debug))) |
| 57 | + (eval-str engine (format "var CLJS_OUTPUT_DIR = \"%s\";" output-dir)) |
| 58 | + (eval-resource engine "goog/base.js" debug) |
| 59 | + (eval-resource engine "goog/deps.js" debug) |
| 60 | + (eval-resource engine "cljs/bootstrap_nashorn.js" debug) |
| 61 | + (eval-str engine |
| 62 | + (format "goog.global.CLOSURE_UNCOMPILED_DEFINES = %s;" |
| 63 | + (json/write-str (:closure-defines opts)))) |
| 64 | + engine) |
| 65 | + |
| 66 | + (defn tear-down-engine [engine] |
| 67 | + (eval-str engine "nashorn_tear_down();")) |
| 68 | + |
| 69 | + (defn load-js-file [engine file] |
| 70 | + (eval-str engine (format "nashorn_load(\"%s\");" file))) |
| 71 | + |
| 72 | + ;; Create a minimal build of Clojurescript from the core library. |
| 73 | + ;; Copied from clj.cljs.repl.node. |
| 74 | + (defn bootstrap-repl [engine output-dir opts] |
| 75 | + (env/ensure |
| 76 | + (let [deps-file ".nashorn_repl_deps.js" |
| 77 | + core (io/resource "cljs/core.cljs") |
| 78 | + core-js (closure/compile core |
| 79 | + (assoc opts :output-file |
| 80 | + (closure/src-file->target-file |
| 81 | + core (dissoc opts :output-dir)))) |
| 82 | + deps (closure/add-dependencies opts core-js)] |
| 83 | + ;; output unoptimized code and the deps file |
| 84 | + ;; for all compiled namespaces |
| 85 | + (apply closure/output-unoptimized |
| 86 | + (assoc opts :output-to (.getPath (io/file output-dir deps-file))) |
| 87 | + deps) |
| 88 | + ;; load the deps file so we can goog.require cljs.core etc. |
| 89 | + (load-js-file engine deps-file)))) |
| 90 | + |
| 91 | + (defn load-ns [engine ns] |
| 92 | + (eval-str engine |
| 93 | + (format "goog.require(\"%s\");" (comp/munge (first ns))))) |
| 94 | + |
| 95 | + ;; Nashorn script stacktraces have a relative path which includes the output-dir |
| 96 | + (defn- strip-file-name [^String file-name output-dir] |
| 97 | + (let [with-slash (str output-dir "/")] |
| 98 | + (if (.startsWith file-name with-slash) |
| 99 | + (string/replace-first file-name with-slash "") |
| 100 | + file-name))) |
| 101 | + |
| 102 | + (def repl-filename "<cljs repl>") |
| 103 | + |
| 104 | + (defrecord NashornEnv [engine debug] |
| 105 | + repl/IReplEnvOptions |
| 106 | + (-repl-options [this] |
| 107 | + {:output-dir ".cljs_nashorn_repl" |
| 108 | + :target :nashorn}) |
| 109 | + repl/IJavaScriptEnv |
| 110 | + (-setup [this {:keys [output-dir bootstrap output-to] :as opts}] |
| 111 | + (init-engine engine opts debug) |
| 112 | + (let [env (ana/empty-env)] |
| 113 | + (if output-to |
| 114 | + (load-js-file engine output-to) |
| 115 | + (bootstrap-repl engine output-dir opts)) |
| 116 | + (repl/evaluate-form this env repl-filename |
| 117 | + '(.require js/goog "cljs.core")) |
| 118 | + ;; monkey-patch goog.isProvided_ to suppress useless errors |
| 119 | + (repl/evaluate-form this env repl-filename |
| 120 | + '(set! js/goog.isProvided_ (fn [ns] false))) |
| 121 | + ;; monkey-patch goog.require to be more sensible |
| 122 | + (repl/evaluate-form this env repl-filename |
| 123 | + '(do |
| 124 | + (set! *loaded-libs* #{"cljs.core"}) |
| 125 | + (set! (.-require js/goog) |
| 126 | + (fn [name reload] |
| 127 | + (when (or (not (contains? *loaded-libs* name)) reload) |
| 128 | + (set! *loaded-libs* (conj (or *loaded-libs* #{}) name)) |
| 129 | + (js/CLOSURE_IMPORT_SCRIPT |
| 130 | + (if (some? goog/debugLoader_) |
| 131 | + (.getPathFromDeps_ goog/debugLoader_ name) |
| 132 | + (goog.object/get (.. js/goog -dependencies_ -nameToPath) name)))))))))) |
| 133 | + (-evaluate [{engine :engine :as this} filename line js] |
| 134 | + (when debug (println "Evaluating: " js)) |
| 135 | + (try |
| 136 | + {:status :success |
| 137 | + :value (if-let [r (eval-str engine js)] (.toString r) "")} |
| 138 | + (catch ScriptException e |
| 139 | + (let [^Throwable root-cause (clojure.stacktrace/root-cause e)] |
| 140 | + {:status :exception |
| 141 | + :value (.getMessage root-cause) |
| 142 | + :stacktrace (NashornException/getScriptStackString root-cause)})) |
| 143 | + (catch Throwable e |
| 144 | + (let [^Throwable root-cause (clojure.stacktrace/root-cause e)] |
| 145 | + {:status :exception |
| 146 | + :value (.getMessage root-cause) |
| 147 | + :stacktrace |
| 148 | + (apply str |
| 149 | + (interpose "\n" |
| 150 | + (map str |
| 151 | + (.getStackTrace root-cause))))})))) |
| 152 | + (-load [{engine :engine :as this} ns url] |
| 153 | + (load-ns engine ns)) |
| 154 | + (-tear-down [this] |
| 155 | + (tear-down-engine engine)) |
| 156 | + repl/IParseStacktrace |
| 157 | + (-parse-stacktrace [this frames-str ret opts] |
| 158 | + (st/parse-stacktrace this frames-str |
| 159 | + (assoc ret :ua-product :nashorn) opts)) |
| 160 | + repl/IParseError |
| 161 | + (-parse-error [_ err _] |
| 162 | + (update-in err [:stacktrace] |
| 163 | + (fn [st] |
| 164 | + (string/join "\n" (drop 1 (string/split st #"\n"))))))) |
| 165 | + |
| 166 | + (defn repl-env* [{:keys [debug] :as opts}] |
| 167 | + (let [engine (create-engine opts)] |
| 168 | + (merge |
| 169 | + (NashornEnv. engine debug) |
| 170 | + opts))) |
| 171 | + |
| 172 | + (defn repl-env |
| 173 | + "Create a Nashorn repl-env for use with the repl/repl* method in Clojurescript." |
| 174 | + [& {:as opts}] |
| 175 | + (repl-env* opts)) |
| 176 | + |
| 177 | + ;; ------------------------------------------------------------------------- |
| 178 | + ;; Command Line Support |
| 179 | + |
| 180 | + (defn -main [& args] |
| 181 | + (apply cli/main repl-env args))) |
| 182 | + |
| 183 | + (do |
| 184 | + (defn repl-env* [{:keys [debug] :as opts}] |
| 185 | + (throw (ex-info "Nashorn not supported" {:type :repl-error}))) |
| 186 | + |
| 187 | + (defn repl-env |
| 188 | + "Create a Nashorn repl-env for use with the repl/repl* method in Clojurescript." |
| 189 | + [& {:as opts}] |
| 190 | + (throw (ex-info "Nashorn not available under this Java runtime" {:type :repl-error}))) |
| 191 | + |
| 192 | + (defn -main [] |
| 193 | + (throw (ex-info "Nashorn not available under this Java runtime" {:type :repl-error}))))) |
0 commit comments