|
12 | 12 | [clojure.stacktrace]
|
13 | 13 | [cljs.analyzer :as ana]
|
14 | 14 | [cljs.env :as env]
|
| 15 | + [cljs.util :as util] |
15 | 16 | [cljs.repl :as repl]
|
16 | 17 | [cljs.compiler :as comp]
|
17 | 18 | [cljs.closure :as closure])
|
18 | 19 | (:import [java.io File]
|
19 | 20 | [javax.script ScriptEngine ScriptEngineManager ScriptException ScriptEngineFactory]
|
20 |
| - [jdk.nashorn.api.scripting NashornException])) |
21 |
| - |
22 |
| -;; Nashorn Clojurescript repl binding. |
23 |
| -;; |
24 |
| -;; Uses the Nashorn load() function to load Javascript files into the script engine. |
25 |
| -;; |
26 |
| -;; Nashorn's load() function docs: |
27 |
| -;; http://docs.oracle.com/javase/8/docs/technotes/guides/scripting/nashorn/shell.html |
28 |
| - |
29 |
| -(comment |
30 |
| - (ns init-repl-test |
31 |
| - (:require [cljs.repl] |
32 |
| - [cljs.repl.nashorn])) |
33 |
| - |
34 |
| - (cljs.repl/repl (cljs.repl.nashorn/repl-env) |
35 |
| - :output-dir "resources/public/compiled" |
36 |
| - :cache-analysis true) |
37 |
| - ) |
38 |
| - |
39 |
| -;; Implementation |
40 |
| - |
41 |
| -(defn create-engine |
42 |
| - ([] (create-engine nil)) |
43 |
| - ([{:keys [code-cache] :or {code-cache true}}] |
44 |
| - (let [args (when code-cache ["-pcc"]) |
45 |
| - factories (.getEngineFactories (ScriptEngineManager.)) |
46 |
| - factory (get (zipmap (map #(.getEngineName %) factories) factories) "Oracle Nashorn")] |
47 |
| - (if-let [engine (if-not (empty? args) |
48 |
| - (.getScriptEngine ^ScriptEngineFactory factory (into-array args)) |
49 |
| - (.getScriptEngine ^ScriptEngineFactory factory))] |
50 |
| - (let [context (.getContext engine)] |
51 |
| - (.setWriter context *out*) |
52 |
| - (.setErrorWriter context *err*) |
53 |
| - engine) |
54 |
| - (throw (IllegalArgumentException. |
55 |
| - "Cannot find the Nashorn script engine, use a JDK version 8 or higher.")))))) |
56 |
| - |
57 |
| -(defn eval-str [^ScriptEngine engine ^String s] |
58 |
| - (.eval engine s)) |
59 |
| - |
60 |
| -(defn eval-resource |
61 |
| - "Evaluate a file on the classpath in the engine." |
62 |
| - [engine path debug] |
63 |
| - (let [r (io/resource path)] |
64 |
| - (eval-str engine (slurp r)) |
65 |
| - (when debug (println "loaded: " path)))) |
66 |
| - |
67 |
| -(defn init-engine [engine output-dir debug] |
68 |
| - (eval-resource engine "goog/base.js" debug) |
69 |
| - (eval-resource engine "goog/deps.js" debug) |
70 |
| - (eval-str engine "var global = this") ; required by React |
71 |
| - (eval-str engine |
72 |
| - (format |
73 |
| - (str "var nashorn_load = function(path) {" |
74 |
| - " var outputPath = \"%s\" + \"/\" + path;" |
75 |
| - (when debug " print(\"loading: \" + outputPath) ; ") |
76 |
| - " load(outputPath);" |
77 |
| - "};") |
78 |
| - output-dir)) |
79 |
| - (eval-str engine |
80 |
| - (str "goog.global.CLOSURE_IMPORT_SCRIPT = function(path) {" |
81 |
| - " nashorn_load(\"goog/\" + path);" |
82 |
| - " return true;" |
83 |
| - "};")) |
84 |
| - (eval-str engine "goog.global.isProvided_ = function(name) { return false; };") |
85 |
| - engine) |
86 |
| - |
87 |
| -(defn load-js-file [engine file] |
88 |
| - (eval-str engine (format "nashorn_load(\"%s\");" file))) |
89 |
| - |
90 |
| -;; Create a minimal build of Clojurescript from the core library. |
91 |
| -;; Copied from clj.cljs.repl.node. |
92 |
| -(defn bootstrap-repl [engine output-dir opts] |
93 |
| - (env/ensure |
94 |
| - (let [deps-file ".nashorn_repl_deps.js" |
95 |
| - core (io/resource "cljs/core.cljs") |
96 |
| - core-js (closure/compile core |
97 |
| - (assoc opts |
98 |
| - :output-file (closure/src-file->target-file core))) |
99 |
| - deps (closure/add-dependencies opts core-js)] |
100 |
| - ;; output unoptimized code and the deps file |
101 |
| - ;; for all compiled namespaces |
102 |
| - (apply closure/output-unoptimized |
103 |
| - (assoc opts :output-to (.getPath (io/file output-dir deps-file))) |
104 |
| - deps) |
105 |
| - ;; load the deps file so we can goog.require cljs.core etc. |
106 |
| - (load-js-file engine deps-file)))) |
107 |
| - |
108 |
| -(defn load-ns [engine ns] |
109 |
| - (eval-str engine |
110 |
| - (format "goog.require(\"%s\");" (comp/munge (first ns))))) |
111 |
| - |
112 |
| -;; Nashorn script stacktraces have a relative path which includes the output-dir |
113 |
| -(defn- strip-file-name [^String file-name output-dir] |
114 |
| - (let [with-slash (str output-dir "/")] |
115 |
| - (if (.startsWith file-name with-slash) |
116 |
| - (string/replace-first file-name with-slash "") |
117 |
| - file-name))) |
118 |
| - |
119 |
| -(def repl-filename "<cljs repl>") |
120 |
| - |
121 |
| -(defrecord NashornEnv [engine debug] |
122 |
| - repl/IReplEnvOptions |
123 |
| - (-repl-options [this] |
124 |
| - {:output-dir ".cljs_nashorn_repl"}) |
125 |
| - repl/IJavaScriptEnv |
126 |
| - (-setup [this {:keys [output-dir bootstrap output-to] :as opts}] |
127 |
| - (init-engine engine output-dir debug) |
128 |
| - (let [env (ana/empty-env)] |
129 |
| - (if output-to |
130 |
| - (load-js-file engine output-to) |
131 |
| - (bootstrap-repl engine output-dir opts)) |
132 |
| - (repl/evaluate-form this env repl-filename |
133 |
| - '(do |
134 |
| - (.require js/goog "cljs.core") |
135 |
| - (set! *print-newline* false) |
136 |
| - (set! *print-fn* js/print))) |
137 |
| - ;; monkey-patch goog.isProvided_ to suppress useless errors |
138 |
| - (repl/evaluate-form this env repl-filename |
139 |
| - '(set! js/goog.isProvided_ (fn [ns] false))) |
140 |
| - ;; monkey-patch goog.require to be more sensible |
141 |
| - (repl/evaluate-form this env repl-filename |
142 |
| - '(do |
143 |
| - (set! *loaded-libs* #{"cljs.core"}) |
144 |
| - (set! (.-require js/goog) |
145 |
| - (fn [name reload] |
146 |
| - (when (or (not (contains? *loaded-libs* name)) reload) |
147 |
| - (set! *loaded-libs* (conj (or *loaded-libs* #{}) name)) |
148 |
| - (js/CLOSURE_IMPORT_SCRIPT |
149 |
| - (aget (.. js/goog -dependencies_ -nameToPath) name))))))))) |
150 |
| - (-evaluate [{engine :engine :as this} filename line js] |
151 |
| - (when debug (println "Evaluating: " js)) |
152 |
| - (try |
153 |
| - {:status :success |
154 |
| - :value (if-let [r (eval-str engine js)] (.toString r) "")} |
155 |
| - (catch ScriptException e |
156 |
| - (let [^Throwable root-cause (clojure.stacktrace/root-cause e)] |
157 |
| - {:status :exception |
158 |
| - :value (.getMessage root-cause) |
159 |
| - :stacktrace (NashornException/getScriptStackString root-cause)})) |
160 |
| - (catch Throwable e |
161 |
| - (let [^Throwable root-cause (clojure.stacktrace/root-cause e)] |
162 |
| - {:status :exception |
163 |
| - :value (.getMessage root-cause) |
164 |
| - :stacktrace |
165 |
| - (apply str |
166 |
| - (interpose "\n" |
167 |
| - (map str |
168 |
| - (.getStackTrace root-cause))))})))) |
169 |
| - (-load [{engine :engine :as this} ns url] |
170 |
| - (load-ns engine ns)) |
171 |
| - (-tear-down [this]) |
172 |
| - repl/IParseStacktrace |
173 |
| - (-parse-stacktrace [this frames-str ret {output-dir :output-dir}] |
174 |
| - (vec |
175 |
| - (map |
176 |
| - (fn [frame-str] |
177 |
| - (let [frame-str (string/replace frame-str #"\s+at\s+" "") |
178 |
| - [function file-and-line] (string/split frame-str #"\s+") |
179 |
| - [file-part line-part] (string/split file-and-line #":")] |
180 |
| - {:file (string/replace (.substring file-part 1) |
181 |
| - (str output-dir File/separator) "") |
182 |
| - :function function |
183 |
| - :line (Integer/parseInt |
184 |
| - (.substring line-part 0 (dec (.length line-part)))) |
185 |
| - :column 0})) |
186 |
| - (string/split frames-str #"\n")))) |
187 |
| - repl/IParseError |
188 |
| - (-parse-error [_ err _] |
189 |
| - (update-in err [:stacktrace] |
190 |
| - (fn [st] |
191 |
| - (string/join "\n" (drop 1 (string/split st #"\n"))))))) |
192 |
| - |
193 |
| -(defn repl-env* [{:keys [debug] :as opts}] |
194 |
| - (let [engine (create-engine opts)] |
195 |
| - (merge |
196 |
| - (NashornEnv. engine debug) |
197 |
| - opts))) |
198 |
| - |
199 |
| -(defn repl-env |
200 |
| - "Create a Nashorn repl-env for use with the repl/repl* method in Clojurescript." |
201 |
| - [& {:as opts}] |
202 |
| - (repl-env* opts)) |
203 |
| - |
204 |
| -(defn -main [] |
205 |
| - (repl/repl (repl-env))) |
| 21 | + [com.google.common.base Throwables])) |
| 22 | + |
| 23 | +(util/compile-if (Class/forName "jdk.nashorn.api.scripting.NashornException") |
| 24 | + (do |
| 25 | + (import 'jdk.nashorn.api.scripting.NashornException) |
| 26 | + ;; Implementation |
| 27 | + |
| 28 | + (defn create-engine |
| 29 | + ([] (create-engine nil)) |
| 30 | + ([{:keys [code-cache] :or {code-cache true}}] |
| 31 | + (let [args (when code-cache ["-pcc"]) |
| 32 | + factories (.getEngineFactories (ScriptEngineManager.)) |
| 33 | + factory (get (zipmap (map #(.getEngineName %) factories) factories) "Oracle Nashorn")] |
| 34 | + (if-let [engine (if-not (empty? args) |
| 35 | + (.getScriptEngine ^ScriptEngineFactory factory (into-array args)) |
| 36 | + (.getScriptEngine ^ScriptEngineFactory factory))] |
| 37 | + (let [context (.getContext engine)] |
| 38 | + (.setWriter context *out*) |
| 39 | + (.setErrorWriter context *err*) |
| 40 | + engine) |
| 41 | + (throw (IllegalArgumentException. |
| 42 | + "Cannot find the Nashorn script engine, use a JDK version 8 or higher.")))))) |
| 43 | + |
| 44 | + (defn eval-str [^ScriptEngine engine ^String s] |
| 45 | + (.eval engine s)) |
| 46 | + |
| 47 | + (defn eval-resource |
| 48 | + "Evaluate a file on the classpath in the engine." |
| 49 | + [engine path debug] |
| 50 | + (let [r (io/resource path)] |
| 51 | + (eval-str engine (slurp r)) |
| 52 | + (when debug (println "loaded: " path)))) |
| 53 | + |
| 54 | + (defn init-engine [engine output-dir debug] |
| 55 | + (eval-resource engine "goog/base.js" debug) |
| 56 | + (eval-resource engine "goog/deps.js" debug) |
| 57 | + (eval-str engine "var global = this") ; required by React |
| 58 | + (eval-str engine |
| 59 | + (format |
| 60 | + (str "var nashorn_load = function(path) {" |
| 61 | + " var outputPath = \"%s\" + \"/\" + path;" |
| 62 | + (when debug " print(\"loading: \" + outputPath) ; ") |
| 63 | + " load(outputPath);" |
| 64 | + "};") |
| 65 | + output-dir)) |
| 66 | + (eval-str engine |
| 67 | + (str "goog.global.CLOSURE_IMPORT_SCRIPT = function(path) {" |
| 68 | + " nashorn_load(\"goog/\" + path);" |
| 69 | + " return true;" |
| 70 | + "};")) |
| 71 | + (eval-str engine "goog.global.isProvided_ = function(name) { return false; };") |
| 72 | + engine) |
| 73 | + |
| 74 | + (defn load-js-file [engine file] |
| 75 | + (eval-str engine (format "nashorn_load(\"%s\");" file))) |
| 76 | + |
| 77 | + ;; Create a minimal build of Clojurescript from the core library. |
| 78 | + ;; Copied from clj.cljs.repl.node. |
| 79 | + (defn bootstrap-repl [engine output-dir opts] |
| 80 | + (env/ensure |
| 81 | + (let [deps-file ".nashorn_repl_deps.js" |
| 82 | + core (io/resource "cljs/core.cljs") |
| 83 | + core-js (closure/compile core |
| 84 | + (assoc opts |
| 85 | + :output-file (closure/src-file->target-file core))) |
| 86 | + deps (closure/add-dependencies opts core-js)] |
| 87 | + ;; output unoptimized code and the deps file |
| 88 | + ;; for all compiled namespaces |
| 89 | + (apply closure/output-unoptimized |
| 90 | + (assoc opts :output-to (.getPath (io/file output-dir deps-file))) |
| 91 | + deps) |
| 92 | + ;; load the deps file so we can goog.require cljs.core etc. |
| 93 | + (load-js-file engine deps-file)))) |
| 94 | + |
| 95 | + (defn load-ns [engine ns] |
| 96 | + (eval-str engine |
| 97 | + (format "goog.require(\"%s\");" (comp/munge (first ns))))) |
| 98 | + |
| 99 | + ;; Nashorn script stacktraces have a relative path which includes the output-dir |
| 100 | + (defn- strip-file-name [^String file-name output-dir] |
| 101 | + (let [with-slash (str output-dir "/")] |
| 102 | + (if (.startsWith file-name with-slash) |
| 103 | + (string/replace-first file-name with-slash "") |
| 104 | + file-name))) |
| 105 | + |
| 106 | + (def repl-filename "<cljs repl>") |
| 107 | + |
| 108 | + (defrecord NashornEnv [engine debug] |
| 109 | + repl/IReplEnvOptions |
| 110 | + (-repl-options [this] |
| 111 | + {:output-dir ".cljs_nashorn_repl"}) |
| 112 | + repl/IJavaScriptEnv |
| 113 | + (-setup [this {:keys [output-dir bootstrap output-to] :as opts}] |
| 114 | + (init-engine engine output-dir debug) |
| 115 | + (let [env (ana/empty-env)] |
| 116 | + (if output-to |
| 117 | + (load-js-file engine output-to) |
| 118 | + (bootstrap-repl engine output-dir opts)) |
| 119 | + (repl/evaluate-form this env repl-filename |
| 120 | + '(do |
| 121 | + (.require js/goog "cljs.core") |
| 122 | + (set! *print-newline* false) |
| 123 | + (set! *print-fn* js/print))) |
| 124 | + ;; monkey-patch goog.isProvided_ to suppress useless errors |
| 125 | + (repl/evaluate-form this env repl-filename |
| 126 | + '(set! js/goog.isProvided_ (fn [ns] false))) |
| 127 | + ;; monkey-patch goog.require to be more sensible |
| 128 | + (repl/evaluate-form this env repl-filename |
| 129 | + '(do |
| 130 | + (set! *loaded-libs* #{"cljs.core"}) |
| 131 | + (set! (.-require js/goog) |
| 132 | + (fn [name reload] |
| 133 | + (when (or (not (contains? *loaded-libs* name)) reload) |
| 134 | + (set! *loaded-libs* (conj (or *loaded-libs* #{}) name)) |
| 135 | + (js/CLOSURE_IMPORT_SCRIPT |
| 136 | + (aget (.. js/goog -dependencies_ -nameToPath) name))))))))) |
| 137 | + (-evaluate [{engine :engine :as this} filename line js] |
| 138 | + (when debug (println "Evaluating: " js)) |
| 139 | + (try |
| 140 | + {:status :success |
| 141 | + :value (if-let [r (eval-str engine js)] (.toString r) "")} |
| 142 | + (catch ScriptException e |
| 143 | + (let [^Throwable root-cause (clojure.stacktrace/root-cause e)] |
| 144 | + {:status :exception |
| 145 | + :value (.getMessage root-cause) |
| 146 | + :stacktrace (NashornException/getScriptStackString root-cause)})) |
| 147 | + (catch Throwable e |
| 148 | + (let [^Throwable root-cause (clojure.stacktrace/root-cause e)] |
| 149 | + {:status :exception |
| 150 | + :value (.getMessage root-cause) |
| 151 | + :stacktrace |
| 152 | + (apply str |
| 153 | + (interpose "\n" |
| 154 | + (map str |
| 155 | + (.getStackTrace root-cause))))})))) |
| 156 | + (-load [{engine :engine :as this} ns url] |
| 157 | + (load-ns engine ns)) |
| 158 | + (-tear-down [this]) |
| 159 | + repl/IParseStacktrace |
| 160 | + (-parse-stacktrace [this frames-str ret {output-dir :output-dir}] |
| 161 | + (vec |
| 162 | + (map |
| 163 | + (fn [frame-str] |
| 164 | + (let [frame-str (string/replace frame-str #"\s+at\s+" "") |
| 165 | + [function file-and-line] (string/split frame-str #"\s+") |
| 166 | + [file-part line-part] (string/split file-and-line #":")] |
| 167 | + {:file (string/replace (.substring file-part 1) |
| 168 | + (str output-dir File/separator) "") |
| 169 | + :function function |
| 170 | + :line (Integer/parseInt |
| 171 | + (.substring line-part 0 (dec (.length line-part)))) |
| 172 | + :column 0})) |
| 173 | + (string/split frames-str #"\n")))) |
| 174 | + repl/IParseError |
| 175 | + (-parse-error [_ err _] |
| 176 | + (update-in err [:stacktrace] |
| 177 | + (fn [st] |
| 178 | + (string/join "\n" (drop 1 (string/split st #"\n"))))))) |
| 179 | + |
| 180 | + (defn repl-env* [{:keys [debug] :as opts}] |
| 181 | + (let [engine (create-engine opts)] |
| 182 | + (merge |
| 183 | + (NashornEnv. engine debug) |
| 184 | + opts))) |
| 185 | + |
| 186 | + (defn repl-env |
| 187 | + "Create a Nashorn repl-env for use with the repl/repl* method in Clojurescript." |
| 188 | + [& {:as opts}] |
| 189 | + (repl-env* opts)) |
| 190 | + |
| 191 | + (defn -main [] |
| 192 | + (repl/repl (repl-env)))) |
| 193 | + (do |
| 194 | + (defn repl-env* [{:keys [debug] :as opts}] |
| 195 | + (throw (ex-info "Nashorn not supported" {:type :repl-error}))) |
| 196 | + |
| 197 | + (defn repl-env |
| 198 | + "Create a Nashorn repl-env for use with the repl/repl* method in Clojurescript." |
| 199 | + [& {:as opts}] |
| 200 | + (throw (ex-info "Nashorn not available under this Java runtime" {:type :repl-error}))) |
| 201 | + |
| 202 | + (defn -main [] |
| 203 | + (throw (ex-info "Nashorn not available under this Java runtime" {:type :repl-error}))))) |
| 204 | + |
| 205 | + |
| 206 | + |
0 commit comments