|
1 | 1 | (ns uix.dom.server.flight |
2 | | - (:require [clojure.string :as str] |
| 2 | + (:require [clojure.java.io :as io] |
| 3 | + [clojure.string :as str] |
3 | 4 | [clojure.walk :as walk] |
4 | 5 | [uix.compiler.attributes :as attrs] |
5 | 6 | [uix.core :refer [$ defui]] |
6 | 7 | [uix.dom.server :as dom.server] |
7 | 8 | [uix.rsc.loader :as loader] |
8 | 9 | [cheshire.core :as json] |
9 | 10 | [clojure.core.async :as async]) |
10 | | - (:import [clojure.lang IBlockingDeref IPersistentVector ISeq])) |
| 11 | + (:import [clojure.lang IBlockingDeref IPersistentVector ISeq RT] |
| 12 | + (java.io InputStreamReader LineNumberReader PushbackReader))) |
11 | 13 |
|
12 | 14 | (defprotocol FlightRenderer |
13 | 15 | (-unwrap [this sb] |
|
163 | 165 | (keyword? tag) (render-dom-element el sb) |
164 | 166 | :else (throw (IllegalArgumentException. (str (type tag) " " tag " is not a valid element type"))))) |
165 | 167 |
|
| 168 | +(defrecord RenderingError [^Throwable error var]) |
| 169 | + |
166 | 170 | (defn- unwrap-component-element [[tag :as el] sb] |
167 | 171 | (let [props (normalize-props el) |
168 | 172 | v (try |
|
171 | 175 | (tag props) |
172 | 176 | (tag))) |
173 | 177 | (catch Throwable e |
174 | | - e))] |
| 178 | + (let [v (-> tag meta :rsc/id symbol resolve)] |
| 179 | + (RenderingError. e v))))] |
175 | 180 | (-unwrap v sb))) |
176 | 181 |
|
177 | 182 | (defn- unwrap-fragment-element [[tag attrs & children] sb] |
|
214 | 219 | :fallback (-unwrap fallback sb) |
215 | 220 | :children tags}])) |
216 | 221 |
|
| 222 | +(defn- source-fn [v] |
| 223 | + (when-let [filepath (:file (meta v))] |
| 224 | + (with-open [rdr (LineNumberReader. (io/reader filepath))] |
| 225 | + (dotimes [_ (dec (:line (meta v)))] (.readLine rdr)) |
| 226 | + (let [text (StringBuilder.) |
| 227 | + pbr (proxy [PushbackReader] [rdr] |
| 228 | + (read [] (let [i (proxy-super read)] |
| 229 | + (.append text (char i)) |
| 230 | + i))) |
| 231 | + read-opts (if (.endsWith ^String filepath "cljc") {:read-cond :allow} {})] |
| 232 | + (if (= :unknown *read-eval*) |
| 233 | + (throw (IllegalStateException. "Unable to read source while *read-eval* is :unknown.")) |
| 234 | + (read read-opts (PushbackReader. pbr))) |
| 235 | + (str text))))) |
| 236 | + |
| 237 | +(defn- render-error [^RenderingError this sb] |
| 238 | + (let [{:keys [^Throwable error var]} this |
| 239 | + st (.getStackTrace error) |
| 240 | + stack (->> st |
| 241 | + (mapv (fn [^java.lang.StackTraceElement frame] |
| 242 | + (str/join " " |
| 243 | + [" at" |
| 244 | + (str (.getClassName frame) |
| 245 | + " " |
| 246 | + (.getMethodName frame)) |
| 247 | + (str |
| 248 | + "(" |
| 249 | + (.getFileName frame) |
| 250 | + ":" |
| 251 | + (.getLineNumber frame) |
| 252 | + ")")]))) |
| 253 | + (into [(str (type error) ": " (ex-message error))]) |
| 254 | + (str/join "\n")) |
| 255 | + src (str "E" (json/generate-string |
| 256 | + {:digest "" |
| 257 | + :name (str (type error)) |
| 258 | + :message (str (type error) " " (ex-message error)) |
| 259 | + :stack stack})) |
| 260 | + id (get-cached-id sb :errors src) |
| 261 | + component-src (source-fn var) |
| 262 | + {:keys [line ns name]} (meta var) |
| 263 | + component-ns (str (-> ns str munge) "$comp_") |
| 264 | + [frame-line error-line] (reduce |
| 265 | + (fn [[idx _] ^java.lang.StackTraceElement f] |
| 266 | + (if (str/starts-with? (.getClassName f) component-ns) |
| 267 | + (reduced [(inc idx) (.getLineNumber f)]) |
| 268 | + [(inc idx) nil])) |
| 269 | + [0 nil] |
| 270 | + st) |
| 271 | + marker-line-offset (- (or error-line line) line)] |
| 272 | + (swap! sb assoc :error-component {:src component-src |
| 273 | + :line marker-line-offset |
| 274 | + :start-line line |
| 275 | + :frame-line frame-line |
| 276 | + :name (str ns "/" name)}) |
| 277 | + (str "$L" id))) |
| 278 | + |
217 | 279 | (defn unwrap-element [[tag :as el] sb] |
218 | 280 | (cond |
219 | 281 | (client-component? tag) el |
|
252 | 314 | (-render [this sb] |
253 | 315 | (-render (str this) sb)) |
254 | 316 |
|
255 | | - Throwable |
| 317 | + RenderingError |
256 | 318 | (-unwrap [this sb] |
257 | 319 | this) |
258 | 320 | (-render [this sb] |
259 | | - (let [stack (->> (.getStackTrace this) |
260 | | - (mapv (fn [^java.lang.StackTraceElement frame] |
261 | | - (str/join " " |
262 | | - [" at" |
263 | | - (str (.getClassName frame) |
264 | | - " " |
265 | | - (.getMethodName frame)) |
266 | | - (str |
267 | | - "(" |
268 | | - (.getFileName frame) |
269 | | - ":" |
270 | | - (.getLineNumber frame) |
271 | | - ")")]))) |
272 | | - (into [(str (type this) ": " (ex-message this))]) |
273 | | - (str/join "\n")) |
274 | | - src (str "E" (json/generate-string |
275 | | - {:digest "" |
276 | | - :name (str (type this)) |
277 | | - :message (str (type this) " " (ex-message this)) |
278 | | - :stack stack})) |
279 | | - id (get-cached-id sb :errors src)] |
280 | | - (str "$L" id))) |
| 321 | + (render-error this sb)) |
281 | 322 |
|
282 | 323 | IBlockingDeref |
283 | 324 | ;; async values serializer |
|
314 | 355 | :suspended {} |
315 | 356 | :imports {} |
316 | 357 | :refs {} |
317 | | - :errors {}})) |
| 358 | + :errors {} |
| 359 | + :error-component {}})) |
318 | 360 |
|
319 | 361 | (defn- render-result [src result sb] |
320 | 362 | (let [id (get-id sb)] |
|
0 commit comments