mirror of
https://github.com/penpot/penpot.git
synced 2026-06-01 21:20:18 +00:00
* 🐛 Handle plugin errors gracefully without crashing the UI Plugin errors (like 'Set is not a constructor') were propagating to the global error handler and showing the exception page. This fix: - Uses a WeakMap to track plugin errors (works in SES hardened environment) - Wraps setTimeout/setInterval handlers to mark errors and re-throw them - Frontend global handler checks isPluginError and logs to console Plugin errors are now logged to console with 'Plugin Error' prefix but don't crash the main application or show the exception page. Signed-off-by: AI Agent <agent@penpot.app> * ✨ Improved handling of plugin errors on initialization * ✨ Fix test and linter --------- Signed-off-by: AI Agent <agent@penpot.app> Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
241 lines
8.3 KiB
Clojure
241 lines
8.3 KiB
Clojure
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
;;
|
|
;; Copyright (c) KALEIDOS INC
|
|
|
|
(ns app.main.data.plugins
|
|
(:require
|
|
[app.common.data.macros :as dm]
|
|
[app.common.exceptions :as ex]
|
|
[app.common.files.changes-builder :as pcb]
|
|
[app.common.time :as ct]
|
|
[app.main.data.changes :as dch]
|
|
[app.main.data.event :as ev]
|
|
[app.main.data.modal :as modal]
|
|
[app.main.data.notifications :as ntf]
|
|
[app.main.errors :as errors]
|
|
[app.main.store :as st]
|
|
[app.plugins.flags :as pflag]
|
|
[app.plugins.register :as preg]
|
|
[app.util.globals :as ug]
|
|
[app.util.http :as http]
|
|
[app.util.i18n :as i18n :refer [tr]]
|
|
[beicon.v2.core :as rx]
|
|
[potok.v2.core :as ptk]
|
|
[promesa.core :as p]))
|
|
|
|
(defn save-plugin-permissions-peek
|
|
[id permissions]
|
|
(ptk/reify ::save-plugin-permissions-peek
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(assoc-in state [:plugins-permissions-peek :data id] permissions))))
|
|
|
|
(defn fetch-manifest
|
|
[plugin-url]
|
|
(->> (http/send! {:method :get
|
|
:uri plugin-url
|
|
:omit-default-headers true
|
|
:response-type :json})
|
|
(rx/map :body)
|
|
(rx/map #(preg/parse-manifest plugin-url %))))
|
|
|
|
(defn save-current-plugin
|
|
[id]
|
|
(ptk/reify ::save-current-plugin
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(update-in state [:workspace-local :open-plugins] (fnil conj #{}) id))))
|
|
|
|
(defn remove-current-plugin
|
|
[id]
|
|
(ptk/reify ::remove-current-plugin
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(update-in state [:workspace-local :open-plugins] (fnil disj #{}) id))))
|
|
|
|
(defn start-plugin!
|
|
[{:keys [plugin-id name version description host code permissions allow-background]} ^js extensions]
|
|
(-> (.ɵloadPlugin
|
|
^js ug/global
|
|
#js {:pluginId plugin-id
|
|
:name name
|
|
:version version
|
|
:description description
|
|
:host host
|
|
:code code
|
|
:allowBackground (boolean allow-background)
|
|
:permissions (apply array permissions)}
|
|
nil
|
|
extensions)
|
|
|
|
(p/catch (fn [cause]
|
|
(ex/print-throwable cause :prefix "Plugin Error")
|
|
(errors/flash :cause cause :type :handled)))))
|
|
|
|
(defn- load-plugin!
|
|
[{:keys [plugin-id name version description host code icon permissions]}]
|
|
(st/emit! (pflag/clear plugin-id)
|
|
(save-current-plugin plugin-id))
|
|
|
|
(-> (.ɵloadPlugin
|
|
^js ug/global
|
|
#js {:pluginId plugin-id
|
|
:name name
|
|
:description description
|
|
:version version
|
|
:host host
|
|
:code code
|
|
:icon icon
|
|
:permissions (apply array permissions)}
|
|
(fn []
|
|
(st/emit! (remove-current-plugin plugin-id))))
|
|
|
|
(p/catch (fn [cause]
|
|
(st/emit! (remove-current-plugin plugin-id))
|
|
(ex/print-throwable cause :prefix "Plugin Error")
|
|
(errors/flash :cause cause :type :handled)))))
|
|
|
|
(defn open-plugin!
|
|
[{:keys [url] :as manifest} user-can-edit?]
|
|
(if url
|
|
;; If the saved manifest has a URL we fetch the manifest to check
|
|
;; for updates
|
|
(->> (fetch-manifest url)
|
|
(rx/subs!
|
|
(fn [new-manifest]
|
|
(let [new-manifest (merge new-manifest (select-keys manifest [:plugin-id]))
|
|
permissions (:permissions new-manifest)
|
|
is-edition-plugin? (or (contains? permissions "content:write")
|
|
(contains? permissions "library:write"))]
|
|
(st/emit! (save-plugin-permissions-peek (:plugin-id new-manifest) permissions))
|
|
(cond
|
|
(and is-edition-plugin? (not user-can-edit?))
|
|
(st/emit! (ntf/warn (tr "workspace.plugins.error.need-editor")))
|
|
(not= (:permissions new-manifest) (:permissions manifest))
|
|
(modal/show!
|
|
:plugin-permissions-update
|
|
{:plugin new-manifest
|
|
:on-accept
|
|
#(do
|
|
(preg/install-plugin! new-manifest)
|
|
(load-plugin! new-manifest))})
|
|
|
|
(not= new-manifest manifest)
|
|
(do (preg/install-plugin! new-manifest)
|
|
(load-plugin! manifest))
|
|
:else
|
|
(load-plugin! manifest))))
|
|
(fn []
|
|
;; Error fetching the manifest we'll load the plugin with the
|
|
;; old manifest
|
|
(load-plugin! manifest))))
|
|
(load-plugin! manifest)))
|
|
|
|
(defn close-plugin!
|
|
[{:keys [plugin-id]}]
|
|
(try
|
|
(.ɵunloadPlugin ^js ug/global plugin-id)
|
|
(catch :default e
|
|
(.error js/console "Error" e))))
|
|
|
|
(defn close-current-plugin
|
|
[& {:keys [close-only-edition-plugins?]}]
|
|
(ptk/reify ::close-current-plugin
|
|
ptk/EffectEvent
|
|
(effect [_ state _]
|
|
(let [ids (dm/get-in state [:workspace-local :open-plugins])]
|
|
(doseq [id ids]
|
|
(let [plugin (preg/get-plugin id)
|
|
permissions (or (dm/get-in state [:plugins-permissions-peek :data id])
|
|
(:permissions plugin))
|
|
is-edition-plugin? (or (contains? permissions "content:write")
|
|
(contains? permissions "library:write"))]
|
|
|
|
(when (or (not close-only-edition-plugins?)
|
|
is-edition-plugin?)
|
|
(close-plugin! plugin))))))))
|
|
|
|
(defn delay-open-plugin
|
|
[plugin]
|
|
(ptk/reify ::delay-open-plugin
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(assoc state ::open-plugin (:plugin-id plugin)))))
|
|
|
|
(defn check-open-plugin
|
|
[]
|
|
(ptk/reify ::check-open-plugin
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [user-can-edit? (dm/get-in state [:permissions :can-edit])]
|
|
(when-let [pid (::open-plugin state)]
|
|
(let [plugin (preg/get-plugin pid)]
|
|
(open-plugin! plugin user-can-edit?)
|
|
(rx/of (ev/event {::ev/name "start-plugin"
|
|
::ev/origin "workspace"
|
|
:name (:name plugin)
|
|
:host (:host plugin)})
|
|
#(dissoc % ::open-plugin))))))))
|
|
|
|
(defn- update-plugin-permissions-peek
|
|
[{:keys [plugin-id url]}]
|
|
(when url
|
|
;; If the saved manifest has a URL we fetch the manifest to check
|
|
;; for updates
|
|
(->> (fetch-manifest url)
|
|
(rx/subs!
|
|
(fn [new-manifest]
|
|
(let [permissions (:permissions new-manifest)]
|
|
(when permissions
|
|
(st/emit! (save-plugin-permissions-peek plugin-id permissions)))))
|
|
(fn [_err]
|
|
;; on error do nothing
|
|
)))))
|
|
|
|
(defn update-plugins-permissions-peek
|
|
[]
|
|
(ptk/reify ::update-plugins-permissions-peek
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(let [now (ct/now)
|
|
expiration (ct/in-past {:days 1})
|
|
updated-at (dm/get-in state [:plugins-permissions-peek :updated-at] 0)
|
|
expired? (> expiration updated-at)]
|
|
|
|
(if expired?
|
|
(let [plugins (preg/plugins-list)]
|
|
(doseq [plugin plugins]
|
|
(update-plugin-permissions-peek plugin))
|
|
(-> state
|
|
(assoc-in [:plugins-permissions-peek :updated-at] now)))
|
|
|
|
state)))))
|
|
|
|
(defn set-plugin-data
|
|
([file-id type namespace key value]
|
|
(set-plugin-data file-id type nil nil namespace key value))
|
|
|
|
([file-id type id namespace key value]
|
|
(set-plugin-data file-id type id nil namespace key value))
|
|
|
|
([file-id type id page-id namespace key value]
|
|
(dm/assert! (contains? #{:file :page :shape :color :typography :component} type))
|
|
(dm/assert! (or (nil? id) (uuid? id)))
|
|
(dm/assert! (or (nil? page-id) (uuid? page-id)))
|
|
(dm/assert! (uuid? file-id))
|
|
(dm/assert! (keyword? namespace))
|
|
(dm/assert! (string? key))
|
|
(dm/assert! (or (nil? value) (string? value)))
|
|
|
|
(ptk/reify ::set-file-plugin-data
|
|
ptk/WatchEvent
|
|
(watch [it state _]
|
|
(let [file-data (dm/get-in state [:files file-id :data])
|
|
changes (-> (pcb/empty-changes it)
|
|
(pcb/with-file-data file-data)
|
|
(assoc :file-id file-id)
|
|
(pcb/set-plugin-data type id page-id namespace key value))]
|
|
(rx/of (dch/commit-changes changes)))))))
|