Skip to content

Commit fe89630

Browse files
committed
improve error reporting
1 parent c3c32ed commit fe89630

File tree

5 files changed

+166
-56
lines changed

5 files changed

+166
-56
lines changed

core/dev/uix/rsc_example/server/core.clj

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,22 @@
8686
(rsc/render-to-flight-stream ($ server.root/page {:route route})
8787
{:on-chunk on-chunk :result result})))})))
8888

89+
(def error-boundary
90+
(uix/create-error-boundary
91+
{:derive-error-state (fn [error] {:error error})}
92+
(fn [[state] {:keys [children]}]
93+
(if-let [error (:error state)]
94+
($ server.root/html-page
95+
($ :h1.text-5xl.text-center "Something went wrong"))
96+
children))))
97+
8998
(defn html-handler [request]
9099
(when-let [route (r/match-by-path router (:uri request))]
91100
(server/as-channel request
92101
{:on-open (fn [ch]
93102
(let [on-chunk (with-gzip request ch "text/html; charset=utf-8")]
94-
(rsc/render-to-html-stream ($ server.root/page {:route route})
103+
(rsc/render-to-html-stream
104+
($ error-boundary ($ server.root/page {:route route}))
95105
{:on-chunk on-chunk})))})))
96106

97107
(defn rsc-action-handler [request]

core/dev/uix/rsc_example/server/root.clj

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -45,22 +45,26 @@
4545
:key movie_id}
4646
($ fav-link {:id movie_id})))))))))
4747

48+
(defui html-page [{:keys [children]}]
49+
($ :html {:lang "en"}
50+
($ :head
51+
($ :meta {:charset "utf-8"})
52+
($ :meta {:name "viewport" :content "width=device-width, initial-scale=1"})
53+
($ :meta {:name "description" :content "UIx RSC Demo page"})
54+
($ :title "UIx RSC Demo")
55+
($ :link {:rel :preconnect :href "https://fonts.googleapis.com"})
56+
($ :link {:rel :preconnect :href "https://fonts.gstatic.com"})
57+
;; todo: this url breaks hydration
58+
#_($ :link {:rel :stylesheet :href "https://fonts.googleapis.com/css2?family=Boldonse&family=Instrument+Sans:ital,wght@0,400..700;1,400..700&family=Instrument+Serif&display=swap"})
59+
($ :link {:rel :stylesheet :href "/rsc-out/main.css"})
60+
($ :script {:src "/rsc-out/rsc.js" :async true}))
61+
($ :body.font-instrumentSans.pb-56
62+
($ header)
63+
children
64+
($ favourites))))
65+
4866
(defui page [{:keys [route]}]
4967
(let [{:keys [path path-params data]} route
5068
{:keys [component]} data]
51-
($ :html {:lang "en"}
52-
($ :head
53-
($ :meta {:charset "utf-8"})
54-
($ :meta {:name "viewport" :content "width=device-width, initial-scale=1"})
55-
($ :meta {:name "description" :content "UIx RSC Demo page"})
56-
($ :title "UIx RSC Demo")
57-
($ :link {:rel :preconnect :href "https://fonts.googleapis.com"})
58-
($ :link {:rel :preconnect :href "https://fonts.gstatic.com"})
59-
;; todo: this url breaks hydration
60-
#_($ :link {:rel :stylesheet :href "https://fonts.googleapis.com/css2?family=Boldonse&family=Instrument+Sans:ital,wght@0,400..700;1,400..700&family=Instrument+Serif&display=swap"})
61-
($ :link {:rel :stylesheet :href "/rsc-out/main.css"})
62-
($ :script {:src "/rsc-out/rsc.js" :async true}))
63-
($ :body.font-instrumentSans.pb-56
64-
($ header)
65-
($ component {:path path :params path-params})
66-
($ favourites)))))
69+
($ html-page
70+
($ component {:path path :params path-params}))))

