diff --git a/common/src/app/common/types/plugins.cljc b/common/src/app/common/types/plugins.cljc
index 1be3578cc1..7fe8a4c7d4 100644
--- a/common/src/app/common/types/plugins.cljc
+++ b/common/src/app/common/types/plugins.cljc
@@ -30,6 +30,7 @@
(def schema:registry-entry
[:map
[:plugin-id :string]
+ [:version {:optional true} :int]
[:name :string]
[:description {:optional true} :string]
[:host :string]
diff --git a/frontend/src/app/main/data/plugins.cljs b/frontend/src/app/main/data/plugins.cljs
index 77f5d55013..6635fc070b 100644
--- a/frontend/src/app/main/data/plugins.cljs
+++ b/frontend/src/app/main/data/plugins.cljs
@@ -82,7 +82,7 @@
(defn- load-plugin!
[{:keys [plugin-id name version description host code icon permissions]}]
- (st/emit! (pflag/clear plugin-id)
+ (st/emit! (pflag/initialize plugin-id version)
(save-current-plugin plugin-id))
(let [load-plugin (unchecked-get ug/global "ɵloadPlugin")]
diff --git a/frontend/src/app/plugins/flags.cljs b/frontend/src/app/plugins/flags.cljs
index ef0ca57ab6..b804df4d9c 100644
--- a/frontend/src/app/plugins/flags.cljs
+++ b/frontend/src/app/plugins/flags.cljs
@@ -6,17 +6,24 @@
(ns app.plugins.flags
(:require
+ [app.common.data :as d]
[app.main.store :as st]
[app.plugins.utils :as u]
[app.util.object :as obj]
[potok.v2.core :as ptk]))
-(defn clear
- [id]
- (ptk/reify ::reset
+(defn initialize
+ "Initialize flags values for plugins"
+ [id version]
+ (ptk/reify ::initialize
ptk/UpdateEvent
(update [_ state]
- (update-in state [:plugins :flags] assoc id {}))))
+ (let [version (d/nilv version 1)]
+ (update-in state [:plugins :flags] assoc id
+ {:natural-child-ordering false
+ ;; For version >= 2 harden the contract by throwing errors
+ ;; on validation failures
+ :throw-validation-errors (>= version 2)})))))
(defn- set-flag
[id key value]
diff --git a/frontend/src/app/plugins/utils.cljs b/frontend/src/app/plugins/utils.cljs
index e08e792ada..2e0db5d324 100644
--- a/frontend/src/app/plugins/utils.cljs
+++ b/frontend/src/app/plugins/utils.cljs
@@ -323,14 +323,27 @@
message to the console."
[plugin-id]
(fn [cause]
- (let [message
- (if-let [explain (-> cause ex-data ::sm/explain)]
- (do
- (js/console.error (sm/humanize-explain explain))
- (error-messages explain))
- (ex-data cause))]
- (js/console.log (.-stack cause))
- (not-valid plugin-id :error message))))
+ (let [explain (-> cause ex-data ::sm/explain)
+ throw? (throw-validation-errors? plugin-id)]
+ (cond
+ ;; If it's a clojure error we throw as a validation error
+ (and throw? explain)
+ (throw-not-valid :error (error-messages explain))
+
+ ;; Unexpected errors we just propagate them
+ throw?
+ (throw cause)
+
+ ;; If not throw is active we log the caught error
+ :else
+ (let [message
+ (if explain
+ (do
+ (js/console.error (sm/humanize-explain explain))
+ (error-messages explain))
+ (ex-data cause))]
+ (js/console.log (.-stack cause))
+ (not-valid plugin-id :error message))))))
(defn is-main-component-proxy?
[p]
diff --git a/frontend/test/frontend_tests/plugins/context_shapes_test.cljs b/frontend/test/frontend_tests/plugins/context_shapes_test.cljs
index dd1a7e3634..0bba39cd40 100644
--- a/frontend/test/frontend_tests/plugins/context_shapes_test.cljs
+++ b/frontend/test/frontend_tests/plugins/context_shapes_test.cljs
@@ -14,7 +14,8 @@
[app.util.object :as obj]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.state :as ths]
- [frontend-tests.helpers.wasm :as thw]))
+ [frontend-tests.helpers.wasm :as thw]
+ [potok.v2.core :as ptk]))
(t/deftest test-common-shape-properties
(thw/with-wasm-mocks*
@@ -25,6 +26,7 @@
^js context (api/create-context "00000000-0000-0000-0000-000000000000")
_ (set! st/state store)
+ _ (ptk/emit! store #(assoc-in % [:plugins :flags "00000000-0000-0000-0000-000000000000" :throw-validation-errors] true))
^js file (. context -currentFile)
^js page (. context -currentPage)
@@ -65,7 +67,7 @@
(t/is (= (.-x shape) 10))
(t/is (= (get-in @store (get-shape-path :x)) 10))
- (set! (.-x shape) "fail")
+ (t/is (thrown? js/Error (set! (.-x shape) "fail")))
(t/is (= (.-x shape) 10))
(t/is (= (get-in @store (get-shape-path :x)) 10)))
@@ -74,7 +76,7 @@
(t/is (= (.-y shape) 50))
(t/is (= (get-in @store (get-shape-path :y)) 50))
- (set! (.-y shape) "fail")
+ (t/is (thrown? js/Error (set! (.-y shape) "fail")))
(t/is (= (.-y shape) 50))
(t/is (= (get-in @store (get-shape-path :y)) 50)))
@@ -85,7 +87,7 @@
(t/is (= (get-in @store (get-shape-path :width)) 250))
(t/is (= (get-in @store (get-shape-path :height)) 300))
- (.resize shape 0 0)
+ (t/is (thrown? js/Error (.resize shape 0 0)))
(t/is (= (.-width shape) 250))
(t/is (= (.-height shape) 300))
(t/is (= (get-in @store (get-shape-path :width)) 250))
@@ -115,7 +117,7 @@
(t/is (= (get-in @store (get-shape-path :proportion-lock)) true)))
(t/testing " - constraintsHorizontal"
- (set! (.-constraintsHorizontal shape) "fail")
+ (t/is (thrown? js/Error (set! (.-constraintsHorizontal shape) "fail")))
(t/is (not= (.-constraintsHorizontal shape) "fail"))
(t/is (not= (get-in @store (get-shape-path :constraints-h)) "fail"))
@@ -124,7 +126,7 @@
(t/is (= (get-in @store (get-shape-path :constraints-h)) :right)))
(t/testing " - constraintsVertical"
- (set! (.-constraintsVertical shape) "fail")
+ (t/is (thrown? js/Error (set! (.-constraintsVertical shape) "fail")))
(t/is (not= (.-constraintsVertical shape) "fail"))
(t/is (not= (get-in @store (get-shape-path :constraints-v)) "fail"))
@@ -175,7 +177,7 @@
(t/is (= (.-blendMode shape) "multiply"))
(t/is (= (get-in @store (get-shape-path :blend-mode)) :multiply))
- (set! (.-blendMode shape) "fail")
+ (t/is (thrown? js/Error (set! (.-blendMode shape) "fail")))
(t/is (= (.-blendMode shape) "multiply"))
(t/is (= (get-in @store (get-shape-path :blend-mode)) :multiply)))
@@ -194,7 +196,7 @@
:color {:color "#fabada" :opacity 1}
:hidden false}]))))
(let [shadow #js {:style "fail"}]
- (set! (.-shadows shape) #js [shadow])
+ (t/is (thrown? js/Error (set! (.-shadows shape) #js [shadow])))
(t/is (= (-> (. shape -shadows) (aget 0) (aget "style")) "drop-shadow"))))
(t/testing " - blur"
@@ -211,7 +213,7 @@
(t/is (= (-> (. shape -exports) (aget 0) (aget "suffix")) "test"))
(t/is (= (get-in @store (get-shape-path :exports)) [{:type :pdf :scale 2 :suffix "test" :skip-children false}]))
- (set! (.-exports shape) #js [#js {:type 10 :scale 2 :suffix "test"}])
+ (t/is (thrown? js/Error (set! (.-exports shape) #js [#js {:type 10 :scale 2 :suffix "test"}])))
(t/is (= (get-in @store (get-shape-path :exports)) [{:type :pdf :scale 2 :suffix "test" :skip-children false}])))
(t/testing " - flipX"
@@ -234,7 +236,7 @@
(t/is (= (get-in @store (get-shape-path :rotation)) 0)))
(t/testing " - fills"
- (set! (.-fills shape) #js [#js {:fillColor 100}])
+ (t/is (thrown? js/Error (set! (.-fills shape) #js [#js {:fillColor 100}])))
(t/is (= (get-in @store (get-shape-path :fills)) [{:fill-color "#B1B2B5" :fill-opacity 1}]))
(t/is (= (-> (. shape -fills) (aget 0) (aget "fillColor")) "#B1B2B5"))
diff --git a/frontend/test/frontend_tests/plugins/page_test.cljs b/frontend/test/frontend_tests/plugins/page_test.cljs
index bcc59c4ce8..d29149e846 100644
--- a/frontend/test/frontend_tests/plugins/page_test.cljs
+++ b/frontend/test/frontend_tests/plugins/page_test.cljs
@@ -32,6 +32,7 @@
store (ths/setup-store file)
_ (set! st/state store)
_ (set! st/stream (ptk/input-stream store))
+ _ (ptk/emit! store #(assoc-in % [:plugins :flags "00000000-0000-0000-0000-000000000000" :throw-validation-errors] true))
context (api/create-context "00000000-0000-0000-0000-000000000000")]
{:file file :store store :context context}))
@@ -89,9 +90,11 @@
^js page2 (aget pages 1)]
(t/is (instance? js/Promise (.openPage context page2 true)))))
-(t/deftest test-open-page-invalid-arg-returns-nil
+(t/deftest test-open-page-invalid-arg-throws
+ ;; With throwValidationErrors enabled an invalid argument surfaces as an
+ ;; exception instead of being silently logged.
(let [^js context (:context (setup))]
- (t/is (nil? (.openPage context "not-a-page")))))
+ (t/is (thrown? js/Error (.openPage context "not-a-page")))))
(t/deftest test-open-page-resolves-when-page-changes
(t/async done
diff --git a/plugins/apps/poc-state-plugin/src/app/app.component.ts b/plugins/apps/poc-state-plugin/src/app/app.component.ts
index 0eae67b448..7a293a1210 100644
--- a/plugins/apps/poc-state-plugin/src/app/app.component.ts
+++ b/plugins/apps/poc-state-plugin/src/app/app.component.ts
@@ -45,6 +45,13 @@ import type { Shape } from '@penpot/plugin-types';
+