|
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 | + (if-not false ;; dev |
| 179 | + (throw e) |
| 180 | + (let [v (-> tag meta :rsc/id symbol resolve)] |
| 181 | + (RenderingError. e v)))))] |
175 | 182 | (-unwrap v sb))) |
176 | 183 |
|
177 | 184 | (defn- unwrap-fragment-element [[tag attrs & children] sb] |
|
214 | 221 | :fallback (-unwrap fallback sb) |
215 | 222 | :children tags}])) |
216 | 223 |
|
| 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 | + |
217 | 292 | (defn unwrap-element [[tag :as el] sb] |
218 | 293 | (cond |
219 | 294 | (client-component? tag) el |
220 | 295 | (fn? tag) (unwrap-component-element el sb) |
221 | 296 | (= :<> tag) (unwrap-fragment-element el sb) |
222 | 297 | (= :uix.core/suspense tag) (unwrap-suspense-element el sb) |
223 | 298 | (keyword? tag) (unwrap-dom-element el sb) |
| 299 | + (and (map? tag) (-> tag meta :uix.core/error-boundary)) (unwrap-error-boundary el sb) |
224 | 300 | :else (throw (IllegalArgumentException. (str (type tag) " " tag " is not a valid element type"))))) |
225 | 301 |
|
226 | 302 | (extend-protocol FlightRenderer |
|
252 | 328 | (-render [this sb] |
253 | 329 | (-render (str this) sb)) |
254 | 330 |
|
255 | | - Throwable |
| 331 | + RenderingError |
256 | 332 | (-unwrap [this sb] |
257 | 333 | this) |
258 | 334 | (-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)) |
281 | 337 |
|
282 | 338 | IBlockingDeref |
283 | 339 | ;; async values serializer |
|
314 | 370 | :suspended {} |
315 | 371 | :imports {} |
316 | 372 | :refs {} |
317 | | - :errors {}})) |
| 373 | + :errors {} |
| 374 | + :error-component {}})) |
318 | 375 |
|
319 | 376 | (defn- render-result [src result sb] |
320 | 377 | (let [id (get-id sb)] |
|
0 commit comments