core/src/uix/core.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@
137137
rsc-id (str *ns* "/" fname)]
138138
`(def ~fname
139139
(with-meta
140-
(core/fn [& ~args-sym]
140+
(core/fn ~(symbol (str "comp-" fname)) [& ~args-sym]
141141
~(with-props-cond props-cond `(first ~args-sym))
142142
(let [~args ~args-sym
143143
~(or rest-sym `_#) (dissoc (first ~args-sym) ~@dissoc-ks)]

core/src/uix/rsc.cljc

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
#?(:clj (:refer-clojure :exclude [partial]))
33
#?(:cljs (:require-macros [uix.rsc]))
44
(:require #?@(:cljs [["@roman01la/react-server-dom-esm/client" :as rsd-client]
5+
[cljs-bean.core :as bean]
56
[clojure.edn :as edn]
67
[clojure.walk :as walk]
8+
[clojure.string :as str]
79
[reitit.core :as r]
810
[reitit.frontend :as rf]
911
[reitit.frontend.easy :as rfe]])
@@ -164,16 +166,49 @@
164166
:padding "32px 0 0"}}
165167
($ :div {:style {:max-width 800}}
166168
($ :div {:style {:color "rgb(206, 17, 38)"
167-
:font-size 21}}
169+
:font-size 21
170+
:margin-bottom 24}}
168171
(.-message error))
169-
($ :pre {:style {:font-size 12
170-
:max-height 360
171-
:overflow-y :auto
172-
:background-color "rgba(206, 17, 38, 0.05)"
173-
:margin "24px 0 16px"
174-
:padding 8
175-
:border-radius 5}}
176-
($ :code (.-stack error)))
172+
(let [{:keys [src line start-line frame-line name]} (bean/bean js/__ERROR_SRC)]
173+
($ :<>
174+
($ :div {:style {:font-size 12
175+
:color "#5a5a5a"}}
176+
name)
177+
($ :pre {:style {:font-size 12
178+
:max-height 360
179+
:overflow-y :auto
180+
:background-color "rgba(206, 17, 38, 0.05)"
181+
:margin "16px 0"
182+
:padding "8px 0"
183+
:border-radius 5}}
184+
($ :code
185+
(->> (str/split-lines src)
186+
(map-indexed (fn [idx ln]
187+
(if (= idx line)
188+
($ :div {:key idx
189+
:style {:background-color "#ff000047"
190+
:padding "0 8px"}}
191+
(str (+ start-line idx) " ")
192+
ln)
193+
($ :div {:key idx
194+
:style {:padding "0 8px"}}
195+
(str (+ start-line idx) " ")
196+
ln)))))))
197+
($ :pre {:style {:font-size 12
198+
:max-height 360
199+
:overflow-y :auto
200+
:background-color "rgba(206, 17, 38, 0.05)"
201+
:margin "24px 0 16px"
202+
:padding "8px 0"
203+
:border-radius 5}}
204+
($ :code
205+
(->> (str/split-lines (.-stack error))
206+
(map-indexed (fn [idx ln]
207+
($ :div
208+
{:key idx
209+
:style {:background-color (when (= idx frame-line) "#ff000047")
210+
:padding "0 8px"}}
211+
ln))))))))
177212
($ :div {:style {:font-size 12
178213
:color "#5a5a5a"}}
179214
"This screen is visible only in development. It will not appear if the app crashes in production. Open your browser’s developer console to further inspect this error.")))
@@ -376,10 +411,13 @@
376411
(let [done-count (atom 0)
377412
set-done #(when (== 2 (swap! done-count inc))
378413
(on-chunk :done))
414+
sb (server.flight/create-state)
415+
emit-error #(when (seq (:error-component @sb))
416+
;; todo: only in dev
417+
(on-chunk (str "<script>window.__ERROR_SRC = " (json/generate-string (:error-component @sb)) ";</script>")))
379418
handle-chunk #(if (= :done %)
380-
(set-done)
419+
(do (emit-error) (set-done))
381420
(on-chunk (str "<script>window.__FLIGHT_DATA.push(" (json/generate-string %) ");</script>")))
382-
sb (server.flight/create-state)
383421
ast (loader/run-with-loader
384422
#(server.flight/-unwrap src sb))
385423
*state (volatile! :state/root)]
@@ -389,6 +427,7 @@
389427
(on-chunk suspense-cleanup-js))
390428
(on-chunk "<script>window.__FLIGHT_DATA ||= [];</script>")
391429
(server.flight/render-to-flight-stream ast {:on-chunk handle-chunk :sb sb :cache *cache*})
430+
;; todo: handle errors in suspended blocks
392431
(stream-suspended sb handle-chunk set-done
393432
(fn [to-id element]
394433
(let [ssb (dom.server/make-static-builder)

dom/src/uix/dom/server/flight.clj

Lines changed: 84 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
(ns uix.dom.server.flight
2-
(:require [clojure.string :as str]
2+
(:require [clojure.java.io :as io]
3+
[clojure.string :as str]
34
[clojure.walk :as walk]
45
[uix.compiler.attributes :as attrs]
56
[uix.core :refer [$ defui]]
67
[uix.dom.server :as dom.server]
78
[uix.rsc.loader :as loader]
89
[cheshire.core :as json]
910
[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)))
1113

1214
(defprotocol FlightRenderer
1315
(-unwrap [this sb]
@@ -163,6 +165,8 @@
163165
(keyword? tag) (render-dom-element el sb)
164166
:else (throw (IllegalArgumentException. (str (type tag) " " tag " is not a valid element type")))))
165167

168+
(defrecord RenderingError [^Throwable error var])
169+
166170
(defn- unwrap-component-element [[tag :as el] sb]
167171
(let [props (normalize-props el)
168172
v (try
@@ -171,7 +175,10 @@
171175
(tag props)
172176
(tag)))
173177
(catch Throwable e
174-
e))]
178+
(if-not false ;; dev
179+
(throw e)
180+
(let [v (-> tag meta :rsc/id symbol resolve)]
181+
(RenderingError. e v)))))]
175182
(-unwrap v sb)))
176183

177184
(defn- unwrap-fragment-element [[tag attrs & children] sb]
@@ -214,13 +221,82 @@
214221
:fallback (-unwrap fallback sb)
215222
:children tags}]))
216223

224+
(defn- source-fn [v]
225+
(when-let [filepath (:file (meta v))]
226+
(with-open [rdr (LineNumberReader. (io/reader filepath))]
227+
(dotimes [_ (dec (:line (meta v)))] (.readLine rdr))
228+
(let [text (StringBuilder.)
229+
pbr (proxy [PushbackReader] [rdr]
230+
(read [] (let [i (proxy-super read)]
231+
(.append text (char i))
232+
i)))
233+
read-opts (if (.endsWith ^String filepath "cljc") {:read-cond :allow} {})]
234+
(if (= :unknown *read-eval*)
235+
(throw (IllegalStateException. "Unable to read source while *read-eval* is :unknown."))
236+
(read read-opts (PushbackReader. pbr)))
237+
(str text)))))
238+
239+
(defn- render-error [^RenderingError this sb]
240+
(let [{:keys [^Throwable error var]} this
241+
st (.getStackTrace error)
242+
stack (->> st
243+
(mapv (fn [^java.lang.StackTraceElement frame]
244+
(str/join " "
245+
[" at"
246+
(str (.getClassName frame)
247+
" "
248+
(.getMethodName frame))
249+
(str
250+
"("
251+
(.getFileName frame)
252+
":"
253+
(.getLineNumber frame)
254+
")")])))
255+
(into [(str (type error) ": " (ex-message error))])
256+
(str/join "\n"))
257+
src (str "E" (json/generate-string
258+
{:digest ""
259+
:name (str (type error))
260+
:message (str (type error) " " (ex-message error))
261+
:stack stack}))
262+
id (get-cached-id sb :errors src)
263+
component-src (source-fn var)
264+
{:keys [line ns name]} (meta var)
265+
component-ns (str (-> ns str munge) "$comp_")
266+
[frame-line error-line] (reduce
267+
(fn [[idx _] ^java.lang.StackTraceElement f]
268+
(if (str/starts-with? (.getClassName f) component-ns)
269+
(reduced [(inc idx) (.getLineNumber f)])
270+
[(inc idx) nil]))
271+
[0 nil]
272+
st)
273+
marker-line-offset (- (or error-line line) line)]
274+
(swap! sb assoc :error-component {:src component-src
275+
:line marker-line-offset
276+
:start-line line
277+
:frame-line frame-line
278+
:name (str ns "/" name)})
279+
(str "$L" id)))
280+
281+
(defn- unwrap-error-boundary [[f & args] sb]
282+
(let [{:keys [display-name render-fn did-catch derive-error-state]} f
283+
props {:children args}]
284+
(try
285+
(-> (render-fn [nil identity] props)
286+
(-unwrap sb))
287+
(catch Exception e
288+
(when did-catch (did-catch e display-name))
289+
(-> (render-fn [(derive-error-state e) identity] props)
290+
(-unwrap sb))))))
291+
217292
(defn unwrap-element [[tag :as el] sb]
218293
(cond
219294
(client-component? tag) el
220295
(fn? tag) (unwrap-component-element el sb)
221296
(= :<> tag) (unwrap-fragment-element el sb)
222297
(= :uix.core/suspense tag) (unwrap-suspense-element el sb)
223298
(keyword? tag) (unwrap-dom-element el sb)
299+
(and (map? tag) (-> tag meta :uix.core/error-boundary)) (unwrap-error-boundary el sb)
224300
:else (throw (IllegalArgumentException. (str (type tag) " " tag " is not a valid element type")))))
225301

226302
(extend-protocol FlightRenderer
@@ -252,32 +328,12 @@
252328
(-render [this sb]
253329
(-render (str this) sb))
254330

255-
Throwable
331+
RenderingError
256332
(-unwrap [this sb]
257333
this)
258334
(-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)))
335+
;; todo: only in dev
336+
(render-error this sb))
281337

282338
IBlockingDeref
283339
;; async values serializer
@@ -314,7 +370,8 @@
314370
:suspended {}
315371
:imports {}
316372
:refs {}
317-
:errors {}}))
373+
:errors {}
374+
:error-component {}}))
318375

319376
(defn- render-result [src result sb]
320377
(let [id (get-id sb)]

0 commit comments

Comments
 (0)